Build maintainable games with Phaser 3 – 2: Adding external config


Second article in series on building games, that are easy to maintain. Adding external config file to modify game without rebuilding source.

last checked with Phaser 3.12.0 – beta 2

Goal

In this part of series on maintainable games we will add external config file. This file will allow us to parametrize game without need to rebuild it every time.

Config

First let’s create file Config.ts inside root source folder of our project:

namespace App {

    export class Config {

        // game dimensions
        public static readonly GAME_WIDTH = 800;
        public static readonly GAME_HEIGHT = 600;
    }
}

This class defines parameters for our game. Currently we are using it only to define dimensions of game. It still is not external config, only some place, where to gather all key game parameters. But, we can already change magic numbers in game config to use config values. Change Game.ts:

            // init game
            super(
                {
                    type: renderer,

                    parent: "game_content",

                    width: App.Config.GAME_WIDTH,   // <==
                    height: App.Config.GAME_HEIGHT, // <==

                    title: "Maintainable Game",
                }
            );

Now, we can create file config.json and put it into assets folder – notice, for test we are using different values for width and height than default values in Config class.

{
  "GAME_WIDTH": 400,
  "GAME_HEIGHT": 300
}

Shared libraries

We now need some way how to load file config.json and get its values into Config class. For these tasks, we will create our first shared library. Shared, because we want to use it in this and all our future games, but edit and maintain it in one place. You can create Libs folder anywhere on disk. important is to say TS compiler, where it can find it. I created my Libs folder on the same level as are folders for individual games. As we can have many libraries and we want to keep them organized, we can create further subfolders under Libs. For now, create only one subfolder and name it Utils. Go inside and create empty file ObjectUtils.ts there.

For now, TS compiler is not able to find it, so we will adjust list of included folders like this:

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

Now it is time to open empty ObjectUtils.ts file and add two methods. First is loadJson() and second is loadValuesIntoObject():

namespace Utils {

    export class ObjectUtils {

        // --------------------------------------------------------------------
        public static loadJson(fileName: string): Promise<any> {

            return new Promise(function (resolve, reject) {

                var request = new XMLHttpRequest();

                request.open('GET', fileName, true);
                request.responseType = 'json';

                request.onload = function () {
                    if (request.status === 200) {
                        resolve(request.response);
                    } else {
                        reject(new Error(`Error loading ${fileName}: ${request.statusText}`));
                    }
                };

                request.onerror = function () {
                    reject(new Error(`Network error while loading ${fileName}`));
                };

                request.send();
            });
        }

        // --------------------------------------------------------------------
        public static loadValuesIntoObject(jsonData: any, targetObject: any) {

            console.log(`----- loading values into ${targetObject.name} -----`);

            for (let property in jsonData) {
                console.log(`name = ${property}, value = ${jsonData[property]}`);
                targetObject[property] = jsonData[property];
            }

            console.log("------------------------------------------------");
        }
    }
}

We created new namespace Utils. Inside it is new calss ObjectUtils with two static methods:

  • loadJson() – this method takes name of .json file to load and returns new Promise. Loading over network can take some time, so when loading is finished (or fails) and we know result, this promise is either resolved or rejected,
  • loadValuesIntoObject() – simply overwrites properties in target object with properties from passed jsonData object.

Last piece

Last missing piece is how to make all this work together. Open App.ts and change launch() method to this:

// --------------------------------------------------------------------
async function launch(): Promise<void> {

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


    // create game
    let game = new MaintainableGame.Game();
    App.game = game;
}

Changes are highlighted. Notice, we changed method signature. It is no longer “function launch(): void”, but “async function launch(): Promise”. This method runs asynchronously and when finished, it returns Promise. This promise is ignored and it looks there is no point in running this method asynchronously. Point is, that if we do it, we can use “await” inside and easy work with other asynchronous methods like they are synchronous.
Look at line 13. We call json loading method. It may take some time before .json file is loaded and result is returned. But if we use await, execution will pause here and wait for return from loading (either with success or failure). If promise inside loadJson() is rejected, we catch it with catch block. Otherwise we get loaded data. We then pass these data into loadValuesIntoObject(). Object, we load data into is App.Config – our class with static parameters.

Test

Compile and run game. You will get this result on screen:

Game is now only 400 x 300 pixels, which are dimensions defined in config.json.

Try to change values in config.json to, let’s say 600 x 150. Do not compile game, just reload browser with F5 key and you get this (make sure, your browser does not use caches during development, otherwise you can get cached result of old code displayed):

We proved, we can change game size from config.json, without need to recompile whole game every time.

Conclusion

Our external config is working and we can easily parametrize game. Next time we will add game settings.


3 responses to “Build maintainable games with Phaser 3 – 2: Adding external config”

  1. Hello,

    thank you for your tutorial, I’m trying to learn from it.
    One thing I don’t understand, I can update the game dimensions from the config.json.

    But if I want to change something in one of the scenes, the changes are not there. I’m working with Visual studio code and open with “Live Server”.

    Any thoughts?

    • Hi Deos,

      I’m going through the same process of learning from this tutorial. I’ll try and answer your question with my limited knowledge.

      From what I can see from the Apps.ts code, the async function gives the ability to change the game screen size (change config.json code) without having to re-compile the typescript code. Where as anything from within the scene (graphics circle in the Menu.ts) needs to be recompiled after making changes.

      I suggest using “TypeScript watch” (Ctrl-Shift-B, “tsc: watch”) when building. In this mode the TypeScript compiler is always running and constantly checking for saved changes in the code. That way any changes will show in the browser straight away (after F5 refresh) if compiled successfully.

      I hope my simple explanation is enough to answer your question.

  2. Hi, config is loaded once on game (re)load. So, if you have some setting that modifies something in scene, you still have to reload the game. I think, that Live server is automatically reloading if there is code change, but will probably not, if you change only external config file (asset).

Leave a Reply to DaVoodooShuffle Cancel reply

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