Engine Communication: Setup & Events
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.
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.
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.
Including the cohtml.js Library
Section titled “Including the cohtml.js Library”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 include it in your project you need to reference the library before the framework’s loading script. You have two options:
Option 1: Add it to the index.html file before the index.js file
Section titled “Option 1: Add it to the index.html file before the index.js file”<!DOCTYPE html><html lang="en"> <head></head> <body> <div id="root"></div> <script src="js/cohtml.js"></script> <script src="./index.tsx" type="module"></script> </body></html>Option 2: Import it in the index.jsx/tsx file before the render function
Section titled “Option 2: Import it in the index.jsx/tsx file before the render function”import { render } from 'solid-js/web';import Menu from './Menu';import './index.css';import 'js/cohtml.js';
const root = document.getElementById('root');
render(() => <Menu />, root!);Both methods will expose the engine object globally and allow you to use it in every component of your project.
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 you simply need to reference the cohtml.d.ts file in the cohtml.js file.
/// <reference path="correct/path/to/cohtml.d.ts" />The TypeScript compiler will automatically pick up the cohtml.d.ts file and use it to
provide autocomplete and documentation for the engine object regardless of the file it is in.
Initialization & Lifecycle
Section titled “Initialization & Lifecycle”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.
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.