Phaser 激励视频广告流程:奖励发放、失败恢复和反作弊边界要闭环

讲解 Phaser Web 游戏接入激励视频广告时的状态机、奖励发放、SDK 异常、用户体验、埋点和防重复领取策略。

激励广告最怕“看了但没给”

Phaser 小游戏接入激励视频广告,看似只是点按钮、调 SDK、播放完成后发奖励。真正上线后,问题会集中爆发:广告加载失败,按钮还亮着;玩家看完视频,SDK 回调丢了;网络断开,奖励状态不明;用户连续点击触发两个广告;复活场景里游戏还在跑,玩家看完回来已经死了;有人伪造回调刷奖励。激励广告是商业化功能,但它首先是一个状态机。如果状态机不闭环,收入和口碑都会受影响。

本文不讨论某个具体广告平台 API,而是讨论 Phaser 客户端应该如何包一层 RewardedAdService。它负责加载、展示、监听、发奖和恢复游戏。Scene 只提出意图:我要用一次广告复活、我要双倍离线收益、我要刷新商店。广告服务返回明确结果:完成、取消、失败、超时、奖励已发或待确认。不要让每个按钮自己直接调用广告 SDK。

广告机会要先建模

不同广告入口有不同规则。复活广告通常一局只能看一次,且必须在死亡后的短时间内;双倍收益广告基于本次结算金额;商店刷新广告可能每天有限次数;跳过等待广告可能影响生产时间。把这些都叫 showAd() 会丢失上下文。建议定义 Reward Opportunity:id、来源、奖励类型、数量、过期时间、次数限制、服务端校验字段。玩家点击按钮时,先创建机会,再展示广告,最后按机会发奖。

机会 id 很关键。它能防止重复领取,也能帮助客服查问题。一次广告发奖应该绑定一个唯一 opportunityId。即使 SDK 回调来了两次,奖励也只发一次;即使客户端崩溃,重新进入后也能查询这个机会是否已经完成。

stateDiagram-v2
  [*] --> Idle
  Idle --> Loading: 预加载广告
  Loading --> Ready: SDK ready
  Loading --> Unavailable: 加载失败
  Ready --> Showing: 玩家确认观看
  Showing --> Completed: SDK 完播回调
  Showing --> Cancelled: 用户关闭或未看完
  Showing --> Failed: SDK 错误或超时
  Completed --> Granting: 提交 opportunityId 发奖
  Granting --> Granted: 发奖成功
  Granting --> Pending: 网络失败,待恢复
  Granted --> Idle
  Pending --> Idle: 下次启动补偿查询
  Cancelled --> Idle
  Failed --> Idle

游戏暂停和恢复要统一

广告播放期间,游戏必须进入安全暂停。对于复活广告,死亡结算界面已经暂停;对于战斗中临时看广告,必须暂停物理、计时器、敌人 AI 和输入。Phaser 的 scene.pause() 可以暂停 Scene,但不一定暂停你所有外部定时器和网络轮询。最好有一个 GamePauseService,广告、弹窗、系统失焦都通过它加锁。多个暂停来源叠加时,只有全部释放才恢复。

恢复时要检查上下文是否仍然有效。比如玩家点击复活广告后,广告播放期间页面被刷新,回来时原来的战斗 Scene 不在了。这时不能直接发复活效果到不存在的 Scene。奖励发放应先进入账户或待领取状态,再由当前场景决定如何表现。复活这种强场景奖励,可以在机会创建时保存 battleId,回来后若 battleId 不匹配,就转换为补偿货币或提示已失效,具体规则要提前定。

SDK 回调不等于奖励可信

广告 SDK 的完成回调是必要信号,但不应是唯一可信依据。纯客户端小游戏可以在本地发奖,但至少要防重复;有服务端的游戏应该让客户端把 opportunityId、广告平台回调信息和玩家状态提交给服务端,由服务端决定是否发奖。客户端收到服务端成功后再播放奖励动画。不要先给奖励再异步上报,因为失败后很难追回,也容易被刷。

如果广告平台支持服务端回调,关键奖励最好走 S2S。客户端只负责展示和查询结果。若暂时没有服务端,也要把本地发奖记录写入存档,包括机会 id、入口、时间、奖励和 SDK 结果。这样至少能排查“看了没给”和“重复给了”。

一个广告服务接口

下面的代码展示客户端侧状态封装。具体 SDK 适配器可以替换,Scene 不需要知道平台差异。

type RewardResult =
  | { status: "granted"; opportunityId: string }
  | { status: "cancelled"; reason: string }
  | { status: "failed"; reason: string }
  | { status: "pending"; opportunityId: string };

interface RewardOpportunity {
  id: string;
  source: "revive" | "doubleReward" | "shopRefresh";
  reward: { itemId: string; count: number };
  expiresAtMs: number;
}

export class RewardedAdService {
  private showing = false;

  constructor(
    private readonly sdk: RewardedAdSdk,
    private readonly grant: (opportunity: RewardOpportunity) => Promise<RewardResult>,
  ) {}

  async show(opportunity: RewardOpportunity): Promise<RewardResult> {
    if (this.showing) return { status: "failed", reason: "ad_already_showing" };
    if (Date.now() > opportunity.expiresAtMs) return { status: "failed", reason: "opportunity_expired" };
    this.showing = true;
    try {
      const watched = await this.sdk.showRewardedVideo({ placement: opportunity.source });
      if (!watched.completed) return { status: "cancelled", reason: watched.reason ?? "not_completed" };
      return await this.grant(opportunity);
    } catch (error) {
      return { status: "failed", reason: error instanceof Error ? error.message : "sdk_error" };
    } finally {
      this.showing = false;
    }
  }
}

这个接口把取消、失败、待确认分开。玩家主动关闭广告不是错误,不应该弹红色报错;SDK 加载失败才是失败;网络发奖失败可以进入 pending,下次启动查询或补发。状态分清楚,UI 文案才不会混乱。

按钮状态要诚实

广告按钮不应该永远可点。它至少有几种状态:加载中、可观看、暂无广告、次数已用完、条件不满足、发奖处理中。每种状态的文案要明确。比如“暂无可用广告,稍后再试”比按钮无响应强得多。复活广告尤其要注意倒计时,广告没加载好时不要让玩家误以为还能无限等。可以在死亡前预加载广告,但不要在未准备好时承诺奖励。

移动端还要处理静音和音频恢复。广告 SDK 可能接管音频焦点,回来后 Phaser 声音需要恢复或重新解锁。广告期间最好暂停游戏音乐,回来后按用户设置恢复。不要让广告音频和游戏音效叠在一起。

埋点要能还原漏斗

激励广告需要完整漏斗:机会展示、按钮点击、广告请求、广告 ready、开始播放、播放完成、取消、失败、发奖请求、发奖成功、发奖失败。每个事件带 opportunityId、source、placement、玩家等级、网络状态和错误码。没有这些数据,只看广告平台收益报表无法解释“为什么点击很多但完成少”。

埋点也能发现体验问题。比如某机型 ad_load_failed 特别高,可能 SDK 资源被拦截;某入口取消率很高,可能奖励不够吸引;发奖 pending 多,可能服务端接口慢。商业化不是只接 SDK,后续优化依赖数据闭环。

反作弊边界要现实

客户端无法完全防止伪造广告完成,尤其是 Web 环境。能做的是提高成本并保护关键奖励。第一,机会 id 一次性使用;第二,奖励由服务端按机会状态发放;第三,入口次数和冷却由服务端或可信存档控制;第四,异常频率上报,比如一分钟内完成 20 次广告;第五,关键排行榜资源不应完全依赖广告本地发放。

纯单机游戏也要防止普通 bug 导致刷奖励。比如 SDK 完成回调和页面恢复回调都调用发奖,或者玩家双击按钮开两个 Promise。状态机和 opportunityId 能解决大多数非恶意重复领取问题。先把正常流程做闭环,再谈更复杂的安全策略。

上线前检查清单

确认每个广告入口都有 Reward Opportunity;确认机会 id 唯一且发奖幂等;确认广告期间游戏通过统一暂停服务暂停;确认取消、失败、pending、成功有不同 UI 文案;确认 SDK 超时不会卡死按钮;确认发奖成功后才播放奖励入账动画;确认下次启动会处理 pending 奖励;确认埋点覆盖完整漏斗;确认广告不可用时有替代路径;确认关键奖励由服务端或至少本地幂等记录保护。

激励广告本质上是一笔交易:玩家付出注意力,游戏给出承诺的奖励。Phaser 可以把入口和奖励动画做得很顺滑,但交易状态必须严谨。只要出现一次“看了没给”,玩家对整个系统的信任就会下降。把状态机、机会 id 和失败恢复做好,商业化才不会伤害游戏体验。

奖励动画要反映真实状态

很多项目为了手感,会在广告关闭瞬间立刻播放金币飞入背包。这个细节如果没有和发奖状态绑定,会制造大量纠纷。建议把动画分成两段:广告完成后先显示“确认奖励中”,服务端或本地幂等记录成功后,再播放入账动画。如果网络慢,可以用短进度提示,而不是让玩家以为已经拿到。pending 状态下可以把奖励放进待领取箱,玩家下次进入大厅时继续查询。

复活类奖励尤其要谨慎。看完广告后,如果原战斗还在,就播放复活动画;如果战斗上下文失效,就按照事先规则转换补偿。不要既不给复活,也不给说明。对于广告商业化,最重要的不是每个异常都完美补偿,而是每个异常都有可解释状态。客服能根据 opportunityId 查到“广告已完播,发奖接口 pending,已在 21:35 补发”,玩家信任会高很多。

测试矩阵不能只测成功路径

接入广告后,QA 至少要覆盖这些场景:广告无填充、加载超时、用户中途关闭、播放完成但发奖接口失败、连续点击按钮、切后台再回来、横竖屏切换、静音模式、弱网、SDK 抛异常、页面刷新后 pending 补偿。Phaser 项目还要检查 Scene 暂停是否完整:计时器是否停了,敌人是否停了,背景音乐是否恢复,输入锁是否释放。成功播放一次只能说明 SDK 能跑,不能说明流程可靠。

继续阅读

探索更多技术文章

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

全部文章 返回首页