Mocking Game Data (Data Binding, Events, Calls)

Overview

Gameface allows you to use JavaScript instead of engine backend for data binding. This lets you simulate the data and Events/Calls that would normally come from the engine, so you can develop and test your UI independently of the backend (with some initial setup).

What you can do

  • Mock game data entirely - useful for testing different game states and UI layout without the engine.
  • Mock Event and Call handlers so UI interactions can be exercised without a running engine.
  • Define mock/fallback handlers that run when no matching handler exists for an Event or a Call.

Tradeoffs / limitations

  • Requires initial setup
  • Keeping JS mocks and engine logic in sync is up to the team.

See the Data binding overview for the engine backend equivalent and Communicating with JavaScript for Event/Call details.

Mocking data binding models

Working with pure JavaScript Types

The API is mostly the same as in the engine backend version. You start by registering a JavaScript model by calling engine.createJSModel(modelName, model).

For example:

engine.createJSModel('player', {
    maxHealth: 100,
    currentHealth: 50,
    heroType: 'warrior',
    pet: {
        type: 'dog'
    },
    traits: [{
        strength: 10,
        intelligence: 20
    }, {
        strength: 15,
        intelligence: 17
    }]
});

This would create a global variable player with the given properties. We can use this model with data binding attributes like:

<div data-bind-value="{{player.currentHealth}}"></div>

<div data-bind-for="trait:{{player.traits}}">
    <div data-bind-value="{{trait.strength}}"></div>
</div>

For more information about the data binding attributes, see the HTML Data Binding documentation.

To update the JavaScript models we use similar functions like the engine backend equivalent - engine.updateWholeModel(model). This function only notifies the SDK that this model has changed. To force an update of the data-bound values (update the DOM) you must also call engine.synchronizeModels().

// Changing the object property like this won't work.
player.pet = { type: 'cat'};

// Instead you can do this:
player.pet.type = 'cat';
engine.updateWholeModel(player);
engine.synchronizeModels();

Augmenting existing models with extra properties

If the backend models are not fully implemented while you’re working on your UI, or if you want to be able to test your changes both in the Player and in the game with minimal script changes, you can use engine.createOrMergeModel(name, jsObject, includeInherited = true). This will check if a model with this name already exists, and if not it will create it from the passed jsObject as if you’ve called engine.createJSModel(modelName, model). Otherwise the function will only add the properties from the supplied jsObject that are missing from the existing model and recursively merge object properties the same way. It doesn’t matter if the existing model is a backend model or a JS model. The includeInherited parameter controls whether to include both own and inherited enumerable properties of jsObject.

For example:

// First let's create a model to merge to later
engine.createJSModel('player', {
    maxHealth: 100,
    currentHealth: 50,
    weapon: { 
        name: "sniper"
    }
});

// Since `player` model already exists, the following code will add the `name` property to the existing model and merge the missing property `damage` from `weapon`.
// Existing primitive properties such as `currentHealth` and `weapon.name` will remain untouched
engine.createOrMergeModel('player', {
    name: "Player 1",
    currentHealth: 20, // remains 50 after the call
    weapon: { 
        name: "riffle" // remains "sniper" after the call
        damage: 50
    }
});

The recursive option works with arrays too, meaning you can add missing properties to array elements. Note that new elements cannot be added to engine bound arrays, so redundant elements will be ignored. For example:

/* Let's have this backend model already created:
model: {
    players: [{status: {hp: 100}}, {status: {hp: 200}}]
}
*/

// The following code will add the `gold` field to each player's `status` but will not add a third player to the `players` array
engine.createOrMergeModel('model', {
    players: [{status: {gold: 123}}, {status: {gold: 0}}, {status: {gold: 500}}]
});

Unregistering a model

Calling engine.unregisterModel(player) will unregister this model and remove the global variable that is associated with it.

Mocking communication between JavaScript and the engine backend (Events and Calls)

Mocking Events

An Event can have multiple handlers both in the backend and JS. You can register a JS handler using the engine.on method:

engine.on('PlayerScoreChanged', function (new_score) {
    var scoreDisplay = document.getElementById('scoreDisplay');
    scoreDisplay.innerHTML = new_score;
});

You can also register a mock handler for a given event, that will be invoked only when there isn’t any registered JS or backend handler using the method engine.mockEvent(eventName, callback).

engine.on('PlayerScoreChanged', function (new_score) {
    var scoreDisplay = document.getElementById('scoreDisplay');
    scoreDisplay.innerHTML = new_score;
});

engine.mockEvent('PlayerScoreChanged', function(new_score) {
    console.log('PlayerScoreChanged mock');
});

engine.mockEvent('PlayerHealthChanged', function(new_health) {
    console.log('PlayerHealthChanged mock');
});

// The mock will not be called because there is a JS handler for this event
engine.trigger('PlayerScoreChanged', 123);

// The mock handler will be called
engine.trigger('PlayerHealthChanged', 100);

Mocking Calls

Calls can have only one handler in the backend and are used to return results from the backend to JavaScript via Promises.

You can register a mock handler in JS that will be invoked if there is no backend handler, using the method engine.mockCall(eventName, callback). Don’t forget to add __Type property to the return value if you’re returning a backend user value to be able to use it in functions that accept such type.

engine.mockCall("getActiveWeapon", function(playerIdx) { 
    return { __Type: "Weapon", dmg: 100, name: "Sniper"} 
});

engine.call("getActiveWeapon", 1).then(function (weapon) {
    engine.trigger("banWeapon", weapon); // some backend handler that accepts Weapon
});