Cocos Creator 业务与原生通信详解

0 阅读6分钟

在移动应用开发中,越来越多的团队选择将 Cocos Creator 作为跨平台游戏/动画引擎嵌入原生 App(如电商互动、营销小游戏、虚拟展厅等)。此时,业务层(登录、支付、分享、数据上报等)往往分布在原生端和 Cocos 两端,如何实现稳定、高效的双向通信,是保证项目顺利落地的关键。本文基于实际项目经验,系统讲解 Cocos Creator(TypeScript)与原生平台(Android/iOS)的业务层交互方案,并提供可直接落地的代码实践。

一、背景与需求

典型混合架构中,原生 App 负责提供系统能力、账号体系、支付渠道等核心业务,Cocos 负责渲染互动场景、执行游戏逻辑。交互场景通常包括:

  • Cocos 调用原生:获取设备信息、调用分享/支付、请求网络数据、触发震动等。
  • 原生调用 Cocos:用户登录成功后通知 Cocos 更新 UI、推送消息到达、支付回调结果等。

因此需要一套双向、异步、可靠的通信机制,同时兼顾易用性与可维护性。

二、技术选型与通信原理

2.1 Cocos Creator 与原生通信基础

Cocos Creator 底层基于 JavaScriptCore(iOS)或 V8(Android)执行 JS/TS 代码,原生代码可以通过 JSB(JavaScript Binding)桥接调用 JS 函数,反之 JS 也能通过反射或桥接模块调用原生方法。

版本原生调用 JS 方式JS 调用原生方式
Cocos Creator 2.xCocosJavascriptJavaBridge.evalString (Android)
[[AppController share] sendEventToCocos] (iOS)
jsb.reflection.callStaticMethod
Cocos Creator 3.xJsbBridge.sendToScriptJsbBridge.sendToNativejsb.reflection(兼容)

推荐使用 JsbBridge(Cocos 3.6+ 内置,低版本可手动移植),它提供了统一的双向字符串传递接口,便于封装成事件系统。

2.2 设计原则

  • 协议统一:所有通信消息采用 JSON 字符串,包含 cmd(命令名)、data(载荷)、callbackId(可选,用于异步回调)。
  • 事件驱动:原生和 Cocos 分别维护事件监听器,通过 on/off/emit 模式解耦业务逻辑。
  • 线程安全:原生调用 Cocos 必须在主线程(UI 线程)执行;Cocos 调用原生可以异步执行耗时操作。

三、统一通信接口设计

我们在 TypeScript 层封装一个 NativeBridge 单例,对外提供 callNative(method, params)onNativeEvent(eventName, callback) 两个核心 API。

// NativeBridge.ts
interface CallbackEntry {
  resolve: (data: any) => void;
  reject: (error: any) => void;
  timeoutId: number;
}

export class NativeBridge {
  private static instance: NativeBridge;
  private eventHandlers: Map<string, ((data: any) => void)[]> = new Map();
  private pendingCalls: Map<string, CallbackEntry> = new Map();
  private callbackId = 0;

  static getInstance(): NativeBridge {
    if (!this.instance) this.instance = new NativeBridge();
    return this.instance;
  }

  // Cocos 调用原生(支持 Promise 异步回调)
  callNative(method: string, params?: any, timeout = 30000): Promise<any> {
    return new Promise((resolve, reject) => {
      const callbackId = `${method}_${Date.now()}_${this.callbackId++}`;
      const timeoutId = setTimeout(() => {
        this.pendingCalls.delete(callbackId);
        reject(new Error(`Call native method ${method} timeout`));
      }, timeout);

      this.pendingCalls.set(callbackId, { resolve, reject, timeoutId });

      const message = JSON.stringify({
        cmd: method,
        data: params,
        callbackId,
      });

      if (typeof (window as any).JsbBridge?.sendToNative === 'function') {
        (window as any).JsbBridge.sendToNative(message);
      } else if (typeof (window as any).jsb?.reflection?.callStaticMethod === 'function') {
        // 降级方案
        (window as any).jsb.reflection.callStaticMethod(
          'com/example/NativeHelper',
          'call',
          '(Ljava/lang/String;)V',
          message
        );
      } else {
        reject(new Error('Native bridge not available'));
      }
    });
  }

  // 原生调用 Cocos 时触发的事件监听
  onNativeEvent(eventName: string, callback: (data: any) => void): void {
    if (!this.eventHandlers.has(eventName)) {
      this.eventHandlers.set(eventName, []);
    }
    this.eventHandlers.get(eventName)!.push(callback);
  }

  offNativeEvent(eventName: string, callback?: (data: any) => void): void {
    if (!callback) {
      this.eventHandlers.delete(eventName);
      return;
    }
    const handlers = this.eventHandlers.get(eventName);
    if (handlers) {
      const index = handlers.indexOf(callback);
      if (index !== -1) handlers.splice(index, 1);
    }
  }

  // 由原生调用,分发事件或回调
  public onNativeMessage(rawMessage: string): void {
    try {
      const msg = JSON.parse(rawMessage);
      if (msg.type === 'event') {
        const handlers = this.eventHandlers.get(msg.event);
        if (handlers) {
          handlers.forEach(handler => handler(msg.data));
        }
      } else if (msg.type === 'response') {
        const pending = this.pendingCalls.get(msg.callbackId);
        if (pending) {
          clearTimeout(pending.timeoutId);
          if (msg.error) pending.reject(msg.error);
          else pending.resolve(msg.data);
          this.pendingCalls.delete(msg.callbackId);
        }
      }
    } catch (e) {
      console.error('[NativeBridge] parse error', e);
    }
  }
}

四、Cocos 端业务使用示例

4.1 调用原生支付接口

import { NativeBridge } from './NativeBridge';

async function pay(productId: string): Promise<void> {
  try {
    const result = await NativeBridge.getInstance().callNative('pay', {
      productId,
      amount: 6.99,
    });
    console.log('支付成功', result);
    // 更新游戏钻石
  } catch (error) {
    console.error('支付失败', error);
    // 提示用户失败
  }
}

4.2 监听原生登录事件

// 在场景启动时注册监听
NativeBridge.getInstance().onNativeEvent('userLogin', (userInfo) => {
  console.log('收到登录信息', userInfo);
  // 更新游戏内用户头像、昵称等
});

// 页面销毁时移除监听(避免内存泄漏)
onDestroy() {
  NativeBridge.getInstance().offNativeEvent('userLogin');
}

五、原生端实现(Android & iOS)

5.1 Android 实现

5.1.1 初始化 JsbBridge(Cocos 3.x 推荐)

Cocos 3.x 的 JsbBridge 在 Android 端对应 JsbBridge.java,我们在 AppActivity 中初始化回调:

// AppActivity.java
import com.cocos.lib.JsbBridge;

public class AppActivity extends CocosActivity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 设置原生消息处理函数,接收 Cocos 发来的消息
        JsbBridge.setCallback(new JsbBridge.ICallback() {
            @Override
            public void onScript(String message) {
                handleFromCocos(message);
            }
        });
    }

    private void handleFromCocos(String jsonMsg) {
        try {
            JSONObject obj = new JSONObject(jsonMsg);
            String cmd = obj.getString("cmd");
            String callbackId = obj.getString("callbackId");
            JSONObject data = obj.optJSONObject("data");

            switch (cmd) {
                case "pay":
                    // 调用支付 SDK
                    doPay(data, callbackId);
                    break;
                case "getDeviceInfo":
                    sendResponse(callbackId, getDeviceInfo());
                    break;
                default:
                    sendError(callbackId, "unknown cmd");
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private void doPay(JSONObject data, String callbackId) {
        // 模拟异步支付
        new Thread(() -> {
            // 支付结果
            boolean success = true;
            JSONObject result = new JSONObject();
            try {
                result.put("orderId", "123456");
                if (success) {
                    sendResponse(callbackId, result);
                } else {
                    sendError(callbackId, "pay failed");
                }
            } catch (Exception e) {}
        }).start();
    }

    private void sendResponse(String callbackId, JSONObject data) {
        JSONObject resp = new JSONObject();
        try {
            resp.put("type", "response");
            resp.put("callbackId", callbackId);
            resp.put("data", data);
            // 必须在主线程调用 Cocos
            runOnUiThread(() -> JsbBridge.sendToScript(resp.toString()));
        } catch (Exception e) {}
    }

    private void sendError(String callbackId, String errorMsg) {
        JSONObject resp = new JSONObject();
        try {
            resp.put("type", "response");
            resp.put("callbackId", callbackId);
            resp.put("error", errorMsg);
            runOnUiThread(() -> JsbBridge.sendToScript(resp.toString()));
        } catch (Exception e) {}
    }

    // 原生主动调用 Cocos(例如登录成功后)
    private void notifyLoginSuccess(String userId) {
        JSONObject msg = new JSONObject();
        try {
            msg.put("type", "event");
            msg.put("event", "userLogin");
            msg.put("data", new JSONObject().put("userId", userId));
            runOnUiThread(() -> JsbBridge.sendToScript(msg.toString()));
        } catch (Exception e) {}
    }
}

5.1.2 老版本或 jsb.reflection 方式(兼容)

若无法使用 JsbBridge,可通过 CocosHelper 工具类:

public class CocosHelper {
    public static void callCocos(String msg) {
        CocosJavascriptJavaBridge.evalString("window.onNativeMessage && window.onNativeMessage('" + escape(msg) + "')");
    }
}

注意转义特殊字符。

5.2 iOS 实现

iOS 中 Cocos 引擎通过 CCRuntime 执行 JS,原生可通过 [[CCDirector sharedDirector] getVRoot]->_runtime->executeScript() 调用 JS 函数。更简单的方式是利用 JsbBridge

5.2.1 使用 JsbBridge

// AppController.mm
#import "platform/ios/CCEAGLView-ios.h"
#import "platform/ios/CCRuntime.h"
#import "jsb_bridge.hpp"

@implementation AppController

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // 设置接收 Cocos 消息的回调
    se::JsbBridge::setCallback([](const se::JsbBridge::StringType& arg){
        NSString *msg = [NSString stringWithUTF8String:arg.c_str()];
        [self handleFromCocos:msg];
    });
    return YES;
}

- (void)handleFromCocos:(NSString *)jsonMsg {
    NSData *data = [jsonMsg dataUsingEncoding:NSUTF8StringEncoding];
    NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
    NSString *cmd = dict[@"cmd"];
    NSString *callbackId = dict[@"callbackId"];
    NSDictionary *params = dict[@"data"];

    if ([cmd isEqualToString:@"pay"]) {
        [self doPay:params callbackId:callbackId];
    }
}

- (void)doPay:(NSDictionary *)params callbackId:(NSString *)callbackId {
    // 模拟异步
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        BOOL success = YES;
        NSDictionary *result = @{@"orderId": @"xxx"};
        [self sendResponse:callbackId data:result error:success ? nil : @"failed"];
    });
}

- (void)sendResponse:(NSString *)callbackId data:(NSDictionary *)data error:(NSString *)error {
    NSMutableDictionary *resp = [NSMutableDictionary dictionary];
    resp[@"type"] = @"response";
    resp[@"callbackId"] = callbackId;
    if (error) {
        resp[@"error"] = error;
    } else {
        resp[@"data"] = data;
    }
    NSError *err;
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:resp options:0 error:&err];
    if (jsonData) {
        NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
        // 必须在主线程调用
        dispatch_async(dispatch_get_main_queue(), ^{
            se::JsbBridge::sendToScript([jsonString UTF8String]);
        });
    }
}

// 原生主动触发事件
- (void)onUserLogin:(NSString *)userId {
    NSDictionary *event = @{
        @"type": @"event",
        @"event": @"userLogin",
        @"data": @{@"userId": userId}
    };
    NSData *jsonData = [NSJSONSerialization dataWithJSONObject:event options:0 error:nil];
    NSString *jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
    dispatch_async(dispatch_get_main_queue(), ^{
        se::JsbBridge::sendToScript([jsonString UTF8String]);
    });
}
@end

六、最佳实践与注意事项

6.1 通信协议规范

  • 定义清晰的消息清单(如 paysharegetUserInfo),避免随意扩展。
  • 所有回调必须携带 callbackId,超时机制防止永久等待。
  • 错误信息应结构化,包含 codemessage

6.2 性能与安全

  • 避免频繁大字符串传递:图片、日志等大数据通过文件或数据库共享,消息只传路径。
  • 敏感信息加密:如用户 token,避免明文在桥接层泄露。
  • 防重放攻击:callbackId 需包含时间戳和随机数。

6.3 生命周期管理

  • Cocos 场景切换时,及时移除不需要的事件监听。
  • 原生端持有 Activity 上下文时注意弱引用,避免内存泄漏。
  • 游戏处于后台时,原生可缓存待发送消息,恢复时再处理。

6.4 调试技巧

  • 在 Cocos 端打印所有发送/接收的 JSON 日志,并用 JSON.stringify 格式化。
  • 原生端使用 Log.d 记录桥接消息,方便排查序列化错误。
  • 提供 mock 模式,在没有原生环境时用 Web 模拟回调。

6.5 异步处理与线程

  • 原生调用 sendToScript 必须在主线程(Android runOnUiThread,iOS dispatch_get_main_queue)。
  • Cocos 调用原生后,原生耗时操作应在子线程执行,结果再回调回主线程发送。

七、总结

本文介绍了一套基于 JsbBridge 和统一消息协议的原生与 Cocos Creator 业务层交互方案,涵盖接口封装、异步 Promise 化、双端实现及工程实践要点。该方案已在多个生产项目中稳定运行,支持登录、支付、分享等复杂业务场景。