添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

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_UE4下的Lua脚本插件PPT
  • UnLua_Programming_Guide_EN
  • Programming in Lua,1th
  • Tencent/UnLua/issus
  • 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提供了对引擎内反射符号的导出,也可以自己静态导出非反射类的符号,并且提供了一个 Commandlet UUnLuaIntelliSenseCommandlet 导出这些符号。

    下面简单介绍一下 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对静态导出的函数有参数个数检查。

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

  • 非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,"");
  • 非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的 : 操作符):
    2. 1
      2
      local GameInstance = UE4.UFlibGameFrameworkStatics.GetNetGameInstance(self);
      local findRet1,findRet2 = GameInstance:FindSubsystem("TouchController")

      指定类和函数名调用

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

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

        1. 以及通过接口的类型调用(因为接口也是UClass,接口中的函数也都标记了UFUNCTION):
        2. 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时也需要对其调用 OnComponentCreated RegisterComponent ,这两个函数不是UFUNCTION,需要手动导出。

          导出ActorComponent中 OnComponentCreated RegisterComponent 等函数:

          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控件事件

          如果要绑定类似 UButton OnPressed / 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

          可以把 SCOPED_NAMED_EVENT 封装到Lua端,但是因为Lua没有C++的RAII机制,无法实时地检测离开作用域时机,所以只能通过在起始和结尾位置添加标记:

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

          在C++端的实现:

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

          暴露给Lua端:

          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)

          然后即可在Lua中使用,并且能够在Unreal Insight中查看Lua端代码执行的消耗:

          提供了三种使用方式:

          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

          建议使用创建对象的方式使用,因为创建的对象,如果漏掉End,在lua对象被销毁时也会调用End,不会造成Insight的数据采集问题,使用 StaticBegin/StaticEnd 则一定要保证所有的代码分支能执行到 StaticEnd
          可以组合lua的 debug.getinfo(1).name 获取函数名字在Insight中显示:

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