JavaScript interactions
Triggering Events from Blueprints
In this section we’ll show an example showing how to send data from a Blueprint to the UI. The first part of the example will show you how to trigger a simple event from a Blueprint. The second one expands it with exporting whole game objects to the UI.
Create and send a simple event from Blueprint
There are two ways of triggering JavaScript events from a Blueprint.
We’ll start with the easier one first. It encapsulates all the required logic that you would otherwise have to do yourself with multiple Blueprint nodes.
- Open a Blueprint where you want the event to be triggered.
- Right-click and search for
Trigger event
. Spawn aTrigger JavaScript Event
node from thePrysm
category. - Set the
Cohtml Component
Pin. You need to connect the Component that will trigger the event. - Set the
Event Name
Pin. This will be the name of the event that’s going to be triggered in JavaScript. - You can dynamically add or remove input argument pins from the details pane, and also give names to the arguments. If you select a currently unsupported type, you’ll get a compilation error. At the time of writing, the supported types for binding are primitive types, arrays and
UCLASS
objects. - Set the argument values either by directly typing the value, or by making links from other nodes.
- Make sure that you invoke the
Trigger JavaScript Event
node after you have received theScriptingReady
event.
Here’s how the final Blueprint looks like:
The second ways is more verbose and explicit. We’ll examine it in the context of the sample game:
Open the Example_Map map of the sample game.
Open the Level Blueprint.
You’ll see the Blueprint already created. You can study it, or directly delete it and re-create it better understand what it does.
Add a
Begin Play
Event.Add a
Get Player Controller
node.Add a
Get HUD
node from thePlayerController
.From the
Get HUD
node, drag a pin and add a Cast toCoherentSampleHUD
node.From the
CoherentSampleHUD
node, drag a pin and get itsPrysm HUD
property.Drag a pin from the
Prysm HUD
object and selectAssign Scripting Ready
. TheScriptingReady_Event
event will be triggered when the page is ready to receive events.You must add a call to this event manually to your JavaScript code in the UI after all your initialengine.on
event subscriptions.To create the event: Drag the
Prysm HUD
pin and create aCreate JSEvent
node. This node produces the object that will represent our event.Connect
Assign to ScriptingReady
withCreate JS Event
. This means that as soon as the page is ready to receive events, we’ll send this one.Drag the
Return value
pin of theCreate JS Event
node and create aSequence
node.Drag a pin from
Then 0
of theSequence
node, and createAdd String
node. The JS event object that we just created can carry an arbitrary amount of arguments that will be available to the JavaScript of the page (each parameter must be added to the event with the appropriateAdd XXXX
node before the event is triggered; the order by which arguments are added to the event will be the order the JavaScript code receives them).Connect the return value of
Create JSEvent
with the target of theAdd String
node.Drag the
Prysm HUD
pin and create aTrigger JSEvent
node. This is the node that will effectively execute the event and send it to JavaScript. It must happen after all arguments have been added.From
Then 0
in theSequence
node, drag a pin and createAdd String
node.Connect
Then 1
to theEventData
pin ofTrigger JSEvent
.
Automatic export of Unreal types to UI scripts
This guide assumes you have completed the first part of the sample.
Now let’s make the entire PlayerCharacter
object available to the UI and display the player name, max health and the max amount of ammo the character can carry on-screen.
In the hud.html page you can see that there is a playerInfo
element that contains the player data we want to show. Populate this data from the game through Blueprints.
- Add a
Get Player Character
node. - Drag the
Return value
pin of theCreate JS Event
node and create aAdd Object
node. The JS event object that we just created can carry an arbitrary amount of arguments that will be available to the JavaScript of the page. Each parameter must be added to the event with the appropriateAdd XXXX
node before the event is triggered. The order by which arguments are added to the event will be the order the JavaScript code receives them. - Connect the
Get Player Character
return value to theObject
pin of theAdd Object
. This means that our event will send the whole player character object to JavaScript and we’ll be able to use it’s properties in the UI. - Drag the
Prysm HUD
pin and create aTrigger JS Event
node. This is the node that will effectively execute the event and send it to JavaScript. It must happen after all arguments have been added. - Connect the JS event to the
EventData
pin. - Set the event name (the
Name
property) toSetPlayerState
. - Connect the
Then 0
pin of the sequence to theAdd Object
node and theThen 1
to theTrigger JS event
node. - Press Play.
The complete Blueprint is identical to the one from the previous section with the only difference being the Add String
was replaced with Add Object
.
As soon as the game starts and the HUD loads, the SetPlayerStats
event will be triggered and the corresponding JavaScript code that populates the Statistics
fields in the UI will execute.
In JavaScript we’ve added a handler for the SetPlayerStats
event (see CoherentSample/Content/Resources/uiresources/MainUI.html).
engine.on('SetPlayerStats', function (character) {
$("#playerName").html(character.PlayerName);
$("#playerMaxHealth").html(character.MaxHealth);
$("#playerMaxAmmo").html(character.MaxAmmo);
$("#playerInfo").css("display", "initial");
});
// IMPORTANT: Signal the game we are redy to receive events
// We have setup all JS event listeners (the 'engine.on' calls)
engine.call("ScriptingReady");
The character
variable contains all the properties of the APlayerCharacter
Actor in the game. Now we can use them to populate the Statistics
of our UI.
UObjects
, only their primitive type UPROPERTY
-tagged fields are exported. UObjects
contained in exported UObjects
are not recursively exported, because they might contain circular dependencies and cause memory outages.The object in JavaScript is a copy of the original one. Changing its properties directly won’t affect the object in the game. However, you can call events back in the game with parameters and use this mechanism to update logic in the game from the UI. This is explained in the next section.
Remarks
Objects sent to JavaScript are copies of the ones in the game. Therefore, sending smaller objects less often is preferable as it incurs less resource usage.
As a general note the best way to structure the scripting is to create Blueprint functions for every interaction and call them when needed from the master Blueprint. This makes the master Event Graph much less cluttered and more readable.
UI Scripting with C++
Prysm provides a very powerful binding framework that can be used to connect the C++ logic of the game with the UI JavaScript code and vice-versa.
To trigger events from C++ to JavaScript, you can use the TriggerEvent
method of the View.
void ACoherentSampleCharacter::OnTabReleased()
{
cohtml::View* View = GetCohtmlView();
if (View)
{
View->TriggerEvent("ToggleMenu");
}
}
Any parameters you pass to it will be also sent to the JavaScript code. In JavaScript you must define which function should be called when an event is triggered from C++. The cohtml.js file exposes a special JavaScript object named engine
that provides the glue and connections between the events in C++ and JS. You must use that object to define all your event handlers. The on
method will bind an event to a JS handler.
engine.on('ToggleMenu', toggleMenu);
In this case as soon as C++ calls TriggerEvent("ToggleMenu")
, the toggleMenu
function will be executed in JavaScript.
Triggering events from JavaScript to C++ is analogous. First, in C++ you need to define which method (or Unreal delegate) will handle a specific event triggered from JS.
Defining C++ handlers must happen after the View has triggered the ReadyForBindings
event. If you want to handle this event, you must subscribe to it and in its handler you should declare all your bindings. In the ACoherentSampleHUD
Actor in the sample game this is done with the following line:
CohtmlHUD->ReadyForBindings.AddDynamic(this, &ACoherentSampleHUD::BindUI);
Now, as soon as the View has been loaded, it will invoke the BindUI
method where you can declare all the bindings for the View.
The BindUI
method is straight-forward:
void ACoherentSampleHUD::BindUI(int32 frameid, const FString& path, bool isMain)
{
CohtmlHUD->GetView()->BindCall("CallFromJavaScript",
cohtml::MakeHandler(&CalledFromJSSampleDelegate,
&(FCalledFromJSSample::ExecuteIfBound)));
CohtmlHUD->GetView()->BindCall("CalledFromJSString",
cohtml::MakeHandler(this,
&ACoherentSampleHUD::CalledFromJSStringHandler));
CohtmlHUD->GetView()->RegisterForEvent(
"CalledFromJSUStruct",
cohtml::MakeHandler(this, &ACoherentSampleHUD::CalledFromJSUStructHandler));
}
Essentially we say: “When JavaScript fires the CallFromJavaScript
event, in C++ you must execute CalledFromJSSampleDelegate
delegate”. The same applies for the second binding but in that case it’ll call the CalledFromJSStringHandler
method of the class.
You can also do this in a Blueprint-only fashion, using the Register for Event
Blueprint node, as shown below.
In JavaScript you just have to use:
engine.call("CallFromJavaScript", 123);
engine.call("CalledFromJSString", "Hello from JavaScript!");
var ustructData = { ... };
engine.trigger("CalledFromJSUStruct", ustructData);
Please note that you can pass arguments from JS to C++ again, but the handler signatures must coincide with the arguments passed, otherwise an error is generated.
In the sample game the handlers for those methods are very simple:
void ACoherentSampleHUD::CalledFromJSHandler(int32 number)
{
UE_LOG(LogScript, Log, TEXT("UE4 Delegate called from JavaScript"));
}
void ACoherentSampleHUD::CalledFromJSStringHandler(const FString& str)
{
UE_LOG(LogScript, Log, TEXT("String received from JS: %s"), *str);
}
We just log that the callbacks were called from JavaScript, so that we know everything works fine. Prysm supports passing parameters to/from JavaScript for all primitive types, STL containers as well as UE’s strings (FString
, FText
, FName
), containers (TMap
, TArray
) and colors & vectors. In addition, any type that Unreal knows about, i.e. USTRUCT
s and UCLASS
s, can be automatically bound. In order to use these types you must include the relevant headers, available in the cohtml/Binding and CohtmlPlugin/Public directories.
Automatic binding for UCLASS
and USTRUCT
types
To enable the automatic binding support for Unreal Engine types you have to:
#include "CohtmlUTypeBinder.h"
It will disable the default CoherentBind
mechanism for all USTRUCT
and UCLASS
types and use the reflection system of the engine to expose them to JavaScript. The automatic binding supports primitive types, strings, C-style arrays, TArray
and TMap
properties and recursively exposes contained USTRUCT
s.
enum
and enum class
types will be exposed to JavaScript by their int
values, but you can also override this behavior and specify that you want a specific class’s (or struct’s) enum
/enum class
to be exposed by their enumeration aliases as string
values. You can read more about this here.The following structs will be fully exposed to JavaScript:
USTRUCT()
struct FBar
{
UPROPERTY()
int32 Value;
};
USTRUCT()
struct FFoo
{
UPROPERTY()
FString String;
UPROPERTY()
TArray<int32> SomeValues;
UPROPERTY()
FBar Bar;
UPROPERTY()
UBar* BoundUClassPtrArray[10];
};
Automatic binding and UPROPERTY
Ignoring
UPROPERTY
. To work around this, you can force lowercase exposure for all properties.UCLASS
es (i.e. UObject
s) are not bound recursively. Their sheer size makes recursive binding unusable for performance reasons (a recursive binding to an AActor
for example would also need to bind the entire ULevel
as the actor holds a pointer to it).To expose UObject
s recursively, you can use the by-ref binding. By-ref binding is lazy and exposes each property as the JavaScript code accesses it. To do so, use:
UPlayer* player = NewObject<UPlayer>();
View->TriggerEvent("setPlayer", cohtml::ByRef(player));
CreateModel
and ExposeAsGlobal
methods are exposed by-ref.In case you don’t want a certain property to be automatically exposed (or would like to do any other verification, or otherwise), you can register a callback function, using the cohtml::OnIgnoreProperty()
function, which returns an Unreal Delegate, allowing you to utilize the Bind
functions that come with it. Before each property for a given class
/struct
gets registered, your bound callback function will be invoked and your custom logic applied. The power in this lies in the fact that you can specify additional parameters when binding your function, since Unreal Delegates implement C++ variadic templates (references are not allowed, however, but you can still use pointers). An example usage would be:
USTRUCT()
struct FFoo
{
UPROPERTY()
int Value;
UPROPERTY()
FBar Bar;
UPROPERTY()
UBar* BoundUClassPtrArray[10];
};
// FProperty or UProperty depending on the Unreal version you are using
// We have a backwards compatibility macro COH_PROP to handle this internally
bool ShouldIgnoreProp(const UStruct* Type, const FProperty* Property, TSet<FString>* PropsToIgnore)
{
if (PropsToIgnore->Contains(Property->GetName()))
{
// Do something with the Property
return true;
}
return false;
};
// Provide the example callback function we created above, as well as the TSet with the property
// names we would like to not bind. Even though the original function signature requires only a
// const UStruct* and const FProperty*, thanks to Unreal's Delegates, we create and bind one with
// additional parameters at our convenience.
TSharedPtr<TSet<FString>> PropsToIgnore = MakeShareable(new TSet<FString>({ "Bar", "BoundUClassPtrArray" }));
cohtml::OnIgnoreProperty().BindStatic(&ShouldIgnoreProp, PropsToIgnore.Get());
In the above example only Value
will be accessible from JavaScript.
cohtml::OnIgnoreProperty().UnBind()
if you need to remove the callback function at some point, since otherwise it will always be called when the UTypeBinder
starts registering properties at runtime. You will also have to take into consideration the lifetime of the additional arguments (if any) you pass in.Automatic binding for methods exposed via UFUNCTION
By including the CohtmlUTypeBinder.h
, your UCLASS
or USTRUCT
methods exposed to Unreal’s reflection system through the UFUNCTION
macro will be automatically exposed to JavaScript as well.
The types of methods that can currently be exposed and invoked in JavaScript are:
- Methods returning a type
- Methods with zero parameters
- Methods with multiple parameters
- Methods without a return type (
void
)
Cases where calls will be aborted:
- Providing a wrong argument
- Providing an unsupported type as argument
Cases where calls will not be aborted (even if used incorrectly):
- Providing too many arguments - i.e. invoking a method with 2 parameters in JavaScript, but providing 3 arguments - the extra argument will be discarded and the function will be successfully invoked, with a warning being thrown, regarding the discarding.
- Invoking a method with default arguments - the method will be invoked, but the default arguments will not be taken into account, with a warning being thrown, notifying that default arguments are unsupported. (Primitive types will be zero-initialized)
Currently supported parameter types:
bool
int32
int64
uint8
uint32
uint64
float
double
FString
FText
FName
Currently unsupported parameter types include:
UObject
UStruct
TArray
TMap
TSet
FVector
FColor
UENUM
Automatic binding and UFUNCTION
Ignoring
In case you want a certain UFUNCTION
to not be automatically exposed, you can do that in a similar fashion to the UPROPERTY
ignoring, explained in the previous sections. You can register a callback function, using the cohtml::OnIgnoreFunction()
function. An example usage would be:
USTRUCT()
struct FFoo
{
UFUNCTION()
void Bar(){}
UFUNCTION()
void Baz(int){}
UFUNCTION()
int FooBar(){ return 0; }
};
bool ShouldIgnoreFunction(const UStruct* Type, const UFunction* Function, TSet<FString>* FunctionsToIgnore)
{
if (FunctionsToIgnore->Contains(Function->GetName()))
{
// Do something with the Function
return true;
}
return false;
};
// Provide the example callback function we created above, as well as the TSet with the property
// names we would like to not bind. Even though the original function signature requires only a
// const UStruct* and const UFunction*, thanks to Unreal's Delegates, we create and bind one with
// additional parameters at our convenience.
TSharedPtr<TSet<FString>> FunctionsToIgnore = MakeShareable(new TSet<FString>({ "Bar", "Baz" }));
cohtml::OnIgnoreFunction().BindStatic(&ShouldIgnoreFunction, FunctionsToIgnore.Get());
In the above example only FooBar
will be accessible from JavaScript.
cohtml::OnIgnoreFunction().UnBind()
if you need to remove the callback function at some point, since otherwise it will always be called when the UTypeBinder
starts registering properties at runtime. You will also have to take into consideration the lifetime of the additional arguments (if any) you pass in.Casing in automatically exposed UCLASS
, USTRUCT
types and UFUNCTIONS
For instance, if we have a class Foo
and a property called Bar
, like the example above, JavaScript might receive a value such as Foo.bar
or Foo.bAr
. For this to occur, there needs to be an FName
in the Engine/Game that shares its content with the name of your property or method, but has a different casing. This usually occurs only in shipping builds and happens because in Unreal Engine properties and methods are stored as FName
s, which are case-insensitive. There is no supported way to know the casing in advance, but you can work around this by turning on the Use Lower Case Names For Auto Exposed Properties in Prysm Plugin’s miscellaneous settings, which will guarantee that property and method names will be exposed as lowercase, regardless of their casing in the engine.
Customizing UCLASS
and USTRUCT
binding
To have more control over the binding of UCLASS
and USTRUCT
types, you can still use a regular CoherentBind
overload. However, to make CoherentBind
be used for a UCLASS
, or USTRUCT
it has to be registered.
If you have custom binding for ULevelCustom
:
void CoherentBind(cohtml::Binder* binder, ULevelCustom* level)
{
if (auto type = binder->RegisterType("LevelCustom", level))
{
type.Property("difficulty", &ULevelCustom::Difficulty);
}
}
Register it using:
cohtml::RegisterCustomBind<ULevelCustom>();
in a file where the CoherentBind
declaration is visible.
Specify value exposure of autobound enum and enum class
If an autobound UCLASS
or USTRUCT
has a member variable that is of type enum
, or enum class
(declared with the UENUM
macro), by default the exposed values will be the actual integer values, instead of the enumeration aliases.
If you would like to have the aliases exposed instead, you would need to use the cohtml::RegisterUEnumExposureByStr
API before creating your model for your respective class
, or struct
. The API requires you to pass an FString
with the name of your enum
, or enum class
.
enum
s have to be passed as TEnumAsByte<NameOfYourEnum>
in order for them to be properly registered, since plain enum
s aren’t allowed to be used with the UPROPERTY
macro and have to be declared with the TEnumAsByte templateHere is an example:
UENUM(BlueprintType)
enum class EnumClassUE4 : uint8
{
Zero,
One,
Two
};
UENUM(BlueprintType)
enum EnumUE4
{
Zero,
One,
Two
};
// Indicate that EnumUE4 values should be exposed as the string alias (Zero, One, etc.)
cohtml::RegisterUEnumExposureByStr("TEnumAsByte<EnumUE4>");
// Indicate that EnumClassUE4 values should be exposed as the string alias (Zero, One, etc.)
cohtml::RegisterUEnumExposureByStr("EnumClassUE4");
Unreal Engine types and containers
Fundamental Unreal Engine types such as FString
, TArray
, TMap
, TSharedPtr
, TUniquePtr
, FColor
, FVector
and UDataTable
are also supported. To use them, include the relevant header. For example to use TArray
:
#include "CohtmlTArrayBinder.h"
Other user defined types
Prysm also has support for binding user defined types that are not UCLASS
, or USTRUCT
, using CoherentBind
overloads. For a detailed guide on this topic, please consult the native C++ documentation of Prysm.