游戏项目怎样接入 iOS 原生能力

人和 AI 一起做游戏 · 第 13 篇

《人和 AI 一起做游戏》第 13 篇:游戏项目怎样接入 iOS 原生能力,Kibble Street TD 历史开发画面与流程图。

本篇依据截至 2026 年 8 月 3 日的开发记录,回顾 2026 年 6—7 月及 8 月初的开发过程。正文中的“当前”、数值、画面、候选资源与验证结论均指当时快照,不代表现售版本的状态。

上一篇:增加功能和删除功能同样重要

当《Kibble Street TD》只有战斗、升级和存档时,大部分工作都发生在同一套游戏逻辑里。准备提交 iOS 版本以后,事情开始改变:排行榜要与 Game Center 通信,商品和交易来自 StoreKit,广告依赖 AdMob 与用户同意状态,统计事件还要送往 Firebase。

这些能力都由平台或第三方服务提供,但玩家最终是在游戏页面里触发它们。一次“提交排行榜成绩”看起来只是点下按钮,背后却可能跨过游戏脚本、原生桥接、系统登录、网络请求和界面回调。任何一层没有把失败说清楚,最外层都只会得到一个没有反应的按钮。

所以这次真正要解决的,不是怎样再调用一个 SDK,而是怎样让平台能力进入游戏,同时不把玩法规则、平台状态和异步回调搅在一起。

先划分职责,再开始写桥

《Kibble Street TD》把这条边界分成三层。游戏脚本继续负责页面、玩法状态和业务决定;Objective-C++ 负责接住游戏运行时与 iOS 之间的消息,并把它们路由到正确服务;Swift 与系统框架负责现代 Apple API、异步任务以及平台返回的事实。

三层代码各自负责什么
三层代码各自负责什么

这个划分最重要的约束是:原生层不重新实现游戏。它不计算战斗伤害,不维护玩家背包,也不决定一笔商品应该发多少资源。相反,游戏脚本也不猜测用户是否已经登录系统账号、交易是否仍未完成,或者系统界面什么时候能够展示。

边界两侧只交换完成当前动作所需的数据。例如排行榜提交的是榜单标识、进度和请求编号;原生层返回成功、取消或具体错误。平台告诉游戏“发生了什么”,游戏再决定页面怎样提示、是否重试,以及后续业务怎样继续。

一个桥接入口,四条独立通道

原生能力刚开始接入时,很容易让每个模块分别占用一个全局回调。模块少时似乎简单,但广告、统计、排行榜和内购同时存在后,后安装的回调可能覆盖先安装的回调,问题还会随着初始化顺序变化而时隐时现。

当前方案在游戏侧只安装一个接收入口,在 iOS 侧也只保留一个总路由。消息到达后,根据通道分发给 StoreKit、Game Center、AdMob 或 Firebase;每个业务服务只订阅自己的通道,不接触其他模块的消息。

一个入口分发到四条原生通道
一个入口分发到四条原生通道

这样做并不是为了把代码做得更抽象,而是为了让冲突集中在一个可检查的位置。未知通道、无法解析的消息和无人处理的回调都会留下明确错误,而不是安静地消失。新增平台能力时,也先进入同一个路由,再建立自己的协议,不需要重新争夺全局入口。

原生调用不是一次普通的函数返回

游戏脚本调用本地计算时,通常能立即得到结果。系统登录、商品加载和购买则不同:请求发出后,玩家可能取消,网络可能超时,系统页面可能稍后才出现,应用也可能在交易完成前被关闭。

因此,一次需要结果的调用会携带唯一请求编号和命令。游戏侧把它登记为“等待中”,同时启动超时计时;原生层完成工作后,返回事件、成功状态、数据或错误,以及相同的请求编号。只有编号与预期事件都匹配,请求才会结束。

异步请求需要编号、等待表和超时
异步请求需要编号、等待表和超时

这条规则能区分两个先后返回的同类请求,也能识别迟到、重复和无法归属的结果。超时不是把错误吞掉,而是让页面获得一个可以解释和重试的失败。

不同能力还需要不同协议。排行榜和内购属于明确的请求—响应;激励广告从加载、展示到关闭与奖励,是一段生命周期事件;统计只是一条旁路消息,发送失败不能反过来阻塞战斗、结算或购买。把三种通信方式硬塞成同一种函数,会让边界看似统一,实际更难判断动作是否真正结束。

一次排行榜提交怎样往返

以排行榜为例,游戏页面发起提交后,排行榜服务先生成请求编号,再通过统一桥接入口发送。iOS 总路由识别 Game Center 通道,把数据交给对应服务;系统完成认证或提交后,原生服务把结果编码为响应,再沿同一通道送回游戏。

游戏侧收到响应后,不直接把它广播给所有页面,而是找到等待相同请求编号的任务,核对事件类型,清理超时计时,再把结果交还给最初的调用者。需要展示系统登录或排行榜页面时,原生层还必须切回主线程处理界面。

这条往返链路的价值在于可追踪。出问题时可以依次确认:页面是否发起、请求是否登记、通道是否命中、系统是否回调、响应是否匹配。相比一句“排行榜偶尔没反应”,每一步都能留下能够继续定位的信息。

为什么 Objective-C++ 和 Swift 会同时出现

只看语言数量,同时保留 Objective-C++ 和 Swift 像是增加复杂度。但两者在这里接触的是不同边界。

Objective-C++ 离游戏运行时的原生接口更近,适合接收桥接消息、调用 iOS 对象并统一路由。StoreKit 2 和应用交易环境等现代接口则以 Swift 实现更直接,它们大量使用异步任务、类型化结果和主线程约束。于是 StoreKit 通道由 Objective-C++ 接住请求,再交给 Swift 服务;Swift 完成后,通过一个很窄的回调把结果送回。

Objective-C++ 与 Swift 只在窄接口处交接
Objective-C++ 与 Swift 只在窄接口处交接

这个接口还刻意保持稳定:Swift 服务用明确的原生名称暴露,Objective-C++ 只声明需要调用的最小方法,不依赖随工程名称变化的自动生成头文件。桥接文件也必须用正确的内存管理方式编译,否则认证回调和数据对象可能在运行中产生崩溃。

所以语言并不是按“新旧”分工,而是按边界分工:哪一层最接近运行时,哪一层最适合平台 API,就让它承担那一小段职责。

接入能力,也要进入构建系统

桥接源码存在,并不代表最终安装包里一定有这项能力。源文件需要进入 iOS 构建目标,Swift 编译器需要启用,StoreKit 与 GameKit 等系统框架需要链接,广告和统计依赖需要安装,相应的权限、隐私声明和服务配置也要随包进入正确位置。

原生能力必须穿过完整构建链路
原生能力必须穿过完整构建链路

《Kibble Street TD》已经有一条基线完成过 Release 归档并导出 IPA,说明游戏工程、原生源码、Swift、系统框架和第三方依赖能够在同一次构建中成立。但这次归档早于最新一轮统计协议调整,因此它不能替当前源码背书。最新状态仍需要重新归档,并继续做真机回归。

这也是发布开发中很重要的一条边界:过去构建成功证明链路曾经成立;代码发生变化以后,只能把它当基线,不能写成最新版本已经通过。

验证要分成四层

当前排行榜、统计、内购和广告的专项静态检查已经通过,它们能验证通道名称、请求协议、超时、恢复与错误分支仍然存在。此前的 Release 归档和 IPA 导出也已经通过。

但静态检查不能代替编译,历史归档不能代替最新归档,归档成功更不能代替外部平台验收。Game Center 仍需要两个真实账号验证榜单边界与账号切换;StoreKit 仍需要 Sandbox 或 TestFlight 交易与进程中断恢复;广告需要真机上的同意流程和展示回归;统计事件需要在真实环境中核对接收结果。

原生能力的四层验证
原生能力的四层验证

把这些状态分开记录,会让“已经完成”更准确。代码合同通过、安装包能够生成、设备行为正确、平台后台收到结果,是四个不同结论。少写一个勾,比把尚未发生的验证写成成功更有价值。

人和 AI 分别负责什么

AI 很适合沿着调用链扫描接口、通道、请求编号、超时和错误分支,也能把源码、构建配置与验证脚本对照起来,发现“代码写了但没有进入构建目标”或“回调存在但没有订阅者”这样的断点。协议调整后,它还能快速列出必须重新验证的相邻模块。

人负责决定边界:哪些状态必须由平台说了算,哪些业务只能留在游戏内;一个系统取消应该怎样影响玩家体验;什么证据足以进入发布结论。真机登录、购买确认、系统授权和后台配置也需要人在真实账号与设备环境中完成。

如果你想开始

不要从复制一段 SDK 示例开始。先画出一条最短调用链:谁发起、发送什么、由谁处理、会返回哪些结果、多久算超时、失败以后谁负责提示。然后只为这条链建立一个通道和一个可以失败的最小请求。

等它能够在日志中完整往返,再接入第二项能力。每增加一项,都检查它是否复用了同一个入口,是否拥有独立通道,是否把平台事实带回业务层,又没有把游戏规则复制到原生代码里。