UE引擎里头跑个nodejs服务器是怎样一种体验?

2021-11-10 14:36:09 浏览数 (1)

源起

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 程序,自然也适用。

官方嵌入例子主要做了两个事情:

  1. v8、nodejs的初始化工作;
  2. libuv事件循环驱动;

完成了上述两个工作nodejs就能在宿主程序里跑起来。当然,如果UE和nodejs各玩各的话也没啥意义,所以要实用化,还要加上第三点

  1. 和引擎的互相访问;

对于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大量时间也不合适,似乎进入了死胡同。

从197秒到6秒

另一个用到nodejs嵌入的是Electron,它会有同样的烦恼么?

终于,找到了Electron创始人zcbenz的这篇文章:《Electron Internals: Message Loop Integration》 ,这是它的中文翻译 。结合文章和代码得知它也需要解决类似的问题,它的解决思路也完全使用于UE引擎。

它的解决思路是:既然问题的根源在于uv_run把io事件等待以及js回调调用绑定在一起,那把他们拆开好了:

  • 启动一个poll线程绕过libuv的api,直接系统调用(window下用IOCP,linux下用epoll,mac下用select)等待libuv的事件
  • poll线程等到事件,则通知主线程去调用uv_run,此时已经有事件,主线程会直接调用js回调,无需等待。

可以看下puerts的最终修改 。

关键函数的说明:

  • PollEvents:Polling线程的逻辑,调用各平台的异步io处理api去阻塞等待,如果有事件,则调用TaskGraph,让GameThread去执行uv_run,并通过信号量等待GameThread完成。
  • UvRunOnce:GameThread任务的主要逻辑,简单的调用uv_run后,通过信号量通知Polling线程继续Polling。

这么一改,下载时间大大改善,但由于Task的执行也有延时,和nodejs还是有差距,最终测试结果在6秒左右。

试一试?

让我们呼应下标题,在UE下启动个典型的nodejs应用试试?

  • clone这个项目:puerts_unreal_demo
  • 后端引擎切换为nodejs
    • 下载nodejs库 ,并解压到puerts_unreal_demoPluginsPuertsThirdParty目录下
    • 打开puerts_unreal_demoPluginsPuertsSourceJsEnvJsEnv.Build.cs文件,把UseNodejs改为true
  • 修改QuickStart.ts为如下内容并重新编译ts工程
代码语言:javascript复制
const PORT = 8081

var http = require('http');

http.createServer(function (request, response) {
    response.writeHead(200, {'Content-Type': 'text/plain'});

    response.end('Hello Worldn');
}).listen(PORT);

console.log(`Server running at http://127.0.0.1:${PORT}/`);
  • 运行该demo
  • 浏览器输入地址测试一下:http://127.0.0.1:8081/

应用场景

UE编辑器插件编写,这是我们最推荐的场景,利用nodejs丰富的组件快速的开发插件,而比起官方的python,用typescript开发能改善插件代码的可维护性。

运行时由于我们的nodejs后端尚未支持手机平台,不太建议,如果游戏只发pc平台,可以尝试使用。

小结

  • 介绍了UE下嵌入nodejs怎么处理nodejs的事件循环,其它有自己主循环的应用也可以参考这个思路
  • 通过本文可以得知UE下nodejs编程的一个可选方案

0 人点赞