限时福利领取


背景与痛点

把实时对战做进 Cocos Creator 时,最痛的往往不是逻辑,而是“网”。
官方给的 WebSocket 只是浏览器接口的搬运工,真到项目里,掉线、重连、乱序、卡顿一样不少。下面这些问题,我踩了不止一次:

  • 连接管理:原生 WebWebSocket 没有“断线重连”概念,弱网一抖,玩家就被踢出房间。
  • 数据序列化:JSON 图方便,但一个 JSON.stringify 就把 60 FPS 降到 30;二进制不会用,又容易写出一堆 readUInt8
  • 性能优化:每帧广播 20 个角色,每个角色 30 字段,峰值 500 msg/s,CPU 直接飙红。
  • 安全:明文 ws:// 一抓一个准,外挂直接发 {"cmd":"killAll"}

一句话:WebSocket 不等于“实时”,能跑起来的那套“框架”才是核心竞争力。

技术选型

方案 优点 缺点 适用场景
HTTP 轮询 实现简单、兼容老后端 延迟高、流量大 弱交互、回合制
Socket.IO 自动降级、房间 API 丰富 包体 +120 KB、需要专用服务端 H5 轻对战
原生 WebSocket 零依赖、可细粒度优化 什么都得自己写 中重度实时、帧同步

结论:Cocos Creator 做多人同屏、帧同步,原生 WebSocket 仍是性价比最高的“毛坯房”,自己装修即可入住。

核心实现

下面代码基于 Cocos Creator 3.8,TypeScript 9,跑在浏览器与原生双端。
设计目标:

  1. 单例 NetManager 全局复用
  2. 自动重连 + 心跳
  3. 支持 protobuf 二进制协议(可选 JSON)
  4. 事件派发,业务层只关心 cmdbody

1. 建立连接与事件监听

// NetManager.ts
import { EventTarget } from 'cc';

enum NetEvent {
    OPEN    = 'open',
    ERROR   = 'error',
    CLOSE   = 'close',
    MESSAGE = 'message',
}

export class NetManager extends EventTarget {
    private static._inst: NetManager;
    public static get inst() {
        return this._inst ?? (this._inst = new NetManager());
    }

    private _ws: WebSocket | null = null;
    private _url: string = '';
    private _reconnectTimer: number = 0;
    private _heartTimer: number = 0;
    private _missedPong: number = 0;

    /** 外部调用:长连接入口 */
    public connect(url: string) {
        if (this._ws?.readyState === WebSocket.OPEN) return;
        this._url = url;
        this._doConnect();
    }

    private _doConnect() {
        this._ws = new WebSocket(this._url);
        this._ws.binaryType = 'arraybuffer'; // 重点:二进制传输
        this._ws.onopen     = () => this._onOpen();
        this._ws.onclose    = () => this._onClose();
        this._ws.onerror    = () => this._onError();
        this._ws.onmessage  = (e) => this._onMessage(e);
    }

    private _onOpen() {
        this._missedPong = 0;
        this._startHeartbeat();
        this.emit(NetEvent.OPEN);
    }

    private _onClose() {
        this._stopHeartbeat();
        this.emit(NetEvent.CLOSE);
        // 断线 3s 后自动重连
        this._reconnectTimer = window.setTimeout(() => this._doConnect(), 3000);
    }

    private _onError() {
        this.emit(NetEvent.ERROR);
        this._ws?.close();
    }

    private _onMessage(e: MessageEvent) {
        // 统一解包
        const buf = new Uint8Array(e.data);
        const cmd = buf[0]; // 第 1 字节命令号
        const body = buf.slice(1);
        this.emit(NetEvent.MESSAGE, cmd, body);
    }

    /** 发送二进制帧 */
    public send(cmd: number, body: Uint8Array) {
        if (this._ws?.readyState !== WebSocket.OPEN) return;
        const pack = new Uint8Array(1 + body.length);
        pack[0] = cmd;
        pack.set(body, 1);
        this._ws.send(pack.buffer);
    }

    /* ===== 心跳 ===== */
    private _startHeartbeat() {
        this._heartTimer = window.setInterval(() => {
            if (this._missedPong > 2) { // 丢 3 次判死亡
                this._ws?.close();
                return;
            }
            this.send(0xFF, new Uint8Array(0)); // 0xFF 为心跳
            this._missedPong++;
        }, 3000);
    }

    private _stopHeartbeat() {
        window.clearInterval(this._heartTimer);
    }
}

2. 业务层监听示例

// GameCtrl.ts
import { NetManager } from './NetManager';
import { proto } from './proto/bundle'; // protobufjs 生成

NetManager.inst.connect('wss://game.example.com/ws');

NetManager.inst.on('message', (cmd: number, buf: ArrayBuffer) => {
    if (cmd === 0x01) { // 角色同步
        const data = proto.RoleSync.decode(new Uint8Array(buf));
        // 更新渲染层
        RoleLayer.inst.sync(data);
    }
});

3. 打包与发送

const role = { id: 1001, x: 1234, y: 5678 };
const body = proto.RoleSync.encode(role).finish();
NetManager.inst.send(0x01, body);

性能优化

  1. 二进制优先:JSON 文本 186 B → protobuf 38 B,带宽降 80%,解析耗时从 1.2 ms 降到 0.15 ms。
  2. 合并发送:每 50 ms 批量合并 20 条消息,减少 send 调用次数,降低内核切换开销。
  3. 对象池:角色同步包复用 Uint8Array,避免 new 造成的 GC 抖动。
  4. 优先级队列:语音、帧同步走高优通道;聊天、道具走普通通道,防止大头包堵队。
  5. 压缩:对 >1 KB 的包头启用 pako.deflate,实测再省 45% 流量,代价 0.3 ms CPU,可接受。

避坑指南

  • 心跳包必须带“回包”校验,否则 NAT 设备会悄悄把连接踢掉。
  • 断线重连要清队列,旧消息再发会乱序;给每条消息加 2 字节 seq,服务端按序重放。
  • 安卓息屏后 45 s 会断 Wi-Fi,回到游戏要先检测 navigator.onLine,再决定是否重连。
  • iOS 切后台 3 min 进程就被挂起,重连后需要重新登录并拉全量快照,防止“幽灵帧”。
  • 并发登录:玩家快速点两次“开始游戏”,可能创建双 WebSocket;单例模式里加 _connecting 标记,保证同时只连一次。

安全考量

  1. 一律 wss://,防止明文被抓包。
  2. 每条逻辑包再做 AES-128-GCM 加密,密钥通过 HTTPS 登录接口下发,一次一密。
  3. 签名:包头附加 4 字节 HMAC-SHA256 截断,服务端拒收伪造包。
  4. 指令白名单:服务端校验 cmd 是否在账号权限表,避免客户端发高级 GM 指令。
  5. 限速:同一 IP 60 包/s 触发阈值,直接拉黑 5 min,防止脚本爆破。

完整流程图

流程图

结语

把 WebSocket 塞进 Cocos Creator 只是起点:帧同步、预测回滚、LOD 网络、服务器帧率……多人实时每往下走一步,都会冒出更细的挑战。
你可以试着把这套 NetManager 移植到微信小游戏的小程序环境,或者把 protobuf 换成 FlatBuffers 做零拷贝,甚至用 WebWorker 把解码线程独立出来。
当延迟 <16 ms、掉线率 <1% 时,玩家才会真正觉得“这游戏不卡”。希望本文的代码与思路,能让你少踩几个坑,把更多时间花在“好玩”本身。欢迎交流你的踩坑记录,一起把实时对战做得更丝滑。

限时福利领取


Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐