美文网首页UGUI
[Unity]Puerts for Unity使用笔记

[Unity]Puerts for Unity使用笔记

作者: pamisu | 来源:发表于2022-02-07 21:44 被阅读0次

    记录一下普洱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 inittsc --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-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'
    };
    

    根据说明,csharp、puerts、path、fs在打包时需要排除,其他配置可以根据个人需求调整。
    同时希望打包支持watch,这样ts代码有改动就能同步更新输出文件,通过获取命令行参数,判断当前是否为watch模式:
    build.js

    var watchMode = false;
    for (let i = 2; i < process.argv.length; i++) {
        if (process.argv[i] == '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 watchnpm 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出了问题,可以从这几个方面检查:

    1. tsconfig.json里有没有开启source map
    2. 打包代码(build.js)里有没有开启source map
    3. source map文件生成了没有
    4. source map文件中的源文件路径是否正确(一般没问题)
    5. C#中是否指定了正确的js输出目录
    6. 加载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 += '咕';
            });
        }
    }
    // 绑定到FairyGUI
    bind(UIMain);
    // 创建实例
    let uiMain = UIMain.createInstance<UIMain>();
    // 设置设计分辨率
    FairyGUI.GRoot.inst.SetContentScaleFactor(800, 600);
    // 添加到UI
    FairyGUI.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]Puerts for Unity使用笔记

        本文链接:https://www.haomeiwen.com/subject/aahvkrtx.html