AdMobのリワード広告のプリロードってどうするのがベストプラクティスか?

AdMobのReward広告のPreloadってどうするのがベストプラクティスなのか分からなかったのですが、Google JapanのAdMob担当の方に質問して教えてもらったのでまとめてみます。

自分の理解が間違っている可能性もあるので組み込む際は自己責任でお願いします。

また、自分はUnityを使っているので今回はUnity上での話になります。

問題点

AdMobでリワード広告を再生するときは当然プリロードしておきたいですよね。

しかし、RewardBasedVideoAdをリクエストすると環境によっては失敗するときがあります。

例えばネットワークにアクセスできない状況だったり、広告在庫がなかったりしたときです。広告在庫に関しては日本ではなくなるってことはあんまりないけど、発展途上国などでは結構あるらしいです。

広告取得に失敗すると、OnAdFailedToLoadに登録したコールバックに処理が回ってきます。

取得に失敗したら、再度プリロードしたいところですが、すぐにまた広告をリクエストすると、

リクエスト -> 失敗 -> リクエスト -> 失敗 -> ....

っとリクエストが怒涛の勢いで積み重なり、負荷がかかっちゃいます。またダッシュボードで確認するとフィルレートがすごい低いことになってしまって、見ていて気持ち良いものではありません。

解決策

そんなこんなで、どうするのが良いのかなぁとずっと思っていたのですが、たまたまGoogleの方に直接質問できる機会があったので質問したところ、

ちゃんとエラーメッセージをみて、それに応じて処理を分けたほうが良いです。

とのこと。まあ、当たり前といえば当たり前ですね^^;

ただ、エラー毎に処理を分けるのがめんどくさいですし、そもそもどんなタイプのエラーがあるのかよく分かりません(-_-;)
(レファレンスあるのか聞けばよかった...)

そんな人に、めんどくさがりにオススメなのは、

失敗したら数分待ってリクエストする

らしいです。

注意点

ただ、数分待ってリクエストするということで、Invokeでも使ってサクッと実装っと思ったのですが、少し注意が必要です。

OnAdFailedToLoadに登録したコールバックはメインスレッドから呼ばれないようなのです。

Unityの関数の中にはメインスレッドで呼んではいけないものというのがいくつかあり、気をつけないといけません。

とりあえず、自分が今までハマったもので言うと、

  • Debug.isDebugBuild
  • Application.internetReachability
  • 音を出す関数(関数名忘れた)
  • Invoke

の4つです。これに違反すると、

UnityException: ○○○○(関数名) can only be called from the main thread.
Constructors and field initializers will be executed from the loading thread when loading a scene.
Don't use this function in the constructor or field initializers, instead move initialization code to the Awake or Start function.

などというメッセージが出ます。(ただ、Androidでしかこのメッセージは見たことないのでAndroid版限定の話なのかも。)

Invokeが使えないので、数分待ってリクエストしようねっというフラグだけ立てて、実際数分後にリクエストする処理はUpdateの中などでするのが吉です。

さらに改善

さらに、たまたま一回だけリクエストに失敗したというパターンもあると思うので、徐々に待ち時間を長くするのが良いと思います。例えば、

5秒後に再リクエスト -> 20秒後に再リクエスト -> 80秒後に再リクエスト -> 5分後に再リクエスト -> 5分後に再リクエスト(5分程度で頭打ちにしておく) -> ...

のような感じです。

参考までに、そんなこんなで実装してみたのが以下になります。

using UnityEngine;
using UnityEngine.UI;
using System;
using UnityEngine.Assertions;
using GoogleMobileAds.Api;

public class AdListener : MonoBehaviour {
    //android
    public string adAppIdAndroid;
    public string adIdRewardAndroid;
    //iOS
    public string adAppIdIOS;
    public string adIdRewardIOS;

    private RewardBasedVideoAd rewardBasedVideo          = null;
    public  Action             userCallbackOnReward      = null;
    public  Action             userCallbackOnRewardClose = null;

    protected bool  isShowingRewardVideo      = false;
    protected float delayedCallTimer          = 0.0f;
    protected int   delayedCallTimeTableIndex = 0;

    //0秒後、5秒後、20秒後、80秒後、300秒後にリクエストをする
    protected float[] delayedCallTimeTable = new float[] {
        0.0f,
        5.0f,
        20.0f,
        80.0f,
        300.0f,
    };

	void Awake () {
#if UNITY_ANDROID
        MobileAds.Initialize(adAppIdAndroid);
#elif UNITY_IPHONE
        MobileAds.Initialize(adAppIdIOS);
#else
#endif
        rewardBasedVideo                        =  RewardBasedVideoAd.Instance;
        // has rewarded the user.
        rewardBasedVideo.OnAdLoaded             += HandleRewardBasedVideoLoaded;
        rewardBasedVideo.OnAdRewarded           += HandleRewardBasedVideoRewarded;
        rewardBasedVideo.OnAdClosed             += HandleRewardBasedVideoRewardedClosed;
        rewardBasedVideo.OnAdFailedToLoad       += HandleRewardBasedVideoRewardedFailed;
        rewardBasedVideo.OnAdLeavingApplication += HandleRewardBasedVideoLeavingApplication;
        initDelayedCallTime();
	}

    void Update()
    {
        //リワード動画再生中にリクエストするとコールバックが呼ばれないことがあったので
        //isShowingRewardVideoで弾いている
        if( !IsLoadedRewardBasedVideo() && !isShowingRewardVideo )
        {
            delayedCallTimer -= Time.deltaTime;
            bool is_request_reward_video = false;
            if( delayedCallTimer < 0 )
            {
                is_request_reward_video = true;
                incrementDelayedCallTime();
            }

            if( is_request_reward_video )
            {
                requestRewardBasedVideoImpl();
            }
        }
    }
    
    void requestRewardBasedVideoImpl()
    {
        if( Application.internetReachability == NetworkReachability.NotReachable ) return;

        string ad_id;
#if UNITY_EDITOR
        ad_id = "unused";
#elif UNITY_ANDROID
        ad_id = adIdRewardAndroid;
#elif UNITY_IOS
        ad_id = adIdRewardIOS;
#else
        ad_id = "unexpected_platform";
#endif
        AdRequest request = new AdRequest.Builder().Build();
        rewardBasedVideo.LoadAd( request, ad_id );
    }

    //リクエスト待ち時間を初期値に戻す
    private void initDelayedCallTime()
    {
        delayedCallTimeTableIndex = 0;
        delayedCallTimer = delayedCallTimeTable[delayedCallTimeTableIndex];
    }

    //次に失敗したときに再度リクエストをするまでの待ち時間を設定
    private void incrementDelayedCallTime()
    {
        delayedCallTimeTableIndex = UnityEngine.Mathf.Min( delayedCallTimeTableIndex + 1, delayedCallTimeTable.Length - 1 );
        delayedCallTimer = delayedCallTimeTable[delayedCallTimeTableIndex];
    }

    public bool IsLoadedRewardBasedVideo()
    {
#if UNITY_EDITOR
        return true;
#else
        return rewardBasedVideo.IsLoaded();
#endif
    }

    public bool ShowRewardBasedVideo()
    {
#if UNITY_EDITOR
        HandleRewardBasedVideoRewarded(null,null);
        HandleRewardBasedVideoRewardedClosed(null,null);
        return true;
#else
        if (rewardBasedVideo.IsLoaded())
        {
            isShowingRewardVideo = true;
            rewardBasedVideo.Show();
            return true;
        }
        else
        {
            return false;
        }
#endif
    }

    public void HandleRewardBasedVideoLoaded(object sender, EventArgs args)
    {
        Debug.Log( "HandleRewardBasedVideoLoaded" );
#if UNITY_EDITOR
        HandleRewardBasedVideoRewarded(null,null);
        HandleRewardBasedVideoRewardedClosed(null,null);
#endif
    }

    // 報酬受け渡し処理
    public void HandleRewardBasedVideoRewarded(object sender, Reward args)
    {
        if( userCallbackOnReward != null )
        {
            userCallbackOnReward();
        }
    }
 
    public void HandleRewardBasedVideoRewardedClosed(object sender, System.EventArgs args)
    {
        if( userCallbackOnRewardClose != null )
        {
            userCallbackOnRewardClose();
        }

        isShowingRewardVideo = false;
        //リクエスト待ち時間を初期値に戻す
        initDelayedCallTime();
    }
 
    // ロード失敗時
    public void HandleRewardBasedVideoRewardedFailed(object sender, AdFailedToLoadEventArgs args)
    {
        Debug.Log( "Failed to load video reward : " + args.Message );
    }

    protected void HandleRewardBasedVideoLeavingApplication(object sender, System.EventArgs args)
    {
        Debug.Log("HandleRewardBasedVideoLeavingApplication");
    }
}