Phaser 活动日历与运营配置:客户端展示、时区和灰度开关要讲清楚

面向 Phaser Web 游戏的 LiveOps 活动系统,说明活动日历、配置拉取、时区处理、入口展示、灰度开关和降级策略。

活动系统不是首页多放几个按钮

当 Phaser 小游戏从一次性内容走向长期运营,活动系统很快会成为客户端最容易混乱的部分。今天有七日签到,明天有周末双倍,后天有 Boss 挑战,节日还有皮肤兑换。运营希望随时开关,策划希望复用模板,美术希望换入口图,客户端希望别每次发版。最糟糕的实现是把活动入口写死在 Scene 里,用本地时间判断开始结束,再让每个活动自己拉一份配置。这样迟早会出现活动提前消失、时区错一天、入口显示但奖励领不了、灰度用户看到错误活动等问题。

一个可上线的活动系统需要活动日历、配置版本、时区规则、入口状态和降级策略。Phaser 只负责展示活动入口、红点、倒计时和活动页面;活动是否开放、玩家是否有资格、奖励是否可领,应该由活动服务或统一配置模型决定。即使是纯静态站点上的小游戏,也应该把活动配置集中管理,而不是散落在多个 Scene。

活动状态要分层

活动不是简单的 active/inactive。至少要区分:未开始、预告中、进行中、结算中、已结束、隐藏。预告中可以显示入口但不能参与;进行中可以完成任务和领奖;结算中可以查看排名但不能继续获得积分;已结束可以展示回顾或完全隐藏。不同活动模板可能有不同状态,但客户端入口应该使用统一状态,避免 UI 到处写特殊判断。

时间也要分层。展示时间面向玩家,比如“5 月 1 日 10:00 开启”;服务器时间用于判断资格;本地时间只用于倒计时动画。跨时区游戏要明确活动以哪个时区为准。国内产品通常使用 Asia/Shanghai,全球产品可能按 UTC 或玩家分区。客户端不要用 new Date() 直接判断活动开放,至少要使用服务端返回的当前时间偏移。

flowchart TD
  A["客户端启动"] --> B["ConfigLoader 拉取活动日历"]
  B --> C["TimeSync 获取服务器时间偏移"]
  C --> D["EventResolver 计算活动状态"]
  D --> E["LobbyScene 显示入口、红点、倒计时"]
  E --> F["玩家进入活动页"]
  F --> G["EventPage 按模板渲染任务、奖励、排名"]
  G --> H["ClaimService 领奖并刷新状态"]
  B --> I["FallbackCache:配置失败时使用上次安全版本"]

配置要有版本和模板

活动配置不要只是一大坨 JSON。建议分成日历层和模板层。日历层说明活动 id、模板类型、开始结束时间、入口资源、目标人群、优先级;模板层说明任务规则、奖励池、兑换项、排行榜字段。这样同一个“累计登录”活动可以复用模板,只换时间和奖励。客户端根据模板类型加载对应页面组件,未知模板要安全隐藏或显示“版本过低”提示。

每份配置都要有版本号和生效时间。客户端拉取到新配置后,先校验字段,再替换当前配置。校验失败不能把旧活动清空。最好保存上一份成功配置作为 fallback。Web 游戏经常遇到 CDN 缓存、弱网、跨域失败,活动入口不能因为一次请求失败就变成空白。

入口展示要解决优先级冲突

大厅空间有限,不可能所有活动都用大 Banner。可以把活动入口分为主推、列表、红点和弹窗。主推通常只展示一个或两个,由优先级和玩家资格决定;列表展示所有进行中和预告活动;红点只在有可领取奖励或即将结束时显示;弹窗要严格限频,避免玩家每次打开游戏都被打断。

活动入口还要考虑冲突。两个活动都配置了首屏弹窗怎么办?同一时间有三个高优先级 Banner 怎么办?这些规则应该在 EventResolver 中处理,而不是每个 UI 组件自己排序。运营配置应该能预览最终入口效果,否则上线前很难发现遮挡和优先级错误。

Phaser 中的配置解析

下面的示例把活动状态计算集中到一个 Resolver。它不依赖 Phaser,Scene 只拿结果展示。

type EventState = "hidden" | "preview" | "active" | "settlement" | "ended";

interface EventConfig {
  id: string;
  template: "login" | "challenge" | "exchange";
  startsAtMs: number;
  endsAtMs: number;
  settlementEndsAtMs?: number;
  previewStartsAtMs?: number;
  priority: number;
  enabled: boolean;
  audience?: string[];
}

export function resolveEventState(event: EventConfig, nowMs: number, playerSegments: string[]): EventState {
  if (!event.enabled) return "hidden";
  if (event.audience?.length && !event.audience.some((seg) => playerSegments.includes(seg))) {
    return "hidden";
  }
  if (event.previewStartsAtMs && nowMs >= event.previewStartsAtMs && nowMs < event.startsAtMs) {
    return "preview";
  }
  if (nowMs >= event.startsAtMs && nowMs < event.endsAtMs) return "active";
  if (event.settlementEndsAtMs && nowMs >= event.endsAtMs && nowMs < event.settlementEndsAtMs) {
    return "settlement";
  }
  return nowMs >= event.endsAtMs ? "ended" : "hidden";
}

这个函数看起来简单,但它把几个高风险判断统一了:开关、人群、预告、进行、结算、结束。真实项目还要加入灰度比例、平台限制、最低客户端版本、地区限制。关键是所有入口和活动页都用同一个结果,不要大厅说 active,活动页又说 ended。

倒计时要用服务器时间偏移

倒计时 UI 可以每秒刷新,但基准应该是 serverNowMs = Date.now() + offsetMs。offset 来自启动时和服务端对时。对时不是为了防住所有作弊,而是为了让展示和服务器判断一致。玩家本地时间错了,倒计时仍然合理。若对时失败,可以使用缓存 offset,并在 UI 上保守展示;领奖这种关键操作仍以服务端返回为准。

倒计时文案要处理边界。活动还有 23 小时 59 分,不要显示“1 天”让玩家误解;最后一分钟可以显示秒;结束后不要继续显示负数。活动状态变化时,入口应自动刷新。Phaser Scene 可以用 time.addEvent 定时更新,但状态计算仍调用 Resolver。

灰度和降级要提前写

活动灰度可能按用户 id、地区、平台、版本、付费段、实验组。客户端不应该自己随机决定是否进灰度,除非纯本地活动。更好的方式是服务端返回玩家 segments,客户端只按配置匹配。这样运营可以调整人群,客服也能解释为什么某玩家看不到活动。

降级策略同样重要。配置拉取失败时,使用上一份成功配置;活动资源加载失败时,入口可以显示默认图或隐藏;领奖失败时,不要先播放成功动画;未知模板活动要隐藏并上报。不要让一个坏活动配置拖垮整个大厅。Phaser Web 游戏尤其要注意资源路径,如果活动 Banner 走 CDN,必须有超时和默认图。

活动页模板要限制自由度

运营活动多了以后,很容易想做一个“万能活动页”。但完全自由的 JSON UI 会把客户端变成小浏览器,调试和适配成本很高。更实际的方案是有限模板:登录签到、任务积分、兑换商店、挑战排行、收集图鉴。每个模板有固定布局和字段,配置只能填内容、奖励、资源和少量样式。这样能保证移动端适配、红点规则和领奖流程稳定。

如果某个节日活动确实需要定制页面,可以作为独立模板加入,而不是把万能模板继续扩张。模板数量增长不可怕,规则失去边界才可怕。

上线前检查清单

确认活动状态不只是 active/inactive;确认所有时间使用统一时区和服务器时间偏移;确认配置有版本、校验和 fallback;确认大厅入口、红点、活动页使用同一个 Resolver;确认未知模板安全隐藏;确认灰度人群由服务端或统一 segment 提供;确认资源加载失败有默认图或隐藏策略;确认领奖以服务端结果为准;确认弹窗限频;确认运营能预览同一玩家在某个时间点看到的入口。

活动系统的价值是让游戏持续有内容,但它也最容易把客户端变成临时判断的仓库。Phaser 负责让活动入口动起来、亮起来,但活动是否该出现,必须由清晰的日历和规则决定。把时间、配置和入口状态讲清楚,运营才敢频繁更新,客户端也不用每次活动都重新发版。

奖励预览和实际发放必须同源

活动页通常会展示奖励列表,领奖时再调用接口或本地结算。这里最容易出现展示和发放不一致:页面写着 100 钻石,实际配置发 80;活动翻倍期间展示翻倍但接口未翻倍;玩家所在灰度组看到了新奖励,但服务端仍按旧规则发放。解决办法是奖励预览和发奖都引用同一份 reward table,并带上配置版本。客户端展示时显示的是 rewardConfigVersion=2026-05-spring-03,发奖请求也提交这个版本,服务端按同一版本解释。

如果是纯客户端活动,也要把奖励快照写进机会记录。玩家点击领取时,不要再动态读取可能已经刷新的活动配置,否则跨天边界会出现“刚才看到的奖励变了”。正确流程是玩家进入活动页时可以刷新展示,但点击领取时创建一次领取记录,记录任务 id、奖励列表、活动版本和时间。发奖成功后写入已领取集合。这样即使活动配置随后更新,已经发生的领取仍然有据可查。

红点系统要避免过度打扰

活动红点看似小功能,实际会极大影响大厅体验。所有活动都抢红点,玩家会很快麻木。建议红点分级:可领取奖励是强红点;即将结束是弱提示;新活动首次出现可以显示一次新标;普通进行中不显示红点。红点计算也应该集中在 EventResolver 或 NotificationService,不要每个活动页自己决定。否则同一个任务完成后,大厅红点消失了,活动列表红点还在,玩家会觉得系统不可信。

红点还要限频。比如一个循环任务每 5 分钟可领一次,领取后不要立刻因为下一轮任务开始又弹强提醒,可以等玩家回到大厅或过一小段时间再提示。运营希望提高点击率,但过度提醒会降低长期留存。Phaser 里的红点表现可以很轻:小圆点、轻微呼吸、列表排序前置,不一定每次都弹窗。

活动配置需要预发布环境

长期运营不能直接把配置推到正式环境。至少要有预发布配置地址和测试账号 segment。客户端启动时根据环境或账号拉取测试活动,运营可以在真实设备上预览入口、倒计时、奖励和资源加载。预览环境还要支持“模拟当前时间”,否则测试一个跨天活动会很痛苦。模拟时间只能在开发或测试账号开放,正式包要禁用。

发布流程也要有回滚。配置中心保留最近几个成功版本,发现活动入口异常时可以快速切回旧版本。客户端 fallback 只能处理拉取失败,处理不了“成功拉到坏配置”的情况,所以服务端或配置发布系统必须有校验和回滚。活动越频繁,越要把发布当成工程流程,而不是运营手工改 JSON。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页