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

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

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

约 45–60 分钟深度阅读 约 4.6 万字符 · 29 段代码示例 支持 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 / SDKUIPlatform / SDKUserInfoModel 各平台 SDK 入口类与数据模型的占位符
MY_SDK_* / MYSDK_* 构建期 CMake 变量前缀(如 MY_SDK_IOS_SDK_DIR)/ 环境变量前缀(如 MYSDK_WITH_IOS_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(${COCOS_X_PATH}/../templates/cmake/,此处
   COCOS_X_PATH 指引擎 native/)经 ${COCOS_X_PATH}/cmake/predefine.cmake 引入,
   其 cc_common_before_target() 调用 cc_gen_plugin_cmake_hook()
     └─ node native/cmake/scripts/plugins_parser.js(日志里显示为 plugin_parser.js)
          递归扫描工程根的 native/、extensions/(最多 7 层)找 cc_plugin.json,
          按 platforms / enable|enabled / disable|disabled / disable-by-platforms
          过滤;module_type 非 release 直接跳过;engine-version 只校验,不匹配仅告警
          └─ 生成 native/engine/<platform>/Pre-AutoLoadPlulgins.cmake:
                set(my_sdk_glue_ROOT "<插件目录>/<platform>/…")
                list(APPEND CMAKE_FIND_ROOT_PATH ${my_sdk_glue_ROOT})
                list(APPEND CC_REGISTERED_PLUGINS my_sdk_glue)
                find_package(my_sdk_glue REQUIRED NAMES "my_sdk_glue")
                     ← 先设了搜索路径,才找得到 <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() 里用全局对象回调登记过的注册函数。 (引擎只有 v8 后端会发出该事件:ScriptEngine.cpp 里发送调用的上一行注释标了 // @deprecated since 3.7.0,紧接着 cc::events::ScriptEngine::broadcast(AFTER_INIT); 官方新写法是 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;
  • 入队几乎不耗时,轮询发生在主线程的每帧回调里,不卡帧;队列上限 256 条, 突发超限时会丢最旧事件(正常事件量远低于上限);
  • 事件统一格式 {"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 可停自动扫描, 总开关 -DUSE_PLUGINS=OFF 则既不扫描也不生成 plugin_registry(默认 ON)。
  • cc_plugin.json 的 platforms / enable|enabled / disable|disabled 参与扫描过滤: 平台不匹配时不会被 find_package,自然也就不进 CC_REGISTERED_PLUGINS; engine-version 只做版本校验,不匹配仅打 warning,不会阻止插件注册。
  • 分层: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 plugins_parser.js
              [searching plugins] plugin dir found: .../native/native-plugin/<你的插件>
         └─ 生成 native/engine/<platform>/Pre-AutoLoadPlulgins.cmake
         └─ 生成 build/<platform>/proj/ 下的 Xcode 工程 + plugin_registry_autogen.cpp

也就是说 macOS / iOS 的插件扫描发生在 Creator 这一步的 CMake 配置里;Android 的扫描 发生在 Gradle 调起 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.0 起把 POST_INIT 标为 deprecated,新写法是 cc::events::ScriptEngine 的 AFTER_INIT(engine/EngineEvents.h);两种写法在 3.8.8 下都仍可用,本工程用的是老写法。

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

⚙️ CMake
add_library(hello_glue STATIC
    ${CMAKE_CURRENT_LIST_DIR}/hello_glue.cpp
    ${HELLO_PLATFORM_SOURCES}          # 由各平台 config 设置
)
target_include_directories(hello_glue
    PUBLIC  ${CMAKE_CURRENT_LIST_DIR}/../include   # 调用方要能 #include "MySDK.h"
    PRIVATE ${HELLO_EXTRA_INCLUDE_DIRS}
)
target_compile_options(hello_glue PRIVATE ${HELLO_COMPILE_OPTIONS})  # 如 Apple 的 -F<dir>
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); 实际用到四组变量:*_PLATFORM_SOURCES(必需)、*_EXTRA_INCLUDE_DIRS、*_COMPILE_OPTIONS (Apple 的 -F<dir>、告警兜底等)、*_LINK_LIBS:

    ⚙️ CMake
    set(HELLO_PLATFORM_SOURCES ${CMAKE_CURRENT_LIST_DIR}/hello_ios.mm)
    set(HELLO_EXTRA_INCLUDE_DIRS ${CMAKE_CURRENT_LIST_DIR}/sdk/SDK)
    set(HELLO_COMPILE_OPTIONS "-F${CMAKE_CURRENT_LIST_DIR}/sdk/SDK")
    set(HELLO_LINK_LIBS "-framework Foundation")
    include(${CMAKE_CURRENT_LIST_DIR}/../src/CMakeLists.txt)
    
  • 二进制分发:官方 tutorial 演示的是把被封装的依赖库做成 imported target(胶水仍从源码编); 本工程导入的是插件胶水本身——binary/import.cmake 建一个 add_library(<target> STATIC IMPORTED GLOBAL),配置 IMPORTED_LOCATION_<CONFIG>、 INTERFACE 头文件目录与链接依赖;src/CMakeLists.txt 检测到该文件时直接引入并 return。

本工程导出脚本现支持 --mode source|binary:默认源码;二进制模式预编译完整原生胶水 (含 JS 绑定与插件入口)和 Android Java JAR,移除胶水实现源码,保留公开头文件、TS、 SDK 依赖、配置与资源。可用 --platforms mac,ios,android 选择平台, --configuration Release 选择配置,--binary-dir <dir> 复用已编译库。 二进制产物名加 -binary;清单记录引擎版本、架构、配置和 SHA256,接入方需匹配 Creator、V8、架构、构建配置与 C++ runtime;更换这些条件需重新预编译。 Windows 需用 MSVC 兼容工具链生成静态库,可在 Windows 本机构建或在 macOS 交叉编译。 本机全局 LLVM / Microsoft SDK 交叉工具链已配置;明确选择 Windows 时导出脚本可自动生成 .lib。 默认二进制导出仍为三平台;Windows 胶水 Debug / Release 交叉编译已验证,SDK 运行待验证。

构建验证 #

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

  • 构建日志出现 [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 的回调
                      └─同时直接调用─→ Callback*(C++ 门面回调,发生在 SDK 原生线程)

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


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();                             // 异步回调;macOS 上 init / setDebug 会同步阻塞约 6s
    // 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 UserInfo.getUid()
token std::string 登录鉴权凭证(用于游戏服二次验签) iOS token / Android getToken()
name std::string 用户昵称 iOS name / Android getName()
avatar std::string 头像 URL iOS avatar / Android getAvatar()
loginMethod std::string 登录渠道(guest / apple / google / email 等) 各端渠道代码
countryCode std::string 国家或地区码 iOS countryCode / Android getCountryCode()
phoneNumber std::string 绑定的手机号(若有) iOS phoneNumber / Android getPhoneNumber()
inheritCode std::string 账号引继码 / 继承码 引继系统凭据
isNewUser bool 是否为新注册用户 首次登录买量埋点判断
isAdult bool 是否已成年(防沉迷相关) 防沉迷状态标记
age int 用户年龄 iOS age / Android getAge()
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 生命周期(建议转发):建议按官方 Demo 在游戏 Activity 的 onCreate 里调用 SDKBridge.setActivity(this),把 onResume / onPause 转发给桥接(Java 静态方法,等价于 C++ 门面的 onResume() / onPause()),并按需处理 onNewIntent / onActivityResult;不转发只会影响三方 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 配 release 签名(配合 GRADLE_TASK=assembleRelease)
scripts/build-all.sh 依次构建 macOS / iOS / Android
scripts/export-plugin.sh 导出游戏可接入的标准 Cocos 插件包(源码 / 二进制,含校验 + zip)
scripts/plugin-binaries.py export-plugin.sh 内部调用的预编译工具:编译各平台胶水静态库与 Android Java 桥接 JAR,校验入口 / 架构 / SHA256
scripts/setup-windows-cross.py 准备 Windows x64 交叉工具链:拉取微软 VSIX / NuGet 包并校验哈希,生成全局 windows-cl 等入口
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 CODE_SIGNING_REQUIRED=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(需 IOS_SIGN=1,ExportOptions 为手动签名)时再用 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 配 release 签名(keystore / 别名 / 密码可用 ANDROID_KEYSTORE / ANDROID_KEY_ALIAS / ANDROID_STORE_PASS 覆盖;只作用于 release 构建类型)。

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

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

--mode source 为默认源码包;--mode binary 先由内部预编译工具编译原生静态库和 Android Java JAR,再校验库中已定义的插件入口与 SHA256,并去除胶水实现源码。 二进制包用 STATIC IMPORTED 目标保留原有 SDK 链接与资源处理,产物名加 -binary。 以下流程描述默认源码模式;两种模式均在校验通过后生成安装脚本、文档与 ZIP。

  1. 用 python 从 cc_plugin.json 读 name / version / modules[0].target / engine-version;
  2. rsync 插件目录到暂存目录(排除 .DS_Store、binary/、预编译 globalsdk-bridge.jar 和未选中的平台目录;默认再排除 ios/sdk);MYSDK_WITH_IOS_SDK=1(且平台含 ios)时把 iOS SDK(约 252MB)也打进去;
  3. 校验(不过就退出、不产出):源码模式查 cc_plugin.json、src/CMakeLists.txt、 src/MySDK.cpp、src/MySDKInternal.h、src/MySDKBindings.cpp、include/MySDK.h、 ts/MySDK.ts 与所选平台的 <target>-config.cmake,二进制模式查 binary/import.cmake 与 binary/manifest.json;target 名一致(add_library(<target> + CC_PLUGIN_ENTRY(<target>,); 各平台 SDK / 配置资源在位(macOS plist 与资源 bundle、Android StudioLib 与 Java 或 JAR、 Windows DLL 与配置、iOS 三个配置 plist 都是硬检查),只有 iOS SDK 本体缺失是警告;
  4. 生成 manifest.json(带时间戳)、INTEGRATION.md(接入说明)、install.sh(rsync 到 <game>/native/native-plugin/<name>/);
  5. ditto 打 zip,打印产物路径与 SHA256。

update-sdk.sh(换 SDK 版本)

  1. 按脚本内置的文件名 glob 匹配(如 iOS MySDK-iOS_*.zip;改成别的包名要同步改脚本), 取 mtime 最新的一个;默认目录写死在脚本里(换机器用 --sdk-zip-dir),也可用 --ios/--mac/--android/--pc 直接指定包、--windows 才会刷新 PC 包;
  2. 解压到临时目录,按目录特征定位真正的 SDK 根(如 macOS 的 osx/、iOS 的 <SDK>/SDK/、 Android 的 StudioLib/、PC 包里的 Plugins/x86_64/*.dll);
  3. 覆盖到插件内 {mac,ios,android,windows}/sdk/:mac / iOS / Android 用 rsync -a --delete (Android 实际落到 android/sdk/StudioLib/);Windows 只是 cp DLL 与配置 json 两个文件;
  4. 只刷新二进制/资源,不改代码:SDK 接口/依赖结构变化时,需要按「新增一个接口」与 「各平台后端与 SDK 位置」手动同步 .mm/.cpp 与 *-config.cmake,然后重新构建验证 (run-mac.sh)并重新导出(export-plugin.sh)。

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

  1. 用 npx marked 把脱敏版文档转成 HTML,再由 Python 套上页面模板(侧边栏 TOC、滚动高亮、 快速检索模态框、代码高亮与环形进度指示器)——需要 Node.js(npx);
  2. 产物输出至 dist/doc-site/;
  3. 通过 npx wrangler pages deploy 部署(默认就部署,DRY_RUN=1 只构建到 dist/doc-site/; 项目名 / 分支用 PROJECT_NAME / BRANCH 覆盖,首次使用先 wrangler login)。

插件内的 android/apply-to-cocos-project.sh(属于分发包的一部分,所以不在 scripts/): 先要求已构建过一次 Android 生成 native/engine/android;源码包把 MySDKBridge.java 拷到 app/src/<pkg>/,二进制包改为让 Gradle 依赖预编译的 globalsdk-bridge.jar,并在 app/build.gradle 末尾追加 SDK fileTree 依赖、multiDexEnabled true、最低 API 24、 Java 关键字 namespace 转义和 OAuth manifest 占位符;脚本内有 marker,可重复执行 (源码 / 二进制切换时会清理旧文件)。


09 四平台编译环境与流程 #

本机(macOS)可以编译四个平台的插件胶水:macOS 用本机 Apple 工具链,iOS 用 iPhoneOS 工具链,Android 用 NDK,Windows 用 LLVM + Microsoft SDK / CRT 交叉工具链。 四个平台的编译产物、工具链与验证范围如下(脚本支持的配置范围不等于已完成的验证范围):

平台 本机工具链 插件二进制 验证范围
macOS Xcode / AppleClang / Creator 内置 CMake arm64、Debug,.a 原生 App、SDK 初始化 / 登录 / 用户中心及 Apple 登录已验证
iOS Xcode / iPhoneOS SDK arm64 真机、Debug,.a 开发签名构建、归档、IPA 导出、包内资源已验证;真机 SDK 调用待验证
Windows LLVM clang-cl / LLD / Microsoft SDK 与 CRT x86_64、Debug / Release,.lib 插件交叉编译、入口符号、普通 C++ 示例链接已验证;完整游戏及 SDK 运行待 Windows 验证
Android Android SDK / NDK / CMake / Gradle / JDK arm64-v8a、Debug,.a + Java 桥接 .jar Debug APK 构建、签名、Java / JNI 桥接已校验;设备运行与 SDK 调用待验证

共用环境 #

项目 当前配置
Creator /Applications/Cocos/Creator/3.8.8/CocosCreator.app
Creator CLI 上述目录下的 Contents/MacOS/CocosCreator
引擎原生源码 Contents/Resources/resources/3d/engine/native
Apple 工程使用的 CMake Creator 内置 3.24.3:Contents/Resources/tools/cmake/bin/cmake
插件源码 native/native-plugin/MySDK/
构建参数 build-config/{mac,ios,windows,android}.json
基础工具 Bash、Python 3、Git、rsync;导出脚本使用 macOS 的 ditto
C++ / JS 引擎 C++17 / V8;当前二进制包只支持 V8
Demo 应用标识 com.example.mysdk.demo,与 SDK 配置、Apple 描述文件对应

脚本里的 Creator 路径固定为 3.8.8;换机器或换安装位置后,先核对脚本路径再重新生成 原生工程——不要沿用另一台机器生成的 CMakeCache.txt(里面存有绝对路径)。 游戏接入时应使用自己的应用标识、SDK 渠道配置和签名材料。

编译、链接和运行的关系 #

⚡ 时序 / 架构流程
① Creator 构建 JS 与资源,生成原生工程 native/engine/<platform>/
      └─ 扫描 cc_plugin.json → find_package(my_sdk_glue)
           → 加载 <platform>/my_sdk_glue-config.cmake
② 分发模式二选一:
     源码   → 编译门面 / 平台后端 / JS 绑定与插件入口(src/CMakeLists.txt)
     二进制 → 导入预编译静态库(binary/import.cmake,同名 IMPORTED target)
③ 宿主游戏链接插件、引擎与平台依赖(静态库里保留的外部引用在这一步解析)
④ 拷贝 SDK 资源与配置,打包并签名
⑤ 在目标平台运行,验证 SDK 调用(init / 登录 / 用户中心 / 回调)

.a / .lib 只包含本插件的门面、事件队列、平台后端、JS 绑定和注册入口,没有合并 完整 Cocos 引擎和厂商 SDK;厂商 SDK 的 framework、AAR / JAR、DLL、配置和资源仍需按 平台提供(位置见「各平台后端与 SDK 位置」)。原生回调只写加锁队列、JS 每帧轮询取, 不要在 SDK 回调线程直接调 V8(机制见「原理」)。

macOS #

环境检查(需要完整 Xcode,并让命令行工具指向它):

💻 Shell / Bash
xcode-select -p
xcodebuild -version
xcrun --sdk macosx --show-sdk-version
# 路径没指向完整 Xcode 时:
sudo xcode-select --switch /Applications/Xcode.app/Contents/Developer

厂商 SDK 是包含 arm64 / x86_64 的静态 universal framework,需要 -Wl,-force_load + -ObjC 全量链接;framework 头文件要 -F 编译选项(只有 -I 找不到 <Framework/Header.h>);Swift 依赖(如三方买量库)要把工具链 usr/lib/swift/macosx 加进库搜索路径;网络诊断类依赖要 -lresolv。 构建后 CMake 会把 {mac}/config/ 的配置 plist 与 SDK 资源 bundle 拷进 App 的 Contents/Resources/;缺配置 plist 时 init 可能没有回调。init / setDebug 阻塞调用线程约 6 秒是 SDK 行为,不是 bug。

💻 Shell / Bash
bash scripts/run-mac.sh                      # 签名构建 + 运行(Apple 登录可用)
bash scripts/build-mac.sh                    # 签名构建 + 打包 dist/*.zip
MYSDK_NOSIGN=1 bash scripts/build-mac.sh     # 只检查无签名构建
CONFIGURATION=Release bash scripts/build-mac.sh

生成过原生 CMake 工程后,可只编译插件 target(改插件后常用,比整包快):

💻 Shell / Bash
"/Applications/Cocos/Creator/3.8.8/CocosCreator.app/Contents/Resources/tools/cmake/bin/cmake" \
  --build build/mac/proj --target my_sdk_glue --config Debug \
  -- CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO

构建 .a 本身不要求签名;Apple 登录需要「证书与私钥 + 带对应 capability 的描述 文件 + mac/entitlements.plist」一起生效,无签名构建的 Apple 登录必然失败。 清理残留 Demo 进程:pkill -9 -f "<App>-desktop.app/Contents/MacOS"(SIGTERM 不可靠)。

iOS #

💻 Shell / Bash
xcrun --sdk iphoneos --show-sdk-version
security find-identity -v -p codesigning

厂商 SDK 是 arm64 真机静态库:设备构建与模拟器构建不可互用,SDK 也不提供模拟器 版本,因此不能靠「切 Apple Silicon 模拟器」验证 iOS 插件。外部 SDK 可用 CMake 参数 -DMY_SDK_IOS_SDK_DIR=/path/to/sdk 指定(构建期 CMake 变量;iPhoneOS 系统 SDK 仍由 Xcode 提供)。 依赖要点:umbrella 导出混淆符号,必须在其他 SDK 头之前 #import <MySDK/MySDKPublic.h>;-F / -force_load / -ObjC / Swift 库路径同 macOS; App 内需要 {ios}/config/ 的配置 plist、GoogleService-Info.plist 和 SDK 资源 bundle (资源拷贝脚本同时处理普通构建与 Archive 的 App 位置)。

💻 Shell / Bash
bash scripts/build-ios.sh                       # 默认开发签名,真机 App + zip
IOS_SIGN=0 bash scripts/build-ios.sh            # 只检查编译 / 链接
IOS_SIGN=1 IOS_IPA=1 bash scripts/build-ios.sh  # 归档并导出 IPA
CONFIGURATION=Release IOS_SIGN=1 IOS_IPA=1 bash scripts/build-ios.sh

流程:Creator 生成工程 → xcodebuild -sdk iphoneos → App 打包 →(可选) archive / exportArchive;scheme 为 <App>-mobile,App 在 build/ios/proj/<配置>-iphoneos/,日志在 build/logs/,zip / IPA 在 dist/。只编译真机插件:

💻 Shell / Bash
"/Applications/Cocos/Creator/3.8.8/CocosCreator.app/Contents/Resources/tools/cmake/bin/cmake" \
  --build build/ios/proj --target my_sdk_glue --config Debug \
  -- CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO ARCHS=arm64 ONLY_ACTIVE_ARCH=NO

静态库预编译可以关签名;设备安装和 Apple 登录需要有效签名、capability、描述文件, 开发描述文件还需包含目标设备。

Windows(macOS 交叉编译) #

用 LLVM clang-cl 生成 MSVC ABI 的 x86_64 COFF 对象,llvm-lib 归档为 .lib, 链接 .exe 用 LLD(MSVC 兼容模式见 LLVM 官方说明):

项目 本机示例
LLVM / LLD 23.1.3,/opt/homebrew/opt/{llvm,lld}/bin
Ninja / 通用 CMake 1.13.2 / 4.4.0(Apple 原生工程继续用 Creator 内置 CMake)
SDK / CRT 根目录 /opt/homebrew/share/windows-msvc
MSVC CRT / STL v143,VC/Tools/MSVC/14.44.35207
Windows SDK 目录版本 10.0.26100.0(NuGet 包 10.0.26100.9169)
全局命令 windows-cl / windows-link / windows-lib / windows-rc
通用 CMake toolchain /opt/homebrew/share/windows-msvc/toolchain.cmake

新机器准备同类环境:

💻 Shell / Bash
brew install llvm lld ninja cmake
python3 scripts/setup-windows-cross.py      # 本工程脚本:下载微软官方包并校验哈希
windows-cl --version

setup-windows-cross.py 不是微软安装器:它读取微软官方清单,校验 CRT VSIX 的 SHA256 与 SDK NuGet 的 SHA512,再写入 SDK 目录、来源记录(sources.json)和全局入口; 迁移机器时核对安装参数和目标路径。交叉编译需要:Microsoft STL / CRT 头文件 (string / vector 等)、Windows SDK 头文件(Win32 / UCRT)、x64 CRT / SDK .lib、 与引擎配置匹配的 Cocos Windows V8 头文件(按 Creator 引擎目录的 native/external-config.json 里 external tag 取,当前 v3.8.8-2;脚本会自动 clone 到 build/plugin-binaries-cocos-external/<tag>/,即 …/v3.8.8-2/)、厂商 MySDK.dll + 配置 json(运行时用,已内置 windows/sdk/)。

插件 .lib 由导出脚本调用 plugin-binaries.py 生成;macOS 上没有 Windows 原生 CMake cache 时自动走交叉分支,编译 src/ 与 windows/ 的胶水并配置插件静态入口、 V8 与平台宏:

💻 Shell / Bash
bash scripts/export-plugin.sh --mode binary --platforms windows
MYSDK_OUT=dist/windows-release bash scripts/export-plugin.sh \
  --mode binary --platforms windows --configuration Release

Debug 用 /MDd、Release 用 /MD,宿主需匹配运行库与兼容的 MSVC toolset。 通用 C++ 工程也能直接用这套环境:

💻 Shell / Bash
windows-cl /std:c++17 /EHsc /MD hello.cpp /Fehello.exe
cmake -S /path/to/source -B /path/to/windows-build -G Ninja \
  -DCMAKE_TOOLCHAIN_FILE=/opt/homebrew/share/windows-msvc/toolchain.cmake \
  -DCMAKE_BUILD_TYPE=Release
cmake --build /path/to/windows-build

运行链路:后端运行时 LoadLibrary 加载 x86_64 的 MySDK.dll 再解析 C 接口, 不依赖厂商导入库;平台 CMake 在游戏构建后把 DLL 与配置 json 拷到 exe 旁边。 当前「交叉编译 + 入口符号 + 普通 C++ exe 链接」已验证;完整 Cocos Windows 游戏与 SDK 调用仍需在 Windows 上验证——COFF 架构与入口校验通过只证明产物满足这些检查, 接口签名按 Unity IL2CPP 生成码还原,调用约定与运行行为待验证。

Android #

项目 本机示例
Android SDK ~/Library/Android/sdk(Android Studio / sdkmanager 管理)
NDK 23.1.7779620,位于 SDK 的 ndk/
Native CMake 3.22.1,位于 SDK 的 cmake/
Build Tools 36.0.0
compile / target SDK API 34(本机另有 Platform 35 / 36)
最低 API 24 / Android 7.0(完整厂商库的要求)
Gradle / JDK wrapper 8.11.1 / Temurin 21
当前 ABI / STL arm64-v8a / c++_static
💻 Shell / Bash
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export ANDROID_NDK_ROOT="$ANDROID_SDK_ROOT/ndk/23.1.7779620"
export JAVA_HOME="$(/usr/libexec/java_home -v 21)"
"$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$ANDROID_SDK_ROOT" \
  'ndk;23.1.7779620' 'cmake;3.22.1' 'build-tools;36.0.0' 'platforms;android-34'

固定 NDK 版本:脚本在未设置环境变量时会自动选择 SDK 中排序靠后的 NDK,多版本共存 时显式设置 ANDROID_NDK_ROOT 可避免版本漂移。调用链:C++ → JNI → SDKBridge → 厂商 SDKUIPlatform;胶水 .a 最终链接进宿主的 libcocos.so,JNI 回调入口 Java_<包名>_SDKBridge_nativeOnEvent 必须保留在最终原生库中。接入脚本注入 Java / JAR、AAR 依赖、最低 API 24、multidex 和 OAuth Manifest 占位符,并处理包名中的 Java 关键字(namespace 转义、applicationId 保持原值);不会自动注入 Activity 生命周期 转发,游戏侧还需处理 onCreate / onNewIntent / onActivityResult 并在设备上验证。

💻 Shell / Bash
bash scripts/build-android.sh                                             # Debug APK
GRADLE_TASK=assembleRelease ANDROID_SIGN=1 bash scripts/build-android.sh  # 签名 Release

流程:写 SDK / NDK 路径 → Creator 生成原生工程 → apply-to-cocos-project.sh 注入 → Gradle 构建;APK 按 Gradle 输出元数据复制到 dist/。原生 CMake cache 在 build/android/proj/build/<配置>/<构建标识>/<ABI>/;新增 ABI 时先改 Creator 平台 配置并构建该 ABI,再导出对应插件二进制。

分发与兼容条件 #

💻 Shell / Bash
bash scripts/export-plugin.sh --mode source              # 四平台源码包(默认不带 iOS SDK)
MYSDK_WITH_IOS_SDK=1 bash scripts/export-plugin.sh --mode binary \
  --platforms mac,ios,android,windows                    # 四平台 Debug 二进制(含 iOS SDK)
MYSDK_OUT=dist/release MYSDK_WITH_IOS_SDK=1 bash scripts/export-plugin.sh \
  --mode binary --platforms mac,ios,android,windows --configuration Release
MYSDK_WITH_IOS_SDK=1 bash scripts/export-plugin.sh --mode binary \
  --platforms mac,ios,android,windows --binary-dir build/plugin-binaries/Debug
  • 源码导出只校验源码与 SDK 布局,不要求先编译游戏;自动预编译 mac / iOS / Android 需要对应的 Creator 原生工程与 CMake cache(先跑一次 build-*.sh),Windows 走交叉 分支、不要求先生成 Windows 游戏工程。
  • 默认 source 四平台;默认 binary 为 mac / iOS / Android(Debug),Windows 需显式包含。 macOS 默认当前工程架构(MYSDK_MAC_ARCHS='arm64;x86_64' 双架构是脚本支持用法,实际 验证为 arm64);Android 采用已生成工程的 ABI;iOS 固定 arm64 真机。
  • 两种模式都默认不带 iOS SDK:接入方自己放到 ios/sdk/,或打包时 MYSDK_WITH_IOS_SDK=1 / 用导出环境变量 MYSDK_IOS_SDK_DIR 指定来源路径(它与构建期 CMake 变量 MY_SDK_IOS_SDK_DIR 不是同一个名字,别混用)。
  • 兼容条件:Creator 版本、V8、架构、构建配置、C++ runtime 必须匹配;最低系统 / API 为 macOS 12 / iOS 15 / Android 24;Android 用 c++_static,Windows 用 MSVC ABI 及 匹配的 /MDd / /MD。升级引擎、改公开头文件、切 ABI / runtime 后要重新预编译; 源码清单声明的较宽引擎范围不代表这些版本都验证过。
  • 缺某个平台的库会失败,不会自动降级为源码分发;Debug / Release 导出默认同名,用 MYSDK_OUT 分开存放;同一预编译根目录的清单不自动累加,以当前 manifest.json 为准。
  • 安装:在导出包目录执行 bash install.sh /path/to/game(只做 rsync 与切换残留清理, 不会拷 TS);再手动 cp ts/MySDK.ts /path/to/game/assets/scripts/。Android 工程 生成后还要跑包里的 android/apply-to-cocos-project.sh(本工程的 build-android.sh 已自动执行)。

排查顺序与维护位置 #

现象 先检查
预览中 available === false 是否运行原生 App;预览不加载插件(正常现象)
找不到插件入口 / target cc_plugin.json、CC_PLUGIN_ENTRY 和平台 CMake 是否都用 my_sdk_glue
二进制无法导入 / 找不到某配置库 Creator、V8、架构、Debug / Release 是否符合 binary/manifest.json
Apple framework 头文件或符号找不到 -F、静态依赖的 force-load、Swift 路径;iOS 混淆映射头的导入顺序
iOS 模拟器链接失败 当前厂商 SDK 只支持 arm64 真机
SDK init 没有回调 App 包内平台配置与资源是否完整,再查运行日志
Apple 登录失败 签名、entitlement、描述文件 capability 和应用标识
Android Gradle / JNI 失败 JDK / NDK、API 24、Java bridge 或 JAR、AAR 依赖、namespace 与 OAuth 参数
Windows 找不到 C++ / Win32 头文件 是否用 windows-cl,SDK / CRT 根目录是否完整
Windows 游戏无法加载 SDK exe 位数、DLL / JSON 位置、DLL 依赖及实际接口调用(需 Windows 运行验证)

维护对象是源码、插件 CMake、构建 JSON 和接入脚本;不要手改 build/、native/engine/ 或生成的 Xcode 工程(library/、temp/、build/、local/、profiles/、dist/ 都是生成物)。改 TS 时以插件 ts/MySDK.ts 为准,并同步 Demo 的 assets/scripts/MySDK.ts。


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

平台 后端文件 SDK 关键点
iOS ios/MySDKios.mm ios/sdk/(umbrella 为 arm64-only 静态) umbrella 导出混淆符号,必须先 #import <MySDK/MySDKPublic.h>;登录 UI 入口在 SDKUIManager
Android android/MySDKandroid.cpp + java/ android/sdk/StudioLib/(官方 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> → 用 MY_SDK_COMPILE_OPTIONS 之类的变量(构建期 CMake 变量)传;
  • 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 案例):引擎自带的 external/<platform>/libs/*.a 里 fat 切片的起始偏移可能不满足 8 字节对齐,新版 Xcode 链接器会报 ignoring archive member ... 64-bit mach-o not 8-byte aligned;这不是库版本或源码问题。处理方式是提取所需架构的切片、重建归档(lipo -thin → ar -x → libtool -static)后替换引擎侧 .a;它和 -Wno-invalid-specialization 也不是同一个问题。

11 游戏侧接入 #

  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), 必须构建原生包才能验证。

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

结论:能「直接通信」,但那条通信管道本身必须由原生代码提供;.h/.mm 不是多余的绕路, 它就是那条管道,外加类型转换、线程调度和平台抽象。

顺带澄清一个常见误解:Unity 也没有「随便调任意 ObjC API」的桥。Unity 的 iOS 原生插件 同样是「ObjC 包成 C 接口(extern "C" 函数)+ C# [DllImport("__Internal")]」, 包装代码照样要手写;Unity 真正的通用反射桥只存在于 Android / Java 侧 (AndroidJavaClass / AndroidJavaObject / AndroidJNI 可直接调任意 Java 类与方法)。 Cocos 对应的 native.reflection(旧别名 jsb.reflection)只能调静态方法、传简单类型(见第 2 点),能力更弱, 所以更需要插件这种「一次抽象、四处实现」的形态。

机制对照:Unity 与 Cocos 的「脚本 ↔ 原生」通道 #

经常有人问「Unity 不是有 SendMessage 吗」。几个同名/近名的机制其实不在同一层,先分清楚:

机制 所在层 Unity Cocos
GameObject.SendMessage 纯脚本层(C# ↔ C#) 按方法名在组件里反射调用,与原生通信无关 无同名 API;用 Node / EventTarget 的 emit 或直接调用
字符串消息桥 原生胶水(原生 ↔ 脚本) 原生→C#:UnitySendMessage("物体名","方法名","字符串");C#→原生:[DllImport("__Internal")](iOS extern "C")/ P/Invoke、Android JNI 原生→JS:JsbBridge.sendToScript + native.bridge.onNative;JS→原生:native.bridge.sendToNative + 原生实现回调(Java onScript / ObjC setCallback)
反射(免写桥) 桥的一个特例 Android AndroidJavaClass / AndroidJavaObject / AndroidJNI(Java 全反射);iOS 没有 ObjC 反射,仍要 extern "C" 包装 native.reflection.callStaticMethod(仅静态方法、简单类型,比 Unity 的 Java 反射弱)
脚本绑定 绑定层 不适用(C# 就是脚本层,直接用 P/Invoke 调原生) sebind / 手动 JSB 绑定——本插件用的就是这一种
  • UnitySendMessage 才是 Unity 的「原生 → 脚本」桥,与 JsbBridge 同类:只能传一个字符串、 按「物体名 + 方法名」查找、被调方法签名只能是 void MethodName(string message),而且 异步、有一帧延迟(Android 对应 UnityPlayer.UnitySendMessage;桌面平台没有它,要用 delegate 回调)。复杂结构两家都得自己 JSON。
  • 本插件走的是第四种:MySDKNative 由 sebind 注册进 JS 引擎,调用就是带类型、有返回值的 普通 JS 函数;事件走「加锁队列 + 每帧 pollEvent()」。相比字符串桥:类型安全、有返回值与 错误处理、一次抽象四平台,C++ 门面还能给非 JS 场景复用,回调也不依赖「物体名 + 方法名」的 字符串查找。

拆开看:

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

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

    • JsbBridge(native.bridge):JS 用 native.bridge.sendToNative(arg0, arg1) 发字符串事件到 Java/ObjC 侧(Unity 的对应物是 UnitySendMessage,见上文机制对照),原生侧必须自己实现回调(Java:JsbBridge.setCallback(new ICallback(){ … onScript(arg0, arg1) }); ObjC:[[JsbBridge sharedInstance] setCallback:^(NSString *arg0, NSString *arg1){ … }]), 原生反向通知 JS 则给 native.bridge.onNative 赋值——还是得写原生胶水,只是省了绑定代码。 适合轻量通知,不适合带复杂返回值/高频/大数据的调用。
    • native.reflection(原生反射,旧写法 jsb.reflection 仍可用):能 callStaticMethod 调静态方法、传基础类型/字符串,但能力与性能都有限(对比 Unity 的 AndroidJavaObject / AndroidJNI 只能算简化版),复杂类型、回调、生命周期都要自己兜。
    • 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 修的就是这条路, 而且顺手把跨平台差异、线程安全、类型转换都封装掉了。


13 Native SDK 更新流程 #

SDK 安装包目录默认写在 scripts/update-sdk.sh 里(换机器用 --sdk-zip-dir 指定):

平台 包(示例命名;脚本按内置的文件名前缀匹配,换名字需同步改脚本)
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 ABI 工具链(clang-cl / `windows-cl` + Microsoft SDK/CRT)与匹配的 x64 运行时(`/MD` 或 `/MDd`)

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


14 参考文档 #

官方手册(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 在同级的 templates/。

已复制
ESC 退出