【Cocos Creator 2.4.15】Electron 套壳加载本地 H5:互动影游双端部署实战

用 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 只负责在桌面端把它当网页打开。 代码只有一套,差异仅在于"谁提供这个网页"。

flowchart LR
    A[Cocos Creator 工程] -->|构建 Web Mobile| B[web-mobile 静态目录]
    B -->|部署到服务器/CDN| C[网页 H5 版]
    B -->|Electron 本地 HTTP 加载| D[Windows 桌面应用]
    D -->|提交 Steam| E[Steam 商店 Windows 版]

二、Cocos 侧:构建 Web Mobile

在 Cocos Creator(本文以 2.4.15 为例)中打开 项目 → 构建发布,平台选择 Web Mobile:

  • 渲染模式:默认即可。
  • MD5 缓存:建议勾选,便于网页端 CDN 缓存更新。
  • 首包 / 远程资源:互动影游视频体积大,不要走远程资源分包逻辑里复杂的配置,保持简单(见第五节资源策略)。
  • 构建完成后,产物位于 build/web-mobile/,结构如下:
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://)—— :warning: 不推荐用于 Cocos

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 与桌面端行为完全一致。

// 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 注入全局变量(推荐,最干净)

// preload.js
const base = process.argv.find(a => a.startsWith('--base='))?.split('=')[1];
window.__LOCAL_BASE__ = base || '';

主进程创建窗口时把地址作为参数传入:

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 传递

win.loadURL(`http://127.0.0.1:${port}/index.html?base=http://127.0.0.1:${port}`);

Cocos 侧(2.4.15 TypeScript)在初始化视频时拼接地址:

// 某视频节点上的脚本
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 内的实现要点

  1. 视频播放(Cocos 2.4.15):使用 VideoPlayer 组件,resourceType 设为 REMOTE,remoteURL 由「本地服务器基址 + 相对路径」拼接(见 3.1 节)。视频文件放在 web-mobile/videos/ 下作为独立文件、由本地服务器提供,不要作为 clip 资源挂到组件,以免被打进 assets/asar 导致桌面端无法流式读取。H5 部署时基址为空,相对路径 videos/ep1.mp4 仍可用,双端代码一致。
  2. 分支选择:用普通 UI 按钮 + 脚本控制节点显隐与场景跳转,逻辑与常规 Cocos 项目一致。
  3. 存档:优先 cc.sys.localStorage;Electron 桌面端可进一步将进度 JSON 写入 app.getPath('userData') 实现跨会话持久化(需通过 preload 暴露接口)。
  4. 首帧与黑屏:影游对"点击即播"体验敏感,建议在首个用户手势后初始化视频,避免浏览器/Electron 自动播放策略导致黑屏。

五、资源策略:视频与 asar 的取舍

互动影游视频常达数百 MB,asar 打包(默认)会把文件压缩进单一归档,导致:

  • 大文件读取变慢,视频无法流式播放;
  • 更新时需整体替换 asar。

推荐做法:

// 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 上线要点

  1. 入口:Steam 启动项指向 Electron 打包出的 .exe 即可,无需改造游戏逻辑。

  2. steam_appid.txt:开发期在项目根放 steam_appid.txt(内容为你的 AppID),便于本地调试。

  3. Steamworks 集成(可选):若需成就、云存档、好友,可用 Greenworks(Node 封装的 Steamworks SDK)在 Electron 主/渲染进程调用;纯叙事影游也可暂不上集成。

  4. 构建上传:通过 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(内容清单,所有分支共用):

    "DepotBuildConfig"
    {
        "DepotID" "12345"
        "ContentRoot" "../../dist/win-unpacked"
        "FileExclusion" "*.pdb"
        "IncludeAllRootFiles" "1"
    }
    

    app_build_12345_beta.vdf(beta 分支上线):

    "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 选择分支:

    "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:

    const { 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」。

  5. 体积与下载:影游体积大,建议在 Steam 后台合理设置 depot 分包与按需下载。

    参考资料(Steam 后台上线图文流程):技术方案之外,Steam 商店入驻与后台操作(账号/税务认证、获取 AppID 与 DepotID、用 ContentBuilder 提交构建、生成版本并选择分支、设置启动项 exe 与支持系统、发布与定价、切换测试分支、开启云存档、申请序列号等)有完整的图文教程,推荐参考:https://blog.csdn.net/qq_30100043/article/details/158468223。该文与本文互补——本文侧重 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 打开),实现移动端覆盖,进一步延展「一套代码、多端分发」的边界。

(帖子被作者删除,如无标记将在 24 小时后自动删除)

实战steam链接:Undertone on Steam
官网:https://www.huyingpai.com/#/home
欢迎体验