图形引擎实战:项目引擎迁移及鸿蒙化
这里将对项目引擎升级以及编译鸿蒙包过程中遇到的一些问题及注意事项进行简单整理这里只提及常规要修改的地方,一个是涉及前向渲染器的类或结构体已经弃用,需要使用通用渲染器进行替换,如:ForwardRendererData替换为UniversalRendererData,ForwardRenderer替换为ForwardRenderer。
这里将对项目引擎升级以及编译鸿蒙包过程中遇到的一些问题及注意事项进行简单整理
1. 引擎迁移
目前项目使用的是Unity 2019.4.40版本,升级到团结引擎1.2.0(Tuanjie是以Unity 2022.3.2 LTS为基础研发的,因此也可以先升级到此版本,再转为团结)的过程中,有一些兼容适配问题需要留意:
1.1 URP管线
这里只提及常规要修改的地方,一个是涉及前向渲染器的类或结构体已经弃用,需要使用通用渲染器进行替换,如:ForwardRendererData替换为UniversalRendererData,ForwardRenderer替换为ForwardRenderer。
另一点是新的URP管线下,如果ScriptableRendererFeature实例在它们被ScriptableRenderer分配之前就访问Render Targets,会出现报错:
You can only call cameraColorTarget inside the scope of a ScriptableRenderPass.
针对这种情况,官方给出如下修改样例,在旧版API下:
public override void AddRenderPasses(ScriptableRenderer renderer,
ref RenderingData renderingData)
{
// The target is used before allocation
m_CustomPass.Setup(renderer.cameraColorTarget);
// Letting the renderer know which passes are used before allocation
renderer.EnqueuePass(m_ScriptablePass);
}
在新版API下:
public override void AddRenderPasses(ScriptableRenderer renderer,
ref RenderingData renderingData)
{
// Letting the renderer know which passes are used before allocation
renderer.EnqueuePass(m_ScriptablePass);
}
public override void SetupRenderPasses(ScriptableRenderer renderer,
in RenderingData renderingData)
{
// The target is used after allocation
m_CustomPass.Setup(renderer.cameraColorTarget);
}
其它兼容性改动视项目具体使用情况参考这里 [1]
1.2. Shader变量
Unity 2022中的核心库Shader会引用2019版本中所没有的一些矩阵变量 [2],具体来讲可以在Common.hlsl中添加三个额外的宏定义:
#define UNITY_MATRIX_I_V unity_MatrixInvV
#define UNITY_PREV_MATRIX_M unity_prev_MatrixM
#define UNITY_PREV_MATRIX_I_M unity_prev_MatrixIM
同时在UnityInput.hlsl中添加三个矩阵的声明即可:
float4x4 unity_MatrixInvV;
float4x4 unity_prev_MatrixM;
float4x4 unity_prev_MatrixIM;
1.3. 其它问题
由于项目已经处于线上阶段,对于编辑器制作上的功能基本不再需要,目前采取的策略是,不影响整体发布的基础上,编辑器下改动较大的库先在鸿蒙分支上移除掉,少量地方需要修改的可以进行兼容适配,比如一些库的引用问题,UnityEditor.Experimental.SceneManagement改为UnityEditor.SceneManagement,UnityEditor.Experimental.AssetImporters改为UnityEditor.AssetImporters等,TreeView在旧版本中仅在UnityEditor.IMGUI.Controls库中,现在在UnityEngine.UIElements中也存在同名类,因为在使用时注意要显示声明出处。
运行时代码相关的修改绝大部分是一些弃用接口的简单替换,如Texture2D的Resize替换为Reinitialize,SelectListView中的onSelectionChanged已经弃用,使用selectionChanged来代替,Refresh接口替换为Rebuild等。
最后还有一部分需要读懂逻辑然后进行修改的,比如UGUI库VertexHelper.cs文件中使用的CanvasRenderer接口AddUIVertexStream、SplitUIVertexStream和CreateUIVertexStream不再需要Vector2的UV,而是Vector4,因为可以添加一个适配函数来做衔接。
以上问题逐个解决后,迁移工作基本完成95%以上。在迁移完成之后,本地启动场景下的某实体Z值一直在持续增加,客户端没有逻辑主动调整此值,费解时偶然发现此实体没有挂Camera组件,但是会有UniversalAdditionalCameraData组件,在2019版本下移除Camera组件后不会同时移除此组件,并且运行时也不会由此自动创建新的相机组件,但在新版本引擎下会引发一系列难以排查的问题。
2. 团结引擎鸿蒙化
团结引擎鸿蒙化的文档可以参考这里 [3],在迁移到团结引擎之后,可以将安卓的PlayerSettings相关配置拷贝至Open Harmony下,因为大部分都是相同的,可以避免一些诸如预编译宏等带来的问题,如果遇Library库的问题,常规方法不能解决的,可以尝试删除此文件夹缓存,再重启工程。
鸿蒙平台打包相关的接口使用细节基本与安卓一致,比如文件访问等上层接口的实现,需要使用web请求而不是File原生接口。相关的打包流程依据项目的不同修改也不同,这里不细说了。
在打包鸿蒙平台下的Bundle资源时,会引发宕机问题,查看Editor.log后发现一直在报以下警告,
WARNING: RGBA Compressed xxxx format is not supported, decompressing texture
是由于贴图压缩格式不匹配,导致贴图持续解压而引发了内存溢出,目前这类问题已经和团结引擎的同学核实过了,ETC和ETC2在日志中报解压log的同时会真正解压,会引发OOM,但是ASTC压缩格式只是会报日志,但不会真正解压,所以使用ASTC格式可以正常打出Bundle资源。
除此之外,三方库和其它自主开发的平台插件同样需要在鸿蒙环境重新开发和编译,对于一些常见的开源三方库如何鸿蒙化来说,比如toLua库 [4]等,可以直接参考Gitee [5],这里从准备虚拟机和鸿蒙环境开始到构建具体库的步骤给出的很详细,如果对源码没有改动且想跳过繁琐步骤,打包前可以查看这里检查是否有已经构建好的库 [6]。至于其它的商业库,比如Criware和HybridCLR等需要联系开发商进行鸿蒙支持。
HarmonyOS NEXT Developer Beta2针对所有应用的变更中有一条是需要禁用LuaJit引擎,且鉴于项目使用的toLua接入了protoBuf并做出了一些微调,这里以此为例,详细说明下如何重编自定义toLua库:
- 首先配置编译环境,指定一个目录(如E:/msys64)安装msys64 [7]
- 添加两个路径至path环境变量,E:/msys64/mingw64/bin以及E:/msys64/usr/bin
- E:/msys64目录下找到mingw64.exe并启动
- 执行以下指令安装所需要的库(视具体情况用pacman安装)
pacman -S mingw-w64-x86_64-gcc
pacman -S make
pacman -S cmake
5. 在tolua_runtime目录(即luajit-2.1文件夹所在目录)下,放入build_oh64.sh脚本文件,文件内容如下:
if [ -z ${OHOS_SDK} ]; then
export OHOS_SDK=/e/OpenHarmony/12
fi
dir=./openharmony
rm -rf $dir
mkdir $dir
cd $dir
echo "cmake_minimum_required(VERSION 3.6)
project(tolua)
set(tolua_src ../tolua.c
../int64.c
../uint64.c
../pb.c
../lpeg.c
../struct.c
../cjson/strbuf.c
../cjson/lua_cjson.c
../cjson/fpconv.c
../luasocket/auxiliar.c
../luasocket/buffer.c
../luasocket/except.c
../luasocket/inet.c
../luasocket/io.c
../luasocket/luasocket.c
../luasocket/mime.c
../luasocket/options.c
../luasocket/select.c
../luasocket/tcp.c
../luasocket/timeout.c
../luasocket/udp.c
../luasocket/usocket.c)
add_library(tolua SHARED \${tolua_src})
target_link_libraries(tolua PRIVATE \${CMAKE_CURRENT_SOURCE_DIR}/libluajit.a)
target_include_directories(tolua PRIVATE \${CMAKE_CURRENT_SOURCE_DIR}/../luajit-2.1/src)
target_include_directories(tolua PRIVATE \${CMAKE_CURRENT_SOURCE_DIR}/../cjson)
target_include_directories(tolua PRIVATE \${CMAKE_CURRENT_SOURCE_DIR}/../)
target_compile_options(tolua PRIVATE -O2)
target_compile_options(tolua PRIVATE -O2 -std=gnu99)
" > CMakeLists.txt
cd ..
sed -i '/#if LJ_TARGET_IOS || LJ_TARGET_CONSOLE/c\#if LJ_TARGET_IOS || LJ_TARGET_CONSOLE || defined(__OHOS__)' luajit-2.1/src/lj_arch.h
cd luajit-2.1/src
export dynamic_cc="${OHOS_SDK}/native/llvm/bin/clang --target=aarch64-linux-ohos"
export target_ld="${OHOS_SDK}/native/llvm/bin/clang --target=aarch64-linux-ohos"
export static_cc=${dynamic_cc}
export target_ar="${OHOS_SDK}/native/llvm/bin/llvm-ar rcus 2>/dev/null"
target_strip=${OHOS_SDK}/native/llvm/bin/llvm-strip
make clean
make -j32 HOST_CC="gcc" TARGET_SYS="OHOS" CFLAGS="-fPIC -DLUAJIT_DISABLE_JIT" DYNAMIC_CC="${dynamic_cc}" TARGET_LD="${target_ld}" STATIC_CC="${static_cc}" TARGET_AR="${target_ar}" TARGET_STRIP=${target_strip}
cp ./libluajit.a ../../openharmony/libluajit.a
make clean
unset dynamic_cc target_ld static_cc target_ar target_strip
cd ../../openharmony
BUILD_PATH=../build.OpenHarmony.arm64-v8a
cmake -B${BUILD_PATH} -DOHOS_ARCH="arm64-v8a" -DCMAKE_TOOLCHAIN_FILE=$OHOS_SDK/native/build/cmake/ohos.toolchain.cmake
cmake --build ${BUILD_PATH} --config Release
mkdir -p ../Plugins/OpenHarmony/libs/arm64-v8a/
cp ${BUILD_PATH}/libtolua.so ../Plugins/OpenHarmony/libs/arm64-v8a
6. 注意以上的OHOS_SDK不要给windows下的路径,比如E:/OpenHarmony/12需要写做/e/OpenHarmony/12,因为sdk的toolchains会将冒号识别为分隔符
7. 回到mingw64指令窗口中,cd到tolua_runtime目录下,然后键入./build_oh64.sh指令进行编译即可
3. 鸿蒙工程
团结引擎这边如果要导出鸿蒙工程,需要在Preferences->External Tools中配置三处路径,OpenHarmony SDK、Node.js SDK以及JDK:
- JDK可以直接使用Android的路径:
xxx\TuanjieEditor\2022.3.2t13\Editor\Data\PlaybackEngines\AndroidPlayer\OpenJDK
2. OpenHarmony SDK需要使用DevEco Studio [8]来进行统一管理,具体步骤是:
- 前往File->Settings->OpenHarmony SDK下载SDK11(我这里使用的是11,其它版本的选择因项目及团结版本而异)至指定路径xxx\OpenHamony
- External Tools中的OpenHarmony SDK路径指定为xxx\OpenHarmony\11
3. Node.js SDK可以通过nvm [9]来管理,在下载nvm后,在nvm.exe目录下打开cmd,并通过命令行安装并使用特定版本的NodeJs:
nvm install 18.20.3
nvm use 18.20.3
在配置完路径后便可以直接导出鸿蒙工程,具体编辑器步骤与安卓相同,勾选上Export Project后点击Export即可,使用脚本进行导出也大同小异,同时,导出工程前一般会编译AssetBundle资源,编译后的资源可以通过脚本自动挪至鸿蒙工程中的/entry/src/main/resources/rawfile/Data/StreamingAssets/bundles路径下或者手动拷贝。
使用DevEco Studio直接打开导出的鸿蒙工程,然后需要执行以下步骤后才可进行Hap包编译:
- 前往View -> Tools Window -> Migrate Assistant,在弹出的框中点击Migrate到5.0.0版本 [10]
- /entry/src/main/module.json5中的srcEntrance改为 srcEntry
- 工程下的build-profile.json5中的products下修改sdk版本并添加runtimeOS,样例如下:
"products": [
{
"name": "default",
"signingConfig": "default",
"runtimeOS": "HarmonyOS",
"compatibleSdkVersion": "4.1.0(11)",
"targetSdkVersion": "4.1.0(11)"
}
]
4. 连接鸿蒙真机
5. File -> Project Structure -> Signing Configs -> 添加签名
6. 构建
当然以上步骤比较繁琐,为了能够投入生产环境中使用,手动修改的方式将不再可取,因此可以在团结引擎中加入导出鸿蒙工程后的处理流程脚本,如下面所示:
#if UNITY_EDITOR
using System.IO;
using Unity.Plastic.Newtonsoft.Json.Linq;
using UnityEditor;
using UnityEditor.Build;
using UnityEditor.Build.Reporting;
public class OpenHarmonyProjectBuildPostProcessor : IPostprocessBuildWithReport
{
public int callbackOrder
{
get { return 1; }
}
public void OnPostprocessBuild(BuildReport report)
{
if (report.summary.platform == BuildTarget.OpenHarmony)
{
MigrateProject(report.summary.outputPath);
}
}
//修改build-profile.json5文件
private void ModifyBuildProfileJson(string projectPath) {
string buildProfileJsonFilePath = projectPath + "/build-profile.json5";
if (File.Exists(buildProfileJsonFilePath)) {
string content = File.ReadAllText(buildProfileJsonFilePath);
JObject jsonObj = JObject.Parse(content);
JArray productsArray = (JArray)jsonObj["app"]["products"];
foreach (JObject product in productsArray)
{
if (product["name"].ToString() == "default")
{
product["compileSdkVersion"] = "5.0.0(12)";
product["compatibleSdkVersion"] = "5.0.0(12)";
product.Add("runtimeOS", "HarmonyOS");
break;
}
}
File.WriteAllText(buildProfileJsonFilePath, jsonObj.ToString());
}
}
//移除冗余Hvigor文件,修改hvigor-config配置文件
private void ModifyHvigorRelatedFiles(string projectPath) {
string hvigorFile01ToDelete = projectPath + "/hvigor/hvigor-wrapper.js";
if (File.Exists(hvigorFile01ToDelete))
{
File.Delete(hvigorFile01ToDelete);
}
string hvigorFile02ToDelete = projectPath + "/hvigorw";
if (File.Exists(hvigorFile02ToDelete))
{
File.Delete(hvigorFile02ToDelete);
}
string hvigorFile03ToDelete = projectPath + "/hvigorw.bat";
if (File.Exists(hvigorFile03ToDelete))
{
File.Delete(hvigorFile03ToDelete);
}
string hvigorConfigFilePath = projectPath + "/hvigor/hvigor-config.json5";
if (File.Exists(hvigorConfigFilePath))
{
string content = File.ReadAllText(hvigorConfigFilePath);
JObject jsonObj = JObject.Parse(content);
jsonObj.Remove("hvigorVersion");
jsonObj.Add("modelVersion", "5.0.0");
jsonObj["dependencies"] = new JObject();
File.WriteAllText(hvigorConfigFilePath, jsonObj.ToString());
}
}
//修改OHPackage文件
private void ModifyOHPackageRelatedFiles(string projectPath) {
string ohPackageFilePath = projectPath + "/oh-package.json5";
if (File.Exists(ohPackageFilePath))
{
string content = File.ReadAllText(ohPackageFilePath);
JObject jsonObj = JObject.Parse(content);
jsonObj.Add("modelVersion", "5.0.0");
File.WriteAllText(ohPackageFilePath, jsonObj.ToString());
}
}
//简单示例 修改entry下的oh-package配置文件
private void MigrateProject(string projectPath) {
ModifyBuildProfileJson(projectPath);
ModifyHvigorRelatedFiles(projectPath);
ModifyOHPackageRelatedFiles(projectPath);
}
}
#endif
鸿蒙出包过程所涉及工具及库版本可参考:
- Tuanjie Editor 1.2.0
- DevEco Studio 5.0.3.403
- OpenHarmony SDK 11
- NodeJs v18.20.3
4. 桥接层
在团结引擎中会避免不了的去和原生接口交互,那么如何做到这点,搭建桥接层就是我们首先需要面临的,可参考如下具体步骤:
- 在引擎的Plugins/OpenHarmony目录下(或者也可以创建子目录,这个子目录会同步到鸿蒙工程中的entry/src/main/ets目录下)创建一个后缀为.tslib或者.etslib的文件,比如命名为OHBridge.etslib,其实这里在导出为鸿蒙工程后,此文件将会是后缀为*.ets的常规脚本文件
- 此文件中可写入你调用一些原生库接口的封装类或函数,然后最后加入比较关键的部分:
//Register[文件名称]
export function RegisterOHBridge() {
let register : Record<string, Object> = {};
register["GameBridgeInstance"] = GameBridge.getInstance();
return register;
}
3. 在团结引擎的脚本文件中这样创建你刚才导出的鸿蒙对象:
OpenHarmonyJSObject gameBridgeInstance = new OpenHarmonyJSObject("GameBridgeInstance");
以上是一个简单的示例,但是实际情况远比这个要复杂,比如此桥接层运行在C#的worker线程上,鉴于鸿蒙架构下线程之间内存不共享,所以如果原生hap库的功能设计UI,那么会在主线程上运行,这样桥接层无法直接调用原生hap库的接口,这个时候需要再搭建一个桥,进行线程间的交互,比如使用worker机制等,这里提供一些简单示例来阐述这个潜在问题:
- 我们首先写一个线程交互的桥接文件
//OHWorkerBridge.ets
import worker, { MessageEvents, ThreadWorkerGlobalScope } from '@ohos.worker';
import { TuanjieLog } from '../common/TuanjieLog';
let messageCallback: (type: string, status: string, data: string) => void;
let workerPort: ThreadWorkerGlobalScope;
export class OHWorkerBridge {
static Game_SomeNativeInterfaceCall: string = "Game_SomeNativeInterfaceCall";
static CallBackInitialized: Boolean = false;
static MessageInitialized: Boolean = false;
private static BindCallback(callback: (type: string, status: string, data: string) => void) {
if (!OHWorkerBridge.CallBackInitialized) {
globalThis.workerPort.onmessage = (e : MessageEvents) => {
if (e.data.type != "OnSensor") {
TuanjieLog.info(`OHWorkerBridge Worker Listener Receive Message : ${JSON.stringify(e)}`);
}
const msg : Record<string, Record<string, string>> = JSON.parse(JSON.stringify(e));
if (msg['data']['type'] == 'someType') {
messageCallback(msg['data']['data_type'], "Success", msg['data']['data']);
}
}
OHWorkerBridge.CallBackInitialized = true;
}
if (messageCallback === callback) {
return;
}
messageCallback = callback;
globalThis.messageCallback = messageCallback;
}
public static InitMessageBind(worker: worker.ThreadWorker) {
if (OHWorkerBridge.MessageInitialized == true) {
return;
}
worker.addEventListener("message", async (e) => {
let msg : Record<string, Record<string, string>> = JSON.parse(JSON.stringify(e));
switch (msg['data']['type']) {
case OHWorkerBridge.Game_SomeNativeInterfaceCall:
{
//主线程创建的一些对象接口调用...
//如果有回调返给worker线程数据,可以调用下面的
worker.postMessage({
'type': 'someType',
'data_type': 'someDataType',
'data': 'someDataHere'
});
break;
}
}
});
OHWorkerBridge.MessageInitialized = true;
}
public static SomeNativeInterfaceCall(callback: (type: string, status: string, data: string) => void) {
OHWorkerBridge.BindCallback(callback);
// 向主线程发送消息
globalThis.workerPort.postMessage({
'type': OHWorkerBridge.Game_SomeNativeInterfaceCall,
'data': ""
});
}
}
2. 然后在TuanjiePlayerAbility.ets/ts中的onCreate生命周期函数最后调用
OHWorkerBridge.InitMessageBind(TuanjieMainWorker.getInstance());
3. 在你的OHBridge.ets中可以直接调用SomeNativeInterfaceCall静态接口来实现目的
OHWorkerBridge.SomeNativeInterfaceCall((type: string, status: string, data: string) => {
//do something...
})
5. 相关问题
- 在导出鸿蒙工程时,如果遇到Internal build system error. BuildProgram exited with code 1.的报错,可以前往查看Tuanjie的Editor.log,一种可能引发此报错的情况是OpenHarmonySDK目录下未包含native/oh-uni-package.json文件,前往DevEco Studio将native包添加下载后问题可解决
- 在导出鸿蒙工程时,如果遇到CommandInvokationFailure: Unable to sync. pnpm.cmd install execute failed的报错,可能是由于NodeJs的版本过高导致,可适当降低版本
- 编译鸿蒙工程时,如果遇到ArkTS相关的问题,可适当提高SDK版本和NodeJs版本(比如提至SDK11和NodeJs18.20.3),同时注意使用DevEco Studio的5.0.x版本
[2] Upgrade from Unity 2019 to 2022
[3] TuanjieEngine-OH
[4] toLua源码
[5] Linux环境编译三方库
[6] Gitee - Lycium
[7] msys2
[9] nvm管理
[10] 一体化工程迁移
欢迎加入我们!
感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com
更多推荐



所有评论(0)