|
记录一下普洱TS的安装、代码打包、调试与FairyGUI集成等,以及使用过程中遇到的问题。
基本使用
安装
按照官方手册,拷贝puerts/unity/Assets下的所有内容到您项目的Assets目录下,在
release中下载插件并解压覆盖到Plugins目录,插件有不同的js引擎版本,不知道选什么的话建议用v8。
Unity示例 在另一个仓库,是独立的Unity工程,看完里面的示例基本上就能明白大致使用方法了。
Hello Kitty
按照国际惯例,先来写个Hello Kitty。
配置
如果仅安装了Puerts,没有拷贝示例代码,则需要在Unity中做一些准备工作。为了快速看效果,只做一个简单的配置,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), };}
执行菜单Puerts->Generate index.d.ts:
将会生成对应的类型声明文件:
TyepeScript工程
在项目根目录下新建一个TsProject文件夹(官方示例中为TsProj),作为TypeScript工程目录。
用vscode打开它,在这之前请确保已经安装好了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#侧的类型声明文件目录,如果你的ts工程目录或者Puerts目录有变更,这里需要修改正确。
outDir指定了编译后js文件的输出目录。其他的配置没什么好说的,可以根据个人喜好调整,更多配置项说明可以查看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" }}
这两个文件也可以用npm init与tsc --init创建。
把官方示例的TsProj文件夹里的copyJsFile.js拷贝过来,新建index.ts,编写Hello World:
index.ts
console.log('Hello Kitty!');
终端里运行:
npm run build
可以看到output文件夹输出了编译后的index.js文件与map文件:
并且这些文件被拷贝到了Recources目录下:
执行
Scripts下新建JsManager.cs,编写执行代码:
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(); } }}
脚本挂到场景中,运行即可看到效果:
C__Users_OSoleMio_OneDrive_文档_Blog_Puerts_hello_world.png
打包与调试
打包
在冻手之前,先看看默认的build都干了些什么:
首先tsc编译,文件输出到output文件夹下,然后执行copyJsFile.js将文件拷贝到了Assets/Resources目录下。
那么打包过程依葫芦画瓢即可,先打包,再拷贝。官方说明中用的是webpack,个人更习惯用esbuild,也差不了太多。
先把esbuild装好,终端里执行:
npm install esbuild --save-dev
拷贝过程懒得自己写了,直接用copyJsFile.js,修改它的代码,导出拷贝方法:
copyJsFile.js
// if (process.argv.length == 4) {// copyFolderRecursiveSync(process.argv[2], process.argv[3]);// } else {// console.error('invalid arguments');// }exports.copyFolder = copyFolderRecursiveSync
新建build.js,加入相应依赖,指定输出目录与拷贝目标目录:
build.js
var copyFolder = require('./copyJsFile').copyFolder;var outputFolder = 'output';var targetFolder = '../Assets/Resources';
编写打包配置:
build.js
// https://esbuild.github.io/api/#build-apivar 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'};
根据说明,csharp、puerts、path、fs在打包时需要排除,其他配置可以根据个人需求调整。
同时希望打包支持watch,这样ts代码有改动就能同步更新输出文件,通过获取命令行参数,判断当前是否为watch模式:
build.js
var watchMode = false;for (let i = 2; i < process.argv.length; i++) { if (process.argv == 'watch') { watchMode = true; break; }}
如果为watch模式,则增加对应watch配置,在Rebuild时将输出文件拷贝到目标目录下:
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") { // 正式打包时将删除输出目录下所有文件 var fs = require('fs'); var path = require('path'); fs.rmSync(path.dirname(options.outfile), { recursive: true, force: true })}
最后执行:
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
随便写点东西,运行npm run watch或npm run build,可以看到打好包的bundle.js:
记得修改执行处的文件名:
JsManager.cs
private void Awake() { jsEnv ??= new JsEnv(new DefaultLoader()); jsEnv.Eval("require('bundle');"); }
调试
调试可以参考官方文档,按文档配置一遍,Unity中运行后,再在vscode中启动调试器即可。这里记录一些我在瞎搞过程中遇到的问题。
调试器连不上
检查launch.json中的端口是否与C#代码中的一致,并且端口未被占用,OnDestroy中需要调用jsEnv.Dispose()销毁,避免退出运行后端口依然处于占用状态。
断点无效
断点为灰色,并提示“Unbound breakpoint”:
这种情况一般是source map出了问题,可以从这几个方面检查:
tsconfig.json里有没有开启source map打包代码(build.js)里有没有开启source mapsource map文件生成了没有source map文件中的源文件路径是否正确(一般没问题)C#中是否指定了正确的js输出目录加载Resources子目录下的js文件时,js输出目录要保持同样的结构
对于第六点,比如js文件不是拷贝到Resources根目录,而是拷贝到Resources/tsbuild目录中:
jsEnv.Eval("require('tsbuild/bundle');");
那么需要让输出目录也保持这个结构:
不过一般不会直接从Resources下加载,用Addressable或AssetBundle的情况比较多。
如果出现程序运行得太快,有些断点没进的情况,并已使用了jsEnv的等待调试器连接,那么可以尝试在launch.json中开启pauseForSourceMap。
Source Map Support
在index.ts中报个异常试试:
JSON.parse('aa');
并不能追踪到源码的报错位置:
在官方faq文档中有解决方法,使用source-map-support。通常只需要require之后install就行,但由于source-map-support是一个nodejs模块,它引用到了node的path与fs,其他js引擎中没有这两个模块,所以需要按照文档中将它们改为C# System.IO的实现。
如果按文档做了一遍还是不行的话,可以尝试修改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; }});
可以追踪到报错位置:
FairyGUI
FairyGUI官方有Puerts的使用说明,按文档搞就完事了。这里主要介绍一个FairyGUI Puerts插件,可以直接生成TypeScript的UI代码,喜欢的话请给作者一个Star。
首先按官方的使用说明在Unity中安装FairyGUI SDK,并做好相关配置,然后随便建个UI工程,目录与Assets、TsProject同级:
将插件仓库克隆到UiProject下的plugins目录,重启FairyGUI编辑器,可以看到新增的插件:
发布设置中设置发布路径:
包设置中记得勾选“为本包生成代码”:
发布即可看到生成的UI代码:
生成的UI代码放在发布路径的包名文件夹下,比如这里包名为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 += '咕'; }); }}// 绑定到FairyGUIbind(UIMain);// 创建实例let uiMain = UIMain.createInstance<UIMain>();// 设置设计分辨率FairyGUI.GRoot.inst.SetContentScaleFactor(800, 600);// 添加到UIFairyGUI.GRoot.inst.AddChild(uiMain);
这里定义一个子类UIMain继承生成的UI_Main,在点击按钮时添加一个”咕“。
运行效果:
生成的代码是如何工作的?
fairygui.ts中提供了一个bind函数,调用FairyGUI提供的API将传入的ts类扩展为组件,并将C#侧会调用的__onConstruct等方法绑定到ts类的对应方法上:
XXXBinder.ts中将所有ts组件类绑定,这里没有用到这个类,而是手动调用bind绑定。
UI_Main.ts的onConstruct中,获取了所有子组件,所以可以直接使用:
个人认为createInstance中的as T有点可疑,毕竟ts中的as只是类型断言,不像C#中有类型转换的功能,这里仅起到类型检查的作用。实际测试中,如果bind父类UI_Main而非子类UIMain,UIMain.createInstance实际返回的依然是父类UI_Main的对象,自然也不会执行子类的方法。总之如果有扩展子类,那么记得手动bind一下子类。 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|