找回密码
 立即注册
查看: 518|回复: 0

[Unity]Puerts for Unity使用笔记

[复制链接]
发表于 2022-2-8 12:12 | 显示全部楼层 |阅读模式
记录一下普洱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一下子类。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

×
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-11-16 20:27 , Processed in 0.092966 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表