UE教程|unlua知识地图

unlua

最近新入职了一家公司,我原来是做数字孪生的项目的,在这一家公司里面要求去学习Lua以及Lyra,还是就是深入了解UE以及相关的面试题。所有今天就腾讯的unlua做一个基础的了解,目的是尽可能多的了解就可以了。如果能够搭起来一个大概的框架以及做一个小的demo的话就更好了。

目的

  1. 了解Lua
  2. 测试运行一个demo
  3. 写一个系列的博客
  4. 了解lua网络同步

参考文献

基于 UnLua 的 Lua 编程指南 | 虚幻社区知识库

GitHub - Tencent/UnLua: A feature-rich, easy-learning and highly optimized Lua scripting plugin for UE.

【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 核心定位

  • 学习内容
    1. UE 与 UnLua 版本匹配 :确认你的 UE 版本(推荐 4.26+ 或 5.0+,兼容性更优),从 UnLua 官方仓库 下载对应版本的插件(注意:UE5 需选择支持"Lyra 模板"的分支)。
    2. 插件安装与工程配置
      • 将 UnLua 文件夹复制到 UE 工程的 Plugins 目录下,重启 UE 并启用插件。
      • 开启"蓝图C++混合工程"(若为纯蓝图工程,需先添加 C++ 类触发工程编译),确保 UnLua 自动生成绑定代码。
    3. UnLua 核心定位:理解 UnLua 不是"替代蓝图/C++",而是"补充"------适合快速迭代逻辑(如UI交互、数值配置)、热更新(需额外集成热更框架),核心是"将 UE 的类/函数暴露给 Lua 调用"。
  • 实操任务
    • 新建一个空白 UE 工程(命名为 UnLua_Learn),成功安装 UnLua 插件,在"内容浏览器"中看到 UnLua 自带的示例文件夹(如 UnLua/Examples)。
    • 运行 UnLua 示例中的 BP_ExampleActor,观察 Lua 脚本如何控制 Actor 移动(初步感受"Lua 驱动 UE 物体")。

1.2Lua 基础回顾(针对 UnLua 场景)

  • 学习内容 :无需重学 Lua 全量语法,聚焦 UnLua 中高频使用的 20% Lua 特性:
    1. 数据类型 :重点掌握 table(UnLua 中映射 UE 类/结构体)、function(绑定 UE 函数/回调)、nil(对应 UE 的空引用)。
    2. 核心语法table 的创建与访问(local obj = {x=10, y=20})、函数定义(local func = function(param) ... end)、循环(for i=1,10 do ... end)。
    3. UnLua 专属 Lua 规则 :Lua 中访问 UE 类成员需用 :(如 Actor:GetActorLocation()),而非 .;UE 枚举需用 UE.EnumName.Value(如 UE.ESlateVisibility.Visible)。
  • 实操任务
    • 在 Lua 脚本中定义一个 PlayerData 表(包含 HP=100MP=50Level=1),编写函数 AddLevel 实现升级(Level+1 时 HP+20),并打印结果。
    • 在 UE 中创建一个空 Actor,通过 UnLua 绑定上述 Lua 脚本,在"世界大纲"中选中 Actor,通过"细节面板"查看 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,是所有项目开发的基础:
    1. 绑定方式(两种高频场景)
      • 蓝图类绑定 :在蓝图类的"类设置"→"蓝图细节"→"UnLua"→"Lua 文件名"中输入脚本路径(如 Scripts/MyActor.lua),UE 会自动生成绑定代码。
      • C++类绑定 :在 C++ 类头文件中添加 UNLUA_EXPORT 宏(如 class UNLUA_EXPORT AMyCppActor : public AActor),并在 Lua 中用 require "UnLua.AMyCppActor" 引入。
    2. 成员变量访问
      • Lua 读取 UE 变量:local pos = self:GetActorLocation()self 代表当前绑定的 UE 实例)。
      • Lua 修改 UE 变量:self:SetActorLocation(UE.FVector(100, 200, 0))(需调用 UE 提供的 Set 函数,不可直接赋值)。
    3. 函数调用
      • Lua 调用 UE 函数:self:Destroy()(调用 Actor 的 Destroy 函数)。
      • UE 调用 Lua 函数:在蓝图中用"Call Lua Function"节点,或在 C++ 中用 CallLuaFunction 方法(如 LuaContext->CallLuaFunction("OnPlayerClick", Param))。
  • 实操任务
    • 新建蓝图类 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%的逻辑会依赖这些函数):
    1. Actor 核心生命周期函数(Lua 中直接定义即可触发)
      • OnBeginPlay():Actor 生成时调用(初始化逻辑,如加载资源、设置初始状态)。
      • Tick(deltaTime):每帧调用(实时逻辑,如移动、碰撞检测)。
      • OnEndPlay(reason):Actor 销毁时调用(清理逻辑,如释放资源、保存数据)。
    2. 回调函数绑定 :处理 UE 事件(如碰撞、按键)的核心方式:
      • 蓝图事件绑定:在蓝图中创建"自定义事件",在 Lua 中用 self:BindEvent("BlueprintEventName", LuaFunctionName) 绑定。
      • 系统事件绑定:如碰撞事件,在 Lua 中用 self:OnActorBeginOverlap.Add(self, "OnOverlap"),并定义 OnOverlap 函数处理碰撞。
  • 实操任务
    • 基于周三的 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 操作,可覆盖大部分入门项目需求:
    1. 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):设置材质。
    2. Text Render Component(文本渲染组件) :显示 3D 文本,常用函数:
      • SetText(TextContent):设置文本内容(如 self.TextRenderComponent:SetText("HP: " .. self.HP))。
      • SetColor(UE.FColor.Red):设置文本颜色。
    3. Spring Arm Component(弹簧臂组件) :控制相机跟随,常用函数:
      • SetRelativeLocation(UE.FVector(0, -500, 200)):设置相机偏移。
      • bUsePawnControlRotation = true:允许鼠标控制相机旋转。
  • 实操任务
    • 新建蓝图类 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%的问题:
    1. 日志打印
      • Lua 中用 print(...)UE.Log(...)(打印到 UE 输出日志),UE.Warn(...)(警告日志,标黄),UE.Error(...)(错误日志,标红)。
      • 打印复杂类型(如 FVector):print("Pos:", self:GetActorLocation():ToString())(需调用 ToString() 转换为字符串)。
    2. 断点调试
      • 在 UE 编辑器中打开"UnLua 调试器"(菜单栏→Window→UnLua Debugger)。
      • 在 Lua 脚本中添加断点(行号前点击,出现红点),运行游戏后触发断点,可查看变量值、单步执行。
    3. 常见问题排查
      • 脚本不生效:检查 Lua 文件名路径是否正确(区分大小写)、UE 是否重新编译了绑定代码(修改蓝图后需保存并编译)。
      • 函数调用报错:检查是否用 : 访问 UE 成员(如 self.GetActorLocation() 会报错,需改为 self:GetActorLocation())、参数类型是否匹配(如 UE 函数需 FVector,不能传数字)。
  • 实操任务
    • 故意在之前的 Player.lua 中写一个错误(如 self:GetActorLoc(),少写 ation),运行游戏,通过 UE 输出日志找到错误信息(定位到错误行号),并修复。
    • Coin.luaToggleColor 函数中添加断点,运行游戏,观察调试器中 isRed 变量的变化,单步执行验证逻辑。

1.7第一周知识整合(完成一个小型 Demo)

  • 核心任务 :整合前 6 天的知识,完成一个"玩家收集硬币"的 Demo,验证核心能力:
    1. 场景搭建:在 UE 中创建一个平面(作为地面),放置 5 个 BP_Coin
    2. 玩家控制:BP_Player 支持 WASD 移动(蓝图中添加"移动组件",Lua 中无需额外处理,或用 self:AddMovementInput(UE.FVector(1,0,0)) 实现)、鼠标控制相机(弹簧臂组件)。
    3. 收集逻辑:玩家碰到硬币时,硬币销毁,玩家 HP+10,并在屏幕上显示当前 HP(用 Text Render Component 或 UI)。
  • 验收标准
    • 游戏运行后,玩家可移动、控制相机。
    • 收集硬币时,能看到日志打印、硬币消失、HP 增加。
    • 能通过调试器定位并修复 Demo 中的 1-2 个小问题(如硬币不销毁、HP 不更新)。

2.0UnLua 进阶核心(从"能用"到"好用",支撑项目迭代)

核心目标:掌握资源管理、UI 开发、热更新基础、多脚本协作,具备构建中小型项目的能力。

2.1UnLua 资源加载与管理(避免内存泄漏)

  • 学习内容 :UE 资源(网格、材质、动画)是项目核心,UnLua 中加载资源需遵循 UE 内存管理规则,否则易导致崩溃:
    1. 资源加载方式(两种高频场景)
      • 同步加载UE.LoadObject(资源类型, 资源路径)(如 local mesh = UE.LoadObject(UE.UStaticMesh, "/Game/Models/Sphere")),适合启动时加载(如玩家模型),缺点是加载慢会卡顿。
      • 异步加载UE.LoadAssetAsync(资源路径, 回调函数)(如 UE.LoadAssetAsync("/Game/Animations/Attack", function(asset) self.AttackAnim = asset end)),适合非紧急资源(如道具模型),避免卡顿。
    2. 资源释放 :UnLua 会自动管理资源引用,但需注意:
      • 避免全局变量持有资源(如 GlobalMesh = mesh),否则资源无法释放,导致内存泄漏。
      • Actor 销毁时,其引用的资源会自动释放,无需手动处理。
  • 实操任务
    • BP_Player 添加"攻击动画",用异步加载方式加载动画资源(路径自行设置),加载完成后打印"Anim Loaded"。
    • 在玩家按下 F 键时,播放攻击动画(self:PlayAnimMontage(self.AttackAnim)),验证异步加载的资源可正常使用。

2.2UnLua 与 Slate UI 开发(基础 UI 实现)

  • 学习内容 :UI 是玩家交互的核心,UnLua 支持 Slate(UE 原生 UI 框架),无需依赖蓝图 UI,适合快速迭代:
    1. 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),按钮中可嵌套文本控件。
    2. UI 显示与隐藏
      • 创建 UI 容器:local Widget = UE.SNew(UE.SWeakWidget).PossiblyNullContent(YourSlateUI)
      • 添加到视图:UE.GEngine:AddOnScreenWidget(Widget, 0)(0 是层级,越大越靠上)。
      • 隐藏 UI:UE.GEngine:RemoveOnScreenWidget(Widget)
  • 实操任务
    • 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 脚本即可),掌握基础流程:
    1. 热更新原理 :UnLua 支持"脚本重载"------将 Lua 脚本放在游戏外部目录(如 Game/Content/Scripts/),游戏运行时读取外部脚本,而非打包后的内置脚本。
    2. 基础热更新流程
      • 步骤 1:在 UE 中配置"脚本搜索路径",在 DefaultEngine.ini 中添加

        ini 复制代码
        [/Script/UnLua.UnLuaSettings] 
        ScriptSearchPaths=/Game/Scripts/;../ExternalScripts/

        ../ExternalScripts/ 是游戏目录外的路径,优先加载外部脚本)。

      • 步骤 2:将 Lua 脚本放在 ExternalScripts/ 目录,游戏运行时会优先加载外部脚本。

      • 步骤 3:更新脚本时,替换 ExternalScripts/ 中的文件,调用 UnLua 的重载函数(如 UE.UnLuaManager:Get().ReloadLuaScripts()),游戏会立即加载新脚本,无需重启。

    3. 热更新注意事项
      • 避免在全局变量中存储"状态数据"(如玩家 HP),重载脚本会重置全局变量,需将状态存在 UE 的 Actor/蓝图变量中(Lua 仅负责逻辑,数据存在 UE 侧)。
      • 热更新仅生效于"后续执行的逻辑",已运行的函数(如 Tick)需重启逻辑(如重新绑定 Tick)才能生效。
  • 实操任务
    • 配置 DefaultEngine.ini 添加外部脚本路径,将 Player.lua 移动到 ExternalScripts/ 目录,验证游戏仍能正常加载脚本。
    • 修改 Player.lua 中"收集硬币加 HP"的逻辑(如从+10改为+20),在游戏运行中调用 UE.UnLuaManager:Get().ReloadLuaScripts(),收集硬币验证 HP 加成更新(无需重启游戏)。
    • 修复热更新后"Tick 移动逻辑失效"的问题(在重载脚本后重新绑定 Tick 函数)。

2.4UnLua 多脚本协作与模块化(项目可维护性)

  • 学习内容 :单脚本无法支撑项目,掌握 Lua 模块化(UnLua 专属规范)是避免代码混乱的核心:
    1. Lua 模块化基础
      • module(..., package.seeall) 定义模块(UnLua 推荐方式),如 PlayerModule.lua 中:

        lua 复制代码
        module(..., package.seeall)
        -- 定义公共函数
        function CalcDamage(attacker, defender)
            return attacker.Attack - defender.Defense
        end
      • 其他脚本用 local PlayerModule = require "Scripts.PlayerModule" 引入,调用 PlayerModule.CalcDamage(...)

    2. UnLua 脚本间通信
      • 方式 1:通过 UE 全局事件(推荐):UE.UGameplayStatics:CallGlobalFunction("LuaGlobal", "OnCoinCollected", HP),其他脚本绑定该全局事件。
      • 方式 2:通过 UE Actor 引用:在 Lua 中用 UE.UGameplayStatics:GetAllActorsOfClass(GetWorld(), UE.BP_Player.Class) 获取玩家实例,直接调用其函数。
    3. 避免循环引用 :模块 A 引用 B、B 引用 A 会导致脚本加载失败,解决方式:延迟引用(在函数内 require)或通过 UE 事件通信。
  • 实操任务
    • 创建 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++,而是"互补",掌握三者协作的核心规则:
    1. 分工原则(前20%核心)
      • C++:负责高性能逻辑(如物理碰撞、渲染优化)、底层接口封装(如自定义 UnLua 绑定)。
      • 蓝图:负责可视化配置(如 UI 布局、动画序列)、简单逻辑封装(如常用事件触发)。
      • Lua:负责业务逻辑(如数值计算、交互规则)、热更新内容(如活动玩法)。
    2. 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"))。
    3. 蓝图封装 Lua 调用
      • 将高频 Lua 逻辑封装为蓝图函数(如"CallLua_PlayerAttack"),其他蓝图直接调用,降低跨语言学习成本。
  • 实操任务
    • 新建 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 处理"的协作。

2.6UnLua 性能优化(避免项目卡顿)

  • 学习内容 :Lua 脚本执行效率低于 C++/蓝图,聚焦 20% 高频优化点,解决 80% 的性能问题:
    1. 高频优化手段
      • 减少 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),每次修改值而非新建)。
    2. 性能检测工具
      • 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

2.7第二周知识整合(完成可热更新的小型项目)

  • 核心任务 :整合第二周知识,升级"玩家收集硬币"Demo,新增热更新、模块化、性能优化特性:
    1. 热更新能力:玩家攻击逻辑(F 键)放在外部脚本,运行中修改"攻击伤害"并重载脚本,验证伤害实时更新。
    2. 模块化拆分:将"HP 计算""UI 更新""热更新触发"拆分为 3 个独立模块,脚本间通过全局事件通信。
    3. 性能优化:缓存所有 Coin 的引用,避免 Tick 中重复遍历;优化 UI 更新逻辑,仅在 HP 变化时更新,而非每帧更新。
    4. 混合开发:用 C++ 实现"硬币生成"函数,Lua 调用该函数动态生成 Coin(按快捷键生成 3 个 Coin)。
  • 验收标准
    • Demo 运行流畅,CPU Profiler 中 Lua 耗时占比 < 5%。
    • 热更新攻击逻辑无需重启游戏,生效后攻击伤害正确变化。
    • 模块化脚本结构清晰,无循环引用、无全局变量滥用。
    • C++ 函数与 Lua 交互正常,动态生成的 Coin 可被正常收集。

3.UnLua 核心进阶(聚焦项目落地痛点)

核心目标:掌握 UnLua 高级特性(协程、调试进阶、打包发布),解决项目落地中的高频问题。

3.1UnLua 协程与异步逻辑(处理耗时操作)

  • 学习内容 :Lua 协程是处理异步逻辑(如加载资源、网络请求)的核心,UnLua 适配了 UE 的异步框架:
    1. 协程基础(UnLua 适配版)
      • 创建协程:local co = coroutine.create(function() ... end),启动协程:coroutine.resume(co)
      • UnLua 专属封装:UE.UnLuaCoroutine:Start(self, CoroutineFunc)(自动绑定 Actor 生命周期,Actor 销毁时终止协程)。
    2. 异步逻辑场景
      • 资源加载等待:在协程中等待异步加载完成(coroutine.yield() 暂停,加载完成后 resume 恢复)。
      • 延迟执行:替代定时器实现"延迟 2 秒执行逻辑"(UE.UnLuaCoroutine:Delay(self, 2, function() ... end))。
    3. 注意事项 :协程中禁止调用 UE 阻塞函数(如 LoadObject 同步加载),避免卡死游戏线程。
  • 实操任务
    • 用协程重构"硬币异步加载"逻辑:在协程中等待动画资源加载完成,加载完成后播放动画,避免回调嵌套。
    • 实现"玩家死亡后延迟 3 秒复活"逻辑(用 UnLua 协程 Delay 函数),复活后重置 HP 并生成新硬币。

3.2UnLua 调试进阶与问题定位(解决复杂 Bug)

  • 学习内容 :掌握进阶调试技巧,解决跨语言、热更新、协程相关的复杂 Bug:
    1. 跨语言调试
      • Lua 调用 C++ 报错:在 C++ 函数中加 UE_LOG 打印参数,结合 Lua 日志定位参数不匹配问题。
      • C++ 调用 Lua 函数失败:用 UE.UnLuaManager:Get():IsLuaFunctionExist("FuncName") 检查函数是否存在,打印参数类型是否匹配。
    2. 热更新调试
      • 脚本重载失败:查看 UE 日志中 UnLua 相关报错(如"脚本语法错误""模块找不到"),用 luac -l script.lua 检查 Lua 语法。
      • 重载后变量丢失:用 UE.UnLuaManager:Get():DumpLuaTable(self) 打印 Actor 绑定的 Lua 表,检查变量是否存在。
    3. 协程调试
      • 协程卡死:用 coroutine.status(co) 查看协程状态(suspended/dead),检查 yield/resume 是否配对。
  • 实操任务
    • 模拟"热更新后 Lua 调用 C++ 函数参数错误"的 Bug,通过日志和参数检查定位并修复。
    • DumpLuaTable 打印玩家 Actor 的 Lua 表,验证热更新后关键变量(如 HP)未丢失。

3.3UnLua 打包发布与路径配置(项目落地最后一步)

  • 学习内容 :UnLua 项目打包需解决路径、脚本加密、依赖问题,是落地的关键:
    1. 打包前配置
      • 步骤 1:在 DefaultGame.ini 中确认脚本路径:ScriptSearchPaths=/Game/Scripts/(打包后仅保留内置脚本)。
      • 步骤 2:禁用 UnLua 调试器:在 DefaultEngine.ini 中添加 UnLuaSettings.bEnableDebugger=False(避免发布版本暴露调试功能)。
      • 步骤 3:脚本加密(可选):用 UnLua 自带的 LuaBytecodeTool.lua 编译为 .luac(字节码),防止脚本被反编译。
    2. 打包后热更新
      • 将外部脚本目录(如 ExternalScripts/)放在打包后的游戏目录下(与 .exe 同级),确保游戏能读取。
      • 测试打包后热更新:替换外部脚本,运行游戏验证逻辑更新生效。
    3. 常见打包问题
      • 脚本找不到:检查打包时是否包含 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. 场景 1:战斗系统基础
      • Lua 处理技能逻辑:定义技能配置表(伤害、冷却、特效),用协程实现技能释放(前摇→释放→后摇)。
      • 碰撞检测:用 UE 碰撞通道+Lua 回调,实现技能命中判定。
    2. 场景 2:存档/读档
      • 将玩家数据(HP、等级、收集硬币数)存在 UE 的 SaveGame 类中,Lua 调用 UE.UGameplayStatics:SaveGameToSlot()/LoadGameFromSlot() 实现存档。
    3. 场景 3:网络同步(基础)
      • UnLua 适配 UE 网络框架:在 Lua 中标记函数为"服务器/客户端"执行(self:ServerFunc() 仅服务器执行,self:ClientFunc() 仅客户端执行)。
  • 实操任务
    • 给 Demo 添加"技能系统":玩家按 R 键释放技能,技能有 3 秒冷却,命中 Coin 后一次性销毁范围内所有 Coin。
    • 实现存档/读档:按 S 键存档(保存当前 HP、收集硬币数),按 L 键读档(恢复数据并重新生成对应数量的 Coin)。

3.5全周期知识整合与复盘(形成可复用的 UnLua 开发规范)

  • 核心任务
    1. 复盘前两周的学习内容,整理 UnLua 开发规范(如脚本命名、模块化规则、热更新流程)。
    2. 基于 Demo 输出一份"UnLua 入门项目模板",包含:
      • 基础目录结构(Scripts/Module/、Scripts/HotUpdate/)。
      • 通用工具函数(资源加载、日志打印、协程封装)。
      • 打包配置模板(.ini 文件配置、加密脚本流程)。
    3. 梳理常见问题清单(如环境配置、调试、性能、打包),形成自查手册。
  • 验收标准
    • 开发规范覆盖"命名、模块化、热更新、性能"四大核心维度,可直接复用在新项目。
    • 项目模板能快速搭建 UnLua 基础工程,无需重复配置环境。
    • 问题清单能解决 80% 的入门阶段常见问题(如脚本不生效、热更新失败、打包丢失脚本)。

总结

  1. 核心聚焦:整个计划始终围绕 UnLua "前20%核心知识点",基础阶段掌握 UE-Lua 绑定、生命周期、组件操作,进阶阶段掌握热更新、模块化、性能优化,落地阶段掌握打包、调试、高频场景解决方案。
  2. 可落地性:每天的学习内容都配套"实操任务",每周有整合性 Demo,从"单点学习"到"项目整合",符合新手从易到难的学习规律。
  3. 项目导向:所有知识点都服务于"可热更新、高性能、易维护"的 UnLua 项目,避开冷门知识点,聚焦企业开发中真正高频使用的能力。
相关推荐
jumu2023 小时前
无人船、AUV与无人车编队路径跟踪的奇妙探索
lua5.4
皇族崛起18 小时前
【3D标注】- Unreal Engine 5.7 与 Python 交互基础
python·3d·ue5
一眼万里*e1 天前
UE中的UObject创建,销毁
ue5
吴梓穆4 天前
UE5 Perforce使用完全手册
ue5
zhangzhangkeji5 天前
UE5 蓝图-游老师-13-事件、函数、宏、事件分发器:在自定义蓝图(包括 UI 控件蓝图)中就可以创建事件分发器
ue5
Zhichao_975 天前
【UE5.3】小白人动画重定向
ue5
Zhichao_975 天前
【UE5.3】为人形角色建立Contrl Rig
ue5
竹欣5 天前
UE杂项(Mass 崩溃排查)
ue5
__Ryan5 天前
BlueprintImplementableEvent和BlueprintNativeEvent
c++·ue5·unreal engine