Unity集成puerTS与TypeScript实战:从零搭建热更新开发环境
1. 为什么选择puerTS与TypeScript组合
如果你正在寻找一种高效的Unity热更新方案,puerTS+TypeScript的组合绝对值得考虑。我在多个商业项目中实际使用过这套方案,最大的感受就是开发效率的提升和后期维护的省心。
puerTS是腾讯开源的TypeScript运行时环境,它完美解决了Unity热更新中的几个痛点:首先,TypeScript作为JavaScript的超集,拥有完整的类型系统,这在大型项目中特别有用;其次,puerTS提供了与Unity引擎深度集成的能力,可以直接调用Unity的API;最重要的是,这套方案完全开源且经过腾讯内部多个项目的验证,稳定性有保障。
相比传统的Lua方案,TypeScript的开发体验要好太多。我至今记得第一次在Unity里用TypeScript调用GameObject时的惊喜 - 代码自动补全、类型检查、接口提示一应俱全,就像在用C#开发一样流畅。而且TypeScript的生态非常丰富,npm上的海量模块都可以直接使用。
2. 环境准备与基础配置
2.1 必备工具安装
在开始之前,我们需要准备以下环境:
- Unity 2021.3.25f1或更高版本(实测2022版本也完全兼容)
- Node.js v16.13.1(建议使用nvm管理多版本)
- 任意TypeScript开发工具(VSCode是首选)
我推荐使用nvm来管理Node.js版本,这样可以避免全局安装带来的冲突。安装好Node.js后,记得检查npm版本:
node -v npm -v2.2 puerTS插件集成
从GitHub下载最新版的puerTS发布包:
https://github.com/Tencent/puerts/releases解压后将puerts文件夹直接拖入Unity项目的Assets目录下。这里有个小技巧:我习惯在Assets下创建Plugins目录,然后把puerts放进去,这样结构更清晰。导入完成后,Unity会自动开始编译插件。
第一次导入时可能会遇到一些编译错误,通常是平台兼容性问题。我遇到最多的是Android平台的兼容性警告,解决方法是在Player Settings中明确指定目标架构。
3. TypeScript项目初始化
3.1 创建TypeScript项目
在Assets目录下创建TsProject文件夹(名字可以自定),然后初始化npm项目:
mkdir TsProject cd TsProject npm init -y npm install typescript --save-dev接下来创建tsconfig.json配置文件,这是TypeScript项目的核心:
{ "compilerOptions": { "target": "esnext", "module": "commonjs", "sourceMap": true, "noImplicitAny": true, "typeRoots": [ "../Puerts/Typing", "../Gen/Typing", "./node_modules/@types" ], "outDir": "output" } }这里有几个关键点需要注意:
- target设置为esnext可以确保使用最新的JS特性
- module必须使用commonjs,这是puerTS的要求
- typeRoots中必须包含Puerts/Typing路径,这样才能获得Unity API的类型提示
3.2 配置构建脚本
修改package.json,添加build和postbuild脚本:
{ "scripts": { "build": "tsc -p tsconfig.json", "postbuild": "node copyJsFile.js output ../Resources" } }copyJsFile.js这个脚本负责将编译后的JS文件复制到Unity的Resources目录,并添加.txt后缀。这是puerTS的特殊要求 - 所有JS资源都必须以.txt后缀存储。脚本内容可以从官方demo中获取。
4. 编写第一个TypeScript脚本
4.1 基础示例代码
在TsProject目录下创建main.ts文件:
import { UnityEngine } from 'csharp'; UnityEngine.Debug.Log('Hello from TypeScript!'); const cube = new UnityEngine.GameObject("TypeScriptCube"); cube.transform.position = new UnityEngine.Vector3(0, 0, 0); const renderer = cube.AddComponent(UnityEngine.MeshRenderer); const material = new UnityEngine.Material(UnityEngine.Shader.Find("Standard")); renderer.material = material;这段代码展示了几个重要特性:
- 直接从csharp模块导入UnityEngine命名空间
- 完整的类型检查和自动补全
- 与C#几乎相同的API调用方式
4.2 处理资源加载
TypeScript中加载Unity资源也很简单:
const prefab = UnityEngine.Resources.Load("Prefabs/Character"); const instance = UnityEngine.Object.Instantiate(prefab) as UnityEngine.GameObject;注意这里的类型断言是必要的,因为Resources.Load返回的是UnityEngine.Object基类。
5. Unity与TypeScript的交互
5.1 C#调用TypeScript
创建Require.cs脚本挂载到场景中的任意GameObject上:
using UnityEngine; using Puerts; public class Require : MonoBehaviour { JsEnv jsEnv; void Start() { jsEnv = new JsEnv(); jsEnv.Eval(@"require('main')"); } void OnDestroy() { jsEnv.Dispose(); } }这个简单的脚本创建了一个JavaScript执行环境,并加载了我们编写的main模块。
5.2 TypeScript调用C#
首先在C#中定义可被调用的方法:
public class PlayerController : MonoBehaviour { public void TakeDamage(int damage) { // 处理伤害逻辑 } }然后在TypeScript中可以这样调用:
const player = UnityEngine.GameObject.Find("Player").GetComponent(PlayerController); player.TakeDamage(10);6. 调试与优化技巧
6.1 调试TypeScript代码
使用sourceMap配置后,可以在Chrome开发者工具中调试TypeScript源码。首先确保tsconfig.json中sourceMap为true,然后在Chrome中打开about:inspect页面,找到Unity进程进行调试。
6.2 性能优化建议
- 避免频繁创建JsEnv实例,尽量复用
- 使用JsEnv的Using方法管理C#对象生命周期
- 对于高频调用的方法,考虑使用Static Wrappers
- 合理使用Promise处理异步操作
我在项目中总结出一个最佳实践:为每个场景创建一个全局的JsEnv,通过自定义消息系统在TypeScript模块间通信,这样可以避免重复初始化的开销。
7. 常见问题解决方案
7.1 类型定义缺失问题
有时会遇到某些Unity API没有类型定义的情况,可以手动扩展:
declare module 'csharp' { namespace UnityEngine { class MyCustomComponent extends MonoBehaviour { static Method(): void; } } }7.2 热重载配置
实现真正的热重载需要一些额外配置:
- 在开发模式下禁用JsEnv的缓存
- 监听文件变化自动重新加载模块
- 使用webpack的watch模式
一个简单的热重载实现:
#if UNITY_EDITOR UnityEditor.EditorApplication.update += () => { if(jsEnv != null) jsEnv.Tick(); }; #endif8. 项目结构最佳实践
经过多个项目的实践,我总结出以下推荐结构:
Assets/ ├── Plugins/ │ └── Puerts/ # puerTS运行时 ├── Resources/ │ └── Scripts/ # 编译后的JS资源 ├── Scripts/ │ ├── Core/ # C#核心代码 │ └── Bridge/ # C#与TS交互层 └── TsProject/ ├── src/ # TS源代码 ├── libs/ # 第三方TS库 ├── typings/ # 自定义类型定义 └── config/ # 构建配置这种结构清晰分离了C#和TypeScript代码,同时保持了良好的可维护性。特别建议将业务逻辑尽量放在TypeScript侧,C#只负责核心系统和平台相关功能。
