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:
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 page we are going to build contains the following:
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.
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.
We'll begin by creating a sample page which we'll test in the browser using the mocking functionality.
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.
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:
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 returnedpromise.otherwise(func) - registers func to be notified whenever the corresponding function on the game side reported an errorpromise.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.
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:
Go ahead and click on the start game button or on the store items and see them update.
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;
}
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.
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.
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.
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:
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:
engine.call("GetItemData") to GetItemData(int32).engine.trigger) to functions