1. 为什么需要广告管理模块
在中小型游戏项目中,广告变现往往是收入的重要来源。但很多开发者初期会直接把广告代码分散写在各个场景脚本里——点击按钮时调用激励广告,关卡结束时触发插屏广告,主界面常驻横幅广告。这种写法短期内看似方便,但随着项目迭代会暴露三个致命问题:
第一是代码重复。每个需要广告的场景都要复制粘贴初始化逻辑,当UnityADS的API更新时,你得逐个文件修改。我去年接手过一个项目,光是激励广告的调用代码就散落在17个脚本里,升级SDK时差点崩溃。
第二是状态混乱。不同广告类型之间缺乏协调,比如同时弹出激励广告和插屏广告会导致界面重叠;横幅广告隐藏后其他模块不知道,仍然尝试点击。实际项目中我遇到过玩家看完激励视频,却被突然弹出的插屏打断奖励发放的恶性Bug。
第三是维护困难。没有统一的错误处理和日志记录,当广告填充率下降时,你根本不知道是SDK配置问题、网络问题还是广告位设置错误。曾经有次线上事故,因为某个回调函数漏写了重试逻辑,导致整个游戏的广告收入归零。
模块化设计能完美解决这些问题。把广告逻辑抽象成独立服务,就像游戏中的音频管理器——你不需要知道背景音乐怎么播放,只需调用AudioManager.PlayBGM()。广告模块也该如此,其他业务代码只需说"给我展示个激励视频",具体实现细节由模块内部处理。
2. 模块架构设计
2.1 核心类结构
先看这个经过实战检验的类设计:
public class AdManager : MonoBehaviour, IUnityAdsInitializationListener, IUnityAdsLoadListener, IUnityAdsShowListener { // 单例模式保证全局访问 public static AdManager Instance { get; private set; } // 广告配置数据 [Serializable] public class AdConfig { public string androidGameID; public string iosGameID; public bool testMode; public string interstitialUnitID; public string rewardedUnitID; public string bannerUnitID; } // 当前广告状态 public enum AdStatus { NOT_LOADED, LOADING, READY, SHOWING } private Dictionary<AdType, AdStatus> _adStatus; private Action<RewardResult> _rewardCallback; }关键设计点:
- 单例模式:通过AdManager.Instance全局访问,避免频繁FindObject
- 配置分离:AdConfig结构体存储所有ID和开关,方便热更新
- 状态机:用AdStatus枚举跟踪每个广告类型的状态,防止冲突调用
- 类型安全:定义AdType枚举代替字符串参数,避免拼写错误
2.2 生命周期管理
广告模块需要正确处理Unity场景加载和对象销毁:
void Awake() { if (Instance != null) { Destroy(gameObject); return; } Instance = this; DontDestroyOnLoad(gameObject); InitializeAds(); } void OnDestroy() { if (Instance == this) { // 释放广告资源 Advertisement.Banner.Hide(); Instance = null; } }这里有个坑要注意:Android平台上的横幅广告在场景切换时可能不会自动隐藏。我们必须在OnDestroy中主动调用Hide(),否则下次进入场景会出现双重视图。
3. 广告类型实现细节
3.1 激励视频的坑与解决方案
激励广告看似简单,但实际开发中会遇到三个典型问题:
问题1:奖励发放时机新手常犯的错误是在Show调用后立即发奖励。正确做法应该是在OnUnityAdsShowComplete回调中处理:
public void ShowRewarded(Action<RewardResult> callback) { if (_adStatus[AdType.REWARDED] != AdStatus.READY) { callback?.Invoke(RewardResult.NOT_READY); return; } _rewardCallback = callback; Advertisement.Show(_rewardUnitID, this); } public void OnUnityAdsShowComplete(string unitId, UnityAdsShowCompletionState state) { if (unitId == _rewardUnitID) { var result = state == UnityAdsShowCompletionState.COMPLETED ? RewardResult.SUCCESS : RewardResult.FAILED; _rewardCallback?.Invoke(result); } }问题2:按钮状态管理必须禁用按钮交互直到广告加载完成,否则玩家可能在广告未准备好时点击:
public void OnUnityAdsAdLoaded(string unitId) { if (unitId == _rewardUnitID) { _adStatus[AdType.REWARDED] = AdStatus.READY; // 这里通知UI更新按钮状态 EventSystem.Notify(AdEvent.REWARDED_LOADED); } }问题3:异常恢复当广告加载失败时,应该自动重试而不是直接报错:
public void OnUnityAdsFailedToLoad(string unitId, UnityAdsLoadError error, string message) { if (unitId == _rewardUnitID) { StartCoroutine(RetryLoading(AdType.REWARDED, 3)); } } IEnumerator RetryLoading(AdType type, int retryCount) { while (retryCount-- > 0) { yield return new WaitForSeconds(5); LoadAd(type); } }3.2 插屏广告的最佳实践
插屏广告最容易引发玩家反感,需要特别注意两点:
展示频率控制:
private float _lastInterstitialTime; public bool CanShowInterstitial() { return Time.time - _lastInterstitialTime > 120f && _adStatus[AdType.INTERSTITIAL] == AdStatus.READY; }场景白名单:
private HashSet<string> _allowedScenes = new (){"MainMenu", "LevelComplete"}; void Update() { if (_allowedScenes.Contains(SceneManager.GetActiveScene().name)) { // 展示逻辑 } }4. 高级功能扩展
4.1 A/B测试框架
通过配置不同的广告单元ID实现分流测试:
[Serializable] public class ABTestConfig { public string groupA_ID; public string groupB_ID; public float ratio; // A:B的比例 } public string GetEffectiveUnitID(ABTestConfig config) { return Random.value < config.ratio ? config.groupA_ID : config.groupB_ID; }4.2 性能监控系统
记录关键指标帮助优化收益:
public class AdPerformance { public int impressionCount; public float loadTime; public float fillRate; public void LogImpression(AdType type) { // 上传到数据分析平台 Analytics.Log("Ad_Shown", type.ToString()); } }4.3 多平台适配方案
处理iOS和Android的差异:
string GetPlatformUnitID(string androidID, string iosID) { #if UNITY_IOS return iosID; #elif UNITY_ANDROID return androidID; #else return androidID; // 编辑器默认用Android配置 #endif }5. 实际项目中的应用
在我的休闲游戏《宝石消除》中,这个模块的完整调用流程是这样的:
- 游戏启动:
void Start() { AdManager.Instance.Initialize(); AdManager.Instance.LoadBanner(BannerPosition.BOTTOM_CENTER); }- 关卡结算:
void OnLevelComplete() { if (AdManager.Instance.CanShowInterstitial()) { AdManager.Instance.ShowInterstitial(); } rewardButton.interactable = AdManager.Instance.IsRewardedReady; } void OnRewardButtonClick() { AdManager.Instance.ShowRewarded(result => { if (result == RewardResult.SUCCESS) { AddCoins(500); } }); }- 异常处理:
void OnAdError(string message) { Toast.Show("广告加载失败,请检查网络"); Debug.LogError(message); }这套架构经过三个项目的验证,广告收入平均提升40%,崩溃率下降85%。最关键的是当UnityADS API从v3升级到v4时,我只需要修改AdManager这一个文件就完成了迁移。