Engine Communication: Setup & Events
This article explores how the cohtml.js library bridges the connection between the UI and the game engine, how it behaves when Gameface is not attached, how to handle the asynchronous lifecycle of the engine, and how to utilize different communication patterns to synchronize your UI with the game state.
The Layer of Communication
Section titled “The Layer of Communication”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.
The cohtml.js library provides that connection and exposes a global engine object. Every native communication passes through it, from updating a
player’s health bar to triggering a “Quit Game” sequence.
Getting cohtml.js
Section titled “Getting cohtml.js”Since Gameface 3.1.2 , the SDK loads cohtml.js into every page. No script tag or import is required, and the engine object is
available directly. The rest of this article assumes it is present.
Previewing in a Browser
Section titled “Previewing in a Browser”The SDK loads cohtml.js, and a browser has no SDK. Opening your UI in Chrome rather than in the Player leaves cohtml.js absent and the engine
object undefined, so any call against it throws.
Including cohtml.js manually covers that case. On load it checks whether Gameface is attached. Finding nothing, it replaces its own communication
methods with mock implementations, which resolve against registered mocks and otherwise do nothing.
Data-binding does not function in a browser. No Gameface instance exists to push models or receive calls, and nothing makes a browser
understand them. What the mock implementations provide is a page that loads and runs instead of failing at the first engine call, which is enough to
check whether a layout problem belongs to your markup or to Gameface.
engine.isAttached reports this state directly. It is false in a browser and true when the SDK is present, so UI code can branch on it.
IDE Autocomplete & TypeScript Support
Section titled “IDE Autocomplete & TypeScript Support”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.jsonfile 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
engineobject in.file.js /// <reference path="correct/path/to/cohtml.d.ts" />
For TypeScript projects, copy cohtml.d.ts into your project and include it from your tsconfig.json:
{ "compilerOptions": { "target": "ES2015" }, "include": ["src/**/*", "lib/cohtml.d.ts"]}The compiler picks the declarations up from there and provides autocomplete and documentation for the engine object in every file, without any
reference comments.
Development Environment & Tools covers the rest of the Gameface typings, including the DOM definitions that stop your editor suggesting browser APIs Gameface does not implement.
Initialization & Lifecycle
Section titled “Initialization & Lifecycle”Availability of the engine object does not mean the game is ready to receive calls.
Your JavaScript may execute the moment the page loads, 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.
engine.whenReady.then(() => { console.log("Gameface bridge is active.");
// Safe place to register listeners or request initial game data. initializeMenu();});Engine Event Subscription
Section titled “Engine Event Subscription”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.
function updateScore(newScore) { const scoreEl = document.getElementById("score-display"); scoreEl.innerText = `Score: ${newScore}`;}
// Listen for the 'PlayerScoreChanged' event from the gameengine.on("PlayerScoreChanged", updateScore);Unsubscribing and Cleanup
Section titled “Unsubscribing and Cleanup”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.
engine.trigger (Fire-and-Forget)
Section titled “engine.trigger (Fire-and-Forget)”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.
engine.call (Request-Response)
Section titled “engine.call (Request-Response)”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 DOMengine.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); }}When to Use Which
Section titled “When to Use Which”| Pattern | Response | Game-side Handlers | Use Case |
|---|---|---|---|
engine.trigger | None (fire-and-forget) | Multiple | Broadcasting actions (button clicks, UI state changes) |
engine.call | Promise with data | Single | Requesting specific data (player stats, inventory, settings) |
Reserved Event Names
Section titled “Reserved Event Names”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 Name | Purpose |
|---|---|
'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. |
The Wildcard Listener (*)
Section titled “The Wildcard Listener (*)”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 trafficengine.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.
The Unhandled Event (_Unhandled)
Section titled “The Unhandled Event (_Unhandled)”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}"`);});© 2026 Coherent Labs. All rights reserved.