- 参考文献
- 导入puerts
- 创建入口文件
- 自定义Loader
- Typescript脚本开发
- 创建 Puerts 配置文件
- 开启watch进程
- 调试代码
Puerts的官方仓库: 链接
Puerts for Unity 基本接入: 链接
视频教程: 链接
- 从 Releases 页面下载对应版本的 Puerts ,并解压;
- lugins_V8_verx.tgz,编译好的 V8 文件。
- Sources_code(zip),Puerts 源码。
- 拷贝源码 puerts-x.x.x.zip解压出来的 unity/Assets/Puerts/目录到 项目/Assets/目录下;
- 拷贝 Plugins_V8_verx.tgz解压出来的 Plugins/目录到 项目/Assets/目录下;
- 在 Assets/Cs/ 创建Main.cs入口文件,绑定到场景节点中。
//Main.cs
using UnityEngine;
using Puerts; // 引用 Puerts
using System.IO;
class Main : MonoBehaviour
{
public bool isDebug = false; // 是否开启调试
public int debugPort = 43990; // 调试端口号
public JsEnv jsEnv; // 定义 jsEnv
private Loader loader;
private string scriptsDir = Path.Combine(Application.streamingAssetsPath, "Scripts");
async void Start()
{
loader = new Loader(scriptsDir);
jsEnv = new JsEnv(loader, debugPort); // 实例化 js 虚拟机
if (isDebug)
{ // 启用调试
await jsEnv.WaitDebuggerAsync();
}
jsEnv.Eval("require('main')");
}
void Update()
{
jsEnv.Tick();
}
}
- 增加 StreamingAssets/Scripts/ 文件夹,创建main.js文件。
//main.js
var _csharp = require("csharp");
_csharp.UnityEngine.Debug.Log('Hello World');
StreamingAssets是 Unity 规定的 流媒体资源存放目录。
在该目录下的资源不会参与引擎编译,也方便使用 AssetBundle 资源热更新方法进行资源热更。
自此可以用JavaScript开发unity项目了。
- 在 Assets/Cs/ 创建 Loader.cs 的 c# 文件,代码如下:
//Loader.cs
using System.IO;
using UnityEngine;
using Puerts;
public class Loader : ILoader
{
// 限制 debugRoot 属性外部只可访问,不可设置
public string debugRoot { get; private set; }
///
/// 获取 Puerts 自带模块路径
///
/// Puerts 自带模块名称
/// 模块完整路径
private string GetPuertsModulePath(string filePath)
{
return PathUnified(Application.dataPath, "Puerts/Src/Resources/", filePath) + ".txt";
}
///
/// 判断模块是否为 Puerts自带模块
///
/// 模块名称
/// true/false
private bool IsPuertsModule(string filePath)
{
return filePath.StartsWith("puerts/");
}
///
/// 构造方法
///
/// Js 脚本存放目录
public Loader(string debugRoot)
{
this.debugRoot = debugRoot;
}
///
/// * 接口要求实现
/// 判断文件是否存在
///
/// 文件路径
/// true/false
public bool FileExists(string filePath)
{
// Puerts 需要调用到其目录下的一些 js 文件,这里通通判为存在
if (IsPuertsModule(filePath)) return true;
#if UNITY_EDITOR
return File.Exists(PathUnified(debugRoot, filePath));
#else
return true;
#endif
}
private string PathToUse(string filepath)
{
return
// .cjs asset is only supported in unity2018+
filepath.EndsWith(".cjs") || filepath.EndsWith(".mjs") ?
filepath.Substring(0, filepath.Length - 4) :
filepath;
}
///
/// * 接口要求实现
/// 文件内容读取
///
/// 模块路径
/// 文件完整路径
/// 文本内容
public string ReadFile(string filePath, out string debugPath)
{
bool isPuerts = IsPuertsModule(filePath);
debugPath = isPuerts ? GetPuertsModulePath(filePath) : PathUnified(debugRoot, filePath);
string pathToUse = this.PathToUse(filePath);
// Puerts 本身调用的 Js 存放在 Resource 目录下,所以可以直接用 Resources.Load 获取
return isPuerts ? Resources.Load(pathToUse).text : File.ReadAllText(debugPath);
}
///
/// 纠正路径(Windows下路径斜杠不正确的问题)
///
///
/// 纠正之后的路径
private string PathUnified(params string[] args)
{
return Path.Combine(args).Replace("\", "/");
}
}
Typescript脚本开发
- 在项目根目录创建TypeScript文件夹,其内创建一个Main.ts文件。
//main.ts
var _csharp = require("csharp");
_csharp.UnityEngine.Debug.Log('Hello World');
- 在根目录创建pacakage.json文件。
{
"name": "mypuerts",
"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"
}
}
- 根目录里创建tsconfig.json。
{
"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"
}
}
- 终端输入 npm init,一直回车直到结束。
- 根目录里创建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);
console.log("watch ", _ts.srcDir, "-->", outPath);
if (filePathInfo.type === 'file' && _ts.isTs(filePath)) {
clearTimeout(_ts.timer[filePath]);
_ts.timer[filePath] = setTimeout(() => {
_ts.buildFile(filePath, outPath);
}, 200);
};
});
}
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);
};
});
}
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;
});
}
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;
}
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);
创建 Puerts 配置文件
- 在 Assets/Editor/ 目录下(没有则手动创建该目录),创建 PuertsConfig.cs文件,内容如下:
using Puerts; using System; using System.IO; using System.Linq; using System.Reflection; using System.Collections.Generic; ///开启watch进程/// 如果你全ts/js编程,可以参考这份自动化配置 /// [Configure] public class PuertsConfig { [Typing] static IEnumerableTypeing { get { return new List () { //仅生成ts接口, 不生成C#静态代码 //typeof(Dictionary ) }; } } [BlittableCopy] static IEnumerable Blittables { get { return new List () { //打开这个可以优化Vector3的GC,但需要开启unsafe编译 //typeof(Vector3), }; } } static IEnumerable Bindings { get { return new List () { //直接指定的类型 typeof(JsEnv), typeof(ILoader), }; } } [Binding] static IEnumerable DynamicBindings { get { // 在这里添加名字空间 var namespaces = new List () { "UnityEngine", "UnityEngine.UI", }; var unityTypes = (from assembly in AppDomain.CurrentDomain.GetAssemblies() where !(assembly.ManifestModule is System.Reflection.Emit.ModuleBuilder) from type in assembly.GetExportedTypes() where type.Namespace != null && namespaces.Contains(type.Namespace) && !IsExcluded(type) select type); string[] customAssemblys = new string[] { "Assembly-CSharp", }; var customTypes = (from assembly in customAssemblys.Select(s => Assembly.Load(s)) where !(assembly.ManifestModule is System.Reflection.Emit.ModuleBuilder) from type in assembly.GetExportedTypes() where type.Namespace == null || !type.Namespace.StartsWith("Puerts") && !IsExcluded(type) select type); return unityTypes .Concat(customTypes) .Concat(Bindings) .Distinct(); } } static bool IsExcluded(Type type) { if (type == null) return false; string assemblyName = Path.GetFileName(type.Assembly.Location); if (excludeAssemblys.Contains(assemblyName)) return true; string fullname = type.FullName != null ? type.FullName.Replace("+", ".") : ""; if (excludeTypes.Contains(fullname)) return true; return IsExcluded(type.BaseType); } //需要排除的程序集 static List excludeAssemblys = new List { "UnityEditor.dll", "Assembly-CSharp-Editor.dll", }; //需要排除的类型 static List excludeTypes = new List { "UnityEngine.iPhone", "UnityEngine.iPhoneTouch", "UnityEngine.iPhoneKeyboard", "UnityEngine.iPhoneInput", "UnityEngine.iPhoneAccelerationEvent", "UnityEngine.iPhoneUtils", "UnityEngine.iPhoneSettings", "UnityEngine.AndroidInput", "UnityEngine.AndroidJavaProxy", "UnityEngine.BitStream", "UnityEngine.ADBannerView", "UnityEngine.ADInterstitialAd", "UnityEngine.RemoteNotification", "UnityEngine.LocalNotification", "UnityEngine.NotificationServices", "UnityEngine.MasterServer", "UnityEngine.Network", "UnityEngine.NetworkView", "UnityEngine.ParticleSystemRenderer", "UnityEngine.ParticleSystem.CollisionEvent", "UnityEngine.ProceduralPropertyDescription", "UnityEngine.ProceduralTexture", "UnityEngine.ProceduralMaterial", "UnityEngine.ProceduralSystemRenderer", "UnityEngine.TerrainData", "UnityEngine.HostData", "UnityEngine.RPC", "UnityEngine.AnimationInfo", "UnityEngine.UI.IMask", "UnityEngine.Caching", "UnityEngine.Handheld", "UnityEngine.MeshRenderer", "UnityEngine.UI.DefaultControls", "UnityEngine.AnimationClipPair", //Obsolete "UnityEngine.CacheIndex", //Obsolete "UnityEngine.SerializePrivateVariables", //Obsolete "UnityEngine.Networking.NetworkTransport", //Obsolete "UnityEngine.Networking.ChannelQOS", //Obsolete "UnityEngine.Networking.ConnectionConfig", //Obsolete "UnityEngine.Networking.HostTopology", //Obsolete "UnityEngine.Networking.GlobalConfig", //Obsolete "UnityEngine.Networking.ConnectionSimulatorConfig", //Obsolete "UnityEngine.Networking.DownloadHandlerMovieTexture", //Obsolete "AssetModificationProcessor", //Obsolete "AddressablesPlayerBuildProcessor", //Obsolete "UnityEngine.WWW", //Obsolete "UnityEngine.EventSystems.TouchInputModule", //Obsolete "UnityEngine.MovieTexture", //Obsolete[ERROR] "UnityEngine.NetworkPlayer", //Obsolete[ERROR] "UnityEngine.NetworkViewID", //Obsolete[ERROR] "UnityEngine.NetworkMessageInfo", //Obsolete[ERROR] "UnityEngine.UI.BaseVertexEffect", //Obsolete[ERROR] "UnityEngine.UI.IVertexModifier", //Obsolete[ERROR] //Windows Obsolete[ERROR] "UnityEngine.EventProvider", "UnityEngine.UI.GraphicRebuildTracker", "UnityEngine.GUI.GroupScope", "UnityEngine.GUI.ScrollViewScope", "UnityEngine.GUI.ClipScope", "UnityEngine.GUILayout.HorizontalScope", "UnityEngine.GUILayout.VerticalScope", "UnityEngine.GUILayout.AreaScope", "UnityEngine.GUILayout.ScrollViewScope", "UnityEngine.GUIElement", "UnityEngine.GUILayer", "UnityEngine.GUIText", "UnityEngine.GUITexture", "UnityEngine.ClusterInput", "UnityEngine.ClusterNetwork", //System "System.Tuple", "System.Double", "System.Single", "System.ArgIterator", "System.SpanExtensions", "System.TypedReference", "System.StringBuilderExt", "System.IO.Stream", "System.Net.HttpListenerTimeoutManager", "System.Net.Sockets.SocketAsyncEventArgs", }; }
开启watch进程后,可以监听ts文件的改动,自动编译成js代码到指定好的目录。
- npm run build/swc,手动编译一次。
- npm run watch/swc,监听Typescript文件。
自此可以开始愉快的TypeScript代码之旅。
调试代码- vscode编译器调试,项目根目录下创建 .vscode/launch.json。
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "attach",
"name": "Attach Program",
"skipFiles": [
"/**"
],
"port": 4399
}
]
}
- chrome调试,chrome浏览器打开 chrome://inspect



