
1. 项目背景与整体思路拆解1.1 一款水果题材休闲小游戏为什么绕不开广告“水果扛狼”这个项目是我去年下半年接手的一个 Unity 小游戏改造活玩法规格不大玩家在有限步数内合成水果、抗住一波波涌上来的“狼”节奏快、单局三到五分钟、没有深度付费设计。这类产品在 Unity 微信小游戏这个生态里非常典型开发成本不高但想靠内购回本基本没戏收入结构里 90% 以上来自广告变现。所以“水果扛狼 接入广告”这件事不是上线后的加分项而是立项时就要规划进主循环的核心系统。我见过太多团队把广告当成最后一个星期的补丁活结果卡在广告位审核、奖励发放时序、频次控制这几个环节上硬生生推迟上线两周。先把这个项目的坐标说清楚技术栈是 Unity 2021.3 LTS用官方小游戏转换工具导出目标平台微信小游戏变现模式为纯 IAA广告变现核心广告位是激励视频辅助位是插屏和 Banner日活预估在三千到一万之间波动。读者如果手上有类似体量的合成类、跑酷类、塔防类小游戏这篇内容里的绝大部分细节可以直接抄。即便你做的是中度产品只要涉及“看广告领奖励”这条链路第三节和第四节的实现思路同样适用。1.2 变现路径的取舍为什么最终只做 IAA立项时讨论过三条路纯 IAA、纯 IAP、混合。纯 IAP 对“水果扛狼”这种轻玩法来说转化率太低玩家没有必须付费的动机点混合模式看起来美好但一旦上了内购就需要设计货币体系、首充礼包、限时折扣美术和策划的排期直接翻倍对一个五人的小团队来说不现实。纯 IAA 的好处是逻辑干净所有商业化都围绕“看广告换资源”展开玩家的抵触感低开发侧只需要把广告 SDK 的调用链路做稳。这里有个容易被忽略的判断依据——留存曲线。这个项目次留能到 35% 左右七留 12%属于典型的中轻度休闲曲线。这个数据意味着单个用户的生命周期观看次数是可预测的用广告收入模型反推就能知道项目能不能活。我的算法是日广告收入 DAU × 人均激励视频观看次数 × 广告填充率 × eCPM ÷ 1000按 DAU 3000、人均观看 2.4 次、填充率 92%、激励视频 eCPM 45 元测算日收入约 298 元月流水九千上下。这个数字谈不上多好看但它验证了项目“能自负盈亏”的底线。如果人均观看次数能通过设计拉到 4 次以上收入直接翻七成——这也是后面第五节要重点讲的频次设计为什么比技术接入更值钱。1.3 广告接入的完整链路拆解很多新手以为“接入广告”就是调一个 API实际上这条链路上有五个环节任何一环掉链子玩家都拿不到奖励。我把它拆成这样环节主要工作常见卡点广告位申请在小游戏后台创建广告位拿到 adUnitId广告位类型选错导致无法展示Unity 侧调用层C# 接口封装、Editor 环境兜底编辑器里直接报错崩溃JS 桥接层jslib 里注册函数、缓存广告实例每次 new 一个实例导致拉取失败展示与回调onLoad / onError / onClose 三条回调时序奖励在 onClose 之前就发了数据与风控埋点、幂等、防刷奖励重复发放这张表建议打印出来贴在显示器边上尤其是第四行“回调时序”我在三个项目上见过因为奖励发早了导致玩家看到一半退出也能白嫖资源的线上事故。2. 广告类型选型与广告位规划2.1 激励视频这个项目的主战场激励视频是“水果扛狼”唯一的核心变现场景因为它天然和玩法循环对齐复活一次、双倍金币、免费抽一次道具、跳过一关。这四个场景我做了取舍最终上了三个——复活、双倍结算、免费道具跳过关卡砍掉了因为跳过会破坏关卡节奏玩家第二天就不来了。激励视频有几个必须刻在脑子里的参数。一是最小可播放时长常见规格在 15 到 30 秒之间具体以后台配置为准二是加载策略广告实例创建后需要主动 load加载成功才能 show冷启动阶段加载耗时会明显高于热态三是回调判定onClose 回调里会带回一个是否完整观看的标记只有完整看完才发奖励。最后这一条是新手最容易写错的地方我后面会给完整的判断代码。还有一个实操细节值得提前说广告实例不要每次展示都重新创建。正确的做法是模块初始化时创建一次然后复用这个实例同时监听 onLoad 和 onError 事件。原因很直白反复创建实例会触发平台侧的频控和资源竞争表现就是“第一次能播第二次拉不起来”这个坑我踩过排查了小半天。2.2 插屏与 Banner什么时候该开什么时候必须关插屏的 eCPM 明显低于激励视频大概在 15 到 25 元区间但胜在无需玩家主动触发。我给它定的规则是“关卡结算后、且连续游戏时长超过 120 秒、且本局没有触发过激励视频”三条同时满足才展示。为什么要加第三条因为玩家刚看完激励视频再弹一个插屏体验非常割裂留存掉的比赚的多。Banner 更微妙。这类游戏的界面是竖屏填满的Banner 放底部会挡住操作区放顶部会压到分数栏。我在“水果扛狼”里最终选择了不开 Banner只在设置页放了一个不影响操作的固定 Banner 位。理由是Banner 的 eCPM 只有个位数收益占比通常不到 5%但造成误触的概率非常高一旦被判定为诱导点击影响的是整个账号的广告能力。提示广告位类型一旦创建就不能更改申请之前先在后台文档里确认清楚尺寸和展示形式。我见过把 Banner 类型拿去申请插屏位结果一直提示“无合适广告”的情况。2.3 广告位规划表与频次控制参数最终定下来的广告位配置是这样的广告位名称类型触发时机频次限制预估 eCPM复活激励激励视频关卡失败弹窗点击单局最多 1 次45 元双倍奖励激励视频结算页点击翻倍单局最多 1 次48 元免费道具激励视频道具栏点击全局冷却 90 秒40 元结算插屏插屏第三关之后、间隔 2 分钟全局冷却 150 秒18 元设置页横幅Banner设置页常驻无4 元频次控制这块我用了三层结构全局冷却时间、单局计数、单日上限。前两个是给玩家体验兜底的第三个是给账号安全兜底的——单日广告展示次数异常偏高比如超过 60 次的时候平台侧容易判定为异常行为这时候该降频就降频别贪。3. Unity 侧改造与 JS 桥接实现3.1 导出配置与构建参数“水果扛狼”从 Unity 导出到微信小游戏用的是官方转换工具这一步有几个参数必须调整否则广告调用会以各种奇怪的方式失败。压缩方案我选了 Brotli纹理压缩选 ASTC包体控制在 4MB 主包加 6MB 分包以内。为什么要卡这么死主要是首包加载速度超过了会影响自然量转化。构建这里还有两个细节。第一引擎侧要把异常处理打开广告调用失败时不至于让整个游戏卡死第二导出的 game.json 里要确认有没有多余权限声明广告功能本身不需要额外权限声明多了反而延长审核。转换工具每次更新都可能改动默认配置所以我习惯每次导出后手动 diff 一遍 game.json这个习惯帮我避过两次上线事故。3.2 用 jslib 写桥接层把广告能力暴露给 C#Unity 和小游戏运行环境之间隔着 WebGL 和 JS 这一层标准做法是写一个 .jslib 文件在 Unity 构建时被合并进运行时代码从而让 C# 侧能用 DllImport 调用。我封装了一个统一入口对外只暴露“播放一个激励视频结果通过回调返回”这一个语义mergeInto(LibraryManager.library, { WX_AD_Init: function (adUnitIdPtr) { var adUnitId UTF8ToString(adUnitIdPtr); if (!GameAdManager.instances[adUnitId]) { var ad wx.createRewardedVideoAd({ adUnitId: adUnitId }); ad.onLoad(function () { GameAdManager.state[adUnitId] ready; }); ad.onError(function (err) { GameAdManager.state[adUnitId] error; GameAdManager.lastError[adUnitId] err err.errCode; }); GameAdManager.instances[adUnitId] ad; GameAdManager.state[adUnitId] loading; } return 1; }, WX_AD_ShowRewarded: function (adUnitIdPtr, cbPtr) { var adUnitId UTF8ToString(adUnitIdPtr); var ad GameAdManager.instances[adUnitId]; if (!ad) { return -1; } var finished false; var onCloseHandler function (res) { ad.offClose(onCloseHandler); var isEnded !!(res res.isEnded ! undefined ? res.isEnded : false); finished true; var result isEnded ? 1 : 0; dynCall(vii, cbPtr, [1, result]); }; ad.onClose(onCloseHandler); ad.show().catch(function () { ad.load() .then(function () { return ad.show(); }) .catch(function () { ad.offClose(onCloseHandler); finished true; dynCall(vii, cbPtr, [0, -3]); }); }); return 0; } });这段代码里有三个关键设计。第一实例缓存放在全局对象里规避了“重复创建导致拉取失败”的问题。第二show() 失败后会自动 load 再 show 一次这是官方文档推荐的兜底链路覆盖了冷启动没加载完的情况。第三onClose 里立刻 offClose防止同一个回调被多次触发导致奖励重复发放。注意不要用res.isEnded直接做真值判断某些低版本环境里这个字段可能是 undefined必须显式判断宁可判成未看完也不能白送。3.3 C# 调用层与回调分发桥接层之外需要一层 C# 封装职责是把 JS 的回调转成 C# 的事件同时处理编辑器环境的兜底。这里面有个必做的动作编辑器里根本没有 wx 对象直接调用会抛异常所以要用条件编译把 Editor 分支隔离掉返回一个模拟的“观看完成”结果方便策划在编辑器里调玩法。public static class AdBridge { public delegate void AdCallback(int success, int code); #if UNITY_WEBGL !UNITY_EDITOR [DllImport(__Internal)] private static extern int WX_AD_Init(string adUnitId); [DllImport(__Internal)] private static extern int WX_AD_ShowRewarded(string adUnitId, AdCallback cb); #endif private static readonly Dictionarystring, AdCallback Pending new(); public static void Init(string adUnitId) { #if UNITY_WEBGL !UNITY_EDITOR WX_AD_Init(adUnitId); #else Debug.Log($[AdBridge] Editor 模拟初始化 {adUnitId}); #endif } public static void ShowRewarded(string adUnitId, Actionbool, int onResult) { if (Pending.TryGetValue(adUnitId, out _)) { onResult(false, -99); // 上一发还没结束直接拒绝 return; } AdCallback cb (success, code) { Pending.Remove(adUnitId); onResult(success 1, code); }; Pending[adUnitId] cb; #if UNITY_WEBGL !UNITY_EDITOR WX_AD_ShowRewarded(adUnitId, cb); #else EditorSimulate(cb); #endif } private static void EditorSimulate(AdCallback cb) { // 编辑器里延时 1 秒模拟播放完成 var t new System.Threading.Timer(_ cb(1, 1), null, 1000, Timeout.Infinite); } }这里用了一个 Pending 字典做“单飞”控制同一个广告位在播放过程中不接受新的请求。这个看似不起眼的判断救过我一次——玩家在结算页疯狂连点“双倍奖励”如果没有这个锁回调会堆叠一次观看发两次奖励。另外注意回调委托要作为静态字段持有引用否则 WebGL 环境下可能被 GC 回收导致回调丢失这个坑非常隐蔽表现是“偶尔奖励发不出来”。4. 实操流程从零把激励视频跑通4.1 广告位创建与测试环境准备后台创建广告位的步骤不复杂但有两个地方必须核对。第一是广告位类型激励视频和插屏是两类完全不同的资源池选错了不会报错只会一直返回“无广告填充”。第二是关联应用广告位必须挂在当前小游戏下面新建项目时容易挂在历史项目上排查时会怀疑人生。创建完拿到 adUnitId 之后我会先在 JS 控制台里手动跑一遍验证// 在开发者工具控制台直接跑验证广告位是否可用 var testAd wx.createRewardedVideoAd({ adUnitId: adunit-xxxxxxxx }); testAd.onLoad(() console.log(loaded)); testAd.onError((e) console.log(error, e.errCode, e.errMsg)); testAd.onClose((res) console.log(close, res res.isEnded)); testAd.load().then(() testAd.show());真机调试这一环不能省。开发者工具里的广告行为和真机差异很大工具里经常能加载出来真机上却是空的原因是真机有额外的频控和用户画像判断。我的习惯是准备三台不同系统的手机每台至少跑 20 次完整链路统计失败率和失败码分布。4.2 加载、展示、重试的完整时序把链路串起来实际的执行顺序是这样的游戏启动 3 秒后异步初始化广告实例不要放在启动第一帧会抢资源进入关卡时如果缓存状态不是 ready触发一次预加载玩家点“复活”时先判断状态ready 直接 show否则展示 loading 并等待最多 3 秒。这里有个我调整过多次的参数等待上限。一开始我设了 8 秒结果玩家在加载圈里干等直接退出后来压到 3 秒超过就提示“广告还没准备好稍后再试”并给一次免费复活作为补偿。这个改动之后复活场景的转化率反而涨了因为玩家不会被卡住。状态触发动作超时处理loading显示轻量 loading不阻塞操作3 秒后降级ready直接 show无error静默重试一次 load失败后展示兜底文案playing禁用相关按钮无再看一段状态机的骨架逻辑不复杂但必须严谨public IEnumerator PlayRewarded(string adUnitId, Actionbool onDone) { ShowLoading(true); bool? result null; AdBridge.ShowRewarded(adUnitId, (ok, code) { result ok; if (!ok) Debug.Log($[Ad] failed code{code}); }); float wait 0f; while (result null wait 3.0f) { wait Time.unscaledDeltaTime; yield return null; } ShowLoading(false); if (result null) { Toast(广告还在准备先送你一次机会); onDone(true); // 降级给奖励 yield break; } onDone(result.Value); }注意这里用的是Time.unscaledDeltaTime因为有些界面会做时间缩放用缩放时间会导致等待被拉长。4.3 奖励发放的幂等与防作弊设计奖励发放必须做到幂等同一个广告订单只能兑一次。我的做法是每次请求生成一个自增的流水号随奖励一起提交服务端或本地存档校验流水号是否已消费。本地校验虽然能被改但小游戏的数据都在客户端改起来本来就容易所以关键资源还是要走一次云端校验尤其是高价值道具。还有一层是时序防护。有些实现是在玩家点击按钮的瞬间就发奖励然后异步等广告回调——这是绝对不能做的会导致“看一半退出也拿奖励”。正确顺序永远是广告回调成功 → 校验完整观看标记 → 自查流水号 → 发奖励 → 刷新 UI。这个顺序我用注释写死在代码里了防止后来接手的人改乱。提示如果是复活场景还要处理一个边界——玩家在广告播放期间切到后台。切回来之后 onClose 不一定触发需要在 onShow 生命周期里补一次状态检查超时未回调就直接按失败处理把界面恢复到可操作状态否则玩家会卡在“广告中”的遮罩里。4.4 埋点与关键指标观测广告接入不做埋点等于蒙眼开车。我至少会上报这五类事件广告请求、加载成功、加载失败带错误码、展示、完整观看。有这五个点就能算出两个核心指标填充率加载成功 ÷ 请求和完播率完整观看 ÷ 展示。上线第一周的数据里我发现“免费道具”这个位置的完播率只有 61%明显低于另外两个位置的 88%。排查下来发现这个按钮放在战斗中玩家点了之后广告弹出来但战斗还在后台推进玩家着急就退出了。改成点击后暂停战斗再展示完播率回到 85%。这个调整不需要动任何技术实现纯靠埋点数据驱动但收入提升接近 20%。5. 常见问题与排查实录5.1 广告拉不起来的分层排查表“没广告”是最常见的反馈但原因可能完全不同。我整理了一张按错误码分层的表现象 / 错误码可能原因处理方式一直 loading 无回调实例未创建或加载未触发检查初始化时机加主动 load返回 1000 类错误广告位配置或状态异常后台核对广告位状态与应用关联返回 1004 类错误无合适广告属正常现象走降级逻辑首次成功后续失败重复创建实例改用缓存复用真机空白、工具正常真机频控或网络环境换机换号复测我的排查顺序固定是先看错误码再看是不是真机独有然后看是不是首次可用最后才怀疑代码。按这个顺序走八成的广告问题在十分钟内能定位。5.2 审核与合规上的实际经验这一块比技术更容易翻车。核心原则只有一条广告必须是玩家主动触发的不能诱导、不能伪装成游戏按钮、不能在误触区域附近布置广告入口。我在“水果扛狼”里把三个激励入口的按钮样式统一改成带“视频”图标的二次确认弹窗点击后还会再问一次“观看完整视频可获得奖励”看起来啰嗦但误触投诉率降到了零。另一个细节是广告过程中的加载提示。不能长时间显示一个没有反馈的转圈需要有文案说明正在准备超过时限要有兜底选项。这类体验问题在审核反馈里经常以“影响用户体验”的形式出现。5.3 性能与内存上的坑广告展示时会额外占用内存低端机上容易出现展示完返回后帧率下降。我做的处理是在广告关闭回调里主动触发一次资源清理并把返回后的界面切到一个轻量场景再切回来实测能缓解大部分卡顿。还有一个小坑是音频。广告播放时游戏音频不会自动静音两个声音叠在一起体验极差。处理方式是在 show 之前把所有 AudioSource 音量降到 0回调里恢复同时监听前后台切换事件补一次静音保险。6. 提升收益的几个实操心得6.1 频次和体验之间的那条线频次控制是收益和留存的拔河。我的经验值是激励视频人均每天 2 到 4 次是最舒服的区间低于 2 次说明入口设计不够自然高于 6 次则大概率有玩家在被迫“刷广告换资源”这种用户留存很差反而拉低整体 ARPU。判断依据可以看第七日留存和人均观看次数的相关性如果某批用户观看次数高但留存低就该收一收奖励力度了。插屏的频次我压得更紧间隔不低于 150 秒且单日不超过 12 次。这个数字是试出来的缩短到 90 秒时次日留存掉了接近 3 个百分点明显不划算。6.2 广告位要长在玩法循环里这是我个人的核心观点广告入口不该是“贴”上去的而应该是玩法本身缺的那一块。复活位之所以完播率高就是因为玩家此刻的动机最强。反过来如果强行在无关的地方加一个“看广告得金币”转化率通常只有 20% 出头。按这个思路我把双倍结算的位置从“结算页角落”挪到了“奖励弹窗的正中”并且把提示改成“再玩一局能拿更多”点击率涨了近一倍。改的就是位置和文案代码一行没动。6.3 小步迭代比一次性做完更重要广告系统的参数包括冷却时间、奖励额度、入口位置没有任何一组是放之四海皆准的。我建议的做法是先上一版最保守的配置跑一周数据然后每次只改一个变量。我在“水果扛狼”上前后调了六轮最终配置和初始版本相比只改了三处道具位加了暂停、插屏间隔从 90 秒调到 150 秒、双倍奖励从 1.5 倍提到 2 倍收入提升了约 35%留存没有下降。最后分享一个我一直在用的小技巧每次调整前把当前配置抄一份存成 JSON 放到项目文档里写清楚日期和当时的核心数据。这个习惯看着笨但当你调到第五轮想回溯“两周前那版为什么效果好”的时候它就是唯一的救命稻草比任何记忆都可靠。