Cocos Creator 3.8.8 脱敏通用版 全平台原生架构

原生插件开发与接入指南(脱敏版)

覆盖 iOS / macOS / Android / Windows 全平台生命周期、CMake 构建流、sebind 绑定、线程安全事件入队、自动化脚本与游戏侧接入的一站式完整手册。

约 18 分钟深度阅读 3.3 万字实战经验总结 支持 macOS · iOS · Android · Windows

本文以 MySDK 表示被封装的原生 SDK(占位符,换成你自己的名字即可),说明原生插件的 开发、构建、导出与接入流程,另有四块通用内容单独成章:

章节 内容
原理 插件从 cc_plugin.json 到 JS 函数调用的完整链路
如何创建一个标准插件 从零开始的步骤、最小代码骨架与校验清单
脚本都做了什么 每个脚本逐步在干什么、产物在哪
参考文档 官方手册 / 引擎源码索引
机制原理解析

另有「常见疑问」一章回答了一个高频问题:JS 为什么不直接调 Native,非要包一层 .h/.mm?

01 占位符与阅读约定 #

文中所有名字都是占位符,换成你自己的即可:

占位符 含义
MySDK 被封装的原生 SDK 名称(也用于 include/MySDK.h、ts/MySDK.ts 等)
my_sdk_glue 插件的 CMake target / 目录内 <target>-config.cmake 前缀(三处必须一致,见「原理」)
MySDKNative sebind 注册到 JS 全局的类名
SDKCoreManager / SDKUIManager / SDKBridge / SDKUserInfoModel 各平台 SDK 入口类与数据模型的占位符
MySDK.framework / MySDK_MacOS.framework iOS / macOS SDK 二进制占位符
com.example.mysdk.demo Demo 与游戏包名占位符(SDK 通常按包名注册,别配错)
<TEAM_ID> / <CERT_NAME (CERT_ID)> / <bundle-id>-devOSX 签名 Team / 证书 / 描述文件占位符
/path/to/sdk-packages 原生 SDK 安装包存放目录
核心避坑原则

最重要的一条:编辑器里的「预览浏览器 / 预览模拟器」不会加载原生插件——它们跑的是 通用引擎,不编译 native/native-plugin/,所以里面 MySDK.available === false。 要跑插件必须构建原生包(macOS 本机用 bash scripts/run-mac.sh)。这是 Cocos 里没有 Unity「Editor Play」等价物的原因。


02 工程结构 #

📁 工程目录树
MySDK/                               # 插件目录(可拷进任意 Cocos Creator 工程)
├── cc_plugin.json                   # 插件清单(module target = my_sdk_glue)
├── include/MySDK.h                  # 跨平台 C++ 门面(UserInfo / Callback / MySDK)
├── src/
│   ├── CMakeLists.txt               # 公共 glue 目标(由各平台 config 引入,不直接使用)
│   ├── MySDK.cpp                    # 门面实现 + 线程安全事件队列
│   ├── MySDKInternal.h              # 平台后端契约(platform* / emit* / pollEvent)
│   └── MySDKBindings.cpp            # sebind 注册全局类 MySDKNative + 插件入口
├── ios/  { my_sdk_glue-config.cmake, MySDKios.mm, sdk/, config/, entitlements.plist }
├── android/ { my_sdk_glue-config.cmake, MySDKandroid.cpp, java/, sdk/, README.md }
├── mac/  { my_sdk_glue-config.cmake, MySDKmac.mm, sdk/, config/, entitlements.plist }
├── windows/ { my_sdk_glue-config.cmake, MySDKwin.cpp, sdk/ }
└── ts/MySDK.ts                      # TypeScript 封装(拷贝到游戏 assets)

工程内其它位置:

📄 规范说明
native/native-plugin/MySDK/          # 插件本体(Creator 递归扫描 native/ 找 cc_plugin.json)
assets/scripts/MySDK.ts              # ts/MySDK.ts 的副本(改一处要同步另一处,以插件目录为准)
assets/scripts/MySDKTest.ts          # Demo:运行时生成按钮面板 + 实时日志
scripts/                             # 全部构建/打包/导出脚本(见「脚本都做了什么」)
signing/                             # 签名材料(工程自包含:macOS/iOS 描述文件与 Android keystore)
build-config/{mac,ios,android,windows}.json   # 各平台 CLI 构建参数
dist/                                # 脚本产物(gitignore)
Doc/                                 # 文档

03 原理:插件是怎么被编译、加载、调用的 #

一句话概括:原生插件是一份和引擎静态编译在一起的 C++ 代码。构建期由 Creator 扫描 cc_plugin.json 把它接进 CMake;运行期引擎启动时统一调用插件的入口函数,入口函数用 sebind 把 C++ 函数挂成 JS 全局对象;SDK 的回调则通过「加锁队列 + 每帧轮询」回传给 JS。

构建期:Creator 怎么发现插件、怎么把它编进引擎 #

⚡ 时序 / 架构流程
① 构建原生平台(编辑器 Build / CLI --build)
   Creator 生成 native/engine/<platform>/;该目录 CMake 配置阶段会执行引擎模板
   templates/cmake/common.cmake 的 cc_gen_plugin_cmake_hook()
     └─ node native/cmake/scripts/plugins_parser.js
          递归扫描工程根的 native/、extensions/(最多 7 层)找 cc_plugin.json,
          按 platforms / enabled / engine-version 过滤
          └─ 生成 native/engine/<platform>/Pre-AutoLoadPlulgins.cmake:
                list(APPEND CC_REGISTERED_PLUGINS my_sdk_glue)
                find_package(my_sdk_glue REQUIRED)  ← 找到 <platform>/my_sdk_glue-config.cmake
② 平台 config.cmake 设置变量后 include src/CMakeLists.txt
     └─ add_library(my_sdk_glue ...)                       # 静态库
        target_link_libraries(my_sdk_glue ${ENGINE_NAME})  # 拿到引擎符号与头文件目录
        target_compile_definitions(my_sdk_glue PRIVATE CC_PLUGIN_STATIC=1)
③ cc_plugin_entry() 生成 plugin_registry_autogen.cpp(并给引擎定义 CC_USE_PLUGINS=1):
       extern "C" void cc_load_plugin_my_sdk_glue();
       extern "C" void cc_load_all_plugins() { cc_load_plugin_my_sdk_glue(); /* … */ }
   这就是 CC_PLUGIN_ENTRY 的另一半——插件里写的
       CC_PLUGIN_ENTRY(my_sdk_glue, load_fn);
   会展开成:
       extern "C" void cc_load_plugin_my_sdk_glue() { load_fn(); }
   (target 名对不上 → 链接期报 cc_load_plugin_xxx 未定义)

运行期:插件什么时候执行、怎么挂到 JS #

⚡ 时序 / 架构流程
④ 引擎启动早期 BaseGame::init() 调 cc_load_all_plugins() → load_fn() 执行:
   订阅脚本引擎事件(BusType::SCRIPT_ENGINE 的 Listener,static 生存期)
⑤ 脚本引擎初始化(ScriptEngine::postInit)时发出 POST_INIT 事件 → 监听器被调用:
   se::ScriptEngine::addRegisterCallback(jsb_register)
   (此时只是登记,回调还没执行;脚本引擎的全局对象已经建好)
⑥ ScriptEngine::start() 依次执行所有登记的回调 → jsb_register(globalObj)
   → sebind::class_<MySDKNative>(...).install(globalObj)
   → 从此刻起 JS 全局多了 MySDKNative,每个 staticFunction 都是一个 JS 函数
⑦ JS 调 MySDKNative.init() → 绑定函数(sebind 在这层做 JS ↔ C++ 参数转换)
   → MySDK::getInstance().init() → internal::platformInit() → 厂商 SDK
机制原理解析

为什么绑定要放到 POST_INIT 之后:load_fn 跑在脚本引擎初始化之前,那时还没有 JS 全局对象可挂;POST_INIT 之后引擎才在 start() 里用全局对象回调登记过的注册函数。 (引擎源码里 Bus 的 POST_INIT 已标注 @deprecated since 3.7.0,官方新写法是 cc::events::ScriptEngine 的 AFTER_INIT(engine/EngineEvents.h),行为等价。)

数据回传:为什么是「入队 + 轮询」 #

⚡ 时序 / 架构流程
⑧ SDK 回调(任意线程)→ emit*() 把「事件名 + JSON」压入加锁队列
   (mutex + deque,超过 256 条丢最旧的)
⑨ JS 每帧在 director.EVENT_AFTER_UPDATE 里调 MySDKNative.pollEvent() 取一条
   (空队列返回空串,一帧最多取 64 条)→ JSON.parse → setEventListener 分发
  • 第 ⑧ 步不能直接调 JS:V8 是单线程的,从任意原生线程进 V8 需要 HandleScope 等前置条件, 实测会崩在 v8::HandleScope::CreateHandle() ... without a HandleScope;
  • 入队几乎不耗时,轮询发生在主线程的每帧回调里,既不丢事件也不卡帧;
  • 事件统一格式 {"event":"loginSuccess","data":{...}},游戏侧只认 setEventListener。

设计要点 #

  • target 名必须三处一致:cc_plugin.json 的 modules[].target、CMake 的 add_library(<target> ...)、C++ 的 CC_PLUGIN_ENTRY(<target>, load_fn); export-plugin.sh 的校验就是在查这个(见「脚本都做了什么」)。
  • 静态链接:插件是静态库,CC_PLUGIN_STATIC=1 时入口函数按静态方式定义(不做 dllexport), 由 cc_load_all_plugins() 统一拉起,不依赖运行时动态加载。
  • Pre-AutoLoadPlulgins.cmake(注意官方拼写是 Plulgins)每次 CMake 配置阶段重新生成, 这也意味着新增插件 / 改插件目录后要重新构建才会生效;-DSKIP_SCAN_PLUGINS=ON 可停自动扫描。
  • cc_plugin.json 的 platforms / enabled / engine-version 参与扫描过滤:平台不匹配时 不会被 find_package,自然也就不进 CC_REGISTERED_PLUGINS。
  • 分层:include/MySDK.h(C++ 门面)→ src/MySDKInternal.h(平台契约)→ 各平台 platform* 实现并调 emit*。非 JS 场景可以直接用 C++ 门面(见「对外接口」)。

04 如何创建一个标准插件(从零) #

前置:先构建一次原生平台,生成 native/ #

插件要被扫描到,前提是跑过一次原生平台的构建(编辑器 ≥ 3.6.3,3.8.x 已实测)。 没有专门的「生成 native 目录」脚本——它就是普通的构建动作,三种做法任选:

  1. 编辑器 UI:打开项目 → 顶部「构建发布」面板 → 选平台(macOS / iOS / Android / Windows)→「构建」。

  2. 本工程的脚本(第一步就是构建;后面还会编译 / 运行 / 打包): bash scripts/run-mac.sh、build-mac.sh、build-ios.sh、build-android.sh、build-all.sh。

  3. 裸 CLI(没有脚本、CI 场景):

    💻 Shell / Bash
    "/Applications/Cocos/Creator/3.8.8/CocosCreator.app/Contents/MacOS/CocosCreator" \
        --project /path/to/project \
        --build "configPath=/path/to/project/build-config/mac.json"
    # 没有 build-config 时也可以直接指定:
    #   --build "platform=mac;debug=true"
    

    ⚠️ Cocos CLI 构建成功也可能返回退出码 36(只生成原生工程、不跑 xcodebuild),别当失败。

这一步里 Creator 实际做了两件事(以 macOS 为例,日志见 build/logs/cocos-mac.log):

⚡ 时序 / 架构流程
Creator 生成 native/engine/<platform>/
  └─ 调用自带 cmake 配置引擎工程(Resources/tools/cmake/bin/cmake):
       cmake -S native/engine/mac -GXcode -B build/mac/proj -DCMAKE_SYSTEM_NAME=Darwin ...
         └─ 执行 cc_gen_plugin_cmake_hook() → node plugin_parser.js
              [searching plugins] plugin dir found: .../native/native-plugin/<你的插件>
         └─ 生成 native/engine/<platform>/Pre-AutoLoadPlulgins.cmake
         └─ 生成 build/<platform>/proj/ 下的 Xcode 工程 + plugin_registry_autogen.cpp

也就是说插件扫描发生在这一步的 CMake 配置里,与随后的 xcodebuild / Gradle 编译无关。 判断「前置」完成:

  • 构建日志里有 [searching plugins] plugin dir found: ...;
  • native/engine/<platform>/Pre-AutoLoadPlulgins.cmake 里能看到你的 target;
  • build/<platform>/proj/plugin_registry_autogen.cpp 里有 cc_load_plugin_<target>()。

其它平台同理:iOS 与 macOS 一样由 Creator 构建阶段跑 CMake;Android 的 CMake 配置由 Gradle 在编译时调用(需要 NDK),扫描结果同样写进 native/engine/android。

目录归属:native/engine/** 是生成物(不要手改,改插件要改自己的 cmake 后重新构建); 插件放 native/native-plugin/——不存在就自己建,Creator 不会动里面的东西。

建目录 + cc_plugin.json #

📁 工程目录树
<project>/native/native-plugin/hello/
└── cc_plugin.json
🟡 JSON
{
  "name": "hello",
  "version": "1.0.0",
  "author": "you",
  "description": "demo native plugin",
  "engine-version": ">=3.6.3",
  "disabled": false,
  "modules": [{ "target": "hello_glue" }],
  "platforms": ["mac", "ios", "android", "windows"]
}

modules[].target 就是后面所有地方要一致的名字;platforms 用于按平台过滤。

写 C++:业务、绑定与插件入口 #

src/hello_glue.cpp(最小可跑骨架):

🔷 C++
#include <string>
#include "bindings/sebind/sebind.h"
#include "bindings/jswrapper/SeApi.h"
#include "plugins/Plugins.h"
#include "plugins/bus/EventBus.h"

namespace {
/// 只用来当静态函数命名空间
struct HelloNative {};

bool jsb_register(se::Object* globalThis) {
    sebind::class_<HelloNative> kls("HelloNative");
    kls.staticFunction("greet", +[](const std::string& n) { return "hello " + n; });
    kls.install(globalThis);
    return true;
}

/// 插件入口:脚本引擎就绪后注册绑定
void load_hello_plugin() {
    using namespace cc::plugin;
    static Listener listener(BusType::SCRIPT_ENGINE);
    listener.receive([](ScriptEngineEvent event) {
        if (event == ScriptEngineEvent::POST_INIT) {
            se::ScriptEngine::getInstance()->addRegisterCallback(jsb_register);
        }
    });
}
} // namespace

// 必须与 cc_plugin.json 的 target 一致
CC_PLUGIN_ENTRY(hello_glue, load_hello_plugin);
版本兼容与本地验证

3.7+ 的官方范例改用 #include "engine/EngineEvents.h" 拿 ScriptEngineEvent; 本工程(3.8.8)直接用 plugins/bus/EventBus.h,两种写法在该版本下都可工作。

src/CMakeLists.txt(公共 glue 目标) #

⚙️ CMake
add_library(hello_glue
    ${CMAKE_CURRENT_LIST_DIR}/hello_glue.cpp
    ${HELLO_PLATFORM_SOURCES}          # 由各平台 config 设置
)
target_include_directories(hello_glue PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../include)
target_compile_definitions(hello_glue PRIVATE CC_PLUGIN_STATIC=1)
target_link_libraries(hello_glue ${ENGINE_NAME} ${HELLO_LINK_LIBS})

每个平台一个 <platform>/hello_glue-config.cmake #

约定:文件名 = target 名 + -config.cmake,find_package 靠它找到插件。内容分两种:

  • 源码分发(本工程做法):设置变量 → include(${CMAKE_CURRENT_LIST_DIR}/../src/CMakeLists.txt):

    ⚙️ CMake
    set(HELLO_PLATFORM_SOURCES ${CMAKE_CURRENT_LIST_DIR}/hello_ios.mm)
    set(HELLO_LINK_LIBS "-framework Foundation")
    include(${CMAKE_CURRENT_LIST_DIR}/../src/CMakeLists.txt)
    
  • 二进制分发(官方 tutorial 做法):直接 add_library(hello_glue STATIC IMPORTED) / add_library(... INTERFACE),target_link_libraries 指向预编译的 .a / .framework。

构建验证 #

构建一次对应原生平台,检查:

  • 构建日志出现 [searching plugins] plugin dir found: .../hello;
  • native/engine/<platform>/Pre-AutoLoadPlulgins.cmake 里出现 hello_glue;
  • 编译链接通过;运行时 cc_load_all_plugins() 会拉起你的插件。

JS 验证与封装 #

原生包启动后,JS 里能直接访问注册的全局对象(HelloNative.greet('x'))。之后按「对外接口」的 约定写一张 TS 包装(建议同时用 typeof HelloNative !== 'undefined' 做降级, 让 Web / 编辑器预览里安全 no-op)。

常见坑清单 #

  • 名字三处不一致 → 链接期报 cc_load_plugin_xxx 未定义,或插件没被 find_package 到;
  • 改了 cc_plugin.json / 新增插件目录后没有重新构建:需要让 CMake 重新跑一次,才会重扫;
  • 预览浏览器 / 模拟器里永远看不到插件效果(见开头提示);
  • 原生线程里直接调 JS → v8::HandleScope::CreateHandle() ... without a HandleScope 崩溃, 一律走「入队 + JS 轮询」(见「原理」);
  • Apple 平台导入 framework 头文件需要编译选项 -F<dir>(只有 -I 找不到 <Framework/Header.h>);
  • 静态 SDK 依赖要用 -force_load + -ObjC 全量链进来(否则 ObjC 分类/类会被裁掉)。

05 架构 #

⚡ 时序 / 架构流程
JS (MySDK.ts)
  └─ pollEvent()  每帧轮询 ─────────────┐
MySDKNative (sebind 注册的全局类)        │
  └─ MySDK::getInstance()               │  ← include/MySDK.h(C++ 门面)
       └─ internal::platform*()         │  ← src/MySDKInternal.h(平台契约)
            ├─ iOS:     MySDKios.mm      → SDKUIManager / SDKCoreManager + NSNotification
            ├─ macOS:   MySDKmac.mm      → SDKCoreManager + NSNotification
            ├─ Android: MySDKandroid.cpp → JNI → SDKBridge.java → SDKUIPlatform
            └─ Windows: MySDKwin.cpp     → LoadLibrary + C 接口
   internal::emit*()  ──写入加锁队列──→  同上,JS 轮询取出后分发给 setEventListener 的回调

回调模型、线程安全、分层原因见「原理」。


06 对外接口 #

TS(游戏侧) #

🟦 TypeScript
import { MySDK, MySDKEvent } from './MySDK';

MySDK.setEventListener((event, data) => {
    switch (event) {
        case MySDKEvent.InitSuccess: MySDK.login(); break;          // data: {}
        case MySDKEvent.LoginSuccess: console.log(data.uid); break; // data: {uid,token,loginMethod,...}
        case MySDKEvent.LoginFail: break;                           // data: {code,message,loginMethod}
        case MySDKEvent.OpenUserCenterSuccess: break;
        // LoginCancel / DisagreePrivacy / LogoutSuccess / LogoutFail / InitFail ...
    }
});

MySDK.setDebug(true);               // 注意:macOS 上会阻塞调用线程约 6s(SDK 行为,非 bug)
MySDK.init();                       // 初始化(结果走回调)
MySDK.login();                      // 登录界面
MySDK.openUserCenter();             // 用户中心
MySDK.logout(); MySDK.switchAccount();
MySDK.isLogin(); MySDK.getUserInfo(); MySDK.getVersion();
MySDK.setLanguage('en'); MySDK.setConfigAppId('1000001');  // 多套配置时

C++(非 JS 场景) #

非 JS 环境(例如引擎二次开发、纯 C++ 游戏模块、原生独立工具或单元测试)可直接使用插件的跨平台 C++ 门面,无需经过 JS 脚本引擎层。

1. 核心流程与代码骨架 #

调用方包含门面头文件 #include "MySDK.h",继承 mysdk::Callback 接收原生异步事件,并通过 MySDK::getInstance() 驱动生命周期:

🔷 C++
#include "MySDK.h"
#include <iostream>

// 1. 实现跨平台统一回调
class MyNativeListener : public mysdk::Callback {
public:
    void onInitSuccess() override {
        std::cout << "[Native] SDK init success -> auto login" << std::endl;
        mysdk::MySDK::getInstance().login();
    }

    void onInitFail(int code, const std::string& message) override {
        std::cerr << "[Native] SDK init fail: " << code << ", " << message << std::endl;
    }

    void onLoginSuccess(const mysdk::UserInfo& user) override {
        std::cout << "[Native] Login success: uid=" << user.uid
                  << ", token=" << user.token
                  << ", method=" << user.loginMethod << std::endl;
    }

    void onLoginFail(int code, const std::string& message, const std::string& method) override {
        std::cerr << "[Native] Login fail: " << code << ", msg=" << message << ", method=" << method << std::endl;
    }

    void onLoginCancel() override {
        std::cout << "[Native] User cancelled login" << std::endl;
    }

    void onDisagreePrivacy() override {
        std::cout << "[Native] User disagreed privacy policy" << std::endl;
    }

    void onLogoutSuccess(const std::string& method) override {
        std::cout << "[Native] Logout success: " << method << std::endl;
    }

    void onLogoutFail(int code, const std::string& message) override {
        std::cerr << "[Native] Logout fail: " << code << ", " << message << std::endl;
    }

    void onOpenUserCenterSuccess() override {
        std::cout << "[Native] User center opened" << std::endl;
    }

    void onOpenUserCenterFail() override {
        std::cerr << "[Native] User center failed to open" << std::endl;
    }
};

// 2. 注册回调并驱动接口
void setupAndRunNativeSDK() {
    auto& sdk = mysdk::MySDK::getInstance();

    // 监听器生命周期需保证覆盖 SDK 调用期(例如全局静态、单例或常驻 Manager 的成员变量)
    static MyNativeListener s_listener;
    sdk.setCallback(&s_listener);

    // 前置参数(务必在 init 前调用)
    sdk.setDebug(true);                     // 开启详细原生日志
    sdk.setLanguage("en");                  // 指定语言(如 en, zh-Hans)
    sdk.setConfigAppId("1000001");          // 多套配置场景切换指定配置

    // 核心生命周期
    sdk.init();                             // 异步初始化(结果触发 onInitSuccess / onInitFail)
    // sdk.login();                         // 唤起登录窗口(通常在 onInitSuccess 后调用)
    // sdk.openUserCenter();                // 唤起用户中心
    // sdk.logout();                        // 登出当前用户
    // sdk.switchAccount();                 // 切换账号

    // 原生生命周期派发(Android 必需;iOS/macOS 为 no-op)
    // sdk.onResume();
    // sdk.onPause();

    // 同步状态查询
    if (sdk.isLogin()) {
        mysdk::UserInfo user = sdk.getUserInfo();
        std::cout << "Current valid user: " << user.name << " (" << user.uid << ")" << std::endl;
    }
    std::cout << "SDK Version: " << mysdk::MySDK::version() << std::endl;
}

2. 用户数据结构 UserInfo #

mysdk::UserInfo 是跨端归一化后的用户信息模型:

字段 类型 说明 各端原生模型对应
uid std::string 平台统一用户唯一 ID iOS SDKUserInfoModel.uid / Android SDKUserInfo.getUserId()
token std::string 登录鉴权凭证(用于游戏服二次验签) iOS token / Android getToken()
name std::string 用户昵称 iOS userName / Android getUserName()
avatar std::string 头像 URL iOS userAvatar / Android getAvatar()
loginMethod std::string 登录渠道(guest / apple / google / email 等) 各端渠道代码
countryCode std::string 国家或地区码 iOS countryCode / Android getCountryCode()
phoneNumber std::string 绑定的手机号(若有) iOS mobile / Android getMobile()
inheritCode std::string 账号引继码 / 继承码 引继系统凭据
isNewUser bool 是否为新注册用户 首次登录买量埋点判断
isAdult bool 是否已成年(防沉迷相关) 防沉迷状态标记
age int 实名认证年龄 实名认证信息
valid bool 登录态是否有效(未登录或登出后为 false) 结构体状态判据

3. C++ 使用规范与踩坑 #

  1. 回调对象生命周期:MySDK::getInstance().setCallback(Callback*) 不会复制或托管传入的对象生命周期。务必确保传入的 Callback 对象在整个 SDK 生命周期内有效(推荐使用单例、静态变量或全局 Manager),切忌传入局部栈变量指针。销毁或重置时可传 nullptr 取消注册。
  2. 线程归一化:原生 SDK 的各种通知和 JNI 回调可能发生在系统线程或 JNI 工作线程。在插件实现层中,emit* 已把事件同步推入加锁队列并直接触发 Callback。如果回调内要操作引擎渲染、场景节点或主线程专有资源,建议使用 Cocos 引擎的 cc::Scheduler::performFunctionInCocosThread 编组回主线程。
  3. 配置参数调用时机:setConfigAppId(...)、setDebug(...)、setLanguage(...) 必须在 init() 之前调用,底层 Objective-C / Java / C DLL 初始化一旦启动,后续修改配置可能不生效。
  4. Android 专用生命周期:Android 平台的 Activity 前后台切换必须在主 Activity 的 onResume 和 onPause 中分别调用 MySDK::getInstance().onResume() 与 onPause(),以确保支付和弹窗等各三方 SDK 正确恢复或暂停。

07 新增一个接口 #

  1. include/MySDK.h:给 MySDK 加方法 / 给 Callback 加回调。
  2. src/MySDKInternal.h:加 platformXxx()(后端实现)与 emitXxx()(事件)。
  3. src/MySDK.cpp:门面转发 platformXxx();emitXxx() 里拼 JSON 入队。
  4. 各平台后端实现 platformXxx()(见「各平台后端与 SDK 位置」),必要时用 emit* 上抛事件。
  5. src/MySDKBindings.cpp:.staticFunction("xxx", ...) 暴露给 JS;事件由 pollEvent 自动带出。
  6. ts/MySDK.ts(并同步到 assets/scripts/MySDK.ts):加类型/枚举/方法。

08 脚本都做了什么 #

所有脚本统一在 scripts/,从仓库根目录的 build-config/*.json 取平台构建参数、 从 signing/ 取签名材料,日志都写到 build/logs/。

脚本 作用
scripts/run-mac.sh macOS:构建 + 运行(签名,可测 Apple 登录)
scripts/build-mac.sh macOS:构建 + 打包 dist/*.zip(MYSDK_NOSIGN=1 关签名)
scripts/build-ios.sh iOS:真机构建(默认签名);IOS_SIGN=0 关签名、IOS_IPA=1 导出 IPA
scripts/build-android.sh Android:APK(需 NDK);ANDROID_SIGN=1 用 signing/android/demo.jks 签名
scripts/build-all.sh 依次构建 macOS / iOS / Android
scripts/export-plugin.sh 导出游戏可接入的标准 Cocos 插件包(含校验 + zip)
scripts/update-sdk.sh 从新的 SDK 包刷新 {mac,ios,android,windows}/sdk
scripts/publish-doc-site.sh 文档静态站点生成与 Cloudflare Pages 一键发布部署

逐步说明:

run-mac.sh(约等于 Unity Play)

  1. CocosCreator --project <root> --build configPath=build-config/mac.json: 构建 JS 资源并生成原生工程 build/mac/proj/<App>.xcodeproj。 ⚠️ CLI 构建成功也可能返回退出码 36(只生成原生工程、不跑 xcodebuild),脚本里用 || true 忽略, 真正的成败看第 2 步。
  2. xcodebuild -project <App>.xcodeproj -scheme <App>-desktop -configuration Debug: 默认走手动签名(DEVELOPMENT_TEAM / CODE_SIGN_IDENTITY / PROVISIONING_PROFILE_SPECIFIER, 并先把 signing/ 里的描述文件拷进 ~/Library/MobileDevice/Provisioning Profiles/); MYSDK_NOSIGN=1 时改传 CODE_SIGNING_ALLOWED=NO(更快,但 Apple 登录会失败)。 链接是增量的(首次会编译整个引擎,较久;之后几十秒)。
  3. pkill -9 -f "<scheme>.app/Contents/MacOS" 清掉残留进程 → open 启动 app。 (SIGTERM 杀不掉它,会攒出一堆窗口。)

build-mac.sh / build-ios.sh / build-android.sh

  • mac:同上 1、2 步,但跳过运行,改为 ditto -c -k 打包 dist/<App>-mac-<Config>.zip; CONFIGURATION=Release 可切 Release。
  • ios:Cocos CLI 构建 → xcodebuild -sdk iphoneos -destination generic/platform=iOS(真机 arm64), 默认签名(描述文件在 signing/),产物 dist/<App>-ios-<Config>.zip;IOS_IPA=1 时再用 signing/ios/ExportOptions.plist 导出 IPA。
  • android:先定位 SDK/NDK(ANDROID_SDK_ROOT/ANDROID_HOME 或 $HOME/Library/Android/sdk, NDK 找不到直接报错并给出 sdkmanager 安装命令)→ 把 sdk/ndk 路径写回 build-config/android.json → Cocos CLI 构建生成 native/engine/android → 调用插件里的 android/apply-to-cocos-project.sh 注入 Java 胶水 + Gradle 依赖 → gradlew 出 APK; ANDROID_SIGN=1 用 signing/android/demo.jks 签名。

build-all.sh:顺序跑上面三个,单个失败不中断(最后汇总退出码),产物统一在 dist/。

export-plugin.sh(导出游戏可接入的分发包)

  1. 用 python 从 cc_plugin.json 读 name / version / modules[0].target / engine-version;
  2. rsync 插件目录到 dist/<name>-cocos-plugin-<version>/(排除 .DS_Store,默认排除 ios/sdk); 加 MYSDK_WITH_IOS_SDK=1 时把 iOS SDK(约 252MB)也打进去;
  3. 校验(不过就退出、不产出):必需文件在不在(cc_plugin.json、src/CMakeLists.txt、 src/MySDKBindings.cpp、ts/MySDK.ts、四个平台的 <target>-config.cmake);target 名三处一致 (add_library(<target> + CC_PLUGIN_ENTRY(<target>,);mac/Android SDK 二进制在位;iOS SDK 只警告;
  4. 生成 manifest.json(带时间戳)、INTEGRATION.md(接入说明)、install.sh(rsync 到 <game>/native/native-plugin/<name>/);
  5. ditto 打 zip,打印产物路径与 SHA256。

update-sdk.sh(换 SDK 版本)

  1. 按文件名在各目录里找最新的包(可用 --sdk-zip-dir 或 --ios/--mac/--android/--pc 指定);
  2. 解压到临时目录,按目录特征定位真正的 SDK 根(如 osx/、<SDK>/SDK/、<sdk>/libs/、 PC 包里的 Plugins/x86_64/*.dll);
  3. rsync -a --delete 覆盖到插件内 {mac,ios,android,windows}/sdk/;
  4. 只刷新二进制/资源,不改代码:SDK 接口/依赖结构变化时,需要按「新增一个接口」与 「各平台后端与 SDK 位置」手动同步 .mm/.cpp 与 *-config.cmake,然后重新构建验证 (run-mac.sh)并重新导出(export-plugin.sh)。

publish-doc-site.sh(文档静态站点生成与发布)

  1. 将脱敏版技术文档通过 Python 生成现代化响应式静态站点(含侧边栏 TOC、滚动高亮、快速检索模态框、代码高亮与环形进度指示器);
  2. 产物输出至 dist/doc-site/;
  3. 支持通过 npx wrangler pages deploy 一键部署至 Cloudflare Pages,方便团队共享与在线查阅。

插件内的 android/apply-to-cocos-project.sh(属于分发包的一部分,所以不在 scripts/): 先要求已构建过一次 Android 生成 native/engine/android;把 MySDKBridge.java 拷到 app/src/<pkg>/,并在 app/build.gradle 末尾追加 fileTree 依赖 + multiDexEnabled true; 脚本内有 marker,可重复执行。


09 各平台后端与 SDK 位置(工程自包含) #

平台 后端文件 SDK 关键点
iOS ios/MySDKios.mm ios/sdk/(arm64-only 静态) umbrella 导出混淆符号,必须先 #import <MySDK/MySDKPublic.h>;登录 UI 入口在 SDKUIManager
Android android/MySDKandroid.cpp + java/ android/sdk/(官方 AAR/JAR 全量包) JNI 运行期按类名查找 SDKBridge;需要 NDK;AAR 由宿主 App 依赖提供
macOS mac/MySDKmac.mm mac/sdk/ 未混淆;登录 UI 入口在 SDKCoreManager
Windows windows/MySDKwin.cpp windows/sdk/(MySDK.dll + 配置 json) C 接口,运行时 LoadLibrary(不参与链接)

Apple 平台通用注意:

  • framework 导入要 -F(编译选项),只有 -I 找不到 <Framework/Header.h> → 用 MYSDK_COMPILE_OPTIONS 之类的变量传;
  • SDK 静态框架一律 -Wl,-force_load + -ObjC 链接;Swift framework(如三方买量库)需要把 工具链 usr/lib/swift/macosx 加进库搜索路径;用到 res_9_* 的要补 -lresolv;
  • 缺配置 plist 会导致 init 不回调:macOS 需要 <SDK>MacConfig.plist + 打点配置 plist; iOS 需要 <SDK>Config.plist + 打点配置 plist + GoogleService-Info.plist(放 {mac,ios}/config/, 构建时自动拷进 bundle;SDK 资源 bundle 同理);
  • Xcode 非常新时,引擎自带 enoki/half.h 的 std trait 特化会被 -Werror 拦下 → 插件 config 里对 ${ENGINE_NAME} 加 -Wno-invalid-specialization 兜底;
  • 静态库链接兼容性(如 libwebp 冲突/重打包):当原生 SDK 内嵌的三方静态库(如 libwebp.a)与引擎内置版本符号冲突或包含不兼容的打包格式时,需在 iOS config 中明确指定链接顺序,必要时通过重打包提取原始目标文件或利用 -Wl,-needed-l... 保证符号解析一致。

10 游戏侧接入 #

  1. 用导出包:bash install.sh /path/to/game(或手动把插件目录拷到 <game>/native/native-plugin/MySDK/)。
  2. 把 ts/MySDK.ts 拷到游戏 assets/scripts/。
  3. 用 MySDKTest.ts 的写法接 setEventListener + init/login/...。
  4. 平台依赖:
    • macOS:需配置带 Sign In with Apple capability 的开发者证书与描述文件(无签名构建 Apple 登录将直接失败);
    • Android:构建环境需安装 NDK(如 23.1.7779620 并配置 ANDROID_NDK_ROOT),构建一次后执行插件的 android/apply-to-cocos-project.sh 注入 Java 依赖;
    • iOS:ios/sdk 需存在(或导出时指定 MYSDK_WITH_IOS_SDK=1),原生 SDK 仅提供 arm64 真机架构(不支持模拟器架构构建),需连接已登记测试设备并配置对应描述文件;
  5. 编辑器「预览浏览器/模拟器」不会加载原生插件(MySDK.available === false), 必须构建原生包才能验证。

11 常见疑问:JS 不能直接和 Native 通信吗?为什么要 .h/.mm? #

结论:能「直接通信」,但那条通信管道本身必须由原生代码提供。Cocos 不像 Unity 那样默认 带一个「随便调任意 ObjC/Java API」的通用反射桥;.h/.mm 不是多余的绕路,它就是那条管道, 外加类型转换、线程调度和平台抽象。

拆开看:

  1. JS 的世界里没有 ObjC/Java 的符号。游戏 JS 跑在 App 内嵌的 V8 里,它能碰到的只有引擎 主动注册给它的东西(cc.*、以及插件注册的 MySDKNative)。SDK 是个 ObjC framework / Java AAR,语言都不同,JS 无处直接引用。

  2. Cocos 现成的「JS ↔ Native」通道,都是原生侧写好的代码:

    • JsbBridge(native.bridge):JS 发字符串事件到 Java/ObjC 侧,原生侧必须自己实现 onScript 回调——也就是说还是得写原生胶水,只是省了绑定代码。适合轻量通知, 不适合带复杂返回值/高频/大数据的调用。
    • jsb.reflection(原生反射):能 callStaticMethod 调静态方法、传基础类型/字符串, 但能力与性能都有限,复杂类型、回调、生命周期都要自己兜。
    • JSB 绑定(sebind / 手动绑定):把 C++ 类/函数注册进 JS——这正是本插件用的方式, 能力最强(任意类型、返回值、错误处理),代价是要写绑定层。
  3. 就算把 SDK 的 API「直接桥过去」,也得有人写那个桥,而且要在桥里解决:

    • 多语言/多平台:iOS/macOS 是 ObjC,Android 是 Java + JNI,Windows 是 C DLL。 游戏只维护一份 JS,必然要在中间做抽象层。放在 C++ 门面最合适:一次抽象,四处实现, 还能给非 JS 场景(引擎二次开发)复用。
    • 线程:V8 是单线程的,SDK 回调来自任意线程。直接回调 JS 实测崩溃 (v8::HandleScope::CreateHandle() ... without a HandleScope)。门面里用 「加锁队列 + 每帧轮询」统一兜底,业务 JS 只认 setEventListener。
    • 类型与生命周期:ObjC 对象、Java 对象、C 指针要统一成 JS 能懂的类型(数字/字符串/JSON)。 写一次比在每个调用点重复做可靠得多。
    • 工程化:链接静态 SDK、-force_load/-ObjC、依赖 framework、配置 plist、entitlements、 Gradle 注入……这些必须有一处原生代码/构建配置承接。把它们收进插件形态,才能打成 「可拷进任意 Cocos 工程」的分发包。
  4. 什么时候可以真的少写原生代码:如果只是「触发一个事件」且原生侧本来就有接入点 (如 AppDelegate 里已有推送回调),用 JsbBridge 转发到 JS 更省事。但要做 SDK 的完整能力 (登录、用户中心、支付、带结果回调),插件 + sebind 绑定是最稳的形态。

一句话:JS 和 Native 通信的「路」永远要 native 侧修;.h/.mm 修的就是这条路, 而且顺手把跨平台差异、线程安全、类型转换都封装掉了。


12 Native SDK 更新流程 #

SDK 安装包统一放在 /path/to/sdk-packages(或把新包放到别处,用参数指定):

平台 包(示例命名)
iOS MySDK-iOS_v<ver>_<hash>_<date>.zip
macOS MySDK_MacOS_v<ver>_<hash>_<date>.zip
Android MySDK-Android-v<ver>-<build>-release.zip
Windows/PC MySDK_Unity_PC_Demo_v<ver>.zip(内含 MySDK.dll)
💻 Shell / Bash
# 1) 刷新工程内的 SDK(覆盖 {mac,ios,android,windows}/sdk)
bash scripts/update-sdk.sh [--sdk-zip-dir /path/to/dir] [--windows]

# 2) 如果 SDK 的「接口/依赖」有变化,同步代码与配置(见「新增一个接口」与
#    「各平台后端与 SDK 位置」):
#    - 新增/改名的头文件、符号 → 对应平台的 .mm/.cpp 与 config.cmake
#    - 依赖框架清单 → 若 SDK 结构变了要调整
#    - 配置 plist 文件名/字段 → {mac,ios}/config/ 与调用(如 appId 变体)
#    - 版本号:cc_plugin.json 的 version
#
# 3) 验证
bash scripts/run-mac.sh            # 或 MYSDK_NOSIGN=1 bash scripts/run-mac.sh 快速验证 init/login/用户中心
bash scripts/export-plugin.sh      # 重新导出分发包

# 4) 多端环境就绪(按需配置):
#    - Android: 需安装 NDK(如 23.1.7779620)并设置 ANDROID_NDK_ROOT 环境变量
#    - iOS: 需配置真机开发证书与 Provisioning Profile(SDK 仅支持 arm64 真机架构)
#    - Windows: 依赖 MSVC 与对应 x64 C++ 运行时

注意:update-sdk.sh 只刷新二进制与资源,不会自动改 C++/CMake 适配代码。若后续升级 Cocos Creator 编辑器大版本,可能会覆盖引擎内部预编译库与 CMake 模板,若升级后出现符号链接冲突需重新核对引擎侧适配与依赖链。


13 参考文档 #

官方手册(Cocos Creator 3.8 LTS) #

引擎源码(v3.8.8) #

版本兼容与本地验证

本地安装目录里可以直接翻源码验证:/Applications/Cocos/Creator/3.8.8/CocosCreator.app/Contents/Resources/resources/3d/engine/native/(含 cocos/plugins/、cocos/bindings/sebind/、cmake/scripts/plugins_parser.js、templates/cmake/common.cmake)。

已复制
ESC 退出