Skip to content

Flappy Bird — 商业化集成

预计阅读 45 分钟
版本信息
最后更新:2026-07-09 · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0) · 微信开发者工具:稳定版 1.06+

本章在 Cocos Creator 版本的基础上,集成微信小游戏的核心商业化能力:激励视频广告、皮肤内购、数据埋点、A/B 测试。你将得到一个从玩法到变现链路完整的 Flappy Bird 项目。


商业化整体流程

用户死亡
  ├─ 展示复活广告选项 ──→ 观看完成 → 复活继续游戏
  │                       └ 未看完 → 结算面板

  └─ 进入结算面板
        ├─ 皮肤商店入口 ──→ 选择皮肤 → 米大师支付 → 服务端验证 → 解锁
        └─ 再来一局

激励视频广告(复活机制)

Flappy Bird 中最高效的广告场景是「死亡后看广告复活」,用户主动触发,eCPM 最高。

typescript
import { _decorator, Component } from 'cc'
const { ccclass } = _decorator

@ccclass('ReviveAdManager')
export class ReviveAdManager extends Component {
  private videoAd: WechatMinigame.RewardedVideoAd | null = null
  private onReviveCallback: (() => void) | null = null
  private onFailCallback: (() => void) | null = null
  private isLoaded = false
  private loadAttempts = 0
  private readonly MAX_LOAD_ATTEMPTS_PER_SESSION = 20
  private retryTimer = -1

  /** 初始化广告 */
  init(adUnitId: string): void {
    if (!adUnitId) {
      console.warn('广告单元 ID 为空')
      return
    }

    this.videoAd = wx.createRewardedVideoAd({ adUnitId })

    this.videoAd.onLoad(() => {
      console.log('激励视频广告加载成功')
      this.isLoaded = true
      this.loadAttempts = 0
    })

    this.videoAd.onError((err) => {
      console.error('激励视频广告出错', err)
      this.isLoaded = false
      // 失败后指数退避重试,避免无限循环
      this.scheduleRetry()
    })

    this.videoAd.onClose((res) => {
      if (res.isEnded) {
        console.log('用户看完了广告,发放奖励')
        this.onReviveCallback?.()
      } else {
        console.log('用户中途关闭广告,不发放奖励')
        this.onFailCallback?.()
      }
      // 关闭后预加载下一条(受加载次数上限保护)
      this.preload()
    })

    this.preload()
  }

  /** 尝试展示复活广告 */
  async showReviveAd(onRevive: () => void, onFail: () => void = () => {}): Promise<void> {
    this.onReviveCallback = onRevive
    this.onFailCallback = onFail

    if (!this.videoAd) {
      onFail()
      return
    }

    try {
      await this.videoAd.show()
    } catch (err) {
      console.warn('广告 show 失败,尝试重新加载', err)
      this.isLoaded = false
      try {
        await this.videoAd.load()
        await this.videoAd.show()
      } catch (loadErr) {
        console.error('广告加载并展示失败', loadErr)
        onFail()
      }
    }
  }

  /** 广告预加载,带会话级加载上限保护 */
  preload(): void {
    if (this.loadAttempts >= this.MAX_LOAD_ATTEMPTS_PER_SESSION) {
      console.warn('本会话广告加载次数已达上限,停止预加载')
      return
    }
    this.loadAttempts++
    this.videoAd?.load().catch((err) => {
      console.warn('广告预加载失败', err)
      this.isLoaded = false
      this.scheduleRetry()
    })
  }

  /** 指数退避重试 */
  private scheduleRetry(): void {
    if (this.loadAttempts >= this.MAX_LOAD_ATTEMPTS_PER_SESSION) return
    clearTimeout(this.retryTimer)
    const delay = Math.min(1000 * 2 ** this.loadAttempts, 30000)
    this.retryTimer = setTimeout(() => this.preload(), delay)
  }

  /** 清理广告资源与定时器 */
  destroy(): void {
    clearTimeout(this.retryTimer)
    if (this.videoAd) {
      // 显式移除事件监听(虽然 destroy 通常会自动清理,但教学示例中显式 off 更安全)
      this.videoAd.offLoad()
      this.videoAd.offError()
      this.videoAd.offClose()
      this.videoAd.destroy()
      this.videoAd = null
    }
    this.onReviveCallback = null
    this.onFailCallback = null
    this.isLoaded = false
  }
}

与 GameController 集成

GameController 中死亡时调用广告复活:

typescript
// 在 GameController.ts 中增加
import { BirdController } from './BirdController'
import { ReviveAdManager } from './ads/ReviveAdManager'

@property(ReviveAdManager) reviveAdManager: ReviveAdManager | null = null

private _reviveCount = 0
private readonly MAX_REVIVE = 1

gameOver() {
  if (this._state === GameState.GameOver) return

  // 如果还有复活次数,先尝试广告复活
  if (this._reviveCount < this.MAX_REVIVE && this.reviveAdManager) {
    this.showReviveOption()
    return
  }

  this._state = GameState.GameOver
  // ... 显示结算面板
}

private showReviveOption() {
  // 显示"观看广告复活"弹窗(可用自定义 UI 实现)
  // 这里用简化逻辑:直接尝试播放广告
  this.reviveAdManager?.showReviveAd(
    () => {
      // 复活成功
      this._reviveCount++
      this._state = GameState.Playing
      this.bird?.getComponent(BirdController)?.flap()
      // 2 秒无敌时间,避免刚复活就死
      this.scheduleOnce(() => {
        // 取消无敌
      }, 2)
    },
    () => {
      // 复活失败,进入真正结算
      this._state = GameState.GameOver
      this.finalizeGameOver()
    }
  )
}

private finalizeGameOver() {
  // 更新最高分、显示结算面板等
  if (this._score > this._bestScore) {
    this._bestScore = this._score
    this.saveBestScore(this._bestScore)
  }
  if (this.finalScoreLabel) this.finalScoreLabel.string = String(this._score)
  if (this.bestScoreLabel) this.bestScoreLabel.string = String(this._bestScore)
  this.gameOverPanel!.active = true
}

restartGame() {
  this._reviveCount = 0
  // ... 清理管道、重置小鸟
}

皮肤系统 + 米大师内购

typescript
import { _decorator, Component, EventTarget } from 'cc'
const { ccclass } = _decorator

export const SkinEvent = new EventTarget()

export interface Skin {
  id: string
  name: string
  preview: string
  price: number // 元
  owned: boolean
}

@ccclass('SkinShop')
export class SkinShop extends Component {
  private skins: Skin[] = [
    { id: 'skin_default', name: '经典小鸟', preview: '', price: 0, owned: true },
    { id: 'skin_golden', name: '金色凤凰', preview: '', price: 3, owned: false },
    { id: 'skin_robot', name: '机器鸟', preview: '', price: 3, owned: false },
  ]

  private currentSkinId = 'skin_default'

  onLoad() {
    this.loadOwnedSkins()
  }

  getSkins(): Skin[] {
    return this.skins
  }

  getCurrentSkinId(): string {
    return this.currentSkinId
  }

  /** 购买皮肤 */
  async purchaseSkin(skin: Skin): Promise<boolean> {
    if (skin.owned) {
      this.equipSkin(skin.id)
      return true
    }

    // 企业主体且开通虚拟支付后才可调用
    const sysInfo = wx.getSystemInfoSync()

    // 截至 2026-07,微信小游戏虚拟支付通常仅支持 Android 真机环境
    if (sysInfo.platform !== 'android') {
      wx.showModal({
        title: '提示',
        content: '虚拟支付当前仅在 Android 客户端可用',
        showCancel: false,
      })
      return false
    }

    // 1) 先向服务端创建订单,获取商户唯一订单号
    const order = await this.createOrder(skin.id)
    if (!order?.outTradeNo) {
      console.error('创建订单失败')
      return false
    }

    return new Promise((resolve) => {
      wx.requestMidasPayment({
        mode: 'game',
        env: 1, // 开发联调用 1(沙箱),上线前切为 0
        offerId: 'your-offer-id', // 米大师后台配置的商品 ID,价格由后台决定
        currencyType: 'CNY',
        platform: 'android', // 米大师小游戏支付当前仅支持 android
        outTradeNo: order.outTradeNo, // 商户唯一订单号,必填
        buyQuantity: 1, // 购买数量(整数),此处购买 1 个皮肤
        zoneId: '1',
        success: (res) => {
          // 支付成功,向服务端验证订单后发放皮肤
          this.verifyAndGrant(skin.id, res).then((success) => resolve(success))
        },
        fail: (err) => {
          console.error('支付失败', err)
          resolve(false)
        },
      })
    })
  }

  /** 向服务端创建订单,获取 outTradeNo */
  private async createOrder(skinId: string): Promise<{ outTradeNo: string } | null> {
    return new Promise((resolve) => {
      wx.request({
        url: 'https://your-server.com/api/createOrder',
        method: 'POST',
        data: { skinId },
        success: (res) => {
          if (res.statusCode === 200 && (res.data as any)?.outTradeNo) {
            resolve({ outTradeNo: (res.data as any).outTradeNo })
          } else {
            resolve(null)
          }
        },
        fail: (err) => {
          console.error('创建订单请求失败', err)
          resolve(null)
        },
      })
    })
  }

  /** 向服务端验证订单(仅在支付 success 后调用一次,不会递归) */
  private async verifyAndGrant(skinId: string, payResult: any): Promise<boolean> {
    return new Promise(async (resolve) => {
      // 获取登录凭证 code(实际应由服务端用 code 换取 openId 后进行订单验证)
      let code = ''
      try {
        code = await this.getCode()
      } catch (err) {
        console.error('获取登录凭证失败', err)
        resolve(false)
        return
      }

      wx.request({
        url: 'https://your-server.com/api/verifyOrder',
        method: 'POST',
        data: {
          skinId,
          orderInfo: payResult,
          code,
        },
        success: (res) => {
          if (res.statusCode === 200 && (res.data as any)?.success) {
            this.setSkinOwned(skinId)
            resolve(true)
          } else {
            resolve(false)
          }
        },
        fail: (err) => {
          console.error('订单验证请求失败', err)
          resolve(false)
        },
      })
    })
  }

  private async getCode(): Promise<string> {
    return new Promise((resolve) => {
      wx.login({
        success: (res) => {
          // 注意:res.code 是临时登录凭证,需要发送到服务端换取 openId 和 session_key
          // 绝不要将 code 直接当作 openId 使用
          resolve(res.code)
        },
        fail: () => resolve(''),
      })
    })
  }

  private setSkinOwned(skinId: string): void {
    const skin = this.skins.find((s) => s.id === skinId)
    if (skin) skin.owned = true

    try {
      const ownedIds = this.skins.filter((s) => s.owned).map((s) => s.id)
      wx.setStorageSync('flappy_owned_skins', JSON.stringify(ownedIds))
    } catch (err) {
      console.error('保存皮肤数据失败', err)
    }
  }

  private loadOwnedSkins(): void {
    try {
      const raw = wx.getStorageSync('flappy_owned_skins')
      if (!raw) return
      const ownedIds: string[] = JSON.parse(raw)
      for (const skin of this.skins) {
        if (ownedIds.includes(skin.id)) skin.owned = true
      }
    } catch (err) {
      console.error('读取皮肤数据失败', err)
    }
  }

  equipSkin(skinId: string): void {
    this.currentSkinId = skinId
    try {
      wx.setStorageSync('flappy_current_skin', skinId)
    } catch (err) {
      console.error('保存当前皮肤失败', err)
    }
    // 通知小鸟组件切换皮肤纹理
    SkinEvent.emit('skinChanged', skinId)
  }
}

在 BirdController 中响应皮肤切换

typescript
// 在 BirdController.ts 的 onLoad 中
import { SkinEvent } from '../shop/SkinShop'

onLoad() {
  // ... 原有逻辑
  SkinEvent.on('skinChanged', this.onSkinChanged, this)
}

onDestroy() {
  // ... 原有逻辑
  SkinEvent.off('skinChanged', this.onSkinChanged, this)
}

private onSkinChanged(skinId: string) {
  // 根据 skinId 切换 SpriteFrame
  const sprite = this.getComponent(Sprite)
  if (sprite) {
    // resources.load(`skins/${skinId}/spriteFrame`, SpriteFrame, (err, frame) => {
    //   if (!err) sprite.spriteFrame = frame
    // })
  }
}

数据埋点

typescript
import { _decorator, Component } from 'cc'
const { ccclass } = _decorator

interface AnalyticsEvent {
  event: string
  data: Record<string, any>
}

@ccclass('Analytics')
export class Analytics extends Component {
  private static batchQueue: AnalyticsEvent[] = []
  private static flushInterval = 10000 // 每 10 秒批量上报
  private static sessionId = ''
  private static initialized = false
  private static timer = -1

  static init(): void {
    if (this.initialized) return
    this.initialized = true
    this.sessionId = this.generateSessionId()
    this.startFlushTimer()
  }

  private static startFlushTimer(): void {
    clearInterval(this.timer)
    this.timer = setInterval(() => this.flush(), this.flushInterval)
  }

  static track(event: string, data: Record<string, any> = {}): void {
    if (!this.initialized) this.init()

    this.batchQueue.push({
      event,
      data: {
        ...data,
        timestamp: Date.now(),
        sessionId: this.sessionId,
      },
    })

    // 关键事件立即上报
    if (event === 'purchase' || event === 'gameOver') {
      this.flush()
    }
  }

  private static flush(): void {
    if (this.batchQueue.length === 0) return

    const batch = [...this.batchQueue]
    this.batchQueue = []

    wx.request({
      url: 'https://your-analytics.com/track',
      method: 'POST',
      data: { events: batch },
      success: (res) => {
        if (res.statusCode >= 400) {
          console.warn('埋点上报失败', res)
          // 服务端返回错误时,可选择重新入队(注意避免无限堆积)
          this.requeue(batch)
        }
      },
      fail: (err) => {
        console.error('埋点网络失败', err)
        // 网络失败后将事件重新入队,下次继续上报
        this.requeue(batch)
      },
    })
  }

  /** 失败事件重新入队,限制队列长度防止内存无限增长 */
  private static requeue(batch: AnalyticsEvent[]): void {
    const maxQueueSize = 100
    this.batchQueue = batch.concat(this.batchQueue).slice(0, maxQueueSize)
  }

  /** 销毁时清理定时器,防止页面/场景切换后仍继续上报 */
  static destroy(): void {
    clearInterval(this.timer)
    this.timer = -1
    this.initialized = false
    this.flush() // 最后一次尝试上报
  }

  private static generateSessionId(): string {
    try {
      let sessionId = wx.getStorageSync('flappy_session_id')
      if (!sessionId) {
        sessionId = `s_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
        wx.setStorageSync('flappy_session_id', sessionId)
      }
      return sessionId
    } catch {
      return `s_${Date.now()}`
    }
  }
}

// 使用示例
Analytics.init()
Analytics.track('gameStart', { level: 1 })
Analytics.track('gameOver', { score: 42, pipesPassed: 42, duration: 120, revived: true })
Analytics.track('adShow', { type: 'rewarded', scene: 'revive' })
Analytics.track('purchase', { skinId: 'skin_golden', price: 3 })

A/B 测试

typescript
import { _decorator, Component } from 'cc'
import { Analytics } from '../analytics/Analytics'
const { ccclass } = _decorator

@ccclass('ABTest')
export class ABTest extends Component {
  /** 获取当前用户的分组,需传入稳定的用户标识 */
  static getGroup(openId: string, testName: string): 'A' | 'B' {
    const hash = this.hashString(openId + testName)
    return hash % 2 === 0 ? 'A' : 'B'
  }

  /** 模拟不同广告展示策略 */
  static getAdStrategy(openId: string): { reviveAdInterval: number; showBanner: boolean } {
    const group = this.getGroup(openId, 'ad_strategy_v1')

    Analytics.track('abTestGroup', { test: 'ad_strategy_v1', group })

    if (group === 'A') {
      // A 组:死亡时每次都展示复活广告
      return { reviveAdInterval: 1, showBanner: false }
    } else {
      // B 组:每 3 次死亡才展示一次复活广告 + 底部 Banner
      return { reviveAdInterval: 3, showBanner: true }
    }
  }

  private static hashString(str: string): number {
    let hash = 0
    for (let i = 0; i < str.length; i++) {
      const char = str.charCodeAt(i)
      hash = (hash << 5) - hash + char
      hash |= 0
    }
    return Math.abs(hash)
  }
}

使用 A/B 测试分组

微信小游戏的 wx.login 是回调式 API,直接使用 await wx.login() 并不可靠。建议复用 wechat-api.md 中封装的 wxLogin(),或在本文件内做一层 Promise 包装:

typescript
// 小程序登录 Promise 封装(可复用 utils/wx-promise.ts 中的 wxLogin)
const wxLogin = () =>
  new Promise<WechatMinigame.LoginSuccessCallbackResult>((resolve, reject) => {
    wx.login({ success: resolve, fail: reject })
  })

// 在 GameController 初始化时获取分组
async initABTest() {
  try {
    const loginRes = await wxLogin()
    // 实际 openId 需由服务端通过 code 换取,这里仅用于演示稳定分组
    const openId = `user_${loginRes.code.slice(-8)}` // 仅示例,切勿用于生产

    const strategy = ABTest.getAdStrategy(openId)
    this._reviveAdInterval = strategy.reviveAdInterval
    this._showBanner = strategy.showBanner
  } catch (err) {
    console.error('A/B 测试初始化失败', err)
  }
}

Banner 广告是常驻在屏幕底部的横幅广告,适合在非核心游戏区域持续展示,对游戏体验干扰最小。

创建与展示

typescript
// src/ads/BannerAdManager.ts
/**
 * Banner 广告管理器
 * 注意:Banner 广告仅在信息流或休闲类游戏中效果较好,
 * 重度游戏可能收益较低。建议根据游戏类型评估。
 */
class BannerAdManager {
  private static instance: BannerAdManager
  private bannerAd: wx.BannerAd | null = null
  private isShowing = false

  // 微信后台创建的广告位 ID
  private readonly AD_UNIT_ID = 'adunit-xxxxx'

  static getInstance(): BannerAdManager {
    if (!BannerAdManager.instance) {
      BannerAdManager.instance = new BannerAdManager()
    }
    return BannerAdManager.instance
  }

  /** 创建 Banner 广告实例 */
  create(): void {
    // 获取屏幕信息计算 Banner 位置
    const sysInfo = wx.getSystemInfoSync()
    const screenWidth = sysInfo.windowWidth
    const screenHeight = sysInfo.windowHeight

    // Banner 标准高度 300 逻辑像素
    const bannerHeight = 300

    this.bannerAd = wx.createBannerAd({
      adUnitId: this.AD_UNIT_ID,
      // 广告样式:宽度自动适配屏幕
      adIntervals: 30, // 自动刷新间隔(秒),最低 30 秒
      style: {
        left: 0,
        top: screenHeight - bannerHeight,
        width: screenWidth,
      },
    })

    // 监听广告加载成功
    this.bannerAd.onLoad(() => {
      console.log('Banner 广告加载成功')
    })

    // 监听广告加载失败
    this.bannerAd.onError((err) => {
      console.error('Banner 广告加载失败', err)
      // 失败时不重试——微信会自动重新请求
    })

    // 监听广告尺寸变化(适配不同设备的 Banner 高度)
    this.bannerAd.onResize((res) => {
      // 调整 Banner 位置,确保在安全区域之上
      this.bannerAd!.style.top = screenHeight - res.height
    })
  }

  /** 显示 Banner(游戏结算页、主菜单等非核心场景) */
  show(): void {
    if (!this.bannerAd || this.isShowing) return
    this.bannerAd.show()
    this.isShowing = true
  }

  /** 隐藏 Banner(游戏进行中、核心玩法区域) */
  hide(): void {
    if (!this.bannerAd || !this.isShowing) return
    this.bannerAd.hide()
    this.isShowing = false
  }

  /** 销毁 Banner(游戏退出时) */
  destroy(): void {
    if (this.bannerAd) {
      this.bannerAd.destroy()
      this.bannerAd = null
      this.isShowing = false
    }
  }
}
场景是否展示 Banner原因
主菜单✅ 展示非核心区域,不影响体验
游戏进行中❌ 隐藏避免遮挡游戏内容,导致误触和投诉
结算页✅ 展示用户停留时间较长,展示价值高
商店/设置✅ 展示非游戏核心内容
激励视频播放时❌ 隐藏避免广告堆叠
Banner 广告最佳实践
  1. 游戏时不展示:Banner 广告显示在 Canvas 上层,会遮挡游戏画面,玩家容易误触——这是最常见的负面评价来源。
  2. 自动刷新不低于 30 秒:刷新太快收益不会显著提升,反而增加加载失败率和用户反感。
  3. 适配安全区域:对于全面屏手机,Banner 底部需留出 Home Indicator 的高度(约 34px),避免被系统手势拦截。
  4. 做好降级onError 回调建议静默处理,不要弹窗提示用户。

插屏广告

插屏广告是全屏弹窗广告,在游戏场景切换(如关卡结束→结算页、重新开始→新关卡)时展示,收益高于 Banner 但需注意频次控制。

创建与展示

typescript
// src/ads/InterstitialAdManager.ts
/**
 * 插屏广告管理器
 * 关键约束:两次展示之间至少间隔 60 秒,否则微信不予展示
 */
class InterstitialAdManager {
  private static instance: InterstitialAdManager
  private interstitialAd: wx.InterstitialAd | null = null
  private lastShowTime = 0
  private readonly MIN_INTERVAL_MS = 60 * 1000 // 微信规定 60 秒最小间隔

  private readonly AD_UNIT_ID = 'adunit-xxxxx'

  static getInstance(): InterstitialAdManager {
    if (!InterstitialAdManager.instance) {
      InterstitialAdManager.instance = new InterstitialAdManager()
    }
    return InterstitialAdManager.instance
  }

  /** 创建插屏广告实例 */
  create(): void {
    this.interstitialAd = wx.createInterstitialAd({
      adUnitId: this.AD_UNIT_ID,
    })

    this.interstitialAd.onLoad(() => {
      console.log('插屏广告加载成功')
    })

    this.interstitialAd.onError((err) => {
      console.error('插屏广告加载失败', err)
    })

    // 用户关闭插屏广告
    this.interstitialAd.onClose(() => {
      // 在此执行场景切换逻辑(例如进入结算页)
      console.log('插屏广告已关闭,进入结算页')
    })
  }

  /**
   * 尝试展示插屏广告
   * @returns 是否成功触发展示
   */
  show(): boolean {
    if (!this.interstitialAd) return false

    // 频次控制:两次展示至少间隔 60 秒
    const now = Date.now()
    if (now - this.lastShowTime < this.MIN_INTERVAL_MS) {
      console.log(
        `插屏广告频次限制,还需等待 ${Math.ceil((this.MIN_INTERVAL_MS - (now - this.lastShowTime)) / 1000)} 秒`
      )
      return false
    }

    this.interstitialAd
      .show()
      .then(() => {
        this.lastShowTime = now
      })
      .catch((err) => {
        console.error('插屏广告展示失败', err)
      })

    return true
  }

  /** 销毁插屏广告 */
  destroy(): void {
    if (this.interstitialAd) {
      this.interstitialAd.destroy()
      this.interstitialAd = null
    }
  }
}

插屏广告展示时机

插屏广告最佳实践
  1. 频次优先:每天展示上限建议 3-5 次,超过后当天不再请求。
  2. 失败静默降级:插屏广告有一定展示失败率(网络、填充率等因素),失败时直接跳过,不影响游戏流程。
  3. 预创建实例:在游戏启动时创建,不要每次展示前创建(减少初始化延迟)。
  4. AB 测试频次:通过实验确定最佳展示频次,平衡收入与留存。

商业化检查清单

功能实现验证方式
激励视频ReviveAdManager沙箱环境测试观看 → 复活 → 未看完 → 游戏结束
皮肤内购SkinShop沙箱环境测试支付 → 服务端验证 → 解锁皮肤
Banner 广告BannerAdManager主菜单、结算页展示;游戏中隐藏
插屏广告InterstitialAdManager场景切换自然断点展示,间隔 ≥ 60 秒
数据埋点Analytics控制台查看上报日志,服务端确认接收
A/B 测试ABTest使用稳定 openId 测试不同分组策略
iOS 适配平台判断iOS 关闭支付入口、仅展示激励视频

商业化最佳实践

小游戏商业化核心原则

1. 广告频次控制

广告是双刃剑——展示太少收入低,展示太多用户流失。核心策略:

  • 激励视频:死亡复活、免费金币、双倍奖励等用户主动触发的场景,eCPM 最高,对体验影响最小
  • 插屏广告:仅在「关卡结束→下一关」等自然断点展示,间隔 ≥ 60 秒
  • Banner 广告:底部常驻,务必避开操作区域,不同机型需动态适配安全区

黄金法则:首日用户前 10 分钟不展示任何非激励广告,先让用户爱上游戏。

2. eCPM 优化技巧

优化方向具体做法预期提升
广告场景设计选择用户"有强烈动机"的时刻(复活、翻倍)填充率 +15-30%
广告位质量单个广告位深度打磨优于铺量eCPM +20-50%
用户分层付费用户关闭广告,免费用户适当增加ARPU 最大化
素材刷新定期更换广告位 UI(避免"广告疲劳")点击率 +5-10%

3. 支付漏斗分析

用户进入皮肤商店 (100%)
  → 点击查看皮肤 (60%)
    → 点击购买按钮 (20%)
      → 调起支付 (15%)
        → 支付成功 (12%)

优化重点

  • 皮肤预览:在付费前展示完整效果(旋转、动画),提升「点击购买」转化
  • 首充优惠:首个皮肤 1 元 / 限时折扣,降低首次付费心理门槛
  • 支付失败重试:网络波动时自动重试,避免用户流失

4. 收入数据看板

上线后需持续关注的核心指标:

指标计算方式健康基准
ARPU总收入 / 活跃用户数休闲游戏 0.1-0.5 元
ARPPU总收入 / 付费用户数休闲游戏 5-20 元
付费率付费用户 / 活跃用户2-5%(休闲)、5-15%(中重度)
广告渗透率看广告用户 / 活跃用户30-60%
次日留存D2 活跃 / D1 新增≥ 30%
7 日留存D7 活跃 / D1 新增≥ 15%

本章小结

本章在 Cocos Creator Flappy Bird 的基础上,完整集成了微信小游戏的核心商业化能力:

  1. 激励视频广告 — 复活机制是小游戏最高效的广告场景,实现了完整的加载/展示/降级策略
  2. 米大师内购 — 皮肤商店从购买到服务端验证的完整支付链路
  3. 数据埋点 — 批量上报 + 关键事件即时发送的分析系统
  4. A/B 测试 — 基于 openId 哈希的稳定分组策略,用于验证广告策略效果

下一步方向

  • 接入更多广告类型(插屏、Banner、原生模板)
  • 引入游戏内经济系统(金币、体力、道具)
  • 搭建运营后台(活动配置、推送通知、用户画像)

📝 课后练习

练习 1:设计完整的复活广告流程(含降级策略)

题目: 设计并实现一个健壮的复活广告系统,要求:

  1. 用户死亡后弹出「观看广告复活」按钮(3 秒倒计时)
  2. 点击后调用激励视频广告
  3. 广告加载失败时,提供降级方案(直接复活 + 提示"今日免费复活次数:X")
  4. 用户看完广告后,给予 2 秒的无敌时间(避免刚复活就撞到管道)
  5. 单局最多复活 2 次,超过后直接进入结算

参考答案: 参考本章 ReviveAdManagerGameController 集成部分,增加倒计时 UI 和免费复活次数限制即可。

练习 2:实现服务端订单校验

题目: 为什么米大师支付成功后不能立即在客户端发放道具?请设计一个服务端校验流程。

参考答案:

  1. 前端支付成功后,获得订单信息
  2. 前端将 code 发送给服务端换取 openId
  3. 服务端调用米大师查询订单接口确认支付状态
  4. 服务端将皮肤归属写入数据库
  5. 服务端返回解锁成功,前端更新本地状态

关键原因:客户端数据不可信,必须依赖服务端权威验证,防止伪造支付成功。


📚 相关阅读

用心学习,持续实践