03 Puerts for Unity 搭建 Ts 编译环境

搭建 TypeScript 环境

在上两节中,脚本文件内容一直都是 Js。Ts 作为 Js 的超集,不能直接丢进 V8 中运行,其在运行时仍然是以 Js 形式出现的。

所以,还需要搭建 Ts -> Js 的编译环境。

Ts 编译器有很多。例如:tsc(亲儿子)、esbuild、swc 等...

这里将介绍两种方案:

  • swc 环境

    • 用 Rust 实现的一套 TypeScript/JavaScript 编译器,号称目前最快
    • 较 Babel/Ts 快 5~20倍,著名的 Deno 也是使用 swc 作为编译器
    • 追求速度,无历史包袱的新项目推荐
  • tsc 环境 TypeScript 原生环境

    • 所有特性、配置等保持与官方一至
    • 较保守的项目推荐

环境搭建

  • 安装 vscode
    • 不是必须的,但推荐安装。因为原生对 Typescript 有很好的支持
    • 后续章节也将基于 vscode 来讲,其它编辑器根据原理自行配置
  • 安装 Node.js(编译环境需要)
  • 安装 yarnnpm,它们都是 JavaScript 包管理器。二选一,推荐前者
    • yarn 安装,下载安装包 安装即可
    • npm 安装,npm 为 Node.js 自带的包管理工具,上面安装Node时就已经安装上了
  • 安装 TypeScript 到系统环境
    • yarn
      • 终端输入 yarn global add typescript 安装
    • npm
      • 终端输入 npm install typescript -g 安装
  • 设置编辑器为 vscode(需要已安装 vscode)
    • Mac
      • Unity -> Preferences...-> External Tools -> External Script Editor
        • 选择 Visual Studio Code
    • Windows
      • Edit -> Prefereneces... -> External Tools -> External Script Editor
        • 选择 Visual Studio Code
  • 项目目录下创建下列文件
    • TypeScript 空目录,将项目的 Ts 脚本都将放置于该目录内
    • package.json 该文件用于保存项目中要用到的模块依赖、构建命令等
      • yarn
        • 终端输入 yarn init,一直回车直到结束
      • npm
        • 终端输入 npm init,一直回车直到结束
    • tsconfig.json 内容可以先是一对 {},即空数据的 json 文件

修改 package.json 并安装依赖

可以将以下内容,复制到初始化好的 package.json 中。

{
  "name": "puerts",
  "version": "1.0.0",
  "scripts": {
    "watch:swc": "node ./Build.js watch",
    "build:swc": "node ./Build.js clear && node ./Build.js build",
    "publish:swc": "node ./Build.js clear && node ./Build.js build && node ./Build.js compress",

    "watch:tsc": "tsc -b tsconfig.json -w",
    "build:tsc": "node ./Build.js clear && tsc -b tsconfig.json",
    "publish:tsc": "node ./Build.js clear && tsc -b tsconfig.json && node ./Build.js compress"
  },
  "dependencies": {
    "@swc-node/core": "^1.3.0",
    "fs-extra": "^9.1.0",
    "uglify-js": "^3.13.1"
  }
}

这里稍对其内容稍作介绍

  • name,项目名称(用于发布到npm仓库上的),在游戏开发中用不上
  • version,项目版本(用于发布到npm仓库上的),在游戏开发中用不上
  • scripts,定义的构建脚本,可以将任意工作流定义其中。然后通过 yarn run 任务名npm run 任务名 来执行某个任务
  • dependencies,这里定义了项目中要用到的依赖
    • @swc-node/core,swc 编译器
    • fs-extra,一个 node 文件操作模块增强版本
    • uglify-js,一个 Js 代码压缩模块

在项目目录下运行 yarnnpm install 来拉取依赖到本地,拉取到的所有的依赖都将保存在 node_modules 目录中。

修改 tsconfig.json 文件

该文件用于配置 Ts 的相关配置项

修改文件内容如下,稍后对参数稍作讲解:

{
    "compilerOptions": {
        "target": "ESNext",
        "module": "CommonJS",
        "jsx": "react-jsx",
        "lib": ["ESNext" ,"DOM"],
        "inlineSourceMap": true,
        "moduleResolution": "node",
        "experimentalDecorators": true,
        "baseUrl": "./TypeScript/",
        "typeRoots": [
            "./Assets/Puerts/Typing",
            "./Assets/Gen/Typing",
            "./node_modules/@types"
        ],
        "outDir": "./Assets/StreamingAssets/Scripts/"
    }
}

参数基本说明:

  • target,编译最高可支持的语法,这里不像浏览器一样,需要考虑很多低版本的兼容问题。自然是全都可以要
  • module,模块规范,Puerts 默认只支持 CommonJs
  • jsx,看项目情况需要,可以使用 jsx 来写 ui
  • lib,内置Js API 的默认类型定义
  • inlineSourceMap,在调试时,开发工具要将 Js 代码与原始 Ts 对应上都依赖于 Map 信息。自然是要开启了(Map 可以独立生成文件,但笔者不是太喜欢磁盘上太多文件存在)
  • moduleResolution,模块解析策略,按 node 方式来吧
  • experimentalDecorators,启用装饰器,这是 W3C 尚未正式列入标准的特性,但非常好用。当然开启了
  • baseUrl,基本目录
  • typeRoots,指定哪些文件默认需要引入,否则就需要手动使用『三斜杠』指令手动引入,由于这些都需要用到,直接写在这里省事儿
  • outDir,编译输出目录

创建 Puerts 配置文件

该配置文件主要有以下几个作用:

  • 用来定义哪些内容需要生态静态类
    • 生成静态类的好处是在 Js 调用时是直接静态调用,起到加快速度的作用。否则为通过反射的方式调用
    • 生成 index.d.ts,供 Ts 引用。撸代码时 Ts 能给到正确的类型推测和提示都利益于该文件
  • 配置 c# 与 Ts 内存结构完全对应的数据(Blittable),直接调用。减少数据传递所带来的 Gc 开销

官方文档:https://github.com/Tencent/puerts/blob/master/doc/unity/manual.md

对于该配置文件有以下几个要求:

  • 须放在 Editor 目录中
  • Cs 文件
  • 须为类打上 [Configure] 标记
  • [Binding][Typing][BlittableCopy][Filter] 需放到被 [Configure] 标记过的类下。

Assets/Editor/ 目录下(没有则手动创建该目录),创建 PuertsConfig.cs 文件,内容如下:

/// 
/// 官方说明 https://github.com/Tencent/puerts/blob/master/doc/unity/manual.md
/// 
using System.Collections.Generic;
using Puerts;
using System;
using UnityEngine;

/// 
/// 配置类
/// ! 配置类必须打 [Configure] 标签
/// ! 必须放在Editor目录
/// 
[Configure]
public class PuertsConfig {

    /// 
    /// 在 Js/Ts 调用时可以找到该类
    /// * 会生成一个静态类(wrap),在 Js 调用时将直接静态调用加快速度,否则通过反射调用
    /// * 会生成到 Assets/Gen/Typing/csharp/index.d.ts ,以在 Ts 中引用
    /// ! 须放在 [Configure] 标记过的类里
    /// 
    /// 
    [Binding]
    static IEnumerable Bindings {
        get {
            var types = new List();
            var namespaces = new HashSet();
            namespaces.Add("tiny");
            namespaces.Add("tiny.utils");
            Dictionary> ignored = new Dictionary>();
            var ignored_classes = new HashSet();

            // 忽略 tiny.EditorUtils 类
            ignored_classes = new HashSet();
            ignored_classes.Add("EditorUtils");
            ignored.Add("tiny", ignored_classes);

            // TODO:在此处添加要忽略绑定的类型
            Dictionary> registered = new Dictionary>();
            foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) {
                var name = assembly.GetName().Name;
                foreach (var type in assembly.GetTypes()) {
                    if (!type.IsPublic) continue;
                    if (type.Name.Contains("<") || type.Name.Contains("*")) continue;   // 忽略泛型,指针类型
                    if (type.Namespace == null || type.Name == null) continue;          // 这是啥玩意?
                    bool accept = namespaces.Contains(type.Namespace);
                    if (accept && ignored.ContainsKey(type.Namespace) && ignored[type.Namespace].Contains(type.Name)) continue;
                    if (accept) {
                        types.Add(type);
                        if (!registered.ContainsKey(type.Namespace)) {
                            var classes = new HashSet();
                            classes.Add(type.Name);
                            registered.Add(type.Namespace, classes);
                        } else {
                            registered[type.Namespace].Add((type.Name));
                        }
                    }
                }
            }
            // 绑定 Unity常用类型
            types.Add(typeof(UnityEngine.Debug));
            types.Add(typeof(UnityEngine.Vector2));
            types.Add(typeof(UnityEngine.Vector3));
            types.Add(typeof(UnityEngine.Vector4));
            types.Add(typeof(UnityEngine.Quaternion));
            types.Add(typeof(UnityEngine.Color));
            types.Add(typeof(UnityEngine.Rect));
            types.Add(typeof(UnityEngine.Bounds));
            types.Add(typeof(UnityEngine.Ray));
            types.Add(typeof(UnityEngine.RaycastHit));
            types.Add(typeof(UnityEngine.Matrix4x4));

            types.Add(typeof(UnityEngine.Time));
            types.Add(typeof(UnityEngine.Transform));
            types.Add(typeof(UnityEngine.Object));
            types.Add(typeof(UnityEngine.GameObject));
            types.Add(typeof(UnityEngine.Component));
            types.Add(typeof(UnityEngine.Behaviour));
            types.Add(typeof(UnityEngine.MonoBehaviour));
            types.Add(typeof(UnityEngine.AudioClip));
            types.Add(typeof(UnityEngine.ParticleSystem.MainModule));
            types.Add(typeof(UnityEngine.AnimationClip));
            types.Add(typeof(UnityEngine.Animator));
            types.Add(typeof(UnityEngine.AnimationCurve));
            types.Add(typeof(UnityEngine.AndroidJNI));
            types.Add(typeof(UnityEngine.AndroidJNIHelper));
            types.Add(typeof(UnityEngine.Collider));
            types.Add(typeof(UnityEngine.Collision));
            types.Add(typeof(UnityEngine.Rigidbody));
            types.Add(typeof(UnityEngine.Screen));
            types.Add(typeof(UnityEngine.Texture));
            types.Add(typeof(UnityEngine.TextAsset));
            types.Add(typeof(UnityEngine.SystemInfo));
            types.Add(typeof(UnityEngine.Input));
            types.Add(typeof(UnityEngine.Mathf));

            types.Add(typeof(UnityEngine.Camera));
            types.Add(typeof(UnityEngine.Camera.RenderRequest));
            types.Add(typeof(UnityEngine.ParticleSystem));
            types.Add(typeof(UnityEngine.AudioSource));
            types.Add(typeof(UnityEngine.AudioListener));
            types.Add(typeof(UnityEngine.Physics));
            types.Add(typeof(UnityEngine.SceneManagement.Scene));
            types.Add(typeof(UnityEngine.Networking.IMultipartFormSection));
            types.Add(typeof(UnityEngine.Networking.UnityWebRequest));
            return types;
        }
    }
    /// 
    /// 对定义的 Blittable 值类型通过内存拷贝传递,可避免值类型传递产生的GC,需要开启unsafe编译选项
    /// ! 只能用在属性上
    /// ! 需要开启 unsafe 编译选项 
    /// ! 须放在 [Configure] 标记过的类里
    /// 
    /// 
    [BlittableCopy]
    static IEnumerable Blittables {
        get {
            return new List() {
                typeof(Vector2),
                typeof(Vector3),
                typeof(Vector4),
                typeof(Quaternion),
                typeof(Color),
                typeof(Rect),
                typeof(Bounds),
                typeof(Ray),
                typeof(RaycastHit),
                typeof(Matrix4x4)
            };
        }
    }
    
    /// 
    /// 过滤函数
    /// ! 只能用在函数上
    /// ! 须放在 [Configure] 标记过的类里
    /// 
    /// 
    /// 
    [Filter]
    static bool FilterMethods(System.Reflection.MemberInfo memberInfo){
        string sig = memberInfo.ToString();

        if (memberInfo.ReflectedType.FullName == "UnityEngine.MonoBehaviour" && memberInfo.Name == "runInEditMode") return true;
        if (memberInfo.ReflectedType.FullName == "UnityEngine.Input" && memberInfo.Name == "IsJoystickPreconfigured") return true;
        if (memberInfo.ReflectedType.FullName == "UnityEngine.Texture" && memberInfo.Name == "imageContentsHash") return true;
        // TODO: 添加要忽略导出的类成员

        return sig.Contains("*");
    }
}

创建编译脚本 Build.js

出于轻量和速度考虑,且这里只需要对 Js 进行一些简单处理,并不需要用到诸如 Webpack、Grunt、Gulp...这些强大的工具。

所以自己撸一个编译相关的处理脚本,在项目根目录下创建 Build.js,其内容如下:

const path = require('path'),
    fs = require('fs-extra'),
    Uglifyjs = require('uglify-js'),
    swc = require('@swc-node/core'),

    // 获取传入的参数模式
    mode = (()=>{
        let argv = require('process').argv;
        return argv[argv.length - 1];
    })();

class Build {
    constructor(mode){
        const _ts = this,
            configPath = path.join(__dirname,'tsconfig.json');

        if(fs.existsSync(configPath)){
            _ts.config = require(configPath);
        }else{
            throw new Error('tsconfig.json 配置文件不存在');
        };
        _ts.srcDir = path.join(__dirname,_ts.config.compilerOptions.baseUrl);
        _ts.outDir = path.join(__dirname,_ts.config.compilerOptions.outDir);
        _ts.timer = {};      // 文件监听计时器
        
        // 根据传入的模式执行相应的任务
        switch (mode) {
            case 'watch':
                _ts.watch();    
            break;
            case 'clear':
                _ts.clear();
            break;
            case 'compress':
                _ts.compress();
            break;
            case 'build':
                _ts.build();
            break;
            default:
                throw new Error("传入的类型错误");
            break;
        }
    }
    // 压缩目录内 JS
    compress(){
        const _ts = this;
        let files = _ts.getFiles(_ts.outDir),
            count = 0;
        files.forEach(item => {
            if(_ts.getPathInfo(item).extname === 'js'){
                let itemStr = fs.readFileSync(item,'utf-8'),
                    result = Uglifyjs.minify(itemStr);
                if(result.error){
                    throw new Error("文件压缩出错");
                };
                fs.writeFileSync(item,result.code);
                count++;
                console.log("文件压缩成功:",item);
            };
        });
        console.log(new Array(51).join("="));
        console.log(`共 ${count} 个文件压缩完成`);
    }

    // 清除目录 Js
    clear(){
        const _ts = this;
        fs.emptyDirSync(_ts.outDir);
        console.log(`目录清除成功:${_ts.outDir}`);
    }

    /**
     * 监听目录,如果文件有改动则进行编译
     */
    watch(){
        const _ts = this;

        fs.watch(_ts.srcDir,{recursive:true},(eventType, fileName)=>{
            let filePath = path.join(_ts.srcDir,fileName),
                outPath = filePath.replace(_ts.srcDir,_ts.outDir).replace(/\.(jsx|js|ts)$/,'.js'),
                filePathInfo = _ts.getPathInfo(filePath);
            if(filePathInfo.type === 'file' && _ts.isTs(filePath)){
                clearTimeout(_ts.timer[filePath]);
                _ts.timer[filePath] = setTimeout(()=>{
                    _ts.buildFile(filePath,outPath);
                },200);
            };
        });
    }
    
    /**
     * 判断输入路径是否为 Ts 文件
     * @param {string} srcPath 输入路径
     * @returns Boolean
     */
    isTs(srcPath){
        return /^jsx|js|ts|es|es6$/.test(this.getPathInfo(srcPath).extname) && !/(\.d\.ts)$/.test(srcPath);
    }

    /**
     * 编译输入目录所有文件
     */
    build(){
        const _ts = this;
        let files = _ts.getFiles(_ts.srcDir);
        files.forEach(item => {
            if(_ts.isTs(item)){
                let outPath = item.replace(_ts.srcDir,_ts.outDir).replace(/\.(jsx|js|ts)$/,'.js');
                _ts.buildFile(item,outPath);
            };
        });
    }

    /**
     * 编译单个文件
     * @param {string} srcPath ts文件路径
     * @param {string} outPath 文件输出路径
     */
    buildFile(srcPath,outPath){
        swc.transform(fs.readFileSync(srcPath,'utf-8'),srcPath,{
            target:"es2016",
            module:"commonjs",
            sourcemap:"inline",
            experimentalDecorators: true,
            emitDecoratorMetadata: true,
            dynamicImport:true
        }).then(v => {
            fs.writeFileSync(outPath,v.code);
            console.log("文件编译成功:",srcPath,"-->",outPath);
        }).catch(e => {
            throw e;
        });
    }

    /**
     * 获取指定路径信息
     * @param {string} targetPath  <必选> 目标路径
     * @returns object 路径信息,包含其类型、扩展名、目录名
     */
    getPathInfo(targetPath){
        let result = {},
            stat = fs.statSync(targetPath);
        result.type = stat.isFile() ? 'file' :
            stat.isDirectory ? 'dir' :
            stat.isSymbolicLink ? 'link' :
            stat.isSocket ? 'socket' :
            'other';
        result.extname = path.extname(targetPath).slice(1);
        result.dirPath = path.dirname(targetPath);
        return result;
    }

    /**
     * 获取指定路径目录下所有文件列表
     * @param {string} dirPath  <必选> 目录路径
     * @param {array} result    <可选> 将结果往哪个队列中添加(用于内部循环调用) 
     * @returns array 文件列表
     */
    getFiles(dirPath,result){
        result = result || [];
        const _ts = this,
            isDir = fs.statSync(dirPath).isDirectory();
        if(isDir){
            let items = fs.readdirSync(dirPath);
            items.forEach(item => {
                let itemPath = path.join(dirPath,item);
                switch (_ts.getPathInfo(itemPath).type) {
                    case 'file':
                        result.push(itemPath);
                    break;
                    case 'dir':
                        _ts.getFiles(itemPath,result);
                    break;
                }
            });
        }else{
            throw new Error("传入的不是有效的目录路径");
        };
        return result;
    }
}

new Build(mode);

创建 main.ts 测试

运行 yarn run watch:swcyarn run watch:tsc启动监听编译,这样 TypeScript 目录下所有的 ts 文件有任何改动。都会实时编译。

TypeScript/ 目录下创建 main.ts文件,内容如下:

import {System, UnityEngine} from 'csharp'

UnityEngine.Debug.Log('Hello World');
let obj:UnityEngine.GameObject = new UnityEngine.GameObject("testObject");
obj.transform.position = new UnityEngine.Vector3(1, 2, 3);

看一下 Assets/StreamingAssets/Scripts/ 有应该有编译出对应的 Js 文件,如果没有可以尝试重新保存一下文件或执行 yarn run build:tsc手动编译(只编译一次)。

试着运行下游戏试试吧!

你可能感兴趣的:(03 Puerts for Unity 搭建 Ts 编译环境)