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:
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.