2.9.16.0
Coherent GT for UE4
Building a Shop UI

The sample demonstrates how you can make Item Shop UI with Coherent GT's components and data binding systems. The component system allows you reuse UI pieces and stitch them together whereas the data binding system lets you easily synchronize the values in the UI with the game. The item shop shows a sortable grid of purchasable items, split between several pages. Items can be purchased as long as the user has enough currency and this is instantly reflected in the grid. The pagination, sorting and data come from the back-end and they are accessed through the front-end.

Switching between pages tells the back-end to return another set of items according to the page. Sorting returns the items in order according to given criteria - by price or by ownership.

Navigation is done using the mouse, keyboard or gamepad. Navigating updates the preview panel - item image, name, quality, type, description and icon.

overview.png

Actions and controls

Open Shop

After loading the scene, press V to open/hide the shop screen. With the gamepad - press the Start button.

Item navigation / focus

  • Focus an item by clicking with the mouse.
  • Navigate around the grid using the arrow keys on the keyboard or the left analog stick on the gamepad.

Confirmation dialog box for purchasing an item

buy_notification.png
Double clicking an item, hitting ENTER on the keyboard or A on the gamepad, opens a modal message and disables further grid navigation to prevent the user from selecting another element while the message is still active.

Once bought, this is reflected on the back-end side which changes the owned state of this item.

Closing the dialog box

Closing the message re-enables navigation through the items. To close the message click on the No button, press Esc on they keyboard or B on the gamepad.

Confirming purchase for the selected item

While the confirmation dialog box is opened - Yes changes the modal message and after some delay this action buys the item with game.buyItem(inventory.resources[currentIndex].id). The performed action tells the back-end to change .isOwned to true.

To confirm the purchase click on the Yes button with the mouse, use the keyboard's Enter key or press the A on the gamepad.

Page navigation

  • With the mouse, clicking on the arrows or on the dots at the bottom of the item list changes the pages accordingly.
  • Page navigation with the keyboard is done with Q (previous) and E (next) keys.
  • Gamepad - LB (previous) or RB (next) button.

Sorting items - by price or by ownership

  • Using the mouse - clicking on the By Price or By Owned.
  • With the gamepad - pressing Y button sorts by price and pressing X sorts by owned.

Locked or already owned items cannot be bought - "if" statements check the resource isOwned and isLocked state and a notification appears to the user that the action cannot be executed.

locked_states.png

Integration in the scene

Given that the HTML is ready and styled, lets start with the front-end part of the integration.

Get/set the shop name:

The data-bind-value will set the div's text content taking the name from the inventory.name resource.

<div class="shop-name" data-bind-value="{{inventory.name}}"></div>

Components / shop items

Since we'll have a lot of items we can create an item component and reuse that for all of them. Here's the code for the component:

<script type="text/html" id="resource-template">
<div class="item-box" data-bind-class-toggle="label:{{this.isNew}}">
<div class="ib-image" style="background-image: url(images/u10.png);">
<div class="ib-brand">Brand &amp; Set</div>
</div>
<div class="ib-price">
<div class="" data-bind-class-toggle="ib-owned:{{this.isOwned}}"></div>
<div class="" data-bind-class-toggle="ib-locked:{{this.isLocked}}"></div>
<div class="ib-price-value" data-bind-value="{{this.gold}}"></div>
</div>
</div>
</script>

The <script> tag is required as otherwise it will be parsed as a regular element but instead we want to just define it as a component. Note how we use data-binding system inside with {{this.isNew}}. this here refers to each item or in our terminology - the component's model. When the component is instantiated it will be bound to a model (in our case, an item) and the data will be synced with the model's data.

Now that we have the component, let's use it. Creating the entire grid of elements is as simple as:

<div class="res-wrapper" data-bind-for="res:{{inventory.resources}}">
<div class="selected" data-bind-template="resource" data-bind-model="{{res}}"></div>
</div>

Note how we use data-bind-for above. This will cause the inventory.resources array to be iterated and its contents repeated. The pair of data-bind-template / data-bind-model on the inner <div> will cause an instance of the "resource" component to be spawned for each item.

To learn more on how each of the data-bind-for and other data binding attributes work see our native SDK docs.

Instantiating template

The template is registered automatically with the attribute data-bind-template-name.

This is the ResourceComponent.html file in components folder :

<script type="text/html" data-bind-template-name="resource">
<div class="item-box" data-bind-class-toggle="label:{{this.isNew}}">
<div class="ib-image" style="background-image: url(images/u10.png);">
<div class="ib-brand">Brand &amp; Set</div>
</div>
<div class="ib-price">
<div class="" data-bind-class-toggle="ib-owned:{{this.isOwned}}"></div>
<div class="" data-bind-class-toggle="ib-locked:{{this.isLocked}}"></div>
<div class="ib-price-value" data-bind-value="{{this.gold}}"></div>
</div>
</div>
</script>

We import this file with <link rel="import" href="components/ResourceComponent.html" into the main html file - shop_components.html

Placing the component element

The items-container element is the wrapper inside which the components will be populated.

<div id="items-container" class="items-container layer">
<div class="res-wrapper" data-bind-for="res:{{inventory.resources}}">
<div class="selected" data-bind-template="resource" data-bind-model="{{res}}"></div>
</div>
</div>

This wrapper contains the populated items and sets some styles for their arrangement. It is also used to control the grid behaviour.

Components Summary

The end result should look like this:

initialised.png

To learn more about component system see our native documentation - Data Binding - Components.

Pagination

For each page the inventory has, an element will be created. Using data-bind-for:

<div id="navDots" class="pagination layer">
<div class="page-button" data-bind-for="page:{{inventory.pages}}"></div>
</div>

Optimization:

  • To avoid performance spikes when displaying the inventory, we've added an optimization that effectively distributes the cost of the display over several frames by showing each row separately. The optimization is toggleable via the button in the sample.

Back-end

The main idea is that only the visible resources are exposed to the front end.

In the C++ we have an FInventoryResources class responsible for sorting and pagination. When an item is bought we set its isOwned property to true. When a request for sorting is received we sort all the items in the inventory, after that we change the ExportedInventory which contains only the resources that are visible on the current page. The front end has access only to this visible resources and displays them. When changing page we place the visible items on this page in the ExportedInventory.

Every resource has an ID. The selling of a resource is done by this ID property. Sold resources have a boolean IsOwned property set to true.

Each resource needs to be updated with UpdateWholeModel when its properties change, for example when sorting, changing the page or buying an item. After updating the resources - SynchronizeModels must be called.

Four main objects are used which are also exported to the JavaScript.

  • FResource which contains all the needed information for the resource like, Quality, Type, Description, IsLocked and so on.
void CoherentBind(Coherent::UIGT::Binder* Binder, FResource* Resource)
{
if (auto type = Binder->RegisterType("Resource", Resource))
{
type.Property("gold", &FResource::Gold)
.Property("isLocked", &FResource::IsLocked)
.Property("isOwned", &FResource::IsOwned)
.Property("isNew", &FResource::IsNew)
.Property("id", &FResource::ResID)
.Property("quality", &FResource::Quality)
.Property("type", &FResource::Type)
.Property("name", &FResource::Name)
.Property("description", &FResource::ItemDescription)
.Property("image", &FResource::ImageName);
}
}

We also have the following helper classes:

  • FPage - represents each page. We store an array of these objects to represent each page with a circle dom element in the front-end using data-bind-for which works with arrays.
  • FInventoryResources - represents the resources and pages that are exported to the front-end.
  • ExportedInventory - represents the resources that are curently visible on the selected page.
void CoherentBind(Coherent::UIGT::Binder* Binder, FInventoryResources* Inventory)
{
if (auto type = Binder->RegisterType("Inventory", Inventory))
{
type.Property("name", &FInventoryResources::Name)
.Property("resources", &FInventoryResources::ExportedInventory)
.Property("pages", &FInventoryResources::Pages);
}
}
  • Game -> The main object which has the logic -> changePage, sort, sortByOwner, buyItem and so on.
void CoherentBind(Coherent::UIGT::Binder* Binder, ACoUIGTShopComponentHUD* Game)
{
if (auto type = Binder->RegisterType("Game", Game))
{
type.Method("changePage", &ACoUIGTShopComponentHUD::ChangePage)
.Method("sort", &ACoUIGTShopComponentHUD::SortByGold)
.Method("sortByOwner", &ACoUIGTShopComponentHUD::SortByOwnership)
.Method("buyItem", &ACoUIGTShopComponentHUD::BuyItem);
}
}

At the beginning, initialization of all the needed objects is done with random values.