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

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

last checked with Phaser 3.12.0 – beta 2

 

Goal

Today we will continue on managing different sponsor APIs. Last time in part 1 we build solid base for handling different events by different sponsors. We also created system for loading external config file for each sponsor, that overrides default config values. Today we will add more examples, how to use it.

 

Adjustments

First, let’s make some small adjustments to our code. Open Preloader.ts scene and change crate() method to this:

        // --------------------------------------------------------------------
        public create(): void {
            console.log("Preloader");

            let self = this;

            // load user settings
            Utils.StorageUtils.load(App.Config.SAVE_KEY)
                .then(function (data: any) {
                    // if data is not null and not undefined
                    if (data != null) {
                        App.settings = data;
                        console.log("Settings loaded...");
                    } else {
                        console.log("No saved settings.");
                    }

                    // continue to menu
                    self.scene.start("Menu");
                });
        }

We are moving load setting code from Game.ts to here. Newly, we are waiting for loading to finish and after then, we continue to Menu scene. Let’s also delete the same code from it previous location in Game.ts:

            // load user settings
            Utils.StorageUtils.load(App.Config.SAVE_KEY)
                .then(function (data: any) {
                    // if data is not null and not undefined
                    if (data != null) {
                        App.settings = data;
                        console.log("Settings loaded...");
                    } else {
                        console.log("No saved settings.");
                    }
                });

And while we are inside Game.ts file, we will add new Play scene. Put following line into it – it will report error for now, as we did not create Play.ts file yet:

            // states
            this.scene.add("Boot", Boot);
            this.scene.add("Preloader", Preloader);
            this.scene.add("Menu", Menu);
            this.scene.add("Play", Play);

 

Play scene

As we will need some more buttons and pictures, assets for this part were updated. You can download it from GitHub. Graphics was made by Tomáš Kopecký and we used it in our HTML game Woodventure.
Create new file Play.ts in Scenes folder and put following code into it:

///<reference path = "SceneBase.ts" />

namespace MaintainableGame {

    export class Play extends SceneBase {

        // --------------------------------------------------------------------
        public create(): void {
            console.log("Play");

            // bacground color
            this.cameras.main.backgroundColor = Phaser.Display.Color.ValueToColor(0xB8C6FF);

            // focus on 0, 0
            this.setView();

            // back icon
            this.addControlls();

            // add some animation
            this.buildScene();
        }

        // --------------------------------------------------------------------
        private addControlls(): void {
            let y = -this.gameHeight / 2 + 50;
            let x = -this.gameWidth / 2 + 50

            // menu icon
            let menu = this.add.sprite(x, y, "Sprites", "IconMenu");
            menu.setInteractive();
            menu.on("pointerdown", function (this: Play) {
                this.backToMenu();
            }, this);
        }

        // --------------------------------------------------------------------
        private buildScene(): void {

            // create pig animation if it does not exist
            if (typeof this.anims.get("pig") === "undefined") {
                this.anims.create({
                    key: "pig",
                    frames: this.anims.generateFrameNames("Sprites", { frames: ["pig01", "pig02", "pig03", "pig04", "pig05", "pig06", "pig07"] }),
                    frameRate: 3,
                    repeat: -1
                });
            }

            // add pig sprite and play animation
            let pig = this.add.sprite(0, 0, "Sprites");
            pig.anims.play("pig");
        }

        // --------------------------------------------------------------------
        private backToMenu(): void {
            // empty for now ...
        }
    }
}

It creates simple scene with animated pig for fun in the middle of the screen. There is menu icon in top left corner that will later send you back to Menu scene. For now it just calls empty backToMenu() method. Usually, sponsors want to report when player exits game and backToMenu() method is good place, where we will place sponsor API calls.

Menu scene adjustments

Open Menu.ts file. So far we had only music and sound controls in this scene. We will add play button, so we can start Play scene. Also sponsors want to report when game started, so pressing play button is right time to report it.

First add following line in create() method:

            // sound and music icons
            this.addAudioControlls();

            // add play button
            this.addPlayButton();

Add addPlayButton() method:

        // --------------------------------------------------------------------
        private addPlayButton(): void {
            // play
            let play = this.add.sprite(0, 0, "Sprites", "IconPlay");
            play.setInteractive();
            play.on("pointerdown", function (this: Menu) {
                this.startGame();
            }, this);
        }

It simply adds interactive sprite with look of play button and it calls startGame() method when it is pressed. Method is not defined yet, so you will get error.

 

Calling sponsor API

Let’s continue with Menu.ts. Define startGame() method with following code:

        // --------------------------------------------------------------------
        private startGame(): void {

            let self = this;

            // report start of game
            Sponsor.api.startGameSession()
                .then(function () {
                    self.scene.start("Play");
                });
        }

Finally! Here we report game start to sponsor API. Remember, we have some default sponsor implementation and if specific sponsor does not need to handle this call, default implementation just writes some output into console. Also, all our methods are asynchronous, because we want to take into account sponsors, that may want to do something somewhere on their server, before they allow game to start and it may take unknown time. All our asynchronous methods return Promise, so after call to startGameSession(), we are waiting until Promise is resolved. When it is resolved, we start Play scene. Currently, our default implementation of sponsor method resolves immediately.

Switch back to backToMenu() method in Play.ts and add this code:

        // --------------------------------------------------------------------
        private backToMenu(): void {

            let self = this;

            // report end of game
            Sponsor.api.endGameSession({ score: 12345, level: 10 })
                .then(function () {
                    self.scene.start("Menu");
                });
        }

Here we return from Play back to Menu. This time we are reporting game end to sponsor API. All our sponsor API calls can take variable number of arguments of type “any”. Here we are, for testing, sending some game result: score and level. It is up to specific sponsor implementation, what it will do with it. Default sponsor implementation is just ignoring it for now.

 

Implementing specific sponsor

If you now switch to fictitious sponsor SBCGames, we created in last part, it will still call default sponsor implementation. To change it, we will implement startGameSession() and endGameSession() for SponsorSBCGames class. Open file SponsorSBCGame.ts in Libs/Sponsor folder and add this code:

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

            await new Promise<void>(function (resolve) {
                console.log("starting timeout - 3 secs");

                setTimeout(function () {
                    console.log("timeout over");
                    resolve();
                }, 3000);
            });
        }

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

            if (typeof parameters[0] !== "undefined") {
                console.log("pramaters = " + JSON.stringify(parameters[0]));
            }

            await new Promise<void>(function (resolve) {
                console.log("starting timeout - 3 secs");

                setTimeout(function () {
                    console.log("timeout over");
                    resolve();
                }, 3000);
            });
        }

What it does is, it simple emulates 3 seconds delay – like if there was some server communication. It creates Promise, that is resolved after 3 seconds and then whole method is resolved, which means, that .then() in game code is called and we can react.

If you now switch to SBC_GAMES sponsor in App.ts …:

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

… recompile and press play button, it will take 3 seconds before game is switched into Play scene. And also returning from game with menu button will again take 3 seconds.

Note: we are not disabling play button after first click, so if you press it several times, you will launch 3 seconds of waiting several times. This is not good – for real game you should disable button input or check whether previous call was already resolved.

 

Sponsor specific loading and saving

Let’s continue in sponsor specific implementation. If you remember, when we created our StoreageUtils for loading and saving, we were thinking a step ahead and added possibility to change default loading/saving into localStorage. We defined ISponsorStorage interface and classes, that implement it can do their own work when loading/saving. Again, we will just simulate 3 seconds delay before data are loaded or before data are saved. In fact, this is only test, so after 3 seconds we use local storage again. First, change a little top of SponsorSBCGames.ts like this:

    export class SponsorSBCGames extends Sponsor implements Utils.ISponsorStorage {

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

            Utils.StorageUtils.sponsorStorage = this;
        }

We say, we will implement ISponsorStorage and we are setting it in StorageUtils. Next, we add ISponsorStorage implementation:


        // #region ISponsorStorage interface implementation

        // --------------------------------------------------------------------
        public async save(key: string, data: any): Promise<void> {

            let storage = window.localStorage;

            await new Promise<void>(function (resolve) {
                console.log("saving somewhere into cloud - it will take 3 seconds");

                setTimeout(function () {
                    console.log("data saved");
                    storage.setItem(key, JSON.stringify(data));
                    resolve();
                }, 3000);
            });
        }

        // --------------------------------------------------------------------
        public async load(key: string): Promise<any> {

            let storage = window.localStorage;

            let result = await new Promise<void>(function (resolve) {
                console.log("loading from cloud - it will take 3 seconds");

                setTimeout(function () {
                    console.log("data loaded");
                    let data = storage.getItem(key);
                    console.log("data  = " + data);

                    resolve(data == null ? null : JSON.parse(data));
                }, 3000);
            });

            return result;
        }

        // --------------------------------------------------------------------
        public fallbackToStandardStorage(): boolean {
            return false;
        }

        // #endregion

There is nothing mysterious here – we just wait 3 seconds and then do loading or saving operation. If loading, we return loaded data. You can recompile and test with SBC_GAMES sponsor set in App.ts. Before Menu screen appears, there is delay now. It is because we are waiting for user data to load in Preloader scene before we continue to Menu scene. And it now takes 3 seconds. In Menu scene, clicking on sound or music buttons take 3 seconds to save. When we were creating StorageUtils class, we added possibility to check, if multiple requests are allowed. It is set to false by default, but you can change it with allowMultipleRequests property. Do this small modification to saveSettings() method in Menu.ts:

        // --------------------------------------------------------------------
        private saveSettings(): void {
            Utils.StorageUtils.save(App.Config.SAVE_KEY, App.settings)
                .then(function () {
                    console.log("Settings saved...");   
                }).catch(function (e) {
                    console.log(e);
                });
        }

Open console and click sound or music icon twice within 3 seconds. Second click will throw error: Error: “Previous load/save request was not finished yet”
It is fully up to you whether you allow multiple save/load requests with setting allowMultipleRequests to true or implement some lock on controls while request is in progress.

 

Very specific sponsor API call

Our code is now handling common game events like start, end, save or load data. But some sponsors may have some very special requirements. Let’s say, that SBCGames sponsor wants you to do something specific when game ends. So, add this method to SponsorSBCGames.ts:

        // --------------------------------------------------------------------
        public someVerySpecificSponsorFunction(): void {
            console.log("Very sponsor specific function for SBCGames");
        }

In Play scene, we will call it like this:

        // --------------------------------------------------------------------
        private backToMenu(): void {

            let self = this;

            // report end of game
            Sponsor.api.endGameSession({ score: 12345, level: 10 })
                .then(function () {
                    self.scene.start("Menu");
                });

            // some very special SBCGames function
            if (Sponsor.is(Sponsor.eSponsorID.SBC_GAMES)) {
                (<Sponsor.SponsorSBCGames>Sponsor.api).someVerySpecificSponsorFunction();
            }
        }

Unfortunately, we have to add specific check, but all the implementation of specific sponsor function remains in its own sponsor class. We take current sponsor api, cast it to specific sponsor class and then we can call any specific method, that is inside of it.

For example, you may also have a group of sponsors, that want to, for example, display ad before some event and another group, that want to display it after that event. Then, instead of polluting you code with something like: “if sponsor is A or sponsor is B or sponsor is C or …”, you can add line to App.Config – something like “SHOW_AD_BEFORE_EVENT_X” and in sponsor specific config set it to true or false. In code you then just check: if(App.Config.SHOW_AD_BEFORE_EVENT_X) or if(!App.Config.SHOW_AD_BEFORE_EVENT_X). Using sponsor specific config may help you to keep your code cleaner.

 

Conclusion

We can now implement various sponsors and keep our game clean. In next part I may take some real-world sponsor and show how to implement their API with our system. Again, all code is available on GitHub.

 

 

 

Bookmark the permalink.

Leave a Reply

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