在游戏界面创建overlay

背景

最近有个 electron 项目，需要在 3D 游戏运行时，在游戏界面上能出现提醒 UI。

electron window alwaysTop

electron 是有控制窗口置顶的接口的

Copy
const payWindow = new BrowserWindow({
...
});

payWindow.setAlwaysOnTop(true, "screen-saver")
payWindow.setVisibleOnAllWorkspaces(true);

一般情况下确实是有用的，但在游戏界面里不行，会使当前游戏界面退出。但后来发现，在游戏模式设置非全屏模式时，是可以正常在游戏界面上展示的，下面会介绍游戏显示模式。

游戏显示模式

上图是 cs的设置界面，我们可以设置不同的显示模式，全屏跟窗口有什么区别呢？

在窗口模式中，此应用程序与所有运行的应用共享可用的桌面屏幕空间。在全屏模式中，应用程序运行于的窗口将覆盖整个桌面，并隐藏所有运行的应用（包括你的开发环境）。

https://learn.microsoft.com/zh-cn/windows/uwp/graphics-concepts/windowed-vs--full-screen-mode

简单理解就是，游戏设置成窗口模式的话，那么其他窗口也是可以显示的，最终屏幕上呈现的就是多个窗口合成到一起再显示，而全屏模式的话，就只会显示游戏的界面，其他应用的窗口都不会再显示了。

理解了这个区别，我们就能自然知道上面 electron 窗口没法置地显示，是正常的。

overwolf

https://www.overwolf.com/

overwolf 是一个用于创建游戏内应用的开发平台，很适合我们的场景。基本架构是。

在本地启动 Overwolf 程序

加载自己开发的 Overwolf 插件

游戏启动时 Overwolf 插件就能启动并在游戏界面上显示

想要在本地开发插件，还必须要先申请下，说明开发插件的用途，他们审核通过才可以在本地开发和加载调试。经过了好几轮邮件的沟通，终于批准了。

运行起来是这样，确实是可以在游戏界面里置顶显示。

但采用这个方案其实有 3 个问题

首先当然是这个插件是依托 Overwolf 平台，所以必须要先在电脑上安装和配置好

其次我们的需求不是说游戏一启动就显示提示，而是在某个条件下才需要显示，这就需要有个通知插件的机制。一种方式可能是通过服务器中转，下发消息。

最后是，这个平台只支持主流游戏，完整的支持列表在这里 https://www.overwolf.com/supported-games/ ，这个问题挺致命的。

综上所述，这个方案没法采用。

goverlay

这个库看起来很符合我们的需求，它的原理是先注入 dll 到游戏进程里，然后传送窗口的渲染内容给 dll，dll 负责把内容通过 hook directX 的接口在游戏窗口里渲染出来。

这里其实涉及到很多细节，比如

inject.exe 和 goverlay.dll 之间的消息通信是通过 SendMessage https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-postmessagea 传递

dll 是怎么注入的

怎么 hook directX

这里不展开了。

构建 native addon

一开始不知道怎么弄的，一直报上图里这样的错。

后来照着 cmake-js 官方的配置为起点 https://github.com/cmake-js/cmake-js#general ，再一点点加回项目的构建配置，然后直接进入子目录 electron-overlay 去构建才通过。

不过再重新下载了 goverlay 仓库试了下，发现就没有报错了，反正这里走了些弯路。

跑 demo

跑了下 demo，启动 cs游戏，跟着步骤注入，并没有如预期那样，demo 的内容没有在游戏界面里呈现。

延迟

首先是，由于 cs游戏默认是全屏，如果我们聚焦到 demo 应用里，这时游戏界面是会隐藏的，输入 Strike，我们会发现找不到窗口标题是 Strike 的窗口。具体原因未知，但如果我们延迟注入，也就是点击注入按钮，然后把 cs的游戏界面唤出来，这时再注入，这时就能找到 cs的窗口。

https://github.com/hiitiger/goverlay/blob/master/client/src/renderer/main.ts#L12

Copy
// before
injectButton.addEventListener("click", () => {
const title = titleInput.value
ipcRenderer.send("inject", title)
})

Copy
// after
injectButton.addEventListener("click", () => {
const title = titleInput.value
setTimeout(() => {
ipcRenderer.send("inject", title)
}, 5000);
})

路径

然后能注入了，会在控制台发现下面这样的日志

injecting {"windowId":22155402,"processId":14492,"threadId":50188,"title":"Counter-Strike 2"}

然后没有了。

我们需要深入到相关代码，把注入结果打印出来。

https://github.com/hiitiger/goverlay/blob/master/client/src/main/electron/app-entry.ts#L516

Copy
// before
if (window.title.indexOf(arg) !== -1) {
console.log(`--------------------\n injecting ${JSON.stringify(window)}`);
this.Overlay.injectProcess(window);
}

Copy
// after
if (window.title.indexOf(arg) !== -1) {
console.log(`--------------------\n injecting ${JSON.stringify(window)}`);
const result = this.Overlay.injectProcess(window);
console.log(result);
}

发现这样的日志

Copy
injecting {"windowId":22155402,"processId":14492,"threadId":50188,"title":"Counter-Strike 2"}
{
injectHelper: '\\\\?\\C:\\Users\\yun77\\waibao\\goverlay-master\\electron-overlay\\injector_helper.x64.exe',
injectDll: '\\\\?\\C:\\Users\\yun77\\waibao\\goverlay-master\\electron-overlay\\n_overlay.x64.dll',
injectSucceed: false
}

说明需要确保 electron-overlay 目录下有injector_helper.x64.exe和n_overlay.x64.dll

但官方文档里在 https://github.com/hiitiger/goverlay#run-demo 这里没提，不过在https://github.com/hiitiger/goverlay#inject-a-specific-game 提了，反正第一次接触时感觉挺困惑的。

DLL 签名

还是不行，然后也是偶然搜那种启动不起来的 issue，发现了这样的 issue https://github.com/hiitiger/goverlay/issues/70#issuecomment-982464487 作者回复了说要可能对 dll 做签名才能正常注入，后来在首页比较靠后的区域也发现了这样的提示 https://github.com/hiitiger/goverlay#note

一般我们从网上下载的可执行文件执行的话，如果没有签名，windows 系统会提示说文件不安全，这是签名的其中一个用处。

网上找了下签名相关的流程 https://www.digicert.com/kb/code-signing/ev-authenticode-certificates.htm ，前提是证书是需要购买的，而且价格不菲，https://order.digicert.com/step1/code_signing 。 看起来门槛还是蛮高的。

偶然发现还有种思路是，自己创建代码签名证书，手动设置本地电脑信任这个证书，再用这个证书给 dll 签名。相关链接可以参见 https://blog.csdn.net/dounick/article/details/105643285。

trust mode

还是不行。。也是偶尔在翻看 issues 时，有提到 cs有安全模式，https://help.steampowered.com/en/faqs/view/09A0-4879-4353-EF95#whitelist 需要加参数才可以去掉安全模式。

-allow_third_party_software

终于跑起来了。。

实际集成到项目

native addon

第一个问题就是引入 native addon 会报错。

Copy
pluginOptions: {
'style-resources-loader': {
preProcessor: 'less',
patterns: [path.join(__dirname, "./src/assets/less/index.less")]
},
electronBuilder:{
chainWebpackMainProcess(config) {
config.module
.rule("node")
.test(/\.node$/)
.use("native-ext-loader")
.loader("native-ext-loader")
}
}
}

原因是现在主进程的文件现在也会经过 webpack 打包，所以需要配置下对.node 后缀的处理。

injector.exe 会弹出控制台

明明 demo 测试时不会弹出控制台窗口，但集成之后会弹出控制台窗口。如果弹出控制台窗口，势必会导致当前全屏的游戏会被打断。

https://github.com/hiitiger/goverlay/blob/master/electron-overlay/src/utils/win-utils.h#L105

Copy
// before
BOOL ret = CreateProcessW(path.c_str(), (LPWSTR)(cmdLine.c_str()), NULL, NULL, FALSE, 0, NULL, dir.c_str(), &StartupInfo, &ProcInfo);

Copy
// after
BOOL ret = CreateProcessW(path.c_str(), (LPWSTR)(cmdLine.c_str()), NULL, NULL, FALSE, CREATE_NO_WINDOW, NULL, dir.c_str(), &StartupInfo, &ProcInfo);

重新构建下，就不再弹出控制台窗口了。

不稳定

发现鼠标 hover 到 overlay 内容会导致 electron 应用退出，这也是个严重的问题。

后来发现原来是拷贝 goverlay 代码时没有把相关的模块也引入进来。

就是这里 https://github.com/hiitiger/goverlay/blob/master/client/src/main/electron/app-entry.ts#L116 ，只拷贝了代码，但忘了把BrowserWindow在文件顶部引入。。

问题是，electron 应用就直接退出了，完全没有一点错误打印到控制台，而且我在代码里是加了如下的代码的

Copy
process.on("uncaughtException", (err) => {
console.log("perf", err);
});

所以针对 native addon 里面的错误处理后面还是得再看下，尽量能让有报错就暴露。

反作弊

尝试使用 PUBG 游戏测试，发现会出来个弹窗。

BattlEye 是一个反作弊系统，保护我们的游戏和玩家免受黑客、作弊和其他形式的攻击。所以 BattlEye 阻止了我们的 dll 的注入。目前暂不清楚能否绕过这个限制，虽然弹窗里说如果不引发问题，是可以被忽略的。

其他

为了读懂 c++ 的代码，还专门去学习了点 c++ 的知识，比如

https://www.youtube.com/watch?v=ZzaPdXTrSb8

https://www.youtube.com/watch?v=vLnPwxZdW4Y

在一些地方打印日志到文件里

画一些简单的架构图，帮助理解。

总结

就是这个样子，其他游戏可能要多测测，个别游戏如果有问题也很正常。