React Native 集成 Unity (Android)

由于公司项目涉及到基于 Unity 的 AR 的 APP 开发,同时还有大量的 UI 交互等特性;团队一开始想到的,是基于 Unity 来绘制 UI 界面,但是在经历了 2 个周的探索和尝试之后,发现开发效率、后期维护、展示效果等都不尽如意,尤其再加上路由控制等等之类的模拟的话,整个项目复杂度以及 Unity 的开发体验变得难受起来。

然后前端这边,提供了 2 个方案,一个是基于 Unity 提供的 Webview 嵌入 H5 页面实现 UI,然后通过 Unity-Webview 提供的 API 进行通信,另一个当然就是 React Native 了,作为前端来说,其在性能等方面对比其他跨平台方案无出其右。Webview 技术实现上当然简单得多,但是在做了一些 Demo 尝试之后,由于 Unity Webview 本身未经任何优化,各种卡顿也是在所难免,于是铤而走险,参考网上几篇教程,开始了 React Native 集成 Unity 的踩坑之旅。

当然,以上都是1年多以前的事儿了,现在想想,各种坑其实也忘得差不多了,目前来看,集成结果也算是稳定运行,所以记下来,以备忘,同时希望可以帮到有类似需求的同学。由于站在前端角度,对 Unity 以及 Android/iOS 开发都不甚了解,难免有描述不周的地方,敬请指正。

Android 的集成相对容易得多,iOS 稍后奉上。

创建 React Native 项目的相关步骤就不说了哈,参考官网文档即可;Unity 项目导出为 Android 工程基本也没什么需要注意的,照常即可。

下述方案,是将 Unity 导出的 Android 工程集成到 React Native 中,并参考 React Native 的原生组件方案封装出来,实现 Js 调用并和 Unity 交互的目的。

拷贝相关依赖文件到 React Native Android 工程目录下,并合并 build.gradle 配置

查看 Unity 的 Android 工程目录下,build.gradle 文件的 dependencies 相关配置,并拷贝相关依赖文件:

  1. libs/... -> android/app/libs/...
  2. src/main/jniLibs -> android/app/src/main/jniLibs

以上属于 Unity 的公共库文件,以后 Unity 项目更新时候,如果没有插件层面的变更,无须再次拷贝替换。

  1. src/main/assets -> android/app/src/assets,该目录为 Unity 程序内容资源目录,每次 Unity 更新时候都需要拷贝替换

其他目录可能要具体看了,比如我们的还涉及到一个 GalleryScreenshot 文件夹,需要拷贝到 android/GalleryScreenshot

当然,如果该文件中还有其他配置项以及依赖文件,也要具体斟酌一起合并到 React Native 的 /android/app/build.gradle 文件去,比如

1
2
compile fileTree(dir: "libs", include: ["*.jar"])
compile project(':GalleryScreenshot')

编写 React-Native-Unity 组件

将下述文件及其内容创建到 /android/app/src/main/java/com/example/unity 目录下,即 unity 目录和 MainActivity.java 文件的父目录并列

封装 Unity 原生模块

遵循 React Native 的模块包装方法,包装 Unity 端通信相关方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// UnityModule.java

package com.example.unity;

import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
import com.facebook.react.modules.core.DeviceEventManagerModule;
import com.unity3d.player.UnityPlayer;

import android.support.annotation.Nullable;
import android.util.Log;

import com.facebook.react.bridge.WritableMap;

public class UnityModule extends ReactContextBaseJavaModule {
private static final String MODULE_NAME = "RNUnityModal";
public static ReactApplicationContext reactContext;

public UnityModule(ReactApplicationContext reactContext) {
super(reactContext);
this.reactContext = reactContext;
}

public static void sendEvent(@Nullable WritableMap params) {
reactContext
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
.emit("unityEvent", params);
}

@Override
public String getName() {
return MODULE_NAME;
}

@ReactMethod
public void sendMessage(String actionName, String params) {
Log.i("Unity/React", ">>>> sendMessage to unity : " + actionName + "-:-" + params);
UnityPlayer.UnitySendMessage("UnityBridge", actionName, params);
}
}

在 MainActivity.java 中添加 Unity 需要的一些系统事件监听

参考 Unity 工程的相关文件,修改 android/app/src/main/java/com/example/…/MainActivity.java 文件,大概如下:

  1. 引入 UnityModule 和 UnityPlayer

    1
    2
    import com.unity3d.player.UnityPlayer;
    import cn.othink.unity.UnityModule;
  2. 在类 MainActivity 中添加相关的静态属性,并在 onCreate 中初始化 Unity,同时,为 Unity 提供接口方法,大概如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    public class MainActivity extends ReactActivity {

    public static UnityPlayer mUnityPlayer; // here
    public static boolean isUnityPlaying = false; // here

    @Override
    protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // 初始化 Unity Player
    this.mUnityPlayer = new UnityPlayer(this);
    this.mUnityPlayer.requestFocus();
    this.mUnityPlayer.resume();
    }

    /**
    * Returns the name of the main component registered from JavaScript.
    * This is used to schedule rendering of the component.
    */
    @Override
    protected String getMainComponentName() {
    return "xxx";
    }

    // 以下为 Unity 需要的相关事件监听

    // Quit Unity
    @Override protected void onDestroy ()
    {
    this.mUnityPlayer.quit();
    super.onDestroy();
    }
    // Pause Unity
    @Override protected void onPause()
    {
    super.onPause();
    this.mUnityPlayer.pause();
    }
    // Resume Unity
    @Override protected void onResume()
    {
    super.onResume();
    this.mUnityPlayer.resume();
    }
    @Override protected void onStart()
    {
    super.onStart();
    this.mUnityPlayer.start();
    }
    @Override protected void onStop()
    {
    super.onStop();
    this.mUnityPlayer.stop();
    }
    // Low Memory Unity
    @Override public void onLowMemory()
    {
    super.onLowMemory();
    this.mUnityPlayer.lowMemory();
    }
    // Trim Memory Unity
    @Override public void onTrimMemory(int level)
    {
    super.onTrimMemory(level);
    if (level == TRIM_MEMORY_RUNNING_CRITICAL)
    {
    this.mUnityPlayer.lowMemory();
    }
    }
    // This ensures the layout will be correct.
    @Override public void onConfigurationChanged(Configuration newConfig)
    {
    super.onConfigurationChanged(newConfig);
    if (isUnityPlaying) mUnityPlayer.configurationChanged(newConfig);
    }
    // Notify Unity of the focus change.
    @Override public void onWindowFocusChanged(boolean hasFocus)
    {
    super.onWindowFocusChanged(hasFocus);
    if (isUnityPlaying) mUnityPlayer.windowFocusChanged(hasFocus);
    }

    // 以下为自定义部份
    public void EventToDo(String eventName, @Nullable String params)
    {
    WritableMap theParams = Arguments.createMap();
    theParams.putString("type", eventName);
    theParams.putString("params", params);
    UnityModule.sendEvent(theParams);
    Log.i("Unity/React:","EventToDo unityEvent >>>: " + eventName);
    if (eventName == "back") {
    this.isUnityPlaying = false;
    }
    }
    }

然后,Unity 里边就可以通过 EventToDo 方法向 React Native 发送信息了。

原生 UI 组件

封装 Unity 视图,以便在 React Native 中使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// UnityView.java

package com.example.unity;

import android.support.annotation.Nullable;
import android.util.Log;

import com.facebook.react.bridge.Arguments;
import com.facebook.react.bridge.WritableMap;
import com.facebook.react.uimanager.SimpleViewManager;
import com.facebook.react.uimanager.ThemedReactContext;
import com.facebook.react.uimanager.annotations.ReactProp;
import com.unity3d.player.UnityPlayer;

import cn.othink.arhezi.MainActivity;

public class UnityView extends SimpleViewManager<UnityPlayer> {
public static final String REACT_CLASS = "RCTUnityView";

@Override
public String getName() {
return REACT_CLASS;
}

@Override
protected UnityPlayer createViewInstance(ThemedReactContext reactContext) {
Log.i("Unity/React:", ">>>> UnityPlayer 启动了");
MainActivity.isUnityPlaying = true;
MainActivity.mUnityPlayer.windowFocusChanged(true);

WritableMap theParams = Arguments.createMap();
theParams.putString("type", "Loaded");
theParams.putString("params", "");
UnityModule.sendEvent(theParams);

return MainActivity.mUnityPlayer;
}

@ReactProp(name = "params")
public void setParams(UnityPlayer unityPlayer, String params) {
if (params != null && !"".equals(params)){
Log.i("Unity/React", ">>>> UnityBridge -> ReceiveMessage params : " + params);
UnityPlayer.UnitySendMessage("UnityBridge", "ReceiveMessage", params);
}
}

@ReactProp(name = "datas")
public void setDatas(UnityPlayer unityPlayer, String datas) {
if (datas != null && !"".equals(datas)){
Log.i("Unity/React", ">>>> UnityBridge -> ReceiveMessage datas : " + datas);
UnityPlayer.UnitySendMessage("UnityBridge", "ReceiveDatas", datas);
}
}
}

注册模块并导出视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// UnityPackage.java

package com.example.unity;
import com.facebook.react.ReactPackage;
import com.facebook.react.bridge.JavaScriptModule;
import com.facebook.react.bridge.NativeModule;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.uimanager.ViewManager;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

public class UnityPackage implements ReactPackage {
@Override
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
List<NativeModule> modules = new ArrayList<>();
UnityModule mUnityModule = new UnityModule(reactContext);
modules.add(mUnityModule);
return modules;
}

public List<Class<? extends JavaScriptModule>> createJSModules() {
return Collections.emptyList();
}

@Override
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
return Arrays.<ViewManager>asList(
new UnityView()
);
}
}

实现对应的 Javascript 模块

在 Js 组件文件夹中新建 Unity 组件文件夹,如 src/components/Unity,分别创建如下 3 个文件,并录入内容

engine.js,主要的通信方法封装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// engine.js

'use strict'
import React, { Component } from 'react'
import {
NativeModules,
NativeEventEmitter,
NativeAppEventEmitter,
} from 'react-native'

const RNUnityModal = NativeModules.RNUnityModal
const RNUnityModalEmitter = new NativeEventEmitter(RNUnityModal)

export default {
// ...RNUnityModal,
sendMessage(params, actionName = 'ReceiveMessage') { // 向 Unity 发送信息
if (actionName === 'ReceiveMessage' && typeof params !== 'string') params = JSON.stringify(params)
RNUnityModal.sendMessage(actionName, params)
},
eventEmitter(eventHandlers) { // 监听来自 Unity 的事件,并约定使用参数中的 type 属性作为 js 事件名
this.listener && this.listener.remove()
this.listener = RNUnityModalEmitter.addListener('unityEvent', event => {
console.log('Unity/Javascript: 收到来自 Unity 的事件:', event)
let type = event['type'].toLowerCase()
eventHandlers[type] && eventHandlers[type](event)
})
},
removeEmitter() { // 移除监听
if (this.listener) {
this.listener.remove()
this.listener = undefined
}
},
quit() { // 发送退出 Unity 请求,并移除事件监听
this.sendMessage('{"type": "quit"}')
this.removeEmitter()
}
}

view.js,UI 组件实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// view.js

'use strict'
import React, { Component } from 'react'
import {
requireNativeComponent,
View,
Platform,
} from 'react-native'
import PropTypes from 'prop-types'

const RCTUnityView = requireNativeComponent('RCTUnityView', UnityView)

class UnityView extends Component {

render() {
return (
<RCTUnityView {...this.props} />
)
}
}

UnityView.propTypes = {
...View.propTypes,
params: PropTypes.string,
datas: PropTypes.string,
}

export default UnityView

index.js,导出上述方法和 UI 对象

1
2
3
4
5
6
7
// index.js 

import _engine from './engine'
import _view from './view'

export const UnityEngine = _engine
export const UnityView = _view

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import React, { Component } from 'react'

import {UnityView, UnityEngine} from '../components/Unity'

class AR extends Component {
constructor(props) {
super(props)

this.state = {
unityParams: '{"hello": "world"}',
unityDatas: '{"bar": "foo"}',
}
}

componentWillMount() {
let handlers = {
quit: event => { // Unity 退出
this.unityHasQuit = true
this.props.navigation.goBack()
},
}
UnityEngine.eventEmitter(handlers)
}

componentWillUnmount() {
if (!this.unityHasQuit) UnityEngine.sendMessage({type: 'quit'})
UnityEngine.removeEmitter()
}

render() {
return (
<UnityView ref='unityView' params={this.state.unityParams} datas={this.state.unityDatas} style={{width: 300, height: 300}} />
)
}
}

以上只是简单的,直接从我们项目中摘要出来的演示代码,主要还是想说明使用方法和实现思路,可能无法直接运行。其中,比如 sendMessage 发送的数据格式,以及事件名称等,还需要具体和 Unity 端协商。

需要注意的几个点

  1. 导航进入页面时候,由于 Unity 渲染会占用大量资源,所以可能会有卡顿。
    解决办法:如果是使用了 React-navigation 作为导航组件,那么监听 didFocus,在此事件之后再渲染 UnityView,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    constructor(props) {
    super(props)

    this.doBlur = false
    this.state = {
    isShowUnityView: false
    }
    }

    componentWillMount() {
    componentWillMount() {
    let handlers = {
    quit: event => { // Unity 退出事件
    this.unityHasQuit = true
    this.props.navigation.goBack()
    },
    }
    UnityEngine.eventEmitter(handlers)
    }
    // didFocus - 页面切换动画完成之后,再渲染 Unity, 以避免页面切换时卡顿
    this.didFocusSubscription = this.props.navigation.addListener('didFocus', this.onDidFocus)
    this.willBlurSubscription = this.props.navigation.addListener('willBlur', this.onBlur)
    }

    onBlur = () => {
    this.doBlur = true
    }

    onDidFocus = () => {
    if (this.doBlur) {
    this.doBlur = false
    } else {
    this.setState({isShowUnityView: true})
    }
    }
  2. 在 Android 上,通过系统返回按键返回上一页时候,由于 UnityView 被自动销毁,componentWillUnmount 里边的 UnityEngine.sendMessage({type: ‘quit’}) 可能会调用不成功,导致 Unity 不能正常退出,下次重新启动时候会出错。
    解决办法:使用 BackHandler 绑定返回按键事件,拦截页面返回,并调用 Unity 的退出方法退出,同时向 Js 发送退出事件,Js 监听到 Unity 退出事件之后再作返回动作,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    componentWillMount() {
    if (Platform.OS === 'android') BackHandler.addEventListener('hardwareBackPress', this.onBackAndroid)
    }

    componentWillUnmount() {
    if (Platform.OS === 'android') BackHandler.removeEventListener('hardwareBackPress', this.onBackAndroid)

    onBackAndroid() {
    UnityEngine.sendMessage({type: 'quit'})
    return true
    }

其他的坑,js 端的,原生端的,上面不尽然,很可能都还会踩到,Google 不行,Bing 也可以尝试哈,一般还是都能解决的,如遇其他疑问,再说哈。

集成的具体效果,可以观摩我们的 APP AR盒子

Proudly powered by Hexo and Theme by Hacker
© 2018 Riant