找回密码
 立即注册
查看: 852|回复: 10

UE4下基于V8实现的代码热刷新

[复制链接]
发表于 2024-1-19 21:44 | 显示全部楼层 |阅读模式
如果能做到进程运行着就能刷新代码,这对游戏这类重状态业务的开发效率提升是很有辅佐的,设想我们颠末一系列的登录,进副本,甚至战斗到一按时间才呈现的bug,没这功能我们改完代码还需要重操作一次,如果没改对,还得继续重来。。。
Puerts结合V8提供的一些能力和自身的特点,做了一套较为完善的代码热刷新撑持,我们先看看效果,然后看看是怎么实现的。
疗效篇

先甩个helloworld级此外演示:
虽说演示的代码十分简单,但也涵括了一个实用的代码热刷新的几个要点:

  • 最基本运行时代码刷新
  • 状态保持,例子中类成员,脚本变量的值热更前后是保持的
  • 匿名函数的更新,这在UI、网络等事件中可能十分常用
可能有人说这例子太简单了,会不会复杂点就hold不住了了,好,再来个花里胡哨点的演示:
疗程篇

其实我但愿做一堆底层分析,写一堆高深莫测的代码来实现这功能。可惜V8没有给我这个装B的机会,它已经内置了刷代码的功能:Inspector的“Debugger.setScriptSource”命令。我们只要监听文件的变化,用url和新代码即可刷新指定代码。
虽说提供了这么个功能,但用起来其实也并不简单,因为“Debugger.setScriptSource”并不是一个C++函数,而是一个RPC动静,而且调用这个RPC之前,还要做一些交互,我下来分享下找到这方式到实现这功能的一个完整思考。
线索
一次偶然且惊喜的发现Chrome Dev Tools链接到Puerts,在上面改削代码然后“Ctrl+S”竟然能热刷新代码。这也能实现我们部门需求了,但还有两个问题:

  • 如果只能通过Dev Tools去改削代码,还是有点割裂,最好能够改削TypeScript后,一条龙的完成蓝图以及JS的刷新
  • 有些场景需求监听“热刷新”事件,以做一些特殊的措置,比如UI界面的重刷。
分析
抓包发现Chrome Dev Tools是通过inspector协议的“Debugger.setScriptSource”命令去更新代码。
第一个想到的是分析“Debugger.setScriptSource”的实现,如果它是调用V8某个API实现的,那么我们直接调用那接口好了。
但遗憾的是,“Debugger.setScriptSource”调用的相关V8接口,都是private的。这功能他仅仅以inspector的方式开放。
方案确定
Dev Tools和Puerts是用WebSocket来交互,而我们是同进程就完全没必要,只要直接动员静给inspector即可。此外,由于inspector协议是JSON动静,我把和inspector交互的逻辑放到js里头,由于js撑持await,还可以很便利的实现twoway rpc等待。
RPC封装关键代码:
function messageHandler(str) {
    let msg = JSON.parse(str);
    if (msg.method === ”Debugger.scriptParsed”) {
        parsedScript.set(msg.params.scriptId, msg.params.url);
        parsedScript.set(msg.params.url, msg.params.scriptId);
    } else if (typeof msg.id === ”number”) {
        if (msg.result && pendingCommnand.has(msg.id)) {
            const resolve = pendingCommnand.get(msg.id);
            pendingCommnand.delete(msg.id);
            resolve(msg.result);
        } else {
            console.error(”unexpect inspector message:” + str);
        }
    }
};

let commandId = 0;

function sendCommand(method, params) {
    return new Promise((resolve, reject) => {
        commandId++;
        pendingCommnand.set(commandId, resolve); dispatchProtocolMessage(JSON.stringify({”id”:commandId,”method”:method,”params”:params}));
    });
}
代码说明:

  • inspector封装为一个动员静的接口dispatchProtocolMessage,和一个接动静的回调
  • twoway动静封装成一个异步rpc:sendCommand
  • 接动静的回调,如果是reply动静,则唤醒异步rpc函数
  • 接动静的回调还措置一个很重要的V8 oneway动静:Debugger.scriptParsed,V8在加载完脚本,会为脚本分配一个ID,然后通过Debugger.scriptParsed把脚本url和id的映射关系通知给inspector客户端。我们收到这个动静就记录下映射关系。
封装好RPC交互,交互逻辑就十分简单了。
初始化关键代码,其实就是发了两个命令过去:
async function enableDebugger() {
    //...
    await sendCommand(”Runtime.enable”, {});
    await sendCommand(”Debugger.enable”, {”maxScriptsCacheSize”:10000000});
}代码热更新关键逻辑:
async function reload(moduleName, url, source) {
    //找url对应的id
    if (scriptId) {
        if (typeof source === ”string”) {
            let orgSourceInfo = await sendCommand(”Debugger.getScriptSource”, {scriptId:”” + scriptId});
            source = (”(function (exports, require, module, __filename, __dirname) { ” + source + ”\n});”);
            if (orgSourceInfo.scriptSource == source) {
                return;
            }
            await sendCommand(”Debugger.setScriptSource”, {scriptId:”” + scriptId,scriptSource:source});
        }
    }
};代码解释:

  • * 先获取虚拟机内的老代码,如果老代码和新代码一样,就不更新
  • * 调用Debugger.setScriptSource更新代码
搞定啦!就那么简单
发表于 2024-1-19 21:45 | 显示全部楼层
大佬,能支持unity吗[发呆]
发表于 2024-1-19 21:46 | 显示全部楼层
ue4支持 JavaScript?我记得只有c++
发表于 2024-1-19 21:46 | 显示全部楼层
安装puerts就可以支持,通过它,写的TypeScript类还能被UE4编辑器识别,可以看下这篇介绍:车雄生:跟我用TypeScript做一个FPS游戏
发表于 2024-1-19 21:47 | 显示全部楼层
和腾讯那套unlua有啥优势?
发表于 2024-1-19 21:47 | 显示全部楼层
https://www.bilibili.com/video/BV1oB4y1A7dY
发表于 2024-1-19 21:47 | 显示全部楼层
虚幻不是有 live ++?  热更新c++
发表于 2024-1-19 21:48 | 显示全部楼层
车神!
发表于 2024-1-19 21:49 | 显示全部楼层
v8 inspector 确实已经干了很多事[捂嘴]
发表于 2024-1-19 21:50 | 显示全部楼层
大神 parsedScript 这个是什么?
懒得打字嘛,点击右侧快捷回复 【右侧内容,后台自定义】
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

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

GMT+8, 2024-11-22 09:46 , Processed in 0.153889 second(s), 26 queries .

Powered by Discuz! X3.5 Licensed

© 2001-2024 Discuz! Team.

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