原生插件开发与接入指南(脱敏版)
覆盖 iOS / macOS / Android / Windows 全平台生命周期、四平台编译环境与工具链、CMake 构建流、sebind 绑定、线程安全事件入队、自动化脚本与游戏侧接入的一站式完整手册。
本文以 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 目录」脚本——它就是普通的构建动作,三种做法任选:
编辑器 UI:打开项目 → 顶部「构建发布」面板 → 选平台(macOS / iOS / Android / Windows)→「构建」。
本工程的脚本(第一步就是构建;后面还会编译 / 运行 / 打包):
bash scripts/run-mac.sh、build-mac.sh、build-ios.sh、build-android.sh、build-all.sh。裸 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
{
"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(最小可跑骨架):
#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 目标)
#
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):CMakeset(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。
本工程导出脚本现支持 --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 的回调
回调模型、线程安全、分层原因见「原理」。
06 对外接口 #
TS(游戏侧) #
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() 驱动生命周期:
#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++ 使用规范与踩坑 #
- 回调对象生命周期:
MySDK::getInstance().setCallback(Callback*)不会复制或托管传入的对象生命周期。务必确保传入的Callback对象在整个 SDK 生命周期内有效(推荐使用单例、静态变量或全局 Manager),切忌传入局部栈变量指针。销毁或重置时可传nullptr取消注册。 - 线程归一化:原生 SDK 的各种通知和 JNI 回调可能发生在系统线程或 JNI 工作线程。在插件实现层中,
emit*已把事件同步推入加锁队列并直接触发Callback。如果回调内要操作引擎渲染、场景节点或主线程专有资源,建议使用 Cocos 引擎的cc::Scheduler::performFunctionInCocosThread编组回主线程。 - 配置参数调用时机:
setConfigAppId(...)、setDebug(...)、setLanguage(...)必须在init()之前调用,底层 Objective-C / Java / C DLL 初始化一旦启动,后续修改配置可能不生效。 - Android 专用生命周期:Android 平台的 Activity 前后台切换必须在主 Activity 的
onResume和onPause中分别调用MySDK::getInstance().onResume()与onPause(),以确保支付和弹窗等各三方 SDK 正确恢复或暂停。
07 新增一个接口 #
include/MySDK.h:给MySDK加方法 / 给Callback加回调。src/MySDKInternal.h:加platformXxx()(后端实现)与emitXxx()(事件)。src/MySDK.cpp:门面转发platformXxx();emitXxx()里拼 JSON 入队。- 各平台后端实现
platformXxx()(见「各平台后端与 SDK 位置」),必要时用emit*上抛事件。 src/MySDKBindings.cpp:.staticFunction("xxx", ...)暴露给 JS;事件由pollEvent自动带出。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)
CocosCreator --project <root> --build configPath=build-config/mac.json: 构建 JS 资源并生成原生工程build/mac/proj/<App>.xcodeproj。 ⚠️ CLI 构建成功也可能返回退出码 36(只生成原生工程、不跑 xcodebuild),脚本里用|| true忽略, 真正的成败看第 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 登录会失败)。 链接是增量的(首次会编译整个引擎,较久;之后几十秒)。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(导出游戏可接入的分发包)
--mode source 为默认源码包;--mode binary 先由内部预编译工具编译原生静态库和
Android Java JAR,再校验库中已定义的插件入口与 SHA256,并去除胶水实现源码。
二进制包用 STATIC IMPORTED 目标保留原有 SDK 链接与资源处理,产物名加 -binary。
以下流程描述默认源码模式;两种模式均在校验通过后生成安装脚本、文档与 ZIP。
- 用 python 从
cc_plugin.json读name/version/modules[0].target/engine-version; rsync插件目录到dist/<name>-cocos-plugin-<version>/(排除.DS_Store,默认排除ios/sdk); 加MYSDK_WITH_IOS_SDK=1时把 iOS SDK(约 252MB)也打进去;- 校验(不过就退出、不产出):必需文件在不在(
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 只警告; - 生成
manifest.json(带时间戳)、INTEGRATION.md(接入说明)、install.sh(rsync到<game>/native/native-plugin/<name>/); ditto打 zip,打印产物路径与 SHA256。
update-sdk.sh(换 SDK 版本)
- 按文件名在各目录里找最新的包(可用
--sdk-zip-dir或--ios/--mac/--android/--pc指定); - 解压到临时目录,按目录特征定位真正的 SDK 根(如
osx/、<SDK>/SDK/、<sdk>/libs/、 PC 包里的Plugins/x86_64/*.dll); rsync -a --delete覆盖到插件内{mac,ios,android,windows}/sdk/;- 只刷新二进制/资源,不改代码:SDK 接口/依赖结构变化时,需要按「新增一个接口」与
「各平台后端与 SDK 位置」手动同步
.mm/.cpp与*-config.cmake,然后重新构建验证 (run-mac.sh)并重新导出(export-plugin.sh)。
publish-doc-site.sh(文档静态站点生成与发布)
- 将脱敏版技术文档通过 Python 生成现代化响应式静态站点(含侧边栏 TOC、滚动高亮、快速检索模态框、代码高亮与环形进度指示器);
- 产物输出至
dist/doc-site/; - 支持通过
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 四平台编译环境与流程 #
本机(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,并让命令行工具指向它):
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。
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(改插件后常用,比整包快):
"/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 #
xcrun --sdk iphoneos --show-sdk-version
security find-identity -v -p codesigning
厂商 SDK 是 arm64 真机静态库:设备构建与模拟器构建不可互用,SDK 也不提供模拟器
版本,因此不能靠「切 Apple Silicon 模拟器」验证 iOS 插件。外部 SDK 可用 CMake 参数
-DMYSDK_IOS_SDK_DIR=/path/to/sdk 指定(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 位置)。
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,产物与日志在 build/ios/ 与
dist/。只编译真机插件:
"/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 |
新机器准备同类环境:
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 头文件(按 native/external-config.json 的
external tag 取)、厂商 MySDK.dll + 配置 json(运行时用,已内置 windows/sdk/)。
插件 .lib 由导出脚本调用 plugin-binaries.py 生成;macOS 上没有 Windows 原生
CMake cache 时自动走交叉分支,编译 src/ 与 windows/ 的胶水并配置插件静态入口、
V8 与平台宏:
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++ 工程也能直接用这套环境:
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 管理,不依赖 Unity 内置 NDK) |
| 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 |
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 并在设备上验证。
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,再导出对应插件二进制。
分发与兼容条件 #
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/ 指定 SDK 来源路径(导出的环境变量与构建期 CMake 变量名 不同,别混用)。 - 兼容条件: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,把ts/MySDK.ts拷到游戏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/(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...保证符号解析一致。
11 游戏侧接入 #
- 用导出包:
bash install.sh /path/to/game(或手动把插件目录拷到<game>/native/native-plugin/MySDK/)。 - 把
ts/MySDK.ts拷到游戏assets/scripts/。 - 用
MySDKTest.ts的写法接setEventListener+init/login/...。 - 平台依赖:
- 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 真机架构(不支持模拟器架构构建),需连接已登记测试设备并配置对应描述文件;
- 编辑器「预览浏览器/模拟器」不会加载原生插件(
MySDK.available === false), 必须构建原生包才能验证。
12
常见疑问:JS 不能直接和 Native 通信吗?为什么要 .h/.mm?
#
结论:能「直接通信」,但那条通信管道本身必须由原生代码提供。Cocos 不像 Unity 那样默认
带一个「随便调任意 ObjC/Java API」的通用反射桥;.h/.mm 不是多余的绕路,它就是那条管道,
外加类型转换、线程调度和平台抽象。
拆开看:
JS 的世界里没有 ObjC/Java 的符号。游戏 JS 跑在 App 内嵌的 V8 里,它能碰到的只有引擎 主动注册给它的东西(
cc.*、以及插件注册的MySDKNative)。SDK 是个 ObjC framework / Java AAR,语言都不同,JS 无处直接引用。Cocos 现成的「JS ↔ Native」通道,都是原生侧写好的代码:
JsbBridge(native.bridge):JS 发字符串事件到 Java/ObjC 侧,原生侧必须自己实现onScript回调——也就是说还是得写原生胶水,只是省了绑定代码。适合轻量通知, 不适合带复杂返回值/高频/大数据的调用。jsb.reflection(原生反射):能callStaticMethod调静态方法、传基础类型/字符串, 但能力与性能都有限,复杂类型、回调、生命周期都要自己兜。- JSB 绑定(sebind / 手动绑定):把 C++ 类/函数注册进 JS——这正是本插件用的方式, 能力最强(任意类型、返回值、错误处理),代价是要写绑定层。
就算把 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 工程」的分发包。
什么时候可以真的少写原生代码:如果只是「触发一个事件」且原生侧本来就有接入点 (如
AppDelegate里已有推送回调),用JsbBridge转发到 JS 更省事。但要做 SDK 的完整能力 (登录、用户中心、支付、带结果回调),插件 + sebind 绑定是最稳的形态。
一句话:JS 和 Native 通信的「路」永远要 native 侧修;.h/.mm 修的就是这条路,
而且顺手把跨平台差异、线程安全、类型转换都封装掉了。
13 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) |
# 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 模板,若升级后出现符号链接冲突需重新核对引擎侧适配与依赖链。
14 参考文档 #
官方手册(Cocos Creator 3.8 LTS) #
- 原生插件(总览):https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/native-plugins/brief.html
- 原生插件创建范例(tutorial,
cc_plugin.json+CC_PLUGIN_ENTRY完整示例): https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/native-plugins/tutorial.html - JSB 2.0 使用指南(
se::Value/se::Object/AutoHandleScope/ 后台线程调用注意事项): https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/JSB2.0-learning.html - JSB 手动绑定 / 自动绑定: https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/jsb-manual-binding.html · https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/jsb-auto-binding.html
- JS ↔ Objective-C / Java 通信(JsbBridge,「常见疑问」里提到的“现成通道”): https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/js-oc-bridge.html · https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/js-java-bridge.html · https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/jsb-bridge-wrapper.html
- 原生反射机制(ObjC / Java): https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/oc-reflection.html · https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/java-reflection.html
- CMake 使用简介 / 原生平台 JS 调试: https://docs.cocos.com/creator/3.8/manual/zh/advanced-topics/cmake-learning.html · https://docs.cocos.com/creator/3.8/manual/zh/editor/publish/debug-jsb.html
- 4.0 手册同章节(内容更新,写法兼容): https://docs.cocos.com/creator/4.0/manual/zh/advanced-topics/native-plugins/brief.html
引擎源码(v3.8.8) #
- 插件宏与入口:
native/cocos/plugins/Plugins.h(CC_PLUGIN_ENTRY/cc_load_all_plugins)· https://github.com/cocos/cocos-engine/blob/v3.8.8/native/cocos/plugins/Plugins.h - 事件总线:
native/cocos/plugins/bus/EventBus.h(BusType/Listener/ScriptEngineEvent)· https://github.com/cocos/cocos-engine/tree/v3.8.8/native/cocos/plugins - sebind:
native/cocos/bindings/sebind/· https://github.com/cocos/cocos-engine/tree/v3.8.8/native/cocos/bindings/sebind - 插件扫描逻辑:
native/cmake/scripts/plugins_parser.js(MAX_SEARCH_LEVEL = 7)· https://github.com/cocos/cocos-engine/blob/v3.8.8/native/cmake/scripts/plugins_parser.js - 生成
Pre-AutoLoadPlulgins.cmake与plugin_registry:templates/cmake/common.cmake· https://github.com/cocos/cocos-engine/blob/v3.8.8/templates/cmake/common.cmake - 官方最小示例仓库:https://github.com/PatriceJiang/ccplugin_tutorial
本地安装目录里可以直接翻源码验证:/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)。