1.14.0.5
Hummingbird
A modern user interface library for games
Data binding

Hummingbird allows for easy communication between your application and the UI via several mechanisms - one of them being data binding. See JavaScript integration for another one.

Overview

Data binding synchronizes the state of your UI with that of the game, effectively eliminating a lot of JavaScript boilerplate that would otherwise be needed. Data binding is a pretty popular feature in the web world and if you are familiar with angular.js or react.js this feature will be very familiar. However, our data bindings are written in C++ and directly integrated into Hummingbird which means that the system has better performance than any other JavaScript libraries.

As this feature is rather complex let's start with a simple example to make sense of it all.

Example

Create a named model

To start, you need to expose an object (by reference), to the UI. We'll call the exposed C++ object a "model" as it will serve as the backing store of the data.

You can create a named model using the cohtml::View::CreateModel method:

struct Player
{
int LeftPos;
};
Player g_Player{ 123 };
// Tell Hummingbird how to bind the player
void CoherentBind(cohtml::Binder* binder, Player* player)
{
if (auto type = binder->RegisterType("Player", player))
{
type.Property("leftPos", &Player::LeftPos);
}
}
void RegisterMyModel()
{
// Expose g_Player with the name "g_TestPlayer" to JS
m_View->CreateModel("g_TestPlayer", &g_Player);
}

The cohtml::View::CreateModel invocation registers a model named g_TestPlayer which corresponds to the g_Player C++ instance. The CoherentBind above it takes care of exporting the player class just like it would with standard binding.

Using the model from the HTML

Once the object is exported from C++, it can be attached to the DOM using the set of data-bind-* properties. To complete the example from above, imagine that we want to move a nameplate in the UI using the player's left position:

<div class="nameplate" data-bind-style-left="{{g_TestPlayer.leftPos}}"></div>

Note the special double curly brace syntax ({{expression}}) that you need to use.

With the model exposed and the HTML using it, the <div> element is now linked to the player. We aren't done yet though - to save on computation Hummingbird will only update the element if you tell it to do so.

Update the model

// Somewhere in your game loop
void Update()
{
    ...
    // Change the player:
    g_Player->SetScore(score);
    // Tell Hummingbird the player changed:
    view->UpdateWholeModel(&g_Player);
    // Finally, tell Hummingbird that now is a good time to synchronize all changes:
    view->SynchronizeModels();
}

Note that cohtml::View::UpdateWholeModel only marks the model as dirty but doesn't do any work. The synchronization is done inside cohtml::View::SynchronizeModels which allows you to call it once for all changes. This improves performance as multiple changes within the same frame won't trigger multiple syncs.

With these 3 simple steps the <div> will automatically update anytime the model changes. Note that you didn't have to write any JavaScript to synchronize the UI and the game. Although this example is contrived, you can imagine how powerful this feature can become when dealing with a complex screen powered by tens of variables. Scroll below for more details on how to make use of the subsystem.

Note
Model's property of pointer type and value nullptr is not supported at the moment, i.e. expressions which are using it won't be updated.
Warning
Data binding attributes are taken into account only for static DOM nodes - i.e. HTML nodes that were present in the mark-up during the parsing of the document. It will not work with dynamically created nodes added to the DOM with JavaScript.

Syntax overview

HTML/JavaScript syntax

The syntax of the data binding attributes is a JavaScript expression. The simplest expressions only refer to the model's properties and are encapsulated in double curly braces like we just saw above:

<div data-bind-style-left="{{myModel.leftPos}}"></div>

where myModel is a named model which has a leftPos property.

You can also construct complex expressions such as:

<div data-bind-style-left="{{myModel.LeftPos}}.toFixed()"></div>
<div data-bind-style-left="Math.pow({{myModel.LeftPos}}, 2)"></div>

Only the code referring to the model's properties needs to be inside the curly braces.

Note
Expressions referring to a single model property are the fastest performance-wise since the bound value is directly assigned within the SDK. Complex expressions require JavaScript evaluation and are more expensive than single value bindings.

Supported data-bind-* attributes

data-bind-value

The data-bind-value attribute takes a value and assigns the node's textContent property to it.

Example:

<span data-bind-value="{{player.health}}"></span>

Styling attributes

The following attributes allow you to modify the element's style:

Data bind attribute Affected style property Accepted values
data-bind-style-left left string or number (px)
data-bind-style-top top string or number (px)
data-bind-style-opacity opacity floating point number between 0 and 1
data-bind-style-width width string or number (px)
data-bind-style-height height string or number (px)
data-bind-style-color color CSS color as string or unsigned RGBA
data-bind-style-background-color background-color CSS color as string or unsigned RGBA
data-bind-style-background-image-url background-image URL to the image
data-bind-style-transform2d transform string, containing 6 comma-separated numbers
data-bind-style-transform-rotate transform: rotate(..) string or number (deg)

All the properties above that take number (px) will assume that the number is a measurement in pixels (e.g. binding 42 to data-bind-style-left will be equivalent to left: 42px).

If data-bind-style-transform-rotate takes a number (deg) then it will assume that the number is a measurement in degrees (e.g. binding 90.5 to data-bind-style-transform-rotate will be equivalent to transform: rotate(90.5deg)).

There are two other styling attributes - data-bind-class and data-bind-class-toggle. data-bind-class takes a string in the format data-bind-class="class-name[;class-name]". The class name can be any CSS class. The class specified by class-name will be added to the element. Here's a brief example:

<head>
<style>
.class-type-left-10 {
left : 10px;
}
.class-type-left-20 {
left : 20px;
}
.class-type-top-30 {
top : 30px;
}
.class-type-top-40 {
top : 40px;
}
</style>
</head>
<body>
<div data-bind-class="'class-type-'+{{this.type_1}};'class-type-'+{{this.type_2}}"></div>
</body>

data-bind-class-toggle takes a string in the format data-bind-class-toggle="class-name:bool_condition[;class-name:bool_condition]". The class-name is the name of some CSS class and bool_condition is a boolean or an expression that evaluates to a boolean. If the boolean is true or the condition evaluates to true, the class specified by class-name is added to the element, otherwise it is removed.

Let's see an example with class toggling:

<head>
<style>
.red {
background-color: red;
}
</style>
</head>
<body>
<div class="ToggleWithExpression" data-bind-class-toggle="red:{{this.Health}} < 50">Something red</div>
<div class="ToggleWithBoolean" data-bind-class-toggle="red:{{this.hasLowHealth}}">Something red</div>

The red class will be present on ToggleWithExpression as long as {{this.Health}} < 50 is true, changing the element's background to red. Respectively the red class will be present on ToggleWithBoolean as long as {{this.hasLowHealth}} is true. Otherwise it won't be applied and the element will have whatever background it usually has.

Note
The faster way is using directly boolean instead of expression which would be evaluated to boolean.

Structural data binding

The attributes above only allow you to modify the visual style and the text content of DOM elements. The real power of the data binding system stems from the fact that you can also modify the entire DOM tree with it. This is done via two other attributes:

  • data-bind-if: Displays a DOM element based on a condition. The expressions in the attribute value should evaluate to a boolean value.
  • data-bind-for: Repeats a DOM node for each element in a collection. The basic syntax is data-bind-for="iter:{{myModel.arrayProperty}}", where myModel.arrayProperty must be an array, and iter is a variable used for iteration, which is available in data binding attributes for the child DOM nodes. The syntax for accessing properties of an iterator in a data-bind-for is {{iter.property}}.

You can also use the full form data-bind-for="index, iter:{{myModel.arrayProperty}}", where index is loop counter. If you don't need use the index or iterator you can use _ e.g. data-bind-for="index, _:{{myModel.arrayProperty}}"

If the data-bind-for collection is a std::vector then the elements could be also raw/std::shared_ptr/std::unique_ptr pointers (or your custom pointer type).

Warning
  • data-bind-for with a collection of primitive types is not supported.
  • Changes via JS to DOM element with data-bind-if attribute will be lost if the expression's value is switching between true and false.
  • Changes via JS to DOM element generated from data-bind-for will cause undefined behavior if the collection's size is changed.

Structural data binding allows you to generate entire screens by just providing a template for each element in a collection (e.g. all items in the player's inventory) and the system will take of repeating the template as many times as necessary.

The next several examples only show the relevant HTML because the C++ model is straightforward:

< !-- displays a warning message if the player's health is low -->
<div data-bind-if="{{g_Player.health}} < 40" id="playerHPLowWarn">
Player needs a medic!
</div>
< !-- The following will result into a list, depending on the weapons the player has, like so:
Rare Gun
Common Sword
Rare Dagger
-->
<div data-bind-for="weapon:{{player.weapons}}">
<span data-bind-if="{{weapon.isRare}}">
Rare
</span>
<span data-bind-if="!{{weapon.isRare}}">
Common
</span>
<span data-bind-value="{{weapon.name}}">
</div>
< !-- Enumerates all pets of the player -->
<div class="petsList">
<div data-bind-for="iter:{{g_Player.pets}}" class="petItem">
<div class="petName" data-bind-value="{{iter.name}}"></div>
<div class="petSpeed" data-bind-value="{{iter.info.speed}}"></div>
<div class="petType" data-bind-value="{{iter.info.isMount}} ? 'Mount' : 'Companion'"></div>
<div>
</div>
< !-- Will result in -->
<div class="petsList">
<div class="petItem">
<div class="petName">Sabertooth tiger</div>
<div class="petSpeed">80</div>
<div class="petType">Mount</div>
</div>
<div class="petItem">
<div class="petName">Eagle</div>
<div class="petSpeed">90</div>
<div class="petType">Companion</div>
</div>
<div class="petItem">
<div class="petName">Pony</div>
<div class="petSpeed">70</div>
<div class="petType">Mount</div>
</div>
</div>
< !-- Access loop counter -->
<div class="petsList">
<div data-bind-for="index, iter:{{g_Player.pets}}" class="petItem">
<div data-bind-class="'petName' + {{index}}" data-bind-value="{{iter.name}}"></div>
<div>
</div>
< !-- Will result in -->
<div class="petsList">
<div class="petItem">
<div class="petName0">Sabertooth tiger</div>
</div>
<div class="petItem">
<div class="petName1">Eagle</div>
</div>
<div class="petItem">
<div class="petName2">Pony</div>
</div>
</div>

Data binding events

The data binding events are dom element attributes for attaching event listeners on the DOM.

Example:

<div class="shop-menu" data-bind-for="item:{{g_Shop.items}}">
<div class="item-box" data-bind-[eventName]="g_Shop.buyItem(event, this, {{item}})">
</div>
</div>
  • event - The JavaScript Event object from the fired event.
  • [eventName]- All events listed in Supported Events. Example: click, mouseup, dblckick, etc.
  • this - Is set to the DOM element on which the handler is registered.

Supported Events

  • abort: is fired when the loading of a resource has been aborted.
  • blur: is fired when an element has lost focus
  • click: is fired when a pointing device button (usually a mouse's primary button) is pressed and released on a single element.
  • dblclick: is fired when a pointing device button (usually a mouse's primary button) is clicked twice on a single element.
  • error: is fired when an error occurred; the exact circumstances vary, events by this name are used from a variety of APIs.
  • focus: is fired when an element has received focus.
  • focusin: is fired when an element is about to receive focus.
  • focusout: is fired when an element is about to lose focus.
  • keydown: is fired when a key is pressed down.
  • keypress: is fired when a key that produces a character value is pressed down.
  • keyup: is fired when a key is released.
  • load: is fired when progression has begun successful.
  • mousedown: is fired when a pointing device button is pressed on an element.
  • mouseover: is fired when a pointing device is moved onto the element that has the listener attached or onto one of its children.
  • mouseout: is fired when a pointing device (usually a mouse) is moved off the element that has the listener attached or off one of its children.
  • mouseenter: is fired when a pointing device (usually a mouse) is moved over the element that has the listener attached.
  • mouseleave: is fired when the pointer of a pointing device (usually a mouse) is moved out of an element that has the listener attached to it.
  • mousemove: is fired when a pointing device (usually a mouse) is moved while over an element.
  • mouseup: is fired when a pointing device button is released over an element.
  • input: is fired synchronously when the value of an <input>, <select>, or <textarea> element is changed.
  • scroll: is fired when the document view or an element has been scrolled.
  • touchstart: is fired when one or more touch points are placed on the touch surface.
  • touchend: is fired when one or more touch points are removed from the touch surface.
  • resize: is fired when the document view has been resized.
  • durationchange: is fired when the duration attribute has been updated.
  • emptied: is fired when the media has become empty.
  • ended: is fired when playback or streaming has stopped because the end of the media was reached or because no further data is available.
  • seeked: is fired when a seek operation completed.
  • seeking: is fired when a seek operation began.
  • timeupdate: is fired when the time indicated by the currentTime attribute has been updated.

C++ Model Details

Updating a model

To signify that a model needs updating use cohtml::View::UpdateWholeModel. It will update all properties of the model, including those that haven't actually changed.

m_View->CreateModel("player", m_Player.get();
m_Player->SetScore(42);
m_View->UpdateWholeModel(m_Player.get());
m_View->SynchronizeModels();

cohtml::View::SynchronizeModels updates all the models that have been marked to have been changed.

C++ Property conversion

To convert from the C++ values of the properties of the model the cohtml::Property interface is used. Simple expressions (like data-bind-value="{{g_Player.score}}") are converted directly to the C++ type used by Hummingbird for the specific data-bind operation. This is implemented via the cohtml::Property::ToNumber, cohtml::Property::ToString, cohtml::Property::ToBoolean, cohtml::Property::ToColor methods. They in turn call the cohtml::CoherentToNumber, cohtml::CoherentToString, cohtml::CoherentToBoolean and cohtml::CoherentToColor functions. Overloading these functions will allow to convert from your C++ type to a Hummingbird type. For example, to convert from MyColor to renoir::Color add

bool CoherentToColor(const MyColor& from, renoir::Color* to)
{
to->Value = TO_RGBA(from);
// or
to->r = from.r();
to->g = from.g();
to->b = from.b();
to->a = from.a();
return true;
}

For some of the operations a fallback using CSS value as string is also allowed. For example, when using data-bind-style-color="{{g_Player.color}} the color property will be converted to renoir::Color. In case this conversion fails, it will convert the property to a string and parse the color from that, using the CSS syntax for that value.

Complex expressions are compiled to JavaScript stub functions that evaluate the expression and return the result. This means that the properties of the bound C++ model are converted to JavaScript values and the result is then converted back to a C++ value. This requires several crossings of the C++ <-> JavaScript boundary (one for the function call and one for each property access of the model), which might be quite expensive. The fallback algorithm, described above, is used for values returned from complex expression as well.

Unregistering a model

Finally, you can unregister models from binding using the cohtml::View::UnregisterModel API. This removes the model by instance pointer. Unregistration will not remove any elements bound to the model - they'll preserve their last state.

Warning
The data binding system records the names of the models that affect their appearance when the node is attached to DOM tree (basically after the HTML is parsed). If you register a model with a name that is used in one of those nodes, the binding will be picked up from the record. If you try to unregister the named model and then register it again, the binding won't work any more, since there is no new record about that binding (it's only updated on DOM node attach event).

Disabling default binding behavior

Hummingbird provides a default version of CoherentBind which will error out if you don't provide a template specialization of CoherentBind for your type. In some cases you might want to implement the default by yourself (e.g. binding all types via the reflection system of your engine). To do that you need to declare a template specialization for cohtml::IsDefaultBindEnabled structure in the way shown below.

For example let's declare DisableDefaultBinding specialization for user-defined type Player:

namespace cohtml
{
template<>
struct DisableDefaultBinding<Player, Player> : TrueType
{
};
}

In the case you need to write a more generic version, you can use the second template parameter where you can use SFINAE and enable_if techniques. If you wish to disable default binding for classes and unions you can write something similar to this:

namespace cohtml
{
template<typename T>
struct DisableDefaultBinding<T, typename std::enable_if<std::is_class<T>::value || std::is_union<T>::value, T>::type> : TrueType
{
};
}

Events for model changes

In case you need to know when your data binding model was updated/synchronized (e.g. you want to run some JavaScript on model changes), you can implement your own events similar to the following:

class MyGame
{
// Class definition
Player m_Player;
bool m_NotifyPlayerUpdate;
}
void MyGame::UpdatePlayerModel()
{
m_View->UpdateWholeModel(&m_Player);
m_NotifyPlayerUpdate = true;
}
void MyGame::SynchronizeModels()
{
if (m_NotifyPlayerUpdate)
{
m_View->TriggerEvent("BeforeModelSynchronize", m_Player);
m_View->SynchronizeModels();
m_View->TriggerEvent("AfterModelSynchronize", m_Player);
m_NotifyPlayerUpdate = false;
}
}
void MyGame::Advance()
{
// Some game logic
UpdatePlayerModel();
// More game logic
SynchronizeModels();
...
}
...
<script src="js/CoherentHummingbird.js"></script>
engine.on('Ready', function() {
engine.on('BeforeModelSynchronize', function(player) {
/// Do your job here
});
engine.on('AfterModelSynchronize', function(player) {
/// Do your job here
});
...
});