Skip to content
SiteEmail

This article explores how the cohtml.js library bridges the connection between the UI and the game engine, how to handle the asynchronous lifecycle of the engine, and how to utilize different communication patterns to synchronize your UI with the game state.

In a typical browser environment the data is usually fetched from a server. In Gameface however the case is a bit different. Since the UI is running in a game engine environment, that engine is the “server” that the UI needs to communicate with.

To establish a connection with the server you need the cohtml.js library. This script must be included in every HTML page that needs to exchange data with the game.

Once this script is loaded, it exposes a global engine object. This object is the single point of entry for all native communication: from updating a player’s health bar to triggering a “Quit Game” sequence passes through it.

The cohtml.js library is included in your Gameface package, typically in the Player/Samples/uiresources/library folder.

To include it in your project you need to reference the library in your HTML file. You must place it before any other scripts to ensure it is loaded first (either in the head or body tags).

<!DOCTYPE html>
<html>
<head></head>
<body>
<div id="health-bar"></div>
<script src="js/cohtml.js"></script>
<script src="js/ui-logic.js"></script>
</body>
</html>

This will expose the engine object globally in your JavaScript code. You can then use it in every script file that communicates with the game engine.

To get the most out of your development environment, you should use the cohtml.d.ts declaration file. This allows editors like VS Code to provide full IntelliSense for the engine API.

For JavaScript projects you have two options depending on your needs:

  • For global autocomplete you can add a jsconfig.json file to your project root and add the following content:

    jsconfig.json
    {
    "compilerOptions": {
    "module": "esnext",
    "target": "esnext",
    "checkJs": true
    },
    "include": ["src/**/*"]
    }
  • For file-specific autocomplete you can add a reference comment to the file you need to use the engine object in.

    file.js
    /// <reference path="correct/path/to/cohtml.d.ts" />

Once you have included the cohtml.js library and the cohtml.d.ts declaration file, you can start using the engine object to communicate with the game engine. However, there is an important timing constraint to be aware of.

Your JavaScript may execute the moment the script is loaded, but the underlying game engine may still be initializing its own systems. Attempting to send or receive data before this handshake is complete will result in failed communication.

The engine provides a specific promise-based lifecycle method: engine.whenReady . You should treat this as the “Main” entry point for your UI logic.

ui-logic.js
engine.whenReady.then(() => {
console.log("Gameface bridge is active.");
// Safe place to register listeners or request initial game data.
initializeMenu();
});

Gameface provides a way to subscribe to events that are triggered by the game engine. This is useful for when you need to react to something that happens in the game (for example, the player took damage, the score changed, or the match ended).

To subscribe to an event sent from the game engine you can use the engine.on(eventName, callback) method.

The engine.on() method takes two required arguments:

  • The name of the event to subscribe to.
  • A JavaScript function that will be executed when the event is triggered.
ui-logic.js
function updateScore(newScore) {
const scoreEl = document.getElementById("score-display");
scoreEl.innerText = `Score: ${newScore}`;
}
// Listen for the 'PlayerScoreChanged' event from the game
engine.on("PlayerScoreChanged", updateScore);

To prevent memory leaks or logic errors, you should unregister your handlers using engine.off(eventName, callback) .

To unregister successfully, you must pass the exact same function reference to engine.off that you used in engine.on.

engine.off("PlayerScoreChanged", updateScore);

Communicating with the Engine: Triggers & Calls

Section titled “Communicating with the Engine: Triggers & Calls”

Communication from the UI back to the game follows two distinct paths: Triggers and Calls . Choosing the right one depends on whether you expect a response from the game engine.

Use engine.trigger(eventName, …args) when you want to signal the game but don’t need any data back. It is a one-way message, similar to a notification: “The player clicked the ‘Resume’ button.”

You can pass arguments alongside the trigger to provide context to the game side:

function handleResumeClick() {
// Send a signal to the game to unpause
engine.trigger("UI_ResumeGame");
}
function selectDifficulty(level) {
// Send a value alongside the event
engine.trigger("UI_DifficultySelected", level);
}

An important characteristic of engine.trigger is that it supports multiple handlers on the game side. When the game registers several functions for the same event name, all of them execute when you trigger it.

function handleQuitClick() {
// One trigger, but the game side may have multiple systems
// reacting to this: save system, audio, networking, analytics, etc.
engine.trigger("UI_QuitGame");
}

This “one signal, many listeners” pattern makes engine.trigger the correct choice for broadcasting UI actions where you don’t need any data back from the game.

Use engine.call(eventName, …args) when you need the game to give you a specific piece of information. This method returns a standard ES6 Promise . The game will perform its logic and then resolve the promise with the requested data.

// Ask the game for the player's name and update the DOM
engine.call("getPlayerName").then((name) => {
const playerName = document.getElementById("playerName");
playerName.innerHTML = name;
});

Unlike engine.trigger, a call supports only one handler on the game side. This is by design: a single function processes the request and returns a value. If you need multiple systems to react, use trigger instead.

You can also use async/await syntax for cleaner sequential logic:

async function loadPlayerProfile() {
try {
const name = await engine.call("getPlayerName");
const level = await engine.call("getPlayerLevel");
document.getElementById("playerName").innerHTML = name;
document.getElementById("playerLevel").innerHTML = `Lvl ${level}`;
} catch (err) {
console.error("Failed to load player profile:", err);
}
}
PatternResponseGame-side HandlersUse Case
engine.triggerNone (fire-and-forget)MultipleBroadcasting actions (button clicks, UI state changes)
engine.callPromise with dataSingleRequesting specific data (player stats, inventory, settings)

Gameface uses a set of internal event names to manage its lifecycle and debugging systems. You must avoid using these names for your own events, as doing so will cause conflicts and unpredictable behavior:

Event NamePurpose
'Ready'Fired internally when the view is ready for bindings. Use engine.whenReady instead.
'*'The wildcard event (see below).
'_Unhandled'Fired when no handler exists for a triggered event.
'_Result'Internal communication for call results.
'_OnReady'Internal lifecycle event.
'_OnError'Internal error propagation.

Sometimes, especially during early development or debugging, you need to see every signal passing through the bridge. Gameface allows you to subscribe to all events simultaneously using the * symbol.

// A global logger for debugging engine traffic
engine.on("*", (...args) => {
const eventName = args[0];
console.log(`[Engine Event]: ${eventName}`, args.slice(1));
});

This wildcard listener acts as a catch-all. It receives every event fired from the game side, with the event name as the first element of the arguments array.

If an event gets triggered but Gameface does not find any callback registered for it, an _Unhandled event will be emitted. This is a useful debugging tool that lets you catch events with no subscriber, helping you identify typos in event names or missing registrations early in development.

engine.on("_Unhandled", (...args) => {
const eventName = args[0];
console.warn(`No handler registered for event: "${eventName}"`);
});