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 in the documentation.
All communication between JavaScript and the game goes through the engine
module.
For ways to update the UI without writing JavaScript, take a look at the data binding feature.
There are two ways for invoking native code from JavaScript and vice-versa.
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.
First the JavaScript code has to attach a handler for the event using engine.on
:
Then we trigger the event in C++, using the cohtml::View::TriggerEvent
method:
There are also global functions in the cohtml
namespace which can be used such as:
cohtml::TriggerEvent(view, "ScoreChanged", 10000);
Every TriggerEvent
call starts with a C++ macro:
By default this macro does nothing and is compiled out. You can, however, add custom code there by defining COHTML_TRIGGER_EVENT_SCOPE
before it's used in View.h. For example, you can add some profiling code such as:
Registering C++ functions for events triggered by JavaScript should happen in the handler for the cohtml::IViewListener::OnReadyForBindings
event.
Event handlers are registered with the cohtml::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.
Triggering the event from JavaScript looks like:
cohtml::MakeHandler
uses template argument deduction to guess the signature of the handler. This requires several specializations of cohtml::FunctorTraits
and overloads of cohtml::EventHandler::InvokeStub
. To extend the number of arguments for event handlers supported by Hummingbird, you have to add additional specializations and overloads.
Promises are used to return results from C++ to JavaScript. Hummingbird promises are modeled after the PromisesA specification.
Call handlers are registered with the cohtml::View::BindCall
method. There may be only one handler for a given call and the handler may return a result.
To get the player name in the view is:
BindCall
with engine.call
when you have single handler and RegisterForEvent
with engine.trigger
when you have more that one handlers.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 use the following pattern:
.Property
syntax, a string literal must be provided. The string literal should stay "alive" at least until the OnBindingsReleased
View callback is called.The CoherentBind
overload must be visible in all places where a Player
instance is exposed to JavaScript, i.e. in any of the following cases:
Player
instance is used as an argument to TriggerEvent
Player
as an argumentPlayer
to JavaScriptIn case the CoherentBind
overload for the Player
class is not visible, you will get the following compilation error:
include\cohtml\binding\binding.h(57): error C2338: cohtml::Overload_CoherentBind_For_Your_Type<T*> 1> include\cohtml\binding\binding.h(191) : see reference to function template instantiation 'void CoherentBind<T>(cohtml::Binder *,T *)' being compiled 1> with 1> [ 1> T=Player 1> ]
Going down the template instantiation stack, you can find where you are using Player
without the CoherentBind
overload visible.
You can define the CoherentBind overload for Player
in any C++ source (.cpp) file, but you'll need to have a declaration for it, that is included everywhere Player
is exposed to Hummingbird.
Depending on the structure of your project, you may consider the following patterns:
Player
is dedicated only for the UI - then add the declaration of CoherentBind
for Player
in the Player
header. This way the overload will be visible everywhere Player
is visible.Player
is generic game type - add a PlayerBinding.h or a MySubsystemBindings.h header with the declaration of CoherentBind
for Player
(or for all the types in the particular game subsystem). After that make sure to include the header where Player
is used with Hummingbird.CoherentBind
overload for Player
has to be either in the namespace of Player
or in the cohtml
namespace. This way it will be found using argument dependent lookup.cohtml::View::TriggerEvent
with a valid instance of your class. Hummingbird uses this instance to cache the exposed properties and in certain cases might need to use this instance. For example in the case of virtual getters or setters, or virtual inheritance, the instance pointer might need to be adjusted before calling a getter or a setter for a property and getting this adjustment is using the virtual table pointer.Hummingbird has built-in support for most STL
containers and std::
classes.
C++ Type | JavaScript Type | Header |
---|---|---|
std::string | String | <cohtml\Binding\String.h> |
std::vector | Array | <cohtml\Binding\Vector.h> |
std::map | Object | <cohtml\Binding\Map.h> |
std::pair | Object | <cohtml\Binding\Pair.h> |
C style array | Array | <cohtml\Binding\Array.h> |
std::shared_ptr | Stored object's type | <cohtml\Binding\SharedPointer.h> |
std::unique_ptr | Stored object's type | <cohtml\Binding\UniquePointer.h> |
raw pointer | Stored object's type | <cohtml\Binding\TypeTraits.h> |
Stored object's type
is the type of the unwrapped object. For example, the JavaScript type of std::shared_ptr<std::string*>
is String
.
Support for additional containers can be added in a similar way.
Support for a custom pointer type can be added in a similar way. All you have to do is to specialize PointerTrait
for it. For example:
After registering the Player
type, events can be triggered in both directions with instances of Player
as an argument and call handlers can return Player
s as return types.
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 g_Game.m_Player
in the game.
If you want to call a 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 (the same name of the type we gave to cohtml::Binder::RegisterType
in CoherentBind
for Player
. Otherwise Hummingbird cannot treat the object as an instance of Player
.
For some calls it's possible that there is no meaningful value to return. For example -
Custom promises have been deprecated! Now we are supporting ECMAScript 6 promises
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:
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:
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.
Hummingbird supports exporting C++ objects by reference. This avoids copying the C++ values to the JavaScript heap and makes the communication between the game and the UI faster. However the user is responsible for keeping the exposed C++ object pointers valid while they are exposed to JavaScript.
The API to expose a C++ object by reference is the same as normal values. The difference is that to expose a reference the object has to be wrapped in a cohtml::ByRef
holder.
Once an object is exposed by reference to JavaScript, any subsequent TriggerEvent
s using ByRef
with the same object will result in the same JavaScript variable send to JavaScript. This allows for 1-to-1 mapping between the C++ and JavaScript objects.
When an exposed object is about to be destroyed (or moved) its reference must be removed from JavaScript to avoid using invalid pointers (use-after-free). To notify JavaScript that a referenced object is no longer available call the cohtml::View::DestroyExposedObject
method with the address of the object being destroyed. It will remove the reference from the JavaScript values and any attempt to access the properties of the object throw an exception in JavaScript.
Here is a example usage of exposing objects by reference.
To take advantage of this C++ classes the JavaScript side can do:
When a C++ object is exposed by reference to JavaScript, its methods can also be called from JavaScript.
After this, JavaScript can use the game
variable as any other object.