記錄一下普洱TS的安裝而芥、代碼打包潦闲、調(diào)試與FairyGUI集成等垦沉,以及使用過程中遇到的問題辈讶。
基本使用
安裝
按照官方手冊,拷貝puerts/unity/Assets下的所有內(nèi)容到您項(xiàng)目的Assets目錄下犀被,在
release中下載插件并解壓覆蓋到Plugins目錄椅您,插件有不同的js引擎版本,不知道選什么的話建議用v8寡键。
Unity示例 在另一個(gè)倉庫掀泳,是獨(dú)立的Unity工程,看完里面的示例基本上就能明白大致使用方法了西轩。
Hello Kitty
按照國際慣例员舵,先來寫個(gè)Hello Kitty。
配置
如果僅安裝了Puerts藕畔,沒有拷貝示例代碼马僻,則需要在Unity中做一些準(zhǔn)備工作。為了快速看效果注服,只做一個(gè)簡單的配置韭邓,Assets下新建Editor目錄,其下新建PuertsConfig.cs:
PuertsConfig.cs
[Configure]
public class PuertsConfig
{
[Binding]
static IEnumerable<Type> Bindings =>
new List<Type>()
{
typeof(Debug),
typeof(Vector3),
typeof(List<int>),
typeof(Dictionary<string, List<int>>),
typeof(Time),
typeof(Transform),
typeof(Component),
typeof(GameObject),
typeof(UnityEngine.Object),
typeof(Delegate),
typeof(System.Object),
typeof(Type),
typeof(ParticleSystem),
typeof(Canvas),
typeof(RenderMode),
typeof(Behaviour),
typeof(MonoBehaviour),
};
}
執(zhí)行菜單Puerts->Generate index.d.ts:
將會(huì)生成對應(yīng)的類型聲明文件:
TyepeScript工程
在項(xiàng)目根目錄下新建一個(gè)TsProject文件夾(官方示例中為TsProj)溶弟,作為TypeScript工程目錄仍秤。
用vscode打開它,在這之前請確保已經(jīng)安裝好了vscode可很、node诗力、npm、typescript我抠,新建tsconfig.json苇本,加入如下配置:
tsconfig.json
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"jsx": "react",
"sourceMap": true,
"noImplicitAny": true,
"typeRoots": [
"../Assets/Puerts/Typing",
"../Assets/Gen/Typing",
"./node_modules/@types"
],
"outDir": "output"
}
}
typeRoots
中指定了C#側(cè)的類型聲明文件目錄,如果你的ts工程目錄或者Puerts目錄有變更菜拓,這里需要修改正確瓣窄。
outDir
指定了編譯后js文件的輸出目錄。其他的配置沒什么好說的纳鼎,可以根據(jù)個(gè)人喜好調(diào)整俺夕,更多配置項(xiàng)說明可以查看TypeScript的官方文檔。
新建package.json贱鄙,加入如下配置:
package.json
{
"name": "tsproj",
"version": "1.0.0",
"description": "ts project",
"scripts": {
"build": "tsc -p tsconfig.json",
"postbuild": "node copyJsFile.js output ../Assets/Resources"
}
}
這兩個(gè)文件也可以用npm init
與tsc --init
創(chuàng)建劝贸。
把官方示例的TsProj文件夾里的copyJsFile.js拷貝過來,新建index.ts逗宁,編寫Hello World:
index.ts
console.log('Hello Kitty!');
終端里運(yùn)行:
npm run build
可以看到output文件夾輸出了編譯后的index.js文件與map文件:
并且這些文件被拷貝到了Recources目錄下:
執(zhí)行
Scripts下新建JsManager.cs映九,編寫執(zhí)行代碼:
JsManager.cs
namespace LearnPuerts
{
public class JsManager : MonoBehaviour
{
private static JsEnv jsEnv;
private void Awake()
{
jsEnv ??= new JsEnv(new DefaultLoader());
jsEnv.Eval("require('index');");
}
private void Update()
{
jsEnv.Tick();
}
private void OnDestroy()
{
jsEnv.Dispose();
}
}
}
腳本掛到場景中,運(yùn)行即可看到效果:
打包與調(diào)試
打包
在凍手之前瞎颗,先看看默認(rèn)的build都干了些什么:
首先tsc編譯件甥,文件輸出到output文件夾下捌议,然后執(zhí)行copyJsFile.js將文件拷貝到了Assets/Resources目錄下。
那么打包過程依葫蘆畫瓢即可引有,先打包瓣颅,再拷貝。官方說明中用的是webpack譬正,個(gè)人更習(xí)慣用esbuild宫补,也差不了太多。
先把esbuild裝好导帝,終端里執(zhí)行:
npm install esbuild --save-dev
拷貝過程懶得自己寫了,直接用copyJsFile.js穿铆,修改它的代碼您单,導(dǎo)出拷貝方法:
copyJsFile.js
// if (process.argv.length == 4) {
// copyFolderRecursiveSync(process.argv[2], process.argv[3]);
// } else {
// console.error('invalid arguments');
// }
exports.copyFolder = copyFolderRecursiveSync
新建build.js,加入相應(yīng)依賴荞雏,指定輸出目錄與拷貝目標(biāo)目錄:
build.js
var copyFolder = require('./copyJsFile').copyFolder;
var outputFolder = 'output';
var targetFolder = '../Assets/Resources';
編寫打包配置:
build.js
// https://esbuild.github.io/api/#build-api
var options = {
bundle: true,
entryPoints: ["index.ts"],
incremental: true,
minify: process.env.NODE_ENV === "production",
outfile: outputFolder + "/bundle.js",
platform: "node",
tsconfig: "./tsconfig.json",
sourcemap: process.env.NODE_ENV === "production" ? false : true,
external: ['csharp', 'puerts', 'path', 'fs'],
treeShaking: true,
logLevel: 'error'
};
根據(jù)說明虐秦,csharp、puerts凤优、path悦陋、fs在打包時(shí)需要排除,其他配置可以根據(jù)個(gè)人需求調(diào)整筑辨。
同時(shí)希望打包支持watch俺驶,這樣ts代碼有改動(dòng)就能同步更新輸出文件,通過獲取命令行參數(shù)棍辕,判斷當(dāng)前是否為watch模式:
build.js
var watchMode = false;
for (let i = 2; i < process.argv.length; i++) {
if (process.argv[i] == 'watch') {
watchMode = true;
break;
}
}
如果為watch模式暮现,則增加對應(yīng)watch配置,在Rebuild時(shí)將輸出文件拷貝到目標(biāo)目錄下:
if (watchMode) {
options.watch = {
onRebuild(error, result) {
if (error) {
console.error('watch build failed:', error);
} else {
copyFolder(outputFolder, targetFolder);
console.log('watch build succeeded:', result);
}
}
}
} else if (process.env.NODE_ENV === "production") {
// 正式打包時(shí)將刪除輸出目錄下所有文件
var fs = require('fs');
var path = require('path');
fs.rmSync(path.dirname(options.outfile), { recursive: true, force: true })
}
最后執(zhí)行:
require('esbuild').build(options)
.then(() => {
copyFolder(outputFolder, targetFolder);
})
.then(() => {
if (watchMode)
console.log('??Watching...');
else {
console.log('??Build finished.');
process.exit(0);
}
});
build.js寫完了楚昭,接下來修改package.json:
...
"scripts": {
"build-product": "cross-env NODE_ENV=production node build.js",
"build": "node build.js",
"watch": "node build.js watch"
},
...
記得把cross-env裝一下:
npm install cross-env --save-dev
隨便寫點(diǎn)東西栖袋,運(yùn)行npm run watch
或npm run build
,可以看到打好包的bundle.js:
記得修改執(zhí)行處的文件名:
JsManager.cs
private void Awake()
{
jsEnv ??= new JsEnv(new DefaultLoader());
jsEnv.Eval("require('bundle');");
}
調(diào)試
調(diào)試可以參考官方文檔抚太,按文檔配置一遍塘幅,Unity中運(yùn)行后,再在vscode中啟動(dòng)調(diào)試器即可尿贫。這里記錄一些我在瞎搞過程中遇到的問題电媳。
調(diào)試器連不上
檢查launch.json中的端口是否與C#代碼中的一致,并且端口未被占用庆亡,OnDestroy中需要調(diào)用jsEnv.Dispose()銷毀匆背,避免退出運(yùn)行后端口依然處于占用狀態(tài)。
斷點(diǎn)無效
斷點(diǎn)為灰色身冀,并提示“Unbound breakpoint”:
這種情況一般是source map出了問題钝尸,可以從這幾個(gè)方面檢查:
- tsconfig.json里有沒有開啟source map
- 打包代碼(build.js)里有沒有開啟source map
- source map文件生成了沒有
- source map文件中的源文件路徑是否正確(一般沒問題)
- C#中是否指定了正確的js輸出目錄
- 加載Resources子目錄下的js文件時(shí)括享,js輸出目錄要保持同樣的結(jié)構(gòu)
對于第六點(diǎn),比如js文件不是拷貝到Resources根目錄珍促,而是拷貝到Resources/tsbuild目錄中:
jsEnv.Eval("require('tsbuild/bundle');");
那么需要讓輸出目錄也保持這個(gè)結(jié)構(gòu):
不過一般不會(huì)直接從Resources下加載铃辖,用Addressable或AssetBundle的情況比較多。
如果出現(xiàn)程序運(yùn)行得太快猪叙,有些斷點(diǎn)沒進(jìn)的情況娇斩,并已使用了jsEnv的等待調(diào)試器連接,那么可以嘗試在launch.json中開啟pauseForSourceMap穴翩。
Source Map Support
在index.ts中報(bào)個(gè)異常試試:
JSON.parse('aa');
并不能追蹤到源碼的報(bào)錯(cuò)位置:
在官方faq文檔中有解決方法犬第,使用source-map-support。通常只需要require之后install就行芒帕,但由于source-map-support是一個(gè)nodejs模塊歉嗓,它引用到了node的path與fs,其他js引擎中沒有這兩個(gè)模塊背蟆,所以需要按照文檔中將它們改為C# System.IO的實(shí)現(xiàn)鉴分。
如果按文檔做了一遍還是不行的話,可以嘗試修改source map文件的獲取過程带膀,在install中加入自定義的處理邏輯:
// require('source-map-support').install();
require('source-map-support').install({
retrieveSourceMap: function (source: string) {
if (source.endsWith('bundle.js')) {
let mapFile = csharp.System.IO.Path.Combine(csharp.UnityEngine.Application.dataPath, '../TsProject/output/bundle.js.map');
if (csharp.System.IO.File.Exists(mapFile)) {
return {
url: source,
map: csharp.System.IO.File.ReadAllText(mapFile)
};
}
}
return null;
}
});
可以追蹤到報(bào)錯(cuò)位置:
FairyGUI
FairyGUI官方有Puerts的使用說明志珍,按文檔搞就完事了。這里主要介紹一個(gè)FairyGUI Puerts插件垛叨,可以直接生成TypeScript的UI代碼伦糯,喜歡的話請給作者一個(gè)Star。
首先按官方的使用說明在Unity中安裝FairyGUI SDK嗽元,并做好相關(guān)配置舔株,然后隨便建個(gè)UI工程,目錄與Assets还棱、TsProject同級:
將插件倉庫克隆到UiProject下的plugins目錄载慈,重啟FairyGUI編輯器,可以看到新增的插件:
發(fā)布設(shè)置中設(shè)置發(fā)布路徑:
包設(shè)置中記得勾選“為本包生成代碼”:
發(fā)布即可看到生成的UI代碼:
生成的UI代碼放在發(fā)布路徑的包名文件夾下珍手,比如這里包名為DefaultPackage办铡。
然后就可以使用了:
index.ts
import { FairyGUI } from 'csharp';
import UI_Main from './src/gen/ui/DefaultPackage/UI_Main';
import { bind } from './src/gen/ui/DefaultPackage/fairygui';
// 加載包
FairyGUI.UIPackage.AddPackage('fgui/DefaultPackage');
// 繼承生成的組件類
class UIMain extends UI_Main {
protected override onConstruct(): void {
super.onConstruct();
this.m_guguButton.onClick.Add(() => {
this.m_guguText.text += '咕';
});
}
}
// 綁定到FairyGUI
bind(UIMain);
// 創(chuàng)建實(shí)例
let uiMain = UIMain.createInstance<UIMain>();
// 設(shè)置設(shè)計(jì)分辨率
FairyGUI.GRoot.inst.SetContentScaleFactor(800, 600);
// 添加到UI
FairyGUI.GRoot.inst.AddChild(uiMain);
這里定義一個(gè)子類UIMain繼承生成的UI_Main,在點(diǎn)擊按鈕時(shí)添加一個(gè)”咕“琳要。
運(yùn)行效果:
生成的代碼是如何工作的寡具?
fairygui.ts中提供了一個(gè)bind函數(shù),調(diào)用FairyGUI提供的API將傳入的ts類擴(kuò)展為組件稚补,并將C#側(cè)會(huì)調(diào)用的__onConstruct等方法綁定到ts類的對應(yīng)方法上:
XXXBinder.ts中將所有ts組件類綁定童叠,這里沒有用到這個(gè)類,而是手動(dòng)調(diào)用bind綁定。
UI_Main.ts的onConstruct中厦坛,獲取了所有子組件五垮,所以可以直接使用:
個(gè)人認(rèn)為createInstance中的as T有點(diǎn)可疑,畢竟ts中的as只是類型斷言杜秸,不像C#中有類型轉(zhuǎn)換的功能放仗,這里僅起到類型檢查的作用。實(shí)際測試中撬碟,如果bind父類UI_Main而非子類UIMain诞挨,UIMain.createInstance實(shí)際返回的依然是父類UI_Main的對象,自然也不會(huì)執(zhí)行子類的方法呢蛤』躺担總之如果有擴(kuò)展子類,那么記得手動(dòng)bind一下子類其障。