@hugebug4ever/react-native-unity 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -0
- package/android/build.gradle +38 -0
- package/android/consumer-rules.pro +3 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/java/com/hugebug4ever/rnunity/RNUnityBridge.kt +5 -0
- package/android/src/main/java/com/hugebug4ever/rnunity/RNUnityHostView.kt +95 -0
- package/android/src/main/java/com/hugebug4ever/rnunity/RNUnityPackage.kt +31 -0
- package/android/src/main/java/com/hugebug4ever/rnunity/RNUnityViewManager.kt +30 -0
- package/android/src/main/java/com/hugebug4ever/rnunity/UnityPlayerAdapter.kt +58 -0
- package/android/src/main/java/com/hugebug4ever/rnunity/UnityRuntimeController.kt +329 -0
- package/android/src/main/java/com/hugebug4ever/rnunity/UnityRuntimeModule.kt +46 -0
- package/android/src/main/java/com/hugebug4ever/rnunity/UnityRuntimeState.kt +16 -0
- package/app.plugin.js +2 -0
- package/cli/index.js +140 -0
- package/ios/RNUnityNativeBridge.mm +6 -0
- package/ios/RNUnityRuntimeController.h +29 -0
- package/ios/RNUnityRuntimeController.mm +316 -0
- package/ios/RNUnityView.h +17 -0
- package/ios/RNUnityView.mm +105 -0
- package/ios/UnityRuntime.mm +49 -0
- package/lib/commonjs/UnityRuntime.js +53 -0
- package/lib/commonjs/UnityRuntime.js.map +1 -0
- package/lib/commonjs/UnityView.js +35 -0
- package/lib/commonjs/UnityView.js.map +1 -0
- package/lib/commonjs/index.js +20 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/specs/NativeUnityRuntime.js +9 -0
- package/lib/commonjs/specs/NativeUnityRuntime.js.map +1 -0
- package/lib/commonjs/specs/NativeUnityView.js +10 -0
- package/lib/commonjs/specs/NativeUnityView.js.map +1 -0
- package/lib/commonjs/types.js +6 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/typescript/UnityRuntime.d.ts +12 -0
- package/lib/typescript/UnityRuntime.d.ts.map +1 -0
- package/lib/typescript/UnityView.d.ts +3 -0
- package/lib/typescript/UnityView.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +4 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/specs/NativeUnityRuntime.d.ts +15 -0
- package/lib/typescript/specs/NativeUnityRuntime.d.ts.map +1 -0
- package/lib/typescript/specs/NativeUnityView.d.ts +32 -0
- package/lib/typescript/specs/NativeUnityView.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +63 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/package/UnityRuntime.ts +64 -0
- package/package/UnityView.tsx +32 -0
- package/package/index.ts +20 -0
- package/package/specs/NativeUnityRuntime.ts +17 -0
- package/package/specs/NativeUnityView.ts +27 -0
- package/package/types.ts +85 -0
- package/package.json +90 -0
- package/plugin/build/index.js +132 -0
- package/plugin/ios/configure_unity.rb +108 -0
- package/plugin/src/index.ts +175 -0
- package/plugin/tsconfig.json +13 -0
- package/react-native-unity.podspec +34 -0
- package/react-native.config.js +9 -0
- package/unity-package/Editor/Exporter.cs +91 -0
- package/unity-package/Editor/Exporter.cs.meta +2 -0
- package/unity-package/Editor/ReactNativeUnity.Editor.asmdef +8 -0
- package/unity-package/Editor/ReactNativeUnity.Editor.asmdef.meta +7 -0
- package/unity-package/Editor.meta +8 -0
- package/unity-package/Runtime/ReactNativeBridge.cs +48 -0
- package/unity-package/Runtime/ReactNativeBridge.cs.meta +2 -0
- package/unity-package/Runtime/ReactNativeUnity.Runtime.asmdef +6 -0
- package/unity-package/Runtime/ReactNativeUnity.Runtime.asmdef.meta +7 -0
- package/unity-package/Runtime.meta +8 -0
- package/unity-package/package.json +11 -0
- package/unity-package/package.json.meta +7 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package com.hugebug4ever.rnunity
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import com.facebook.react.bridge.ActivityEventListener
|
|
6
|
+
import com.facebook.react.bridge.LifecycleEventListener
|
|
7
|
+
import com.facebook.react.bridge.Promise
|
|
8
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
9
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
10
|
+
import com.facebook.react.bridge.ReactMethod
|
|
11
|
+
import com.facebook.react.turbomodule.core.interfaces.TurboModule
|
|
12
|
+
|
|
13
|
+
internal class UnityRuntimeModule(private val context: ReactApplicationContext) :
|
|
14
|
+
ReactContextBaseJavaModule(context), TurboModule, LifecycleEventListener, ActivityEventListener {
|
|
15
|
+
|
|
16
|
+
init {
|
|
17
|
+
context.addLifecycleEventListener(this)
|
|
18
|
+
context.addActivityEventListener(this)
|
|
19
|
+
UnityRuntimeController.bind(context, context.currentActivity)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
override fun getName() = NAME
|
|
23
|
+
|
|
24
|
+
@ReactMethod fun start(detachedBehavior: String, promise: Promise) = UnityRuntimeController.start(detachedBehavior, promise)
|
|
25
|
+
@ReactMethod fun pause(promise: Promise) = UnityRuntimeController.pause(promise)
|
|
26
|
+
@ReactMethod fun resume(promise: Promise) = UnityRuntimeController.resume(promise)
|
|
27
|
+
@ReactMethod fun unload(promise: Promise) = UnityRuntimeController.unload(promise)
|
|
28
|
+
@ReactMethod fun getState(promise: Promise) = promise.resolve(UnityRuntimeController.stateName())
|
|
29
|
+
@ReactMethod fun getDiagnostics(promise: Promise) = promise.resolve(UnityRuntimeController.diagnostics())
|
|
30
|
+
@ReactMethod fun sendMessage(gameObject: String, methodName: String, message: String, promise: Promise) =
|
|
31
|
+
UnityRuntimeController.sendMessage(gameObject, methodName, message, promise)
|
|
32
|
+
@ReactMethod fun addListener(eventName: String) = Unit
|
|
33
|
+
@ReactMethod fun removeListeners(count: Double) = Unit
|
|
34
|
+
|
|
35
|
+
override fun onHostResume() {
|
|
36
|
+
UnityRuntimeController.updateActivity(context.currentActivity)
|
|
37
|
+
UnityRuntimeController.setAppBackground(false)
|
|
38
|
+
}
|
|
39
|
+
override fun onHostPause() = UnityRuntimeController.setAppBackground(true)
|
|
40
|
+
override fun onHostDestroy() = Unit
|
|
41
|
+
override fun onNewIntent(intent: Intent) = Unit
|
|
42
|
+
override fun onActivityResult(activity: Activity, requestCode: Int, resultCode: Int, data: Intent?) = Unit
|
|
43
|
+
|
|
44
|
+
companion object { const val NAME = "UnityRuntime" }
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
package com.hugebug4ever.rnunity
|
|
2
|
+
|
|
3
|
+
internal enum class UnityRuntimeState(val wireName: String) {
|
|
4
|
+
IDLE("idle"),
|
|
5
|
+
STARTING("starting"),
|
|
6
|
+
RUNNING_DETACHED("runningDetached"),
|
|
7
|
+
RUNNING_ATTACHED("runningAttached"),
|
|
8
|
+
PAUSED_DETACHED("pausedDetached"),
|
|
9
|
+
PAUSED_ATTACHED("pausedAttached"),
|
|
10
|
+
UNLOADING("unloading"),
|
|
11
|
+
QUITTED("quitted"),
|
|
12
|
+
FAILED("failed")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
internal enum class PauseReason { USER, APP_BACKGROUND, NO_HOST }
|
|
16
|
+
|
package/app.plugin.js
ADDED
package/cli/index.js
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { spawnSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const VERSION_PATTERN = /^6000\.3\./;
|
|
9
|
+
|
|
10
|
+
function fail(message) {
|
|
11
|
+
console.error(`[rn-unity] ${message}`);
|
|
12
|
+
process.exitCode = 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function parseArgs(values) {
|
|
16
|
+
const result = { _: [] };
|
|
17
|
+
for (let i = 0; i < values.length; i += 1) {
|
|
18
|
+
const value = values[i];
|
|
19
|
+
if (value.startsWith('--')) result[value.slice(2)] = values[++i] ?? true;
|
|
20
|
+
else result._.push(value);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function appRoot(args) {
|
|
26
|
+
return path.resolve(String(args.project || process.cwd()));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function readConfig(root) {
|
|
30
|
+
const packagePath = path.join(root, 'package.json');
|
|
31
|
+
const packageJson = fs.existsSync(packagePath)
|
|
32
|
+
? JSON.parse(fs.readFileSync(packagePath, 'utf8'))
|
|
33
|
+
: {};
|
|
34
|
+
let expo = packageJson.expo || {};
|
|
35
|
+
const appConfigPath = path.join(root, 'app.config.js');
|
|
36
|
+
if (fs.existsSync(appConfigPath)) {
|
|
37
|
+
try {
|
|
38
|
+
const loaded = require(appConfigPath);
|
|
39
|
+
expo = loaded.expo || loaded.default?.expo || loaded.default || loaded;
|
|
40
|
+
} catch {
|
|
41
|
+
// Doctor continues with package.json and filesystem checks.
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return {
|
|
45
|
+
packageJson,
|
|
46
|
+
expo,
|
|
47
|
+
androidExportPath: path.resolve(root, packageJson.reactNativeUnity?.androidExportPath || 'unity/builds/android/unityLibrary'),
|
|
48
|
+
iosExportPath: path.resolve(root, packageJson.reactNativeUnity?.iosExportPath || 'unity/builds/ios'),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function installedEditors() {
|
|
53
|
+
const result = spawnSync('unity', ['editors', '--installed', '--format', 'json'], {
|
|
54
|
+
encoding: 'utf8',
|
|
55
|
+
shell: process.platform === 'win32',
|
|
56
|
+
});
|
|
57
|
+
if (result.status !== 0) return [];
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(result.stdout).data || [];
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function doctor(args) {
|
|
66
|
+
const root = appRoot(args);
|
|
67
|
+
const config = readConfig(root);
|
|
68
|
+
const requestedPlatform = args.platform;
|
|
69
|
+
if (requestedPlatform && !['android', 'ios', 'all'].includes(requestedPlatform)) {
|
|
70
|
+
return fail('--platform must be android, ios, or all');
|
|
71
|
+
}
|
|
72
|
+
const platforms = requestedPlatform === 'all' || (!requestedPlatform && process.platform === 'darwin')
|
|
73
|
+
? new Set(['android', 'ios'])
|
|
74
|
+
: new Set([requestedPlatform || 'android']);
|
|
75
|
+
const checks = [];
|
|
76
|
+
const check = (name, ok, detail, required = true) => checks.push({ name, ok, detail, required });
|
|
77
|
+
const deps = { ...config.packageJson.dependencies, ...config.packageJson.devDependencies };
|
|
78
|
+
check('React Native package', Boolean(deps['react-native']), deps['react-native'] || 'not found');
|
|
79
|
+
check('New Architecture', config.expo.newArchEnabled === true, `newArchEnabled=${String(config.expo.newArchEnabled)}`);
|
|
80
|
+
const editors = installedEditors();
|
|
81
|
+
const editor = editors.find((item) => VERSION_PATTERN.test(item.version));
|
|
82
|
+
check('Unity 6000.3 editor', Boolean(editor), editor?.version || 'not installed');
|
|
83
|
+
if (platforms.has('android')) {
|
|
84
|
+
check('Android unityLibrary', fs.existsSync(path.join(config.androidExportPath, 'build.gradle')), config.androidExportPath);
|
|
85
|
+
check('Android manifest', fs.existsSync(path.join(config.androidExportPath, 'src/main/AndroidManifest.xml')), 'src/main/AndroidManifest.xml');
|
|
86
|
+
const unityPropertiesPath = path.join(config.androidExportPath, '..', 'gradle.properties');
|
|
87
|
+
const unityProperties = fs.existsSync(unityPropertiesPath) ? fs.readFileSync(unityPropertiesPath, 'utf8') : '';
|
|
88
|
+
const abiLine = unityProperties.match(/^unity\.abiFilters=(.+)$/m)?.[1] || 'not found';
|
|
89
|
+
check('Android x86_64 ABI', abiLine.split(',').includes('x86_64'), abiLine);
|
|
90
|
+
const settings = path.join(root, 'android', 'settings.gradle');
|
|
91
|
+
const appGradle = path.join(root, 'android', 'app', 'build.gradle');
|
|
92
|
+
check('Android Gradle integration',
|
|
93
|
+
fs.existsSync(settings) && fs.readFileSync(settings, 'utf8').includes("include ':unityLibrary'") &&
|
|
94
|
+
fs.existsSync(appGradle) && fs.readFileSync(appGradle, 'utf8').includes("implementation project(':unityLibrary')"),
|
|
95
|
+
'settings.gradle and app/build.gradle');
|
|
96
|
+
} else {
|
|
97
|
+
check('Android checks', true, 'not requested', false);
|
|
98
|
+
}
|
|
99
|
+
if (platforms.has('ios')) {
|
|
100
|
+
check('iOS Unity project', fs.existsSync(path.join(config.iosExportPath, 'Unity-iPhone.xcodeproj')), config.iosExportPath);
|
|
101
|
+
const podfile = path.join(root, 'ios', 'Podfile');
|
|
102
|
+
check('iOS Podfile integration', fs.existsSync(podfile) && fs.readFileSync(podfile, 'utf8').includes('ReactNativeUnity.configure_unity_project'), podfile);
|
|
103
|
+
} else {
|
|
104
|
+
check('iOS checks', true, 'not requested on this host', false);
|
|
105
|
+
}
|
|
106
|
+
const conflicts = ['@azesmway/react-native-unity', 'react-native-unity-view'].filter((name) => deps[name]);
|
|
107
|
+
check('Conflicting packages', conflicts.length === 0, conflicts.join(', ') || 'none');
|
|
108
|
+
for (const item of checks) console.log(`${item.required ? (item.ok ? 'PASS' : 'FAIL') : 'SKIP'} ${item.name}: ${item.detail}`);
|
|
109
|
+
if (checks.some((item) => item.required && !item.ok)) process.exitCode = 1;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function exportUnity(args) {
|
|
113
|
+
const platform = args.platform;
|
|
114
|
+
if (!['android', 'ios'].includes(platform)) return fail('--platform must be android or ios');
|
|
115
|
+
if (!args['unity-project']) return fail('--unity-project is required');
|
|
116
|
+
if (!args.output) return fail('--output is required');
|
|
117
|
+
const editor = installedEditors().find((item) => VERSION_PATTERN.test(item.version) && item.location);
|
|
118
|
+
if (!editor) return fail('Unity 6000.3 editor not found');
|
|
119
|
+
const projectPath = path.resolve(String(args['unity-project']));
|
|
120
|
+
const outputPath = path.resolve(String(args.output));
|
|
121
|
+
const result = spawnSync(editor.location, [
|
|
122
|
+
'-batchmode', '-quit', '-nographics',
|
|
123
|
+
'-projectPath', projectPath,
|
|
124
|
+
'-executeMethod', 'ReactNativeUnity.Editor.Exporter.ExportFromCommandLine',
|
|
125
|
+
'-rnUnityPlatform', platform,
|
|
126
|
+
'-rnUnityOutput', outputPath,
|
|
127
|
+
'-logFile', '-',
|
|
128
|
+
], { stdio: 'inherit' });
|
|
129
|
+
if (result.error) return fail(result.error.message);
|
|
130
|
+
if (result.status !== 0) return fail(`Unity export failed with exit code ${result.status}`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const args = parseArgs(process.argv.slice(2));
|
|
134
|
+
switch (args._[0]) {
|
|
135
|
+
case 'doctor': doctor(args); break;
|
|
136
|
+
case 'export': exportUnity(args); break;
|
|
137
|
+
default:
|
|
138
|
+
console.log('Usage: rn-unity doctor [--project path] [--platform android|ios|all]\n rn-unity export --platform android|ios --unity-project path --output path');
|
|
139
|
+
if (args._[0]) process.exitCode = 1;
|
|
140
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
#import <UIKit/UIKit.h>
|
|
3
|
+
#import <React/RCTBridgeModule.h>
|
|
4
|
+
|
|
5
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
6
|
+
|
|
7
|
+
FOUNDATION_EXPORT NSNotificationName const RNUnityStateChangeNotification;
|
|
8
|
+
FOUNDATION_EXPORT NSNotificationName const RNUnityMessageNotification;
|
|
9
|
+
FOUNDATION_EXPORT NSNotificationName const RNUnityErrorNotification;
|
|
10
|
+
|
|
11
|
+
@class RNUnityView;
|
|
12
|
+
|
|
13
|
+
@interface RNUnityRuntimeController : NSObject
|
|
14
|
+
+ (instancetype)shared;
|
|
15
|
+
- (void)start:(NSString *)detachedBehavior resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
|
|
16
|
+
- (void)pause:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
|
|
17
|
+
- (void)resume:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
|
|
18
|
+
- (void)unload:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
|
|
19
|
+
- (void)sendMessage:(NSString *)gameObject methodName:(NSString *)methodName message:(NSString *)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject;
|
|
20
|
+
- (void)attachHost:(RNUnityView *)host;
|
|
21
|
+
- (void)detachHost:(NSString *)hostId;
|
|
22
|
+
- (void)setApplicationBackgrounded:(BOOL)backgrounded;
|
|
23
|
+
- (NSString *)stateName;
|
|
24
|
+
- (NSString *)diagnosticsJSON;
|
|
25
|
+
- (void)receiveMessage:(NSString *)message;
|
|
26
|
+
@end
|
|
27
|
+
|
|
28
|
+
NS_ASSUME_NONNULL_END
|
|
29
|
+
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#import "RNUnityRuntimeController.h"
|
|
2
|
+
#import "RNUnityView.h"
|
|
3
|
+
#import <UnityFramework/UnityFramework.h>
|
|
4
|
+
#import <mach-o/ldsyms.h>
|
|
5
|
+
|
|
6
|
+
NSNotificationName const RNUnityStateChangeNotification = @"RNUnity:stateChange";
|
|
7
|
+
NSNotificationName const RNUnityMessageNotification = @"RNUnity:message";
|
|
8
|
+
NSNotificationName const RNUnityErrorNotification = @"RNUnity:error";
|
|
9
|
+
|
|
10
|
+
typedef NS_ENUM(NSInteger, RNUnityState) {
|
|
11
|
+
RNUnityStateIdle,
|
|
12
|
+
RNUnityStateStarting,
|
|
13
|
+
RNUnityStateRunningDetached,
|
|
14
|
+
RNUnityStateRunningAttached,
|
|
15
|
+
RNUnityStatePausedDetached,
|
|
16
|
+
RNUnityStatePausedAttached,
|
|
17
|
+
RNUnityStateUnloading,
|
|
18
|
+
RNUnityStateQuitted,
|
|
19
|
+
RNUnityStateFailed,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
@interface RNUnityRuntimeController () <UnityFrameworkListener>
|
|
23
|
+
@property(nonatomic) RNUnityState state;
|
|
24
|
+
@property(nonatomic) NSInteger generation;
|
|
25
|
+
@property(nonatomic) NSTimeInterval lastTransitionAt;
|
|
26
|
+
@property(nonatomic, strong, nullable) UnityFramework *framework;
|
|
27
|
+
@property(nonatomic, weak, nullable) RNUnityView *host;
|
|
28
|
+
@property(nonatomic, weak, nullable) RNUnityView *pendingHost;
|
|
29
|
+
@property(nonatomic, strong) NSMutableSet<NSString *> *pauseReasons;
|
|
30
|
+
@property(nonatomic, strong) NSMutableArray<RCTPromiseResolveBlock> *startResolves;
|
|
31
|
+
@property(nonatomic, strong) NSMutableArray<RCTPromiseRejectBlock> *startRejects;
|
|
32
|
+
@property(nonatomic, copy, nullable) RCTPromiseResolveBlock unloadResolve;
|
|
33
|
+
@property(nonatomic, copy, nullable) RCTPromiseRejectBlock unloadReject;
|
|
34
|
+
@property(nonatomic, copy, nullable) NSString *pendingStartBehavior;
|
|
35
|
+
@property(nonatomic, copy, nullable) RCTPromiseResolveBlock pendingStartResolve;
|
|
36
|
+
@property(nonatomic, copy, nullable) RCTPromiseRejectBlock pendingStartReject;
|
|
37
|
+
@property(nonatomic, copy) NSString *defaultDetachedBehavior;
|
|
38
|
+
@property(nonatomic, strong, nullable) NSDictionary *lastError;
|
|
39
|
+
@property(nonatomic, strong, nullable) dispatch_block_t unloadTimeout;
|
|
40
|
+
@end
|
|
41
|
+
|
|
42
|
+
@implementation RNUnityRuntimeController
|
|
43
|
+
|
|
44
|
+
+ (instancetype)shared {
|
|
45
|
+
static RNUnityRuntimeController *controller;
|
|
46
|
+
static dispatch_once_t onceToken;
|
|
47
|
+
dispatch_once(&onceToken, ^{ controller = [RNUnityRuntimeController new]; });
|
|
48
|
+
return controller;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
- (instancetype)init {
|
|
52
|
+
if ((self = [super init])) {
|
|
53
|
+
_state = RNUnityStateIdle;
|
|
54
|
+
_pauseReasons = [NSMutableSet set];
|
|
55
|
+
_startResolves = [NSMutableArray array];
|
|
56
|
+
_startRejects = [NSMutableArray array];
|
|
57
|
+
_defaultDetachedBehavior = @"pause";
|
|
58
|
+
_lastTransitionAt = NSDate.date.timeIntervalSince1970 * 1000;
|
|
59
|
+
}
|
|
60
|
+
return self;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
- (void)onMain:(dispatch_block_t)block {
|
|
64
|
+
if (NSThread.isMainThread) block(); else dispatch_async(dispatch_get_main_queue(), block);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
- (NSString *)stateName {
|
|
68
|
+
switch (_state) {
|
|
69
|
+
case RNUnityStateIdle: return @"idle";
|
|
70
|
+
case RNUnityStateStarting: return @"starting";
|
|
71
|
+
case RNUnityStateRunningDetached: return @"runningDetached";
|
|
72
|
+
case RNUnityStateRunningAttached: return @"runningAttached";
|
|
73
|
+
case RNUnityStatePausedDetached: return @"pausedDetached";
|
|
74
|
+
case RNUnityStatePausedAttached: return @"pausedAttached";
|
|
75
|
+
case RNUnityStateUnloading: return @"unloading";
|
|
76
|
+
case RNUnityStateQuitted: return @"quitted";
|
|
77
|
+
case RNUnityStateFailed: return @"failed";
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
- (BOOL)isRunningOrPaused {
|
|
82
|
+
return _state >= RNUnityStateRunningDetached && _state <= RNUnityStatePausedAttached;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
- (void)transition:(RNUnityState)next {
|
|
86
|
+
if (_state == next) return;
|
|
87
|
+
_state = next;
|
|
88
|
+
_lastTransitionAt = NSDate.date.timeIntervalSince1970 * 1000;
|
|
89
|
+
NSDictionary *event = @{ @"state": self.stateName, @"generation": @(_generation) };
|
|
90
|
+
[NSNotificationCenter.defaultCenter postNotificationName:RNUnityStateChangeNotification object:self userInfo:event];
|
|
91
|
+
[_host emitState:self.stateName generation:_generation];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
- (NSDictionary *)error:(NSString *)code message:(NSString *)message cause:(nullable NSString *)cause {
|
|
95
|
+
return @{
|
|
96
|
+
@"code": code, @"message": message, @"state": self.stateName,
|
|
97
|
+
@"generation": @(_generation), @"nativeCause": cause ?: @"",
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
- (void)reject:(RCTPromiseRejectBlock)reject code:(NSString *)code message:(NSString *)message {
|
|
102
|
+
reject(code, message, nil);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
- (nullable UnityFramework *)loadFramework:(NSError **)error {
|
|
106
|
+
UnityFramework *framework = [UnityFramework getInstance];
|
|
107
|
+
if (framework) return framework;
|
|
108
|
+
NSString *path = [NSBundle.mainBundle pathForResource:@"UnityFramework" ofType:@"framework" inDirectory:@"Frameworks"];
|
|
109
|
+
NSBundle *bundle = path ? [NSBundle bundleWithPath:path] : nil;
|
|
110
|
+
if (!bundle || ![bundle loadAndReturnError:error]) return nil;
|
|
111
|
+
return [UnityFramework getInstance];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
- (void)start:(NSString *)detachedBehavior resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
115
|
+
[self onMain:^{
|
|
116
|
+
self.defaultDetachedBehavior = [detachedBehavior isEqualToString:@"keepRunning"] ? @"keepRunning" : @"pause";
|
|
117
|
+
if (self.state == RNUnityStateStarting) {
|
|
118
|
+
[self.startResolves addObject:resolve]; [self.startRejects addObject:reject]; return;
|
|
119
|
+
}
|
|
120
|
+
if ([self isRunningOrPaused]) { resolve(nil); return; }
|
|
121
|
+
if (self.state == RNUnityStateUnloading) {
|
|
122
|
+
if (self.pendingStartReject) self.pendingStartReject(@"E_UNLOADING", @"A newer start request replaced this queued request", nil);
|
|
123
|
+
self.pendingStartBehavior = self.defaultDetachedBehavior;
|
|
124
|
+
self.pendingStartResolve = resolve; self.pendingStartReject = reject; return;
|
|
125
|
+
}
|
|
126
|
+
if (self.state == RNUnityStateQuitted) { [self reject:reject code:@"E_PROCESS_RESTART_REQUIRED" message:@"Unity has quit in this process"]; return; }
|
|
127
|
+
if (self.state == RNUnityStateFailed) { [self reject:reject code:@"E_NATIVE_INVARIANT" message:@"Unity runtime is in a failed state"]; return; }
|
|
128
|
+
[self startNewGeneration:resolve reject:reject];
|
|
129
|
+
}];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
- (void)startNewGeneration:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
133
|
+
_generation += 1;
|
|
134
|
+
[self transition:RNUnityStateStarting];
|
|
135
|
+
[_startResolves addObject:resolve]; [_startRejects addObject:reject];
|
|
136
|
+
NSError *error = nil;
|
|
137
|
+
UnityFramework *framework = [self loadFramework:&error];
|
|
138
|
+
if (!framework) {
|
|
139
|
+
[self fail:@"E_RUNTIME_START_FAILED" message:@"UnityFramework could not be loaded" cause:error.localizedDescription];
|
|
140
|
+
for (RCTPromiseRejectBlock item in _startRejects) item(@"E_RUNTIME_START_FAILED", error.localizedDescription, error);
|
|
141
|
+
[_startResolves removeAllObjects]; [_startRejects removeAllObjects]; return;
|
|
142
|
+
}
|
|
143
|
+
_framework = framework;
|
|
144
|
+
[framework setExecuteHeader:&_mh_execute_header];
|
|
145
|
+
[framework setDataBundleId:"com.unity3d.framework"];
|
|
146
|
+
[framework registerFrameworkListener:self];
|
|
147
|
+
char *argv[] = { (char *)"rn-unity" };
|
|
148
|
+
[framework runEmbeddedWithArgc:1 argv:argv appLaunchOpts:@{}];
|
|
149
|
+
[self transition:RNUnityStateRunningDetached];
|
|
150
|
+
if ([_defaultDetachedBehavior isEqualToString:@"pause"] && !_pendingHost) [self addPauseReason:@"NO_HOST"];
|
|
151
|
+
if (_pendingHost) [self attachNow:_pendingHost];
|
|
152
|
+
for (RCTPromiseResolveBlock item in _startResolves) item(nil);
|
|
153
|
+
[_startResolves removeAllObjects]; [_startRejects removeAllObjects];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
- (void)attachHost:(RNUnityView *)host {
|
|
157
|
+
[self onMain:^{
|
|
158
|
+
if (!host.window || host.bounds.size.width <= 0 || host.bounds.size.height <= 0) return;
|
|
159
|
+
if (self.host && ![self.host.hostId isEqualToString:host.hostId]) {
|
|
160
|
+
[host emitError:[self error:@"E_HOST_IN_USE" message:@"Another UnityView is already attached" cause:nil]]; return;
|
|
161
|
+
}
|
|
162
|
+
self.pendingHost = host;
|
|
163
|
+
if ([self isRunningOrPaused]) [self attachNow:host];
|
|
164
|
+
}];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
- (void)attachNow:(RNUnityView *)host {
|
|
168
|
+
UIView *root = _framework.appController.rootView;
|
|
169
|
+
if (!root) return;
|
|
170
|
+
_host = host; _pendingHost = nil;
|
|
171
|
+
[host mountUnityView:root];
|
|
172
|
+
[self removePauseReason:@"NO_HOST"];
|
|
173
|
+
[self transition:_pauseReasons.count == 0 ? RNUnityStateRunningAttached : RNUnityStatePausedAttached];
|
|
174
|
+
[host emitReadyWithState:self.stateName generation:_generation];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
- (void)detachHost:(NSString *)hostId {
|
|
178
|
+
[self onMain:^{
|
|
179
|
+
if (![self.host.hostId isEqualToString:hostId]) {
|
|
180
|
+
if ([self.pendingHost.hostId isEqualToString:hostId]) self.pendingHost = nil;
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
RNUnityView *oldHost = self.host;
|
|
184
|
+
[oldHost unmountUnityView:self.framework.appController.rootView];
|
|
185
|
+
self.host = nil;
|
|
186
|
+
if ([oldHost.detachedBehavior isEqualToString:@"pause"]) [self addPauseReason:@"NO_HOST"];
|
|
187
|
+
[self transition:self.pauseReasons.count == 0 ? RNUnityStateRunningDetached : RNUnityStatePausedDetached];
|
|
188
|
+
}];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
- (void)addPauseReason:(NSString *)reason {
|
|
192
|
+
BOOL wasRunning = _pauseReasons.count == 0;
|
|
193
|
+
[_pauseReasons addObject:reason];
|
|
194
|
+
if (wasRunning) [_framework pause:YES];
|
|
195
|
+
[self syncPauseState];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
- (void)removePauseReason:(NSString *)reason {
|
|
199
|
+
if (![_pauseReasons containsObject:reason]) return;
|
|
200
|
+
[_pauseReasons removeObject:reason];
|
|
201
|
+
if (_pauseReasons.count == 0) [_framework pause:NO];
|
|
202
|
+
[self syncPauseState];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
- (void)syncPauseState {
|
|
206
|
+
if (![self isRunningOrPaused]) return;
|
|
207
|
+
BOOL attached = _host != nil;
|
|
208
|
+
[self transition:_pauseReasons.count == 0
|
|
209
|
+
? (attached ? RNUnityStateRunningAttached : RNUnityStateRunningDetached)
|
|
210
|
+
: (attached ? RNUnityStatePausedAttached : RNUnityStatePausedDetached)];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
- (void)pause:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
214
|
+
[self onMain:^{ if (![self isRunningOrPaused]) { [self reject:reject code:@"E_RUNTIME_NOT_READY" message:@"Unity is not running"]; return; } [self addPauseReason:@"USER"]; resolve(nil); }];
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
- (void)resume:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
218
|
+
[self onMain:^{ if (![self isRunningOrPaused]) { [self reject:reject code:@"E_RUNTIME_NOT_READY" message:@"Unity is not running"]; return; } [self removePauseReason:@"USER"]; resolve(nil); }];
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
- (void)setApplicationBackgrounded:(BOOL)backgrounded {
|
|
222
|
+
[self onMain:^{ if (![self isRunningOrPaused]) return; if (backgrounded) [self addPauseReason:@"APP_BACKGROUND"]; else [self removePauseReason:@"APP_BACKGROUND"]; }];
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
- (void)unload:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
226
|
+
[self onMain:^{
|
|
227
|
+
if (self.state == RNUnityStateIdle) { resolve(nil); return; }
|
|
228
|
+
if (self.state == RNUnityStateUnloading) { [self reject:reject code:@"E_UNLOADING" message:@"Unity is already unloading"]; return; }
|
|
229
|
+
if (self.state == RNUnityStateQuitted) { [self reject:reject code:@"E_PROCESS_RESTART_REQUIRED" message:@"Unity has quit in this process"]; return; }
|
|
230
|
+
if (self.state == RNUnityStateFailed) { [self reject:reject code:@"E_NATIVE_INVARIANT" message:@"Unity runtime is in a failed state"]; return; }
|
|
231
|
+
[self.host unmountUnityView:self.framework.appController.rootView];
|
|
232
|
+
self.host = nil; self.pendingHost = nil; [self.pauseReasons removeAllObjects];
|
|
233
|
+
[self transition:RNUnityStateUnloading];
|
|
234
|
+
self.unloadResolve = resolve; self.unloadReject = reject;
|
|
235
|
+
NSInteger token = self.generation;
|
|
236
|
+
dispatch_block_t timeout = dispatch_block_create(0, ^{
|
|
237
|
+
if (self.state == RNUnityStateUnloading && self.generation == token) {
|
|
238
|
+
[self fail:@"E_UNLOAD_TIMEOUT" message:@"Unity did not report unload completion" cause:nil];
|
|
239
|
+
if (self.unloadReject) self.unloadReject(@"E_UNLOAD_TIMEOUT", @"Unity did not report unload completion", nil);
|
|
240
|
+
self.unloadResolve = nil; self.unloadReject = nil;
|
|
241
|
+
if (self.pendingStartReject) {
|
|
242
|
+
self.pendingStartReject(@"E_PROCESS_RESTART_REQUIRED", @"Unity unload timed out; restart the app process before starting Unity again", nil);
|
|
243
|
+
}
|
|
244
|
+
self.pendingStartResolve = nil; self.pendingStartReject = nil; self.pendingStartBehavior = nil;
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
self.unloadTimeout = timeout;
|
|
248
|
+
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 15 * NSEC_PER_SEC), dispatch_get_main_queue(), timeout);
|
|
249
|
+
[self.framework unloadApplication];
|
|
250
|
+
}];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
- (void)unityDidUnload:(NSNotification *)notification {
|
|
254
|
+
[self onMain:^{
|
|
255
|
+
if (self.state != RNUnityStateUnloading) return;
|
|
256
|
+
if (self.unloadTimeout) dispatch_block_cancel(self.unloadTimeout);
|
|
257
|
+
[self.framework unregisterFrameworkListener:self]; self.framework = nil;
|
|
258
|
+
[self transition:RNUnityStateIdle];
|
|
259
|
+
if (self.unloadResolve) self.unloadResolve(nil);
|
|
260
|
+
self.unloadResolve = nil; self.unloadReject = nil;
|
|
261
|
+
if (self.pendingStartResolve) {
|
|
262
|
+
RCTPromiseResolveBlock nextResolve = self.pendingStartResolve;
|
|
263
|
+
RCTPromiseRejectBlock nextReject = self.pendingStartReject;
|
|
264
|
+
self.defaultDetachedBehavior = self.pendingStartBehavior ?: @"pause";
|
|
265
|
+
self.pendingStartResolve = nil; self.pendingStartReject = nil; self.pendingStartBehavior = nil;
|
|
266
|
+
[self startNewGeneration:nextResolve reject:nextReject];
|
|
267
|
+
}
|
|
268
|
+
}];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
- (void)unityDidQuit:(NSNotification *)notification {
|
|
272
|
+
[self onMain:^{
|
|
273
|
+
[self.framework unregisterFrameworkListener:self]; self.framework = nil; [self transition:RNUnityStateQuitted];
|
|
274
|
+
if (self.unloadReject) self.unloadReject(@"E_PROCESS_RESTART_REQUIRED", @"Unity quit during unload", nil);
|
|
275
|
+
self.unloadResolve = nil; self.unloadReject = nil;
|
|
276
|
+
if (self.pendingStartReject) self.pendingStartReject(@"E_PROCESS_RESTART_REQUIRED", @"Unity has quit in this process", nil);
|
|
277
|
+
self.pendingStartResolve = nil; self.pendingStartReject = nil; self.pendingStartBehavior = nil;
|
|
278
|
+
}];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
- (void)sendMessage:(NSString *)gameObject methodName:(NSString *)methodName message:(NSString *)message resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject {
|
|
282
|
+
[self onMain:^{
|
|
283
|
+
if (![self isRunningOrPaused]) { [self reject:reject code:@"E_RUNTIME_NOT_READY" message:@"Unity is not ready for messages"]; return; }
|
|
284
|
+
[self.framework sendMessageToGOWithName:gameObject.UTF8String functionName:methodName.UTF8String message:message.UTF8String];
|
|
285
|
+
resolve(nil);
|
|
286
|
+
}];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
- (void)receiveMessage:(NSString *)message {
|
|
290
|
+
[self onMain:^{
|
|
291
|
+
NSDictionary *event = @{ @"generation": @(self.generation), @"message": message };
|
|
292
|
+
[NSNotificationCenter.defaultCenter postNotificationName:RNUnityMessageNotification object:self userInfo:event];
|
|
293
|
+
[self.host emitMessage:message generation:self.generation];
|
|
294
|
+
}];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
- (void)fail:(NSString *)code message:(NSString *)message cause:(nullable NSString *)cause {
|
|
298
|
+
[self transition:RNUnityStateFailed];
|
|
299
|
+
_lastError = [self error:code message:message cause:cause];
|
|
300
|
+
[NSNotificationCenter.defaultCenter postNotificationName:RNUnityErrorNotification object:self userInfo:_lastError];
|
|
301
|
+
[_host emitError:_lastError];
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
- (NSString *)diagnosticsJSON {
|
|
305
|
+
NSMutableDictionary *data = [@{
|
|
306
|
+
@"state": self.stateName, @"generation": @(_generation),
|
|
307
|
+
@"pauseReasons": _pauseReasons.allObjects, @"lastTransitionAt": @(_lastTransitionAt),
|
|
308
|
+
@"unityVersion": @"6000.3.x", @"packageVersion": @"0.1.0-next.14",
|
|
309
|
+
} mutableCopy];
|
|
310
|
+
if (_host) data[@"hostId"] = _host.hostId;
|
|
311
|
+
if (_lastError) data[@"lastError"] = _lastError;
|
|
312
|
+
NSData *json = [NSJSONSerialization dataWithJSONObject:data options:0 error:nil];
|
|
313
|
+
return [[NSString alloc] initWithData:json encoding:NSUTF8StringEncoding];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
@end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#import <React/RCTViewComponentView.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
@interface RNUnityView : RCTViewComponentView
|
|
6
|
+
@property(nonatomic, readonly) NSString *hostId;
|
|
7
|
+
@property(nonatomic, copy) NSString *detachedBehavior;
|
|
8
|
+
- (void)mountUnityView:(UIView *)view;
|
|
9
|
+
- (void)unmountUnityView:(nullable UIView *)view;
|
|
10
|
+
- (void)emitReadyWithState:(NSString *)state generation:(NSInteger)generation;
|
|
11
|
+
- (void)emitMessage:(NSString *)message generation:(NSInteger)generation;
|
|
12
|
+
- (void)emitState:(NSString *)state generation:(NSInteger)generation;
|
|
13
|
+
- (void)emitError:(NSDictionary *)error;
|
|
14
|
+
@end
|
|
15
|
+
|
|
16
|
+
NS_ASSUME_NONNULL_END
|
|
17
|
+
|