Design Talks - Ad Managers

In this post I want to discuss the concept of Ad Managers and specifically how I manage my Ad code.
 
Manager?
The title uses the work ‘manager’ and that implies a lot of code, features, and complexity. 
 
My actual code has more features than I’m showing (below) but is still pretty slim. 
 
I have found that complexity in ad ‘managers’ rarely pays back dividends and often just creates trouble/bugs.
 
 
Motivation For This Post
I have seen a lot of confusion in the forums about how to successfully and consistently show Ads in a game.
 
I found the repetition somewhat weird until I realized what the problem(s) were.  As always, I think it comes down to understanding basic principles and an understanding of issues related to the task at hand.   Then, one need to have the ability to write code to deal with the issue(s).  
 
 
The Most Common Issue - Event Programming is Not Procedural/Imperative Programming
 
 
As I said above, I have seen innumerable posts where users are completely baffled as to how ads and ad code work.
 
In some of these posts, the OP is simply confused about how to get an ID or where to use it.  i.e. S/he needs to read the docs more closely or is confused by what the docs say.  I’m not addressing this issue in today’s post.
 
The biggest issue I have seen is that people new to game development, programming, and or new to ads simply don’t understand that ad code is  asynchronous.  They don’t understand you cannot write code like this and have it work consistently:

local provider = require "plugins.someprovider" provider.init( ... ) provider.load( adType ) provider.show( adType )

Users do this, because they are used to procedural/imperative programming where init() finishes, then load() finishes, then show() finishes.   This is both naive and very wrong.

Unfortunately, each of these functions merely generates a network request and move on, so the implied dependency fails.

On top of the lack of understanding of event-driven programming, these users simply don’t know how to code the ad listener nor do they know where to put it.
 
So, I often seem them:

  • Not code one.
  • Code one that does nothing useful.
  • Put it in main.lua then struggle to make use of it.

UPDATE: I also often see users confused about the fact that you can’t test ads in the simulator.  I have a solution for this.
 
 
 
Over The Years
Before I get to my current approaches to handing ads, I wanted to mention that I have tried many solutions over the year.  Additionally, back in the day before aggregator and mediators like Appodeal, one had to write one’s own ad mediation solution.  i.e. If you wanted more than one ad network and you wanted to ‘use the best one at the best time’, you had to work it out on your own.
 
Today, new users are very lucky to have companies whose whole job it is to mediate ad sources and bring you the best bidders and ensure you get ads.  
 
Before, one had to hunt far and wide for solutions or code one’s own.  Speaking to the prior, I know there are people in this community who once wrote their own mediation and aggregation solutions.  I sincerely hope that some of those folks read this post and reply with links to their own code and or post their own insights.
 
I learned from them and would love to learn more.
 
 
My Current Approaches
OK, so what do I do?   Well, my current approach is pretty hands off and simple.
 
First, I create a standalone module or related-modules set that contain all of my ad code.
 
Second, in this module, I add a thin layer on top of the provider I am using.
 
Third, instead of using a single listener, I have a listener hierarchy and/or a phase distributor (for lack of a better word).
 
 
Skeleton of Current Real Solution
The following is a simplified/reduced copy of one of my current ad manager.  The real one adds more features than I’m showing here to make it much more robust and easier to use. 
 
Still this slimmer version shows the basic idea.

local helpers = require "scripts.ads.helpers" local fakeAds = require "scripts.ads.fake" -- -- SETTINGS VARIABLES HERE local initDelay = 30 -- wait 30 ms then start init process -- IDs (may be more sophisticated but this is the gist) local androidID = "yadayada" local iosID = "yadayada" local os = helpers.os() local id = (os == "android") and androidID or iosID -- ... more settings local m = {} local temporaryListeners = {} local function listener( event ) for key, aListener in pairs( temporaryListeners ) do aListener( event ) end end -- == -- Helper to call init (allows me to defer it slightly after loading module -- == local function doInit() local function initListener( event ) -- body not shown for brevity end m.listen( "init", initListener ) -- local someProvider = require( "plugin.someProvider" ) someProvider.init( listener, lparams ) end -- ============================================================= -- Temporary listener helpers. -- ============================================================= function m.listen( name, aListener ) temporaryListeners[name] = aListener end function m.ignore( name ) temporaryListeners[name] = nil end function m.ignoreAll() temporaryListeners = {} end -- ============================================================= -- Expose isLoaded() and load() from provider -- ============================================================= function m.isLoaded( adType ) -- body not shown for brevity end function m.load( adType ) -- body not shown for brevity end -- ============================================================= -- Custom Show Functions -- ============================================================= function m.showBanner( position, placement ) -- body not shown for brevity end function m.showInterstitial( onComplete, placement ) -- body not shown for brevity end function m.showRewarded( onSuccess, onFailure, placement ) -- body not shown for brevity end -- ============================================================= -- Hide Banner Helper -- ============================================================= function m.hideBanner() -- body not shown for brevity end -- ============================================================= -- Banner height -- ============================================================= function m.height() -- body not shown for brevity end   -- ============================================================= -- Initialize ads as last step of 'preparing' the module -- ============================================================= if( helpers.onSim() == false ) then timer.performWithDelay( initDelay, doInit end return m

 
The Parts
The key parts of the above module are:

  • doInit() - A helper function that is called using timer.performWithDelay() after the module is loaded.
  • isLoaded(), load(), … - I usually expose some of the provider’s functions because they are useful for making decisions later.  As I rule, I only expose features I absolutely need.
  • The Show Functions - I expose the show functionality for each ad type separately because then I can write custom show code in the module function itself that does all the heavy lifting.  This allows me to write thinner game code later.
    • showBanner( position, placement ) - Banner show.
    • showInterstitial( onComplete, placement ) - Interstitial show.
      • If passed in, ‘onComplete()’ is called when add is hidden/closed.
    • showRewarded( onSuccess, onFailure, placement ) - Rewarded video show.
      • If passed in, ‘onSuccess()’ is if the user is considered to have watched the ad.
      • If passed in, ‘onFailure()’ is if the user skips the ad or otherwise does not meet the ‘has watched’ criteria.
  • hideBanner() - Hide banner ad.
  • bannerHeight() - Get height of ad.   Not all providers supply this feature, but my modules all do for consistency.  I just return 0 for cases where there is no possible return.

Specialized Listeners
You may be wondering, what these features are:

  • listen( name, aListener ) - Adds a temporary listener that is called whenever ad events come in.
  • ignore( name ) - Removes a named temporary listener.
  • ignoreAll() - Removes all temporary listeners.

These functions allows me to do this kind of thing:

local applovinHelper = require "scripts.ads.helpers.applovin" local function initListener( event ) local isError = event.isError local phase = event.phase -- if( phase == "init" ) then applovinHelper.ignore( "init" ) if( isError ) then return else applovinHelper.load( "banner" ) end end end m.listen( "init", initListener ) local function loadListener( event ) local isError = event.isError local phase = event.phase -- if( phase == "loaded" ) then applovinHelper.ignore( "loaded" ) if( isError ) then return else applovinHelper.showBanner( "bottom" ) end end end m.listen( "loaded", loadedListener ) -- ... now do init() call. Not shown. --

In a nutshell, the code above let’s met set up two one-time event listeners for the two phases:

  • “init” - Called when initialization finishes and does a load()

How Is This Different From The Examples?
 
Hopefully, you’re not asking this, but if you are.  The concepts above are the same as the example code shown for most listeners, with these major exceptions:

  • I can have as many or as few (custom) event listeners when and where I need them to handle specific actions.  This makes coding up event-driven ad code (which is fully asynchronous) super simple.
  • I wrap the code that is often different between providers in the module show*() helpers.  So, I can easily swap out ad helpers as needed with very little code changing.

Phase Distributor Concept
What about the ‘phase’ distributor thingy I mentioned above?
 
Well… until recently, in addition to custom listeners, I also had the ability to set a callback/listener for a specific ad phase.  This was essentially a very focused and slimmer version of the custom listener code. 
 
For example, I could set a function up to be called if a ‘loaded’ event came in for a ‘rewardedVideo’ ad.
 
I used this for a bit, but found it to be less useful than custom/temporary listeners.  Still, you might find the idea valuable so I am mentioning it.
 
 
Testing In The Simulator - Fake Ads
In case you missed it I mentioned ‘fake ads’ above in one or two places. 
 
What are they and what do they do?
 
I personally prefer to do as much testing as I can in the simulator.  I save device testing for, validation, feature verification, and look-and-feel evaluation. 
 
To that end, I really hated the idea of having to test my code over and over on a device to get the ads working well with my games.  So, I made a fake ads module that mimics the behaviors of real ads and shown a fake placholder in the simulator.  
 
Here is a spoiler screenshot of one of my many upcoming templates with the fake ads (and other modules) incorporated:
spoiler_birdclimb.png
 
As you can see not only do I get a placeholder, but it tells me what ad provider it is substituting for and show the ID for the OS I’m simulating.  This way I can verify my code is using the right ad id before I ever get to device.
 
 
How do you handle this?
As always I want to close this post with questions for the readers.  Please:

  • Share with me and with others how you handle ads.
  • If you have code you can share or old examples you like please link them here.
  • If you think I was unclear or have suggestions for improving my method please let me know.

Placeholder

Hey @roaminggamer, you’ve hit the nail on the head.  The two main issues are not understanding all the different requirements in the ad provider’s portal and the event/async nature of when you need to do things.

With regards to the ad provider portals, there just is no way we could ever create a tutorial or guide for each provider given how frequently they update their sites and the simple wide variety of how different each of them are.

As for the code we provide in the documentation and sample apps, it probably contributes to the problem because its really difficult to show the various steps in the context of how they would be used in a real app. Even the sample apps are designed to attach showing ads to buttons which isn’t realistic to how you use ads in the real world.

I need to find the time to write a proper guide for this. But even then, questions like when do you use banners? When do you use full screen type ads? are really hard to answer because each developer will have different requirements as to when they should load an ad, when they should show it, etc.

I’d like to see some more discussion on how other developers are loading ads. 

Rob

Unfortunately, I have nothing to contribute as I was building my apps as premium and have only recently decided to redesign and base them all as IAP / ads before release.

However, I do look forward to the discussion, thanks Ed! @roaminggamer

I had created a personal advertising module using appodeal.

I apologize if the comments are not English, but the code I think is quite intuitive and the variables are still in English:

Advertising.lua

local adsPlugin\_1 = require( "plugin.appodeal" ) --seleziono la chiave dell app local appKey\_1 if ( system.getInfo("platformName") == "Android" ) then -- Android     appKey\_1 = "myAppKey" elseif ( system.getInfo("platformName") == "iPhone OS" ) then --iOS     appKey\_1 = "myAppKey" end local M = {} --=================-- --Variabili private-- --=================-- --varibile che verifica se ho gia inviato una richiesta di inizializzazione local initRequest = false --indica se il plung è inizializzato local initialized = false --indica se sto mostrando un banner local therIsBannar = false --funzione che eventualmente viene eseguita appena inizia un video o un rewardedVideo local onStartVideo --funzione che eventualmente viene eseguita appena inizia un video o un rewardedVideo local onFinishVideo --================-- --Funzioni private-- --================-- --listenere che passo all inizializzazione dell banner local function adsListener( event )     -- Successful initialization of the Appodeal plugin     if ( event.phase == "init" ) then         initialized = true      -- An ad loaded successfully      elseif ( event.phase == "loaded" ) then      -- The ad was displayed/played      elseif ( event.phase == "displayed" or event.phase == "playbackBegan" ) then          if( (event.type == "interstitial") or (event.type == "rewardedVideo") )then              if(onStartVideo) then                  onStartVideo()                  onStartVideo = nil              end          end      -- The ad was closed/hidden/completed      elseif ( event.phase == "hidden" or event.phase == "closed" or event.phase == "playbackEnded" ) then          if( (event.type == "interstitial") or (event.type == "rewardedVideo") )then              if(onFinishVideo) then                  onFinishVideo()                  onFinishVideo = nil              end          end      -- The user clicked/tapped an ad      elseif ( event.phase == "clicked" ) then      -- The ad failed to load      elseif ( event.phase == "failed" ) then     end end --funzione che controlla la conessione local function checksInternetConnection()     local socket = require("socket")          local test = socket.tcp()     test:settimeout(1, 't') -- Set timeout to 1 second          local testResult = test:connect("www.google.com", 80)-- Note that the test does not work if we put http:// in front          local thereIs     if not(testResult == nil) then      print("Internet access is available")      thereIs = true     else      print("Internet access is not available")      thereIs = false     end          test:close()     test = nil     return thereIs end --=================-- --Funzioni publiche-- --=================-- --Inizializzo il plung function M.initializes( adParams )         if(initRequest == false) then         --tipi di publicita che uttilizzero, se non passato inizializzo tutto di default         local adTypes = adParams or {"banner", "interstitial", "rewardedVideo" }         --invio la richiesta di inizializzazione         adsPlugin\_1.init(adsListener, {             appKey = appKey\_1,             supportedAdTypes = adTypes,             testMode = false         })         --seganalo che ho gia fatto la richiesta di questo plung         initRequest = true     end end --funzione che riceve come paramentro una stringa e restituisce un true se tale publicita è carica function M.loadAdControl( adName )     --verifico che abbia inserito una stringa valida altrimenti restituisco subito false     if((adName ~= "banner") and (adName ~= "interstitial") and (adName ~= "rewardedVideo")) then         return false     end     --controllo se tale publicita è carica     if( adsPlugin\_1.isLoaded(adName) ) then      return true     end     --se non è carico restituisco false     return false end --mostro il banner function M.showBanner( )     --avvio un timer per attivare il banner. Quando il plung sara initialized e il banner pronto il timer verra arrestato     local function pronto( event )         if(initialized)then             if( M.loadAdControl("banner") ) then                 --mostro il banner                  adsPlugin\_1.show( "banner", { yAlign="bottom" } )                  --segnalo che ho attivato il banner                  therIsBannar = true                  --cancello il timer                  timer.cancel( event.source )              end         end     end     local tmp = timer.performWithDelay( 1000, pronto, -1 ) end --nascondo il banner function M.hideBanner()     if(therIsBannar) then         adsPlugin\_1.hide( "banner" )     end end --se disponibile mostro un immagine di publicita altrimenti restituisco false function M.showInterstitialVideo( startFun, endFun )     if(initialized) then         if( M.loadAdControl("interstitial") and checksInternetConnection() )then             --imposto eventuali funzioni passate come parametro             onStartVideo = startFun             onFinishVideo = endFun             --mostro il video o l immagine             adsPlugin\_1.show( "interstitial" )              return true         end     end     return false end --se disponibile mostro un video ricompensa altimenti restituisca false function M.showRewardedVideo( startFun, endFun )     if(initialized) then         if( M.loadAdControl("rewardedVideo") and checksInternetConnection() ) then             --imposto eventuali funzioni passate come parametro             onStartVideo = startFun             onFinishVideo = endFun             --mostro il video             adsPlugin\_1.show( "rewardedVideo" )              return true         end     end     return false end function M.show( adType, startFun, endFun )          local result     if(adType=="banner")then         result = M.showBanner()      elseif(adType=="interstitial")then          result = M.showInterstitialVideo(startFun, endFun)      elseif(adType=="video")then          result = M.showInterstitialVideo(startFun, endFun)      elseif(adType=="rewardedVideo")then          result = M.showRewardedVideo(startFun, endFun)     end     return result end return M

main.lua

local Advertising = require("libs.Advertising") local function settingAds()     --libreria necessaria--     local Advertising = require("libs.Advertising")     --what I wont...     local adTypes = {"rewardedVideo","banner"}     --inizializzo le publicita, passando come parametro l eventuale nuova tabella     Advertising.initializes( adTypes )     --if I want the banner right away     Advertising.show("banner") end settingAds()

otherFiles.lua

ocal Advertising = require("libs.Advertising") local function startFun()     print("startVideo") end local function endFun()     print("endVideo")     --my checks after viewing an advertisement     --...     --... end if(Advertising.show("video", startFun, endFun) )then     --the video is shown successfully else     --the video can not be shown end

I hope it can serve

Placeholder

Hey @roaminggamer, you’ve hit the nail on the head.  The two main issues are not understanding all the different requirements in the ad provider’s portal and the event/async nature of when you need to do things.

With regards to the ad provider portals, there just is no way we could ever create a tutorial or guide for each provider given how frequently they update their sites and the simple wide variety of how different each of them are.

As for the code we provide in the documentation and sample apps, it probably contributes to the problem because its really difficult to show the various steps in the context of how they would be used in a real app. Even the sample apps are designed to attach showing ads to buttons which isn’t realistic to how you use ads in the real world.

I need to find the time to write a proper guide for this. But even then, questions like when do you use banners? When do you use full screen type ads? are really hard to answer because each developer will have different requirements as to when they should load an ad, when they should show it, etc.

I’d like to see some more discussion on how other developers are loading ads. 

Rob

Unfortunately, I have nothing to contribute as I was building my apps as premium and have only recently decided to redesign and base them all as IAP / ads before release.

However, I do look forward to the discussion, thanks Ed! @roaminggamer

I had created a personal advertising module using appodeal.

I apologize if the comments are not English, but the code I think is quite intuitive and the variables are still in English:

Advertising.lua

local adsPlugin\_1 = require( "plugin.appodeal" ) --seleziono la chiave dell app local appKey\_1 if ( system.getInfo("platformName") == "Android" ) then -- Android     appKey\_1 = "myAppKey" elseif ( system.getInfo("platformName") == "iPhone OS" ) then --iOS     appKey\_1 = "myAppKey" end local M = {} --=================-- --Variabili private-- --=================-- --varibile che verifica se ho gia inviato una richiesta di inizializzazione local initRequest = false --indica se il plung è inizializzato local initialized = false --indica se sto mostrando un banner local therIsBannar = false --funzione che eventualmente viene eseguita appena inizia un video o un rewardedVideo local onStartVideo --funzione che eventualmente viene eseguita appena inizia un video o un rewardedVideo local onFinishVideo --================-- --Funzioni private-- --================-- --listenere che passo all inizializzazione dell banner local function adsListener( event )     -- Successful initialization of the Appodeal plugin     if ( event.phase == "init" ) then         initialized = true      -- An ad loaded successfully      elseif ( event.phase == "loaded" ) then      -- The ad was displayed/played      elseif ( event.phase == "displayed" or event.phase == "playbackBegan" ) then          if( (event.type == "interstitial") or (event.type == "rewardedVideo") )then              if(onStartVideo) then                  onStartVideo()                  onStartVideo = nil              end          end      -- The ad was closed/hidden/completed      elseif ( event.phase == "hidden" or event.phase == "closed" or event.phase == "playbackEnded" ) then          if( (event.type == "interstitial") or (event.type == "rewardedVideo") )then              if(onFinishVideo) then                  onFinishVideo()                  onFinishVideo = nil              end          end      -- The user clicked/tapped an ad      elseif ( event.phase == "clicked" ) then      -- The ad failed to load      elseif ( event.phase == "failed" ) then     end end --funzione che controlla la conessione local function checksInternetConnection()     local socket = require("socket")          local test = socket.tcp()     test:settimeout(1, 't') -- Set timeout to 1 second          local testResult = test:connect("www.google.com", 80)-- Note that the test does not work if we put http:// in front          local thereIs     if not(testResult == nil) then      print("Internet access is available")      thereIs = true     else      print("Internet access is not available")      thereIs = false     end          test:close()     test = nil     return thereIs end --=================-- --Funzioni publiche-- --=================-- --Inizializzo il plung function M.initializes( adParams )         if(initRequest == false) then         --tipi di publicita che uttilizzero, se non passato inizializzo tutto di default         local adTypes = adParams or {"banner", "interstitial", "rewardedVideo" }         --invio la richiesta di inizializzazione         adsPlugin\_1.init(adsListener, {             appKey = appKey\_1,             supportedAdTypes = adTypes,             testMode = false         })         --seganalo che ho gia fatto la richiesta di questo plung         initRequest = true     end end --funzione che riceve come paramentro una stringa e restituisce un true se tale publicita è carica function M.loadAdControl( adName )     --verifico che abbia inserito una stringa valida altrimenti restituisco subito false     if((adName ~= "banner") and (adName ~= "interstitial") and (adName ~= "rewardedVideo")) then         return false     end     --controllo se tale publicita è carica     if( adsPlugin\_1.isLoaded(adName) ) then      return true     end     --se non è carico restituisco false     return false end --mostro il banner function M.showBanner( )     --avvio un timer per attivare il banner. Quando il plung sara initialized e il banner pronto il timer verra arrestato     local function pronto( event )         if(initialized)then             if( M.loadAdControl("banner") ) then                 --mostro il banner                  adsPlugin\_1.show( "banner", { yAlign="bottom" } )                  --segnalo che ho attivato il banner                  therIsBannar = true                  --cancello il timer                  timer.cancel( event.source )              end         end     end     local tmp = timer.performWithDelay( 1000, pronto, -1 ) end --nascondo il banner function M.hideBanner()     if(therIsBannar) then         adsPlugin\_1.hide( "banner" )     end end --se disponibile mostro un immagine di publicita altrimenti restituisco false function M.showInterstitialVideo( startFun, endFun )     if(initialized) then         if( M.loadAdControl("interstitial") and checksInternetConnection() )then             --imposto eventuali funzioni passate come parametro             onStartVideo = startFun             onFinishVideo = endFun             --mostro il video o l immagine             adsPlugin\_1.show( "interstitial" )              return true         end     end     return false end --se disponibile mostro un video ricompensa altimenti restituisca false function M.showRewardedVideo( startFun, endFun )     if(initialized) then         if( M.loadAdControl("rewardedVideo") and checksInternetConnection() ) then             --imposto eventuali funzioni passate come parametro             onStartVideo = startFun             onFinishVideo = endFun             --mostro il video             adsPlugin\_1.show( "rewardedVideo" )              return true         end     end     return false end function M.show( adType, startFun, endFun )          local result     if(adType=="banner")then         result = M.showBanner()      elseif(adType=="interstitial")then          result = M.showInterstitialVideo(startFun, endFun)      elseif(adType=="video")then          result = M.showInterstitialVideo(startFun, endFun)      elseif(adType=="rewardedVideo")then          result = M.showRewardedVideo(startFun, endFun)     end     return result end return M

main.lua

local Advertising = require("libs.Advertising") local function settingAds()     --libreria necessaria--     local Advertising = require("libs.Advertising")     --what I wont...     local adTypes = {"rewardedVideo","banner"}     --inizializzo le publicita, passando come parametro l eventuale nuova tabella     Advertising.initializes( adTypes )     --if I want the banner right away     Advertising.show("banner") end settingAds()

otherFiles.lua

ocal Advertising = require("libs.Advertising") local function startFun()     print("startVideo") end local function endFun()     print("endVideo")     --my checks after viewing an advertisement     --...     --... end if(Advertising.show("video", startFun, endFun) )then     --the video is shown successfully else     --the video can not be shown end

I hope it can serve