当前位置: 首页 > backend >正文

UnLua源码分析(一)初始化流程

UnLua源码分析(一)初始化流程

  • 接入
  • 插件启动
  • 注册设置
  • 默认参数集
  • 注册回调
  • SetActive
  • PostLoadMapWithWorld
  • ULuaEnvLocator
    • 启动Lua虚拟机
    • 初始化UE相关的Lua Lib
    • 创建与Lua交互的数据结构
    • 注册静态导出的类,函数,枚举
    • Lua层初始化
  • UUnLuaManager
  • 总结
  • Reference

UnLua是适用于UE的一个高度优化的Lua脚本解决方案。我们今天先来分析一下它的初始化流程。本文基于UE 5.5的环境,分析的UnLua源码版本为是最新的Devlop分支。

接入

首先是去下载Develop分支的源码,这个最新分支修复了UE 5.4版本的编译问题。不过很不幸,它不能在5.5版本下编译通过,主要原因也是UE 5.5版本的某些API发生了变化。接入时可以参考GitHub上的相关issue。编译通过之后,就可以参考UnLua官方给的新手教程,进行Lua开发了。打开官方的TPS工程,在Tutorial目录下也有若干展示UnLua特性的例子。

插件启动

UnLua是以插件的形式加载到UE的,那么我们很容易找到它的启动入口,位于UnLuaModule.cpp中的FUnLuaModule::StartupModule函数。我们这里只截取当前关心的内容,其他部分先略去:

virtual void StartupModule() override
{RegisterSettings();FCoreUObjectDelegates::PostLoadMapWithWorld.AddRaw(this, &FUnLuaModule::PostLoadMapWithWorld);CreateDefaultParamCollection();#if AUTO_UNLUA_STARTUP
#if WITH_EDITORif (!IsRunningGame()){FEditorDelegates::PreBeginPIE.AddRaw(this, &FUnLuaModule::OnPreBeginPIE);FEditorDelegates::PostPIEStarted.AddRaw(this, &FUnLuaModule::OnPostPIEStarted);FEditorDelegates::EndPIE.AddRaw(this, &FUnLuaModule::OnEndPIE);FGameDelegates::Get().GetEndPlayMapDelegate().AddRaw(this, &FUnLuaModule::OnEndPlayMap);}if (IsRunningGame() || IsRunningDedicatedServer())
#endifSetActive(true);
#endif
}

可以看到,负责启动的入口函数还是比较简洁的,第一步是注册一些设置,第二步是创建默认的参数集,第三步会根据当前是否为编辑器环境,如果是则注册一些回调函数,来控制编辑器环境下UnLua的生命周期,如果是打包版则直接启动UnLua。我们先来看看第一个步骤,注册设置。

注册设置

RegisterSettings负责向UE编辑器注册UnLua的配置项,并且注册了配置修改的回调,然后便从ini文件中加载读取当前的配置。

void RegisterSettings()
{
#if WITH_EDITORISettingsModule* SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>("Settings");if (!SettingsModule)return;const auto Section = SettingsModule->RegisterSettings("Project", "Plugins", "UnLua",LOCTEXT("UnLuaEditorSettingsName", "UnLua"),LOCTEXT("UnLuaEditorSettingsDescription", "UnLua Runtime Settings"),GetMutableDefault<UUnLuaSettings>());Section->OnModified().BindRaw(this, &FUnLuaModule::OnSettingsModified);
#endif#if ENGINE_MAJOR_VERSION >=5 && !WITH_EDITOR// UE5下打包后没有从{PROJECT}/Config/DefaultUnLua.ini加载,这里强制刷新一下FString UnLuaIni = TEXT("UnLua");GConfig->LoadGlobalIniFile(UnLuaIni, *UnLuaIni, nullptr, true);UUnLuaSettings::StaticClass()->GetDefaultObject()->ReloadConfig();
#endifauto& Settings = *GetDefault<UUnLuaSettings>();bPrintLuaStackOnSystemError = Settings.bPrintLuaStackOnSystemError;
}

在Project Settings/Plugins目录下,可以看到UnLua的配置项,我们暂时不去关心这些配置的具体用途。
在这里插入图片描述

默认参数集

CreateDefaultParamCollection会从一个UBT自动生成的inl文件中,读取UE中包含默认参数的函数,加入到一个名为GDefaultParamCollection的全局Map中。

TMap<FName, FFunctionCollection> GDefaultParamCollection;void CreateDefaultParamCollection()
{static bool CollectionCreated = false;if (!CollectionCreated){CollectionCreated = true;#include "DefaultParamCollection.inl"}
}

打开DefaultParamCollection.inl文件可以看到大量的函数名称和参数名称,例如:

FC = &GDefaultParamCollection.Add(TEXT("UAvoidanceManager"));
PC = &FC->Functions.Add(TEXT("RegisterMovementComponent"));
PC->Parameters.Add(TEXT("AvoidanceWeight"), new FFloatParamValue(0.500000f));

对照引擎代码,的确可以在UAvoidanceManager中找到函数的定义:

ENGINE_API bool RegisterMovementComponent(class UMovementComponent* MovementComp, float AvoidanceWeight = 0.5f);

注册回调

编辑器环境下,会去监听当前是否处于PIE模式。可以看到UnLua的初始化逻辑分为两块,一部分在进入PIE模式之前执行,一部分则在进入PIE模式之后再执行。

void OnPreBeginPIE(bool bIsSimulating)
{SetActive(true);
}void OnPostPIEStarted(bool bIsSimulating)
{UEditorEngine* EditorEngine = Cast<UEditorEngine>(GEngine);if (EditorEngine)PostLoadMapWithWorld(EditorEngine->PlayWorld);
}

打包版同样也会先调用SetActive,然后在加载地图时调用PostLoadMapWithWorld。显然这两个函数就是UnLua初始化的核心函数了。

SetActive

SetActive接受一个bool类型的参数,说明它同时负责启动和销毁UnLua的逻辑,这里我们先只关心初始化的部分,一些细节也先略去:

virtual void SetActive(const bool bActive) override
{if (bIsActive == bActive)return;if (bActive){GUObjectArray.AddUObjectCreateListener(this);GUObjectArray.AddUObjectDeleteListener(this);const auto& Settings = *GetMutableDefault<UUnLuaSettings>();const auto EnvLocatorClass = *Settings.EnvLocatorClass == nullptr ? ULuaEnvLocator::StaticClass() : *Settings.EnvLocatorClass;EnvLocator = NewObject<ULuaEnvLocator>(GetTransientPackage(), EnvLocatorClass);EnvLocator->AddToRoot();for (const auto Class : TObjectRange<UClass>()){for (const auto& ClassPath : Settings.PreBindClasses){if (!ClassPath.IsValid())continue;const auto TargetClass = ClassPath.ResolveClass();if (!TargetClass)continue;if (Class->IsChildOf(TargetClass)){const auto Env = EnvLocator->Locate(Class);Env->TryBind(Class);break;}}}}bIsActive = bActive;
}

主要也是三件事情,首先是对UObject的创建和销毁进行了监听,这个很自然,因为UnLua需要为UObject绑定相关的Lua信息,实现Lua层与C++层之间的交互;第二是创建了一个ULuaEnvLocator类型的对象,通过类的定义可知它主要负责从上层管理Lua虚拟机环境,这个类型还支持通过配置进行修改;最后是如果配置项中存在需要预先绑定的类,则在此时尝试进行绑定。这里绑定的概念是双向的,意味着会把C++层的方法暴露给Lua层,同时也把Lua层覆盖或新增的方法设置进来,这块内容留到后面再详细展开。

UCLASS()
class UNLUA_API ULuaEnvLocator : public UObject
{GENERATED_BODY()
public:virtual UnLua::FLuaEnv* Locate(const UObject* Object);virtual void HotReload();virtual void Reset();TSharedPtr<UnLua::FLuaEnv, ESPMode::ThreadSafe> Env;
};

默认配置下UnLua有3个需要提前绑定的类:
在这里插入图片描述

PostLoadMapWithWorld

相较之下,PostLoadMapWithWorld就比较简单了,它主要就是创建出UUnLuaManager类型的对象了,这个manager负责具体的绑定工作。

void PostLoadMapWithWorld(UWorld* World) const
{if (!World || !bIsActive)return;const auto Env = EnvLocator->Locate(World);if (!Env)return;const auto Manager = Env->GetManager();if (!Manager)return;Manager->OnMapLoaded(World);
}

通过上述分析,我们进一步发现初始化的核心逻辑就在ULuaEnvLocatorUUnLuaManager中。

ULuaEnvLocator

ULuaEnvLocator提供了一个Locate函数,负责返回一个FLuaEnv类型的对象。这个对象是UnLua的核心对象,负责管理Lua虚拟机。

UnLua::FLuaEnv* ULuaEnvLocator::Locate(const UObject* Object)
{if (!Env){Env = MakeShared<UnLua::FLuaEnv, ESPMode::ThreadSafe>();Env->Start();}return Env.Get();
}

接下来对FLuaEnv的构造函数进行逐步分析。

启动Lua虚拟机

#if PLATFORM_WINDOWS// 防止类似AppleProResMedia插件忘了恢复Dll查找目录// https://github.com/Tencent/UnLua/issues/534const auto Dir = FPaths::ConvertRelativePathToFull(FPaths::ProjectDir() / TEXT("Binaries/Win64"));FPlatformProcess::PushDllDirectory(*Dir);L = lua_newstate(GetLuaAllocator(), nullptr);FPlatformProcess::PopDllDirectory(*Dir);
#elseL = lua_newstate(GetLuaAllocator(), nullptr);
#endifAllEnvs.Add(L, this);luaL_openlibs(L);AddSearcher(LoadFromCustomLoader, 2);AddSearcher(LoadFromFileSystem, 3);AddSearcher(LoadFromBuiltinLibs, 4);

此时Lua虚拟机已创建完成,并且Lua的标准库也都加载进来了。此外,UnLua还调整了Lua文件的搜索路径,使得Lua虚拟机可以读取到UE工程目录下的源文件。当然,我们也可以自定义自己的Loader。

初始化UE相关的Lua Lib

    UELib::Open(L);

Open函数会向Lua的全局环境中注册UE的表,表中包含几个基本的UE库函数,同时还设置了__index元方法,这样Lua层在访问UE.XXX时就会触发这里的逻辑。

static constexpr luaL_Reg UE_Functions[] = {{"LoadObject", UObject_Load},{"LoadClass", UClass_Load},{"NewObject", Global_NewObject},{NULL, NULL}
};int UnLua::UELib::Open(lua_State* L)
{lua_newtable(L);lua_pushstring(L, "__index");lua_pushcfunction(L, UE_Index);lua_rawset(L, -3);lua_pushvalue(L, -1);lua_setmetatable(L, -2);lua_pushvalue(L, -1);lua_pushstring(L, REGISTRY_KEY);lua_rawset(L, LUA_REGISTRYINDEX);luaL_setfuncs(L, UE_Functions, 0);lua_setglobal(L, NAMESPACE_NAME);// global access for legacy supportlua_getglobal(L, LUA_GNAME);luaL_setfuncs(L, UE_Functions, 0);lua_pop(L, 1);#if WITH_UE4_NAMESPACE == 1// 兼容UE4访问lua_getglobal(L, NAMESPACE_NAME);lua_setglobal(L, "UE4");
#elif WITH_UE4_NAMESPACE == 0// 兼容无UE4全局访问lua_getglobal(L, LUA_GNAME);lua_newtable(L);lua_pushstring(L, "__index");lua_getglobal(L, NAMESPACE_NAME);lua_rawset(L, -3);lua_setmetatable(L, -2);
#endifreturn 1;
}

创建与Lua交互的数据结构

    ObjectRegistry = new FObjectRegistry(this);ClassRegistry = new FClassRegistry(this);ClassRegistry->Initialize();FunctionRegistry = new FFunctionRegistry(this);DelegateRegistry = new FDelegateRegistry(this);ContainerRegistry = new FContainerRegistry(this);PropertyRegistry = new FPropertyRegistry(this);EnumRegistry = new FEnumRegistry(this);EnumRegistry->Initialize();lua_pushstring(L, "StructMap"); // create weak table 'StructMap'LowLevel::CreateWeakValueTable(L);lua_rawset(L, LUA_REGISTRYINDEX);lua_pushstring(L, "ArrayMap"); // create weak table 'ArrayMap'LowLevel::CreateWeakValueTable(L);lua_rawset(L, LUA_REGISTRYINDEX);

通过名字就能得知,这里创建了保存与Lua交互信息的Object、Class、Container、Struct、Array等注册表。它们的主要作用是将Lua层的对象与C++层的对象进行映射,方便调用和管理。具体细节我们等遇到了再说。

注册静态导出的类,函数,枚举

    // register statically exported classesauto ExportedNonReflectedClasses = GetExportedNonReflectedClasses();for (const auto& Pair : ExportedNonReflectedClasses)Pair.Value->Register(L);// register statically exported global functionsauto ExportedFunctions = GetExportedFunctions();for (const auto& Function : ExportedFunctions)Function->Register(L);// register statically exported enumsauto ExportedEnums = GetExportedEnums();for (const auto& Enum : ExportedEnums)Enum->Register(L);

所谓的静态导出,就是在UnLua加载时,利用静态变量初始化的方式,预先导出给Lua的类,函数和枚举。比如TArray,我们在LuaLib_Array.cpp中,可以找到它静态导出的代码:

static const luaL_Reg TArrayLib[] =
{{"Length", TArray_Length},{"Num", TArray_Length},{"Add", TArray_Add},{"AddUnique", TArray_AddUnique},{"Find", TArray_Find},{"Insert", TArray_Insert},{"Remove", TArray_Remove},{"RemoveItem", TArray_RemoveItem},{"Clear", TArray_Clear},{"Reserve", TArray_Reserve},{"Resize", TArray_Resize},{"GetData", TArray_GetData},{"Get", TArray_Get},{"GetRef", TArray_GetRef},{"Set", TArray_Set},{"Swap", TArray_Swap},{"Shuffle", TArray_Shuffle},{"LastIndex", TArray_LastIndex},{"IsValidIndex", TArray_IsValidIndex},{"Contains", TArray_Contains},{"Append", TArray_Append},{"ToTable", TArray_ToTable},{"__gc", TArray_Delete},{"__call", TArray_New},{"__pairs", TArray_Pairs},{"__index", TArray_Index},{"__newindex", TArray_NewIndex},{nullptr, nullptr}
};EXPORT_UNTYPED_CLASS(TArray, false, TArrayLib)IMPLEMENT_EXPORTED_CLASS(TArray)

EXPORT_UNTYPED_CLASS是一个宏,它定义了一个struct,和该struct类型的静态变量,以及它的构造函数,包含了静态导出的逻辑:

#define EXPORT_UNTYPED_CLASS(Name, bIsReflected, Lib) \struct FExported##Name##Helper \{ \static FExported##Name##Helper StaticInstance; \FExported##Name##Helper() \: ExportedClass(nullptr) \{ \UnLua::IExportedClass *Class = UnLua::FindExportedClass(#Name); \if (!Class) \{ \ExportedClass = new UnLua::TExportedClassBase<bIsReflected>(#Name); \UnLua::ExportClass(ExportedClass); \Class = ExportedClass; \} \Class->AddLib(Lib); \} \~FExported##Name##Helper() \{ \delete ExportedClass; \} \UnLua::IExportedClass *ExportedClass; \};

IMPLEMENT_EXPORTED_CLASS宏就是对该静态变量进行初始化,这样在UnLua启动时,就会自动调到它的构造函数,完成静态导出。

#define IMPLEMENT_EXPORTED_CLASS(Name) \FExported##Name##Helper FExported##Name##Helper::StaticInstance;

Lua层初始化

    UnLuaLib::Open(L);

在UnLua完成C++层面的构造之后,UnLua会再执行一段Lua逻辑,完成最后的初始化工作。

int Open(lua_State* L)
{lua_register(L, "print", LogInfo);luaL_requiref(L, "UnLua", LuaOpen, 1);luaL_dostring(L, R"(setmetatable(UnLua, {__index = function(t, k)local ok, result = pcall(require, "UnLua." .. tostring(k))if ok thenrawset(t, k, result)return resultelset.LogWarn(string.format("failed to load module UnLua.%s\n%s", k, result))endend}))");#if UNLUA_ENABLE_FTEXTluaL_dostring(L, "UnLua.FTextEnabled = true");
#elseluaL_dostring(L, "UnLua.FTextEnabled = false");
#endif#if UNLUA_WITH_HOT_RELOADluaL_dostring(L, R"(pcall(function() _G.require = require('UnLua.HotReload').require end))");
#endifLegacySupport(L);lua_pop(L, 1);return 1;
}

可以看到,UnLua在全局环境中定义了UnLua表,访问UnLua.XXX时,会直接去加载UnLua.XXX.lua文件,另外UnLua重写了require函数,改用HotReload模块,用于热重载的支持。

UUnLuaManager

UUnLuaManager构造函数则主要初始化UE Input相关的逻辑。

UUnLuaManager::UUnLuaManager(): InputActionFunc(nullptr), InputAxisFunc(nullptr), InputTouchFunc(nullptr), InputVectorAxisFunc(nullptr), InputGestureFunc(nullptr), AnimNotifyFunc(nullptr)
{if (HasAnyFlags(RF_ClassDefaultObject)){return;}GetDefaultInputs();             // get all Axis/Action inputsEKeys::GetAllKeys(AllKeys);     // get all key inputs// get template input UFunctions for InputAction/InputAxis/InputTouch/InputVectorAxis/InputGesture/AnimNotifyUClass *Class = GetClass();InputActionFunc = Class->FindFunctionByName(FName("InputAction"));InputAxisFunc = Class->FindFunctionByName(FName("InputAxis"));InputTouchFunc = Class->FindFunctionByName(FName("InputTouch"));InputVectorAxisFunc = Class->FindFunctionByName(FName("InputVectorAxis"));InputGestureFunc = Class->FindFunctionByName(FName("InputGesture"));AnimNotifyFunc = Class->FindFunctionByName(FName("TriggerAnimNotify"));
}

总结

自此我们梳理了UnLua的整个初始化流程,UnLua的初始化主要分为两个部分,一部分是C++层的初始化,另一部分是Lua层的初始化。C++层主要完成了Lua虚拟机的创建和UE相关的注册表的创建,Lua层则完成了最后的注册和热重载支持。UnLua的设计思路还是比较清晰的,后续我们会继续分析UnLua与UE交互的一些细节。

Reference

[1] UnLua GitHub

[2] UE4和UnLua交互核心环境分析

http://www.xdnf.cn/news/7072.html

相关文章:

  • 13.Ext系列文件系统
  • 【2025版】SpringCloud Gateway网关快速入门
  • 相机Camera日志分析之十二:高通相机Camx hal拍照1帧logcat日志capture拍照帧详解
  • 基于CNN的猫狗识别(自定义CNN模型)
  • AIDA64 extreme7.5 版本注册激活方法
  • 掌握LINQ:查询语法与方法语法全解析
  • 什么是 Flink Pattern
  • 内容中台的AI基石是什么?
  • TDengine 在新能源领域的价值
  • 前端动画库 Anime.js 的V4 版本,兼容 Vue、React
  • OpenHarmony外设驱动使用 (四),Face_auth
  • 蓝牙通讯协议学习
  • 内容社区系统开发文档(中)
  • 继MCP、A2A之上的“AG-UI”协议横空出世,人机交互迈入新纪元
  • windows环境下c语言链接sql数据库
  • Kubernetes控制平面组件:Kubelet详解(六):pod sandbox(pause)容器
  • JSON Schema 高效校验 JSON 数据格式
  • 微服务项目->在线oj系统(Java版 - 2)
  • c++编写中遇见的错误
  • 【AWS入门】Amazon SageMaker简介
  • 4:OpenCV—保存图像
  • 解决 Tailwind CSS 代码冗余问题
  • 机器学习(12)——LGBM(1)
  • Python爬虫基础
  • 选择合适的AI模型:解析Trae编辑器中的多款模型及其应用场景
  • Go 语言中的一等公民(First-Class Citizens)
  • Flutter与Kotlin Multiplatform(KMP)深度对比及鸿蒙生态适配解析
  • STM32单片机开发环境搭建 keil/proteus仿真/STM32CubeMX
  • 【OpenGL学习】(三)元素缓冲对象(EBO)的使用
  • Limesurvay系统“48核心92GB服务器”优化方案