Puerts的官方仓库: 链接
Puerts for Unity 基本接入: 链接
视频教程: 链接
lugins_V8_verx.tgz
,编译好的 V8 文件。Sources_code(zip)
,Puerts 源码。puerts-x.x.x.zip
解压出来的 unity/Assets/Puerts/
目录到 项目/Assets/
目录下;Plugins_V8_verx.tgz
解压出来的 Plugins/
目录到 项目/Assets/
目录下;//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();
}
}
//main.js
var _csharp = require("csharp");
_csharp.UnityEngine.Debug.Log('Hello World');
StreamingAssets
是 Unity 规定的 流媒体资源存放目录。
在该目录下的资源不会参与引擎编译,也方便使用 AssetBundle 资源热更新方法进行资源热更。
自此可以用JavaScript开发unity项目了。
//Loader.cs
using System.IO;
using UnityEngine;
using Puerts;
public class Loader : ILoader
{
// 限制 debugRoot 属性外部只可访问,不可设置
public string debugRoot { get; private set; }
/// <summary>
/// 获取 Puerts 自带模块路径
/// </summary>
/// <param name="filePath">Puerts 自带模块名称</param>
/// <returns>模块完整路径</returns>
private string GetPuertsModulePath(string filePath)
{
return PathUnified(Application.dataPath, "Puerts/Src/Resources/", filePath) + ".txt";
}
/// <summary>
/// 判断模块是否为 Puerts自带模块
/// </summary>
/// <param name="filePath">模块名称</param>
/// <returns>true/false</returns>
private bool IsPuertsModule(string filePath)
{
return filePath.StartsWith("puerts/");
}
/// <summary>
/// 构造方法
/// </summary>
/// <param name="debugRoot">Js 脚本存放目录</param>
public Loader(string debugRoot)
{
this.debugRoot = debugRoot;
}
/// <summary>
/// * 接口要求实现
/// 判断文件是否存在
/// </summary>
/// <param name="filePath">文件路径</param>
/// <returns>true/false</returns>
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;
}
/// <summary>
/// * 接口要求实现
/// 文件内容读取
/// </summary>
/// <param name="filePath">模块路径</param>
/// <param name="debugPath">文件完整路径</param>
/// <returns>文本内容</returns>
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<TextAsset>(pathToUse).text : File.ReadAllText(debugPath);
}
/// <summary>
/// 纠正路径(Windows下路径斜杠不正确的问题)
/// </summary>
/// <param name="args"></param>
/// <returns>纠正之后的路径</returns>
private string PathUnified(params string[] args)
{
return Path.Combine(args).Replace("\\", "/");
}
}
//main.ts
var _csharp = require("csharp");
_csharp.UnityEngine.Debug.Log('Hello World');
{
"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"
}
}
{
"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
,一直回车直到结束。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);
};
});
}
/**
* 判断输入路径是否为 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);
PuertsConfig.cs
文件,内容如下:using Puerts;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Collections.Generic;
/// <summary>
/// 如果你全ts/js编程,可以参考这份自动化配置
/// </summary>
[Configure]
public class PuertsConfig
{
[Typing]
static IEnumerable<Type> Typeing
{
get
{
return new List<Type>()
{
//仅生成ts接口, 不生成C#静态代码
//typeof(Dictionary<int,int>)
};
}
}
[BlittableCopy]
static IEnumerable<Type> Blittables
{
get
{
return new List<Type>()
{
//打开这个可以优化Vector3的GC,但需要开启unsafe编译
//typeof(Vector3),
};
}
}
static IEnumerable<Type> Bindings
{
get
{
return new List<Type>()
{
//直接指定的类型
typeof(JsEnv),
typeof(ILoader),
};
}
}
[Binding]
static IEnumerable<Type> DynamicBindings
{
get
{
// 在这里添加名字空间
var namespaces = new List<string>()
{
"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<string> excludeAssemblys = new List<string>{
"UnityEditor.dll",
"Assembly-CSharp-Editor.dll",
};
//需要排除的类型
static List<string> excludeTypes = new List<string>
{
"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/launch.json
。{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "attach",
"name": "Attach Program",
"skipFiles": [
"<node_internals>/**"
],
"port": 4399
}
]
}