用 Cocos + Electron 一套代码双端部署:互动影游的 Web H5 与 Steam Windows 版实践
版本标注:Cocos Creator 2.4.15 (2.x 系列);Electron 27+;electron-builder 24+。全文以 2.4.15 为准,涉及 3.x 差异处单独说明。
内容类型:进阶方案 + 踩坑记录。阅读前置:了解 Cocos Creator 基本构建流程与 Node.js。
一、为什么是 Cocos + Electron
互动影游(Interactive Movie / FMV Game)以预渲染视频 + 分支选择 + 轻量交互为核心特征:资源以视频为主,逻辑并不复杂,但分发渠道多样------既想放到官网/CDN 直接网页播放,也想上 Steam 卖 Windows 版。
两条看似不同的路,本质可以收敛为同一套产物:
- Web H5 路线 :Cocos 构建
web-mobile→ 部署到服务器/CDN → 浏览器打开。 - Windows/Steam 路线 :同一个
web-mobile目录 → 用 Electron 作为「壳」加载本地 H5 → 打包成.exe/安装包 → 提交 Steam。
核心结论:Cocos 只负责生产一份 web-mobile 静态目录,Electron 只负责在桌面端把它当网页打开。 代码只有一套,差异仅在于"谁提供这个网页"。
二、Cocos 侧:构建 Web Mobile
在 Cocos Creator(本文以 2.4.15 为例)中打开 项目 → 构建发布 ,平台选择 Web Mobile:
- 渲染模式:默认即可。
- MD5 缓存:建议勾选,便于网页端 CDN 缓存更新。
- 首包 / 远程资源 :互动影游视频体积大,不要走远程资源分包逻辑里复杂的配置,保持简单(见第五节资源策略)。
- 构建完成后,产物位于
build/web-mobile/,结构如下:
bash
web-mobile/
├── index.html # 入口
├── main.js # 启动脚本
├── src/ # 引擎与设置(settings.json 等)
├── assets/ # 图集、脚本、配置
└── videos/ # 你额外放入的影游视频(建议独立目录)
关键约束 :Cocos 启动时通过 fetch/XHR 加载 src/settings.json 等文件。这意味着 web-mobile 不能以 file:// 协议直接打开 (浏览器会因 origin 为 null 拦截跨域请求,导致白屏)。这一点直接决定了 Electron 的加载方式,见第三节。
三、Electron 侧:加载本地 H5 的三种方案
方案 A:win.loadFile()(file://)------ ⚠️ 不推荐用于 Cocos
js
mainWindow.loadFile(path.join(__dirname, 'web-mobile/index.html'));
最简单,但 loadFile 本质仍是 file:// 协议。Cocos 的 fetch 资源加载在 file:// 下会被 CORS 阻断。此方案对 Cocos 产物基本不可用,仅适合纯静态无 fetch 的页面。
方案 B:本地 HTTP 服务器(推荐)
在主进程启动一个极简静态服务器,用 loadURL('http://127.0.0.1:PORT') 加载。彻底规避 file:// 限制,且 H5 与桌面端行为完全一致。
js
// main.js
const { app, BrowserWindow } = require('electron');
const http = require('http');
const fs = require('fs');
const path = require('path');
const mime = { '.html':'text/html', '.js':'application/javascript',
'.json':'application/json', '.mp4':'video/mp4', '.png':'image/png',
'.wasm':'application/wasm' };
// 定位 web-mobile 目录:开发时用工程内路径,打包后从 resources 读取
function getWebRoot() {
const dev = path.join(__dirname, 'web-mobile');
const packed = path.join(process.resourcesPath, 'web-mobile');
return fs.existsSync(packed) ? packed : dev;
}
function startServer(root) {
return new Promise(resolve => {
const server = http.createServer((req, res) => {
let urlPath = decodeURIComponent(req.url.split('?')[0]);
if (urlPath === '/') urlPath = '/index.html';
const file = path.join(root, urlPath);
if (!file.startsWith(root) || !fs.existsSync(file)) {
res.writeHead(404); return res.end('Not Found');
}
res.writeHead(200, { 'Content-Type': mime[path.extname(file)] || 'application/octet-stream' });
fs.createReadStream(file).pipe(res);
});
server.listen(0, '127.0.0.1', () => resolve(server));
});
}
async function createWindow() {
const root = getWebRoot();
const server = await startServer(root);
const { port } = server.address();
const win = new BrowserWindow({
width: 1280, height: 720,
webPreferences: { contextIsolation: true, nodeIntegration: false }
});
// 影游需要视频自动播放:关闭自动播放手势限制
app.commandLine.appendSwitch('autoplay-policy', 'no-user-gesture-required');
win.loadURL(`http://127.0.0.1:${port}/index.html`);
}
app.whenReady().then(createWindow);
3.1 把本地服务器地址告知 Cocos(视频加载地址处理,关键)
本文采用方案 B。在此方案下,视频文件是作为独立文件由本地服务器提供 ,而非走 Cocos 的 assets 资源系统。Cocos 2.4.15 的 VideoPlayer 组件需要用基于本地服务器地址的绝对 URL 来加载视频(resourceType = REMOTE + remoteURL),否则会找不到文件,或回退到 assets 加载、导致桌面端打包后失败。
由于本地服务器端口是动态分配的(server.listen(0)),必须在运行时把实际地址传给渲染进程(Cocos)。推荐两种方式:
方式一:通过 preload 注入全局变量(推荐,最干净)
js
// preload.js
const base = process.argv.find(a => a.startsWith('--base='))?.split('=')[1];
window.__LOCAL_BASE__ = base || '';
主进程创建窗口时把地址作为参数传入:
js
const win = new BrowserWindow({
width: 1280, height: 720,
webPreferences: {
contextIsolation: true, nodeIntegration: false,
preload: path.join(__dirname, 'preload.js'),
additionalArguments: [`--base=http://127.0.0.1:${port}`]
}
});
方式二:通过 URL query 传递
js
win.loadURL(`http://127.0.0.1:${port}/index.html?base=http://127.0.0.1:${port}`);
Cocos 侧(2.4.15 TypeScript)在初始化视频时拼接地址:
ts
// 某视频节点上的脚本
import { _decorator, Component, VideoPlayer } from 'cc';
const { ccclass, property } = _decorator;
@ccclass('MoviePlayer')
export class MoviePlayer extends Component {
@property(VideoPlayer) videoPlayer: VideoPlayer = null;
onLoad() {
const base = (window as any).__LOCAL_BASE__ || ''; // 本地服务器基址;H5 部署时为空
const url = `${base}videos/ep1.mp4`; // 相对路径在 H5 同样可用
this.videoPlayer.resourceType = VideoPlayer.ResourceType.REMOTE;
this.videoPlayer.remoteURL = url;
this.videoPlayer.play();
}
}
要点:
base为空时(纯 H5 部署),videos/ep1.mp4即相对当前页面的地址,与本地服务器路径语义一致,同一套代码双端通用。- 视频务必放在
web-mobile/videos/(随extraResources外置),不要作为 clip 资源挂到组件上,避免被打进 assets/asar,导致桌面端无法流式读取。
方案 C:自定义协议 app://
通过 protocol.registerFileProtocol 注册私有协议,安全性略好,但需逐路径处理,复杂度高于方案 B。对影游这类「整目录提供服务」的场景,方案 B 更省心。
结论:统一采用方案 B。它让桌面端和网页端的运行环境完全等价,问题排查也最简单。
四、互动影游在 Cocos 内的实现要点
- 视频播放(Cocos 2.4.15) :使用
VideoPlayer组件,resourceType设为REMOTE,remoteURL由「本地服务器基址 + 相对路径」拼接(见 3.1 节)。视频文件放在web-mobile/videos/下作为独立文件、由本地服务器提供,不要 作为 clip 资源挂到组件,以免被打进 assets/asar 导致桌面端无法流式读取。H5 部署时基址为空,相对路径videos/ep1.mp4仍可用,双端代码一致。 - 分支选择:用普通 UI 按钮 + 脚本控制节点显隐与场景跳转,逻辑与常规 Cocos 项目一致。
- 存档 :优先
cc.sys.localStorage;Electron 桌面端可进一步将进度 JSON 写入app.getPath('userData')实现跨会话持久化(需通过 preload 暴露接口)。 - 首帧与黑屏:影游对"点击即播"体验敏感,建议在首个用户手势后初始化视频,避免浏览器/Electron 自动播放策略导致黑屏。
五、资源策略:视频与 asar 的取舍
互动影游视频常达数百 MB,asar 打包(默认)会把文件压缩进单一归档,导致:
- 大文件读取变慢,视频无法流式播放;
- 更新时需整体替换 asar。
推荐做法:
json
// package.json(electron-builder 关键字段)
"build": {
"appId": "com.yourstudio.yourmovie",
"productName": "Your Interactive Movie",
"directories": { "output": "dist" },
"files": ["main.js", "preload.js"],
"extraResources": [
{ "from": "build/web-mobile", "to": "web-mobile" }
],
"win": {
"target": ["nsis", "zip"],
"icon": "build/icon.ico"
}
}
将 web-mobile(含 videos/)整体放入 extraResources,不进 asar ,本地服务器直接 fs 流式读取,性能与兼容性最佳。H5 网页部署时,同样把 web-mobile 整体上传即可,路径语义完全一致。
六、Steam 上线要点
-
入口 :Steam 启动项指向 Electron 打包出的
.exe即可,无需改造游戏逻辑。 -
steam_appid.txt :开发期在项目根放
steam_appid.txt(内容为你的 AppID),便于本地调试。 -
Steamworks 集成(可选) :若需成就、云存档、好友,可用 Greenworks(Node 封装的 Steamworks SDK)在 Electron 主/渲染进程调用;纯叙事影游也可暂不上集成。
-
构建上传 :通过 Steamworks 后台配置 Depot、Build 后,用 SDK 自带的 ContentBuilder(SteamCMD) 将
dist/产物(含resources/web-mobile)上传。落地需解决两个问题:关键参数如何获取 与 多分支如何实现自动化。4.1 关键参数获取方法
参数 含义 获取位置 AppID应用唯一标识 Steamworks 后台「应用管理」首页 URL 中的 appid=xxxx,或「技术工具」页DepotID内容仓库 ID Steamworks 后台「SteamPipe → Depot 配置」中创建并查看 发布分支(Branch) 上线通道(default/beta/test) Steamworks 后台「SteamPipe → 分支」配置,可设密码保护分支 ContentBuilder 路径 上传工具 Steamworks SDK 内 tools/ContentBuilder/(含steamcmd.exe与示例脚本)发布账号 具「发布」权限的 Steam 账号 后台「用户管理」授权;首次登录需 Steam Guard 一次性码 4.2 多分支打包构建自动化配置 Steam 的多分支通过 app_build_xxxx.vdf中的SetLive字段区分。为每个分支维护一份app_build配置(仅SetLive不同),再配合一条统一的上传脚本即可。depot_build_12345.vdf(内容清单,所有分支共用):vdf"DepotBuildConfig" { "DepotID" "12345" "ContentRoot" "../../dist/win-unpacked" "FileExclusion" "*.pdb" "IncludeAllRootFiles" "1" }app_build_12345_beta.vdf(beta 分支上线):vdf"AppBuild" { "AppID" "12345" "Desc" "Interactive movie beta" "BuildOutput" "../../output" "Depots" { "12345" "depot_build_12345.vdf" } "SetLive" "beta" }(
default分支只需将SetLive改为default,test同理。)自动化串联:用 npm script 把「Cocos 命令行构建 → electron-builder 打包 → SteamCMD 上传」串成一条命令,并用环境变量
STEAM_BRANCH选择分支:json"scripts": { "build:h5": "CocosCreator --project . --build platform=web-mobile", "build:win": "electron-builder --win nsis", "steam:upload": "node scripts/steam-upload.js", "release": "npm run build:h5 && npm run build:win && npm run steam:upload" }scripts/steam-upload.js根据分支选择对应 vdf 并调用 SteamCMD:jsconst { execFileSync } = require('child_process'); const branch = process.env.STEAM_BRANCH || 'default'; const vdf = `scripts/app_build_12345_${branch}.vdf`; execFileSync('tools/ContentBuilder/builder/steamcmd.exe', [ '+login', process.env.STEAM_USER, process.env.STEAM_PASS, '+run_app_build', `../${vdf}`, '+quit' ], { stdio: 'inherit' });运行:
STEAM_BRANCH=beta STEAM_USER=xxx STEAM_PASS=yyy npm run release。进阶:将
release接入 GitHub Actions / Jenkins,在打 tag(如beta-v1.2)时自动按分支上传,实现「一次提交、自动出包上 Steam」。 -
体积与下载:影游体积大,建议在 Steam 后台合理设置 depot 分包与按需下载。
参考资料(Steam 后台上线图文流程) :技术方案之外,Steam 商店入驻与后台操作(账号/税务认证、获取 AppID 与 DepotID、用 ContentBuilder 提交构建、生成版本并选择分支、设置启动项 exe 与支持系统、发布与定价、切换测试分支、开启云存档、申请序列号等)有完整的图文教程,推荐参考:blog.csdn.net/qq_30100043...。该文与本文互补------本文侧重 Electron 工程化打包与多分支自动化上传,该文侧重 Steamworks 后台的可视化操作步骤,二者结合即可走通从出包到上线的完整链路。
七、常见问题与坑
| 问题 | 根因 | 解决 |
|---|---|---|
| 桌面端白屏 | 用 file:// 加载,Cocos 的 fetch 被 CORS 拦截 |
改用本地 HTTP 服务器(方案 B) |
| 视频无法播放 | 视频进了 asar,无法流式读取 | extraResources 外置,不进 asar |
| 自动播放被拦 | 浏览器/Electron 默认需用户手势 | autoplay-policy=no-user-gesture-required |
| 开发/打包路径不一致 | __dirname 在 asar 内含义变化 |
用 process.resourcesPath 定位 extraResources |
| 网页与桌面表现不一致 | 视频用绝对/远程路径 | 统一用相对路径引用资源 |
| Windows 解码异常 | H5 依赖浏览器解码器,桌面端用系统解码器 | 统一输出 H.264 MP4,兼容性最佳 |
八、发布前检查表(套用模板)
- 已在标题/正文标注 Cocos 版本(本文 2.4.15)
-
web-mobile可在浏览器独立打开(非file://) - Electron 用本地 HTTP 服务器加载,非
loadFile -
web-mobile(含videos/)已配置extraResources,未进 asar - 资源引用全部使用相对路径
- Electron 已把本地服务器地址(动态端口)告知 Cocos,VideoPlayer.remoteURL 据此拼接
- 视频格式统一为 H.264 MP4
- 已处理自动播放策略(影游必需)
- 安装包 icon、appId、productName 已配置
- Steam
steam_appid.txt与 Depot 配置就绪 - 网页端与桌面端均实测可通关至结局分支
九、总结
Cocos + Electron 的组合让互动影游实现一次构建、双端分发 :Cocos 产出 web-mobile 静态目录,H5 路线直接部署,Windows/Steam 路线用 Electron 起本地服务器加载同一目录。落地的三个技术关键点是------拒绝 file:// 改用本地 HTTP 服务、视频等资源外置 extraResources 不进 asar、资源引用统一相对路径 。满足这三点,即可在同一套代码基础上,低成本同时覆盖网页与 Steam 两大渠道。其中视频加载地址由 Cocos 在运行时按本地服务器基址动态拼接 remoteURL,H5 部署时基址为空、退化为相对路径,双端逻辑完全一致。此外,Android 与 iOS 同样可通过系统 WebView 加载同一 H5 链接(或将 web-mobile 作为本地资源由 WebView 打开),实现移动端覆盖,进一步延展「一套代码、多端分发」的边界。欢迎试玩www.huyingpai.com/#/home Steam链接:store.steampowered.com/app/5129220...