2.9.16.0
Coherent GT for UE4
Asynchronous mode usage

This tutorial will show you how to design the communication between the game and the UI and how to write code that runs correctly under Coherent GT's asynchronous mode.

The sample will simulate the following:

  1. Going from the main game menu to the in-game HUD.
  2. Continuously updating UI values from the engine.
  3. Calling UE4 functions from JavaScript.

We will also take a separate look at what the designers and the game programmers should do.

The tutorial here follows the creation of the AsyncSample_Map.umap found in the provided sample game. Note that some parts of the example map are skipped for brevity and simplicity (such as styling and animation) in favour of additional emphasis put on the HTMl structure and JS <–> UE4 communication

The scenario

The page we are going to build contains the following:

  1. A main menu, containing the game's name and a single button which starts the game upon being clicked. Clicking the button will trigger a StartGame event in the game, which in turn will initialize whatever's needed and trigger a GameStarted event back at us.
  2. An in-game HUD, containing:
    • Party member interface containing 4 allied player's portraits, health and mana bars. The health and mana values for each party member will be continuously updated from the engine.
    • An in-game store, that displays what items are available for purchasing via their icon and name. Upon clicking an item in the store, we'll ask the game how many items are still available and what their price is and display that in a popup above the item's icon.

When building the UI we will stay away from third party libraries (such as jQuery) as they are too heavy computation-wise and not made with performance in mind. Instead, we'll make use of the provided methods of the document node such as document.getElementById and document.querySelector. Consult the performance guide to see what libraries to avoid when building your UI.

Why the GameStarted event?

Note that the GameStarted event is of great importance. A naive implementation might decide that the game has begun immediately after StartGame has been triggered but that will not be the case if the game is running under asynchronous mode. By allowing the game to explicitly notify the UI when it has begun one can safely run the game under both synchronous and asynchronous mode.

The UI Designer Point of View

We'll begin by creating a sample page which we'll test in the browser using the mocking functionality.

The HTML

We'll split the entire UI in two <section>:

<section id="pregame-menu">
    ...
</section>

<section id="ingame-hud">
    ...
</section>

The #pregame-menu section is easier so let's start with it. We need a clickable button and a heading with the game's name:

<section id="pregame-menu">
    <h1>The awesomest game ever</h1>
    <button id="start-game-button">Start new game</button>
</section>

Next up, our in-game HUD will consist of party group interface:

<section id="ingame-hud" class="hidden" >
    <div id="raiding-party">
        <div class="party-member-container">
            <img class="party-member-portrait" src="party_member_1.png"></img>
            <div>
                <div class="meter orange">
                    <span style="width: 75%"></span>
                </div>
                <div class="meter blue">
                    <span style="width: 75%"></span>
                </div>
            </div>
            <span>Party Member 1</span>
        </div>
    <!- - Code above repeated 3 more times -->
    </div>
    <!- - Store goes here in a second -->
    ...
</section>

Quick explanation of the code above - the #raiding-party will contain all of the raiding party interface. For each party member, we add an <img> for the portrait and two bars using a <div class="meter">. A <span> with variable width will fill the bars. We've added the "hidden" class (defined as .hidden {opacity: 0;}) to the section because it won't be visible until the start button has been clicked.

We also need a store:

<table class="store">
    <thead>
        <tr>
            <th colspan="99">Our store</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td align="center">
                <img src="item_icon_1.png"></img>
                </br>
                <span>Item 1</span>
            </td>
            <!- - Some more table items -->
        <!- - Some more table rows -->
    </tbody>
</table>

And a popup element to display the extra info about each item:

<!- - This element is hidden by default and is used to show a popup with info about a specified item -->
<div id="item-data-popup">
    <span>Available items </span>
    <span id="popup-available-item-count">5</span>
    <br>
    <span>Price per unit </span>
    <span id="popup-price-per-unit">20</span>
</div>

Note that for brevity and simplicity, the sample uses hardcoded values for the items and the party members. In production, you might want to use some kind of template-based runtime HTML generation.

The JS code

Time to make the page interactive! Add some scripts:

<!- - The core Coherent GT library -->
<script src="../coherent.js"></script>
<!- - Additional Coherent GT library providing mocking in the browser -->
<script src="../coherent.mock.js"></script>
<!- - The script containing our HUD's bussiness logic -->
<script src="hud.js"></script>

Now to make sure that clicking the start button actually does something, we'll edit hud.js:

// Make sure that any triggered event happens after the engine is ready
engine.on("Ready", function () {
    document.querySelector("#start-game-button").addEventListener("click", function () {
        engine.trigger("StartGame");
    });
}

We also need to respond to the GameStarted event that the engine will raise:

var beginGame = function () {
    // Remove the main menu
    document.querySelector("#pregame-menu").classList.add("hidden");
    // Show the in-game HUD
    document.querySelector("#ingame-hud").classList.remove("hidden");
};

engine.on("GameStarted", function () {
    beginGame();
});

After the game has begun, it will continuously send RaidPartyDataUpdate events containing the new values of health and mana for each member. We need to slightly modify the previous code:

// Two arrays containing the health and mana bars of the party members
var healthBars = null,
    manaBars = null;
engine.on("GameStarted", function () {
    beginGame();
    // Cache the health and mana bars for faster access
    healthBars = document.querySelectorAll("#raiding-party .meter.orange span");
    manaBars = document.querySelectorAll("#raiding-party .meter.blue span");
});

// RaidPartyDataUpdate will be triggered with the index of the party member
// and his updated health and mana points
engine.on("RaidPartyDataUpdate", function (index, hp, mp) {
    // Note that healthBars / manaBars are instances of NodeList, not plain arrays
    healthBars.item(index).style.width = (hp * 100) + "%";
    manaBars.item(index).style.width = (mp * 100) + "%";
});

Things are starting to get in shape. We only need to implement the store-related functions now:

var beginGame = function () {
    var onStoreItemClicked = function (index) {
        var popup = document.querySelector("#item-data-popup");
        // Magical code to place the popup above the store item
        ..
        popup.classList.remove("hidden");

        // Ask the game for data about the item at the given index
        var promise = engine.call("GetItemData", index);
        // When the game returns the data, update the popup
        promise.success(function (data) {
            // Update the data in the popup
            popup.querySelector("#popup-available-item-count").textContent = data.availableItems;
            popup.querySelector("#popup-price-per-unit").textContent = data.pricePerUnit;
        });
    };

    var attachStoreHandlers = function () {
        // Attach a handler to every item in the store
        var inventoryCells = document.querySelectorAll(".store td");
        for (var i = 0; i < inventoryCells.length; i++) {
            inventoryCells.item(i).addEventListener("click", onStoreItemClicked.bind(undefined, i));
        }
    };

    // Remove the main menu
    document.querySelector("#pregame-menu").classList.add("hidden");
    // Show the in-game HUD
    document.querySelector("#ingame-hud").classList.remove("hidden");
    attachStoreHandlers();
};

The engine.call("GetItemData", index) deserves some attention. Calling engine.call will return a promise object. Promises are a generic concept widely used in asynchronous programming. They represents the output of some computation which hasn't necessarily happened yet. They provide ways to get notified whenever their state changes.

At their most basic, promises guarantee that:

  • A promise can only succeed or fail once. It cannot succeed or fail twice, neither can it switch from success to failure or vice versa
  • If a promise has succeeded or failed and you later add a success/failure callback, the correct callback will be called, even though the event took place earlier

The Promise type implemented in coherent.js is sticks to the Promises/A standard but there are small differences.

Some of the operations allowed on our promises are:

  • promise.success(func) - registers func to be notified whenever the promise has been resolved. func will be called with a single argument - whatever the corresponding function on the game side returned
  • promise.otherwise(func) - registers func to be notified whenever the corresponding function on the game side reported an error
  • promise.then(successFunc, otherwiseFunc) - equivalent to promise.success(successFunc); promise.otherwiseFunc(otherwise)

By attaching our success handler using:

promise.success(function (data) {
    // Update the data in the popup
    popup.querySelector("#popup-available-item-count").textContent = data.availableItems;
    popup.querySelector("#popup-price-per-unit").textContent = data.pricePerUnit;
});

we update the popup whenever an item is clicked.

Mocking it up

Let's say that these pesky game programmers haven't completed their part of the deal, but you need to test the just created UI. You can then mock the communication with the engine using engine.mock and test the interface in Google Chrome:

// Helper function that clamps the given value to the range [min, max]
var clamp = function(min, max, value) {
  return Math.min(Math.max(value, min), max);
};

// engine.isAttached is true only if we are running inside Coherent GT
// so !engine.isAttached tests whether we are running in the browser
if (!engine.isAttached) {
    engine.mock("StartGame", function () {
        // Initialize our dummy game with the generic data
        var healthLevels = [];
        var manaLevels = [];
        for (var i = 0; i < PARTY_SIZE; i++) {
            healthLevels[i] = manaLevels[i] = 0.5;
        }
        setInterval(function () {
            // Randomly update the health and mana of our party members and trigger an update
            // event every 50 ms (50 ms was chosen arbitrarily)
            for (var i = 0; i < PARTY_SIZE; i++) {
                healthLevels[i] = clamp(0, 1, healthLevels[i] + Math.random() * 0.05 - 0.025);
                manaLevels[i] = clamp(0, 1, manaLevels[i] + Math.random() * 0.05 - 0.025);
                engine.trigger("RaidPartyDataUpdate", i, healthLevels[i], manaLevels[i]);
            }
        }, 25)
        // Signal the UI the game has been initialized
        engine.trigger("GameStarted");
    });
    engine.mock("GetItemData", function (index) {
        // Return some dummy values
        return {
            availableItems: index * index,
            pricePerUnit: index * 10
        };
    })
}

Go launch CoUIGTTestFPS/Content/uiresources/async_sample/hud.html in Chrome to see the code above in action. The expected result (after adding proper styling and images) is:

async_sample_mocked.png
The async sample mocked in the browser

Go ahead and click on the start game button or on the store items and see them update.

The game developer point of view

Setting up the HUD

Your job as a game developer is to provide the data to the UI. Let's see how do this effortlessly. In order to cover as much ground as possible, some of the code will be in Blueprints and some in C++.

To setup the HUD, we create a new HUD class inheriting from CoherentUIGTGameHUD:

#include "CoherentUIGTGameHUD.h"
#include "CoUIGTTestFPSAsyncSampleHUD.generated.h"

UCLASS()
class ACoUIGTTestFPSAsyncSampleHUD : public ACoherentUIGTGameHUD
{
    GENERATED_UCLASS_BODY()
private:
    // Used to bind GetItemData(int32) to engine.call("GetItemData")
    UFUNCTION()
    void BindUI();
    TMap<FString, int32> GetItemData(int32 ItemIndex);
};

Implementing this class is straightforward:

ACoUIGTTestFPSAsyncSampleHUD::ACoUIGTTestFPSAsyncSampleHUD(const FObjectInitializer& PCIP)
    : Super(PCIP)
{
    // When the underlying Coherent GT View is ready for bindings, bind our function
    CoherentUIGTHUD->ReadyForBindings.AddDynamic(this, &ACoUIGTTestFPSAsyncSampleHUD::BindUI);
}

void ACoUIGTTestFPSAsyncSampleHUD::BindUI()
{
    // Coherent::UIGT::View::BindCall magically hooks up our method to the JavaScript event
    CoherentUIGTHUD->GetView()->BindCall(
        "GetItemData",
        Coherent::UIGT::MakeHandler(this, &ACoUIGTTestFPSAsyncSampleHUD::GetItemData));
}

TMap<FString, int32> ACoUIGTTestFPSAsyncSampleHUD::GetItemData(int32 ItemIndex)
{
    // This example uses TMap to send a JSON-like object to the UI for simplicity
    // but you may also consider using a FIntPoint for better performance
    TMap<FString, int32> Data;
    // Provide some dummy values.
    // In the real world this function will consult a database.
    Data.Add("availableItems", ItemIndex * ItemIndex);
    Data.Add("pricePerUnit", ItemIndex * 10);
    return Data;
}

Using blueprints to finish the deal

Now we need to start using this new class. Go to World Settings -> Gamemode Override and replace whatever the current HUD class with ACoUIGTTestFPSAsyncSampleHUD. Of course doing so requires that you also use a GameMode class that's implemented in Blueprint, otherwise you'll need to create one more C++ class for overriding the GameMode. See details here.

We'll implement the rest of HUD in Blueprint so open up the level blueprint (Blueprints -> Open Level Blueprint). The first step would be to set up the input forwarding to our HUD. Doing so is described in Input (Blueprints) so we won't spend much time here.

Secondly, we need to get the HUD instance and set the page which we'll be showing. In our case, that would be coui://uiresources/async_sample/hud.html.

settingup_blueprints_async_sample.png
Setting Up the HUD

We also create an object of type CoherentUIGTJSEvent called JSData. We'll use that to send the data to JavaScript but we only need to create a single instance and bind our arrays to it - this object holds pointers to any bound arrays and objects so it always uses the latest and greatest data. Primitive types (numbers, booleans and strings) on the other hand are copied and must be updated per frame but we won't use them in this sample.

updating_ui_async_sample.png
Creating JS Event data

Thirdly, just like we bound GetItemData(int32) in the C++ code, we need to bind a handler to StartGame. To this we create a custom Blueprint event and bind it via Bind Event To JavaScriptEvent. This is also shown in Example_Map.umap (like the input forwarding) so there's no need to get into details. The important thing is the StartGame handler which does two things - populates the stats of each party member and sets HasGameStarted to true.

register_startgame_handler.png
Registering the StartGame handler

The final part would be to update the party's stats and trigger RaidPartyDataUpdate. We do so by looping over all party members, updating their health and mana and finally sending that to JS:

loop_party_members_async_sample.png
Looping over party members
updating_ui_async_sample.png
Updating the values in the UI

Summary

This completes our journey through the vast UI-binding field. It is important to remember that in this sample we never assumed anything about when exactly an event will be triggered - we only care that it will be triggered at some point. This protects us from failures under asynchronous node. All in all, here's what we did:

  1. Created the basic HTML structure and JS code
  2. Mocked the application which allows us to iterate faster in Google Chrome instead of playing the map in UE4.
  3. Created a simple C++ HUD class and bound engine.call("GetItemData") to GetItemData(int32).
  4. Overrode the default HUD class
  5. Bound the other used events (from engine.trigger) to functions
Note
Reloading the page (e.g. hitting F5 in the Inspector) will reset the UI state but our Blueprint code will still believe the UI is prepared for receiving data from it so it will keep sending. This will cause errors in the JavaScript. A simple fix would be to have the JS code ask the game about the actual current state but the sample doesn't do that to improve readability. In your actual game you will likely want to implement the latter so that you can iterate quickly by reloading the page.