小伙伴们端午安康鸭~
本篇是系列专栏的第三篇。如需了解其他内容,可前往:
https://www.bilibili.com/opus/662400522504896535CocosCreator & Steam 开发手记(3):CocosCreator 与 Electron 通信
与前文一致

到目前为止,我们 exe 有了,Steamworks 也接入了,已经满足了上架 Steam 的所有要求。那这就是我想要的 PC 游戏了吗?并不是,我总觉的没那味儿,终究是少了什么。
思前想后我终于发现:游戏的设置界面里居然没有设置分辨率,以及切换 窗口/全屏 的选项啊!这才是 PC 游戏的灵魂,是它与手游最大的区别,必须安排上!
实现的思路也非常简单呀,我们只需要从 CocosCreator 的游戏逻辑出发,调用 Electron 的相关接口就好了。
那么话不多说,开整!
这次不同以往,我们必须先对 Electron 的流程模型有一些基本的认识,不然后边的内容可能会看着一头雾水。
该模型主要由三部分组成:主进程、渲染器和预加载脚本。
1. 主进程:每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点,对应到我们项目中的 EleProj/index.js。

Electron 主进程入口
2. 渲染器:每个 Electron 应用都会为每个打开的 BrowserWindow 生成一个单独的渲染器进程。如果你还记得第一篇的内容,就会发现我们游戏就运行在这里。

游戏运行在 BrowserWindow 中
3. 预加载脚本:它包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 ,充当桥梁的角色。目前我们还没有创建它,这也是我们将要做的事。
更多详细信息可参考官方文档,链接:https://www.electronjs.org/zh/docs/latest/tutorial/process-model
了解了 Electron 流程模型后就可以正式开始了。(注:出于尽量简单的目的,演示中的目录结构和代码组织并不合理,请结合实际项目自行调整。)
1. 在主场景下添加一个空节点,再创建一个同名脚本,并将脚本挂载到节点上。

新增EleNode节点
2. 在 EleNode.ts 脚本中写入以下代码:
import { Component, EventKeyboard, input, Input, KeyCode, _decorator } from 'cc';
const { ccclass } = _decorator;
// 在preload脚本中暴露的全局变量,稍后我们会创建它
const electron = globalThis.electronAPI
@ccclass('EleNode')
export class EleNode extends Component {
/** 是否全屏 */
private _isFull = false
onLoad() {
// 注册键盘事件监听
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this)
}
onDestroy() {
// 注销键盘事件监听
input.off(Input.EventType.KEY_DOWN, this._onKeyDown, this)
}
private _onKeyDown(event: EventKeyboard) {
if (!electron) {
return
}
switch (event.keyCode) {
// 当按下F11时,切换 窗口/全屏 显示
case KeyCode.F11:
// 调用自定义electron接口,设置是否全屏
electron.setFullScreen(!this._isFull)
// 更新缓存
this._isFull = !this._isFull
break
default:
break
}
}
} 3. 构建项目,将构建好的 web-mobile 文件夹复制到 EleProj 目录下。
4. 在 EleProj 目录下创建 preload.js 预加载脚本,并写入以下代码:
// contextBridge模块负责在隔离的上下文中创建一个安全的、双向的、同步的桥梁。
// ipcRenderer模块负责从渲染器进程到主进程的异步通信。
const { contextBridge, ipcRenderer } = require('electron')
// 暴露全局对象electronAPI,供cocos使用
contextBridge.exposeInMainWorld('electronAPI', {
// 暴露对象方法setFullScreen(),供cocos使用
setFullScreen: (bool) => {
// 向主进程发送消息"setFullScreen",并传递一个参数bool
ipcRenderer.send("set_full_screen", bool)
},
}) 5. 修改 index.js:
// app模块控制应用程序的事件生命周期。
// BrowserWindow模块创建和管理应用程序窗口。
// ipcMain模块负责从主进程到渲染进程的异步通信。
const { app, BrowserWindow, ipcMain } = require('electron')
const path = require('path')
// 加载greenworks模块
const greenworks = require('greenworks')
// 提升窗口对象的作用域到整个文件
let mainWindow = null
// 解决steam overlay无效的问题
app.commandLine.appendSwitch('--in-process-gpu')
/** 设置是否全屏回调 */
function onSetFullScreen(event, bool) {
if (mainWindow) {
mainWindow.setFullScreen(bool)
}
}
/** 创建窗口 */
const createWindow = () => {
mainWindow = new BrowserWindow({
width: 1336, // 整数型 (可选) - 窗口的宽度(以像素为单位)。 默认值为 800。
height: 768, // 整数型 (可选) - 窗口的高度(以像素为单位)。 默认值为 600。
resizable: false, // boolean (可选) - 窗口大小是否可调整。 默认值为 true。
title: "Demo", // string(可选) - 默认窗口标题 默认为"Electron"。 如果由loadURL()加载的HTML文件中含有标签<title>,此属性将被忽略。
webPreferences: { // Object (可选) - 网页功能设置。
preload: path.join(__dirname, 'preload.js') // string (可选) -在页面运行其他脚本之前预先加载指定的脚本,脚本路径为文件的绝对路径。
}
})
mainWindow.removeMenu() // 删除窗口的菜单栏。
// 加载 index.html
mainWindow.loadFile('web-mobile/index.html')
}
// 这段程序将会在 Electron 结束初始化和创建浏览器窗口的时候调用
// 部分 API 在 ready 事件触发后才能使用。
app.whenReady().then(() => {
// 渲染器消息监听
ipcMain.on('set_full_screen', onSetFullScreen)
// restartAppIfNecessary 方法将查看您的可执行文件是否通过 Steam 启动
// 如果返回 true,则在必须时启动 Steam 客户端,然后通过客户端再次启动您的游戏,而您应尽快退出进程。
// 而如果返回 false,则您的游戏是由 Steam 客户端启动,无需进行任何操作。
//
// 它需要一个参数:你的AppID。
// 出于测试目的,我在这里使用 Spacewar 的 AppID - 480。
// 这是 Steamworks SDK 中包含的示例游戏(默认情况下,Steam 上的每个人都自动拥有此游戏)
let relaunch = greenworks.restartAppIfNecessary(480)
if (relaunch) {
app.quit()
}
else {
if (greenworks.init()) {
console.log('Steam inited successfully')
// 创建窗口
createWindow()
}
else {
app.quit()
}
}
})
// 当所有窗口都被关闭的时候退出程序。
app.on('window-all-closed', () => {
app.quit()
}) 我把整个文件的代码贴了出来,因为改动的地方比较零散,自上而下共有:
1)导入 ipcMain 模块和 path 模块
2)提升窗口对象作用域
3)新增设置是否全屏的回调函数
4)简化创建窗口对象时的参数,并配置preload预加载脚本
5)监听渲染器消息
6. 运行 Electron 进行测试,记得先打开 Steam 哈。
不出意外的话,现在可以通过按“F11”进行窗口和全屏的切换啦!
以此类推,如果想解锁 Steam 成就的话,可重复以上流程,并在接收到你定义的消息后执行以下逻辑:
/** 获得成就回调 */
function onGetAchv(event, achievement) {
/**
* greenworks.activateAchievement(achievement, success_callback, [error_callback])
*
* 解锁成就
* `achievement` String 在Steam后台配置的成就
* `success_callback` Function() 成功回调
* `error_callback` Function(err) 失败回调
*/
greenworks.activateAchievement(achievement, () => { console.log("success") }, (err) => { console.log("err: " + err) })
} 虽然实现了切换 窗口/全屏 显示的功能,但它的逻辑其实是不严谨的:我们在实际切换前就更新了 this._isFull 的值(注意进程间的通信是异步的)。
试想一下,如果这期间出了什么岔子导致切换失败,那 this._isFull 的值不就错了吗?
因此,比较合理的做法应该是:在切换成功后将结果返回给 CocosCreator 以便更新缓存,并打印到控制台进行验证。
1. 默认情况下,渲染器进程的日志是不会在控制台打印的,我们需要在 index.js 中增加以下代码:
...
// 解决steam overlay无效的问题
app.commandLine.appendSwitch('--in-process-gpu')
// 打印 Chromium's 日志到 stderr
app.commandLine.appendSwitch('--enable-logging')
... (注:虽然它在调试的时候很有用,但上线前务必记得删除。)
2. 继续修改 index.js,在设置是否全屏后,向渲染器发送消息:
...
/** 设置是否全屏回调 */
function onSetFullScreen(event, bool) {
if (mainWindow) {
mainWindow.setFullScreen(bool)
// 向渲染器发送消息"setFullScreenResult",并传递一个参数bool
mainWindow.webContents.send('set_full_screen_result', bool)
}
}
... 3. 修改 preload.js 预加载脚本,增加方法,监听主进程消息:
// contextBridge模块负责在隔离的上下文中创建一个安全的、双向的、同步的桥梁。
// ipcRenderer模块负责从渲染器进程到主进程的异步通信。
const { contextBridge, ipcRenderer } = require('electron')
// 暴露全局对象electronAPI,供cocos使用
contextBridge.exposeInMainWorld('electronAPI', {
// 暴露对象方法setFullScreen(),供cocos使用
setFullScreen: (bool) => {
// 向主进程发送消息"setFullScreen",并传递一个参数bool
ipcRenderer.send("set_full_screen", bool)
},
// 暴露对象方法onsetFullScreenResult(),供cocos使用
onsetFullScreenResult: (callback) => {
// 主进程消息监听
ipcRenderer.on('set_full_screen_result', callback)
},
}) 4. 回到 CocosCreator,修改 EleNode.ts 脚本:
...
@ccclass('EleNode')
export class EleNode extends Component {
...
onLoad() {
if (electron) {
// 调用自定义electron接口,并传入回调函数
electron.onsetFullScreenResult((event, bool) => {
this._isFull = bool
console.log("cocos this._isFull = " + bool)
})
}
// 注册键盘事件监听
input.on(Input.EventType.KEY_DOWN, this._onKeyDown, this)
}
...
private _onKeyDown(event: EventKeyboard) {
...
switch (event.keyCode) {
// 当按下F11时,切换 窗口/全屏 显示
case KeyCode.F11:
// 调用自定义electron接口,设置是否全屏
electron.setFullScreen(!this._isFull)
break
default:
break
}
}
} 5. 再次构建项目,替换文件,并运行 Electron 进行测试。
不出意外的话,我们按“F11”依然能正常切换显示方式,并且在控制台中会打印以下日志:

收到消息后成功打印日志

至此,不论是“正向通信”还是“逆向通信”,我们都能实现了。但其实还有一种方式,是真正意义上的双向通信,感兴趣的小伙伴可参考官方文档进行学习,链接:https://www.electronjs.org/zh/docs/latest/tutorial/ipc
那么以上就是全部内容啦,如果对你有帮助,求个一键三连。
如果阅读时遇到什么问题,欢迎留言或私信我,我都会回复的(不管我会不会),大家一起进步嘛啊哈哈哈哈哈哈。