寫在前面
經過一段時間的改進, 後來用 QuickJS 代替了 duktape 另外實作了一個 unity-jsb, 此項目修正了若幹 unity-duktape 實作上的問題和BUG, 且運作效率更高.
本文介紹的插件 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 生成綁定代碼.

編寫 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 實時監聽腳本的修改并自動完成編譯, 增量編譯的速度極快, 幾乎無感.
運作項目, 就可以看到腳本中調用 console.log 産生的日志輸出:
調試
到這, 經常用日志調試法調試的夥伴們要崩潰了, 這日志根本看不出來是從哪一行代碼産生的啊, 這可怎麼調試. 有辦法, 隻需要在腳本中調用
那麼所有腳本産生的日志輸出就會自動帶上腳本調用棧. 如圖所示:
當然日志調試法不是萬能的, 更多時候還是需要進行斷點調試. 首先在代碼中調用
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.
命中斷點後, 就可以安逸檢視各個變量值的情況了.
有一點需要注意的, 因為腳本是在主線程執行的, 是以調試過程在沒有打開 Run in background 選項的情況下可能無法流暢進行, 建議開啟該選項再進行斷點調試.
基于 duktape-unity 在 Unity 項目中使用 typescript 作為腳本的基本流程就是這樣. 代碼在這裡:
https://github.com/ialex32x/duktape-framework
上述代碼 Unity 2018.3.5+可以運作, 更低版本沒有測試過.
後續有時間會繼續更新代碼, 示範 websocket, protobuf, 熱更新, 界面架構等的使用方法.
目前項目還不成熟, 如果您對使用 typescript/javascript 寫腳本感興趣, 不妨嘗試一下, 給個星星. 有問題建議, 歡迎拍磚, issue. 期待您的關注 😃