CocosCreator & Steam 开发手记(3):CocosCreator 与 Electron 通信
单核满载的C爷
编辑于 2022年06月05日 02:36
收录于文集
共3篇

写在前面

        小伙伴们端午安康鸭~

        本篇是系列专栏的第三篇。如需了解其他内容,可前往:

开发环境

        与前文一致

        到目前为止,我们 exe 有了,Steamworks 也接入了,已经满足了上架 Steam 的所有要求。那这就是我想要的 PC 游戏了吗?并不是,我总觉的没那味儿,终究是少了什么。

        思前想后我终于发现:游戏的设置界面里居然没有设置分辨率,以及切换 窗口/全屏 的选项啊!这才是 PC 游戏的灵魂,是它与手游最大的区别,必须安排上!

        实现的思路也非常简单呀,我们只需要从 CocosCreator 的游戏逻辑出发,调用 Electron 的相关接口就好了。

        那么话不多说,开整!

0、了解 Electron 流程模型

        这次不同以往,我们必须先对 Electron 的流程模型有一些基本的认识,不然后边的内容可能会看着一头雾水。

        该模型主要由三部分组成:主进程、渲染器和预加载脚本。

        1. 主进程:每个 Electron 应用都有一个单一的主进程,作为应用程序的入口点,对应到我们项目中的 EleProj/index.js。

Electron 主进程入口

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

游戏运行在 BrowserWindow 中

        3. 预加载脚本:它包含了那些执行于渲染器进程中,且先于网页内容开始加载的代码 ,充当桥梁的角色。目前我们还没有创建它,这也是我们将要做的事。

        更多详细信息可参考官方文档,链接:https://www.electronjs.org/zh/docs/latest/tutorial/process-model

一、CocosCreator → Electron

        了解了 Electron 流程模型后就可以正式开始了。(注:出于尽量简单的目的,演示中的目录结构和代码组织并不合理,请结合实际项目自行调整。)

  1. 在主场景下添加一个空节点,再创建一个同名脚本,并将脚本挂载到节点上。

新增EleNode节点

  2. 在 EleNode.ts 脚本中写入以下代码:

代码块
JavaScript
自动换行
复制代码
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 预加载脚本,并写入以下代码:

代码块
JavaScript
自动换行
复制代码
// 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:

代码块
JavaScript
自动换行
复制代码
// 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 成就的话,可重复以上流程,并在接收到你定义的消息后执行以下逻辑:

代码块
JavaScript
自动换行
复制代码
/** 获得成就回调 */
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) })
}
复制成功

二、Electron → CocosCreator

        虽然实现了切换 窗口/全屏 显示的功能,但它的逻辑其实是不严谨的:我们在实际切换前就更新了 this._isFull 的值(注意进程间的通信是异步的)。

        试想一下,如果这期间出了什么岔子导致切换失败,那 this._isFull 的值不就错了吗?

        因此,比较合理的做法应该是:在切换成功后将结果返回给 CocosCreator 以便更新缓存,并打印到控制台进行验证。

  1. 默认情况下,渲染器进程的日志是不会在控制台打印的,我们需要在 index.js 中增加以下代码:

代码块
JavaScript
自动换行
复制代码
...
// 解决steam overlay无效的问题
app.commandLine.appendSwitch('--in-process-gpu')
// 打印 Chromium's 日志到 stderr
app.commandLine.appendSwitch('--enable-logging')
...
复制成功

        (注:虽然它在调试的时候很有用,但上线前务必记得删除。)

  2. 继续修改 index.js,在设置是否全屏后,向渲染器发送消息:

代码块
JavaScript
自动换行
复制代码
...
/** 设置是否全屏回调 */
function onSetFullScreen(event, bool) {
    if (mainWindow) {
        mainWindow.setFullScreen(bool)
        // 向渲染器发送消息"setFullScreenResult",并传递一个参数bool
        mainWindow.webContents.send('set_full_screen_result', bool)
    }
}
...
复制成功

  3.  修改 preload.js 预加载脚本,增加方法,监听主进程消息:

代码块
JavaScript
自动换行
复制代码
// 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 脚本:

代码块
JavaScript
自动换行
复制代码
...
@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

        那么以上就是全部内容啦,如果对你有帮助,求个一键三连。

        如果阅读时遇到什么问题,欢迎留言或私信我,我都会回复的(不管我会不会),大家一起进步嘛啊哈哈哈哈哈哈。