1.14.0.5
Hummingbird
A modern user interface library for games
JavaScript integration

Hummingbird interprets and executes JavaScript code that can be used for scripting the UI. The execution itself is performed by a JavaScript virtual machine. Hummingbird uses different VMs on different platforms to provide the best available option. On platforms where code JIT compilation is allowed like Windows and Android, Hummingbird uses V8, while on iOS it uses the JavaScriptCore framework.

All currently implemented JavaScript functions and objects can be viewed in the JavaScript DOM API section of the documentation.

All communication between JavaScript and the game goes through the engine module.

There are two ways for invoking native code from JavaScript and vice-versa.

  • Events
  • Calls

Events

Events allow to call multiple handlers in both directions, but they can not return any value. To register a JavaScript handler for an event use the engine.on method.

C# triggering JavaScript

EngineTrigger_ToUI.png

First the JavaScript code has to attach a handler for the event using engine.on:

engine.on('PlayerScoreChanged', function (new_score) {
var scoreDisplay = document.getElementById('scoreDisplay');
scoreDisplay.innerHTML = new_score;
});

Then we trigger the event in C#, using the cohtml.View.TriggerEvent method:

// set player score to 10000
view.TriggerEvent('PlayerScoreChanged', 10000);

There are also global functions in the cohtml namespace which can be used such as:

cohtml.ViewExtensions.TriggerEvent(view, 'ScoreChanged', 10000);
Note
In order for the binding to work you need to include the CoherentHummingbird.js library in your HTML page.

JavaScript triggering C#

EngineTrigger_ToGame.png

Registering C# functions for events triggered by JavaScript should happen in the handler for the cohtml.Net.IViewListener.OnReadyForBindings callback. There is a convenient event that you can subscribe to - cohml.ViewListener.ReadyForBindings;

Event Handlers

Event handlers are registered with the cohtml.Net.View.RegisterForEvent method. They cannot return any value to JavaScript, but may trigger an event for JavaScript. There may be more than one C# handler for a given event. There also may be both C# and JavaScript handlers for the same event.

public class Game : MonoBehaviour
{
void Start()
{
viewComponent.Listener.ReadyForBindings += RegisterEvents;
}
void RegisterEvents()
{
viewComponent.View.RegisterForEvent("OnQuitClicked", (System.Action)Quit);
}
void Quit()
{
Application.Quit();
}
}

Triggering the event from JavaScript looks like:

engine.on('OnQuitClicked', function () {
ShowMessage('Bye');
});
function OnClick() {
// this will execute both Game.Quit in C#
// and ShowMessage('Bye') in JavaScript
engine.trigger('OnQuitClicked');
});

Call Handlers

Promises are used to return results from C# to JavaScript. Hummingbird promises are modeled after the PromisesA specification.

EngineCall.png

Call handlers are registered with the cohtml.Net.View.BindCall method. There may be only one handler for a given call and the handler may return a result.

public class Game : MonoBehaviour
{
void Start()
{
viewComponent.Listener.ReadyForBindings += RegisterEvents;
}
void RegisterEvents()
{
viewComponent.View.BindCall("getPlayerName",
(System.Func<string>)GetPlayerName);
}
string GetPlayerName()
{
return m_PlayerName;
}
}

To get the player name in the view:

// call 'Game.GetPlayerName' with callback for the result
engine.call('getPlayerName').then(function (name) {
var playerName = document.getElementById('playerName');
playerName.innerHTML = name;
});
Note
For better performance it is recommended to use BindCall with engine.call when you have single handler and RegisterForEvent with engine.trigger when you have more that one handlers.

Exposing C# Types

To be able to use your C# types as arguments or results for event and call handlers, the C# type must be exposed to Hummingbird. To expose a C# type to Hummingbird you should add the cohtml.Net.CoherentType attribute above the type.

namespace MyGame
{
[CoherentType] // by default explicitly expose properties
struct Player
{
[CoherentProperty] // expose name
public string name;
[CoherentProperty("score")] // expose Score as "score"
public double Score { get; set; }
// Health remains invisible to JavaScript
public double Health;
}
}

C# and JavaScript Communication

After registering the Player type, events can be triggered in both directions with instances of Player as an argument and call handlers can return Players as return types.

public class Game : MonoBehaviour
{
Player playerOne;
void Start()
{
viewComponent.View.TriggerEvent("StartWithPlayer", playerOne);
}
}

Then in JavaScript, we receive the object with the specified properties. The value of each property is the same as in the moment of triggering the event. The JavaScript callback may store a reference to the object, but its properties WILL NOT be synchronized with the actual Game.playerOne in the game.

engine.on('StartWithPlayer', function (player) {
var playerName = document.getElementById('playerName');
playerName.innerHTML = player.name;
var scoreDisplay = document.getElementById('scoreDisplay');
scoreDisplay.innerHTML = player.score;
});

If you want to call C# handler with an instance of Player created in JavaScript there is one important detail - the object must have a property __Type with value Player.

Note
It is important to give full assembly name to the type. Sending an event with __Type: Player will look for this type in the global namespace. If your type resides in a namespace or inside a class you should prefix the type: namespace.class.type
function CreatePlayer() {
var player = {
__Type: 'MyGame.Player', // set the type name
name: "MyTestPlayer",
score: 0
};
engine.call('CreatePlayer', player).then(function (success) {
if (success) {
ShowMessage('Welcome ' + player.name);
} else {
ShowMessage('Sorry, try another name');
}
});
});
bool CreatePlayer(Player player)
{
if (player.Name.Contains(" "))
{
// sorry, no spaces in player name
return false;
}
// create the player
return true;
}

For some calls it's possible that there is no meaningful value to return. For example:

Item GetItem(int slot)
{
if (HasItemAt(slot))
{
return GetItemAt(slot);
}
else
{
// notify the view that there is not item at this slot
view->SetScriptError(cohtml.SCE_NoResult, "no item at slot");
return null;
}
}
engine.call('GetItem', slot).then(function (item) {
ShowMessage('Item at slot ' + slot + ' costs ' + item.Price);
},
// called when there is no item at this slot
function (errorType, message) {
console.log('could not get item at slot ' + slot);
}
});

Customizing Promises

Custom promises have been deprecated! Now we are supporting ECMAScript 6 promises

JavaScript extensions

Hummingbird provides extensions on-top of the vanilla DOM-related JavaScript as defined by the standard. The extensions are aimed to provide better performance and a more natural workflow to UI developers. The prime example of such an extension are the Node.leftXX/Node.topXX methods. In vanilla JS, to update the position of an element you have to call Node.top/Node.left, however the functions take a string parameter. To update an element's left property you have to do:

myNode.left = newPos + "px";

The newPos variable is a number in pixels, but the runtime will have to convert it to a string, append the 'px' specifier and send it to the native code. The native code at that moment will convert it back to a number to re-calculate the position. This is very inefficient, it creates JavaScript garbage and unnecessary work.

In Hummingbird you can just say:

myNode.leftPX = newPos;

No garbage is generated and the calculation is much faster both in JS and native code. A complete list of extensions is available in the JavaScript DOM API section of the documentation.