基于 UnLua 的 Lua 编程指南

UE 使用的是 C++ 这种编译型语言,在编译之后就成了二进制,只有通过玩家重新安装才能打到更新游戏的目的。但是对于游戏业务而言,对于需求调整和 bug 修复时间要求非常迫切,频繁地让玩家更新 App 是不能接受的,游戏项目一般使用 Lua 作为游戏业务的脚本语言,是为了把运行时不可变的 C++ 代码变成运行时可更新的 Lua 代码。

UE 官方没有提供 Lua 的支持,但是腾讯开源了UnLua,在我当前的项目使用了,这两天我梳理了一下 UnLua 的资料(主要是官方文档、issus、宣讲 PPT),加上自己测试 UnLua 写了一个小 Demo 的感悟,形成了本篇 UE 结合 UnLua 的编程指南,主要是总结使用 UnLua 来写业务的一些基本方法和坑,方便查看,本篇文章会持续更新。

另外,Lua 文件打包成 Pak 可以用我之前开源的工具:hxhb/HotPatcher,而且我基于 UnLua 自己修改了一个版本,添加了一些额外的优化,源码集成了 Luasocket/Luapanda/lpeg/Sproto/Luacrypt 库,可以直接使用 LuaPanda 调试,Github 地址为:hxhb/debugable-unlua.

参考资料

UnLua 注意事项

这些是 UnLua 官方仓库里我摘录出来的一些可能有坑的地方。

  • 并非所有的 UFUNCTION 都支持,只支持BlueprintNativeEvent/BlueprintImplementationEvent/Replication Notify/Animation Notify/Input Event
  • UnLua 不支持多 State,所以在 PIE 模式下运行多个 Client 会有问题。issus/78
  • 不要在 Lua 里访问蓝图定义的结构体。issues/119 / issus/40(新的提交已支持)
  • UnLua 里使用 self.Super 不会递归向下遍历继承层次中的所有基类。issus/131
  • 非 dynamic delegate 不支持。issus/128
  • 不可以直接导出类的 static 成员,但可以为它封装一个 static 方法。issus/22
  • 不支持绑定 lua 脚本到对象实例,NewObject 指定的脚本是绑定到 UCLASS 的。issus/134
  • Unlua 里使用 UE4.TArray 的下标规则是从 1 开始的,与引擎中 TArray 以 0 开始不同.issues/41
  • 注意导出给 Lua 的函数很多与蓝图中的名字不同,如蓝图中的 GetActorTransform 其实在 lua 里要用 GetTransform,在 lua 里使用的名字都是 C++ 真实定义的函数名字,而不是DisplayName 的名字。

Lua 代码提示

导出符号

UnLua 提供了对引擎内反射符号的导出,也可以自己静态导出非反射类的符号,并且提供了一个CommandletUUnLuaIntelliSenseCommandlet导出这些符号。

下面简单介绍一下 Commandlet 的调用方法:

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

那么 UnLua 的 Commandlet 用法为:

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

执行完毕之后,会生成 UnLua/Interamate/IntelliSense 这个目录,里面有引擎导出到 lua 的符号。

VSCode 中使用

在 VSCode 中安装 Emmylua 插件,安装之后把上一步生成的 IntelliSense 目录添加到 vcode 的工作区即可。

调用父类函数

在 UEC++ 中,当我们重写了一个父类的虚函数,可以通过调用 Super:: 来指定调用父类实现,但是在 Lua 中不同。

1
self.Super.ReceiveBeginPlay(self)

在 Lua 使用 self.Super 来调用父类的函数。

注意:UnLua 里的 Super 只是简单模拟了“继承”语义,在继承多层的情况下会有问题,Super 不会主动向下遍历 Super。“父类”不是 Class()返回的表的元表,只设置在 Super 这个 field 上(元表在插件的 c++ 代码里定义了,这个过程已经固化)。

对于 基类的基类 的 Super 调用:

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

该问题摘录于 UnLua 的 issus:Class 内的 Super 的使用问题

调用被覆写的方法

注意:Lua 和原来的类并不是继承关系,而是依附关系,lua 依赖于蓝图或者 C++ 的类。Lua 覆写的类的函数,相当于给当前类的函数换了一个实现。

当在 Lua 中重写了一个附属类的 UFUNCTION 函数时,可以通过下列方法调用, 有点类似于 Super 但要写成Overridden

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

注意一定要传 self 进去,不然 Unlua 调用的时候执行的参数检查会 Crash,在 UnLua 中调用 UE 函数有点类似于拿到成员函数的原生指针,必须要手动传 this 进去。

调用 UE 的 C++ 函数

UnLua 在编译时可以开启是否启用 UE 的 namespace(也就是 UE 的函数都需要加 UE4 前缀)。

调用方法为:

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

参数与 C++ 调用的相匹配(具有默认参数的同样可以不写):

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

覆写多返回值的函数

蓝图

覆写的 lua 代码:

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

C++

因为 C++ 的多返回值是通过传递引用参数进去实现的,所以在 Lua 中这个不太一样。

如下面的 C++ 函数:

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;
}

这个函数接收 WorldContextObject 的参数,并接收 FString/int32/UObject* 这三个类型的引用类型,并返回一个 bool,那么这个函数该怎么在 lua 中接收这些参数值的?

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

这种 local ret1,ret2=func() 的这种写法是 Lua 里的规则,可以看 Programming in Lua,4th 的第六章。

注意:接收引用参数的返回值和真正函数返回值的顺序是:ret1,ret2,ret3 都是引用参数,最终函数的返回 bool 是最后一个 ret4。

非 const 引用参数作为返回值需要注意的问题

注意:当调用 UFUNCTION 函数时,非 const 引用参数可以忽略,但是非 UFUNCTION 而是静态导出的函数则不行,因为 UnLua 对静态导出的函数有参数个数检查。

而且,使用引用作为返回参数的的使用方式需要考虑到下面两种情况:

  1. 非 const 引用作为纯输出
1
2
3
4
5
6
void GetPlayerBaseInfo(int32 &Level, float &Health, FString &Name)
{
Level = 7;
Health = 77;
Name = "Marcus";
}

这种情况下返回值和传入值是没有任何关系的。在 lua 中可以这么使用:

1
local level,heath,name = self:GetPlayerBaseInfo(0,0,"");
  1. 非 const 引用参数既作为输入又作为输出
1
2
3
4
5
6
void GetPlayerBaseInfo(int32 &Level, float &Health, FString &Name)
{
Level += 7;
Health += 77;
Name += "Marcus";
}

在这种情况下,返回值和输入是有直接关系的,所以不能像情况 1 中那样使用:

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

在这种情况下,在 lua 里调用传入的参数和返回的参数是都 必须 要传递和接收的,这样才会有正常的行为,如果不接收返回值:

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

level,heath,name这些传进去的对象的值并不会像 C++ 中传递的引用那样值会改变。

所以函数怎么调用还是要看函数里是怎么写的。

这个在 UnLua 的 issus 里有提到:issus/25

检测是否继承接口

C++ 里使用UKismetSystemLibrary::DoesImplementInterface,Lua 里也一样,不过区别是接口类型的传入:

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} inheritanced {2}",CubeClass,UE4.UMyInterface))
end

要使用 UE4.U*Interface 这种形式。

获取 TScriptInterface 接口对象

当我们在 C++ 中获得一个接口时,获得的类型是 TScriptInterface<> 类型,本来以为还要自己导出 TScriptInterface 才可以拿到接口,但是发现并不是这样,UnLua 里可以直接拿到 TScriptInterface<> 就像普通的 UObject 对象:

如下面这样一个函数:

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

在 Lua 里调用:

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

这个得到的类型在 C++ 里是 TScriptInterface<> 但在 Lua 里得到的就是该接口的 UObject 对象。

调用成员函数

上面讲了怎么得到 TScriptInterface 的接口(在 lua 里得到的其实就是该接口的 UObject),那么怎么通过它来调用接口的函数呢?有三种方法。

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;
};

通过对象调用函数

  1. 可以通过拿到实现接口的对象然后通过该对象调用函数(使用 lua 的 : 操作符):
1
2
local GameInstance = UE4.UFlibGameFrameworkStatics.GetNetGameInstance(self);
local findRet1,findRet2 = GameInstance:FindSubsystem("TouchController")

指定类和函数名调用

  1. 也可以直接通过指定实现该接口的类型名字来调用,就像函数指针,需要把调用该函数的对象传递进去:
1
2
local GameInstance = UE4.UFlibGameFrameworkStatics.GetNetGameInstance(self);
local findRet1,findRet2 = UE4.UNetGameInstance.FindSubsystem(GameInstance,"TouchController")

指定接口的类和函数名调用

  1. 以及通过接口的类型调用(因为接口也是 UClass,接口中的函数也都标记了 UFUNCTION):
1
2
local GameInstance = UE4.UFlibGameFrameworkStatics.GetNetGameInstance(self);
local findRet1,findRet2 = UE4.UINetGameInstance.FindSubsystem(GameInstance,"TouchController")

获取 UClass

lua 中获取 uclass 可以用于创建对象,其方法为:

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

Load 的路径是该类的PackagePath

如,在 lua 中加载 UMG 的类然后创建并添加至视口:

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

注意:UnLua 官方的 UE4.UClass.Load 实现是默认只能创建蓝图类的,具体实现看 LuaLib_Class.cpp 中的 UClass_Load 的实现。

可以修改一下支持 C++ 的类:

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;
}

C++ 类的路径为:

1
/Script/GWorld.GWorldGameEngine

/Script 开头,其后是该 C++ 类所属的模块名,.之后的是类名(不包含 U/A 之类的开头)。

LoadObject

把资源加载到内存:

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

比如加载某个材质球给模型:

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

注:LuaObject 在 UnLua 里对应的是LoadObject<Object>:

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

创建对象

注意:Lua 中创建的对象使用动态绑定是绑定到该类的 UCLASS 上,并不是绑定到该 New 出来的实例。

UnLua 中对 NewObject 处理的代码为Global_NewObject

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

SpawnActor

Lua 中 SpawnActor 以及动态绑定:

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

lua 中调用 NewObject 以及动态绑定:

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

UnLua 中的 NewObject 可以接收四个参数,依次是:创建的 UClass、Outer、Name,以及动态绑定的 Lua 脚本。

Component

注意:原版 UnLua 只可以加载 BP 的 UClass,这个需要做改动 (修改LuaLib_Class.cpp 中的 UClass_Load 函数,检测传入是 C++ 类时把添加 _C 后缀的逻辑去掉), 而且创建 Component 时也需要对其调用 OnComponentCreatedRegisterComponent,这两个函数不是 UFUNCTION,需要手动导出。

导出 ActorComponent 中 OnComponentCreatedRegisterComponent等函数:

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)

则使用时与 C++ 的使用方法一致:

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

创建 UMG 首先需要获取到 UI 的 UClass,然后使用 UWidgetBlueprintLibrary::Create 来创建,与 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()

绑定代理

动态多播代理

在 C++ 代码中写了一个动态多播代理:

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

// in class
UPROPERTY()
FGameInstanceDyDlg GameInstanceDyDlg;

在 lua 中绑定,可以绑定到 lua 的函数:

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

同样也可以对该代理进行调用、清理、移除:

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

动态代理

C++ 中有如下动态代理声明:

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

// in class
UPROPERTY()
FGameInstanceDyDlg GameInstanceDyDlg;

在 lua 中绑定:

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

动态代理支持 Bind/Unbind/Execute 操作:

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

不支持非 Dynamic Delegate

因为 BindStatic/BindRaw/BindUFunction 这些都是模板函数,UnLua 的静态导出方案不支持将他们导出。

官方 issus:如何正确静态导出继承自 FScriptDelegate 的普通委托

使用异步事件

Delay

如果想要使用类似 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 voidDelay(UObject* WorldContextObject, float Duration, struct FLatentActionInfo LatentInfo );

在 lua 中可以通过 协程 (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

就是通过 coroutine.create 绑定上一个函数,可以直接在 coroutine.create 里写,或者绑定上一个已有的函数:

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

但是要 注意一点:在绑定已有的 lua 函数时,传递的参数需要多一个self,标识调用指定函数的调用者。

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

这里的第一个 self,就是在通过self 调用LoadingMap_C.DoDelay,后面的两个参数才作为传递给协程函数的参数。

调用代码为:

1
2
3
4
function LoadingMap_C:ReceiveBeginPlay()
-- 5s 后输出 HelloWorld
self:DelayFunc(5.0)
end

注意:对于直接是 UFUNCTION 但是带有 FLatentActionInfo 的函数可以直接使用上面的方法,但是对于 UE 封装的异步节点,不是函数而是一个类的节点需要自己导出。

AsyncLoadPrimaryAsset

在 C++ 里可以使用 UAsyncActionLoadPrimaryAsset::AsyncLoadPrimaryAsset 来异步加载资源:

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 is 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);

想要在 Lua 中使用的话需要把 FPrimaryAssetId 这个结构导出:

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)

然后就可以在 Lua 中使用了,如异步加载关卡资源,加载完成后打开:

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

有些需求需要 Timer 循环调用,在 C++ 里可以使用 UKismetSystemLibrary::K2_SetTimerDelegate,在蓝图中对应的是SetTimerByEvent,因为它是UFUNCTION 的函数,所以在 lua 中也可以调用。

绑定代理和清理操作:

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}会创建出来一个 Delegate,本来还以为要自己导出一个创建 Dynamic Delegate 的方法,其实不用。

绑定 UMG 控件事件

如果要绑定类似 UButtonOnPressed/OnClicked/OnReleased/OnHovered/OnUnhovered等事件,它们都是多播代理,所以要用 Add 来添加:

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

在 lua 中绑定:

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

Unlua 里使用 UE4.TArray 的下标规则是从 1 开始的,而不是与引擎中相同的 0.issues/41

Cast

UnLua 里的类型转换语法为:

1
Obj:Cast(UE4.AActor)

如:

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

lua 使用蓝图结构

在 bp 里创建一个蓝图结构:

在 UnLua 里可以通过下列方式访问:

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))

可以像 C++ 的结构那样使用, 需要 注意 的地方为, 需要在蓝图结构类型的名字前加F, 如上面的例子里, 在蓝图中的名字为BPStruct, 在 UnLua 中访问时则为FBPStruct.

Insight Profiler

详见 wiki:Insight Profiling in lua