主题切换
Flappy Bird — 商业化集成
预计阅读 45 分钟
版本信息
最后更新:2026-07-09 · 适用基础库:≥ 3.9.0(最低兼容 ≥ 2.30.0) · 微信开发者工具:稳定版 1.06+
本章在 Cocos Creator 版本的基础上,集成微信小游戏的核心商业化能力:激励视频广告、皮肤内购、数据埋点、A/B 测试。你将得到一个从玩法到变现链路完整的 Flappy Bird 项目。
商业化整体流程
用户死亡
├─ 展示复活广告选项 ──→ 观看完成 → 复活继续游戏
│ └ 未看完 → 结算面板
│
└─ 进入结算面板
├─ 皮肤商店入口 ──→ 选择皮肤 → 米大师支付 → 服务端验证 → 解锁
└─ 再来一局合规前提
- 个人开发者无法开通微信小游戏虚拟支付。内购功能需要企业主体 + 游戏版号。
- iOS 端小游戏虚拟支付长期受限;截至 2026-07,具体方案以微信官方最新公告为准。在正式上线前,iOS 平台通常需要关闭支付入口或做功能开关。
- 所有广告展示必须避免诱导点击,激励视频必须由用户主动触发。
激励视频广告(复活机制)
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
// })
}
}支付安全:必须服务端验证订单
客户端的 success 回调不可信——用户可能通过抓包、修改返回值等手段伪造支付成功。生产环境必须:
- 前端将
payResult(含orderId)和登录code发送给服务端。 - 服务端用
code换取openId。 - 服务端调用米大师「查询订单」接口,校验订单状态、金额、签名。
- 服务端将皮肤归属写入数据库后,再通知前端解锁。
只有在服务端确认订单真实成功后,才允许修改玩家资产。
数据埋点
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 广告
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 | 原因 |
|---|---|---|
| 主菜单 | ✅ 展示 | 非核心区域,不影响体验 |
| 游戏进行中 | ❌ 隐藏 | 避免遮挡游戏内容,导致误触和投诉 |
| 结算页 | ✅ 展示 | 用户停留时间较长,展示价值高 |
| 商店/设置 | ✅ 展示 | 非游戏核心内容 |
| 激励视频播放时 | ❌ 隐藏 | 避免广告堆叠 |
Banner 广告最佳实践
- 游戏时不展示:Banner 广告显示在 Canvas 上层,会遮挡游戏画面,玩家容易误触——这是最常见的负面评价来源。
- 自动刷新不低于 30 秒:刷新太快收益不会显著提升,反而增加加载失败率和用户反感。
- 适配安全区域:对于全面屏手机,Banner 底部需留出 Home Indicator 的高度(约 34px),避免被系统手势拦截。
- 做好降级:
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
}
}
}插屏广告展示时机
展示时机限制
关键规则:
- ❌ 禁止在游戏过程中突然弹出(导致差评和用户流失)
- ❌ 禁止在激励视频播放后立即展示插屏(广告疲劳)
- ✅ 推荐在「结算弹窗弹出之前」展示(用户心理预期是"要停一下了")
- ✅ 推荐在「返回主菜单」之后展示(场景切换自然断点)
- ✅ 每次展示间隔 ≥ 60 秒(微信 SDK 限制,更频繁的请求会被静默拒绝)
typescript
// 在 GameManager 中使用插屏广告
// 玩家死亡 → 延迟 1.5 秒 → 尝试展示插屏 → 关闭后弹出结算
onPlayerDeath(): void {
setTimeout(() => {
const shown = InterstitialAdManager.getInstance().show()
if (shown) {
// 插屏关闭后(onClose 回调中)展示结算弹窗
} else {
// 频次限制或加载失败,直接展示结算弹窗
this.showResultPanel()
}
}, 1500)
}插屏广告最佳实践
- 频次优先:每天展示上限建议 3-5 次,超过后当天不再请求。
- 失败静默降级:插屏广告有一定展示失败率(网络、填充率等因素),失败时直接跳过,不影响游戏流程。
- 预创建实例:在游戏启动时创建,不要每次展示前创建(减少初始化延迟)。
- 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 的基础上,完整集成了微信小游戏的核心商业化能力:
- 激励视频广告 — 复活机制是小游戏最高效的广告场景,实现了完整的加载/展示/降级策略
- 米大师内购 — 皮肤商店从购买到服务端验证的完整支付链路
- 数据埋点 — 批量上报 + 关键事件即时发送的分析系统
- A/B 测试 — 基于 openId 哈希的稳定分组策略,用于验证广告策略效果
下一步方向:
- 接入更多广告类型(插屏、Banner、原生模板)
- 引入游戏内经济系统(金币、体力、道具)
- 搭建运营后台(活动配置、推送通知、用户画像)
📝 课后练习
练习 1:设计完整的复活广告流程(含降级策略)
题目: 设计并实现一个健壮的复活广告系统,要求:
- 用户死亡后弹出「观看广告复活」按钮(3 秒倒计时)
- 点击后调用激励视频广告
- 广告加载失败时,提供降级方案(直接复活 + 提示"今日免费复活次数:X")
- 用户看完广告后,给予 2 秒的无敌时间(避免刚复活就撞到管道)
- 单局最多复活 2 次,超过后直接进入结算
参考答案: 参考本章 ReviveAdManager 与 GameController 集成部分,增加倒计时 UI 和免费复活次数限制即可。
练习 2:实现服务端订单校验
题目: 为什么米大师支付成功后不能立即在客户端发放道具?请设计一个服务端校验流程。
参考答案:
- 前端支付成功后,获得订单信息
- 前端将
code发送给服务端换取openId - 服务端调用米大师查询订单接口确认支付状态
- 服务端将皮肤归属写入数据库
- 服务端返回解锁成功,前端更新本地状态
关键原因:客户端数据不可信,必须依赖服务端权威验证,防止伪造支付成功。
📚 相关阅读
- 10 运营与变现 — 变现模式矩阵与数据指标详解
- 07 微信 API 与开放能力 — 广告、支付、云开发 API 详解
- 09 发布、审核与合规 — 广告合规要求与审核注意事项
- 异常监控与日志 — 商业化数据的异常监控