搭建 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(编译环境需要)
- 安装
yarn
或npm
,它们都是 JavaScript 包管理器。二选一,推荐前者- yarn 安装,下载安装包 安装即可
- npm 安装,npm 为 Node.js 自带的包管理工具,上面安装Node时就已经安装上了
- 安装 TypeScript 到系统环境
- yarn
- 终端输入
yarn global add typescript
安装
- 终端输入
- npm
- 终端输入
npm install typescript -g
安装
- 终端输入
- yarn
- 设置编辑器为 vscode(需要已安装 vscode)
- Mac
- Unity -> Preferences...-> External Tools -> External Script Editor
- 选择 Visual Studio Code
- Unity -> Preferences...-> External Tools -> External Script Editor
- Windows
- Edit -> Prefereneces... -> External Tools -> External Script Editor
- 选择 Visual Studio Code
- Edit -> Prefereneces... -> External Tools -> External Script Editor
- Mac
- 项目目录下创建下列文件
-
TypeScript
空目录,将项目的 Ts 脚本都将放置于该目录内 -
package.json
该文件用于保存项目中要用到的模块依赖、构建命令等- yarn
- 终端输入
yarn init
,一直回车直到结束
- 终端输入
- npm
- 终端输入
npm init
,一直回车直到结束
- 终端输入
- yarn
-
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 代码压缩模块
-
在项目目录下运行 yarn
或 npm 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:swc
或 yarn 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
手动编译(只编译一次)。
试着运行下游戏试试吧!