前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >在Lua中实现对UE4 C++代码的自动补全

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

原创
作者头像
阿苏勒
发布2020-03-30 18:06:52
6.1K1
发布2020-03-30 18:06:52
举报
文章被收录于专栏:阿苏勒的精神小屋

本文介绍了在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. 从头实现一个简单模板引擎

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 原理
    • Emmylua对Unity函数的自动补全
      • UE4中Lua自动补全的实现原理
      • 具体实现
        • 获取UE4的反射信息
          • 生成Emmylua格式注释文件
          • 总结
          • 参考文献
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档