背景:本地通知调度为什么会变成真实问题
运营想加本地通知:体力满了提醒、建造完成提醒、每日奖励提醒、活动结束前提醒。需求听起来简单,移动端插件也能发通知。但第一次测试就暴露了问题:玩家关闭活动后仍收到活动提醒;体力已经被服务器补满又消耗,通知时间不准;同一天收到了三条类似文案;Android 13 没授权时没有任何解释;玩家点通知回到游戏后不知道该打开哪个页面。通知系统如果只是“定个时间发一条”,很快会变成打扰。
本地通知的核心是调度和取消,而不是发送。客户端要根据玩家状态安排提醒,状态变化时取消或更新旧提醒,权限变化时给出合适入口,点击通知后恢复到正确页面,还要记录通知带来的回流。Godot 层通常通过平台插件调用 iOS/Android 通知能力,但业务规则应该留在客户端统一服务里,避免每个玩法自己注册一堆不可追踪的提醒。
flowchart TD
A["玩法状态变化"] --> B["NotificationScheduler"]
B --> C["计算候选提醒"]
C --> D["权限与频控检查"]
D --> E{是否允许调度}
E -- "否" --> F["记录跳过原因"]
E -- "是" --> G["取消旧通知"]
G --> H["注册平台本地通知"]
H --> I["保存本地索引"]
J["玩家点击通知"] --> K["解析 deep link"]
K --> L["进入对应页面并上报回流"]
通知必须有业务所有者
每条通知都要知道自己属于哪个业务、哪个对象和哪个版本。比如体力满提醒的 key 可以是 energy_full,建造完成提醒可以带 building_id,活动提醒要带 activity_id 和 activity_version。这样状态变化时才能精准取消。活动结束后,如果只知道通知 ID 而没有业务索引,很容易漏掉旧提醒。我们在本地保存通知索引,包含 key、trigger_time、payload、创建原因和平台返回 ID。调度前先查索引,决定更新、跳过还是取消。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。本地通知调度相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
频控比文案更重要
通知文案写得再好,频率过高也会被关闭权限。我们设置了全局频控和业务频控:同一天最多几条,夜间不发,短时间内相似提醒合并。体力满和建筑完成如果时间接近,可以合成“有新的资源可以领取”。活动结束提醒要避开玩家刚在线时的推送,因为玩家已经在游戏里看到提示。频控规则最好可配置,但客户端要有默认保护,避免远程配置错误导致通知轰炸。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。本地通知调度相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
权限请求要等到玩家理解价值
移动端通知权限很宝贵。第一次启动就弹系统权限,很多玩家会拒绝。更好的时机是在玩家第一次开启相关提醒时,例如建造一个需要等待的建筑后,告诉玩家可以在完成时提醒。Godot 客户端可以先显示自定义说明,再调用平台权限请求。被拒绝后不要反复弹,只在设置页显示开启入口。Android 新版本、iOS 临时权限、渠道系统设置都可能不同,平台层返回统一状态:unknown、granted、denied、limited。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。本地通知调度相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
状态变化时要主动取消
体力通知的正确逻辑不是注册一次就完事。玩家上线领取体力、购买体力、消耗体力、服务器调整上限,都可能改变满体力时间。每次相关状态变化,Scheduler 重新计算目标时间,取消旧通知并注册新通知。如果目标时间太近,比如两分钟内,就可以不发,避免玩家刚切出去就收到提醒。活动通知同理,活动下线、玩家完成目标、奖励已领取,都要取消剩余提醒。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。本地通知调度相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
点击通知需要 deep link
玩家点通知回到游戏后,应该进入相关页面,而不是只打开大厅。通知 payload 里带 deep link,例如 game://energy、game://building/123、game://activity/snow_festival。启动时如果资源和登录还没完成,先把 deep link 放入队列,等大厅 ready 后再导航。导航失败要有降级,比如活动已结束就打开活动列表并提示。点击回流也要埋点,区分通知展示、点击、成功到达页面和页面失效。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。本地通知调度相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
通知 QA 要跨真实设备
本地通知在模拟器和真机、不同 Android 厂商、不同系统权限下差异很大。QA 至少覆盖授权、拒绝、改系统时间、杀进程、重启设备、活动取消、重复调度、点击 deep link、夜间频控和多语言文案。Godot 层的 Scheduler 可以用假平台服务做自动化测试,真机则验证平台行为。通知是对玩家注意力的请求,工程上要能取消、能解释、能追踪,产品上也要克制。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。本地通知调度相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
调度规则要基于服务器时间
本地通知常常涉及“体力几小时后满”“活动明晚结束”。如果完全依赖设备时间,玩家改系统时间会让提醒不准。联网游戏最好用服务器时间计算关键事件,再换算成本地触发时间。客户端保存 server_time_offset,调度时用服务器时间判断活动是否仍有效。离线游戏则要承认设备时间是唯一来源,但也要处理时间倒退和大幅跳变。
当应用恢复前台时,Scheduler 应重新对账:检查哪些通知已经过期、哪些状态已经变化、哪些需要取消。不要假设平台一定按时触发,也不要假设玩家收到通知后一定点击。通知只是提醒,不是业务事实。真正的体力、建造、活动状态仍要由游戏状态计算。
通知内容要避免过期承诺
文案里不要写太绝对的奖励承诺,除非客户端能保证状态。比如“你的宝箱已经可以领取”比“登录领取 100 钻石”更稳,因为奖励可能因活动结束或账号状态变化失效。活动结束提醒要带活动名和剩余时间,但点击后如果活动已结束,要给出合理解释。通知文案越具体,状态校验就越重要。
多语言也不能忽略。通知可能在游戏进程外显示,不能依赖运行时字体和 UI。平台通知文案要提前本地化,payload 里保存语言版本或文案 key。玩家切换语言后,未来通知应该使用新语言,已注册通知是否更新要看平台能力和业务价值。至少关键提醒应该取消后重建。
深链导航要等待客户端 ready
玩家从通知启动游戏时,登录、资源校验、热更新、隐私弹窗都可能挡在前面。Deep link 不能一进进程就强行打开页面。我们会把通知 payload 放进 PendingIntentQueue,等账号状态、资源版本和大厅 UI 都 ready 后再消费。消费前还要校验目标是否仍有效:建筑是否存在,活动是否开放,体力是否真的满。
如果目标失效,降级路径要自然。建筑不存在就打开主城,活动结束就打开活动列表或公告,体力不满就打开体力来源页。点击通知后的体验应该像有人给玩家指路,而不是把玩家丢到错误弹窗里。
取消和重建要有事务感
状态变化时,常常需要取消旧通知并注册新通知。这个过程如果中途失败,可能造成旧通知残留或新通知缺失。我们会先在本地索引里标记目标计划,再调用平台取消旧通知,成功后注册新通知,最后更新索引状态。若注册失败,索引记录失败原因,下次状态刷新时重试。虽然本地通知不是金融交易,但有索引状态能让排查清楚很多。
平台返回的通知 ID 要持久化。不要只存在内存里,否则杀进程后无法取消旧通知。对重复业务 key,注册前先查旧 ID 并取消。很多“活动结束还提醒”的问题,就是因为旧通知 ID 丢了。
通知效果要闭环分析
通知不是发出去就结束。至少要记录调度、平台注册成功、用户点击、点击后到达目标页、目标页完成行为。展示事件有些平台拿不到,可以用调度和点击近似,但点击和回流一定要记录。这样运营才能知道哪些提醒有用,哪些只是打扰。
分析时也要看负面指标:通知权限关闭率、通知后立即退出、同日多次点击失败。若某类通知带来很多点击但到达页失效,说明调度条件或 deep link 校验有问题。若权限关闭率上升,说明频率或时机过 aggressive。技术系统提供数据,产品才能做克制的调整。
不同平台能力要统一成最小接口
iOS 和 Android 的本地通知能力、权限状态、渠道设置、角标、精确闹钟限制都不完全一样。Godot 层不要直接依赖平台细节,而是定义最小接口:request_permission、get_permission_state、schedule、cancel、cancel_by_group、open_settings。平台插件各自适配,返回统一错误码。业务只关心提醒是否注册成功,以及失败是否可引导用户处理。
Android 通知渠道尤其需要提前设计。不同类型提醒可以放不同 channel,例如资源完成、活动提醒、系统通知。玩家可能关闭某个 channel,但保留其他提醒。Scheduler 读取平台状态时,要能知道是总权限关闭,还是某个渠道关闭。提示文案也不同:总权限关闭引导系统设置,渠道关闭则引导通知类别。
离线和重装后的行为
本地通知可能在玩家长期离线时触发,也可能在游戏卸载重装后失去索引。客户端不要假设索引永远完整。启动时可以尝试同步平台已注册通知和本地索引,做不到的平台就以本地状态为准,重新计算未来通知。重装后旧通知通常会消失,但账号状态可能从云端恢复,Scheduler 应根据恢复后的状态重新安排。
长期离线回来时,过期通知不要一股脑补发。体力三天前满了,玩家现在打开游戏,只需要在游戏内显示状态,不需要再弹旧提醒。Scheduler 只调度未来事件,过期事件用于埋点或清理,不再通知。这个规则能避免玩家回归时被历史提醒打扰。
结语
这类系统在 Godot 里往往不是“某个 API 会不会用”的问题,而是边界有没有提前说清楚。节点、资源、平台能力和业务状态都很灵活,灵活就意味着团队需要给它们加上可维护的秩序。我的经验是,先把生命周期、输入输出、失败路径和调试信息写明,再去追求抽象优雅。这样项目进入频繁迭代期时,新增需求不会把旧功能挤得变形,排查问题的人也能从日志、结构和约定里找到线索。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。