Bluelua 新特性: 在 lua 中重载蓝图函数

lua中重载 C++ 函数

现在 Github: Bluelua 中已经实现了在 lua 中重载 C++ BlueprintImplementableEventBlueprintNativeEvent 函数,在 C++ 中调用这两种 Event 会自动调用到 lua 中的实现,如果 lua 中没有对应实现,那么会去调用蓝图中的实现,调用过程分别如图

eventflow.jpg

其中实现的原理很简单,因为 UObject 类中有个虚函数 ProcessEvent,在 C++ 中调用上面两种 Event 的时候,最终都会进到这个虚函数中,所以可以重载这个虚函数,然后在判断 lua 中是否有同名函数,如果有就直接调用 lua 的实现,没有或者调用失败就继续走原来的流程

lua中重载蓝图函数

问题

在将现有的蓝图逻辑移到 lua 中的时候经常遇到一个问题,假如直接在蓝图A中定义了一个函数,然后在蓝图B中进行调用,这个时候如果我们将蓝图A的逻辑移到 lua 中,就发现这个函数是移不了的,因为它在另外的蓝图B里使用了,如果要移的话,需要将蓝图B的逻辑也移到 lua 中,这样本来只是在迁移一个蓝图,结果发现需要连带迁移很多其他蓝图,非常不方便

如果定义在蓝图中的函数也可以在 lua 中进行重载,当外部蓝图调用它的时候实际上是调用了 lua 中的实现,这样就可以集中修改一个蓝图而不用担心影响到其他蓝图,会方便很多。或者想在蓝图中直接调用 lua 的函数,那么可以在蓝图里把函数原型定义出来,然后在 lua 里实现,蓝图进行调用,实现混合编程

和上面重载 Event 方法不同,一个蓝图调用另外的蓝图函数,这整个过程都是在虚拟机中完成的,也就是 UObject::CallFunctionUObject::ProcessInternal 这两个函数中完成,不幸的是 CallFunction 不是虚函数,无法重载,所以当蓝图函数被调用时是完全不知道的,也就无法将调用转到 lua 里

解决方法

Bluelua 里解决以上问题的方法比较 Tricky:

  1. 首先加载 lua 文件完成的时候,会调用 lua 中 OnInitBPFunctionOverriding 的方法,这个方法里需要返回一个 table,包含所有 lua 中要重载的蓝图函数名称
  2. 然后会在类的反射对象 UClass 中查找这些方法,将这些方法加上一个 FUNC_Native 属性,伪装成一个定义在C++中的蓝图可调用方法,然后将 Native Function 的入口从 UObject::ProcessInternal 改成自己的静态函数 ProcessBPFunctionOverride,这样当蓝图函数被调用的时候,就会进到 ProcessBPFunctionOverride 里,然后在这里将对应的函数调用转到 lua 中

总结

重载一个蓝图函数的完整步骤如下,例子见 BlueluaDemo: LuaUseCaseWidget

lua 中实现函数 OnInitBPFunctionOverriding

1
2
3
4
5
6
function m:OnInitBPFunctionOverriding()
return {
'TestLuaOverrideBPFunction',
'TestFunction2',
}
end

lua 中实现函数 TestLuaOverrideBPFunction

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
function m:TestLuaOverrideBPFunction(Param1, Param2, Param3, Param4, Param5, Param6, Param7, Param8, Param9, Param10, Param11, Param12, Param13)
print('TestLuaOverrideBPFunction in lua')
print('Param1:', Param1)
print('Param2:', Param2)
print('Param3:', Param3)
print('Param4:', Param4)
print('Param5:', Param5)
print('Param6:', Param6)
print('Param7:', Param7)
print('Param8:', Param8.X, Param8.Y, Param8.Z)
print('Param9:', Param9.Yaw, Param9.Roll, Param9.Pitch)

print('Param10 Location:', Param10.Translation.X, Param10.Translation.Y, Param10.Translation.Z)
print('Param10 Rotation:', Param10.Rotation.X, Param10.Rotation.Y, Param10.Rotation.Z)
print('Param10 Scale:', Param10.Scale3D.X, Param10.Scale3D.Y, Param10.Scale3D.Z)
print('Param11:', Param11)

for i, v in ipairs(Param12) do
print('Param12', i, v)
end

for i, v in ipairs(Param13) do
print('Param13', i, v)
end

local KismetMathLibrary = LoadClass('KismetMathLibrary')

Param1 = not Param1
Param2 = Param2 + 100
Param3 = Param3 + 100
Param4 = Param4 + 100
Param5 = '150'
Param6 = '160'
Param7 = '170'

Param8 = KismetMathLibrary:MakeVector(180, 180, 180)
Param9 = KismetMathLibrary:MakeRotator(190, 190, 190)

Param12 = {5, 4, 3, 2, 1}
return Param12, Param1, Param2, Param3, Param4, Param5, Param6, Param7, Param8, Param9, Param10, Param11, Param12, Param13
end

蓝图中的定义和调用不需要修改

define.png

call.png

运行结果

result.png

如果将 lua 中的 OnInitBPFunctionOverridingTestLuaOverrideBPFunction 两个函数删掉,再次运行游戏,结果会调用到蓝图的实现,如图

result_bp.png

Have fun!