martes, 3 de febrero de 2015

Integrating iAd in Cocos2d-x v3.x

Hi all!

First of all, Happy New Year! (better late than never, right?). Sorry for the delay, but I've really been busy this last month, and Christmas didn't help either... 

Today I want to share with you what I've been doing these last days. In the next game I'm working on, I wanted to include a free version with advertisements, but I had no experience on this task. The thing turned out to be quite simple (at least in iOS, which is the first target I have in mind), as the iAd Framework is intuitive. However, I had to face an additional challenge: integrating this framework with another framework I'm building my game upon, Cocos2d-x. The main contributions that helped me achieve this goal are this and this.

So, first of all, we need to understand the model that iAd Framework follows. In a nutshell, you need to create an interface that follows a protocol (which is the equivalent in Objective-C to a Java interface), that is, that should implement certain methods. The name of the protocol is AdBannerViewDelegate. So let us call our interface AdBanner and let us create a header file with it:

AdBanner.h
 #import <Foundation/Foundation.h>  
 #import <iAd/iAd.h>    
 @interface AdBanner : NSObject<ADBannerViewDelegate>  

The methods that we should implement are listed next, together with a brief description: 

- (void)bannerViewDidLoadAd:(ADBannerView *)banner

This method is called when a banner view has a new ad to display.

- (BOOL) bannerViewActionShouldBegin:(ADBannerView *)banner willLeaveApplication:(BOOL) willLeave

This method is called when the user taps the banner and should return YES if the banner action should execute. If willLeave=YES, then another app will be launched to execute the action, and no additional actions are required; NO if the action is going to be executed inside the game, so we should pause the activities that require interaction with the game.

- (void)bannerViewActionDidFinish: (ADBannerView *) banner


This method is called after the user has navigated the ad. It must resume activities if bannerViewActionShouldBegin: paused the gameplay.


- (void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error


This method is called if any error occurs. 

Note that the arguments refer to an ADBannerView object, which is the object that can contain and will show ads. The first thing therefore is to create an ADBannerView object and to include it in the hierarchy of views. Note that in a Cocos2d-x game, we typically have just one view, an EAGLView, which is a subclass of UIView that renders an OpenGL scene.  

In order to add the banner view, we need first to gain access to the RootViewController, that is, the object in charge of managing the different views. Also, we will need to access the size of the screen so as to place the banner, and therefore, we are going to obtain a UIWindow object that can provide that information. Finally, we want to add a boolean value to determine whether the banner view is currently visible or not. The resulting header and implementation files are presented next:

AdBanner.h
 #import <Foundation/Foundation.h>  
 #import <iAd/iAd.h>  
 @class RootViewController;   
 @interface AdBanner : NSObject<ADBannerViewDelegate>  
 {  
   UIWindow* window;  
   RootViewController* rootViewController;  
   ADBannerView* adBannerView;  
   bool adBannerViewIsVisible;  
 }  


AdBanner.mm (Note that the extension of Objective-C files are .m. However, as we will have to integrate with C++, we rename the file to .mm, which is an Objective-C++ file)
 -(id)init  
 {  
   if(self=[super init])  
   {  
     adBannerViewIsVisible = YES;  
     rootViewController =  
       (RootViewController*) [[[UIApplication sharedApplication] keyWindow] rootViewController];  
     window = [[UIApplication sharedApplication] keyWindow];  
     [self createAdBannerView];  
   }  
   return self;  
 }  
 - (void)createAdBannerView  
 {  
   adBannerView = [[ADBannerView alloc] initWithFrame:CGRectZero];  
   adBannerView.delegate = self;  
   [adBannerView setAutoresizingMask:UIViewAutoresizingFlexibleWidth];  
   CGRect adFrame = adBannerView.frame;  
   adFrame.origin.x = 0.0f;  
   adFrame.origin.y = window.bounds.size.height;  
   adBannerView.frame = adFrame;  
   [[rootViewController view] addSubview: adBannerView];  
 }  

In init, we state that the banner view should be visible and we retrieve the root view controller and the main window. Then, we call createAdBannerView, which creates and initializes the adBannerView object. The next step is very important: we state that the actual delegate of the adBannerView is the current object; this means that when the adBannerView object changes, it will notify our object by calling the methods of the protocol previously explained. The following method (setAutoresizingMask:) indicates that if the superview changes, the width of the banner view can be freely re-sized. This is important if you want to support changes in orientation for example. Then, we specify that the subview should be hidden, right after the bottom of the screen. Note that while the origin of the coordinates system in Cocos2d-x is in the bottom-left corner and the y component grows upwards, the origin of the UIView class is in the top-left corner, and the y component grows downwards. So, when we write:

adFrame.origin.y = window.bounds.size.height;

, we are placing the origin of the UIView on the bottom-left corner of the screen, and the banner would grow downwards. That's why it would be initially hidden. 

Finally, we add the banner view to the views hierarchy through the root view controller object. 

Now, we are going to add the protocol methods in the implementation file.

AdBanner.mm
- (BOOL)bannerViewActionShouldBegin:(ADBannerView *)banner willLeaveApplication:(BOOL)willLeave  
 {  
   NSLog(@"Banner view is beginning an ad action");  
   if ( !willLeave )  
   {  
     //insert code here to suspend any services that might conflict with the advertisement  
   }  
   return YES;  
 }  

 - (void)bannerViewActionDidFinish: (ADBannerView *) banner  
 {  
   //if necessary, insert code here to resume services suspended by the previous method  
 }  
 - (void)bannerViewDidLoadAd:(ADBannerView *)banner  
 {  
   NSLog(@"New ad available");  
   [self layoutAnimated:YES]; //shows the banner with a pop-up animation 
 }  
 
 -(void)bannerView:(ADBannerView *)banner didFailToReceiveAdWithError:(NSError *)error  
 {  
   NSLog( @"%@", error.description );  
   [self layoutAnimated: YES];  //hides the banner with an animation
 }  

Note that both bannerViewDidLoadAd: and bannerView: didFailToReceiveAdWithError: call a third instance method layoutAnimated:, which is not part of the protocol. This method is listed next:

AdBanner.mm
//... protocol methods above... 
-(void)layoutAnimated:(BOOL)animated  
 {  
   CGRect bannerFrame = adBannerView.frame;  
   //Has the banner an advestiment?  
   if ( adBannerView.bannerLoaded && adBannerViewIsVisible )  
   {  
     NSLog(@"Banner has advertisement");  
     bannerFrame.origin.y = window.bounds.size.height - bannerFrame.size.height;  
   } else  
   {  
     NSLog( @"Banner has NO advertisement" );  
     //if no advertisement loaded, move it offscreen  
     bannerFrame.origin.y = window.bounds.size.height;  
   }  
   [UIView animateWithDuration:animated ? 0.25 : 0.0 animations:^{  
     [rootViewController.view layoutIfNeeded];  
     adBannerView.frame = bannerFrame;  
   }];  
 }  

The code is more or less self-explanatory. We first check whether the banner view has actually an advertisement and whether it is visible. If it is, we place it on the bottom of the screen; otherwise, we place it offscreen (right after the bottom). We then call the static method animateWithDuration provided by UIView in order to show a simple pop-up animation.

Integrating with Cocos2d-x

Let's recap. Now, we have two files: AdBanner.h and AdBanner.mm, which encapsulates an AdBanner object capable of showing and hiding an ad banner view object. On the other hand, we have a Cocos2d-x scene onto which we would like to show the ad banner.

I'm going to show you a real example. In the new game I'm working on, I want to display the ad banner in the menu screen, which is a cocos2d:Layer, as depicted next:

MenuScreen.h
 #include "cocos2d.h"  
 class MenuScreen: public cocos2d::Layer  
 {  
 public:  
   static cocos2d::Scene* CreateScene();  
   virtual bool init();   
   CREATE_FUNC( MenuScreen );  
 private:  
   cocos2d::Size visibleSize;  
   cocos2d::Point origin;  
   cocos2d::Sprite *mBackground;  
   cocos2d::MenuItemImage *mPlayButton;  
   cocos2d::MenuItemImage *mSoundConfigButton;  
   cocos2d::MenuItemImage *mCreditsButton;  
   cocos2d::MenuItemImage *mGameCenterButton;  
   void ToNewGame();  
   void SoundConfig();  
   void ToCredits();  
   void ToGameCenter();  
 };  

It would be wonderful if I could do the following:

MenuScreen.h
#include "cocos2d.h"
#include "AdBanner.h" //THIS WON'T WORK!
 class MenuScreen: public cocos2d::Layer  
 {  
 public:  
   static cocos2d::Scene* CreateScene();  
   virtual bool init();   
   CREATE_FUNC( MenuScreen );  
 private:  
   cocos2d::Size visibleSize;  
   cocos2d::Point origin;  
   cocos2d::Sprite *mBackground;  
   cocos2d::MenuItemImage *mPlayButton;  
   cocos2d::MenuItemImage *mSoundConfigButton;  
   cocos2d::MenuItemImage *mCreditsButton;  
   cocos2d::MenuItemImage *mGameCenterButton;  
   void ToNewGame();  
   void SoundConfig();  
   void ToCredits();  
   void ToGameCenter();  
   AdBanner* adBanner; //THIS WON'T WORK!
 };  

That is, adding the header of AdBanner and defining a pointer to an AdBanner object in the class. However, as mentioned in the comments, this won't compile, because MenuScreen.h will be eventually included in its implementation, which is a .cpp file that would complain as soon as it finds Objective-C syntax (e.g. @interface).

The solution is to create an intermediate C++ object that mediates between C++ and Objective-C. For this purpose, let us create a class called AdBannerC, just like this:

AdBannerC.h
 #import "AdBanner.h";  
 class AdBannerC  
 {  
 public:  
   AdBannerC();  
   ~AdBannerC();  
 private:  
   AdBanner* impl;  
 };  

AdBannerC.mm
 #include "AdBannerC.h"  
 AdBannerC::AdBannerC()  
 {  
   impl = [[AdBanner alloc] init];  
 }    
 AdBannerC::~AdBannerC()  
 {  
   [impl removeView];  
 }   

Now, it would seem reasonable to include AdBanncerC.h in MenuScreen.h and write:

AdBannerC* adBanner = new AdBannerC();

However, THIS DOESN'T WORK! Why? Because we have said before that a .cpp file (well, to be more accurate, the C++ compiler) does not understand Objective-C syntax. If we include AdBannerC.h in MenuScreen.h, AdBanner.h is also being included, and AdBanner.h has Objective-C syntax!

The trick that we can apply here is commonly called the PIMPL idiom and it exploits the fact that a forward declaration of a type is enough to declare a pointer to that type. I'll explain myself. Consider this alternative definition of the AdBannerC class:

AdBannerC.h
 struct AdBannerImpl; //Forward declaration
 class AdBannerC  
 {  
 public:  
   AdBannerC();  
   ~AdBannerC();   
 private:  
   AdBannerImpl* impl;  
 };  

As you can see, now we don't have any imports, so any .cpp file can safely include this header file and won't find any strange Objective-C syntax. We have done a so-called forward declaration, meaning that we are telling the compiler that the type AdBannerImpl exists in some implementation file. This is enough for the compiler to let us define a pointer to this type. A struct in C++ is equivalent to a class, except that default values of its members are public.

The implementation file would be as follows:

AdBannerC.mm
 #include "AdBannerC.h"  
 #import "AdBanner.h"  
 struct AdBannerImpl  
 {  
   AdBanner* wrapped;  
 };  
 AdBannerC::AdBannerC()  
 {  
   impl = new AdBannerImpl();  
   impl -> wrapped = [[AdBanner alloc] init];  
 }  
 AdBannerC::~AdBannerC()  
 {  
   [impl -> wrapped removeView];  
   delete impl;  
 }  

Now, any scene in the game can create a banner view by just importing AdBannerC.h and doing:

adBanner = new AdBannerC();

Done!

However, as we mentioned earlier, sometimes it might be necessary to pause and resume the gameplay when the player interacts with the banner. How can we accomplish this? Easy: we just need to pass an instance of the scene to the banner object and let this banner object call the appropriate method of the scene. So, the first step is to create these methods:

MenuScreen.h
 class MenuScreen: public cocos2d::Layer  
 {  
 public:  
   // there's no 'id' in cpp, so we recommend returning the class instance pointer  
   static cocos2d::Scene* CreateScene();  
   // Here's a difference. Method 'init' in cocos2d-x returns bool, instead of returning 'id' in cocos2d-iphone  
   virtual bool init();  
   void PauseGame();  //It pauses the current activities
   void ResumeGame(); //It resumes the activities
   //rest of class definition
}; 

MenuScreen.cpp
 void MenuScreen::PauseGame()  
 {  
   Director::getInstance() -> stopAnimation();  
 }  
 void MenuScreen::ResumeGame()  
 {  
   Director::getInstance() -> startAnimation();  
 }  

In order to pause and resume the game, we simply call the corresponding methods provided by the cocos2d::Director class.

Next step is passing the instance of the scene to the ad banner object. For this purpose, we need to modify slightly the AdBannerC class to make it accept an instance of MenuScreen:

AdBannerC.h
 class MenuScreen; 
 struct AdBannerImpl;
 class AdBannerC  
 {  
 public:  
   AdBannerC( MenuScreen& ms );  
   ~AdBannerC();   
 private:  
   AdBannerImpl* impl;  
 };  

AdBannerC.mm
 #include "AdBannerC.h"  
 #import "AdBanner.h"  
 struct AdBannerImpl  
 {  
   AdBanner* wrapped;  
 };  
 AdBannerC::AdBannerC( MenuScreen& ms )  
 {  
   impl = new AdBannerImpl();  
   impl -> wrapped = [[AdBanner alloc] initWithMenuInstance: ms];  
 }  
 AdBannerC::~AdBannerC()  
 {  
   [impl -> wrapped removeView];  
   delete impl;  
 }  

Thus, in MenuScreen.cpp, we would do the following:

AdBannerC *adBanner = new AdBannerC( *this );

Note that we have changed the initialization of impl->wrapped by using a new method called initWithMenuInstance:. Therefore, we need to define this method in AdBanner:

AdBanner.h
 #import <Foundation/Foundation.h>  
 #import <iAd/iAd.h>  
 @class RootViewController;  
 class MenuScreen;  
 @interface AdBanner : NSObject<ADBannerViewDelegate>  
 {  
   UIWindow* window;  
   RootViewController* rootViewController;  
   ADBannerView* adBannerView;  
   bool adBannerViewIsVisible;  
   MenuScreen* ms;   
   bool needToRestore;  
 }  
 -(id)initWithMenuInstance: (MenuScreen&) ms;   
 @end  

In addition to the new method, we have also added a new variable: needToRestore. This boolean value will tell us whether we need to resume the gameplay depending on whether our app was moved to the background or not.

AdBanner.mm
 #import "RootViewController.h"  
 #import "AdBanner.h"  
 #import "MenuScreen.h"  
 @implementation AdBanner  
 -(id)initWithMenuInstance: (MenuScreen &) msVar  
 {  
   ms = &msVar;  
   needToRestore = NO;  
   return [self init];  
 }    
 - (BOOL)bannerViewActionShouldBegin:(ADBannerView *)banner willLeaveApplication:(BOOL)willLeave  
 {  
   NSLog(@"Banner view is beginning an ad action");  
   if ( !willLeave )  
   {  
     //Given that we are pausing the gameplay, we need to restore it later.
     needToRestore = YES;  
     // insert code here to suspend any services that might conflict with the advertisement  
     ms -> PauseGame();  
   }  
   return YES;  
 }  
 - (void)bannerViewActionDidFinish: (ADBannerView *) banner  
 {  
   if( needToRestore )  
   {  
     ms -> ResumeGame();  
   }  
   needToRestore = NO;  
 }  

//... rest of methods

And that's it! Wow! This post has been longer than I expected, but I hope that you found it interesting and useful for your own projects.

Some final thoughts:

  • In order to make sure that ad banner objects are actually deleted, activate Automatic Reference Counting (ARC) for the .mm files. Check here for how this is done. 
  • Always prefer smart pointers over raw pointers.
  • The solution I propose here is fine when you want to display an ad banner in just two or three scenes at most. If you will have lots of scene in your game showing ad banners, you should generalize all the scenes that may show ads into a superclass that implements the methods ResumeGame() and PlayGame(), and pass an instance of this superclass to the banner object. 

Thanks for your reading and don't hesitate to ask or correct if that's the case!
See you :)

3 comentarios: