-
Notifications
You must be signed in to change notification settings - Fork 426
简单说明
目前unreal提供的开发语言包括c++和蓝图,但这2个开发语言都或多或少存在一定的问题,对于c++来说,最大的问题是c++开发人员越来越少,精通c++开发的同学更少,而且c++本身并不是描述业务的最好语言,稍微不留心就容易崩溃或者内存泄露,这是不能接受的,其次unreal下修改c++编译时间太过漫长,如果还修改了header文件,那每次编译真的可以去喝一杯咖啡了,工作效率不高,还有就是调试,如果你想快点运行,选择dev build,则势必丢失一些调试信息,或者出现错误调用栈,或者某些局部变量看不到信息,或者某些断点失效,如果你选择debug build,则启动速度慢,运行慢,好好的一个i7多核计算机,搞的比乌龟都慢,如果正好赶上项目上线,问题频发,想死的心都有。
如果你选择使用蓝图,我只能说作为程序员你骨骼清奇,这玩意都能用于实际业务开发,跑跑demo,做做prototype还行,一般没有业务用蓝图作为主要开发语言用于产品,它最大的问题是不能merge,无法多人协作开发;稍微修改一些接口,某些slot就要重新链接;稍微大一点的蓝图程序看着都头晕。一般蓝图都是作为配置或者流程描述来用。
有介于此,做一套脚本化的开发语言是必须的。
对,你没有看错,Unreal早期版本其实内建支持lua,只需要自己开启一个宏WITH_LUA,然后重新编译unreal引擎,就可以开启lua,但这个功能在unreal仅仅是概念演示,而且从某个版本之后也不再维护了,实际使用起来也有很多问题,更重要的是,这个功能需要重新编译UE4,这对于大多数拿着引擎就是开箱即用的开发组,重新编译引擎是不现实的,所以我们需要提供一个扩展的插件,不用重新编译,也能方便使用lua来开发。
这说起来就有点激动了,说了这么多也总算进入正题了。
1)对于蓝图类和蓝图方法的调用
什么是蓝图类和蓝图方法呢?就是所以标记了了UCLASS和UFUNCTION的类和函数,UE4为这些类和函数提供反射能力,通过使用反射,slua可以方便调用这些函数,蓝图自己也使用这些反射能力来支持蓝图调用,所以理论上我们使用这些能力来供lua调用,不会比蓝图自己更慢。
2)支持使用lua function作为蓝图的事件代理
在蓝图里支持代理,例如:
/** Called when the button is clicked */
UPROPERTY(BlueprintAssignable, Category="Button|Event")
FOnButtonClickedEvent OnClicked;
这个OnClicked就是代理,可以绑定一个c++函数,或者绑定一个蓝图slot用于触发事件调用函数,slua支持传入一个lua function作为代理函数,调用进入lua函数。例如:
-- find sub widget from the panel
local btn2=ui:FindWidget('Button1');
local index = 1
-- handle click event
btn2.OnClicked:Add(function()
index=index+1
print('say helloworld',index)
end);
3)对于非蓝图类和非蓝图方法,支持基于静态代码生成的自动导出 和 基于模板展开的手动添加
在实际项目中,我们有很多代码并非是蓝图类,但也需要在lua中使用,比如最常见的FVector,这个类并不是蓝图类(一般蓝图类都是U开头的类),但我们需要在lua中使用FVector来完成位置、方向的计算,我们就需要把FVector导出到lua中使用,为此slua附带了一个工具,通过这个工具可以自动化的导出我们指定的c++类,并生成对应的静态代码,这个工具是基于libclang,它使用libclang产生的反射信息来完成代码生成,这点上类似Unity版的slua,最终生成的代码如下:
static void bind(lua_State* L) {
LuaObject::newType(L, "FVector");
LuaObject::addOperator(L, "__add", __add);
LuaObject::addOperator(L, "__sub", __sub);
LuaObject::addOperator(L, "__mul", __mul);
LuaObject::addOperator(L, "__div", __div);
LuaObject::addOperator(L, "__eq", __eq);
LuaObject::addField(L, "X", get_X, set_X, true);
LuaObject::addField(L, "Y", get_Y, set_Y, true);
LuaObject::addField(L, "Z", get_Z, set_Z, true);
......
LuaObject::addMethod(L, "BoxPushOut", BoxPushOut, false);
LuaObject::addMethod(L, "Parallel", Parallel, false);
LuaObject::addMethod(L, "Coincident", Coincident, false);
LuaObject::addMethod(L, "Orthogonal", Orthogonal, false);
LuaObject::addMethod(L, "Coplanar", Coplanar, false);
LuaObject::addMethod(L, "Triple", Triple, false);
LuaObject::addMethod(L, "RadiansToDegrees", RadiansToDegrees, false);
LuaObject::addMethod(L, "DegreesToRadians", DegreesToRadians, false);
LuaObject::finishType(L, "FVector", __ctor, __gc);
}
可以看到slua将FVector的成员方法都导出了,整体的代码风格与slua unity版本类似。
除了支持静态代码生成的导出,也支持基于可变参数模板的导出,这需要手动添加简单的导出代码,例如:
class Foo {
public:
void bar(const char*,int) {}
FString getStr() { return FString(UTF8_TO_TCHAR("some text")); }
};
DefLuaClass(Foo)
DefLuaMethod(bar,&Foo::bar)
DefLuaMethod(getStr,&Foo::getStr)
EndDef()
}
slua会基于可变参数模板自动展开代码,产生正确参数解析和函数返回值,生成对应的导出函数,不需要对原始c++类做任何注入式的修改。
4)支持数学运算符重载
正如上面提到的FVector,它需要若干计算功能的函数,如果是突兀的Add,Mul看起来很奇怪,而且本身FVector在c++层面也支持运算符重载,所以slua也将这部分能力导出到了lua里,支持在lua层面的运算符重载,方便代码书写。
5)从蓝图直接调用到lua并返回任意返回值
一般使用lua的情景是从c++代码调用lua,但蓝图提供了热更新的能力,有时候我们希望通过蓝图的热更新能力来启动lua代码,这个时候就需要从蓝图调用lua函数,同时返回lua返回值到蓝图,例如有如下lua函数:
function bpcall(a,b,c,d)
print("call from bp",a,b,c,d)
return 1024,"return from lua"
end
我们可以构造如下蓝图来调用lua
我们可以传入任意数量的参数,任意参数类型,并返回任意个数的返回值。
6)支持out类型的蓝图参数和引用类型的c++参数作为返回值
与c#类似,蓝图也支持out类型的参数用于返回多余的返回值,而c++这里,一般我们使用非const引用来返回多余参数(当然也可能不),slua支持这种使用情况,对于out类型的蓝图函数参数会额外返回,对于非const的函数参数也会额外返回,对于c++这里,slua无法区分函数设计时的语义,只要非const的引用类型,都会额外当做返回值返回,当然你可以选择忽略不使用。
7)通过静态代码生成,导出了UE4所有的enum,并使用int支持enum参数
8)支持扩展方法
类似c#的extension method,slua unreal也支持扩展方法,什么是扩展方法呢?比如一个UUserWidget这个蓝图类,存在如下方法:
/** @returns The uobject widget corresponding to a given name */
UWidget* GetWidgetFromName(const FName& Name) const;
它并不是蓝图方法,但存在在蓝图类里,我们可能非常需要这个函数能够导出到lua使用,但我们又不想为此修改引擎代码,添加一个UFUNCTION标签,这时我们可以做一个扩展描述:
REG_EXTENSION_METHOD(UUserWidget,"GetWidgetFromName",&UUserWidget::GetWidgetFromName);
REG_EXTENSION_METHOD_IMP(UUserWidget,"RemoveWidgetFromName",{
CheckUD(UUserWidget,L,1);
CheckUDEX(UWidget,widget,L,2);
bool ret = UD->WidgetTree->RemoveWidget(widget->ud);
return LuaObject::push(L,ret);
});
这样就为UUserWidget添加2个扩展方法,这2个方法可以在lua侧被调用,可以看到第一个GetWidgetFromName方法直接使用UUserWidget的成员方法,第二个RemoveWidgetFromName方法则是手动实现了一个版本,通过这样描述,我们不需要修改UE4引擎就可以为UUserWidget添加扩展的lua方法,非常方便。
最后我们看一个完整使用demo代码:
-- import blueprint class to use
local Button = import('Button');
local ButtonStyle = import('ButtonStyle');
local TextBlock = import('TextBlock');
local SluaTestCase=import('SluaTestCase');
-- call static function of uclass
SluaTestCase.StaticFunc()
-- create Button
local btn=Button();
local txt=TextBlock();
-- load panel of blueprint
local ui=slua.loadUI('/Game/Panel.Panel');
-- add to show
ui:AddToViewport(0);
-- find sub widget from the panel
local btn2=ui:GetWidgetFromName('Button1');
local index = 1
-- handle click event
btn2.OnClicked:Add(function()
index=index+1
print('say helloworld',index)
end);
-- handle text changed event
local edit=ui:FindWidget('TextBox_0');
local evt=edit.OnTextChanged:Add(function(txt) print('text changed',txt) end);
-- use FVector
local p = actor:K2_GetActorLocation()
local h = HitResult()
local v = FVector(math.sin(tt)*100,2,3)
local offset = FVector(0,math.cos(tt)*50,0)
-- support Operator
local ok,h=actor:K2_SetActorLocation(v+offset,true,h,true)
-- get referenced value
print("hit info",h)