UE hot update: Lua programming guide based on UnLua

UE热更新:基于UnLua的Lua编程指南

UE uses C++ as a compiled language, which means it becomes binary after compilation, and players can only update the game by reinstalling it. However, in game development, there’s often an urgent need for requirement adjustments and bug fixes, and frequently prompting players to update the app is unacceptable. Generally, game projects use Lua as the scripting language to transform the immutable C++ code at runtime into updatable Lua code.

Although UE does not officially support Lua, Tencent has open-sourced UnLua, which is being used in my current project. Over the past few days, I’ve organized some materials on UnLua (mainly the official documentation, issues, and presentation PPT) and combined it with my own experience from testing UnLua while writing a small demo, resulting in this programming guide for UE combined with UnLua. This article summarizes some basic methods and pitfalls for using UnLua to write business logic and will be updated continuously.

Additionally, Lua files can be packaged into Pak using my previously open-sourced tool: hxhb/HotPatcher. I’ve also modified a version based on UnLua, adding some extra optimizations. The source code integrates the Luasocket/Luapanda/lpeg/Sproto/Luacrypt libraries, and you can use LuaPanda for debugging. The GitHub address is: hxhb/debugable-unlua.

Reference Materials:

UnLua Considerations

These are some potential pitfalls I’ve extracted from the UnLua official repository.

  • Not all UFUNCTIONs are supported; only BlueprintNativeEvent/BlueprintImplementationEvent/Replication Notify/Animation Notify/Input Event are supported.
  • UnLua does not support multiple states, so running multiple clients in PIE mode will have issues. issues/78
  • Do not access Blueprint-defined structures in Lua. issues/119 / issues/40 (New submissions are now supported)
  • Using self.Super in UnLua will not recursively traverse all base classes in the inheritance hierarchy. issues/131
  • Non-dynamic delegates are not supported. issues/128
  • You cannot directly export static members of a class, but you can encapsulate them in a static method. issues/22
  • You cannot bind a Lua script to an object instance; the script specified in NewObject is bound to the UCLASS. issues/134
  • The index rule for using UE4.TArray in UnLua starts from 1, different from TArray in the engine which starts from 0. issues/41
  • Be aware that many functions exported to Lua have different names than those in Blueprints. For instance, GetActorTransform in Blueprints is actually used as GetTransform in Lua, and the names used in Lua are the actual defined names in C++, not the DisplayName.

Lua Code Hints

Exporting Symbols

UnLua provides support for exporting reflection symbols within the engine, and you can also statically export symbols from non-reflection classes. It provides a Commandlet class called UUnLuaIntelliSenseCommandlet to export these symbols.

Here’s a brief introduction to calling the Commandlet:

1
UE4Editor-cmd.exe PROJECT_PATH.uproject -run=COMMANDLET

The usage of UnLua’s Commandlet is:

1
D:\UnrealEngine\Epic\UE_4.23\Engine\Binaries\Win64\UE4Editor-cmd.exe C:\Users\imzlp\Documents\UnrealProjectSSD\MicroEnd_423\MicroEnd_423.uproject -run=UnLuaIntelliSense

After execution, the directory UnLua/Interamate/IntelliSense will be generated, which contains symbols exported to Lua by the engine.

Using VSCode

Install the Emmylua plugin in VSCode, and after installation, add the previously generated IntelliSense directory to the vcode workspace.

Calling Parent Class Functions

In Unreal C++, when we’ve overridden a virtual function from a parent class, we can specify calling the parent class implementation through Super::, but it’s different in Lua.

1
self.Super.ReceiveBeginPlay(self)

In Lua, self.Super is used to call the parent class’s function.

Note: In UnLua, Super simply simulates the “inheritance” semantics; in the case of multi-layer inheritance, there will be issues. Super will not actively traverse down the inheritance hierarchy. The “parent class” is not the metatable returned by Class(), but is set on the Super field (the metatable is defined in the C++ code of the plugin, and this process has been solidified).

For calling the superclass's superclass:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
a.lua
local a = Class()

function a:test()
end

return a

--------------------------------------

b.lua
local b = Class("a")

--------------------------------------

c.lua
local c = Class("b")
function a:test()
c.Super.Super.test(self)
end

This issue was excerpted from UnLua issues: Usage Issue of Super in Class

Calling Overridden Methods

Note: Lua and the original class do not have an inheritance relationship but rather an affiliation relationship. Lua depends on Blueprint or C++ classes. The functions of the Lua-overridden class effectively replace the implementation of the current class’s functions.

When overriding a UFUNCTION in a Lua subclass, you can call it using the following method, which is somewhat similar to Super, but written as Overridden:

1
2
3
function BP_Game_C:ReceiveBeginPlay()
self.Overridden.ReceiveBeginPlay(self)
end

Ensure to pass self in, as failing to do so will cause a crash during execution due to parameter checks in UnLua. Calling UE functions in UnLua is akin to grabbing the raw pointer to a member function; you must manually pass this to it.

Calling UE C++ Functions

UnLua allows enabling or disabling the UE namespace during compilation (i.e., all UE functions require the UE4 prefix).

The calling method is:

1
UE4.UKismetSystemLibrary.PrintString(self,"HelloWorld")

The parameters match those of C++ calls (those with default parameters can also be omitted):

1
UKismetSystemLibrary::PrintString(this, TEXT("Hello"))

Overriding Functions with Multiple Return Values

Blueprints

The overridden Lua code:

1
2
3
4
function LoadingMap_C:GetName(InString)
UE4.UKismetSystemLibrary.PrintString(self, InString)
return true, "helloworld"
end

C++

Since multiple return values in C++ are realized by passing reference parameters, it’s different in Lua.

For the following C++ function:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// .h
UFUNCTION(BlueprintCallable, Category = "GameCore|Flib|GameFrameworkStatics", meta = (CallableWithoutWorldContext, WorldContext = "WorldContextObject"))
static bool LuaGetMultiReturnExample(UObject* WorldContextObject, FString& OutString, int32& OutInt, UObject*& OutGameInstance);
// .cpp
bool UFlibGameFrameworkStatics::LuaGetMultiReturnExample(UObject* WorldContextObject, FString& OutString, int32& OutInt, UObject*& OutGameInstance)
{
bool bStatus = false;
if (WorldContextObject)
{
OutString = TEXT("HelloWorld");
OutInt = 1111;
OutGameInstance = UGameplayStatics::GetGameInstance(WorldContextObject);
bStatus = true;
}

return bStatus;
}

This function takes WorldContextObject as a parameter and receives three types as reference parameters, returning a bool. How to receive these parameter values in Lua?

1
local ret1, ret2, ret3, ret4 = UE4.UFlibGameFrameworkStatics.LuaGetMultiReturnExample(self, nil, nil, nil)

This local ret1, ret2 = func() syntax follows Lua’s rules; you can refer to Programming in Lua, 4th edition, Chapter Six.

Note: The order for receiving reference parameter return values and the actual function return value is: ret1, ret2, and ret3 are the reference parameters, while the final function return bool is ret4.

Notes on Non-const Reference Parameters as Return Values

Note: When calling UFUNCTION functions, non-const reference parameters can be ignored, but this does not apply to statically exported functions because UnLua performs a check on the number of parameters for static exports.

Additionally, using references as return parameters requires consideration of the following two scenarios:

  1. Non-const reference as pure output
1
2
3
4
5
6
void GetPlayerBaseInfo(int32 &Level, float &Health, FString &Name)
{
Level = 7;
Health = 77;
Name = "Marcus";
}

In this case, the return value and input value have no relationship. In Lua, you can use:

1
local level, heath, name = self:GetPlayerBaseInfo(0, 0, "");
  1. Non-const reference parameter used as both input and output
1
2
3
4
5
6
void GetPlayerBaseInfo(int32 &Level, float &Health, FString &Name)
{
Level += 7;
Health += 77;
Name += "Marcus";
}

In this scenario, the return value and input are directly related. Therefore, you cannot use it like in case 1:

1
2
local level, heath, name
level, heath, name = self:GetPlayerBaseInfo(level, heath, name);

In this case, when calling Lua, both passed-in parameters and returned parameters must be specified; otherwise, normal behavior won’t occur as the values of level, heath, and name passed won’t change like C++ reference passing.

Thus, how to call the function always depends on how it is written.

This is also mentioned in UnLua’s issues: issues/25

Checking Interface Inheritance

In C++, you can use UKismetSystemLibrary::DoesImplementInterface, and the same applies in Lua, but with a difference in the type of the interface passed in:

1
2
3
4
5
6
7
local CubeClass = UE4.UClass.Load("/Game/Cube_Blueprint.Cube_Blueprint")
local World = self:GetWorld()
local Cube_Ins = World:SpawnActor(CubeClass, self:GetTransform(), UE4.ESpawnActorCollisionHandlingMethod.AlwaysSpawn, self, self)

if UE4.UKismetSystemLibrary.DoesImplementInterface(Cube_Ins, UE4.UMyInterface) then
print(fmt("{1} inherited {2}", CubeClass, UE4.UMyInterface))
end

You need to use the UE4.U*Interface format.

Obtaining TScriptInterface Interface Objects

When obtaining an interface in C++, the obtained type is TScriptInterface<>. Originally, I thought that I’d have to export TScriptInterface to access the interface. However, in UnLua, you can directly get TScriptInterface<> just like a normal UObject object.

For example, the following function:

1
2
UFUNCTION(BlueprintCallable, Category = "GameCore|Flib|GameFrameworkStatics", meta = (CallableWithoutWorldContext, WorldContext = "WorldContextObject"))
static TScriptInterface<IINetGameInstance> GetNetGameInstance(UObject* WorldContextObject);

Calling it in Lua:

1
local GameInstance = UE4.UFlibGameFrameworkStatics.GetNetGameInstance(self);

The obtained type in C++ is TScriptInterface<>, but in Lua, it becomes the UObject of that interface.

Calling Member Functions

Having obtained the TScriptInterface interface (which is actually the UObject of that interface in Lua), there are three methods to call interface methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// This class does not need to be modified.
UINTERFACE(BlueprintType, MinimalAPI)
class UINetGameInstance : public UIBaseEntityInterface
{
GENERATED_BODY()
};

class GWORLD_API IINetGameInstance
{
GENERATED_BODY()
public:
UFUNCTION(Category = "GameCore|GamePlayFramework")
virtual bool FindSubsystem(const FString& InSysName, TScriptInterface<IISubsystem>& OutSubsystem) = 0;
};

Calling Functions via Objects

  1. You can obtain the object that implements the interface and then call the function through that object (using Lua’s : operator):
1
2
local GameInstance = UE4.UflibGameFrameworkStatics.GetNetGameInstance(self);
local findRet1, findRet2 = GameInstance:FindSubsystem("TouchController")

Specifying Class and Function Name

  1. You can directly call by specifying the type name that implements the interface, much like a function pointer, passing the calling object’s instance as a parameter:
1
2
local GameInstance = UE4.UflibGameFrameworkStatics.GetNetGameInstance(self);
local findRet1, findRet2 = UE4.UNetGameInstance.FindSubsystem(GameInstance, "TouchController")

Specifying Interface Class and Function Name

  1. You can also call through the interface type (as interfaces are UClass and their functions are also marked as UFUNCTION):
1
2
local GameInstance = UE4.UflibGameFrameworkStatics.GetNetGameInstance(self);
local findRet1, findRet2 = UE4.UINetGameInstance.FindSubsystem(GameInstance, "TouchController")

Obtaining UClass

To obtain a UClass in Lua for object creation, the method is:

1
local uclass = UE4.UClass.Load("/Game/Core/Blueprints/AI/BP_AICharacter.BP_AICharacter_C")

The Load path corresponds to the PackagePath of that class.

For example, loading a UMG class in Lua and creating it to add to the viewport:

1
2
3
4
5
function LoadingMap_C:ReceiveBeginPlay()
local UMG_C = UE4.UClass.Load("/Game/Test/BPUI_TestMain.BPUI_TestMain_C")
local UMG_TestMain_Ins = UE4.UWidgetBlueprintLibrary.Create(self, UMG_C)
UMG_TestMain_Ins:AddToViewport()
end

Note: The official implementation of UE4.UClass.Load in UnLua is defaulted to only create blueprint classes. For specifics, see the implementation in LuaLib_Class.cpp where UClass_Load is implemented.

You can modify it to support C++ classes:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
int32 UClass_Load(lua_State *L)
{
int32 NumParams = lua_gettop(L);
if (NumParams != 1)
{
UE_LOG(LogUnLua, Log, TEXT("%s: Invalid parameters!"), ANSI_TO_TCHAR(__FUNCTION__));
return 0;
}

const char *ClassName = lua_tostring(L, 1);
if (!ClassName)
{
UE_LOG(LogUnLua, Log, TEXT("%s: Invalid class name!"), ANSI_TO_TCHAR(__FUNCTION__));
return 0;
}


FString ClassPath(ClassName);

bool IsCppClass = ClassPath.StartsWith(TEXT("/Script"));

if (!IsCppClass)
{
const TCHAR *Suffix = TEXT("_C");
int32 Index = INDEX_NONE;
ClassPath.FindChar(TCHAR('.'), Index);
if (Index == INDEX_NONE)
{
ClassPath.FindLastChar(TCHAR('/'), Index);
if (Index != INDEX_NONE)
{
const FString Name = ClassPath.Mid(Index + 1);
ClassPath += TCHAR('.');
ClassPath += Name;
ClassPath.AppendChars(Suffix, 2);
}
}
else
{
if (ClassPath.Right(2) != TEXT("_C"))
{
ClassPath.AppendChars(TEXT("_C"), 2);
}
}
}

FClassDesc *ClassDesc = RegisterClass(L, TCHAR_TO_ANSI(*ClassPath));
if (ClassDesc && ClassDesc->AsClass())
{
UnLua::PushUObject(L, ClassDesc->AsClass());
}
else
{
lua_pushnil(L);
}

return 1;
}

The path for C++ classes is:

1
/Script/GWorld.GWorldGameEngine

It starts with /Script, followed by the module name to which the C++ class belongs, and after the . is the class name (excluding U/A, etc.).

LoadObject

Load resources into memory:

1
local Object = LoadObject("/Game/Core/Blueprints/AI/BT_Enemy")

For instance, loading a material ball for a model:

1
2
3
4
function Cube3_Blueprint_C:ReceiveBeginPlay()
local MatIns = LoadObject("/Game/TEST/Cube_Mat_Ins")
UE4.UPrimitiveComponent.SetMaterial(self.StaticMeshComponent, 0, MatIns)
end

Note: LuaObject in UnLua corresponds to LoadObject<Object>:

1
2
3
4
5
6
int32 UObject_Load(lua_State *L)
{
// ...
UObject *Object = LoadObject<UObject>(nullptr, *ObjectPath);
// ...
}

Creating Objects

Note: Objects created in Lua with dynamic binding are bound to the UCLASS of that class, not the instance created from the New.

UnLua’s handling of NewObject is covered in Global_NewObject:

1
2
FScopedLuaDynamicBinding Binding(L, Class, ANSI_TO_TCHAR(ModuleName), TableRef);
UObject *Object = StaticConstructObject_Internal(Class, Outer, Name);

SpawnActor

In Lua, SpawnActor and dynamic binding:

1
2
3
local World = self:GetWorld()
local WeaponClass = UE4.UClass.Load("/Game/Core/Blueprints/Weapon/BP_DefaultWeapon.BP_DefaultWeapon")
local NewWeapon = World:SpawnActor(WeaponClass, self:GetTransform(), UE4.ESpawnActorCollisionHandlingMethod.AlwaysSpawn, self, self, "Weapon.BP_DefaultWeapon_C")

NewObject

In Lua, calling NewObject and dynamic binding:

1
local ProxyObj = NewObject(ObjClass, self, nil, "Objects.ProxyObject")

UnLua’s NewObject can accept four parameters: the UClass being created, Outer, Name, and the Lua script for dynamic binding.

Component

Note: The original UnLua only allows loading BP’s UClass. Modifications are needed (modify LuaLib_Class.cpp‘s UClass_Load function to eliminate the logic of adding the _C suffix when passing a C++ class), and when creating Components, you also need to call OnComponentCreated and RegisterComponent. These two functions are not UFUNCTIONs and need to be manually exported.

Export OnComponentCreated and RegisterComponent from ActorComponent:

1
2
3
4
5
6
7
8
9
// Export Actor Component
BEGIN_EXPORT_REFLECTED_CLASS(UActorComponent)
ADD_FUNCTION(RegisterComponent)
ADD_FUNCTION(OnComponentCreated)
ADD_FUNCTION(UnregisterComponent)
ADD_CONST_FUNCTION_EX("IsRegistered", bool, IsRegistered)
ADD_CONST_FUNCTION_EX("HasBeenCreated", bool, HasBeenCreated)
END_EXPORT_CLASS()
IMPLEMENT_EXPORTED_CLASS(UActorComponent)

Usage remains consistent with C++ usage:

1
2
3
4
5
6
7
8
9
local StaticMeshClass = UE4.UClass.Load("/Script/Engine.StaticMeshComponent")
local MeshObject = LoadObject("/Engine/VREditor/LaserPointer/CursorPointer")
local StaticMeshComponent = NewObject(StaticMeshClass, self, "StaticMesh")
StaticMeshComponent:SetStaticMesh(MeshObject)
StaticMeshComponent:RegisterComponent()
StaticMeshComponent:OnComponentCreated()
self:ReceiveStaticMeshComponent(StaticMeshComponent)
-- StaticMeshComponent:K2_AttachToComponent(self.StaticMeshComponent, "", EAttachmentRule.SnapToTarget, EAttachmentRule.SnapToTarget, EAttachmentRule.SnapToTarget)
UE4.UStaticMeshComponent.K2_AttachToComponent(StaticMeshComponent, self.StaticMeshComponent, "", EAttachmentRule.SnapToTarget, EAttachmentRule.SnapToTarget, EAttachmentRule.SnapToTarget)

UMG

Creating UMG first requires obtaining the UClass of the UI, then using UWidgetBlueprintLibrary::Create to create it, consistent with C++:

1
2
3
local UMG_C = UE4.UClass.Load("/Game/Test/BPUI_TestMain.BPUI_TestMain_C")
local UMG_TestMain_Ins = UE4.UWidgetBlueprintLibrary.Create(self, UMG_C)
UMG_TestMain_Ins:AddToViewport()

Binding Delegates

Dynamic Multicast Delegates

In C++ code, a dynamic multicast delegate is written as follows:

1
2
3
4
5
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FGameInstanceDyDlg, const FString&, InString);

// in class
UPROPERTY()
FGameInstanceDyDlg GameInstanceDyDlg;

In Lua, binding it can bind to Lua functions:

1
2
3
4
5
6
7
local GameInstance = UE4.UFlibGameFrameworkStatics.GetNetGameInstance(self);
GameInstance.GameInstanceDyMultiDlg:Add(self, LoadingMap_C.BindGameInstanceDyMultiDlg)

-- test bind dynamic multicast delegate lua func
function LoadingMap_C:BindGameInstanceDyMultiDlg(InString)
UE4.UKismetSystemLibrary.PrintString(self, InString)
end

Similarly, you can also invoke, clean up, and remove the delegate:

1
2
3
4
5
6
-- remove 
GameInstance.GameInstanceDyMultiDlg:Remove(self, LoadingMap_C.BindGameInstanceDyDlg)
-- Clear
GameInstance.GameInstanceDyMultiDlg:Clear()
-- broadcast
GameInstance.GameInstanceDyMultiDlg:Broadcast("66666666")

Dynamic Delegates

In C++, the following dynamic delegate is declared:

1
2
3
4
5
DECLARE_DYNAMIC_DELEGATE_OneParam(FGameInstanceDyDlg, const FString&, InString);

// in class
UPROPERTY()
FGameInstanceDyDlg GameInstanceDyDlg;

In Lua, you bind it as follows:

1
2
3
4
5
6
GameInstance.GameInstanceDyDlg:Bind(self, LoadingMap_C.BindGameInstanceDyMultiDlg)

-- test bind dynamic delegate lua func
function LoadingMap_C:BindGameInstanceDyDlg(InString)
UE4.UKismetSystemLibrary.PrintString(self, InString)
end

Dynamic delegates support Bind/Unbind/Execute operations:

1
2
3
4
5
6
-- bind
GameInstance.GameInstanceDyDlg:Bind(self, LoadingMap_C.BindGameInstanceDyMultiDlg)
-- UnBind
GameInstance.GameInstanceDyDlg:Unbind()
-- Execute
GameInstance.GameInstanceDyDlg:Execute("GameInstanceDyMultiDlg")

Non-Dynamic Delegates Are Not Supported

Since BindStatic/BindRaw/BindUFunction are all template functions, UnLua’s static export scheme does not support exporting them.

Official issue: How to correctly statically export normal delegates inherited from FScriptDelegate

Using Asynchronous Events

Delay

If you wish to use a function similar to Delay:

1
2
3
4
5
6
7
8
9
/** 
* Perform a latent action with a delay (specified in seconds). Calling again while it is counting down will be ignored.
*
* @param WorldContextWorld context.
* @param Duration length of delay (in seconds).
* @param LatentInfo The latent action.
*/
UFUNCTION(BlueprintCallable, Category = "Utilities|FlowControl", meta = (Latent, WorldContext = "WorldContextObject", LatentInfo = "LatentInfo", Duration = "0.2", Keywords = "sleep"))
static void Delay(UObject* WorldContextObject, float Duration, struct FLatentActionInfo LatentInfo);

In Lua, you can implement this using coroutine:

1
2
3
4
5
6
7
8
9
function LoadingMap_C:DelayFunc(Induration)
coroutine.resume(coroutine.create(
function(WorldContectObject, duration)
UE4.UKismetSystemLibrary.Delay(WorldContectObject, duration)
UE4.UKismetSystemLibrary.PrintString(WorldContectObject, "Helloworld")
end
),
self, Induration)
end

You can bind it directly in coroutine.create or bind to an existing function:

1
2
3
4
5
6
7
8
function LoadingMap_C:DelayFunc(Induration)
coroutine.resume(coroutine.create(LoadingMap_C.DoDelay), self, self, Induration)
end

function LoadingMap_C:DoDelay(WorldContectObject, duration)
UE4.UKismetSystemLibrary.Delay(WorldContectObject, duration)
UE4.UKismetSystemLibrary.PrintString(WorldContectObject, "Helloworld")
end

However, notice this point: when binding an existing Lua function, you need to pass an extra self to denote the caller of the function.

1
coroutine.resume(coroutine.create(LoadingMap_C.DoDelay), self, self, Induration)

Here, the first self indicates that LoadingMap_C.DoDelay is being called via self, while the last two parameters are passed to the coroutine function.

Invocation code looks like this:

1
2
3
4
function LoadingMap_C:ReceiveBeginPlay()
-- Outputs HelloWorld after 5 seconds
self:DelayFunc(5.0)
end

Note: For UFUNCTIONs with FLatentActionInfo, you can directly use the above method, but for UE encapsulated asynchronous nodes, which are not functions but class nodes, you need to export them yourself.

AsyncLoadPrimaryAsset

In C++, you can use UAsyncActionLoadPrimaryAsset::AsyncLoadPrimaryAsset to load resources asynchronously:

1
2
3
4
5
6
/** 
* Load a primary asset into memory. The completed delegate will go off when the load succeeds or fails; you should cast the Loaded object to verify it is the correct type.
* If LoadBundles are specified, those bundles are loaded along with the asset.
*/
UFUNCTION(BlueprintCallable, meta = (BlueprintInternalUseOnly = "true", Category = "AssetManager", AutoCreateRefTerm = "LoadBundles", WorldContext = "WorldContextObject"))
static UAsyncActionLoadPrimaryAsset* AsyncLoadPrimaryAsset(UObject* WorldContextObject, FPrimaryAssetId PrimaryAsset, const TArray<FName>& LoadBundles);

To use this in Lua, you need to export the FPrimaryAssetId structure:

1
2
3
4
5
6
7
8
9
10
#include "UnLuaEx.h"
#include "LuaCore.h"
#include "UObject/PrimaryAssetId.h"

BEGIN_EXPORT_CLASS(FPrimaryAssetId, const FString&)
ADD_FUNCTION_EX("ToString", FString, ToString)
ADD_STATIC_FUNCTION_EX("FromString", FPrimaryAssetId, FromString, const FString&)
ADD_FUNCTION_EX("IsValid", bool, IsValid)
END_EXPORT_CLASS()
IMPLEMENT_EXPORTED_CLASS(FPrimaryAssetId)

You can then use it in Lua to asynchronously load level resources and open them upon completion:

1
2
3
4
5
6
7
8
9
10
function Cube_Blueprint_C:ReceiveBeginPlay()
local Map = UE4.FPrimaryAssetId("Map:/Game/Test/LoadingMap")
local AsyncActionLoadPrimaryAsset = UE4.UAsyncActionLoadPrimaryAsset.AsyncLoadPrimaryAsset(self, Map, nil)
AsyncActionLoadPrimaryAsset.Completed:Add(self, Cube_Blueprint_C.ReceiveLoadedMap)
AsyncActionLoadPrimaryAsset:Activate()
end

function Cube_Blueprint_C:ReceiveLoadedMap(Object)
UE4.UGameplayStatics.OpenLevel(self, "/Game/Test/LoadingMap", true)
end

SetTimer

For some requirements, you need Timer to call continuously; in C++, you can use UKismetSystemLibrary::K2_SetTimerDelegate, and in Blueprints, this corresponds to SetTimerByEvent, which is a UFUNCTION, so you can also call it in Lua.

Binding the delegate and cleanup operations:

1
2
3
4
5
6
7
8
9
10
11
12
13
function LoadingMap_C:ReceiveBeginPlay()
UpdateUILoopCount = 0;
UpdateUITimerHandle = UE4.UKismetSystemLibrary.K2_SetTimerDelegate({self, LoadingMap_C.UpdateUI}, 0.3, true)
end

function LoadingMap_C:UpdateUI()
if UpdateUILoopCount < 10 then
print("HelloWorld")
UpdateUILoopCount = UpdateUILoopCount + 1
else
UE4.UKismetSystemLibrary.K2_ClearAndInvalidateTimerHandle(self, UpdateUITimerHandle)
end
end

{self, FUNCTION} creates a Delegate. Initially, I thought I would need to export a method to create a Dynamic Delegate myself, but it turns out I didn’t need to.

Binding UMG Control Events

If you want to bind events like OnPressed/OnClicked/OnReleased/OnHovered/OnUnhovered for UButton, these are multicast delegates, so you’ll use Add to add them:

1
2
3
4
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnButtonClickedEvent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnButtonPressedEvent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnButtonReleasedEvent);
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnButtonHoverEvent);

In Lua, to bind:

1
2
3
4
5
6
7
function UMG_LoadingMap_C:Construct()
self.ButtonItem.OnPressed:Add(self, UMG_LoadingMap_C.OnButtonItemPressed)
end

function UMG_LoadingMap_C:OnButtonItemPressed()
print("On Button Item Pressed")
end

UE4.TArray

In UnLua, the index rule for using UE4.TArray starts from 1, as opposed to the engine’s 0-based indexing. issues/41

Cast

The syntax for type casting in UnLua is:

1
Obj:Cast(UE4.AActor)

For instance:

1
local Character = Pawn:Cast(UE4.ABP_CharacterBase_C)

Using Blueprint Structures in Lua

Create a blueprint structure in bp:

In UnLua, you can access it in the following way:

1
2
3
4
5
local bp_struct_ins = UE4.FBPStruct()
bp_struct_ins.string = "123456"
bp_struct_ins.int32 = 12345
bp_struct_ins.float = 123.456
print(fmt("string: {},int32: {},float: {}",bp_struct_ins.string,bp_struct_ins.int32,bp_struct_ins.float))

You can use it like a C++ structure, but note that you need to add F in front of the blueprint structure type name. In the example above, the name in the blueprint is BPStruct, while in UnLua, it is accessed as FBPStruct.

Insight Profiler

You can encapsulate SCOPED_NAMED_EVENT on the Lua side, but since Lua does not have C++ RAII mechanisms, it cannot detect when it leaves the scope in real-time. Therefore, you can only add marks at the beginning and end:

1
2
3
4
5
function TimerMgr:UpdateTimer(DeltaTime)
local profile_tag = UE4.FProfileTag("TimerMgr_UpdateTimer")
-- dosomething...
profile_tag:End()
end

The implementation on the C++ side:

ProfileTag.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#pragma once

#include "CoreMinimal.h"
#include "ProfileTag.generated.h"

USTRUCT()
struct FProfileTag
{
GENERATED_BODY()
FProfileTag(){}

void Begin(const FString& Name)
{
if(bInit) return;
StaticBegin(Name);
bInit = true;
}
void End()
{
if(bInit)
{
StaticEnd();
bInit = false;
}
}

~FProfileTag()
{
End();
}

static void StaticBegin(const FString& Name)
{
#if PLATFORM_IMPLEMENTS_BeginNamedEventStatic
FPlatformMisc::BeginNamedEventStatic(FColor::Yellow, *Name);
#else
FPlatformMisc::BeginNamedEvent(FColor::Yellow, *Name);
#endif
}

static void StaticEnd()
{
FPlatformMisc::EndNamedEvent();
}
bool bInit = false;
};

Expose it to the Lua side:

Lualib_ProfileTag.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include "UnLuaEx.h"
#include "LuaCore.h"
#include "ProfileTag.h"
static int32 FProfileTag_New(lua_State *L)
{
int32 NumParams = lua_gettop(L);
if (NumParams < 1)
{
UE_LOG(LogUnLua, Log, TEXT("%s: Invalid parameters!"), ANSI_TO_TCHAR(__FUNCTION__));
return 0;
}

void *Userdata = NewTypedUserdata(L, FProfileTag);
FProfileTag *V = new(Userdata) FProfileTag();
if (NumParams > 1)
{
FString Name = ANSI_TO_TCHAR((char*)lua_tostring(L,2));
V->Begin(Name);
}
return 1;
}

static int32 FProfileTag_Begin(lua_State *L)
{
int32 NumParams = lua_gettop(L);
if (NumParams < 1)
{
UE_LOG(LogUnLua, Log, TEXT("%s: Invalid parameters!"), ANSI_TO_TCHAR(__FUNCTION__));
return 0;
}

FProfileTag *V = (FProfileTag*)GetCppInstanceFast(L, 1);
if (!V)
{
UE_LOG(LogUnLua, Log, TEXT("%s: Invalid FProfileTag!"), ANSI_TO_TCHAR(__FUNCTION__));
return 0;
}
FString Name = ANSI_TO_TCHAR((char*)lua_tostring(L,2));
V->Begin(Name);
return 0;
}
static int32 FProfileTag_End(lua_State *L)
{
int32 NumParams = lua_gettop(L);
if (NumParams < 1)
{
UE_LOG(LogUnLua, Log, TEXT("%s: Invalid parameters!"), ANSI_TO_TCHAR(__FUNCTION__));
return 0;
}

FProfileTag *V = (FProfileTag*)GetCppInstanceFast(L, 1);
if (!V)
{
UE_LOG(LogUnLua, Log, TEXT("%s: Invalid FProfileTag!"), ANSI_TO_TCHAR(__FUNCTION__));
return 0;
}

V->End();
return 0;
}

static const luaL_Reg FProfileTagLib[] =
{
{"__call",FProfileTag_New},
{ "Begin", FProfileTag_Begin },
{ "End", FProfileTag_End },
{ nullptr, nullptr }
};

BEGIN_EXPORT_REFLECTED_CLASS(FProfileTag)
ADD_STATIC_FUNCTION_EX("StaticBegin",void,StaticBegin,const FString&)
ADD_STATIC_FUNCTION_EX("StaticEnd",void,StaticEnd)
ADD_LIB(FProfileTagLib)
END_EXPORT_CLASS()
IMPLEMENT_EXPORTED_CLASS(FProfileTag)

Then you can use it in Lua and view the execution cost of the Lua code in Unreal Insight:

Three usage methods are provided:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- using 1
function TimerMgr:UpdateTimer(DeltaTime)
local profile_tag = UE4.FProfileTag("TimerMgr_UpdateTimer")
-- dosomething...
profile_tag:End()
end

-- using 2
function TimerMgr:UpdateTimer(DeltaTime)
local profile_tag = UE4.FProfileTag()
profile_tag:Begin(("TimerMgr_UpdateTimer"))
-- dosomething...
profile_tag:End()
end

-- using 3
function TimerMgr:UpdateTimer(DeltaTime)
UE4.FProfileTag.StaticBegin(("TimerMgr_UpdateTimer"))
-- dosomething...
UE4.FProfileTag.StaticEnd()
end

It is recommended to use the object creation method, as the created object will call End when the Lua object is destroyed if it is forgotten, preventing issues with Insight’s data collection. Using StaticBegin/StaticEnd must ensure that all code branches can reach StaticEnd.
You can combine Lua’s debug.getinfo(1).name to get the function name and display it in Insight:

1
2
3
4
5
function TimerMgr:UpdateTimer(DeltaTime)
local profile_tag = UE4.FProfileTag(debug.getinfo(1).name)
-- dosomething...
profile_tag:End()
end
未完待续,欢迎指出问题和交流意见。

Scan the QR code on WeChat and follow me.

Title:UE hot update: Lua programming guide based on UnLua
Author:LIPENGZHA
Publish Date:2020/03/24 10:50
Update Date:2021/06/22 11:08
Word Count:12k Words
Link:https://en.imzlp.com/posts/36659/
License: CC BY-NC-SA 4.0
Reprinting of the full article is prohibited.
Your donation will encourage me to keep creating!