本文介绍了在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补全原理其实就是两条:
- 通过反射获取类信息
- 生成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的反射系统的类图:
我们需要关注的,主要是四种类型:
UStruct
:所有的反射类。我们遍历的目标。UEnum
:所有的反射的枚举。我们遍历的目标。UProperty
:反射类中的属性字段。UFunction
:反射类中的函数字段。
于是方案变得非常清晰:
- 通过UE4的反射系统获取到所有反射信息
- 生成Emmylua格式的注释,来让Emmylua插件生成补全信息。
具体实现
获取UE4的反射信息
下面一步步地将需要用到的功能列举出来:
- 获取全部反射类和子类
UE4提供了一个接口GetObjectsOfClass(UClass* ClassToLookFor)
,接受一个类型,返回所有该类型的反射类和子类。
例如,我如果调用:
代码语言:txt复制TArray<UObject*> ClassArray;
GetObjectsOfClass(UStruct::StaticClass(), ClassArray);
那么我可以获取到所有继承自UStruct的类。
- 遍历某类中的所有字段
使用TFieldIterator<ClassType>
。这严格来说并不是一个函数。这是UE4提供的一个迭代器类,可以访问某个UClass(及其子类)下的所有指定类型的字段。
例如,我如果调用:
代码语言:txt复制for (TFieldIterator<UFunction> Iterator(CppStruct); Iterator; Iterator)
{
UFunction* Function = *Iterator;
// 可以对每个Function做任意处理
}
那么我可以获取到CppStruct这个类中的所有函数。同理,我也可以获取到这个类中的所有UProperty。
PS: 这个遍历会将本类和其所有父类的字段都遍历一遍。如果不加处理,最终生成的临时文件会非常大,严重影响IO速度和整体生成速度。笔者在这里使用了临时结构,构造了非常多的TSet来进行过滤。最终文件大小减小了70%。
- 获取父类
使用UStruct::GetSuperStruct()
来获取父类
- 获取类名前缀和类名
使用UStruct::GetPrefixCPP()
来获取类名前缀。
使用UStruct::GetName()
来获取类名。
- 获取某个字段的类型
使用UProperty::GetCPPType(FStrint& ExtendedTypeText)
来获取类型。如果类型是一个模板,那么会将模板中的类型字符串赋值给ExtendedTypeText来返回。
- 获取函数的形参和返回类型
通过TFieldIterator<UProperty>(Function)
来访问函数的形参和返回值。对于遍历到的每个UProperty,检查其位域属性PropertyFlags
。如果EPropertyFlags::CPF_ReturnParm
位为1,那么说明这是返回值,否则说明这是形参。
不管是形参还是返回值,如果要获取其名称和类型,与获取普通UProperty的名称和类型的方法相同。
- 获取所有类的接口
通过UClass中的Interfaces
属性来访问其所有接口类。
- 获取全部枚举、枚举名以及枚举值
这些放在一起说明。通过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文件)。
于是最终的流程为:
- 将UE4的反射信息生成.json文件。
- 用python对.json文件中的数据进行一层加工(为了简化模板代码的逻辑)
- 按照加工后的的数据格式,写模板代码。
- 调用模板引擎生成代码。
采用这种方式,只需要定义模板代码为:
代码语言: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注释代码来支持自动补全和跳转。
参考文献
- 知乎InsideUE4专栏
- UE4 反射系统详细剖析
- Emmylua官网
- 从头实现一个简单模板引擎