1. 为什么要在项目里接入广告系统
做独立游戏或者小团队项目,绕不开的一个现实问题就是变现。我见过不少开发者,游戏做完了,玩法也打磨得不错,但就是不知道怎么把流量变成收入,最后项目不了了之。广告变现是移动端游戏最主流的变现方式之一,尤其是对于体量不大、付费意愿不强的休闲游戏来说,激励视频和插屏广告几乎是标配。
UnityAds 是 Unity 官方推出的一套广告聚合与投放方案,它最大的好处是和 Unity 引擎的集成度非常高,不需要额外引入一堆第三方 SDK,也不用为了适配不同广告平台写一堆胶水代码。你只需要在项目里接入一次,就能通过后台配置来管理多个广告来源,这对独立开发者来说省了太多事。
这篇文章主要面向已经有一定 Unity 开发基础、准备给项目加入广告变现能力的开发者。我会从整体设计思路讲起,把接入流程、关键参数、常见坑点都拆开说清楚。即使你之前完全没接触过广告 SDK,跟着走一遍也能跑通。文章里涉及的具体数值和配置,都是基于常见实践给出的参考,实际项目里你需要根据自己的情况调整。
先说一个核心认知:广告接入不是“加个 SDK 就完事”的事情。它涉及到初始化时机、加载策略、展示频率控制、用户留存和收益之间的平衡。很多新手一上来就把广告塞得到处都是,结果用户第二天就流失了。所以我会把“为什么这么设计”讲透,而不是只给你一堆代码。
2. 接入前的整体设计与思路拆解
2.1 广告类型的选择逻辑
UnityAds 支持几种主流的广告形式,每种形式的适用场景和收益模型都不一样。选错了类型,不仅收益上不去,还会严重影响用户体验。
激励视频是收益最高、对用户体验影响最小的一种。用户主动选择观看,看完之后获得游戏内奖励,比如复活一次、双倍金币、解锁皮肤等。这种广告的完成率通常很高,因为用户有明确的动机。我一般建议把激励视频作为核心变现手段,尤其是在关卡失败、资源不足这些节点上设置入口。
插屏广告是在游戏自然停顿点弹出的全屏广告,比如关卡结束、返回主菜单时。它的收益比激励视频低一些,但展示频次可以更高。关键是要控制好弹出时机,绝对不能在用户操作过程中突然弹出来,那会让人非常烦躁。
横幅广告是常驻在屏幕顶部或底部的条幅广告。它的单次收益很低,但胜在持续曝光。适合放在菜单界面或者非核心玩法区域。我个人的经验是,横幅广告对留存的影响比较微妙,如果放的位置太显眼,用户会觉得屏幕被占用,体验下降。
还有一种可试玩广告,用户可以先试玩一小段再决定是否下载,这种广告的转化率通常不错,但对接入方的要求也更高一些。
选择逻辑可以总结成一句话:激励视频做深度,插屏广告做频次,横幅广告做补充。三者搭配使用,收益和体验才能平衡。
2.2 初始化时机的考量
UnityAds 的初始化时机是一个非常关键的决策点。初始化太早,会拖慢游戏启动速度;初始化太晚,又会导致广告来不及加载,用户点击时无广告可看。
我试过几种方案,最后比较稳妥的做法是:在游戏启动后的第一个加载界面或者主菜单出现时进行初始化。这个时候用户已经在等待了,初始化占用的那点时间不会造成额外的感知延迟。同时,初始化完成后立刻开始预加载第一条广告,这样等用户第一次触发广告入口时,大概率已经有缓存好的广告可以展示。
具体来说,可以在第一个场景的Start方法里调用初始化,但不要阻塞主线程。UnityAds 的初始化是异步的,你可以注册回调来监听初始化结果。如果初始化失败,要有重试机制,比如间隔几秒后重试一次,最多重试三次。
注意:不要在
Awake里做初始化,因为这个时候其他系统可能还没准备好,容易出问题。也不要在游戏进行到一半时才初始化,那样首次广告的等待时间会很长。
2.3 广告加载策略的设计
广告加载策略直接决定了广告的填充率和展示成功率。核心原则是:提前加载,按需展示,及时补充。
提前加载的意思是,不要等用户点击了广告按钮才去请求广告,那时候已经来不及了。正确的做法是在初始化完成后就开始加载,并且在每次广告关闭后立刻加载下一条。这样形成一个循环,保证随时有广告可用。
按需展示是指,根据游戏内的具体场景来决定展示哪种广告。比如用户在关卡失败时点击“看广告复活”,这时候展示激励视频;用户在通关后返回菜单,这时候可以展示插屏广告。不同场景对应不同的广告位,后台可以分别配置。
及时补充是指,如果某次加载失败了,要有重试机制。网络波动、广告库存不足都可能导致加载失败,不能一次失败就放弃。我一般会设置一个重试队列,失败后延迟几秒再试,最多重试几次。
2.4 收益与体验的平衡点
这是最考验开发者经验的地方。广告太多,用户流失;广告太少,收益不够。怎么找平衡点?
我的经验是,激励视频的入口可以多设,但插屏广告的频率一定要克制。激励视频是用户主动选择的,多给几个入口反而能提高用户参与度。比如除了复活,还可以设置“看广告获得双倍奖励”“看广告刷新每日任务”等。插屏广告则要严格控制,我一般设置为每两到三关展示一次,而且要在关卡结算界面之后,而不是在关卡进行中。
另外,新用户的前几关最好不要展示插屏广告,让他们先体验游戏的核心乐趣。等用户玩了几关、产生了一定粘性之后,再逐步增加广告展示。这个策略对留存率的保护效果非常明显。
3. 核心细节解析与实操要点
3.1 后台配置的关键参数
在接入代码之前,需要先在 UnityAds 的后台完成一些配置。这些配置决定了广告的行为和收益。
首先是项目创建。登录后台后,创建一个新项目,选择对应的平台(iOS 或 Android)。创建完成后会得到一个 Game ID,这个 ID 是后续代码里要用的关键标识。
然后是广告位配置。每个广告位对应一个 Placement ID。你可以为激励视频、插屏广告、横幅广告分别创建不同的广告位。广告位的名称要清晰,比如“rewarded_video_level_fail”“interstitial_level_complete”,这样后期看数据的时候一目了然。
还有一个重要的参数是广告位类型。UnityAds 后台支持配置广告位的类型,比如激励视频、插屏、横幅等。类型选错了,代码里调用就会出问题。我见过有人把激励视频的广告位配成了插屏类型,结果用户看完广告拿不到奖励,排查了半天才发现是后台配置的问题。
提示:建议在后台为每个广告位设置一个“测试模式”开关,开发阶段打开测试模式,可以避免产生无效展示,也方便调试。
3.2 代码接入的核心步骤
代码接入其实不复杂,但有几个关键点容易出错。下面我按顺序拆解。
第一步是引入命名空间。在脚本顶部加上using UnityEngine.Advertisements;,这样才能调用 UnityAds 的 API。
第二步是初始化。调用Advertisement.Initialize(gameId, testMode),其中gameId是后台创建项目时得到的 ID,testMode是一个布尔值,开发阶段设为true,上线前改成false。
using UnityEngine; using UnityEngine.Advertisements; public class AdManager : MonoBehaviour { private string gameId = "你的GameID"; private bool testMode = true; void Start() { Advertisement.Initialize(gameId, testMode); } }第三步是加载广告。对于激励视频,需要调用Advertisement.Load(placementId)。加载完成后会触发OnUnityAdsAdLoaded回调。你可以在回调里更新 UI,比如把广告按钮从灰色变成可点击状态。
第四步是展示广告。调用Advertisement.Show(placementId),同时传入一个ShowOptions对象来处理回调。激励视频需要实现IUnityAdsShowListener接口,在OnUnityAdsShowComplete回调里判断用户是否完整观看了广告,如果完整观看则发放奖励。
public class RewardedAd : MonoBehaviour, IUnityAdsLoadListener, IUnityAdsShowListener { private string placementId = "rewarded_video"; public void LoadAd() { Advertisement.Load(placementId, this); } public void ShowAd() { Advertisement.Show(placementId, this); } public void OnUnityAdsAdLoaded(string placementId) { Debug.Log("广告加载完成: " + placementId); } public void OnUnityAdsFailedToLoad(string placementId, UnityAdsLoadError error, string message) { Debug.LogError("广告加载失败: " + message); } public void OnUnityAdsShowComplete(string placementId, UnityAdsShowCompletionState state) { if (state == UnityAdsShowCompletionState.COMPLETED) { // 发放奖励 } } public void OnUnityAdsShowFailure(string placementId, UnityAdsShowError error, string message) { Debug.LogError("广告展示失败: " + message); } public void OnUnityAdsShowStart(string placementId) { } public void OnUnityAdsShowClick(string placementId) { } }这段代码是激励视频的完整流程。注意OnUnityAdsShowComplete里的state参数,只有COMPLETED才代表用户完整观看了广告,SKIPPED表示用户中途跳过,这种情况下不应该发放奖励。
3.3 广告位 ID 的管理方式
随着项目变大,广告位会越来越多,如果直接在代码里硬编码字符串,后期维护会很痛苦。我建议用一个静态类或者 ScriptableObject 来集中管理所有广告位 ID。
public static class AdPlacements { public const string RewardedVideoLevelFail = "rewarded_video_level_fail"; public const string RewardedVideoDoubleReward = "rewarded_video_double_reward"; public const string InterstitialLevelComplete = "interstitial_level_complete"; public const string BannerMainMenu = "banner_main_menu"; }这样做的好处是,如果后台改了广告位 ID,只需要改一个地方,不用全局搜索替换。而且代码可读性也更好,看到AdPlacements.RewardedVideoLevelFail就知道是哪个位置的广告。
3.4 测试模式与正式模式的切换
开发阶段一定要打开测试模式,否则你的测试展示会被计入真实数据,可能影响账号的广告投放效果。测试模式下,UnityAds 会返回测试广告,不会产生真实收益,但流程和正式广告完全一样。
上线前记得把testMode改成false。我踩过一次坑,上线后忘了改,结果跑了半天发现没有收益,排查了半天才想起来是测试模式没关。后来我养成了一个习惯:在打包脚本里加一个检查,如果testMode还是true,就弹一个警告。
注意:测试模式下,广告的填充率和展示行为可能和正式模式有差异。上线前最好用正式模式在真机上跑一遍完整流程,确认没有问题。
4. 实操过程与核心环节实现
4.1 从零搭建一个广告管理模块
前面讲了零散的接入点,现在把它们串起来,做一个完整的广告管理模块。这个模块要负责初始化、加载、展示、重试、频率控制等所有事情。
先定义模块的整体结构。我一般会分成三个部分:AdInitializer负责初始化,AdLoader负责加载和缓存,AdController负责展示和频率控制。这样职责清晰,后期扩展也方便。
AdInitializer的核心逻辑是:在游戏启动时调用初始化,监听初始化结果,成功后通知AdLoader开始加载广告。如果初始化失败,启动重试计时器。
public class AdInitializer : MonoBehaviour { private string gameId = "你的GameID"; private bool testMode = false; private int retryCount = 0; private const int MaxRetry = 3; void Start() { Initialize(); } private void Initialize() { Advertisement.Initialize(gameId, testMode, new AdInitializationListener(this)); } public void OnInitializeSuccess() { Debug.Log("广告初始化成功"); AdLoader.Instance.PreloadAll(); } public void OnInitializeFailed(string message) { if (retryCount < MaxRetry) { retryCount++; Invoke(nameof(Initialize), 5f); } } }AdLoader负责维护每个广告位的加载状态。它内部用一个字典记录每个广告位是否已经加载完成,并提供Load和IsReady方法。
public class AdLoader : MonoBehaviour { public static AdLoader Instance; private Dictionary<string, bool> adReadyState = new Dictionary<string, bool>(); void Awake() { Instance = this; } public void PreloadAll() { Load(AdPlacements.RewardedVideoLevelFail); Load(AdPlacements.InterstitialLevelComplete); } public void Load(string placementId) { adReadyState[placementId] = false; Advertisement.Load(placementId, new AdLoadListener(this, placementId)); } public bool IsReady(string placementId) { return adReadyState.ContainsKey(placementId) && adReadyState[placementId]; } public void OnAdLoaded(string placementId) { adReadyState[placementId] = true; } public void OnAdLoadFailed(string placementId) { adReadyState[placementId] = false; Invoke(nameof(RetryLoad), 3f); } private void RetryLoad() { // 重试逻辑 } }AdController负责展示广告,并控制展示频率。它内部记录上次展示插屏广告的时间,如果距离上次展示不足设定的间隔,就跳过这次展示。
public class AdController : MonoBehaviour { private float lastInterstitialTime = 0f; private const float InterstitialCooldown = 120f; public void ShowRewardedVideo(string placementId, System.Action onComplete) { if (!AdLoader.Instance.IsReady(placementId)) { onComplete?.Invoke(); return; } Advertisement.Show(placementId, new RewardedShowListener(onComplete)); } public void ShowInterstitial(string placementId) { if (Time.time - lastInterstitialTime < InterstitialCooldown) { return; } if (!AdLoader.Instance.IsReady(placementId)) { return; } lastInterstitialTime = Time.time; Advertisement.Show(placementId); } }这个模块的代码量不大,但把核心逻辑都覆盖了。实际项目里你可以根据需求继续扩展,比如加入更复杂的频率控制策略、A/B 测试等。
4.2 激励视频的完整调用流程
激励视频的调用流程比插屏广告复杂一些,因为涉及到奖励发放。我把它拆成几个步骤。
第一步,在游戏里设置广告入口。比如关卡失败界面上放一个“看广告复活”按钮。按钮的初始状态应该是不可点击的,等广告加载完成后再启用。
第二步,用户点击按钮后,先检查广告是否已经加载好。如果没加载好,可以显示一个提示“广告加载中,请稍候”,同时触发一次加载请求。
第三步,广告加载好后,调用Show方法展示。展示过程中,游戏应该暂停,避免用户在观看广告时还能操作游戏。
第四步,广告关闭后,根据OnUnityAdsShowComplete回调里的state判断是否发放奖励。如果是COMPLETED,发放奖励并继续游戏;如果是SKIPPED,不发放奖励,但也要继续游戏。
第五步,无论展示成功还是失败,都要立刻触发下一次加载,保证下一条广告能及时准备好。
提示:奖励发放一定要做幂等处理。我遇到过一种情况,用户看完广告后网络波动,回调触发了两次,结果奖励发了两次。后来我在发放奖励前加了一个标记位,确保同一次展示只发一次奖励。
4.3 插屏广告的时机控制
插屏广告的展示时机比激励视频更讲究。展示得太频繁,用户会烦;展示得太少,收益又上不去。
我的做法是设置一个冷却时间,比如 120 秒。在冷却时间内,即使触发了展示条件,也不展示。同时,在关卡结算界面之后展示插屏广告,而不是在关卡进行中。
具体实现上,可以在关卡完成的回调里调用ShowInterstitial。这个方法内部会检查冷却时间,如果还在冷却期内就直接返回,不展示广告。
另外,新用户的前三关不展示插屏广告。这个可以通过记录用户已完成的关卡数来实现。如果关卡数小于 3,直接跳过插屏展示。
还有一个细节是,插屏广告展示时,游戏应该暂停。但插屏广告关闭后,游戏不应该自动恢复,而是等用户主动操作后再恢复。这样可以避免用户还没反应过来,游戏就继续了。
4.4 横幅广告的布局与刷新
横幅广告的接入相对简单,但布局上有一些讲究。UnityAds 的横幅广告支持几种位置:顶部、底部、左上、右上等。我一般选择底部,因为底部对游戏视野的影响最小。
横幅广告的刷新是自动的,不需要手动调用。但你可以设置刷新间隔,不过这个参数在后台配置,代码里改不了。
横幅广告的展示和隐藏可以通过Show和Hide方法控制。比如在游戏主菜单显示横幅,进入关卡后隐藏。这样可以避免横幅遮挡游戏内容。
注意:横幅广告在部分设备上可能会遮挡底部 UI,尤其是那些有虚拟导航栏的设备。建议在布局时给底部留出足够的空间,或者根据设备的安全区域动态调整。
5. 常见问题与排查技巧实录
5.1 广告加载失败的原因与排查
广告加载失败是最常见的问题,原因可能有很多。我整理了一个排查表,按优先级从高到低排列。
| 问题现象 | 可能原因 | 排查方法 |
|---|---|---|
| 一直加载失败 | Game ID 配置错误 | 检查后台的 Game ID 和代码里的是否一致 |
| 一直加载失败 | 广告位 ID 配置错误 | 检查 Placement ID 是否和后台一致 |
| 偶尔加载失败 | 网络波动 | 检查设备网络,增加重试机制 |
| 特定地区加载失败 | 广告库存不足 | 这是正常现象,增加重试和备用广告位 |
| 测试模式正常,正式模式失败 | 正式模式配置问题 | 检查后台项目是否已上线,广告位是否已激活 |
| 初始化失败 | SDK 版本过旧 | 更新 UnityAds SDK 到最新版本 |
排查的时候,先看日志。UnityAds 的日志会输出具体的错误码和错误信息,根据错误码去查文档,基本能定位到问题。
我遇到过一次比较隐蔽的问题:广告在编辑器里能加载,但在真机上不行。后来发现是打包时漏掉了网络权限。Android 需要在AndroidManifest.xml里声明INTERNET权限,iOS 需要确保NSAppTransportSecurity配置正确。这种问题日志里不一定有明确提示,需要自己检查打包配置。
5.2 广告展示后没有回调
有时候广告展示了,但回调没有触发。这种情况通常有几个原因。
一是广告位类型不匹配。比如你把激励视频的广告位配成了插屏类型,展示时可能不会触发OnUnityAdsShowComplete回调。检查后台的广告位类型设置。
二是回调对象被销毁了。如果你在展示广告后切换了场景,原来的回调对象可能已经被销毁,回调自然就收不到了。解决办法是把广告管理模块做成DontDestroyOnLoad,或者用静态回调。
三是 SDK 版本问题。某些旧版本的 UnityAds SDK 在特定平台上存在回调丢失的 bug。更新到最新版本通常能解决。
5.3 收益数据异常的处理
收益数据异常通常表现为:展示次数正常,但收益为零;或者收益远低于预期。
如果展示次数正常但收益为零,先检查是不是还在测试模式。测试模式下展示的广告不产生收益。如果确认不是测试模式,检查后台的广告位是否已经激活,有些广告位需要审核通过后才能产生收益。
如果收益远低于预期,可能是广告类型选择的问题。激励视频的收益通常高于插屏和横幅,如果激励视频的展示占比很低,整体收益就会偏低。可以调整广告入口的布局,把激励视频的入口做得更显眼一些。
还有一个容易被忽略的点是广告的填充率。填充率是指请求广告后成功返回广告的比例。填充率低,展示次数就少,收益自然上不去。填充率受地区、时间段、广告位类型等多种因素影响。如果某个地区的填充率特别低,可以考虑增加备用广告位,或者调整广告展示策略。
5.4 用户投诉广告太多的应对
用户投诉广告太多,说明广告频率控制出了问题。这时候需要从几个方面排查。
先看插屏广告的展示频率。如果冷却时间设置得太短,比如 30 秒,用户会感觉广告无处不在。我建议至少 120 秒,最好 180 秒以上。
再看广告入口的数量。激励视频的入口可以多,但插屏广告的触发点要少。检查一下是不是在太多地方触发了插屏展示。
还有一个细节是广告的关闭按钮。有些广告的关闭按钮很小,用户找不到,就会觉得被强制观看。这种情况可以通过后台配置来调整,但更根本的是控制广告展示频率,减少用户的抵触情绪。
如果投诉已经比较多,可以临时降低广告频率,观察留存率的变化。等留存率回升后,再逐步恢复。这个过程需要持续监控数据,不能拍脑袋决定。
5.5 不同平台的适配差异
UnityAds 在 iOS 和 Android 上的行为有一些差异,需要分别处理。
iOS 上,广告的加载速度通常比 Android 慢一些,因为 iOS 对网络请求的限制更严格。所以 iOS 上的预加载要更早开始,重试间隔也要适当延长。
Android 上,设备碎片化严重,不同厂商的设备表现差异很大。有些低端设备上,广告展示可能会卡顿甚至崩溃。建议在低端设备上降低广告展示频率,或者只展示激励视频,不展示插屏和横幅。
另外,iOS 的隐私政策要求更严格,需要在Info.plist里配置相关的隐私描述。如果配置不正确,广告可能无法加载。这个在打包前一定要检查。
6. 广告策略的持续优化思路
6.1 数据监控的关键指标
广告接入完成后,需要持续监控几个关键指标,才能不断优化策略。
填充率是最基础的指标,反映了广告请求的成功率。填充率低,说明广告库存不足或者配置有问题。
展示率是展示次数除以请求次数,反映了广告的实际展示比例。展示率低,可能是加载失败或者用户没有触发展示。
完成率主要针对激励视频,反映了用户完整观看广告的比例。完成率高,说明奖励设置合理,用户愿意看完。
eCPM是每千次展示的收益,是衡量广告价值的核心指标。eCPM 高,说明广告位的价值高,可以适当增加展示频率。
留存率是最终要的指标。广告策略的调整不能只看收益,还要看对留存的影响。如果收益涨了但留存掉了,长期来看是亏的。
6.2 A/B 测试的实操方法
A/B 测试是优化广告策略的有效手段。具体做法是:把用户随机分成两组,一组用策略 A,一组用策略 B,对比两组的收益和留存数据。
比如你想测试插屏广告的冷却时间,可以设置 A 组 120 秒,B 组 180 秒。跑一周后对比两组的 eCPM 和次日留存。如果 B 组的 eCPM 略低但留存明显更高,那 B 组策略可能更优。
A/B 测试的关键是只改变一个变量。如果同时改多个参数,就无法判断是哪个参数起了作用。另外,测试周期要足够长,至少一周,避免偶然波动影响结论。
6.3 季节性调整与活动配合
广告收益有明显的季节性波动。比如节假日期间,广告主的预算增加,eCPM 通常会上升。这时候可以适当增加广告展示频率,抓住收益高峰。
反过来,在广告淡季,eCPM 下降,可以降低展示频率,保护用户体验。等旺季再恢复。
另外,游戏内做活动的时候,也可以配合调整广告策略。比如活动期间,把激励视频的奖励翻倍,鼓励用户多看广告。活动结束后再恢复原样。
6.4 长期运营的注意事项
广告变现是一个长期的事情,不能只看短期收益。我个人的经验是,留存率比短期收益更重要。一个留存率高的游戏,即使广告频率低,长期收益也会超过那些广告满天飞但用户流失快的游戏。
所以,每次调整广告策略后,都要观察留存率的变化。如果留存率下降超过 5%,就要考虑回滚策略。
还有一点是不要忽视用户反馈。应用商店的评论、社区里的讨论,都是了解用户对广告态度的重要渠道。如果很多用户提到广告问题,就要认真对待,及时调整。
最后,广告 SDK 的版本要定期更新。新版本通常会修复一些 bug,优化加载速度,甚至提升收益。但更新前要在测试环境充分验证,避免引入新的问题。
我个人在实际操作中的体会是,广告接入这件事,技术实现只是第一步,真正的功夫在策略调整和数据分析上。同样的 SDK,同样的广告位,不同的展示策略,收益可能差好几倍。所以不要接完就放着不管,要持续观察、持续优化。踩过几次坑之后,你会慢慢找到适合自己项目的节奏。