当前位置: 首页 > 工具软件 > Puerts > 使用案例 >

Puerts的使用流程

昌勇锐
2023-12-01

参考文献

Puerts的官方仓库: 链接
Puerts for Unity 基本接入: 链接
视频教程: 链接

导入puerts

  1. Releases 页面下载对应版本的 Puerts ,并解压;
    • lugins_V8_verx.tgz,编译好的 V8 文件。
    • Sources_code(zip),Puerts 源码。
  2. 拷贝源码 puerts-x.x.x.zip解压出来的 unity/Assets/Puerts/目录到 项目/Assets/目录下;
  3. 拷贝 Plugins_V8_verx.tgz解压出来的 Plugins/目录到 项目/Assets/目录下;

创建入口文件

  1. 在 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();
  }
}
  1. 增加 StreamingAssets/Scripts/ 文件夹,创建main.js文件。
//main.js
var _csharp = require("csharp");
_csharp.UnityEngine.Debug.Log('Hello World');

StreamingAssets是 Unity 规定的 流媒体资源存放目录
在该目录下的资源不会参与引擎编译,也方便使用 AssetBundle 资源热更新方法进行资源热更。
自此可以用JavaScript开发unity项目了。

自定义Loader

  1. 在 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; }

  /// <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("\\", "/");
  }
}

Typescript脚本开发

  1. 在项目根目录创建TypeScript文件夹,其内创建一个Main.ts文件。
//main.ts
var _csharp = require("csharp");
_csharp.UnityEngine.Debug.Log('Hello World');
  1. 在根目录创建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"
  }
}
  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"
  }
}
  1. 终端输入 npm init,一直回车直到结束。
  2. 根目录里创建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);
			};
		});
	}

	/**
	 * 判断输入路径是否为 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);

创建 Puerts 配置文件

  1. 在 Assets/Editor/ 目录下(没有则手动创建该目录),创建 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进程

开启watch进程后,可以监听ts文件的改动,自动编译成js代码到指定好的目录。

  1. npm run build/swc,手动编译一次。
  2. npm run watch/swc,监听Typescript文件。

自此可以开始愉快的TypeScript代码之旅。

调试代码

  1. 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": [
        "<node_internals>/**"
      ],
      "port": 4399
    }
  ]
}
  1. chrome调试,chrome浏览器打开 chrome://inspect
 类似资料: