1. 做完《水果扛狼》才明白:广告不是加个按钮,是把变现逻辑嵌进玩法
《水果扛狼》是一个很典型的 Unity 休闲闯关小游戏:水果角色站桩防守,一波一波狼群从屏幕边缘冲过来,玩家靠拖拽合成、升级炮台把它们拦在门外。整个 Demo 从美术到关卡配置拢共花了不到三周,主体玩法跑通之后,最后一块拼图就是微信小游戏接入广告。当时我的判断非常朴素——游戏本体都做完了,接广告无非就是在复活按钮上加几行调用代码,半天搞定。
现实是,这半天变成了两周。前三天卡在环境,中间一周卡在广告拉不起来和“体验版明明有广告、线上却没有”,最后几天在调广告频率和埋点。真正让我改变认知的,不是那些报错,而是一个数据:把激励视频从“复活才给”改成“复活 / 双倍金币 / 解锁新水果”三个入口之后,单用户日均广告展示次数翻了接近两倍,而次日留存几乎没掉。这说明广告位置本身不是损耗,位置设计得蠢才是损耗。
这篇就把《水果扛狼》从 Unity 工程到微信小游戏广告上线的整条路子摊开讲,包括Unity 侧用什么方案转小游戏、四种广告位怎么选、激励视频的完整代码骨架、插屏和 Banner 的层级与频率坑,以及上线之后怎么用埋点把广告收益拆成可以动手调的数字。适合两类人看:一类是手里已经有能跑通的 Unity 休闲游戏、准备上小游戏平台试试水;另一类是广告已经接上了,但收益一直上不去、想搞清楚问题出在哪一环。
说明:本文涉及的广告位 ID、SDK 回调签名会随平台版本变化,代码以结构示意为主,具体字段请对照当前版本的官方示例;文中所有收益数字均为个人实测的经验区间,不构成任何承诺。
1.1 立项时就要想清楚的三件事
大部分做小游戏的人会把“接广告”当成发布前的一个收尾动作,这个顺序其实是反的。广告能不能挣钱,在你设计关卡节奏和数值曲线的时候就已经决定了一大半。我在《水果扛狼》上复盘出三件必须在立项阶段想清楚的事。
第一件是单局时长。激励视频的黄金插入点是“玩家想要某个东西但暂时得不到”的瞬间,而单局时长决定了这种瞬间出现的频率。单局 30 秒的快节奏游戏,复活机会太多,玩家反而不珍惜,广告频次上去了但留存掉得厉害;单局 8 分钟以上的重度游戏,玩家对打断极其敏感,插屏基本不敢用。《水果扛狼》最后定的单局 3 到 5 分钟,一关打四五波狼,这个节奏下“差一点点就过关”的挫败感出现频率刚好,复活按钮的点击率也就有了保障。
第二件是资源稀缺度。游戏里如果金币随便刷、道具随便给,那“看广告换奖励”就没人点。反过来如果卡得太死,玩家直接卸载。我的做法是把金币产出压到刚好够升级一次炮台,第二级开始明显吃力,这时候“看 15 秒广告拿 200 金币”就是一个自然的出口,而不是硬塞。
第三件是广告位与玩法的绑定关系。这一条最容易被忽视。同样是激励视频,入口放在设置菜单里和放在结算界面的“再来一次”旁边,点击率能差三到五倍。《水果扛狼》里我把所有激励视频入口都做成了带图标和明确收益文案的按钮,而不是藏在某个二级菜单里,玩家看到就知道“点这个能拿到什么”。
1.2 IAA 模式下这款游戏必须具备的三个“广告钩子”
纯广告变现(IAA)的小游戏,本质上是在卖用户的注意力。你必须有足够多“用户愿意用注意力换东西”的场景,否则广告位摆在那儿也没人点。《水果扛狼》最后沉淀下来三个钩子,我觉得可以直接套到同类闯关游戏上。
复活钩子是最硬的。狼群突破防线、血量清零的瞬间,弹出“看广告原地复活,保留当前进度”,这个位置的转化率通常是最高的,因为损失厌恶在起作用——玩家已经投入了三分钟,不想白费。这里有个细节:复活面板上要给一个“放弃”按钮,而且不能做得太隐蔽,否则会被判定为诱导,也真的会伤体验。
翻倍钩子放在结算界面。过关之后给基础金币,然后一个“观看广告,本关金币翻倍”。这个位置的点击率取决于基础金币给得够不够多——如果基础金币只有 50,翻倍也才 100,玩家懒得点。我的做法是基础金币给足,翻倍给的是“额外一份”,心理感受是白捡的。
解锁钩子用在内容节点上。新水果角色、新皮肤、限时道具,这些原本要花金币或者干脆要内购的东西,给一条“看广告直接解锁”的路。这个钩子对长线留存的作用最大,因为它给了非付费玩家一个持续回访的理由。
这三个钩子之外,我还试过一个“看广告跳过冷却”,效果一般,因为《水果扛狼》本身没有强冷却机制,硬加一个反而显得别扭。这就说明钩子必须长在原有玩法上,硬造出来的位置数据一定难看。
1.3 流量主门槛与后台准备清单
在动手写代码之前,有一堆后台配置必须先做完,否则你代码写得再对,广告也拉不起来。这部分是最枯燥但最不能跳的。
第一,小游戏必须先发布上线并且累计独立访客达到平台要求,才能开通流量主。这一步没有捷径,只能先上线一个没有广告或者广告很少的版本,靠内容和分享把量做起来。我当时是先上线了纯免费版本,跑了两周多,独立访客过了线才去申请,审核大概一两个工作日。
第二,开通流量主之后,在后台的广告管理里创建广告位。注意广告位是按类型分别创建的:激励视频一个、插屏一个、Banner 一个,每个都会给你一个独立的adUnitId。这个 ID 一定要和代码里的常量对上,我见过不止一次因为复制串行导致整块广告不工作的情况。
第三,确认小游戏的类目和内容符合广告投放要求。涉及抽奖、诱导分享、内容擦边的小游戏,广告位可能会被限制。这一点在立项选题的时候就要避开。
第四,把appId、adUnitId这些配置从代码里抽出来,做成可切换的配置。原因很实际:测试环境和正式环境的广告位 ID 不一样,混着用会导致你测试时看到的填充率数据完全失真。我在《水果扛狼》里建了一个AdConfig静态类,按环境宏区分,省了很多事。
2. 四种广告位在《水果扛狼》里各干什么活,我用一张表把取舍说清楚
微信小游戏提供的广告形态主要有激励视频、插屏、Banner、格子(以及后来的原生模板类)。新手最容易犯的错是“能接的都接上”,结果游戏变成一个广告播放器,留存崩盘之后再回头砍,前面的用户已经流失了。我在《水果扛狼》上把这四种广告位分别跑了两周,下面说结论。
2.1 激励视频:唯一值得优先做好的广告位
如果精力有限只能做一种,那就做激励视频,没有第二个选项。原因有三层。
第一层是收益结构。激励视频的 eCPM 在几种广告形态里通常是最高的,而且用户是主动观看、完播率高,广告主愿意出更高的价格。插屏和 Banner 属于被动曝光,单价低得多。
第二层是体验损耗最小。用户是自己点的,心理预期就是“我要看一段广告换东西”,不会觉得被打断。这一点决定了激励视频可以做得很频繁而不伤留存,前提是奖励给到位。
第三层是可控性最强。激励视频的展示时机完全由你在代码里控制,你可以选择在玩家最想要东西的那一刻弹出来,而插屏和 Banner 更多的是“打扰”。
在《水果扛狼》里,激励视频承担了绝大部分广告收入,三个钩子(复活、翻倍、解锁)全部走激励视频。实现上要注意的点后面第 4 节会展开,这里先说一个设计层面的经验:奖励文案要写具体数字。“观看视频获得奖励”这种话点击率很低,“观看视频获得 200 金币”或者“观看视频立即复活”效果明显更好,因为玩家能立刻算出这笔交易划不划算。
2.2 插屏:收益第二,但对留存伤害最大
插屏广告是关卡切换、结算之后自动弹出的全屏广告。它的收益贡献排第二,但风险也排第一,因为它不由用户主动触发。
我用《水果扛狼》做过一轮对照:A 组每关结算后都弹插屏,B 组每三关弹一次且两次之间至少间隔一分钟。结果是 A 组的广告收入大概高出三四成,但次日留存掉了将近 8 个百分点,七日留存掉得更多。这个代价明显不划算,最后采用的是 B 组策略。
插屏的几个实操要点:一是必须有频率闸门,不能每次结算都弹;二是不能在关键操作中间弹,比如玩家正在拖拽合成的时候弹一个全屏,体验会非常糟糕;三是弹出时机要避开激励视频,两个广告连着播,用户会直接退出。我的做法是结算界面先让玩家看到战利品,等他点“下一关”的时候再判断要不要出插屏,这样广告是跟着一次主动操作走的,感受上没那么突兀。
2.3 Banner 与格子广告:在横屏闯关游戏里为什么慎用
Banner 是常驻在屏幕边缘的横幅广告,格子广告是网格状排列的一组小广告。这两种在国内小游戏里用得很多,但在《水果扛狼》这个项目上我都放弃了。
Banner 的问题在于空间侵占。这款游戏是竖屏,狼群从四周冲进来,屏幕边缘全是有效操作区,Banner 一挂上去直接遮住了左下角的技能按钮,测试的时候好几个玩家反馈“按钮点不到”。Banner 是按展示时长和点击计费的,收益本来就低,为了这点收益牺牲操作区,完全划不来。如果一定要用,建议只在主菜单、商店这类静态界面挂,进入战斗立刻隐藏。
格子广告的收益相对高一些,但它的形态在闯关游戏里很难找到自然的位置,硬塞进去很像页游。我最后只在“每日签到”这类低频页面试了一下,数据一般,就撤了。
2.4 广告位参数对照表
下面这张表是我在项目里实际用的配置,可以直接当模板改:
| 广告类型 | 使用场景 | 触发方式 | 频率控制 | 主要风险 |
|---|---|---|---|---|
| 激励视频 | 复活、金币翻倍、解锁角色 | 用户点击按钮 | 不限次数,但同一入口 3 秒内防连点 | 奖励未到账导致投诉 |
| 插屏 | 关卡结算后进入下一关 | 自动,挂在“下一关”按钮上 | 每 3 关一次,间隔 ≥ 60 秒 | 频次过高伤留存 |
| Banner | 主菜单、商店 | 页面进入时创建,离开时销毁 | 仅非战斗界面 | 遮挡操作区、层级问题 |
| 格子广告 | 签到页、活动页 | 页面进入时创建 | 低频页面 | 破坏美术风格 |
这张表背后其实就一条原则:主动触发的广告可以多做,被动曝光的广告必须克制。所有“用户为了某个明确目标而点”的广告,做多少都不算过分;所有“用户没得选只能看”的广告,多一次都是流失。
3. Unity 工程转小游戏:接 SDK 之前要先把这几件事理顺
《水果扛狼》是拿 Unity 做的,要跑在微信小游戏上,中间必须过一道转换。这块的坑主要不在广告本身,而在转换链路和包体,我把顺序理一下。
3.1 从 WebGL 到小游戏的转换链路
Unity 官方支持的导出目标里并没有“微信小游戏”这个选项,实际路径是:Unity 导出WebGL产物,再用平台提供的转换工具把 WebGL 产物转成小游戏的包结构。所以整个链路是三段:Unity WebGL 构建、转换工具处理、开发者工具/真机预览。
这里第一件要确认的事是Unity 版本。转换方案对 Unity 版本有明确的支持范围,版本太老或者太新都可能出问题。我当时用的是 LTS 版本,构建前把项目里用不到的包全部清掉,特别是那些只在编辑器里用的工具包,它们会拖慢构建也会增加包体。
第二件事是渲染与图形 API 的选择。WebGL 1.0 和 2.0 的兼容性不一样,部分机型对 2.0 支持不完整,表现为黑屏或者材质错乱。稳妥的做法是先按 WebGL 1.0 跑通,测试机型覆盖够了再考虑升级。
第三件事是音频。WebGL 的音频播放受浏览器策略限制,小游戏环境里也有类似约束,首次播放必须有用户交互触发。这个和广告有直接关系——激励视频播放时应该把游戏音量降下来或者静音,视频结束再恢复,否则会出现游戏音效和广告声音叠在一起的情况,非常难受。
3.2 导入 SDK 包后的目录结构与命名空间
转换方案和广告 SDK 通常是以一个 Unity Package 或者资源包的形式提供的,导入之后项目里会多出一个专门放小游戏相关代码的目录。C# 侧调用广告接口,走的是 SDK 提供的封装类,命名空间一般是WeChatWASM这一套。
导入之后有几件事要立刻检查:一是包里有没有自带的示例场景,跑一遍能快速确认 SDK 是否正常工作;二是构建配置里有没有自动注入的模板文件,缺了它广告组件在真机上根本不会渲染;三是.jslib桥接文件有没有被正确包含,这是 C# 和平台 JS 之间通信的通道,缺了就是“代码能编译但运行时报方法不存在”。
我遇到过一次很典型的情况:SDK 更新之后,旧的桥接文件没被覆盖,导致广告创建接口在真机上直接静默失败,日志里什么都没有。排查了大半天才发现是文件版本不一致。所以SDK 升级之后一定要清一次中间产物再重新构建,不要相信增量构建。
3.3 首包体积与广告 SDK 的取舍
小游戏对首包体积有严格限制,超出之后要么分包要么压缩资源。广告 SDK 本身会增加一部分体积,这对本来就在体积红线上挣扎的项目是个现实问题。
《水果扛狼》的处理方式是把资源做了几件事:图集重新打包,把零散的小图合进同一张图集,减少纹理切换也减少文件数量;音频统一转成压缩格式,BGM 用流式加载;把非首关必需的资源放进分包,首包只保留第一关和主菜单。这一套下来,首包体积压到了一个比较安全的水位,广告 SDK 加进来也没超。
这里有个经验:不要为了省体积把广告 SDK 做懒加载。听起来很合理,但实际上广告实例需要预加载才有库存,等你真正要展示的时候再去初始化,大概率是空的,用户点了按钮没反应,直接就走了。广告相关的初始化应该放在游戏启动后的第一个空闲时机,而不是等用到才做。
4. 激励视频的完整实现:实例、预加载、奖励收口的三层结构
这一节是整篇的核心。激励视频的代码本身不长,但写错的方式特别多,我把结构拆成三层讲:实例管理、预加载与展示控制、奖励收口。
4.1 为什么必须单例 + 长生命周期
广告实例的创建是有成本的,更重要的是,同一个广告位反复创建实例会导致回调丢失。这是我最开始踩的坑:每个界面各自new一个广告管理器,结果 A 界面创建的实例的回调被 B 界面的实例覆盖,广告播完了奖励发不出去。
正确的做法是做一个挂在不销毁节点上的单例管理器,全局只创建一次广告实例,所有界面通过这个单例去展示广告。代码骨架大概是这样:
using System; using UnityEngine; using WeChatWASM; public class RewardAdManager : MonoBehaviour { private static RewardAdManager _instance; public static RewardAdManager Instance { get { if (_instance == null) { var go = new GameObject("RewardAdManager"); _instance = go.AddComponent<RewardAdManager>(); DontDestroyOnLoad(go); _instance.Init(); } return _instance; } } private RewardedVideoAd _ad; private bool _isLoaded = false; private bool _isShowing = false; private Action<bool> _onResult; private void Init() { var param = new WXRewardedVideoAdParam { adUnitId = AdConfig.RewardAdUnitId }; _ad = WX.CreateRewardedVideoAd(param); _ad.OnLoad(res => { _isLoaded = true; }); _ad.OnError(err => { _isLoaded = false; _isShowing = false; Debug.LogError($"[Ad] reward ad error: {err.errCode} / {err.errMsg}"); CallbackOnce(false); }); _ad.OnClose(res => { _isShowing = false; _isLoaded = false; // isEnded 为 true 表示完整看完,false 表示中途关闭 CallbackOnce(res != null && res.isEnded); _ad.Load(); // 关闭后立刻预加载下一条 }); _ad.Load(); } private void CallbackOnce(bool success) { var cb = _onResult; _onResult = null; cb?.Invoke(success); } }这段代码里有三个关键点值得单独说。第一,DontDestroyOnLoad是必须的,否则切场景的时候管理器被销毁,正在播放的广告回调就丢了。第二,OnClose里立刻调用Load(),保证下一条广告提前准备好,这样用户第二次点击时不用等。第三,_onResult用一次性回调的方式管理,避免重复触发导致奖励发两次——这个问题在真机上偶发,非常难查。
4.2 预加载策略与 show 失败的兜底
Show()并不是任何时候都能成功。如果当前没有可用库存,Show()会直接失败,而且不同版本的 SDK 表现不一样:有的走OnError回调,有的走返回的 Promise 的 reject。如果你不做兜底,用户点了按钮就是没反应。
我的处理逻辑是这样的:调用Show()之前先判断_isLoaded,如果为 false,就先调用Load()并等待OnLoad回调再展示;如果等待超时(我设的是 5 秒),就提示“广告加载中,请稍后再试”,并且把奖励按失败处理,绝不能出现“没看广告却给了奖励”的情况,那会让整个经济系统失效。
public void Show(Action<bool> onResult) { if (_isShowing) return; // 防重复点击 _isShowing = true; _onResult = onResult; if (_isLoaded) { _ad.Show(); } else { _ad.Load(); StartCoroutine(WaitAndShow(5f)); } } private System.Collections.IEnumerator WaitAndShow(float timeout) { float t = 0f; while (!_isLoaded && t < timeout) { t += Time.unscaledDeltaTime; yield return null; } if (_isLoaded) { _ad.Show(); } else { _isShowing = false; CallbackOnce(false); } }还有一个细节:_isShowing这个标志位一定要在OnClose和OnError里都复位。我有一次只在一个分支里复位,结果某次广告加载失败之后,这个标志位永久卡在 true,之后所有点击都没反应,表现就是“广告功能整个废掉”,但日志里一点报错都没有。
4.3 isEnded 判断与奖励发放的原子性
激励视频的核心是“看完才给奖励”,这个判断全靠OnClose回调里的isEnded字段。这里有两个必须守住的原则。
第一个原则是只有isEnded === true才发奖励。用户中途关闭视频,不管看了多少秒,都不能给。如果为了体验宽容一下,很快就会出现“点开就关也能拿奖励”的刷子,整个钩子的价值就没了。
第二个原则是奖励发放必须是原子的、幂等的。原子性指的是从“确认看完”到“写入金币”这中间不能被打断,不能有 yield,不能有异步请求;幂等指的是同一次观看只能发一次奖励,即使回调因为某些原因被触发两次。我的做法是在OnClose里先把回调取出置空,再执行发奖,天然就做到了幂等。
// 业务侧调用示例 public void OnClickRevive() { RewardAdManager.Instance.Show(success => { if (success) { GameManager.Instance.RevivePlayer(); Analytics.Track("ad_reward_grant", "revive"); } else { UIManager.ShowToast("未完整观看视频,无法获得奖励"); } }); }注意success为 false 时的提示文案,要说清楚是因为“没看完”,而不是“广告出错”,这样用户知道再点一次还有机会。
4.4 一套可以直接抄的代码骨架
把上面几块合起来,业务侧的使用应该简单到只有一行。界面层完全不关心广告实例怎么创建、什么时候预加载、失败了怎么兜底,只关心“我要展示一个激励视频,告诉我结果”。
这个分层带来的好处在后期维护时特别明显。平台 SDK 升级、广告位 ID 更换、埋点字段调整,全部在管理器里改,业务代码一行不动。我在项目中期把广告 SDK 从旧版换到新版,因为做了这层封装,改动只花了一个小时。
另外建议在管理器里加一个开关:调试阶段可以强制返回成功,方便测试奖励逻辑,不用每次都真的看完 15 秒广告。
#if UNITY_EDITOR private const bool ForceSuccessInEditor = true; #endif这段一定要包在编辑器宏里,千万别带到正式包里。
5. 插屏与 Banner 的落地细节:层级、频率和真机上的那些意外
激励视频做扎实之后,插屏和 Banner 属于锦上添花,但它们的坑一点不少,主要集中在一个地方:原生组件层级。
5.1 插屏的调用时机与频率闸门
插屏的接口比激励视频简单,创建之后直接Show()就行,但频率控制必须自己做,因为平台的限制相对宽松,不控制就是你自己在劝退用户。
我用的频率闸门逻辑是三条同时生效:一是关卡计数,每通关三关才允许出一次;二是时间间隔,上一次插屏距离现在至少 60 秒;三是当日次数上限,单用户单日最多 12 次。这三条任意一条不满足就不弹。
public bool CanShowInterstitial() { if (Time.realtimeSinceStartup - _lastInterstitialTime < 60f) return false; if (_levelSinceLastInterstitial < 3) return false; if (_todayInterstitialCount >= 12) return false; return true; }这个逻辑看起来啰嗦,但每一条都有它的道理。只拉时间间隔不拉关卡数,会出现“玩家在同一关卡反复失败、反复被弹广告”的情况,这是最伤人的;只拉关卡数不拉时间间隔,会在快速通关时连续弹;当日上限是防止极端情况下的失控。
5.2 Banner 的原生组件层级与安全区适配
Banner 在小游戏里是原生组件,它不是渲染在你的游戏画面里的,而是叠在游戏画布上的一层。这意味着 Z 轴顺序、点击穿透、显示隐藏这些都跟 Unity 里的 UI 完全不是一套逻辑。
最典型的问题是遮挡。Banner 的位置用屏幕坐标指定,但不同机型的屏幕比例差别很大,有的机型上下有异形屏避让区,你算出来的底部位置可能正好压在虚拟按键区上。我的处理方式是先调用OnResize拿到广告实际渲染的尺寸,再根据实际尺寸重新计算位置,让它居中贴底,同时预留出安全区。
bannerAd.OnResize(res => { // 按实际渲染尺寸重新定位,避免不同机型裁切不一致 int screenW = Screen.width; int screenH = Screen.height; bannerAd.style.left = (screenW - res.width) / 2; bannerAd.style.top = screenH - res.height; });第二个问题是显示与隐藏的时机。Banner 创建之后就一直在,进入战斗必须调用Hide(),离开战斗再Show()。忘了隐藏的话,战斗界面上会一直挂着一条横幅,非常出戏。我后来干脆把它封装成一个“Banner 区域管理器”,页面栈变化的时候统一开关,避免漏掉。
第三个问题是销毁。Banner 不销毁的话会一直占用资源,而且切页面之后可能残留。我的原则是:主菜单进入时创建,离开时销毁,不复用。创建销毁的开销对于菜单页来说完全可以接受。
6. 从“体验版没广告”到“审核被驳回”:我踩过的六个坑和完整排查链路
这一节讲的是实操中真正卡住我的问题。我把排查过程完整写出来,因为广告这块的坑大部分不是“报错告诉你哪里错了”,而是“什么都不报,就是不工作”,只能靠排除法。
6.1 开发者工具里永远没有广告
我最开始的认知是:在开发者工具里先把广告调通,再上真机。结果在工具里折腾了两个小时,广告一次都没出来过。
原因是广告组件在开发者工具里大多不渲染真实广告,工具环境的流量和广告库存是分开的,拿不到真实填充。所以第一件要接受的事就是:广告功能必须在真机上验证,工具只能用来验证代码逻辑有没有报错。
这个认知转变之后,我的调试流程改成了:工具里看 console 有没有异常、接口调用有没有发出;真机上扫码预览看广告实际能不能拉起来。两边配合,效率高很多。真机预览用的是预览二维码,这个模式下广告是真实拉取的,能看到实际的填充情况。
6.2 广告拉取失败的几类真实原因
真机上广告拉不起来,原因大概能分成四类,我按排查顺序列一下。
第一类是广告位 ID 错误或者未生效。新建的广告位有时候需要一段时间才生效,刚创建完立刻测试可能是空的。另外测试时用了正式环境的 ID 但小游戏还没过审,也会拉不到。
第二类是账户状态问题。流量主未开通、结算信息未完善、账户有异常,都会导致广告请求被拒。这类问题的表现是OnError里返回明确的错误码,但错误信息比较笼统,需要去后台核对账户状态。
第三类是请求频率超限。短时间内反复调用Load()会触发限流,表现为一段时间内一直失败,过一会儿又好了。这个特别容易被误判成“代码问题”,实际上是你自己刷太狠了。调试的时候要克制,别一秒点十次。
第四类是机型与系统环境。部分低端机型或者系统版本较老的设备,广告组件支持不完整。这类问题的特征是“同一份包,大部分机型正常,个别机型一直失败”,只能靠机型覆盖测试发现。
6.3 iOS 高性能模式与广告组件的冲突
这一条值得单独拎出来说,因为它最隐蔽。小游戏在 iOS 上有一个高性能模式,开启之后渲染性能更好,但部分原生组件在这个模式下表现异常,广告就是其中之一。表现可能是广告黑屏、只出声音不出画面、或者干脆不显示。
排查这个问题花了我将近两天。因为安卓一切正常,iOS 大部分机型也正常,只有特定系统版本的设备出问题。最后是逐个关掉高性能模式做对照,才定位到原因。
处理方式有两种:一是对受影响的场景关闭高性能模式,代价是渲染性能下降;二是针对广告展示期间做特殊处理,比如展示前临时切换。我采用的是第一种,因为《水果扛狼》的画面负载不重,关掉高性能模式对帧率影响可以接受。如果你的项目画面很重,就需要权衡一下,或者去做更细的场景化切换。
6.4 一个完整的排查顺序
踩完这些坑之后,我整理了一个固定的排查顺序,之后每次广告出问题都按这个走,基本能在半小时内定位:
- 确认环境:是开发者工具还是真机?工具里不渲染广告是正常的,先排除这一层误判。
- 看控制台:
OnError有没有回调,错误码是什么。没有回调说明请求根本没发出去,问题在代码或者桥接层。 - 核对配置:
adUnitId是否和后台一致,小游戏appId是否匹配,环境宏有没有串。 - 核对账户:后台流量主状态、广告位状态是否正常。
- 控制变量:换个机型试,换网络试,等十分钟再试(排除限流)。
- 看层级:如果回调成功但画面没有广告,问题多半在渲染层级或者高性能模式。
- 看审核:如果只有线上版本不行、预览版正常,去查版本审核状态和广告内容合规。
这个顺序的价值在于它是有层次的:先排除环境误判,再看代码,再看配置,最后才是平台侧。反过来从平台侧开始查,会浪费大量时间。
7. 埋点与调优:把“广告收入”拆成能操作的三个数字
广告上线只是开始,真正决定收益的是后面持续调优。调优的前提是你能看到数据,而且看到的是可以动手改的数据。
7.1 必须埋的五个事件
我把广告相关的埋点收敛到五个事件,不多不少,够用且不增加太多工作量。
ad_entry_show:广告入口曝光。用来算入口的点击率,判断入口位置合不合适。ad_click:用户点击广告按钮。和上一条一起算 CTR。ad_load_result:广告拉取结果,带成功/失败和错误码。用来监控填充率。ad_close:广告关闭,带isEnded、观看时长、广告类型。用来算完播率。ad_reward_grant:奖励发放,带奖励类型和数量。用来核对发放和展示是否对得上。
这五个事件串起来,从“看到入口”到“拿到奖励”的整条链路就完整了。中间任何一环掉得厉害,都能立刻定位。
7.2 用填充率、完播率、人均展示次数定位问题
拿到数据之后,我主要看三个指标,每个指标对应一类问题。
填充率(拉取成功次数 / 请求次数)低,说明广告请求没成功。这时候要去看ad_load_result里的错误码分布。如果集中在某几个错误码,通常是账户或配置问题;如果分散,可能是网络或机型问题。填充率还和地域、时段有关,晚上通常比凌晨高,这个属于正常波动,不用过度反应。
完播率(isEnded === true的次数 / 展示次数)低,说明用户点开广告之后很快就关了。原因一般是奖励不够吸引,或者广告时长太长。激励视频的时长通常在 15 到 30 秒,如果你的奖励只值几秒钟的游戏进度,用户当然不愿意等。这时候要调的是奖励数值,而不是广告本身。
人均展示次数低,说明入口不够或者用户没动力点。这时候要去看ad_entry_show和ad_click的比值,如果入口曝光很多但点击很少,说明位置或者文案有问题;如果入口曝光本身就少,说明入口藏太深了。
7.3 我实际调过一轮之后的结论
《水果扛狼》上线一个月之后,我做了一轮比较系统的调优,几个结论我觉得对同类项目有参考价值。
第一个结论是奖励数值比入口位置更重要。我一开始花了大量精力调整按钮位置、颜色、文案,收益提升有限。后来把复活奖励从“原样复活”改成“满血复活 + 3 秒无敌”,点击率直接上了一个台阶。用户不是不愿意看广告,是不愿意为了一点点收益看广告。
第二个结论是插屏收益的天花板很低,不要为它牺牲留存。插屏的单价和激励视频差距很大,即使把频次拉满,收益增量也有限,但留存损失是实打实的。我最后把插屏频次又降了一档,总收入几乎没变,留存回升了。
第三个结论是新用户的第一个广告位要给得大方。新用户前几次看广告的体验决定了他以后愿不愿意继续点。我的做法是新用户前三次激励视频奖励翻倍,成本可控,但对养成习惯很有效。
第四个结论是必须做广告展示的容错兜底。线上总会有各种原因导致广告拉不起来,这时候如果按钮点了没反应,用户会以为是游戏坏了。我后来在所有广告入口都加了 loading 状态和失败提示,虽然不增加收入,但显著减少了负面反馈。
最后再分享一个小技巧:广告相关的配置尽量做成可远程下发的。频率闸门、奖励数值这些参数,上线之后大概率要调好几轮,如果每次都发版,黄花菜都凉了。我在项目里做了一个简单的配置读取逻辑,从后台拉一份 JSON 覆盖默认值,调参效率提升非常明显。当然要记得做本地缓存和默认值兜底,别因为配置请求失败导致广告功能整个不可用。