Build maintainable games with Phaser 3 – 4: Managing sponsor APIs – part 1


Fourth article in series on building games, that are easy to maintain. This time how to manage different sponsor APIs in easy way.

last checked with Phaser 3.12.0 – beta 2

Goal

If you are publishing your game with multiple sponsors, than you know, that almost every sponsor company has its own API. These APIs are usually very different. Our today target is to build new library, that will help us to manage these differences. It will also keep most of the sponsor stuff separated from game. Also, we want to implement every sponsor API only once for all games we may publish with such sponsor.

Sponsor library

First, create empty folder named “Sponsor” in Libs folder – on the same level, where you already has Utils folder. And before we forget it, add new line into tsconfig.json, into include part:

  "include": [
    "../Libs/Utils/**/*",
    "../Libs/Sponsor/**/*",
    "lib/**/*",
    "src/**/*"
  ],

In new folder create file List.ts containing these lines:

namespace Sponsor {

    // list of all sponsors
    export enum eSponsorID { NONE, SBC_GAMES };
}

From now on, our game will always have to have some sponsor. Enum eSponsorID lists all sponsors for which we have implementation. Sponsor NONE will be empty sponsor in case we do not want to use any specific sponsor. SBC_GAMES is fictitious sponsor, we will use in this tutorial. By the way, creating fictitious sponsors has sense in some cases. I did so, for example, when I needed to create CORDOVA version of game with Heyzap ads mediation.
You will add to this list every new sponsor API you implement.

Create next new file Features.ts – it will contain this interface:

namespace Sponsor {

    // define basic features for sponsor
    export interface ISponsorFeatures {
        id: Sponsor.eSponsorID;
        name: string;
        hasConfig: boolean;
    }
}

This defines basic set of features every sponsor has. Currently it looks more like properties than features, but we will extend it on game side with game specific properties like: “show sponsor logo”, “enable achievements”, etc.
Important property is hasConfig. Currently, our game has file config.json in assets folder (read part 2 of series). This config can override various settings in App.Config class and game can be configured without recompilation. If sponsor has config (hasConfig = true), game will look into assets/sponsor folder for file name.json, where name is name defined in sponsor features, for additional config. With settings in it, you can override settings loaded from config.json. Let’s say our fictitious sponsor SBC_GAMES has name defined like “sbc_games” and hasConfig is set to true. Game will look for file “assets/sponsor/sbc_games.json”. Default config.json is loaded always, but sponsor specific config is loaded after it and can change some values. We will get to this later, now just create empty sponsor folder in assets.

Create file Sponsor.ts. This file is little bit longer, so here is listing first (for now, you will get two errors for api and id being not defined yet):

namespace Sponsor {

    // set of base sponsor features
    export interface ISponsor {
        startGameSession(...parameters: any[]): Promise<void>;
        endGameSession(...parameters: any[]): Promise<void>;

        submitScore(...parameters: any[]): Promise<void>;

        showAd(...parameters: any[]): Promise<void>;
    }

    /**
     * Sponsor class is abstract adapter implementing ISponsor interface
     */
    export abstract class Sponsor implements ISponsor {

        private static _instance: Sponsor = null;

        private _features: ISponsorFeatures = null;

        private _game: Phaser.Game = null;

        // --------------------------------------------------------------------
        public static get instance(): Sponsor {

            // check null or undefined
            if (Sponsor._instance == null) {
                throw new Error(`Sponsor is ${Sponsor._instance}. Sponsor must be always initialized.`);
            }

            return Sponsor._instance;
        }

        // --------------------------------------------------------------------
        public static get initialized(): boolean {
            return Sponsor._instance != null;
        }


        // --------------------------------------------------------------------
        protected constructor(features: ISponsorFeatures) {

            // instance
            Sponsor._instance = this;

            this._features = features;

            api = this;
            id = features.id;
        }


        // --------------------------------------------------------------------
        public get id(): eSponsorID {
            return this.features.id;
        }

        // --------------------------------------------------------------------
        public get features(): ISponsorFeatures {
            return this._features;
        }

        // --------------------------------------------------------------------
        public set game(game: Phaser.Game) {
            this._game = game;
        }


        // #region Default interface implementation

        // --------------------------------------------------------------------
        public async startGameSession(...parameters: any[]): Promise<void> {
            console.log(`Sponsor ${eSponsorID[this.id]}: startGameSession() with parameters ${parameters}`);
        }

        // --------------------------------------------------------------------
        public async endGameSession(...parameters: any[]): Promise<void> {
            console.log(`Sponsor ${eSponsorID[this.id]}: endGameSession() with parameters ${parameters}`);
        }

        // --------------------------------------------------------------------
        public async submitScore(...parameters: any[]): Promise<void> {
            console.log(`Sponsor ${eSponsorID[this.id]}: submitScore() with parameters ${parameters}`);
        }

        // --------------------------------------------------------------------
        public async showAd(...parameters: any[]): Promise<void> {
            console.log(`Sponsor ${eSponsorID[this.id]}: showAd() with parameters ${parameters}`);
        }

        // #endregion
    }
}

First, we are creating ISponsor interface. While sponsor APIs are very different, they have something common. Almost every sponsor wants you to report game start and game end as well as ask for an ad from time to time in right moment. Many sponsors also want you to send score or level or other data when player dies. ISponsor defines basic interface for these methods. All methods take various number and types of parameters to provide enough flexibility. Also notice, that all methods return Promise and and implementation has keyword async in method signature. We are preparing ahead for sponsors, that want you to wait for some asynchronous response before game can continue.
Second, we define abstract Sponsor class. Its main purpose is to provide empty implementation for ISponsor interface. Specific sponsor will override this basic implementation. Default implementation only sends debug string to console log. Beside this, class constructor takes set of sponsor features and has some getters and setters to access it. It will also keep reference to game. This will be convenient as some sponsors require you for example to pause sound when video ad is playing. As last thing, instance of sponsor class is accessible from game through call to static getter “instance” like this: Sponsor.Sponsor.instance (first Sponsor is namespace, second is class name).

We are finally getting to specific sponsor implementations. We have two sponsors: NONE and SBC_GAMES. Implement both with files SponsorNone.ts…

namespace Sponsor {

    export class SponsorNone extends Sponsor {

        // --------------------------------------------------------------------
        public constructor(features: ISponsorFeatures) {
            super(features);
        }
    }
}

… and SponsorSBCGames.ts:

namespace Sponsor {

    export class SponsorSBCGames extends Sponsor {

        // --------------------------------------------------------------------
        public constructor(features: ISponsorFeatures) {
            super(features);
        }
    }
}

Currently, it does nothing. It is OK for sponsor NONE. For Sponsor SBC_GAMES, we will add some example implementation in next part of tutorial.

Last file in Sponsor library is file Utils.ts. This file contains several methods for our convenience:

namespace Sponsor {

    export let api: Sponsor;
    export let id: eSponsorID

    // --------------------------------------------------------------------
    export function is(sponsorID: eSponsorID): boolean {
        return id === sponsorID;
    }

    // --------------------------------------------------------------------
    export function features(): ISponsorFeatures {
        return api.features;
    }

    // --------------------------------------------------------------------
    export function hasFeature(featureName: string): boolean {
        return typeof api.features[featureName] !== "undefined";
    }

    // --------------------------------------------------------------------
    export function isFeatureOn(featureName: string): boolean {
        let featureValue = api.features[featureName];

        return typeof featureValue === "boolean" && featureValue;
    }
}

This will remove errors from Sponsor class. As you can see, we do not have to write Sponsor.Sponsor.instance in game, when we need to get instance of sponsor class. Instead, we can write just Sponsor.api. Beside this, we can check whether sponsor has some feature or not and if it is boolean, we can ask if it is on or off.

Game

We are ready to use sponsor library in our game. On source top level add file SponsorConfig.ts. This file will customize basic sponsor features defined in library with regard to actual game. Let’s split listing into three parts:

namespace App {

    // extend sponsor features with game specific properties
    export type SponsorFeatures = Sponsor.ISponsorFeatures &
    {
        defaultLanguage: string;
        showFlags: boolean;
    };

Here we are creating new type SponsorFeatures, that has all properties ISponsorFeatures has plus some new properties specific to game. As an example, I added default language and whether this sponsor wants to show some language selection flags in menu. In next part of listing we set values for each sponsor:

    // define features for each single sponsor
    export const SPONSOR_FEATURES: { [key: number]: SponsorFeatures } = {

        [Sponsor.eSponsorID.NONE]: {
            id: Sponsor.eSponsorID.NONE,
            name: "none",
            hasConfig: false,

            defaultLanguage: "cs",
            showFlags: false
        },

        [Sponsor.eSponsorID.SBC_GAMES]: {
            id: Sponsor.eSponsorID.SBC_GAMES,
            name: "sbc_games",
            hasConfig: true,

            defaultLanguage: "en",
            showFlags: true
        }
    };

As you can see, we set values for both sponsors – NONE and SBC_GAMES. NONE sponsor has no config file, default language is Czech and no flags are needed in menu. SBC_GAMES sponsor has specific config (sbc_games.json), default language is English and some language selection shall be shown in menu. Last part of listing is just for convenience:

    // --------------------------------------------------------------------
    export function getSponsorFeatures(id: Sponsor.eSponsorID): SponsorFeatures {

        // are features for sponsor in list?
        if (SPONSOR_FEATURES[id] == null) {
            throw new Error(`Features for sponsor ${Sponsor.eSponsorID[id]} are not in SPONSOR_FEATURES list.`);
        }

        return SPONSOR_FEATURES[id];
    }
}

This method will return correct SponsorFeatures object and its added value is checking, whether requested object exist. If not, it throws error and makes finding bugs or typos easier.

Now, we will make two small changes, that will enable us testing later. In sponsor features for SBC_GAMES, we set hasConfig to true, so let’s create some new config property and sbc_games.json config file. Open Config.ts and in the bottom add new property SPONSOR_ASSOCIATION. Let’s say, that some sponsors want to display line in bottom of menu screen saying, that game was published in association with them. Default value of this property is empty string:

        // in association with
        public static readonly SPONSOR_ASSOCIATION = "";
    }
}

Create sponsor specific file sbc_games.json and place it into assets/sponsor folder:

{
  "SPONSOR_ASSOCIATION": "In association with SBCGames.io"
}

Finally, we can connect all things together and use it in App.ts. Open it and on top add reference to SponsorConfig.ts file and add sponsor variable like this:

/// <reference path ="Settings.ts" />
/// <reference path="SponsorConfig.ts" />

namespace App {

    // sponsor
    export const sponsor: Sponsor.eSponsorID = Sponsor.eSponsorID.NONE;

    // game
    export let game: Phaser.Game = null;

This is key place, where you are selecting sponsor! Now we are compiling our game for sponsor NONE. If we change sponsor to Sponsor.eSponsorID.SBG_GAMES and recompile it, it will be ready for SBCGames. Keep sponsor NONE for now and make further changes to launch() method.

// -------------------------------------------------------------------------
async function launch(): Promise {

    let sponsorFeatures = App.getSponsorFeatures(App.sponsor);

    if (App.sponsor === Sponsor.eSponsorID.NONE) {
        new Sponsor.SponsorNone(sponsorFeatures);
    } else if (App.sponsor === Sponsor.eSponsorID.SBC_GAMES) {
        new Sponsor.SponsorSBCGames(sponsorFeatures);
    }


    // load main game config
    try {
        let configJson = await Utils.ObjectUtils.loadJson("assets/config.json");
        Utils.ObjectUtils.loadValuesIntoObject(configJson, App.Config);
    } catch (e) {
        throw e;
    }

    // load sponsor config if exist
    if (Sponsor.isFeatureOn("hasConfig")) {
        try {
            let sponsorConfigJson = await Utils.ObjectUtils.loadJson(`assets/sponsor/${Sponsor.features().name}.json`);
            Utils.ObjectUtils.loadValuesIntoObject(sponsorConfigJson, App.Config);
        } catch (e) {
            throw e;
        }
    }


    // create game
    let game = new MaintainableGame.Game();
    App.game = game;
    // save game reference also into sponsor implementation
    Sponsor.api.game = game;
}

On top, we create right instance of Sponsor class, based on selected sponsor. Remember we have implementation for each sponsor in sponsor library. Here we are passing only sponsor features into constructor, but it is common, that some sponsors also assign you some game id you have to use during initialization.

After loading main game config, we newly load sponsor specific config (only if “hasConfig” in sponsor features is set to true). In the end, after we create game instance, we simply store reference to game into sponsor instance.

Test

We are ready to test our work. Notice, this is only first part of managing sponsors tutorial, so the following test will only check if right sponsor config is loaded. In next part we will make deeper tests. Open file Menu.ts and in the end of create() method add these lines:

            // text
            if (App.Config.SPONSOR_ASSOCIATION.length > 0) {
                this.add.text(-this.gameWidth / 2 + 20, this.gameHeight / 2 - 30, App.Config.SPONSOR_ASSOCIATION);
            }

Compile game and run it, you will see this:


Nothing happens… It is OK, our sponsor NONE has no config and default value for SPONSOR_ASSOCIATION is empty string. So, change sponsor in top of App.ts to SBC_GAMES:

    export const sponsor: Sponsor.eSponsorID = Sponsor.eSponsorID.SBC_GAMES;

Recompile and run the game. See text line in bottom:


SPONSOR_ASSOCIATION value was loaded from sponsor specific config file.

Conclusion

In first part of managing sponsor APIs we put solid base we will use in part 2. We can now load sponsor specific configuration files and define specific sponsor features. We can switch sponsors with change of single line. In second part we will explore more capabilities of our sponsor library. Target is to move sponsor specific stuff out of game as much as possible, so game only says: “Hey, I am starting” and sponsor specific implementation is responsible for handling it.
As usually, all code is available on GitHub.


Leave a Reply

Your email address will not be published. Required fields are marked *