有个项目需求,如下:
- 界面基本是App风格
- 显示内容时需要用到3D模型,而且需要可操作3D对象
如果只用Unity来做,也能做出UI界面,但风格与原生App还是有些差异,模拟实现起来工作量也比较大。
想着用ReactNative结合Unity来做,网上查找了一些资料,的确有这么做的,但有些在现有版本上并没有成功,如react-native-unity-view,只到发现Unity官方支持原生应用与Unity结合,但是没有发现与ReactNative结合比较好的,考虑在这些人的经验上自己实现一套。
第一步:环境准备
机器上nvm管理了nodejs6、nodejs8,但看到nodejs到12了,准备用最新版的试试,先装好nodejs。
yarn & react-native-cli
Chaim:workspace Chaim$ sudo npm install -g yarn react-native-cli
初始化项目
Chaim:react-native Chaim$ react-native init ReactNativeUnitylib
This will walk you through creating a new React Native project in /Users/Chaim/Documents/workspace/react-native/ReactNativeUnitylib
Using yarn v1.21.1
Installing react-native...
yarn add v1.21.1
......
info Installing required CocoaPods dependencies
Run instructions for iOS:
• cd /Users/Chaim/Documents/workspace/react-native/ReactNativeUnitylib && npx react-native run-ios
- or -
• Open ReactNativeUnitylib/ios/ReactNativeUnitylib.xcworkspace in Xcode or run "xed -b ios"
• Hit the Run button
Run instructions for Android:
• Have an Android emulator running (quickest way to get started), or a device connected.
• cd /Users/Chaim/Documents/workspace/react-native/ReactNativeUnitylib && npx react-native run-android
这次为什么不用nvm,而直接装呢?那是因为有些命令会直接打开命令行启动node,给这些命令一个可用的node吧!
说明一下,编译Xcode项目启动的Packages Server, 是在Build Phases中脚本设置的。
第二步:原生与Unity结合
初始项目修改下签名,正常就能运行起来,这步是将RN生成的项目与Unity结合,先在原生代码中显示Unity内容。
根据官方示例生成的UnityProject项目,build ios版本。
添加UnityFramwork.framework时XCode就崩溃,解决了很长时间,最终通过在Build Phases中拷贝framework的方法解决。
原生显示不管RN时比较简单,如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self initRNandUnity];
UIWindow* unityWindow = [[[self ufw] appController] window];
self.window = unityWindow;
[self.window makeKeyAndVisible];
return YES;
}
- (bool)unityIsInitialized { return [self ufw] && [[self ufw] appController]; }
// Method to initialize RN and Unity
- (void)initRNandUnity
{
/////////////////
// Unity bundle loading
if([self unityIsInitialized]) { // safeguard for unity bundle dynamic load/unload example (not in use)
// showAlert(@"Unity already initialized", @"Unload Unity first");
return;
}
[self setUfw: UnityFrameworkLoad()];
// Set UnityFramework target for Unity-iPhone/Data folder to make Data part of a UnityFramework.framework and uncomment call to setDataBundleId
// ODR is not supported in this case, ( if you need embedded and ODR you need to copy data )
[[self ufw] setDataBundleId: "com.unity3d.framework"];
// [[self ufw] registerFrameworkListener: self];
// [NSClassFromString(@"FrameworkLibAPI") registerAPIforNativeCalls:self];
[[self ufw] runEmbeddedWithArgc: gArgc argv: gArgv appLaunchOpts: appLaunchOpts];
self.unityView = [[[self ufw] appController] rootView]; // set view to root
}
将Unity window直接赋值给self.window显示即可。
同时还要注意下全局变量,在main.mm中定义,在AppDelegate.mm中使用,如下:
// keep arg for unity init from non main
extern int gArgc;
extern char** gArgv;
否则runEmbeddedWithArgc()又会异常。
处理这个时间完全超出预计,本来以为加上RN控件一天搞定,结果估计还得一天...
第三步、Unity上显示RN
可以按此处一样,在Unity上显示RN控件:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[self initRNandUnity];
self.window = [[[self ufw] appController] window];
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ReactNativeUnitylib"
initialProperties:nil];
// rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
rootView.frame = CGRectMake(0, 0, self.unityView.frame.size.width, 400); // React
rootView.backgroundColor = UIColor.clearColor; // transparent view on ios, only items set in react can be seen
[self.unityView addSubview:rootView]; // to-do: adding an animation
[self.window makeKeyAndVisible];
return YES;
}
当然也可以Unity是一个单独的窗口,显示在RN窗口前面,如下:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
RCTBridge *bridge = [[RCTBridge alloc] initWithDelegate:self launchOptions:launchOptions];
RCTRootView *rootView = [[RCTRootView alloc] initWithBridge:bridge
moduleName:@"ReactNativeUnitylib"
initialProperties:nil];
rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
UIViewController *rootViewController = [UIViewController new];
rootViewController.view = rootView;
self.window.rootViewController = rootViewController;
[self.window makeKeyAndVisible];
[self initRNandUnity];
UIWindow* unityWindow = [[[self ufw] appController] window];
CGRect viewRect = CGRectMake(0, 0, self.window.rootViewController.view.frame.size.width, 400);
UIView* unityView = [[[self ufw] appController] rootView];
unityView.frame = viewRect;
[unityWindow makeKeyAndVisible];
return YES;
}
但是发现无论怎么设置Unity窗口高度,Unity都是全屏!这样怎么才能把Unity做为一个组件加载呢?麻烦了!
查找了些资料,发现在Untiy启动时设置frame尺寸可能可以,可以用子类化来做,先简单直接改Unity启动代码吧!
- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
......
CGRect viewRect = CGRectMake(0, 0, 300, 400);
_window = [[UIWindow alloc] initWithFrame: viewRect];
// _window = [[UIWindow alloc] initWithFrame: [UIScreen mainScreen].bounds];
_unityView = [self createUnityView];
......
}
还真是可以,下一步当做组件来显示看来有戏了!
~好吧,我考虑复杂了,一直在改rootView的尺寸,其实应该修改window尺寸即可修改unity窗口尺寸,如下:
UIWindow* unityWindow = [[[self ufw] appController] window];
CGRect viewRect = CGRectMake(100, 100, 400, 400);
unityWindow.frame = viewRect;
第四步、实现ReactNative的Unity组件
先参考ReactNative原生UI组件实现一个原生组件。
RCTUnityView.h
//
// RCTUnityView.h
// ReactNativeUnitylib
//
// Created by Chaim on 2019/12/23.
// Copyright © 2019 Facebook. All rights reserved.
//
#ifndef RCTUnityView_h
#define RCTUnityView_h
#import <UIKit/UIKit.h>
@interface RCTUnityView : UIView
@property (nonatomic, strong) UIView* uView;
@end
#endif /* RCTUnityView_h */
RCTUnityView.mm
//
// RCTUnityView.m
// ReactNativeUnitylib
//
// Created by Chaim on 2019/12/23.
// Copyright © 2019 Facebook. All rights reserved.
//
#import "RCTUnityView.h"
#import "AppDelegate.h"
@implementation RCTUnityView
-(id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self){
// UIView *cellView = [[UIView alloc] init];
// cellView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:0.0f blue:0.0f alpha:1];
AppDelegate * appDelegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
UIView* unityView = [[[appDelegate ufw] appController] window];
self.uView = (UIView*)unityView;
}
return self;
}
-(void)layoutSubviews
{
[super layoutSubviews];
[self.uView removeFromSuperview];
self.uView.frame = self.bounds;
[self insertSubview:self.uView atIndex:0];
[self.uView setNeedsLayout];
}
@end
RCTUnityViewManager.h
//
// RCTUnityViewManager.h
// ReactNativeUnitylib
//
// Created by Chaim on 2019/12/23.
// Copyright © 2019 Facebook. All rights reserved.
//
#ifndef RCTUnityViewManager_h
#define RCTUnityViewManager_h
#import <React/RCTViewManager.h>
@interface RCTUnityViewManager : RCTViewManager
@end
#endif /* RCTUnityViewManager_h */
RCTUnityViewManager.m
//
// RCTUnityViewManager.h
// ReactNativeUnitylib
//
// Created by Chaim on 2019/12/23.
// Copyright © 2019 Facebook. All rights reserved.
//
#ifndef RCTUnityViewManager_h
#define RCTUnityViewManager_h
#import <React/RCTViewManager.h>
@interface RCTUnityViewManager : RCTViewManager
@end
#endif /* RCTUnityViewManager_h */
RCTUnityView.js
import { requireNativeComponent } from 'react-native';
export default requireNativeComponent('RCTUnityView');
App.js
import RCTUnityView from './RCTUnityView.js';
<ScrollView>
<RCTUnityView style={{left:50, top: 0, width: 300, height: 500 }} />
</ScrollView>
最终结果如下,和其它控件一起,也可以滚动:
非常感谢此文的指引!
参考
https://forum.unity.com/threads/integration-unity-as-a-library-in-native-ios-app.685219
https://github.com/CGS-Canada/react-native-unity
https://docs.unity3d.com/2020.1/Documentation/Manual/UnityasaLibrary-iOS.html
https://blog.mutoo.im/2015/09/make-subclass-from-unityappcontroller/
https://twnkls.com/blogs/howto-native-ios-plugins-for-unity3d/
附
解决Embed UnityFramework.framework崩溃问题
1:首先需要在“ 构建阶段”选项卡下添加“ 新建副本文件”阶段。
2: Second change the name of the added phase to Embed Frameworks
3: Change the destination to Frameworks.
4: Add the framework for which the error occurred.