在 Unity 中使用 Typescript 脚本
本文介绍的插件 duktape-unity 可以使你在 Unity3D 中使用 javascript/typescript 来写脚本. 这里说的脚本是指可以在包括iOS上动态执行的脚本. 目前主流的选择是 lua 和 ILRuntime(C#), 都是很成熟的方案. typescript 或许可以是介于两者之间的一种选择, 即有动态脚本语言的特性, 又有强类型的辅助.
概况
duktape-unity 的使用方法与 slua/ulua 等是类似的, 通过生成绑定代码并对部分值类型进行特定优化来提高交互效率. 并针对 js 的使用习惯, 做了一些 js 风格的封装, 例如事件/委托的封装, 有两种方式在 js 中将 js 函数注册到 C# 事件/委托上 (这里均以 ts 为例):
// 方法一, 直接注册 js 函数
UnityEngine.Application.lowMemory.on(() => {
console.log("low memory");
});
// 方法二, 构造一个 Delegate 对象进行注册, 这种方式 Delegate 对象本身就是一个 Dispatcher, 可以自己再 on/off 多个响应者
let delegate = new DuktapeJS.Delegate0();
delegate.on(this, () => {
console.log("low memory")
});
delegate.on(this, () => {
// ...
});
UnityEngine.Application.lowMemory.on(delegate);
而且, 得益于自动生成的 d.ts 声明的帮助, 写脚本时是有类型验证的.
集成
Assets/Duktape 是插件主要代码, 直接放到工程目录(可以是子目录)中即可. Assets/DuktapeExtra 是插件辅助代码, 可选, 目前主要作用是将 js stacktrack 转换到 typescript stacktrack, 在调试过程中有较大帮助.
接着, 在项目根目录编辑生成绑定代码的配置文件 ./duktape.json:
{
"outDir": "Assets/Generated", // 生成csharp绑定代码的目录
"typescriptDir": "Assets/Generated", // 生成 d.ts 声明的目录
// rootpath of ts/js project
"workspace": "",
"logPath": "Temp/duktape.log", // 生成csharp绑定代码过程中产生的日志文件
// auto, cr, lf, crlf
"newLineStyle": "auto", // 生成代码的换行符
// 隐式生成 (在下列模块中定义的类型中未明确禁止生成的类都将生成绑定)
"implicitAssemblies": [
"UnityEngine",
"UnityEngine.CoreModule",
"UnityEngine.UI",
"UnityEngine.UIModule",
"UnityEngine.TextRenderingModule",
"UnityEngine.AnimationModule"
],
// 显式生成 (在下列模块中定义的类型必须明确指定需要生成绑定的类型)
"explicitAssemblies": [
"Assembly-CSharp"
],
// 禁止生成绑定的类型全名前缀
"typePrefixBlacklist": [
"JetBrains.",
"Unity.Collections.",
"Unity.Jobs.",
"Unity.Profiling.",
"UnityEditor.",
"UnityEditorInternal.",
"UnityEngineInternal.",
"UnityEditor.Experimental.",
"UnityEngine.Experimental.",
"Unity.IO.LowLevel.",
"Unity.Burst.",
// more types ...
"UnityEngine.Assertions."
],
// 生成绑定代码所在的命名空间
"ns": "DuktapeJS",
// 生成的绑定代码中使用的缩进符
"tab": " "
}
然后执行菜单 Duktape/Generate Bindings 生成绑定代码.
generate_bindings.png
编写 C# 调用 js 的启动代码:
// Launcher.cs
public class Launcher : MonoBehaviour, IDuktapeListener
{
public bool debuggerSupport;
public string entryScript = "main";
private DuktapeVM _vm;
public void OnBinded(DuktapeVM vm, int numRegs) { }
public void OnBindingError(DuktapeVM vm, Type type) { }
public void OnProgress(DuktapeVM vm, int step, int total) { }
public void OnTypesBinding(DuktapeVM vm) { }
public void OnLoaded(DuktapeVM vm)
{
_vm.AddSearchPath("Assets/Scripts/out");
if (debuggerSupport)
{
DuktapeDebugger.CreateDebugger(_vm);
}
_vm.EvalMain(entryScript);
}
void Awake()
{
_vm = new DuktapeVM();
_vm.Initialize(this);
}
}
接着, 就可以开始愉快的用 typescript 写脚本了. 上一步生成绑定代码时, 会自动生成 C# 类对应的 d.ts 申明文件, 因此写脚本的体验接近 C#, 定义跳转/引用查询/重命名等功能一应俱全.
// main.ts
import { MyBridge } from "./app";
console.log("hello, world!");
(function () {
let go = new UnityEngine.GameObject("_jsgo");
let bridge = go.AddComponent(DuktapeJS.Bridge);
bridge.SetBridge(new MyBridge());
})();
完成 ts 脚本的编写, 用 tsc 编译出 js 结果就可以运行了. 这步可以利用 tsc watch 实时监听脚本的修改并自动完成编译, 增量编译的速度极快, 几乎无感.
tsc_watch.png
运行项目, 就可以看到脚本中调用 console.log 产生的日志输出:
console_log.png
调试
到这, 经常用日志调试法调试的伙伴们要崩溃了, 这日志根本看不出来是从哪一行代码产生的啊, 这可怎么调试. 有办法, 只需要在脚本中调用
enableStacktrace(true);
那么所有脚本产生的日志输出就会自动带上脚本调用栈. 如图所示:
console_log_with_stacktrace.png
当然日志调试法不是万能的, 更多时候还是需要进行断点调试. 首先在代码中调用
DuktapeDebugger.CreateDebugger(vm) 即可启动调试服务.
在 vscode 端需要安装插件 Duktape Debugger (HaroldBrenes), vscode 插件管理器中目前版本为 0.5.6, 可以用于 js 脚本的调试, 对 ts 存在一些bug, 可以使用笔者修改过的版本 duk-debug-0.5.6.vsix 在插件管理器中手工安装即可.
在 vscode 中正确配置 launch.json 后, 即可进行远程调试. 首先要启动游戏, 然后启动 Attach 调试, 调试是可以反复多次的, 游戏运行过程中仍然可以再次 Attach.
命中断点后, 就可以安逸查看各个变量值的情况了.
breakpoint.png有一点需要注意的, 因为脚本是在主线程执行的, 所以调试过程在没有打开 Run in background 选项的情况下可能无法流畅进行, 建议开启该选项再进行断点调试.
run_in_background.png
基于 duktape-unity 在 Unity 项目中使用 typescript 作为脚本的基本流程就是这样. 代码在这里:
https://github.com/ialex32x/duktape-framework
上述代码 Unity 2018.3.5+可以运行, 更低版本没有测试过.
后续有时间会继续更新代码, 演示 websocket, protobuf, 热更新, 界面框架等的使用方法.
目前项目还不成熟, 如果您对使用 typescript/javascript 写脚本感兴趣, 不妨尝试一下, 给个星星. 有问题建议, 欢迎拍砖, issue. 期待您的关注 :)