diff --git a/.gitignore b/.gitignore index d3b53df..b513993 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,4 @@ android/keystores/debug.keystore # generated by bob lib/ +/azesmway-react-native-unity-1.0.11.tgz diff --git a/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java b/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java index b944779..bf44133 100644 --- a/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java +++ b/android/src/main/java/com/azesmwayreactnativeunity/UPlayer.java @@ -97,7 +97,7 @@ public FrameLayout requestFrame() throws NoSuchMethodException { return (FrameLayout) getFrameLayout.invoke(unityPlayer); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { - return unityPlayer; + return (FrameLayout)(Object) unityPlayer; } } diff --git a/ios/RNUnityView.mm b/ios/RNUnityView.mm index a4caa74..d089f52 100644 --- a/ios/RNUnityView.mm +++ b/ios/RNUnityView.mm @@ -187,6 +187,11 @@ - (instancetype)initWithFrame:(CGRect)frame { gridViewEventEmitter->onUnityMessage(event); } }; + + // Start Unity immediately, don't wait for updateProps + if (![self unityIsInitialized]) { + [self initUnityModule]; + } } return self; diff --git a/package.json b/package.json index 063bf5e..b515f01 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@azesmway/react-native-unity", "version": "1.0.11", + "private": true, "description": "React Native Unity", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -14,6 +15,8 @@ "ios", "cpp", "unity", + "app.plugin.js", + "plugin/build", "*.podspec", "!ios/build", "!android/build", @@ -31,8 +34,10 @@ "test": "jest", "typecheck": "tsc --noEmit", "lint": "eslint \"**/*.{js,ts,tsx}\"", - "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", - "prepare": "bob build", + "build:plugin": "tsc --project plugin/tsconfig.json", + "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib plugin/build", + "prepare": "bob build && yarn build:plugin", + "pack": "del-cli azesmway-react-native-unity-1.0.11.tgz && npm pack", "release": "release-it" }, "keywords": [ diff --git a/plugin/src/index.ts b/plugin/src/index.ts index 9dd56df..5ac08b5 100644 --- a/plugin/src/index.ts +++ b/plugin/src/index.ts @@ -1,21 +1,21 @@ +import type { ConfigPlugin } from '@expo/config-plugins'; import { AndroidConfig, + withDangerousMod, withGradleProperties, withProjectBuildGradle, withSettingsGradle, withStringsXml, } from '@expo/config-plugins'; -import type { ConfigPlugin } from '@expo/config-plugins'; +import * as fs from 'fs'; +import * as path from 'path'; -const withUnity: ConfigPlugin<{ name?: string }> = ( - config, - { name = 'react-native-unity' } = {} -) => { - config.name = name; +const withUnity: ConfigPlugin<{}> = (config, {} = {}) => { config = withProjectBuildGradleMod(config); config = withSettingsGradleMod(config); config = withGradlePropertiesMod(config); config = withStringsXMLMod(config); + config = withIosFabricRegistration(config); return config; }; @@ -23,44 +23,50 @@ const REPOSITORIES_END_LINE = `maven { url 'https://www.jitpack.io' }`; const withProjectBuildGradleMod: ConfigPlugin = (config) => withProjectBuildGradle(config, (modConfig) => { - if (modConfig.modResults.contents.includes(REPOSITORIES_END_LINE)) { + if ( + modConfig.modResults.contents.includes(REPOSITORIES_END_LINE) && + !modConfig.modResults.contents.includes(':unityLibrary') + ) { // use the last known line in expo's build.gradle file to append the newline after modConfig.modResults.contents = modConfig.modResults.contents.replace( REPOSITORIES_END_LINE, REPOSITORIES_END_LINE + '\nflatDir { dirs "${project(\':unityLibrary\').projectDir}/libs" }\n' ); - } else { - throw new Error( - 'Failed to find the end of repositories in the android/build.gradle file`' - ); } return modConfig; }); const withSettingsGradleMod: ConfigPlugin = (config) => withSettingsGradle(config, (modConfig) => { - modConfig.modResults.contents += ` + if (!modConfig.modResults.contents.includes(':unityLibrary')) { + modConfig.modResults.contents += ` include ':unityLibrary' project(':unityLibrary').projectDir=new File('../unity/builds/android/unityLibrary') `; + } return modConfig; }); const withGradlePropertiesMod: ConfigPlugin = (config) => withGradleProperties(config, (modConfig) => { - modConfig.modResults.push({ - type: 'property', - key: 'unityStreamingAssets', - value: '.unity3d', - }); + const alreadySet = modConfig.modResults.some( + (item) => item.type === 'property' && item.key === 'unityStreamingAssets' + ); + if (!alreadySet) { + modConfig.modResults.push({ + type: 'property', + key: 'unityStreamingAssets', + value: '.unity3d', + }); + } return modConfig; }); // add string const withStringsXMLMod: ConfigPlugin = (config) => - withStringsXml(config, (config) => { - config.modResults = AndroidConfig.Strings.setStringItem( + withStringsXml(config, (modConfig) => { + modConfig.modResults = AndroidConfig.Strings.setStringItem( [ { _: 'Game View', @@ -69,9 +75,68 @@ const withStringsXMLMod: ConfigPlugin = (config) => }, }, ], - config.modResults + modConfig.modResults ); - return config; + return modConfig; }); +// Patches RCTThirdPartyComponentsProvider.mm (generated by expo prebuild) to register +// RNUnityView with Fabric's component registry so that updateProps is dispatched correctly. +const withIosFabricRegistration: ConfigPlugin = (config) => + withDangerousMod(config, [ + 'ios', + (modConfig) => { + const iosRoot = modConfig.modRequest.platformProjectRoot; + const projectName = modConfig.modRequest.projectName ?? ''; + + // The file may be at the ios/ root or inside ios// + const candidates = [ + path.join(iosRoot, 'RCTThirdPartyComponentsProvider.mm'), + path.join(iosRoot, projectName, 'RCTThirdPartyComponentsProvider.mm'), + ]; + + const providerPath = candidates.find((p) => fs.existsSync(p)); + if (!providerPath) { + console.warn( + '[react-native-unity] RCTThirdPartyComponentsProvider.mm not found. ' + + 'RNUnityView may not be registered with Fabric. ' + + 'Run `npx expo prebuild` again after the initial build.' + ); + return modConfig; + } + + let contents = fs.readFileSync(providerPath, 'utf-8'); + + const dictEntry = `@"RNUnityView" : RNUnityViewCls(),`; + + // Nothing to do if already patched + if (contents.includes(dictEntry)) { + return modConfig; + } + + const forwardDecl = + 'Class RNUnityViewCls(void);'; + + // Insert forward declaration before @implementation + if (!contents.includes(forwardDecl)) { + contents = contents.replace( + /(@implementation RCTThirdPartyComponentsProvider)/, + `${forwardDecl}\n\n$1` + ); + } + + // Insert dictionary entry before the closing }; of the components dict. + // The dict sits inside a dispatch_once block so the unique pattern is: + // (indent)};(whitespace)}); + contents = contents.replace( + /([ \t]*)(};)([ \t]*\n[ \t]*\}\);)/, + `$1 ${dictEntry}\n$1$2$3` + ); + + fs.writeFileSync(providerPath, contents, 'utf-8'); + + return modConfig; + }, + ]); + export default withUnity; diff --git a/tsconfig.json b/tsconfig.json index 05362c2..37e43d4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,8 @@ "compilerOptions": { "baseUrl": "./", "paths": { - "@azesmway/react-native-unity": ["./src/index"] + "@azesmway/react-native-unity": ["./src/index"], + "@azesmway/react-native-unity/plugin": ["./plugin/src/index"], }, "allowUnreachableCode": false, "allowUnusedLabels": false,