AdMob広告をC++のみで実装する(cocos2d-x & Firebase C++ SDK)

環境・バージョン

・cocos2d-x 1.6
・Firebase C++ SDK 4.3.0

導入

cocos2d-xを使ったアプリで広告を表示しようとすると、基本的にiOSとAndroidで別々の実装になることが多いと思います。
しかし、Firebase C++ SDKを使えばソースコードを統一して広告を表示できるので、実装してみました。

導入方法は ↓ のドキュメント通りに行えば出来たので、詳しくは説明しません。

[公式マニュアル] https://firebase.google.com/docs/admob/cpp/cocos2d-x?hl=ja

実装

作成したクラス

・AdvertiseManager
 アプリ内で広告に関する操作を管理するクラスです。
 このクラスは、Firebase C++ SDKとの依存度は低く、初期化と定義だけです。
・FirebaseHelper
 所謂便利クラスです。
 iOSとAndroidで別れてしまうユーティリティとかも入る想定です。
・BannerView
 バナーを表示するクラスです。
 Firebaseのバナー表示クラスも同名なので、クラス名は変更するべきです。
・InterstitialView
 インタースティシャルを表示するクラスです。
・RewardedVideoView
 動画広告を表示するクラスです。

AdvertiseManager

アプリ内で広告に関する操作を管理するクラスです。
AdMobに登録したアプリID・広告ユニットIDの定義、広告再取得のスレッドなどもここで行います。

initializeはAppDelegate.cppのapplicationDidFinishLaunchingで呼び出します。

ちなみに、mから始まる変数はメンバ変数、mpから始まる変数はポインターのメンバ変数です。

AdvertiseManager.cpp(一部抜粋)
#include "AdvertiseManager.h"
#include <cocos2d.h>
#include <math.h>

#include "advertise/FirebaseHelper.h"
#include "advertise/BannerView.h"
#include "advertise/InterstitialView.h"
#include "advertise/RewardedVideoView.h"

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID || CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
#include "firebase/admob.h"
#include "firebase/admob/types.h"
#include "firebase/app.h"
#include "firebase/future.h"
#endif

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include <jni.h>
#include <platform/android/jni/JniHelper.h>
#endif

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
const char* AdvertiseManager::ADMOB_APP_ID = "Androidアプリ ID";
const char* AdvertiseManager::ADMOB_BANNER_ID = "Android広告ユニット ID";
const char* AdvertiseManager::ADMOB_INTERSTITIAL_ID = "Android広告ユニット ID";
const char* AdvertiseManager::ADMOB_REWARDED_VIDEO_ID = "Android広告ユニット ID";
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
const char* AdvertiseManager::ADMOB_APP_ID = "iOSアプリ ID";
const char* AdvertiseManager::ADMOB_BANNER_ID = "iOS広告ユニット ID";
const char* AdvertiseManager::ADMOB_INTERSTITIAL_ID = "iOS広告ユニット ID";
const char* AdvertiseManager::ADMOB_REWARDED_VIDEO_ID = "iOS広告ユニット ID";
#else
const char* AdvertiseManager::ADMOB_APP_ID = "";
const char* AdvertiseManager::ADMOB_BANNER_ID = "";
const char* AdvertiseManager::ADMOB_INTERSTITIAL_ID = "";
const char* AdvertiseManager::ADMOB_REWARDED_VIDEO_ID = "";
#endif

USING_NS_CC;
using namespace chrono;

AdvertiseManager::AdvertiseManager() {}

AdvertiseManager::~AdvertiseManager() {
    mThreadEnd = true;
    mThread.join();
    delete mpBannerView;
    delete mpInterstitialView;
    delete mpRewardedVideoView;
}

// シングルトン
AdvertiseManager* AdvertiseManager::getInstance() {
    static AdvertiseManager instance;
    return &instance;
}

// AppDelegateでアプリ起動時に呼び出すこと
void AdvertiseManager::initialize() {

    // Firebase
#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
    // Initialize Firebase for Android.
    firebase::App* app = firebase::App::Create(firebase::AppOptions(), JniHelper::getEnv(), JniHelper::getActivity());
    // Initialize AdMob.
    firebase::admob::Initialize(*app, ADMOB_APP_ID);
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
    // Initialize Firebase for iOS.
    firebase::App* app = firebase::App::Create(firebase::AppOptions());
    // Initialize AdMob.
    firebase::admob::Initialize(*app, ADMOB_APP_ID);
#endif

    mpBannerView = new BannerView();
    mpInterstitialView = new InterstitialView();
    mpRewardedVideoView = new RewardedVideoView();

    mpBannerView->init(ADMOB_BANNER_ID);
    mpInterstitialView->init(ADMOB_INTERSTITIAL_ID);
    mpRewardedVideoView->init(ADMOB_REWARDED_VIDEO_ID);

    mpInterstitialView->setEvent(onShowCoveringAd);
    mpRewardedVideoView->setEvent(onShowCoveringAd);

    // 広告取得失敗後、一定時間たったら再取得するためのスレッド
    int intervalMilliSec = static_cast<int>(floor(Director::getInstance()->getAnimationInterval() * 1000));
    mThread = thread([&, intervalMilliSec]() {

        while (!mThreadEnd) {

            if (mIsUpdateEnable) {
                bool isShowAd = UserDefault::getInstance()->getBoolForKey("表示するかのキー");
                bool isShowVideo = UserDefault::getInstance()->getBoolForKey("表示するかのキー");

                mpBannerView->setEnable(isShowAd);
                mpInterstitialView->setEnable(isShowAd);
                mpRewardedVideoView->setEnable(isShowVideo);
            }

            mpBannerView->update();
            mpInterstitialView->update();
            mpRewardedVideoView->update();

            this_thread::sleep_for(milliseconds(intervalMilliSec));
        }

    });
}

void AdvertiseManager::updateEnable() {
    // mThreadのスレッドで処理
    mIsUpdateEnable = true;
}

void AdvertiseManager::showInterstitialImage() {
    mpInterstitialView->show();
}

void AdvertiseManager::showRewardedVideo(function<void()> rewardedCallback, function<void()> noLoadCallback, function<void()> closeCallback) {
    mpRewardedVideoView->setAdEvent(rewardedCallback, noLoadCallback, closeCallback);
    mpRewardedVideoView->show();
}

void AdvertiseManager::onShowCoveringAd() {
    // インタースティシャル・動画広告が表示された時にやりたい処理
}

FirebaseHelper

所謂便利クラスです。
公式マニュアルにも作るように書かれています。
自分は、AdRequestの情報も全広告で同じものなので、ここに書きました。

FirebaseHelper.cpp
#include "FirebaseHelper.h"

#if (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
#include "platform/android/jni/JniHelper.h"
#endif

USING_NS_CC;

firebase::admob::AdParent getAdParent() {
#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
    // Returns the iOS RootViewController's main view (i.e. the EAGLView).
    return (id)Director::getInstance()->getOpenGLView()->getEAGLView();
#elif (CC_TARGET_PLATFORM == CC_PLATFORM_ANDROID)
    // Returns the Android Activity.
    return JniHelper::getActivity();
#else
    // A void* for any other environments.
    return 0;
#endif
}

firebase::admob::AdRequest getAdRequest() {
    firebase::admob::AdRequest adRequest = {};

    // If the app is aware of the user's gender, it can be added to the
    // targeting information. Otherwise, "unknown" should be used.
    adRequest.gender = firebase::admob::kGenderUnknown;

    // The user's birthday, if known. Note that months are indexed from one.
    adRequest.birthday_day = 1;
    adRequest.birthday_month = 1;
    adRequest.birthday_year = 2000;

    // Additional keywords to be used in targeting.
    static const char* kKeywords[] = {};
    adRequest.keyword_count = 0;
    adRequest.keywords = kKeywords;

    // "Extra" key value pairs can be added to the request as well.
    static const firebase::admob::KeyValuePair kRequestExtras[] = {};
    adRequest.extras_count = 0;
    adRequest.extras = kRequestExtras;

    // Register the device IDs associated with any devices that will be used to
    // test your app. Below are sample test device IDs used for making the ad request.
    static const char* kTestDeviceIDs[] = {
        "テスト用のDeviceIdを入れる。",
    };
    adRequest.test_device_id_count = sizeof(kTestDeviceIDs) / sizeof(kTestDeviceIDs[0]);
    adRequest.test_device_ids = kTestDeviceIDs;

    return adRequest;
}

BannerView

バナーを表示するクラスです。
Firebaseのバナー表示クラスも同名なので、クラス名は変更するべきです。(わかってて変えてない自分はバカです。)

OnCompletionのリスナーは、直前に対象の処理を行ってから設定するようにしています。
最初、リスナーだけ最初にセットしてInitialize,LoadAdを後から呼び出したらOnCompletionが呼ばれなくてハマりました。

すべての広告で共通ですが、広告を再ロードするときはInitializeからやるようにしています。
LoadAdからでも問題がなくても、Initializeからやった方が個人的に安心するので。

BannerView.cpp
#include "BannerView.h"
#include "FirebaseHelper.h"

#include "firebase/admob.h"
#include "firebase/admob/types.h"
#include "firebase/app.h"
#include "firebase/future.h"

BannerView::~BannerView() {
    if (mpBannerView != nullptr) {
        delete mpBannerView;
    }
}

void BannerView::init(const string& unitId) {
    mUnitId = unitId;
    mAdRequest = getAdRequest();
    initView();
}

void BannerView::setEnable(bool isEnable) {
    mIsEnable = isEnable;
}

void BannerView::update() {

    if (mIsEnable) {
        if (mIsLoaded && !mIsShow) {
            mIsShow = true;
            mpBannerView->Show();
        }
    } else {
        if (mIsShow) {
            mIsShow = false;
            mpBannerView->Hide();
        }
    }

    // 一定フレームがたったら広告を再取得するようにViewを初期化する
    if (0 < mMakeFrameRemainCount) {
        --mMakeFrameRemainCount;
        if (mMakeFrameRemainCount <= 0) {
            initView();
        }
    }
}

void BannerView::initView() {
    firebase::admob::AdSize ad_size;
    ad_size.ad_size_type = firebase::admob::kAdSizeStandard;
    ad_size.width = 320;
    ad_size.height = 50;

    // my_ad_parent is a reference to an iOS UIView or an Android Activity.
    // This is the parent UIView or Activity of the banner view.
    mpBannerView = new firebase::admob::BannerView();
    mpBannerView->Initialize(getAdParent(), mUnitId.c_str(), ad_size);
    mpBannerView->InitializeLastResult().OnCompletion(onCompletionInitialize, this);
}

void BannerView::onCompletionInitialize(const firebase::Future<void>& future, void* user_data) {
    BannerView* pBannerView = static_cast<BannerView*>(user_data);
    if (future.error() == firebase::admob::kAdMobErrorNone) {
        // 初期化後ではないとMoveToがうまく動かないみたい...
        pBannerView->mpBannerView->MoveTo(firebase::admob::BannerView::Position::kPositionBottom);
        pBannerView->mpBannerView->LoadAd(pBannerView->mAdRequest);
        pBannerView->mpBannerView->LoadAdLastResult().OnCompletion(onCompletionLoadAd, pBannerView);
    } else {
        // 広告を再取得するまでのフレーム数をセット
        pBannerView->mMakeFrameRemainCount = ADVERTISE_REMAKE_FRAME;
    }
}

void BannerView::onCompletionLoadAd(const firebase::Future<void>& future, void* user_data) {
    BannerView* pBannerView = static_cast<BannerView*>(user_data);
    if (future.error() == firebase::admob::kAdMobErrorNone) {
        pBannerView->mIsLoaded = true;
    } else {
        // 広告を再取得するまでのフレーム数をセット
        pBannerView->mMakeFrameRemainCount = ADVERTISE_REMAKE_FRAME;
    }
}

InterstitialView

インタースティシャルを表示するクラスです。

コメントにも書いてますが、Showした後にするinitViewを叩いていたら、広告がAndroidでは表示出来て、iOSでは表示されませんでした。
おそらく、Androidは広告が即表示なのに対し、iOSはアニメーションで広告が表示されるので、Showで表示する広告をiOSはまだ参照していて、Initializeで消されるのではないかと思います。

InterstitialView.cpp
#include "InterstitialView.h"
#include "FirebaseHelper.h"

#include "firebase/admob.h"
#include "firebase/admob/types.h"
#include "firebase/app.h"
#include "firebase/future.h"

InterstitialView::~InterstitialView() {
    if (mpInterstitialAd != nullptr) {
        delete mpInterstitialAd;
    }
}

void InterstitialView::init(const string& unitId) {
    mUnitId = unitId;
    mAdRequest = getAdRequest();
    initView();
}

void InterstitialView::setEnable(bool isEnable) {
    mIsEnable = isEnable;
}

void InterstitialView::setEvent(const function<void()>& onShow) {
    mOnShow = onShow;
}

void InterstitialView::update() {
    if (!mIsEnable) {
        return;
    }

    // 広告を取得するフレームmMakeFrameRemainCountで制御
    if (!mIsLoaded) {
        if (0 < mMakeFrameRemainCount) {
            --mMakeFrameRemainCount;
            if (mMakeFrameRemainCount <= 0) {
                initView();
            }
        }
    }
}

void InterstitialView::show() {
    if (!mIsEnable) {
        return;
    }

    if (mIsLoaded) {
        mIsLoaded = false;
        mpInterstitialAd->Show();
        mpInterstitialAd->ShowLastResult().OnCompletion(onCompletionShow, this);

        // ShowLastResult().OnCompletionで次のロードを行う処理をする。
        // ここでinitViewしてはいけない。iOSは表示しようとする広告を消して初期化が走るっぽい。→広告が表示されなくなる。

        if (mOnShow) {
            mOnShow();
        }
    } else if (0 < mMakeFrameRemainCount) {
        loadNextFrame();
    }
}

void InterstitialView::initView() {

    // my_ad_parent is a reference to an iOS UIView or an Android Activity.
    // This is the parent UIView or Activity of the banner view.
    if (mpInterstitialAd != nullptr) {
        delete mpInterstitialAd;
    }
    mpInterstitialAd = new firebase::admob::InterstitialAd();
    mpInterstitialAd->Initialize(getAdParent(), mUnitId.c_str());
    mpInterstitialAd->InitializeLastResult().OnCompletion(onCompletionInitialize, this);
}

void InterstitialView::loadNextFrame() {
    mMakeFrameRemainCount = 1;
}

void InterstitialView::onCompletionInitialize(const firebase::Future<void>& future, void* user_data) {
    InterstitialView* pInterstitialView = static_cast<InterstitialView*>(user_data);
    if (future.error() == firebase::admob::kAdMobErrorNone) {
        pInterstitialView->mpInterstitialAd->LoadAd(pInterstitialView->mAdRequest);
        pInterstitialView->mpInterstitialAd->LoadAdLastResult().OnCompletion(onCompletionLoadAd, pInterstitialView);
    } else {
        pInterstitialView->mMakeFrameRemainCount = ADVERTISE_REMAKE_FRAME;
    }
}

void InterstitialView::onCompletionLoadAd(const firebase::Future<void>& future, void* user_data) {
    InterstitialView* pInterstitialView = static_cast<InterstitialView*>(user_data);
    if (future.error() == firebase::admob::kAdMobErrorNone) {
        pInterstitialView->mIsLoaded = true;
        pInterstitialView->mMakeFrameRemainCount = 0;
    } else {
        pInterstitialView->mMakeFrameRemainCount = ADVERTISE_REMAKE_FRAME;
    }
}

void InterstitialView::onCompletionShow(const firebase::Future<void>& future, void* user_data) {
    InterstitialView* pInterstitialView = static_cast<InterstitialView*>(user_data);
    if (future.error() == firebase::admob::kAdMobErrorNone) {
        pInterstitialView->loadNextFrame();
    } else {
        pInterstitialView->mMakeFrameRemainCount = ADVERTISE_REMAKE_FRAME;
    }
}


RewardedVideoView

動画広告を表示するクラスです。

LoggingRewardedVideoListenerという内部クラスを作っています。
バナーとインタースティシャルと違い、報酬獲得時と画面を閉じた時に処理をしたかったので、これだけListenerクラスを使ってイベントを拾っています。

動画広告はユーザ始動で表示することが多いので、広告をロードできていない時のコールバックも用意してます。

動画広告は一度Initializeすれば次はLoadAdからで問題ないです。(Initialize2度目でエラーです。)
ですが、個人的にInitializeからやると安心するので、mIsFirstInitedなんてバカみたいなフラグ作ってます。
まぁデストラクタにも役立ってますが。

RewardedVideoView.cpp
#include "RewardedVideoView.h"
#include "FirebaseHelper.h"

#include "firebase/admob.h"
#include "firebase/admob/types.h"
#include "firebase/app.h"
#include "firebase/future.h"

RewardedVideoView::~RewardedVideoView() {
    if (mIsFirstInited) {
        firebase::admob::rewarded_video::Destroy();
    }
}

void RewardedVideoView::init(const string& unitId) {
    mUnitId = unitId;
    mAdRequest = getAdRequest();
    mListener.pAdView = this;
    initView();
}

void RewardedVideoView::setEnable(bool isEnable) {
    mIsEnable = isEnable;
}

void RewardedVideoView::setEvent(const function<void()>& onShow) {
    mOnShow = onShow;
}

void RewardedVideoView::update() {
    if (!mIsEnable) {
        return;
    }

    if (!mIsLoaded) {
        if (0 < mMakeFrameRemainCount) {
            --mMakeFrameRemainCount;
            if (mMakeFrameRemainCount <= 0) {
                initView();
            }
        }
    }
}

void RewardedVideoView::setAdEvent(function<void()> rewardedCallback, function<void()> noLoadCallback, function<void()> closeCallback) {
    mRewardedCallback = rewardedCallback;
    mCloseCallback = closeCallback;
    mNoLoadCallback = noLoadCallback;
}

void RewardedVideoView::show() {
    if (!mIsEnable) {
        return;
    }

    if (mIsLoaded) {
        mIsLoaded = false;
        firebase::admob::rewarded_video::Show(getAdParent());
        if (mOnShow) {
            mOnShow();
        }
    } else {
        mNoLoadCallback();
        if (0 < mMakeFrameRemainCount) {
            loadNextFrame();
        }
    }
}

void RewardedVideoView::initView() {

    // Initializeを2回打つとエラーになるので、Destroyする
    if (mIsFirstInited) {
        firebase::admob::rewarded_video::Destroy();
    }
    mIsFirstInited = true;

    firebase::admob::rewarded_video::Initialize();
    firebase::admob::rewarded_video::InitializeLastResult().OnCompletion(onCompletionInitialize, this);
}

void RewardedVideoView::loadNextFrame() {
    mMakeFrameRemainCount = 1;
}

void RewardedVideoView::onCompletionInitialize(const firebase::Future<void>& future, void* user_data) {
    RewardedVideoView* pRewardedVideoView = static_cast<RewardedVideoView*>(user_data);
    firebase::admob::rewarded_video::SetListener(&(pRewardedVideoView->mListener));
    if (future.error() == firebase::admob::kAdMobErrorNone) {
        firebase::admob::rewarded_video::LoadAd(pRewardedVideoView->mUnitId.c_str(), pRewardedVideoView->mAdRequest);
        firebase::admob::rewarded_video::LoadAdLastResult().OnCompletion(onCompletionLoadAd, pRewardedVideoView);
    } else {
        pRewardedVideoView->mMakeFrameRemainCount = ADVERTISE_REMAKE_FRAME;
    }
}

void RewardedVideoView::onCompletionLoadAd(const firebase::Future<void>& future, void* user_data) {
    RewardedVideoView* pRewardedVideoView = static_cast<RewardedVideoView*>(user_data);
    if (future.error() == firebase::admob::kAdMobErrorNone) {
        pRewardedVideoView->mIsLoaded = true;
        pRewardedVideoView->mMakeFrameRemainCount = 0;
    } else {
        pRewardedVideoView->mMakeFrameRemainCount = ADVERTISE_REMAKE_FRAME;
    }
}

void RewardedVideoView::LoggingRewardedVideoListener::OnRewarded(firebase::admob::rewarded_video::RewardItem reward) {
    pAdView->mRewardedCallback();
}

void RewardedVideoView::LoggingRewardedVideoListener::OnPresentationStateChanged(firebase::admob::rewarded_video::PresentationState state) {
    if (state == firebase::admob::rewarded_video::PresentationState::kPresentationStateHidden) {
        pAdView->mCloseCallback();
        pAdView->loadNextFrame();
    }
}

最後に

割と自分のアプリ仕様のための処理が入っているので、良しなに読み替えて見てください。

AdMobメディエーションは未実装です。
というか、確認してませんがたぶん非対応なんじゃないかと思います。
マニュアルに記述がないし、(多くは見てませんが)他アドネットワークのSDKにcocos2d-x用はあってもC++用なんて見たことないので。

最後にこの話をするのはとても卑怯なのですが言わせてください。
訳あって、これを実装した後に別のアドネットワークを表示することにしたので、このソースコードは日の目を見ずにお蔵入りすることになりました(T_T) そこそこハマりどころがあって苦労したので残念です(´-ω-`)
一応、本番運用実績はなくとも、appleのアプリ審査には通りました!

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.