This article is for the 20th day of the Unreal Engine 4 (UE4) Advent Calendar 2018.

The previous day’s article was by 0xUMA: Trying out automated testing in UE4.

Overview

There are times when you want to switch behavior patterns after creating a package for Windows from UE4. Initially, I handled this by allowing parameter changes via external text files (like XML or JSON).

However, eventually, the request came to change the logic itself.
So, I will write about how I externalized the logic using JavaScript with Unreal.js.

The sample project created for this article is available below.

Selecting the Approach

In UE4, even if you change C++ code from the Editor, you can hot-reload it, allowing immediate (though with a slight delay) confirmation of behavior. However, this cannot be used after packaging.

Therefore, I looked for a plugin for scripting functionality that can run in the UE4 Runtime and found the following three:

  • Unreal.js
  • UnrealEnginePython
  • MonoUE
    [MonoUE](https://mono-ue.github.io/)

MonoUE uses C#, which I personally like, so I was interested, but it didn’t seem easy to use casually as a script, so I removed it from the candidates.

Considering the remaining two, I personally preferred JavaScript, so I chose Unreal.js.

What is Unreal.js?

This was helpful for obtaining basic information about Unreal.js. Although the article is about 2 years old, it was very helpful as it could be used almost as is even in the current 4.21 environment.

Introduction to Unreal.js
Unreal.js 入門 - Qiita

Installation

Add Unreal.js from the Marketplace.

Then, open the UE4 project in the Editor and enable Unreal.js from Edit->Plugins. Restart the Editor, and it becomes usable.

As a test, place an Actor in the level, add a JavaScript Component, and enter Sample.js in Script Source File. Create Sample.js in Content/Scripts and write the following content:

console.log("test")

With this setup, play the level. If “test” is displayed in the Output Log, it’s working correctly.

Checking After Packaging

Create a package for Windows using File -> Package Project -> Windows -> Windows (64-bit).

After creating the package, copy the following directory to the Content directory of the created package:

[Engine Install Path]\Engine\Plugins\Marketplace\UnrealJS\Content\Scripts

Start the application, close it immediately, and check if “test” is output in Saved/Logs/***.log. If it is, it’s OK.

Creating a Dynamic Loading Mechanism

Using Unreal.js’s JavaScript Component allows executing js files, but it has the following limitations:

  • Once executed, the JavaScript cannot be changed midway.
  • The js file must be placed under Content/Scripts.

Therefore, let’s create a mechanism to eliminate these limitations, allowing js files to be placed anywhere and updated even while the script is running. Specifically, let’s create it with the following policy:

  • The js file will only contain class definitions describing the externalized logic.
  • The base class for the classes described in the js file will be defined in C++.
  • The js file can be reloaded even while the application is running.
  • Minimize file access frequency.
  • Allow js files to be placed outside of Content/Scripts.

C++ Class Implementation (JSObject)

Define the base class for the classes defined in the js file.

FString NotifyTrigger(); is the interface for accessing the JavaScript side from the application side. This time, it’s a method returning FString, but change this according to the situation. Multiple methods can be provided. Make the method必ず UFUNCTION(BlueprintImplementableEvent) to make it an override-only method. Therefore, do not write the implementation in the cpp side.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "UObject/NoExportTypes.h"
#include "JSObject.generated.h"

/**
 *
 */
UCLASS(BlueprintType)
class UNREALJSSAMPLE_API UJSObject : public UObject
{
    GENERATED_BODY()

public:
    UJSObject();

    UFUNCTION(BlueprintImplementableEvent)
    FString NotifyTrigger();

};
// Fill out your copyright notice in the Description page of Project Settings.

#include "JSObject.h"

UJSObject::UJSObject()
{

}

C++ Class Implementation (JSComponent)

Implement this by referencing the implementation of Unreal.js’s JavaScript Component.

OnRegister

Almost the same as Unreal.js’s JavaScript Component. Preparation for using JavaScript and registration of linkage between the C++ side and JavaScript side are also done at this point.

Among these, describing the single line Context->Expose("Root", this); allows the reference of this Component class to be used as Root within JavaScript.

void UJSComponent::OnRegister()
{
    auto ContextOwner = GetOuter();
    if (ContextOwner && !HasAnyFlags(RF_ClassDefaultObject) && !ContextOwner->HasAnyFlags(RF_ClassDefaultObject))
    {
        if (GetWorld() && ((GetWorld()->IsGameWorld() && !GetWorld()->IsPreviewWorld())))
        {
            UJavascriptIsolate* Isolate = nullptr;
            UJavascriptStaticCache* StaticGameData = Cast<UJavascriptStaticCache>(GEngine->GameSingleton);
            if (StaticGameData)
            {
                if (StaticGameData->Isolates.Num() > 0)
                    Isolate = StaticGameData->Isolates.Pop();
            }

            if (!Isolate)
            {
                Isolate = NewObject<UJavascriptIsolate>();
                Isolate->Init(false);
                Isolate->AddToRoot();
            }

            auto* Context = Isolate->CreateContext();
            JavascriptContext = Context;
            JavascriptIsolate = Isolate;

            Context->Expose("Root", this);
            Context->Expose("GWorld", GetWorld());
            Context->Expose("GEngine", GEngine);
        }
    }

    Super::OnRegister();
}

LoadJSFile

Reads the js file, instantiates the class described within it, and holds the reference.

void UJSComponent::LoadJSFile()
{
    if (JavascriptContext == nullptr) return;

    // ScriptSourceFile contains the relative path from Content, so convert it to an absolute path
    auto scriptSourceFilePath = FPaths::Combine(FPaths::ProjectContentDir(), ScriptSourceFile);
    scriptSourceFilePath = FPaths::ConvertRelativePathToFull(scriptSourceFilePath);

    // Read the content of the js file (don't execute yet)
    FString script;
    FFileHelper::LoadFileToString(script, *scriptSourceFilePath);

    // Extract the class name written in js
    const FRegexPattern pattern = FRegexPattern(FString(TEXT("class\\s+(.+)\\s+extends\\s+JSObject")));
    FRegexMatcher matcher(pattern, script);
    if (matcher.FindNext())
    {
        auto className = matcher.GetCaptureGroup(1);

        // Add code to the script for class instantiation and reference holding
        script = TEXT("(function (global) {\r\n") + script;
        script += TEXT("let MyUObject_C = require('uclass')()(global,") + className + TEXT(")\r\n");
        script += TEXT("let instance = new MyUObject_C()\r\n");
        script += TEXT("Root.SetJsObject(instance)\r\n");
        script += TEXT("})(this)");

        // Execute the script
        JavascriptContext->RunScript(script);
    }
}

The point is not to execute the js file directly as a script, but to add script lines above and below the js content to perform class instantiation and reference holding.

The part script += TEXT("Root.SetJsObject(instance)\r\n"); uses the mechanism associated during OnRegister() to call the Component’s function from within the script. This allows the JavaScript class instance to be held as a property of the Component implemented in C++.

NotifyTrigger

This is a function for calling the function of the class implemented in JavaScript via the Component from Blueprint. Since the reference to the js class is held in JsObject, it’s called via that.

FString UJSComponent::NotifyTrigger()
{
    if (JsObject != nullptr)
    {
        return JsObject->NotifyTrigger();
    }

    return FString();
}

Blueprint Implementation (SampleActor)

Add the previously created JSComponent and TextRender to an empty Actor.

Also, create a function to call the method NotifyTrigger implemented in js. Make it so that the string of TextRender is rewritten based on the result of NotifyTrigger.

Next, create a function to reload the js file.

Blueprint Implementation (SampleUMG)

Create two buttons in a Widget. Make one call the SampleActor’s NotifyEvent method, and the other call the UpdateJS method.

Create js File

Create the js file to be used as a script. The created file can be placed anywhere, but this time, save it in the same hierarchy as the Content folder.
Here, define a class that inherits the UJSObject class previously implemented in C++. Since the application calls UJSObject’s NotifyTrigger, override that part’s implementation within the script.

class MyUObject extends JSObject {
    NotifyTrigger(){
        let prop1 = 1;
        let prop2 = 2;
        return "ABC" + prop1 + prop2;
    }
}

Register js File Path to JSComponent

Place SampleActor in the level, and input the path to the js file as a relative path from Content into the ScriptSourceFile of the JSComponent already added to SampleActor.

It’s okay even if it’s in a higher hierarchy than Content.

Execution

Create a Win64 package from the Editor.

Copy the following directory to the Content directory of the created package:

[Engine Install Path]\Engine\Plugins\Marketplace\UnrealJS\Content\Scripts

Also, copy the created js file to the same hierarchy as the package’s Content folder.

Once the above preparations are complete, run the application.

TextRender displays the initial value.

Pressing the Trigger button calls the function implemented in JavaScript, and the display of TextRender changes.

Now, without closing the application, rewrite the content of the js file and save it by overwriting.

class MyUObject extends JSObject {
    NotifyTrigger(){
        let prop1 = 3;
        let prop2 = 4;
        return "ZZZ" + prop1 + prop2;
    }
}

Press the application’s UpdateJS button, then press Trigger. The display of TextRender changes to the result of the rewritten script.

Summary

Using Unreal.js, we were able to use scripting functionality dynamically even after package creation.

The advantage of this implementation is that by entrusting only the bare minimum to JavaScript, it’s somewhat advantageous in terms of speed, and once the logic solidifies, performance can be further improved by replacing it with a C++ class.

Please give it a try if you like.

Tomorrow’s article is by fukusuke8gou about UMG.

References