unlua
最近新入职了一家公司,我原来是做数字孪生的项目的,在这一家公司里面要求去学习Lua以及Lyra,还是就是深入了解UE以及相关的面试题。所有今天就腾讯的unlua做一个基础的了解,目的是尽可能多的了解就可以了。如果能够搭起来一个大概的框架以及做一个小的demo的话就更好了。
目的
- 了解Lua
- 测试运行一个demo
- 写一个系列的博客
- 了解lua网络同步
参考文献
【UE5 C++】腾讯Unlua青春版 自己实现 一个工程搞懂反射_哔哩哔哩_bilibili
问题
unlua和lua有什么区别?
- unlua并非独立于 Lua 的新语言,而是一种基于lua设计的工具,类似与C#编程语言。
unlua和lyra
- unlua是腾讯开发一个UE脚本绑定插件
- lyra是UE开源的一个第一人称和第三人称的脚手架。方便我们开发游戏,降低了开发的门槛。
UnLua 作为 Unreal Engine(UE)的 Lua 绑定框架,核心价值是"用 Lua 快速扩展 UE 功能",80%的项目需求仅依赖 20%的核心概念(如蓝图-Lua 交互、UE 类绑定、生命周期管理等)。本计划聚焦这 20%核心,先不讨论底层的源码。
核心目标:完成 UnLua 环境配置,理解 Lua 与 UE 的基础通信逻辑,能通过 Lua 操作 UE 基础组件(如 Actor、变量、函数)。
1.1环境搭建与 UnLua 核心定位
- 学习内容 :
- UE 与 UnLua 版本匹配 :确认你的 UE 版本(推荐 4.26+ 或 5.0+,兼容性更优),从 UnLua 官方仓库 下载对应版本的插件(注意:UE5 需选择支持"Lyra 模板"的分支)。
- 插件安装与工程配置 :
- 将 UnLua 文件夹复制到 UE 工程的
Plugins目录下,重启 UE 并启用插件。 - 开启"蓝图C++混合工程"(若为纯蓝图工程,需先添加 C++ 类触发工程编译),确保 UnLua 自动生成绑定代码。
- 将 UnLua 文件夹复制到 UE 工程的
- UnLua 核心定位:理解 UnLua 不是"替代蓝图/C++",而是"补充"------适合快速迭代逻辑(如UI交互、数值配置)、热更新(需额外集成热更框架),核心是"将 UE 的类/函数暴露给 Lua 调用"。
- 实操任务 :
- 新建一个空白 UE 工程(命名为 UnLua_Learn),成功安装 UnLua 插件,在"内容浏览器"中看到 UnLua 自带的示例文件夹(如
UnLua/Examples)。 - 运行 UnLua 示例中的
BP_ExampleActor,观察 Lua 脚本如何控制 Actor 移动(初步感受"Lua 驱动 UE 物体")。
- 新建一个空白 UE 工程(命名为 UnLua_Learn),成功安装 UnLua 插件,在"内容浏览器"中看到 UnLua 自带的示例文件夹(如
1.2Lua 基础回顾(针对 UnLua 场景)
- 学习内容 :无需重学 Lua 全量语法,聚焦 UnLua 中高频使用的 20% Lua 特性:
- 数据类型 :重点掌握
table(UnLua 中映射 UE 类/结构体)、function(绑定 UE 函数/回调)、nil(对应 UE 的空引用)。 - 核心语法 :
table的创建与访问(local obj = {x=10, y=20})、函数定义(local func = function(param) ... end)、循环(for i=1,10 do ... end)。 - UnLua 专属 Lua 规则 :Lua 中访问 UE 类成员需用
:(如Actor:GetActorLocation()),而非.;UE 枚举需用UE.EnumName.Value(如UE.ESlateVisibility.Visible)。
- 数据类型 :重点掌握
- 实操任务 :
- 在 Lua 脚本中定义一个
PlayerData表(包含HP=100、MP=50、Level=1),编写函数AddLevel实现升级(Level+1 时 HP+20),并打印结果。 - 在 UE 中创建一个空 Actor,通过 UnLua 绑定上述 Lua 脚本,在"世界大纲"中选中 Actor,通过"细节面板"查看 Lua 中定义的变量(验证绑定生效)。
- 在 Lua 脚本中定义一个
unlua table
在Unlua框架中,C++与Lua之间用于传递复杂结构化数据的luaTable交互层。就是table是用来传输数据的,用在C++和lua之间,所以呢它非常的重要。本质上是Unlua封装的一套API,能够去让C++暴露的一种结构化的数据。比如说像UE结构体,类的实例对象等等。
用途
用来解决C++与lua之间的用于传递复杂结构化数据的luatable交互层。
逻辑上解耦,就是可以通过table控制C++逻辑,技能参数通过luaTable定义,C++读取后实例化。
demo
c
// 假设LUA脚本中定义了角色配置
playerConfig = {
Name = "Warrior",
Level = 20,
HP = 1500,
Skills = { "Slash", "Shield Bash","Charge"}, // 嵌套的Table
Position = { X = 100.0 ,Y = 200.0, Z = 0.0 } // 对应FVector
}
c++
#include "UnLua.h"
#include "LuaContext.h"
#include "Kismet/KismetStringLibrary.h"
void ReadPlayerConfigFromLua()
{
// 1. 获取 UnLua 上下文(UE 中通常从 UWorld 或 Lua 虚拟机实例获取)
UWorld* World = GEngine->GetWorldContexts()[0].World();
if (!World) return;
ULuaContext* LuaContext = ULuaContext::Get(World);
if (!LuaContext) return;
// 2. 获取 Lua 中的 PlayerConfig Table(压入 Lua 栈)
LuaContext->GetLuaState()->GetGlobal("PlayerConfig");
if (!LuaContext->GetLuaState()->IsTable(-1)) // 检查栈顶是否为 Table
{
UE_LOG(LogTemp, Error, TEXT("PlayerConfig is not a Lua Table!"));
LuaContext->GetLuaState()->Pop(1); // 清理栈,避免内存泄漏
return;
}
// 3. 读取 Table 中的基础类型(字符串、整数、浮点数)
FString PlayerName;
int32 PlayerLevel = 0;
float PlayerHP = 0.0f;
// 读取字符串键 "Name" 对应的值
LuaContext->GetLuaState()->GetField(-1, "Name"); // 栈:... -> Table -> NameValue
if (LuaContext->GetLuaState()->IsString(-1))
{
PlayerName = UTF8_TO_TCHAR(LuaContext->GetLuaState()->ToString(-1));
}
LuaContext->GetLuaState()->Pop(1); // 弹出 NameValue,栈恢复为 ... -> Table
// 读取整数键 "Level" 对应的值
LuaContext->GetLuaState()->GetField(-1, "Level");
if (LuaContext->GetLuaState()->IsNumber(-1))
{
PlayerLevel = LuaContext->GetLuaState()->ToInteger(-1);
}
LuaContext->GetLuaState()->Pop(1);
// 读取浮点数键 "HP" 对应的值
LuaContext->GetLuaState()->GetField(-1, "HP");
if (LuaContext->GetLuaState()->IsNumber(-1))
{
PlayerHP = static_cast<float>(LuaContext->GetLuaState()->ToNumber(-1));
}
LuaContext->GetLuaState()->Pop(1);
// 4. 读取嵌套 Table "Skills"(数组型 Table)
TArray<FString> PlayerSkills;
LuaContext->GetLuaState()->GetField(-1, "Skills"); // 栈:... -> Table -> SkillsTable
if (LuaContext->GetLuaState()->IsTable(-1))
{
int32 SkillCount = LuaContext->GetLuaState()->ObjLen(-1); // 获取数组长度
for (int32 i = 1; i <= SkillCount; ++i) // Lua 数组索引从 1 开始
{
LuaContext->GetLuaState()->PushInteger(i); // 压入索引
LuaContext->GetLuaState()->GetTable(-2); // 从 SkillsTable 中取对应值
if (LuaContext->GetLuaState()->IsString(-1))
{
PlayerSkills.Add(UTF8_TO_TCHAR(LuaContext->GetLuaState()->ToString(-1)));
}
LuaContext->GetLuaState()->Pop(1); // 弹出技能名
}
}
LuaContext->GetLuaState()->Pop(1); // 弹出 SkillsTable
// 5. 打印读取结果
UE_LOG(LogTemp, Log, TEXT("Player Config:"));
UE_LOG(LogTemp, Log, TEXT("Name: %s"), *PlayerName);
UE_LOG(LogTemp, Log, TEXT("Level: %d"), PlayerLevel);
UE_LOG(LogTemp, Log, TEXT("HP: %.1f"), PlayerHP);
UE_LOG(LogTemp, Log, TEXT("Skills:"));
for (const FString& Skill : PlayerSkills)
{
UE_LOG(LogTemp, Log, TEXT("- %s"), *Skill);
}
// 6. 清理栈(弹出 PlayerConfig Table)
LuaContext->GetLuaState()->Pop(1);
}
常见错误
1.忽略操作规则,导致栈失衡
- 没有及时处理pop里面的零时数据,导致后来栈溢出读取的数据错误。
2.混淆了luatable的键的类型,比如整数键vs字符键,错误的把数组部分以及哈希部分使用同一种方式读取,其实读取数组型table时使用GetFiled而不是GetTable;或者索引从零开始读取到nil.
1.3UE 类与 Lua 的绑定(核心中的核心)
- 学习内容 :UnLua 最核心的能力------将 UE 的蓝图/C++ 类暴露给 Lua,是所有项目开发的基础:
- 绑定方式(两种高频场景) :
- 蓝图类绑定 :在蓝图类的"类设置"→"蓝图细节"→"UnLua"→"Lua 文件名"中输入脚本路径(如
Scripts/MyActor.lua),UE 会自动生成绑定代码。 - C++类绑定 :在 C++ 类头文件中添加
UNLUA_EXPORT宏(如class UNLUA_EXPORT AMyCppActor : public AActor),并在 Lua 中用require "UnLua.AMyCppActor"引入。
- 蓝图类绑定 :在蓝图类的"类设置"→"蓝图细节"→"UnLua"→"Lua 文件名"中输入脚本路径(如
- 成员变量访问 :
- Lua 读取 UE 变量:
local pos = self:GetActorLocation()(self代表当前绑定的 UE 实例)。 - Lua 修改 UE 变量:
self:SetActorLocation(UE.FVector(100, 200, 0))(需调用 UE 提供的 Set 函数,不可直接赋值)。
- Lua 读取 UE 变量:
- 函数调用 :
- Lua 调用 UE 函数:
self:Destroy()(调用 Actor 的 Destroy 函数)。 - UE 调用 Lua 函数:在蓝图中用"Call Lua Function"节点,或在 C++ 中用
CallLuaFunction方法(如LuaContext->CallLuaFunction("OnPlayerClick", Param))。
- Lua 调用 UE 函数:
- 绑定方式(两种高频场景) :
- 实操任务 :
- 新建蓝图类
BP_Player(继承自Character),绑定 Lua 脚本Scripts/Player.lua。 - 在 Lua 中编写
OnBeginPlay函数(UE Actor 生命周期函数,UnLua 自动触发),实现"玩家生成时打印位置"(print("Player Spawned at:", self:GetActorLocation()))。 - 在蓝图中添加一个"按键事件(如 F 键)",调用 Lua 中的
PlayAttackAnim函数(Lua 中定义该函数,调用self:PlayAnimMontage(AttackMontage),需先在蓝图中给AttackMontage变量赋值)。
- 新建蓝图类
1.4UE 生命周期与 Lua 回调
- 学习内容 :UnLua 会自动将 UE 的核心生命周期函数映射到 Lua,是控制逻辑时序的关键(80%的逻辑会依赖这些函数):
- Actor 核心生命周期函数(Lua 中直接定义即可触发) :
OnBeginPlay():Actor 生成时调用(初始化逻辑,如加载资源、设置初始状态)。Tick(deltaTime):每帧调用(实时逻辑,如移动、碰撞检测)。OnEndPlay(reason):Actor 销毁时调用(清理逻辑,如释放资源、保存数据)。
- 回调函数绑定 :处理 UE 事件(如碰撞、按键)的核心方式:
- 蓝图事件绑定:在蓝图中创建"自定义事件",在 Lua 中用
self:BindEvent("BlueprintEventName", LuaFunctionName)绑定。 - 系统事件绑定:如碰撞事件,在 Lua 中用
self:OnActorBeginOverlap.Add(self, "OnOverlap"),并定义OnOverlap函数处理碰撞。
- 蓝图事件绑定:在蓝图中创建"自定义事件",在 Lua 中用
- Actor 核心生命周期函数(Lua 中直接定义即可触发) :
- 实操任务 :
- 基于周三的
BP_Player,在 Lua 中添加Tick函数,实现"玩家每帧向 X 轴正方向移动 1 单位"(local pos = self:GetActorLocation() self:SetActorLocation(UE.FVector(pos.X+1, pos.Y, pos.Z)))。 - 给
BP_Player添加"碰撞体(Capsule Component)",在 Lua 中绑定OnActorBeginOverlap事件,实现"碰到标签为'Coin'的 Actor 时,打印'Got Coin'并销毁 Coin"(function self:OnOverlap(otherActor) if otherActor:GetActorLabel() == "Coin" then print("Got Coin") otherActor:Destroy() end end)。
- 基于周三的
1.5UE 常用组件的 Lua 控制(快速实现可视化效果)
- 学习内容 :UE 组件是实现功能的"积木",掌握 3 个高频组件的 Lua 操作,可覆盖大部分入门项目需求:
- Static Mesh Component(静态网格组件) :控制物体的外观,Lua 中通过
self.StaticMeshComponent访问,常用函数:SetStaticMesh(MeshAsset):设置网格资源(需先在 UE 中加载资源,如local mesh = UE.LoadObject(UE.UStaticMesh, "/Game/Models/Cube") self.StaticMeshComponent:SetStaticMesh(mesh))。SetMaterial(0, MaterialAsset):设置材质。
- Text Render Component(文本渲染组件) :显示 3D 文本,常用函数:
SetText(TextContent):设置文本内容(如self.TextRenderComponent:SetText("HP: " .. self.HP))。SetColor(UE.FColor.Red):设置文本颜色。
- Spring Arm Component(弹簧臂组件) :控制相机跟随,常用函数:
SetRelativeLocation(UE.FVector(0, -500, 200)):设置相机偏移。bUsePawnControlRotation = true:允许鼠标控制相机旋转。
- Static Mesh Component(静态网格组件) :控制物体的外观,Lua 中通过
- 实操任务 :
- 新建蓝图类
BP_Coin(继承自 Actor),添加"Static Mesh Component"(设置为球体网格)和"Text Render Component"(显示"Coin")。 - 在
BP_Coin的 Lua 脚本中,实现OnBeginPlay时"文本颜色每 0.5 秒切换一次红/绿"(用UE.GetWorld():GetTimerManager():SetTimer定时器,local isRed = true function ToggleColor() if isRed then self.TextRenderComponent:SetColor(UE.FColor.Green) else self.TextRenderComponent:SetColor(UE.FColor.Red) end isRed = not isRed end)。
- 新建蓝图类
1.6调试与问题排查(避免卡壳的关键技能)
- 学习内容 :初学者最易因"不会调试"放弃,掌握 UnLua 高频调试方法,能解决 80%的问题:
- 日志打印 :
- Lua 中用
print(...)或UE.Log(...)(打印到 UE 输出日志),UE.Warn(...)(警告日志,标黄),UE.Error(...)(错误日志,标红)。 - 打印复杂类型(如 FVector):
print("Pos:", self:GetActorLocation():ToString())(需调用ToString()转换为字符串)。
- Lua 中用
- 断点调试 :
- 在 UE 编辑器中打开"UnLua 调试器"(菜单栏→Window→UnLua Debugger)。
- 在 Lua 脚本中添加断点(行号前点击,出现红点),运行游戏后触发断点,可查看变量值、单步执行。
- 常见问题排查 :
- 脚本不生效:检查 Lua 文件名路径是否正确(区分大小写)、UE 是否重新编译了绑定代码(修改蓝图后需保存并编译)。
- 函数调用报错:检查是否用
:访问 UE 成员(如self.GetActorLocation()会报错,需改为self:GetActorLocation())、参数类型是否匹配(如 UE 函数需 FVector,不能传数字)。
- 日志打印 :
- 实操任务 :
- 故意在之前的
Player.lua中写一个错误(如self:GetActorLoc(),少写ation),运行游戏,通过 UE 输出日志找到错误信息(定位到错误行号),并修复。 - 在
Coin.lua的ToggleColor函数中添加断点,运行游戏,观察调试器中isRed变量的变化,单步执行验证逻辑。
- 故意在之前的
1.7第一周知识整合(完成一个小型 Demo)
- 核心任务 :整合前 6 天的知识,完成一个"玩家收集硬币"的 Demo,验证核心能力:
- 场景搭建:在 UE 中创建一个平面(作为地面),放置 5 个
BP_Coin。 - 玩家控制:
BP_Player支持 WASD 移动(蓝图中添加"移动组件",Lua 中无需额外处理,或用self:AddMovementInput(UE.FVector(1,0,0))实现)、鼠标控制相机(弹簧臂组件)。 - 收集逻辑:玩家碰到硬币时,硬币销毁,玩家 HP+10,并在屏幕上显示当前 HP(用 Text Render Component 或 UI)。
- 场景搭建:在 UE 中创建一个平面(作为地面),放置 5 个
- 验收标准 :
- 游戏运行后,玩家可移动、控制相机。
- 收集硬币时,能看到日志打印、硬币消失、HP 增加。
- 能通过调试器定位并修复 Demo 中的 1-2 个小问题(如硬币不销毁、HP 不更新)。
2.0UnLua 进阶核心(从"能用"到"好用",支撑项目迭代)
核心目标:掌握资源管理、UI 开发、热更新基础、多脚本协作,具备构建中小型项目的能力。
2.1UnLua 资源加载与管理(避免内存泄漏)
- 学习内容 :UE 资源(网格、材质、动画)是项目核心,UnLua 中加载资源需遵循 UE 内存管理规则,否则易导致崩溃:
- 资源加载方式(两种高频场景) :
- 同步加载 :
UE.LoadObject(资源类型, 资源路径)(如local mesh = UE.LoadObject(UE.UStaticMesh, "/Game/Models/Sphere")),适合启动时加载(如玩家模型),缺点是加载慢会卡顿。 - 异步加载 :
UE.LoadAssetAsync(资源路径, 回调函数)(如UE.LoadAssetAsync("/Game/Animations/Attack", function(asset) self.AttackAnim = asset end)),适合非紧急资源(如道具模型),避免卡顿。
- 同步加载 :
- 资源释放 :UnLua 会自动管理资源引用,但需注意:
- 避免全局变量持有资源(如
GlobalMesh = mesh),否则资源无法释放,导致内存泄漏。 - Actor 销毁时,其引用的资源会自动释放,无需手动处理。
- 避免全局变量持有资源(如
- 资源加载方式(两种高频场景) :
- 实操任务 :
- 给
BP_Player添加"攻击动画",用异步加载方式加载动画资源(路径自行设置),加载完成后打印"Anim Loaded"。 - 在玩家按下 F 键时,播放攻击动画(
self:PlayAnimMontage(self.AttackAnim)),验证异步加载的资源可正常使用。
- 给
2.2UnLua 与 Slate UI 开发(基础 UI 实现)
- 学习内容 :UI 是玩家交互的核心,UnLua 支持 Slate(UE 原生 UI 框架),无需依赖蓝图 UI,适合快速迭代:
- Slate 基础结构 :Lua 中用
UE.SNew(控件类型)创建 UI,如:- 文本控件:
UE.SNew(UE.STextBlock).Text(UE.FText.FromString("HP: 100")).ColorAndOpacity(UE.FLinearColor.Red)。 - 按钮控件:
UE.SNew(UE.SButton).OnClicked(function() print("Button Clicked") end),按钮中可嵌套文本控件。
- 文本控件:
- UI 显示与隐藏 :
- 创建 UI 容器:
local Widget = UE.SNew(UE.SWeakWidget).PossiblyNullContent(YourSlateUI)。 - 添加到视图:
UE.GEngine:AddOnScreenWidget(Widget, 0)(0 是层级,越大越靠上)。 - 隐藏 UI:
UE.GEngine:RemoveOnScreenWidget(Widget)。
- 创建 UI 容器:
- Slate 基础结构 :Lua 中用
- 实操任务 :
- 在
Player.lua中创建一个"HP 显示 UI":顶部居中,红色文本,显示当前 HP(如"HP: 120")。 - 玩家收集硬币时,更新 UI 文本(
self.HP = self.HP + 10 self.HPText:SetText(UE.FText.FromString("HP: " .. self.HP)))。 - 添加一个"暂停按钮"(右下角),点击后隐藏 HP UI,再次点击显示(用
bIsPaused变量控制)。
- 在
你希望我帮你补全这份 UnLua 学习计划,核心是掌握 UnLua 最关键的前20%知识点,先完成基础环境配置和 UE-Lua 通信,再进阶到资源管理、UI、热更新等支撑项目迭代的能力。我会基于你已有的框架,补全未完成的热更新部分,并完善第二周剩余天数的学习内容,确保整个计划聚焦核心、可落地。
2.3UnLua 热更新基础(项目迭代核心需求)
- 学习内容 :热更新是 UnLua 的核心优势之一(无需重新打包游戏,更新 Lua 脚本即可),掌握基础流程:
- 热更新原理 :UnLua 支持"脚本重载"------将 Lua 脚本放在游戏外部目录(如
Game/Content/Scripts/),游戏运行时读取外部脚本,而非打包后的内置脚本。 - 基础热更新流程 :
-
步骤 1:在 UE 中配置"脚本搜索路径",在
DefaultEngine.ini中添加ini[/Script/UnLua.UnLuaSettings] ScriptSearchPaths=/Game/Scripts/;../ExternalScripts/(
../ExternalScripts/是游戏目录外的路径,优先加载外部脚本)。 -
步骤 2:将 Lua 脚本放在
ExternalScripts/目录,游戏运行时会优先加载外部脚本。 -
步骤 3:更新脚本时,替换
ExternalScripts/中的文件,调用UnLua的重载函数(如UE.UnLuaManager:Get().ReloadLuaScripts()),游戏会立即加载新脚本,无需重启。
-
- 热更新注意事项 :
- 避免在全局变量中存储"状态数据"(如玩家 HP),重载脚本会重置全局变量,需将状态存在 UE 的 Actor/蓝图变量中(Lua 仅负责逻辑,数据存在 UE 侧)。
- 热更新仅生效于"后续执行的逻辑",已运行的函数(如 Tick)需重启逻辑(如重新绑定 Tick)才能生效。
- 热更新原理 :UnLua 支持"脚本重载"------将 Lua 脚本放在游戏外部目录(如
- 实操任务 :
- 配置
DefaultEngine.ini添加外部脚本路径,将Player.lua移动到ExternalScripts/目录,验证游戏仍能正常加载脚本。 - 修改
Player.lua中"收集硬币加 HP"的逻辑(如从+10改为+20),在游戏运行中调用UE.UnLuaManager:Get().ReloadLuaScripts(),收集硬币验证 HP 加成更新(无需重启游戏)。 - 修复热更新后"Tick 移动逻辑失效"的问题(在重载脚本后重新绑定 Tick 函数)。
- 配置
2.4UnLua 多脚本协作与模块化(项目可维护性)
- 学习内容 :单脚本无法支撑项目,掌握 Lua 模块化(UnLua 专属规范)是避免代码混乱的核心:
- Lua 模块化基础 :
-
用
module(..., package.seeall)定义模块(UnLua 推荐方式),如PlayerModule.lua中:luamodule(..., package.seeall) -- 定义公共函数 function CalcDamage(attacker, defender) return attacker.Attack - defender.Defense end -
其他脚本用
local PlayerModule = require "Scripts.PlayerModule"引入,调用PlayerModule.CalcDamage(...)。
-
- UnLua 脚本间通信 :
- 方式 1:通过 UE 全局事件(推荐):
UE.UGameplayStatics:CallGlobalFunction("LuaGlobal", "OnCoinCollected", HP),其他脚本绑定该全局事件。 - 方式 2:通过 UE Actor 引用:在 Lua 中用
UE.UGameplayStatics:GetAllActorsOfClass(GetWorld(), UE.BP_Player.Class)获取玩家实例,直接调用其函数。
- 方式 1:通过 UE 全局事件(推荐):
- 避免循环引用 :模块 A 引用 B、B 引用 A 会导致脚本加载失败,解决方式:延迟引用(在函数内
require)或通过 UE 事件通信。
- Lua 模块化基础 :
- 实操任务 :
- 创建
GameLogicModule.lua模块,定义UpdatePlayerHP(player, addHP)函数(负责更新玩家 HP 并打印日志)。 - 修改
Player.lua,移除原有 HP 加成逻辑,改为引入GameLogicModule并调用UpdatePlayerHP(self, 10)。 - 新增
UIModule.lua,绑定 UE 全局事件OnCoinCollected,在事件回调中更新 HP 显示 UI(实现"收集硬币→更新数据→更新UI"的跨脚本协作)。
- 创建
2.5UnLua 与蓝图/C++ 的混合开发(工程最佳实践)
- 学习内容 :UnLua 不是"替代"蓝图/C++,而是"互补",掌握三者协作的核心规则:
- 分工原则(前20%核心) :
- C++:负责高性能逻辑(如物理碰撞、渲染优化)、底层接口封装(如自定义 UnLua 绑定)。
- 蓝图:负责可视化配置(如 UI 布局、动画序列)、简单逻辑封装(如常用事件触发)。
- Lua:负责业务逻辑(如数值计算、交互规则)、热更新内容(如活动玩法)。
- C++ 扩展 UnLua 能力 :
- 自定义 C++ 函数暴露给 Lua:在 C++ 函数前加
UFUNCTION(BlueprintCallable, Category="UnLua"),UnLua 会自动绑定,Lua 可直接调用(如self:MyCustomCppFunc(100))。 - 自定义 UE 结构体暴露给 Lua:用
USTRUCT()定义结构体,加UNLUA_EXPORT宏,Lua 中可直接创建(local data = UE.FMyStruct(10, "test"))。
- 自定义 C++ 函数暴露给 Lua:在 C++ 函数前加
- 蓝图封装 Lua 调用 :
- 将高频 Lua 逻辑封装为蓝图函数(如"CallLua_PlayerAttack"),其他蓝图直接调用,降低跨语言学习成本。
- 分工原则(前20%核心) :
- 实操任务 :
- 新建 C++ 类
AMyCustomActor,添加UFUNCTION(BlueprintCallable)修饰的函数PrintCustomLog(FString Msg)(功能:打印带前缀的日志,如[Custom] xxx),并添加UNLUA_EXPORT宏。 - 在 Lua 中调用
self:PrintCustomLog("Coin Collected"),验证日志正常输出。 - 制作蓝图函数
BP_CallLua_UpdateHP(内部调用"Call Lua Function"节点),在BP_Coin的销毁事件中调用该蓝图函数,实现"蓝图触发→Lua 处理"的协作。
- 新建 C++ 类
2.6UnLua 性能优化(避免项目卡顿)
- 学习内容 :Lua 脚本执行效率低于 C++/蓝图,聚焦 20% 高频优化点,解决 80% 的性能问题:
- 高频优化手段 :
- 减少 Tick 中的耗时操作:避免在 Tick 中调用
GetAllActorsOfClass(遍历所有 Actor 极耗性能),改用"碰撞事件"或"全局变量缓存 Actor 引用"。 - 缓存 UE 对象引用:如
self.CoinClass = UE.LoadClass(UE.AActor, "/Game/Blueprints/BP_Coin")缓存类引用,避免每次调用都加载。 - 避免频繁创建 table:table 是 Lua 中开销较高的类型,复用已有 table(如
local tempVec = UE.FVector(0,0,0),每次修改值而非新建)。
- 减少 Tick 中的耗时操作:避免在 Tick 中调用
- 性能检测工具 :
- UE 内置工具:打开"Session Frontend"→"CPU Profiler",筛选"Lua"相关耗时函数,定位卡顿点。
- UnLua 自带工具:
UE.UnLuaManager:Get():DumpLuaMemory()(打印 Lua 内存占用)、DumpLuaFunctionCallStats()(打印函数调用次数/耗时)。
- 高频优化手段 :
- 实操任务 :
- 优化周四的"玩家移动 Tick 逻辑":缓存
self:GetActorLocation()的结果,避免每帧重复调用(原逻辑每帧调用 2 次,优化后仅 1 次)。 - 用
CPU Profiler对比优化前后的 Lua 耗时,验证 Tick 函数耗时降低。 - 修复"收集硬币时遍历所有 Actor"的低效逻辑:改用碰撞事件直接获取 Coin 引用,而非
GetAllActorsOfClass。
- 优化周四的"玩家移动 Tick 逻辑":缓存
2.7第二周知识整合(完成可热更新的小型项目)
- 核心任务 :整合第二周知识,升级"玩家收集硬币"Demo,新增热更新、模块化、性能优化特性:
- 热更新能力:玩家攻击逻辑(F 键)放在外部脚本,运行中修改"攻击伤害"并重载脚本,验证伤害实时更新。
- 模块化拆分:将"HP 计算""UI 更新""热更新触发"拆分为 3 个独立模块,脚本间通过全局事件通信。
- 性能优化:缓存所有 Coin 的引用,避免 Tick 中重复遍历;优化 UI 更新逻辑,仅在 HP 变化时更新,而非每帧更新。
- 混合开发:用 C++ 实现"硬币生成"函数,Lua 调用该函数动态生成 Coin(按快捷键生成 3 个 Coin)。
- 验收标准 :
- Demo 运行流畅,CPU Profiler 中 Lua 耗时占比 < 5%。
- 热更新攻击逻辑无需重启游戏,生效后攻击伤害正确变化。
- 模块化脚本结构清晰,无循环引用、无全局变量滥用。
- C++ 函数与 Lua 交互正常,动态生成的 Coin 可被正常收集。
3.UnLua 核心进阶(聚焦项目落地痛点)
核心目标:掌握 UnLua 高级特性(协程、调试进阶、打包发布),解决项目落地中的高频问题。
3.1UnLua 协程与异步逻辑(处理耗时操作)
- 学习内容 :Lua 协程是处理异步逻辑(如加载资源、网络请求)的核心,UnLua 适配了 UE 的异步框架:
- 协程基础(UnLua 适配版) :
- 创建协程:
local co = coroutine.create(function() ... end),启动协程:coroutine.resume(co)。 - UnLua 专属封装:
UE.UnLuaCoroutine:Start(self, CoroutineFunc)(自动绑定 Actor 生命周期,Actor 销毁时终止协程)。
- 创建协程:
- 异步逻辑场景 :
- 资源加载等待:在协程中等待异步加载完成(
coroutine.yield()暂停,加载完成后resume恢复)。 - 延迟执行:替代定时器实现"延迟 2 秒执行逻辑"(
UE.UnLuaCoroutine:Delay(self, 2, function() ... end))。
- 资源加载等待:在协程中等待异步加载完成(
- 注意事项 :协程中禁止调用 UE 阻塞函数(如
LoadObject同步加载),避免卡死游戏线程。
- 协程基础(UnLua 适配版) :
- 实操任务 :
- 用协程重构"硬币异步加载"逻辑:在协程中等待动画资源加载完成,加载完成后播放动画,避免回调嵌套。
- 实现"玩家死亡后延迟 3 秒复活"逻辑(用 UnLua 协程 Delay 函数),复活后重置 HP 并生成新硬币。
3.2UnLua 调试进阶与问题定位(解决复杂 Bug)
- 学习内容 :掌握进阶调试技巧,解决跨语言、热更新、协程相关的复杂 Bug:
- 跨语言调试 :
- Lua 调用 C++ 报错:在 C++ 函数中加
UE_LOG打印参数,结合 Lua 日志定位参数不匹配问题。 - C++ 调用 Lua 函数失败:用
UE.UnLuaManager:Get():IsLuaFunctionExist("FuncName")检查函数是否存在,打印参数类型是否匹配。
- Lua 调用 C++ 报错:在 C++ 函数中加
- 热更新调试 :
- 脚本重载失败:查看 UE 日志中
UnLua相关报错(如"脚本语法错误""模块找不到"),用luac -l script.lua检查 Lua 语法。 - 重载后变量丢失:用
UE.UnLuaManager:Get():DumpLuaTable(self)打印 Actor 绑定的 Lua 表,检查变量是否存在。
- 脚本重载失败:查看 UE 日志中
- 协程调试 :
- 协程卡死:用
coroutine.status(co)查看协程状态(suspended/dead),检查yield/resume是否配对。
- 协程卡死:用
- 跨语言调试 :
- 实操任务 :
- 模拟"热更新后 Lua 调用 C++ 函数参数错误"的 Bug,通过日志和参数检查定位并修复。
- 用
DumpLuaTable打印玩家 Actor 的 Lua 表,验证热更新后关键变量(如 HP)未丢失。
3.3UnLua 打包发布与路径配置(项目落地最后一步)
- 学习内容 :UnLua 项目打包需解决路径、脚本加密、依赖问题,是落地的关键:
- 打包前配置 :
- 步骤 1:在
DefaultGame.ini中确认脚本路径:ScriptSearchPaths=/Game/Scripts/(打包后仅保留内置脚本)。 - 步骤 2:禁用 UnLua 调试器:在
DefaultEngine.ini中添加UnLuaSettings.bEnableDebugger=False(避免发布版本暴露调试功能)。 - 步骤 3:脚本加密(可选):用 UnLua 自带的
LuaBytecodeTool将.lua编译为.luac(字节码),防止脚本被反编译。
- 步骤 1:在
- 打包后热更新 :
- 将外部脚本目录(如
ExternalScripts/)放在打包后的游戏目录下(与.exe同级),确保游戏能读取。 - 测试打包后热更新:替换外部脚本,运行游戏验证逻辑更新生效。
- 将外部脚本目录(如
- 常见打包问题 :
- 脚本找不到:检查打包时是否包含
Scripts文件夹(UE 打包默认不包含非蓝图/C++ 文件,需手动添加到"Content Browser"并标记为"Always Cook")。 - 加密脚本运行失败:确保
luac版本与 UnLua 内置 Lua 版本一致(UnLua 基于 Lua 5.1/5.4,需对应版本工具)。
- 脚本找不到:检查打包时是否包含
- 打包前配置 :
- 实操任务 :
- 配置打包参数,将"玩家收集硬币"Demo 打包为 Windows 可执行文件。
- 对
Player.lua进行字节码加密,替换打包后的脚本,验证游戏正常运行。 - 测试打包后热更新:修改外部脚本的 HP 加成逻辑,运行打包后的游戏,验证收集硬币时 HP 加成更新。
3.4UnLua 高频场景实战(覆盖 80% 项目需求)
- 学习内容 :聚焦 3 个高频项目场景,掌握 UnLua 落地解决方案:
- 场景 1:战斗系统基础
- Lua 处理技能逻辑:定义技能配置表(伤害、冷却、特效),用协程实现技能释放(前摇→释放→后摇)。
- 碰撞检测:用 UE 碰撞通道+Lua 回调,实现技能命中判定。
- 场景 2:存档/读档
- 将玩家数据(HP、等级、收集硬币数)存在 UE 的
SaveGame类中,Lua 调用UE.UGameplayStatics:SaveGameToSlot()/LoadGameFromSlot()实现存档。
- 将玩家数据(HP、等级、收集硬币数)存在 UE 的
- 场景 3:网络同步(基础)
- UnLua 适配 UE 网络框架:在 Lua 中标记函数为"服务器/客户端"执行(
self:ServerFunc()仅服务器执行,self:ClientFunc()仅客户端执行)。
- UnLua 适配 UE 网络框架:在 Lua 中标记函数为"服务器/客户端"执行(
- 场景 1:战斗系统基础
- 实操任务 :
- 给 Demo 添加"技能系统":玩家按 R 键释放技能,技能有 3 秒冷却,命中 Coin 后一次性销毁范围内所有 Coin。
- 实现存档/读档:按 S 键存档(保存当前 HP、收集硬币数),按 L 键读档(恢复数据并重新生成对应数量的 Coin)。
3.5全周期知识整合与复盘(形成可复用的 UnLua 开发规范)
- 核心任务 :
- 复盘前两周的学习内容,整理 UnLua 开发规范(如脚本命名、模块化规则、热更新流程)。
- 基于 Demo 输出一份"UnLua 入门项目模板",包含:
- 基础目录结构(Scripts/Module/、Scripts/HotUpdate/)。
- 通用工具函数(资源加载、日志打印、协程封装)。
- 打包配置模板(.ini 文件配置、加密脚本流程)。
- 梳理常见问题清单(如环境配置、调试、性能、打包),形成自查手册。
- 验收标准 :
- 开发规范覆盖"命名、模块化、热更新、性能"四大核心维度,可直接复用在新项目。
- 项目模板能快速搭建 UnLua 基础工程,无需重复配置环境。
- 问题清单能解决 80% 的入门阶段常见问题(如脚本不生效、热更新失败、打包丢失脚本)。
总结
- 核心聚焦:整个计划始终围绕 UnLua "前20%核心知识点",基础阶段掌握 UE-Lua 绑定、生命周期、组件操作,进阶阶段掌握热更新、模块化、性能优化,落地阶段掌握打包、调试、高频场景解决方案。
- 可落地性:每天的学习内容都配套"实操任务",每周有整合性 Demo,从"单点学习"到"项目整合",符合新手从易到难的学习规律。
- 项目导向:所有知识点都服务于"可热更新、高性能、易维护"的 UnLua 项目,避开冷门知识点,聚焦企业开发中真正高频使用的能力。