2015年7月7日 星期二

Unity:Google 的 AdMob 廣告做法(iOS & Android)

以往,要使用 Unity 接入 Google 的 AdMob 廣告,需要自行下載 AdMob SDK 來撰寫 Plugin,或者直接購買其他開發者做好的 Plugin 來使用,如今,由於 Google Developers 網站上直接就有提供免費的 Plugin 可供使用,而且網站上也提供了中文版說明,使得 Unity 套用 AdMob 廣告變得方便很多,不過,實作上還是有些細節要特別注意,這在該網站上就沒有詳細說明。

在使用 AdMob 廣告之前,要先登入或申請 AdMob 帳號後,在所建立的應用程式中取得「廣告單元」編號,依照網頁指示即可申請取得,在此就不多加詳述了。

AdMob 的 廣告單元編號



首先,我們要先下載兩個 Google Mobile Ads 相關的檔案:

  • 在 Unity 專案中使用的 Plugin。
  • 在 Xcode 專案中使用的 framework。

我們平常可以很簡單的在 Google 搜尋關鍵字 google mobile ads sdk 即可找到 Google Developer 網頁上有關如何在 Unity 使用 AdMob 的相關說明,在網頁上直接點擊藍色的按鈕「下載外掛程式」即可進入下載 Google Mobile Ads Unity Plugin,而點擊左側的「SDK 下載」即可找到下載 Google Mobile Ads SDK iOS 的連結。

藍色按鈕下載外掛程式,左側「SDK下載」可找到其他 SDK
下載 GoogleMobileAds.unitypackage

下載好相關檔案之後,直接將所下載的 GoogleMobileAds.unitypackage 匯入到 Unity 專案中,不用多做什麼,Plugin 的部分就已經算是建立完成,接下來就是寫 script 來使用它。

AdMob 廣告主要分為橫幅和插頁式兩種,這兩種的呼叫方式有些微不同,所以必須分開來製作,另外,基本上測試時不應該投放真實的廣告,而是提供測試機的 Device ID 給 AdMob 來投放測試廣告,所以,還需要另外提供給橫幅及插頁廣告公用的 Script 以取得 Device ID。

這個 AdMob 所需要的 Device ID 其實有些特別,如果在 Google Play 下載一些取得 Device ID 的 App 來測試,會發現幾個 App 所取得的都是不同的 ID,通常,AdMob Device ID Finder 所取得的 ID 才是比較符合 AdMob 所需要的。

一般而言,只要將這個 App 安裝到所有的測試機上面取出 Device ID 之後,在程式碼或是 Unity 之中建表紀錄,這樣 AdMob 就能知道哪幾台機器正在做為測試機請求投放廣告,只是這種做法有點死板,特別是在將 App 提供給很多人測試時,會變得相當麻煩,所以我們必須要有一套機制。

如果要發佈為測試用的 App,在 Unity 中要 Build 時都應該勾選 Development Build,然後,在程式碼中透過 Debug.isDebugBuild 直接判斷是否為開發中的版本,並直接自該裝置取得 Deveice ID 告知 AdMob 本機為測試機,使用這種一勞永逸的機制,將來就不需要收集測試機的 Device ID 來建表紀錄了。

而這裡所需要的 Device ID 是個加密過的編碼,每個平台的取得方式不同,所以還需要使用 Unity 官方文件中的 Platform Dependent Compilation 中的這些 Platform Defines 區隔開來個別撰寫取得方式,以下,將可得到這樣的公用 Script 內容:

using UnityEngine;
using System;
using System.Text;
using System.Security.Cryptography;

public class AdCommon {

    private static string Md5Sum(string strToEncrypt){

        UTF8Encoding ue = new UTF8Encoding();

        byte[] bytes = ue.GetBytes(strToEncrypt);

        MD5CryptoServiceProvider md5 = new MD5CryptoServiceProvider();
        byte[] hashBytes = md5.ComputeHash(bytes);

        string hashString = "";
        for (int i = 0; i < hashBytes.Length; i++) {

            hashString += Convert.ToString(hashBytes[i], 16).PadLeft(2 , '0');
        }

        return hashString.PadLeft(32, '0');
    }

    public static string DeviceIdForAdmob{

        get{
#if UNITY_EDITOR
            return SystemInfo.deviceUniqueIdentifier;
#elif UNITY_ANDROID
            AndroidJavaClass jc = new AndroidJavaClass("com.unity3d.player.UnityPlayer");
            AndroidJavaObject currentActivity = jc.GetStatic<AndroidJavaObject>("currentActivity");
            AndroidJavaObject contentResolver = currentActivity.Call<AndroidJavaObject>("getContentResolver");
            AndroidJavaObject secure = new AndroidJavaObject("android.provider.Settings$Secure");
            string deviceID = secure.CallStatic<string>("getString" , contentResolver, "android_id");
            return Md5Sum(deviceID).ToUpper();
#elif UNITY_IOS
            return Md5Sum(UnityEngine.iOS.Device.advertisingIdentifier);
#else
            return SystemInfo.deviceUniqueIdentifier;
#endif
        }
    }
}

完成了以上的 AdCommon Script 之後,就有了取得 Device ID 的方法,接下來,就可以開始撰寫橫幅廣告以及插頁廣告的 Script。

在原本的 Plugin 中,由於,橫幅廣告的尺寸定義(AdSize)本身是個 Class,不同尺寸項目使用靜態唯讀的欄位來宣告,為了方便在 Unity 的 Inspector 視窗用選項的方式來設置,我們需要另外宣告個 enum 以及配合 Dictionary 來指定橫幅廣告尺寸。

同時,為了能夠在整個專案中的任何其它 Script 中可以直接請求橫幅廣告的功能,還需要特別宣告一個代表 Class 本身的靜態欄位 ins 並使物件本身不會因為轉換場景而被銷毀。

剩下的就是操作橫幅廣告的功能,基本上,橫幅廣告在請求投放廣告下載完成後,即會直接顯示在畫面上,之後,再以畫面需求在不同時間點讓它隱藏或重新顯示,而不需要再次請求投放,原則上,在持續投放廣告的過程中,應該會自動更換不同的廣告。

其中,我們要特別注意的是,在請求投放廣告的時候必須要判斷目前所安裝的是不是開發中版本 Development Build,並讓使用此版本的裝置都自動將本機的 Device ID 告知 AdMob,讓它投放測試用廣告,以免在測試過程中重複點擊廣告而被判定不當點擊廣告,因此而被 AdMob 廣告停權就太不划算了。

using System.Collections.Generic;
using GoogleMobileAds.Api;

public class AdBanner : MonoBehaviour {

    public enum BannerSize{

        BANNER,
        MEDIUM_RECTANGLE,
        FULL_BANNER,
        LEADERBOARD,
        SMART_BANNER
    }

    public static AdBanner ins;

    public string unitId;
    public BannerSize size = BannerSize.BANNER;
    public AdPosition position = AdPosition.Top;

    private BannerView bannerView;
    private Dictionary<BannerSize,AdSize> adSize = new Dictionary<BannerSize, AdSize>(){
        {BannerSize.BANNER , AdSize.Banner},
        {BannerSize.MEDIUM_RECTANGLE , AdSize.MediumRectangle},
        {BannerSize.FULL_BANNER , AdSize.IABBanner},
        {BannerSize.LEADERBOARD , AdSize.Leaderboard},
    {BannerSize.SMART_BANNER , AdSize.SmartBanner}
    };

    void Awake(){

        if(ins == null){

            ins = this;
            DontDestroyOnLoad(gameObject);

        }else if(ins != this){

            Destroy(gameObject);
        }
    }

    void Start(){

        this.RequestBanner();
    }

    private void RequestBanner(){

        if(!string.IsNullOrEmpty(this.unitId)){

            this.bannerView = new BannerView(this.unitId , this.adSize[this.size] , this.position);

            AdRequest.Builder _builder = new AdRequest.Builder();

            if(Debug.isDebugBuild) _builder.AddTestDevice(AdCommon.DeviceIdForAdmob);

            this.bannerView.LoadAd(_builder.Build());
        }
    }

    public void ShowBanner(){

        if(this.bannerView != null){

            this.bannerView.Show();
        }
    }

    public void HideBanner(){

        if(this.bannerView != null){

            this.bannerView.Hide();
        }
    }
}

插頁廣告的 Script 做法與橫幅廣告相當類似,不過並不需要設置廣告尺寸以及位置,比較不同的是,插頁廣告並不會請求投放廣告成功之後就顯示出來,而是分段執行的,所以在請求投放之後要另外執行顯示廣告,為了確保廣告能正確投放,在執行顯示廣告之前必須判斷是否已經下載完成。



還有,在每次顯示廣告之前都必須重新請求廣告投放,所以在請求投放廣告時,要將原本的廣告物件銷毀後再重新請求投放廣告。

using UnityEngine;
using System;
using GoogleMobileAds.Api;

public class AdInterstitial : MonoBehaviour {

    public static AdInterstitial ins;

    public string unitId;
    private InterstitialAd interstitialAd;

    void Awake(){

        if(ins == null){

            ins = this;
            DontDestroyOnLoad(gameObject);

        }else if(ins != this){

            Destroy(gameObject);
        }
    }

    public void RequestInterstitial(){

        if(!string.IsNullOrEmpty(this.unitId)){

            if(this.interstitialAd != null){
                this.interstitialAd.Destroy();
                this.interstitialAd = null;
            }

            this.interstitialAd = new InterstitialAd(this.unitId);

            AdRequest.Builder _builder = new AdRequest.Builder();

            if(Debug.isDebugBuild) _builder.AddTestDevice(AdCommon.DeviceIdForAdmob);

            this.interstitialAd.LoadAd(_builder.Build());
        }
    }

    public void ShowInterstitial(){

        if(this.interstitialAd != null && this.interstitialAd.IsLoaded()){

            this.interstitialAd.Show();
        }
    }
}

在完成以上三個程式檔之後,有關投放廣告的功能算是已經完成,我們可以自行利用 UGUI 製作畫面上的按鈕來測試,使用上只要在場景中分別建立橫幅廣告以及插頁廣告專用的 GameObject 並賦予 AdBanner 以及 AdInterstitial 後,設置好廣告單元編號 Unit Id 即可直接在 Unity 編輯器中做初步的測試。測試時,可在 Console 視窗看到相關的對應訊息。

設置好 Ad Banner 的廣告單元編號以及尺寸、位置
設置好 Ad Interstitial 的廣告單元編號
使用 UGUI 製作的測試按鈕
顯示測試相對應的訊息

iOS

在 Unity 編輯器中測試無誤後,就可以直接 Build 到 iOS 平台建立 Xcode 專案,在 Build 的時候務必記得勾選 Development Build。
記得勾選 Development Build
開啟剛建立好的 Xcode 專案,直接在左側的導覽區使用滑鼠右鍵選單選擇 Add Files to "..." 或是從選單列選擇 File > Add Files to "..." 來加入先前下載的 Google Mobile Ads SDK iOS framework(必須先解壓縮)。

選擇 Add Files to
為 Xcode 專案加入 GoogleMobileAds.framework
接下來,在左側導覽區選擇專案名,然後在中間的編輯區切換到 General 查看最下方的 framework,檢查並添加其他需要的 framework,此部份可以參考 Google Developers 網站上的說明 https://developers.google.com/mobile-ads-sdk/docs/admob/ios/quick-start#manually_using_the_sdk_download

添加需要的 framework
此時,Xcode 專案需要添加的東西已經完成,如果馬上連接裝置並點擊上方的 Build 按鈕,會 Build Failed,並出現一些關於使用 @import 的解析問題。

發生有關 @import 的問題
此時,回到上一動,在左側導覽區選擇專案使中間的工作區顯示專案設置內容,並切換到 Build Settings,搜尋關鍵字 modules 可找到 Enable Modules (C and Objective-C) 的設置,將該欄位設置為 Yes,再重新 Build 即可成功。

將 Enable Modules (C and Objective-C) 設置為 Yes
這邊要特別提醒的是,在 iOS 的裝置上測試 AdMob 廣告時,記得將設定裡面的「隱私權 > 廣告 > 限制廣告追蹤」關閉,否則將無法正確投放測試廣告。

關閉限制廣告追蹤
顯示測試用的橫幅廣告 
顯示測試用的插頁廣告
Android

在 Android 實裝 AdMob 廣告時,還需要在 Unity 專案中加入 Google Play Service 的 Library,這個 Library 可在 {Android SDK 路徑}/extras/google/google_play_services/libproject/google-play-services_lib 找到,直接將 google-play-services_lib 資料夾整個放到 Unity 專案的 Plugins/Android 路徑下就可以了,之後,直接 Build 成為 apk 檔並安裝到 Android 裝置上即可,當然,Build 時記得也要勾選 Development Build 才可以正常投放測試廣告。

另外,Google Developers 網站上提到要在 Unity 專案裡的 Plugins/Android/AndroidManifest.xml 加入 <meta-data android:name="unityplayer.ForwardNativeEventsToDalvik" android:value="true" /> 以確保廣告可供點擊,但實際使用時,加入這一行反倒是讓 UGUI 按鈕無法點擊,經測試,不需要修改 AndroidManifest.xml 即可正常播放廣告且可正常點擊,因此,這邊就先不理會此部份。

以上就是有關於實作和測試 AdMob 廣告,以及 Build 到實機測試的部分,一般而言,廣告都投放正常,並可正確點擊就可以了;不過,Google Developers 網站上還提供了其他指定請求可以使用,有興趣可以研究一下;當然,也提供了廣告事件,可以在某些廣告狀況之後,再做其他的事情,只是廣告事件是依賴廣告物件去註冊使用的,所以,一般不應該直接註冊,而是要先確認廣告物件是否存在,所以,最好是另外宣告方法去註冊事件以及取消事件,以上的 AdBanner 可以再補充以下方法來做廣告事件的註冊以及取消。

public void RegisterBannerLoaded(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdLoaded += _handle;
}
public void RegisterBannerFailedToLoad(EventHandler<AdFailedToLoadEventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdFailedToLoad += _handle;
}
public void RegisterBannerOpened(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdOpened += _handle;
}
public void RegisterBannerClosing(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdClosing += _handle;
}
public void RegisterBannerClosed(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdClosed += _handle;
}
public void RegisterBannerLeftApplication(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdLeftApplication += _handle;
}

public void CancelBannerLoaded(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdLoaded -= _handle;
}
public void CancelBannerFailedToLoad(EventHandler<AdFailedToLoadEventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdFailedToLoad -= _handle;
}
public void CancelBannerOpened(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdOpened -= _handle;
}
public void CancelBannerClosing(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdClosing -= _handle;
}
public void CancelBannerClosed(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdClosed -= _handle;
}
public void CancelBannerLeftApplication(EventHandler<EventArgs> _handle){
    if(this.bannerView == null) return;
    this.bannerView.AdLeftApplication -= _handle;
}

例如,當需要在橫幅廣告被載入之後處理一些事情,可使用以下程式碼實現:

public void HandleAdLoaded(object sender, EventArgs args)
{
    // ... do something...
}

AdBanner.ins.RegisterBannerLoaded(this.HandleAdLoaded);

相同的做法,AdInterstitial 也可以補充相關的事件註冊與取消,程式碼如下:

public void RegisterInterstitialLoaded(EventHandler<EventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdLoaded += _handle;
}
public void RegisterInterstitialFailedToLoad(EventHandler<AdFailedToLoadEventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdFailedToLoad += _handle;
}
public void RegisterInterstitialOpened(EventHandler<EventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdOpened += _handle;
}
public void RegisterInterstitialClosing(EventHandler<EventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdClosing += _handle;
}
public void RegisterInterstitialClosed(EventHandler<EventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdClosed += _handle;
}
public void RegisterInterstitialLeftApplication(EventHandler<EventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdLeftApplication += _handle;
}

public void CancelInterstitialLoaded(EventHandler<EventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdLoaded -= _handle;
}
public void CancelInterstitialFailedToLoad(EventHandler<AdFailedToLoadEventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdFailedToLoad -= _handle;
}
public void CancelInterstitialOpened(EventHandler<EventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdOpened -= _handle;
}
public void CancelInterstitialClosing(EventHandler<EventArgs> _handle){
    if(this.interstitialAd == null) return;
    this.interstitialAd.AdClosing -= _handle;
}
public void CancelInterstitialClosed(EventHandler<EventArgs> _handle){
    if(interstitialAd == null) return;
    interstitialAd.AdClosed -= _handle;
}
public void CancelInterstitialLeftApplication(EventHandler<EventArgs> _handle){
    if(interstitialAd == null) return;
    interstitialAd.AdLeftApplication -= _handle;
}

Google 的 AdMob 廣告應該算目前行動 App 使用最廣泛的,不過,使用 Unity 遊戲引擎通常都是用於開發遊戲產品,Unity 官方也提供了 Unity Ads 讓人投放遊戲相關的廣告並讓開發者也有機會從中獲取利潤,那也是個不錯的選擇。

範例專案下載

目前版本:

OS X Yosemite 10.10.4
Xcode 6.4

Unity 5.1.1f1
Google Mobile Ads Unity Plugin v2.3.0

Target iOS Version: 6.0
Google Mobile Ads iOS v7.3.1
Test Device: iOS 8.4

Minimum API Level: Android 2.3.1 (API Level 9)
Android SDK Build-tools: Rev. 21.1.2
Android 5.0.1 (API 21)
Test Device: Android 4.2.2