在Lua中实现对UE4 C++代码的自动补全

2020-03-30 18:06:52 浏览数 (1)

本文介绍了在Emmylua插件的支持下,如何获取到UE4的反射信息,并如何生成Emmylua格式的Lua注释代码来支持自动补全和跳转。

TOC

引言

随着吃鸡的火热,手游越来越迈入重度手游的时代,画面愈发成为各大游戏比拼的重头戏之一。因此越来越多的项目组开始使用UE4引擎来进行开发。而手游的热更,目前最流行的方案还是基于Lua。同时Lua的开发效率优势也使得越来越多的UE4游戏项目组使用Lua C 来作为开发语言。

Lua作为一门在游戏领域大众,在非游戏领域小众的语言(甚至如果不是云风的大力推广,Lua可能在游戏领域可能会更小众一些),UE4对Lua也并不提供原生支持。我们项目接入的是slua-unreal,可以提供UE4中进行Lua开发的基础支持。

不过,如何能够保证在UE4中进行Lua开发的效率?Lua能够像C 或者C#一样支持代码补全和跳转吗?

废话不多说,先上效果:

当然,这个补全的前提是你接入的lua框架(我们项目是slua-unreal)需要支持对UE4反射变量的访问。

原理

Emmylua对Unity函数的自动补全

如果你使用Unity Lua开发,可能在一些工具和插件中已经见识过Lua对于Unity函数的自动补全。笔者是Emmylua插件重度用户,因此在这里简单介绍一下Emmylua插件的自动补全机制以及对于Unity的自动补全原理。

Emmylua是一个基于IntelliJ IDEA的Lua插件(后续也出了VSCode)的版本。简单说,Intellij IDEA(和VSCode)提供了一套友好的插件开发环境。提供了一系列的规则来实现任意语言的高亮、跳转、补全的功能。Emmylua就是基于这个IDE开发的一个Lua的插件。它特别之处在于定义了一套自定义注释的语法,可以实现类变量的补全。例如:

代码语言:txt复制
-- 定义一个类
---@class Test
---@field a number

-- 将变量A声明为类Test
---@type Test
local A

-- 输入A.就可以补全A中的变量
A.Test

更详细的规则可以参见Emmylua官网。

在Emmylua 1.2.2版本中,提供了一个功能,可以识别C#的dll,并生成对应的lua类型注释。它的原理并不难,就是利用C#的反射功能,读取dll中的反射信息,并生成对应的lua注释文件。

例如,我定义一个非常简单的C#类:

代码语言:txt复制
namespace DP {
    class Test {
        public int a;
        public string func(int p) {
            // do something
            return ""
        }
    }
}

使用Emmylua生成的lua文件为:

代码语言:txt复制
--- fields
---@field public a number
--- properties
---@class DP.Test : table
local m = {}

---@param p number
---@return string
function m.func(p) end

DP.Test = m
return m

这样,如果有某个lua变量定义类型为DP.Test,就可以补全Test类中包含的函数或者字段。

总结Unity的Lua补全原理其实就是两条:

  1. 通过反射获取类信息
  2. 生成Emmylua格式的注释

UE4中Lua自动补全的实现原理

了解了Unity的补全原理,这套机制是不是可以用在UE4上呢?UE4的原生语言是C ,C 这货也有反射?

答案是:可以!!

UE4的一大迷人之处,就是支持反射。一系列的特性都是基于它自带的反射机制。简单来说,UE4的反射系统,是针对UObject的。通过在定义时对变量打标签(UPROPERTY、UFUNCTION等),UE4会通过UHT来静态扫描代码,从而生成.generated.h和.gen.cpp文件,并通过static构造的方式,使得生成的文件在main函数之前调用,从而生成反射信息。

如果想要详细了解UE4的反射机制,可以参看笔者另一篇文章:UE4 反射系统详细剖析

这里我们需要对UE4的反射结构有初步的了解。以下是UE4的反射系统的类图:

UObject全家福类图UObject全家福类图

我们需要关注的,主要是四种类型:

  • UStruct:所有的反射类。我们遍历的目标。
  • UEnum:所有的反射的枚举。我们遍历的目标。
  • UProperty:反射类中的属性字段。
  • UFunction:反射类中的函数字段。

于是方案变得非常清晰:

  1. 通过UE4的反射系统获取到所有反射信息
  2. 生成Emmylua格式的注释,来让Emmylua插件生成补全信息。

具体实现

获取UE4的反射信息

下面一步步地将需要用到的功能列举出来:

  1. 获取全部反射类和子类

UE4提供了一个接口GetObjectsOfClass(UClass* ClassToLookFor),接受一个类型,返回所有该类型的反射类和子类。

例如,我如果调用:

代码语言:txt复制
TArray<UObject*> ClassArray;
GetObjectsOfClass(UStruct::StaticClass(), ClassArray);

那么我可以获取到所有继承自UStruct的类。

  1. 遍历某类中的所有字段

使用TFieldIterator<ClassType>。这严格来说并不是一个函数。这是UE4提供的一个迭代器类,可以访问某个UClass(及其子类)下的所有指定类型的字段。

例如,我如果调用:

代码语言:txt复制
for (TFieldIterator<UFunction> Iterator(CppStruct); Iterator;   Iterator)
{
    UFunction* Function = *Iterator;
    // 可以对每个Function做任意处理
}

那么我可以获取到CppStruct这个类中的所有函数。同理,我也可以获取到这个类中的所有UProperty。

PS: 这个遍历会将本类和其所有父类的字段都遍历一遍。如果不加处理,最终生成的临时文件会非常大,严重影响IO速度和整体生成速度。笔者在这里使用了临时结构,构造了非常多的TSet来进行过滤。最终文件大小减小了70%。

  1. 获取父类

使用UStruct::GetSuperStruct()来获取父类

  1. 获取类名前缀和类名

使用UStruct::GetPrefixCPP()来获取类名前缀。

使用UStruct::GetName()来获取类名。

  1. 获取某个字段的类型

使用UProperty::GetCPPType(FStrint& ExtendedTypeText)来获取类型。如果类型是一个模板,那么会将模板中的类型字符串赋值给ExtendedTypeText来返回。

  1. 获取函数的形参和返回类型

通过TFieldIterator<UProperty>(Function)来访问函数的形参和返回值。对于遍历到的每个UProperty,检查其位域属性PropertyFlags。如果EPropertyFlags::CPF_ReturnParm位为1,那么说明这是返回值,否则说明这是形参。

不管是形参还是返回值,如果要获取其名称和类型,与获取普通UProperty的名称和类型的方法相同。

  1. 获取所有类的接口

通过UClass中的Interfaces属性来访问其所有接口类。

  1. 获取全部枚举、枚举名以及枚举值

这些放在一起说明。通过GetObjectsOfClass(UEnum::StaticClass()来访问所有枚举。

通过UEnum::NumEnums()可以获取到枚举中的变量总数。

通过UEnum::GetNameByIndex()来访问枚举名。

通过UEnum::GetValueByIndex()来访问枚举值。

通过上述接口,就可以完整地收集到UE4反射系统的所有需要的信息。

生成Emmylua格式注释文件

既然有了UE4的所有反射信息,生成Emmylua文件不是很简单?

看起来似乎是这样的。不过还是有个问题,如何生成?

Emmylua生成C#代码的Lua文件的做法,是直接在C#代码中写死格式。其部分源代码如下:

代码语言:txt复制
contentSb.Append("---@class ");
contentSb.Append(CS_NAME_SPACE);
contentSb.Append(".");
contentSb.Append(nameSpace);
contentSb.Append(" : table");
contentSb.AppendLine();

恩。。上面代码的最终生成的代码如下:

代码语言:txt复制
---@class DP.Test : table

如果我将来需要改生成的格式,我就需要来找到这处代码修改、编译、运行。或者需要提供使用者自定义生成格式的功能,这种方法显然做不到。

对于IDE来说,使用C#的原生StringBuilder类来实现模板代码生成,具有最好的性能,虽然降低了灵活性,但可以理解。不过我们格式代码的生成是交给构建机定时做的,而且生成时间在可接受范围内(一般人的PC上大约耗时两秒),于是笔者决定采用另一种方案:基于模板引擎来生成代码。

笔者之前用python实现过一个简单的模板引擎(如果感兴趣,可以移步这里:从头实现一个简单模板引擎),已经在项目中大量使用。因此这次也是直接拿来用也具有最低的开发成本。

UE4支持直接生成python对象调用python函数。不过为了可调试性和可扩展性,笔者采用的方案是先生成中间文件(json格式),再将json文件直接传给模板引擎来生成文件(该模板引擎原生支持json文件)。

于是最终的流程为:

  1. 将UE4的反射信息生成.json文件。
  2. 用python对.json文件中的数据进行一层加工(为了简化模板代码的逻辑)
  3. 按照加工后的的数据格式,写模板代码。
  4. 调用模板引擎生成代码。

采用这种方式,只需要定义模板代码为:

代码语言:txt复制
---@class {{namespace}}.{{class.name}} : table

一行代码,而且具有更强的可读性。

拿UButton类举例,最终生成的Lua注释代码如下:

代码语言:txt复制
---@class UE4.UButton : UE4.UContentWidget
---@field public Style UE4.USlateWidgetStyleAsset
---@field public WidgetStyle UE4.FButtonStyle
---@field public ColorAndOpacity UE4.FLinearColor
---@field public BackgroundColor UE4.FLinearColor
---@field public ClickMethod UE4.EButtonClickMethod
---@field public TouchMethod UE4.EButtonTouchMethod
---@field public PressMethod UE4.EButtonPressMethod
---@field public IsFocusable boolean
---@field public OnClicked UE4.FOnButtonClickedEvent
---@field public OnPressed UE4.FOnButtonPressedEvent
---@field public OnReleased UE4.FOnButtonReleasedEvent
---@field public OnHovered UE4.FOnButtonHoverEvent
---@field public OnUnhovered UE4.FOnButtonHoverEvent
---@field public SetTouchMethod fun(self:UE4.UButton, InTouchMethod:UE4.EButtonTouchMethod)
---@field public SetStyle fun(self:UE4.UButton, InStyle:UE4.FButtonStyle):UE4.FButtonStyle
---@field public SetPressMethod fun(self:UE4.UButton, InPressMethod:UE4.EButtonPressMethod)
---@field public SetColorAndOpacity fun(self:UE4.UButton, InColorAndOpacity:UE4.FLinearColor)
---@field public SetClickMethod fun(self:UE4.UButton, InClickMethod:UE4.EButtonClickMethod)
---@field public SetBackgroundColor fun(self:UE4.UButton, InBackgroundColor:UE4.FLinearColor)
---@field public IsPressed fun(self:UE4.UButton):boolean

PS:这里全部采用注释,而不是像Emmylua一样对每个类生成一个local的table。这是为了避免一些新接触项目的开发同学误解这个文件的用途。不需要了解这套机制,也能够知道这些注释代码仅仅是注释而已,对逻辑没有任何影响。

总结

本文介绍了在Emmylua插件的支持下,如何获取到UE4的反射信息,并如何生成Emmylua格式的Lua注释代码来支持自动补全和跳转。

参考文献

  1. 知乎InsideUE4专栏
  2. UE4 反射系统详细剖析
  3. Emmylua官网
  4. 从头实现一个简单模板引擎
lua

0 人点赞