原生插件开发与接入指南(脱敏版)
覆盖 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。
构建验证 #
构建一次对应原生平台,检查:
- 构建日志出现
[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(导出游戏可接入的分发包)
- 用 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 各平台后端与 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 游戏侧接入 #
- 用导出包:
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), 必须构建原生包才能验证。
11
常见疑问: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 修的就是这条路,
而且顺手把跨平台差异、线程安全、类型转换都封装掉了。
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) |
# 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) #
- 原生插件(总览):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)。