puerts群上问得最多的一个问题是:为什么npm下载的有些库跑不起来。
不像python、lua、java等语言有个专门的、独立的可执行程序,js虚拟机更多的时候是嵌入到某个宿主里头,比如浏览器、nodejs。js虚拟机实现了某个js标准(比如es5、es6),宿主能力也会通过一些api导出给js使用,比如浏览器的dom操作,nodejs的异步io等。
而puerts则是js虚拟机的另外一个宿主(游戏引擎),向js虚拟机导出的完整的游戏引擎能力。
了解到这些,问题就很好答了:如果仅仅用到某个es规范的js库,它在这些环境可以通用,但如果用到了宿主提供的api则是专用的。
不能用的原因知道了,但禁不住还是想用怎么办?
最容易想到是模拟:你使用的库依赖了哪些原环境的api,新环境实现即可。事实上也有一些尝试在一个环境模拟另一环境的第三方支持。
这方案显而易见工作量大,也很难保证和原api完全一致。
能不能干脆嵌入个nodejs到UE呢?答案是肯定的。可以看笔者之前写的这篇文章《c++游戏服务器嵌入v8 js引擎胎教级教程》 ,里面介绍了怎么在C++程序里头嵌入nodejs,UE也是C++程序,自然也适用。
官方嵌入例子主要做了两个事情:
完成了上述两个工作nodejs就能在宿主程序里跑起来。当然,如果UE和nodejs各玩各的话也没啥意义,所以要实用化,还要加上第三点
对于1,没什么难度,照着官方例子写即可;对于3,puerts已经实现了完善的v8和UE互相访问机制,nodejs也是基于v8,自然可以无缝使用该机制。所以重点是2的实现。
官方的例子是在主线程直接循环等待并处理libuv事件,如果我们也在UE的GameThread这么干会将导致整个界面卡住,行不通。
另开一个线程去调用uv_run?也不行,uv_run在有事件时,需要调用js回调,v8不支持多线程访问,而且多线程也不符合js的语义。
通过UE定时器去调用uv_run。实测功能都正常,只是异步io处理很慢。调用http模块下载一个72.6M的文件,耗时197秒,而nodejs程序不到1秒。
无论把定时器间隔改多小也没什么改善,看UE代码才知道原因:UE定时器最小精度是一帧,一帧才执行一次uv_run,难怪那么慢。
即使找到比定时器更频繁的GameThread轮询方式,占用了GameThread大量时间也不合适,似乎进入了死胡同。
另一个用到nodejs嵌入的是Electron,它会有同样的烦恼么?
终于,找到了Electron创始人zcbenz的这篇文章:《Electron Internals: Message Loop Integration》 ,这是它的中文翻译 。结合文章和代码得知它也需要解决类似的问题,它的解决思路也完全使用于UE引擎。
它的解决思路是:既然问题的根源在于uv_run把io事件等待以及js回调调用绑定在一起,那把他们拆开好了:
可以看下puerts的最终修改 。
关键函数的说明:
这么一改,下载时间大大改善,但由于Task的执行也有延时,和nodejs还是有差距,最终测试结果在6秒左右。
让我们呼应下标题,在UE下启动个典型的nodejs应用试试?
UseNodejs
改为trueconst PORT = 8081
var http = require('http');
http.createServer(function (request, response) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello World\n');
}).listen(PORT);
console.log(`Server running at http://127.0.0.1:${PORT}/`);
UE编辑器插件编写,这是我们最推荐的场景,利用nodejs丰富的组件快速的开发插件,而比起官方的python,用typescript开发能改善插件代码的可维护性。
运行时由于我们的nodejs后端尚未支持手机平台,不太建议,如果游戏只发pc平台,可以尝试使用。