设计新架构目的
Cocos官方并未提供iOS原生内嵌Cocos方案,目前大多采用的方案是:
Cocos编辑器生成iOS工程,以该iOS工程作为基底,再做功能开发。该方案有以下几个问题:
- 代码层:Cocos和原生通信代码都在主工程中,随着后续功能迭代,主工程会越来越臃肿,不方便后续维护管理。
- 团队协作:开发者必须从Cocos编辑器中生成iOS工程,在对工程配置后,才能开发,开发效率极低。
- 工程侵入性:在已有的iOS项目中,几乎无法内嵌Cocos游戏,只能以web形式内嵌,大大降低游戏性能、体验。
新架构特性
针对上述方案的缺点, 新架构应满足以下几点特性:
- iOS工程基于原生生成,Cocos引擎以pods库形式被引用。
- Cocos与原生通信代码与主工程解耦,保持主工程代码整洁。
- 团队成员通过pods命令可以直接生成、运行项目。
新架构之路
编译Cocos引擎库
通过Cocos编辑器找到Cocos引擎源码,引擎源码目录:/Applications/Cocos/Creator/3.7.4/CocosCreator.app/Contents/Resources/resources/3d/engine/native/
生成cocos引擎需要以下几个文件及目录
CMakeLists.txt
cmake/
cocos/
extensions/
external/
templates/
utils/CMakeLists.header.txt
通过CMake指令即可生成Cocos引擎工程,CMake指令:
#!/bin/bash
mkdir -p build
cp CMakeLists.header.txt build/CMakeLists.txt
cmake -Sbuild -Bbuild -DCC_USE_GLES2=OFF -DCC_USE_VULKAN=OFF -DCC_USE_GLES3=OFF -DCC_USE_METAL=ON \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DUSE_PHYSICS_PHYSX=ON \
-DCMAKE_OSX_SYSROOT=iphoneos \
-DCMAKE_SYSTEM_NAME=iOS \
-GXcode
if [[ -f build/compile_commands.json ]]; then
cp build/compile_commands.json .
fi
这里只需要生成iOS工程,修改了部分配置文件,已上传github仓库,可直接使用。
Cocos引擎封装
接下来在主工程中引用引擎库,但是使用引擎方法会出现找不到头文件错误。而要直接引用头文件,需要找到引擎中所有头文件,并且要按Cocos的目录存放,这工作量相当大。所以对引擎做一层封装,主工程只要依赖封装的类,即可实现对引擎方法的调用。
分析Cocos生成的iOS工程,看看iOS工程主要调用了哪些方法:
-
初始化(iOS工程-main.m)
cc::BasePlatform* platform = cc::BasePlatform::getPlatform(); if (platform->init()) { return; } -
应用生命周期(iOS工程-AppDelegate.mm)
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions; - (void)applicationWillResignActive:(UIApplication *)application; - (void)applicationDidBecomeActive:(UIApplication *)application; - (void)applicationWillTerminate:(UIApplication *)application; - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator -
创建Cocos页面(iOS工程-AppDelegate.mm)
CGRect bounds = [[UIScreen mainScreen] bounds]; self.window = [[UIWindow alloc] initWithFrame:bounds]; // Should create view controller first, cc::Application will use it. _viewController = [[ViewController alloc] init]; _viewController.view = [[View alloc] initWithFrame:bounds]; _viewController.view.contentScaleFactor = UIScreen.mainScreen.scale; _viewController.view.multipleTouchEnabled = true; [self.window setRootViewController:_viewController]; [self.window makeKeyAndVisible];- 初始化应用window(Cocos引擎通过UIApplication.shared.delegate.window获取应用window,执行渲染操作)。
- 创建window的ViewController,并对ViewController的View赋值,赋值对象是引擎的cc.View
-
cc.View的宿主ViewController实现转屏方法(iOS工程-ViewController.mm)
AppDelegate* delegate = [[UIApplication sharedApplication] delegate]; [delegate.appDelegateBridge viewWillTransitionToSize:size withTransitionCoordinator:coordinator]; float pixelRatio = [delegate.appDelegateBridge getPixelRatio]; //CAMetalLayer is available on ios8.0, ios-simulator13.0. CAMetalLayer *layer = (CAMetalLayer *)self.view.layer; CGSize tsize = CGSizeMake(static_cast<int>(size.width * pixelRatio), static_cast<int>(size.height * pixelRatio)); layer.drawableSize = tsize; -
cc.BaseGame类实现(iOS工程-Game.h)
#pragma once
#include "cocos/cocos.h"
/**
@brief The cocos2d Application.
The reason for implement as private inheritance is to hide some interface call
by Director.
*/
class Game : public cc::BaseGame {
public:
Game();
int init() override;
// bool init() override;
void onPause() override;
void onResume() override;
void onClose() override;
};
该类无需对iOS工程暴露,实现方法即可。
通过创建cocos_bridge库,封装上述用到的方法,iOS工程依赖cocos_bridge即可实现对引擎调用。
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@interface xx_cocos_bridge : NSObject
+ (xx_cocos_bridge *)instance;
- (void)initPlatform;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions;
- (void)applicationWillResignActive:(UIApplication *)application;
- (void)applicationDidBecomeActive:(UIApplication *)application;
- (void)applicationWillTerminate:(UIApplication *)application;
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator;
- (UIView*)getCocosView;
- (float)getCocosPixelRatio;
+ (void)callS:(NSString *)funcName args:(NSString *)args;
@end
在cocos引擎工程中创建cocos_bridge库,实现上述方法,配置头文件路径、编译选项,可编译出独立的封装库。
到此你可以得到[lib_engine.a, lib_cocos_bridge.a, coco_bridge.h]。因为Cocos引擎还依赖了第三方库,所以以下第三方库也要一并加进来(/external/ios/libs/*)。接下来解决通过pods引用这些静态库。
制作Cocos引擎Pods库
通过制作pod库方便iOS主工程依赖Cocos引擎以及引擎封装类,该库需要包含封装类的头文件,以及Cocos静态库和Cocos的依赖三方库
-
pod库的Classes文件下放封装类头文件
-
pod库的Classes文件下放静态库文件 libcocos_engine.a是编译出来的引擎静态库,libkk_cocos.a是封装类的静态库,其他静态库都是Cocos引擎依赖的三方静态库。
-
podspec文件配置 frameworks和library是从Cocos的demo工程中找出来的,需要全部加上。
s.source_files = 'kk_cocos_game/Classes/**/*.h'
s.vendored_libraries = "kk_cocos_game/Libs/**/*.a"
s.frameworks = 'AudioToolbox', 'AVFoundation', 'AVKit', 'CoreVideo', 'CoreMotion', 'CFNetwork', 'CoreMedia', 'CoreText', 'CoreGraphics', 'GameController', 'JavaScriptCore', 'Metal', 'MetalKit', 'MetalPerformanceShaders', 'OpenAL', 'OpenGLES', 'QuartzCore', 'SystemConfiguration', 'Security', 'UIKit', 'WebKit'
s.library = 'sqlite3', "iconv", "z"
至此iOS主工程只要需要通过pod命令即可轻松依赖Cocos引擎,接下来详解主工程中如何显示Cocos场景以及场景切换。
Cocos与原生界面管理
场景切换
Cocos画面渲染需要用到AppDelegate下的window,所以iOS13后SceneDelegate方案暂时还不能用。 Cocos与原生界面切换本质就是通过切换各自持有的window显示/隐藏实现。 因此在AppDelegate中需要声明两个window,代码如下:
@interface AppDelegate()
// 新声明的原生window,本项目用的是Flutter页面
@property (nullable, nonatomic, strong) UIWindow *flutterWindow;
@property (nonatomic, strong) FlutterEngine *flutterEngine;
@end
@implementation AppDelegate
@synthesize window;
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions {
// 初始化游戏引擎
[[kk_cocos_bridge instance] initPlatform];
window = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
// 创建game window的ViewController
GameViewController *vc = [[GameViewController alloc] init];
// ViewController的view属性需要使用Cocos的View赋值
vc.view = [[kk_cocos_bridge instance] getCocosView];
window.rootViewController = vc;
[window makeKeyAndVisible];
// Flutter初始化,如果只用原生页面,此处不用考虑
_flutterEngine = [[FlutterEngine alloc] initWithName:@"com.xxx.flutter-engine"];
[_flutterEngine run];
[GeneratedPluginRegistrant registerWithRegistry:_flutterEngine];
_flutterWindow = [[UIWindow alloc]initWithFrame:[UIScreen mainScreen].bounds];
FlutterViewController *flutterVC = [[FlutterViewController alloc]initWithEngine:_flutterEngine nibName:nil bundle:nil];
_flutterWindow.rootViewController = flutterVC;
// 优先显示Flutter场景
[self.flutterWindow makeKeyAndVisible];
[[kk_cocos_bridge instance] application:application didFinishLaunchingWithOptions:launchOptions];
return YES;
}
再添加两个切换窗口显示的方法,就基本完成Cocos界面控制。
不过从工程架构看,该方案的侵入性还是不友好,还需要对AppDelegate解耦,做一套组件化方案。
AppDelegate组件化
通过对网上的方案比较,决定采用TDFModuleKit方案(实现原理作者原文)
- 创建一个pod库,该库依赖TDFModuleKit,管理window生命周期,负责window切换工作。
// XXWindowModule.h
// XXWindowModule 继承TDFModule
@interface XXWindowModule: TDFModule<TDFModuleProtocol>
@end
// XXWindowModule.m
// XXWindowModule需要再load阶段调用registerModule
+ (void)load {
[self registerModule];
}
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary<UIApplicationLaunchOptionsKey,id> *)launchOptions {
// 把主工程的代码块迁移到该模块下即可.
...
return YES;
}
2. window切换方法
- (void)showGameWindow:(NSNumber*)landscape {
dispatch_async(dispatch_get_main_queue(), ^{
self.currentWindowType = @"Game";
[self.window makeKeyAndVisible];
[self updateOrientation:landscape.longLongValue == 1 ? UIInterfaceOrientationMaskLandscape : UIInterfaceOrientationMaskPortrait];
});
}
// 本项目原生页面用Flutter替代
- (void)showFlutterWindow {
dispatch_async(dispatch_get_main_queue(), ^{
self.currentWindowType = @"Flutter";
[self updateOrientation:UIInterfaceOrientationMaskPortrait];
[self.flutterWindow makeKeyAndVisible];
});
}
3. 方法调用
通过performSelector即可完成切换window方法调用
UIApplication.shared.delegate?.perform(Selector(("showGameWindow:")),with: landscape)
游戏资源
目前采取比较粗暴的方式,直接把Cocos打包的游戏资源,拖入到iOS主工程中。
总结
至此基本完成iOS内嵌Cocos架构,能满足基础的原生与Cocos场景切换。如果还有精力,可以对Cocos引擎层代码再做一些修改(比如使用SceneDelegate方案、游戏资源管理)。
鉴于网上关于Cocos与原生开发的技术文章还是太少,因此记录下本次实践记录,可以给更多开发者一个实践参考。如有更好方案或疑问欢迎评论区交流。