找回密码
 立即注册
查看: 312|回复: 3

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

[复制链接]
发表于 2022-1-8 09:53 | 显示全部楼层 |阅读模式
源起

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++程序,自然也适用。
官方嵌入例子主要做了两个事情:

  • v8、nodejs的初始化工作;
  • libuv事件循环驱动;
完成了上述两个工作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大量时间也不合适,似乎进入了死胡同。
从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_demo\Plugins\Puerts\ThirdParty目录下
    • 打开puerts_unreal_demo\Plugins\Puerts\Source\JsEnv\JsEnv.Build.cs文件,把UseNodejs改为true

  • 修改QuickStart.ts为如下内容并重新编译ts工程
const 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}/`);

  • 运行该demo
  • 浏览器输入地址测试一下:http://127.0.0.1:8081/
应用场景

UE编辑器插件编写,这是我们最推荐的场景,利用nodejs丰富的组件快速的开发插件,而比起官方的python,用typescript开发能改善插件代码的可维护性。
运行时由于我们的nodejs后端尚未支持手机平台,不太建议,如果游戏只发pc平台,可以尝试使用。
小结


  • 介绍了UE下嵌入nodejs怎么处理nodejs的事件循环,其它有自己主循环的应用也可以参考这个思路
  • 通过本文可以得知UE下nodejs编程的一个可选方案
欢迎各种使用、点赞、issue、pr:
发表于 2022-1-8 09:57 | 显示全部楼层
大佬,nodejs 库有点问题呀,打包不了,ue4.27
发表于 2022-1-8 10:04 | 显示全部楼层
ssl符号冲突么?下载最新的libnode库
发表于 2022-1-8 10:09 | 显示全部楼层
新库好使,12.30提交的这也太及时了吧,谢谢
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

小黑屋|手机版|Unity开发者联盟 ( 粤ICP备20003399号 )

GMT+8, 2024-9-22 23:26 , Processed in 0.065417 second(s), 22 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表