Skip to content
SiteEmail

This article covers how to develop and test your UI without a running game engine by mocking data models , events , and calls entirely in JavaScript. You will learn what a model is, how to detect your environment, and how to simulate the full data lifecycle that would normally come from the game’s backend.

Frontend developers should not have to launch a heavy 3D game engine just to test whether a health bar fills correctly or an inventory grid renders properly. By mocking data models directly in JavaScript, you can build, test, and iterate on your UI completely independently of the game backend. Once you are satisfied with the result, the same UI code works without modification when connected to the real engine.

Before diving into mocking, it is important to understand what a model is in the context of Gameface.

A model is a named JavaScript object that represents a slice of game state. It could hold anything the UI needs to display: player health, inventory items, character names, quest progress, or settings. In a production environment, the game engine (the C++ backend) exposes these models to the frontend. During development, you create them yourself in JavaScript.

What makes models powerful is data-binding . Gameface can connect a model’s properties directly to the DOM using special HTML attributes. Instead of manually writing document.getElementById("health").innerText = player.health every time a value changes, you declare the relationship once in your HTML and the engine keeps the DOM synchronized automatically.

Here is a minimal example to illustrate the concept:

mock-setup.js
// Create a model named 'PlayerModel' with initial game state
engine.createJSModel("PlayerModel", {
health: 100,
name: "Warrior",
});
PlayerHud.tsx
import TextBlock from '@components/Basic/TextBlock/TextBlock';
const PlayerHud = () => (
<>
<TextBlock attr:data-bind-value="{{PlayerModel.name}}" />
<TextBlock attr:data-bind-value="{{PlayerModel.health}}" />
</>
);

The double-curly-brace syntax ({{PlayerModel.health}}) is Gameface’s data-binding expression. You will explore this in depth in the Data-Binding Basics article. For now, the key takeaway is: models are the data source, and data-binding is the mechanism that connects them to the DOM.

A well-structured project should automatically enable mock data only when the UI is not running inside the actual game engine. This way you keep one codebase that works in both contexts without manual toggling. There are two simple ways to detect your environment:

  1. Using the engine’s own event system.
  2. Using Vite’s environment variables.

The most straightforward approach uses the engine’s own event system. The game backend can fire a specific event (e.g., "InEngine") when the view loads inside the engine. If that event never fires, you know you are running in a standalone browser or the Gameface Player.

environment.js
let isInEngine = false;
engine.whenReady.then(() => {
engine.on("InEngine", () => {
isInEngine = true;
});
// The 'InEngine' event fires synchronously during whenReady
// if the backend triggers it on OnReadyForBindings.
if (!isInEngine) {
console.log("Running outside the engine. Loading mock data...");
// Create Mocked Models here
initializeMocks();
} else {
console.log("Running inside the engine. Using real game data.");
}
});

To create a mock model, use the engine.createJSModel(modelName, modelObject) method. This registers a named JavaScript object as a data-binding model and exposes it as a global variable.

mock-setup.js
engine.createJSModel("player", {
maxHealth: 100,
currentHealth: 75,
heroType: "warrior",
pet: {
type: "wolf",
name: "Fang",
},
traits: [
{ strength: 10, intelligence: 20 },
{ strength: 15, intelligence: 17 },
],
});

After calling createJSModel, a global variable player becomes available. You can immediately bind its properties in your HTML:

player-hud.html
<div class="player-card">
<h2 data-bind-value="{{player.heroType}}"></h2>
<div class="health-bar" data-bind-style-width="{{player.currentHealth}}"></div>
<p>Pet: <span data-bind-value="{{player.pet.name}}"></span></p>
</div>
<!-- Iterating over arrays -->
<ul data-bind-for="trait:{{player.traits}}">
<li>STR: <span data-bind-value="{{trait.strength}}"></span></li>
</ul>

In a real game, the backend pushes updates to data-bound models automatically. When you are mocking in JavaScript, you must manually tell the engine that a model has changed and that it should update the DOM.

Simply changing a property on the model object does nothing to the DOM by itself:

// This changes the JS object but the DOM still shows 75
player.currentHealth = 50;

To force the UI to reflect the new value, you need a two-step process:

  1. Call engine.updateWholeModel(model) to mark the model as “dirty” (i.e., tell the engine “this model’s data has changed and needs re-reading”).

  2. Call engine.synchronizeModels() to flush all dirty models to the DOM. The engine walks through every model you marked in step 1, compares the current values to the bound DOM nodes, and applies the updates.

Here is the correct pattern:

update-health.js
// Update the property
player.currentHealth = 50;
// Notify the engine that this model changed
engine.updateWholeModel(player);
// Flush all pending model updates to the DOM
engine.synchronizeModels();

Sometimes the backend model is only partially implemented while you are building the UI. You might have a player model coming from the engine with health and name, but the UI also needs gold and questLog that the backend team has not exposed yet.

Use engine.createOrMergeModel(name, jsObject) for this scenario. It checks whether a model with that name already exists:

  • If the model does not exist, it creates it (same as createJSModel).
  • If the model already exists, it adds only the missing properties from your object. Existing values remain untouched.
augment-model.js
// Assume the backend already created a 'player' model with:
// { maxHealth: 100, currentHealth: 50, weapon: { name: "sniper" } }
// This adds 'gold' and 'weapon.damage' without touching existing values
engine.createOrMergeModel("player", {
gold: 500,
currentHealth: 999, // Ignored: already exists, stays at 50
weapon: {
name: "rifle", // Ignored: already exists, stays at "sniper"
damage: 75, // Added: did not exist before
},
});

When a model is no longer needed (for example, when transitioning between screens or cleaning up after a match), you can remove it entirely:

engine.unregisterModel(player);

This removes the model from the data-binding system and deletes the associated global variable. Any DOM elements still bound to this model will no longer receive updates.

In the previous article, you learned about engine.on, engine.trigger, and engine.call for event communication. When developing without the engine, you need a way to simulate the data flow. There are two approaches to this:

The Universal Approach: engine.on + engine.trigger

Section titled “The Universal Approach: engine.on + engine.trigger”

An important fact to internalize: engine.on handlers fire regardless of where the event originates. Whether the event is triggered from your own JavaScript code (engine.trigger) or from the game’s backend, the same engine.on handler catches it.

This means your standard development workflow is straightforward:

health-handler.js
// This handler is your real production code.
// It runs the same way whether the trigger comes from JS or from the backend.
engine.on("PlayerHealthChanged", (newHealth) => {
document.getElementById("healthDisplay").innerHTML = newHealth;
});

During development, since there is no backend to trigger the event, you simulate it yourself:

dev-trigger.js
// Simulating the backend during dev - fire the event manually
engine.trigger("PlayerHealthChanged", 80);

In production, the backend fires PlayerHealthChanged instead, your engine.on handler catches it identically, and you simply remove your dev engine.trigger calls. This covers the majority of frontend mocking needs.

Mocking Backend-Side Behavior: engine.mockEvent

Section titled “Mocking Backend-Side Behavior: engine.mockEvent”

engine.mockEvent solves a different problem. It is not about mocking your own frontend handlers. It is about simulating what the backend would do in response to events your UI sends out.

Consider this scenario: your UI has a “Quit Game” button that fires engine.trigger("UI_QuitGame"). In production, a backend handler catches that event and shuts down the game. During development mockEvent lets you simulate that backend reaction:

mock-backend-reaction.js
// Simulate what the backend would do when the UI signals "quit"
engine.mockEvent("UI_QuitGame", () => {
console.log("Mock backend: Received quit signal. Cleaning up...");
showExitConfirmation();
});

The key behavior of mockEvent: once a real backend handler is registered for "UI_QuitGame", the mock automatically becomes inactive. You do not need to remove it manually.

ui-quit-button.js
document.getElementById("quit-btn").addEventListener("click", () => {
// During dev: the mockEvent handler fires
// In production: the real backend handler fires instead
engine.trigger("UI_QuitGame");
});

The same pattern applies to engine.call. Use engine.mockCall(eventName, callback) to simulate what the backend would return when your UI requests data. The mock only responds when no real backend handler exists.

Your mock callback should return the data that the real backend would return. This value becomes the resolved value of the Promise returned by engine.call:

mock-calls.js
// Simulate the backend response for a weapon data request
engine.mockCall("getActiveWeapon", (playerIdx) => {
return {
__Type: "Weapon",
name: "Sniper",
damage: 100,
rarity: "legendary",
};
});
// Your UI code works identically in dev and production
engine.call("getActiveWeapon", 1).then((weapon) => {
console.log(`Equipped: ${weapon.name} (${weapon.damage} dmg)`);
});

This is where mockCall is genuinely useful: your UI uses engine.call to request data, and during dev the mock provides a response. In production, the real backend handler resolves the Promise instead, and the mock becomes inactive automatically.