【PuerTS】我们把Node.js放进了Unity里(一)
近期,PuerTS 更新了1.0.18版本,这个版本最主要的更新是继 V8 和 QuickJS 之后,新增了一种执行引擎:Node.js。但这个 Node.js 属于有限的撑持,考虑到几点:
[*]Node.js 官方不负责维护移动平台 Node.js 的不变实现
[*]把 Node.js 带入手游包,会导致很多不必要的权限申请
[*]对比游戏运行时,编纂开发阶段更需要 Node.js 的能力
所以我们目前只撑持了 win/osx/linux 等桌面平台。但也已经足以大大提升 puerts 用户扩展 Unity 编纂器的能力。
<hr/>简单的示例
也许很多伴侣还不知道 puerts 是什么,这里反复介绍一遍:
puerts 是 Unity/Unreal 的一个 JS 运行环境。以 Unity 为例,你可以在 MonoBehaviour 里如是执行一段 JS :
void Start()
{
jsEnv = new Puerts.JsEnv();
jsEnv.Eval(@”
// js代码
const CS = require('csharp');
let gameObject = new CS.UnityEngine.GameObject('testObject');
CS.UnityEngine.Debug.Log(gameObject.name);
”);
}可以看到,在这个小 js 环境里,你是可以调用任何 Unity 接口的。只需要通过 require('csharp') 就能打开 javascript 到 c# 的大门。
而 puerts 撑持 Node.js 后,你还能打开到 fs、http、buffer (Node.js 内置库们)的大门。
我们在 puerts_unity_demo 里增加了第9号示例,你可以按照它一窥这个功能的用法。这里我贴一段启动 http 处事器的代码:
env = new JsEnv();
// env = new JsEnv(new DefaultLoader(), 9222);
env.Eval(
@”
const http = require('node:http');
http.createServer((req, res)=> { res.writeHead(200);res.end('helloworld') }).listen(9223);
”
);这样运行之后,你在你的浏览器打开 http://localhost:9223,会发现你能打开一个包含 helloworld 的网页。
看到了吗,你甚至能在游戏里创建一个 http 处事器!
以此类推,其实所有 Node.js 的功能你都可以在 Unity 里使用了(已知 process 的某些方式除外,跟 Unity 的进程打点有冲突)
<hr/>复杂的示例
我们在 puerts_unity_demo 中添加了一个更为具体的示例功能,它可以监听 TS 文件的变化,执行 tsc 编译,并将改动热重载至执行引擎里。
[*]监听 ts 并编译这个功能其实是刚需,只要你用 typescript 写逻辑,就必然要颠末这步。
[*]热重载这个功能其实也有很多人呼唤过,因为在游戏调试运行过程中里实时改削逻辑,是游戏开发里的刚需,趁这个机会,我们和社区重要贡献者 throw-out 一起做了一个这个功能的简单实现。
这整个功能我们在 demo 项目中暂且将它定名为 TSC&HotReload。你可以直接下载 demo 查看。
这个例子里有几个值得一说的用法:
ts-node
首先 TSC&HotReload 功能本身也是用 typescript 写的。
因为有了 Node.js 能力,我们不再需要像运行时一样,要先将写好的 ts 编译成 js,再放到 resources 目录加载。
我们完全可以通过 npm 模块 ts-node,直接 require 执行 typescript 文件:
TSC
我们参考 typescript 官方提供的例子 调用 Typescript Compiler API。可以监听某个 ts 文件的变化,一旦变化,就编译 ts 并将 js 写回到文件目录。以后可以直接在 Unity 里进行 typescript 编译而不需要切出去再开一个命令行。
有些开发者可能需要在 js 文件写入后再进行重定名或移动操作(比如 Demo 项目的 QuickStart 例子,将 js 文件添加 txt 后缀并移入 Assets)你也可以在编译后的回调做这种事。
热重载
这个功能的道理基于之前车神的文章:https://zhuanlan.zhihu.com/p/364505146。
我们调用 tsc 编译 typescript 后,我能拿到编译后的文件内容及其路径。
然后我们只需要和 JsEnv 的调试 server 成立一条 websocket 连接,把编译后的内容发送给它。
然后js虚拟机就会让这段新的脚本代替之前的内容运行了。
<hr/>puerts 植入 Node.js 的一部门技术道理
下面来解释一部门往 Unity 注入 Node.js 的道理。
大师都知道,启动 Node.js 一般会使用如下形式:
node index.js这种形式下 Node.js 是以一个独立进程的形式存在的。但 puerts 想要的效果则是在 Unity 进程的主线程里能直接使用 Node.js 的能力,这怎么办呢?
在 Node.js 生态里,其实早就有两个类似的先行者:NW 和 electron。它们将Node.js 植入进 Chromium,并成为了主流的桌面客户端技术。
它们技术成熟之后,Node.js 官方为了鼓励更多类似的例子呈现,在官方文档中提供了一个指引:在你的 C++ 法式中植入 Node.js。
puerts 也就是依照上面这个指引完成的。实际上代码并不复杂,你可以直接参阅 puerts 的代码,大部门代码参考 JSEngine的构造函数 即可。
libnode.dll 和 puerts.dll
node编译
首先如果遵循 Node.js 官方文档的话,我们需要将 Node.js 编译成一个动态库引入。Node.js 官方并没有提供动态库版本 Node.js 的下载,但我们通过查看 Node.js 仓库的 BUILDING.MD 能找到编译方式。
动态库定位
编译出 puerts 之后,我们需要将 node 的动态库和编译出来的 puerts 动态库一同放进 Unity。
此中 windows 相对来说斗劲明确一些,两个 dll 放一起即可。
在 osx 则相对麻烦,当 puerts 链接了 node 动态库时,在最终引用时它还是会回到编译时的路径查找dylib,而不是在当前目录。
解决法子也很简单,设置编译出来的 puerts 的 RPATH 即可,让它在执行时会在同目录下查找dylib。
set(CMAKE_SKIP_RPATH FALSE)
set(CMAKE_INSTALL_RPATH ”@loader_path/”)uv_run 在 Unity 里的措置
Node.js 官方指引代码里有一个并不适用于我们场景的点:事件循环的措置。
如上图所示,官方示例里是用一个 while 循环不竭执行 uv_run 的,也就是这个线程会不竭查抄有没有待执行的 Node.js 异步任务,如果有则执行。这样一个 while 循环如果放在 unity 的主线程里是不成接受的,整个游戏过程城市被直接卡死。
当然解决法子也很简单。puerts 本身就有设计 Tick 这样的周期,我们只需要将 uv_run 放到 Tick 里,游戏(或是引擎编纂器)每一帧更新的时候调用 Tick 就可以了。
如上措置之后其实有一个副感化:Node.js 措置事件的速度会比原版 Node.js 更慢(最坏情况下要延迟一帧),大部门情况其实没有问题,但如果你是用主线程的 Node.js 环境措置超大数据量的 IO,吞吐量会受到影响。当然,这种事情一般没有必要在主线程做,对吧?你完全可以在子线程的 jsEnv 做这种事,而且用 while 而不是 Unity 的 Update 来驱动 Tick。 标题党[撇嘴] 意犹未尽阿,感觉还有很多没说 最近因为想给某个程序引入Node.js的插件系统,在研究嵌入Node.js,遇到了一个问题:Windows下Node.js的native模块是通过PE delay load来导入napi的,导入表里面写的文件名是node.exe。但是为了允许用户对主程序改名,模块中还有个delay load hook,如果node.exe的模块地址获取失败的话就会返回当前进程主模块的地址。这个方案在Node.js作为主exe运行时很好,然而作为DLL的情况下就有问题了。我个人想到的解决方案是,利用delay load获取不到函数地址会抛出异常的特性,注册一个Vectored Exception Handler,截获这个异常,然后返回正确的地址。不过Linux下是什么情况我还没调查过。 想请问下往unity里引入node.js有什么实际的应用场景吗
页:
[1]