c++调用lua的方法

UE C++ 调用 Lua 的方法详解(基于 UnLua)


一、前置知识:C++ 为什么能调用 Lua?

回顾一下 UnLua 的核心架构:

复制代码
┌──────────────┐                    ┌──────────────┐
│   C++ 代码    │                    │   Lua 脚本    │
│              │                    │              │
│  调用 UFunction ──→ UnLua 中转 ──→  执行 Lua 函数  │
│              │                    │              │
│  ← 返回值 ────── Lua 栈传回 ←──── push 返回值    │
└──────────────┘                    └──────────────┘

核心原理:UnLua 在绑定时,将 UFunction 的执行指针替换为自己的中转函数。当 C++ 调用该 UFunction 时,中转函数会将参数通过 Lua 栈 传给 Lua 虚拟机,执行 Lua 函数,再把返回值通过栈传回 C++。

C++ 侧完全不知道 Lua 的存在 ------它只是调用了一个 UFunction,至于这个函数最终由蓝图执行还是 Lua 执行,C++ 不关心。


二、方式一:BlueprintImplementableEvent(最推荐)

2.1 原理

C++ 声明一个"蓝图可实现事件",不提供 C++ 实现。Lua 覆写这个函数后,C++ 调用时自动走到 Lua。

2.2 代码示例

C++ 声明(.h 文件):

cpp 复制代码
UCLASS()
class AMyCharacter : public ACharacter
{
    GENERATED_BODY()

public:
    // 声明一个蓝图可实现事件
    // C++ 不写实现体,由 Lua(或蓝图)来实现
    UFUNCTION(BlueprintImplementableEvent, Category = "Skill")
    void OnSkillActivated(int32 SkillId, float CooldownTime);
};

C++ 调用(.cpp 文件):

cpp 复制代码
void AMyCharacter::UseSkill(int32 SkillId)
{
    // 像调用普通函数一样调用,不需要任何 Lua 相关代码
    OnSkillActivated(SkillId, 3.0f);
}

Lua 覆写

lua 复制代码
-- Characters/MyCharacter.lua
local MyCharacter = UnLua.Class()

function MyCharacter:OnSkillActivated(SkillId, CooldownTime)
    print(string.format("技能 %d 激活,冷却 %.1f 秒", SkillId, CooldownTime))
    -- 在这里写 Lua 侧的逻辑
    self:StartCooldownTimer(CooldownTime)
end

return MyCharacter

2.3 调用链详解

复制代码
C++ 调用 OnSkillActivated(1001, 3.0)
       │
       ▼
UE 引擎执行 UFunction("OnSkillActivated")
       │
       ▼
UFunction 的执行指针已被 UnLua 替换 → 进入 UnLua 中转函数
       │
       ├── 1. 通过 UObject 映射找到对应的 Lua Table
       ├── 2. 在 Table 中查找 "OnSkillActivated" 函数
       ├── 3. 将 C++ 参数转换并 push 到 Lua 栈:
       │       push self (userdata)
       │       push 1001 (integer)
       │       push 3.0  (number)
       ├── 4. lua_pcall 调用 Lua 函数
       └── 5. Lua 执行完毕,从栈上取返回值(如果有)传回 C++

2.4 带返回值的情况

cpp 复制代码
// C++ 声明
UFUNCTION(BlueprintImplementableEvent)
float CalculateDamageMultiplier(int32 SkillLevel);

// C++ 使用返回值
float Multiplier = CalculateDamageMultiplier(5);
float FinalDamage = BaseDamage * Multiplier;
lua 复制代码
function MyCharacter:CalculateDamageMultiplier(SkillLevel)
    return 1.0 + SkillLevel * 0.2  -- 等级5 → 2.0倍
end

2.5 适用场景

  • 游戏逻辑事件:技能激活、角色死亡、任务完成等
  • UI 更新通知:血量变化、经验值变化等
  • 流程控制:关卡开始、关卡结束、存档等

三、方式二:BlueprintNativeEvent(有默认实现)

3.1 与方式一的区别

BlueprintImplementableEvent BlueprintNativeEvent
C++ 默认实现 _Implementation 后缀)
Lua 未覆写时 什么都不做 执行 C++ 默认实现
Lua 覆写后 执行 Lua 执行 Lua(可选调用 C++ 默认实现)
适用场景 纯 Lua 实现的逻辑 有合理默认行为,Lua 可选择定制

3.2 代码示例

C++ 声明 + 默认实现

cpp 复制代码
// .h 文件
UFUNCTION(BlueprintNativeEvent, Category = "Combat")
float CalculateDamage(float BaseDamage, int32 ArmorLevel);

// .cpp 文件 ------ 注意函数名加 _Implementation 后缀
float AMyCharacter::CalculateDamage_Implementation(float BaseDamage, int32 ArmorLevel)
{
    // C++ 默认实现:简单的护甲减伤
    float ArmorReduction = ArmorLevel * 5.0f;
    return FMath::Max(BaseDamage - ArmorReduction, 0.0f);
}

C++ 调用

cpp 复制代码
void AMyCharacter::ApplyDamage(float BaseDamage)
{
    // 直接调用,不带 _Implementation 后缀
    float FinalDamage = CalculateDamage(BaseDamage, CurrentArmorLevel);
    Health -= FinalDamage;
}

Lua 覆写(可选)

lua 复制代码
function MyCharacter:CalculateDamage(BaseDamage, ArmorLevel)
    -- 自定义伤害计算:百分比减伤
    local Reduction = 1.0 - (ArmorLevel * 0.05)
    return BaseDamage * math.max(Reduction, 0.1)
end

3.3 在 Lua 中调用 C++ 默认实现

如果 Lua 想在自定义逻辑的基础上,也执行 C++ 的默认实现:

lua 复制代码
function MyCharacter:CalculateDamage(BaseDamage, ArmorLevel)
    -- 先执行 C++ 默认实现
    local DefaultDamage = self.Overridden.CalculateDamage(self, BaseDamage, ArmorLevel)
    
    -- 在默认结果上做额外处理
    if self:HasBuff("IronSkin") then
        return DefaultDamage * 0.5  -- 铁皮 buff 再减半
    end
    return DefaultDamage
end

self.Overridden.XXX 是 UnLua 提供的语法,用于调用被覆写前的原始 C++ 实现。


四、方式三:直接操作 Lua C API(底层方式)

4.1 什么时候需要?

当你需要:

  • 调用 Lua 全局函数(不属于任何 UObject)
  • 调用指定 Lua 模块中的函数
  • 在非 UObject 上下文中与 Lua 交互

4.2 代码示例

C++ 侧

cpp 复制代码
#include "lua.hpp"
#include "UnLuaBase.h"

void AMyManager::CallLuaGlobalFunction()
{
    // ========== 1. 获取 Lua 虚拟机 ==========
    lua_State* L = UnLua::GetState();
    if (!L) return;

    // ========== 2. 将要调用的函数压栈 ==========
    // lua_getglobal 会在 Lua 全局表中查找 "OnGameEvent" 函数
    // 找到后将其压入栈顶
    lua_getglobal(L, "OnGameEvent");

    // 检查栈顶是否是函数
    if (!lua_isfunction(L, -1))
    {
        lua_pop(L, 1);  // 不是函数,弹出并返回
        return;
    }

    // ========== 3. 压入参数 ==========
    // 参数按顺序压栈,先压的是第一个参数
    lua_pushstring(L, "PlayerDied");   // 参数1: 事件名(string)
    lua_pushinteger(L, 1001);          // 参数2: 玩家ID(integer)
    lua_pushnumber(L, 3.14);           // 参数3: 某个数值(number)
    lua_pushboolean(L, true);          // 参数4: 是否重生(boolean)

    // ========== 4. 调用函数 ==========
    // lua_pcall(L, 参数个数, 返回值个数, 错误处理函数索引)
    // 错误处理函数索引传 0 表示使用默认错误处理
    int Result = lua_pcall(L, 4, 1, 0);

    if (Result != LUA_OK)
    {
        // 调用失败,栈顶是错误信息
        const char* ErrorMsg = lua_tostring(L, -1);
        UE_LOG(LogTemp, Error, TEXT("Lua call failed: %s"), UTF8_TO_TCHAR(ErrorMsg));
        lua_pop(L, 1);  // 弹出错误信息
        return;
    }

    // ========== 5. 获取返回值 ==========
    // 调用成功后,返回值在栈顶
    if (lua_isinteger(L, -1))
    {
        int ReturnValue = lua_tointeger(L, -1);
        UE_LOG(LogTemp, Log, TEXT("Lua returned: %d"), ReturnValue);
    }
    lua_pop(L, 1);  // 弹出返回值,清理栈
}

Lua 侧

lua 复制代码
-- 全局函数
function OnGameEvent(EventName, PlayerId, Value, bRespawn)
    print(string.format("事件: %s, 玩家: %d, 值: %.2f, 重生: %s",
        EventName, PlayerId, Value, tostring(bRespawn)))
    return 42  -- 返回值
end

4.3 Lua 栈操作图解

复制代码
操作过程中 Lua 栈的变化:

初始状态:          getglobal 后:      push 参数后:         pcall 后:
┌──────┐          ┌──────────────┐   ┌──────────────┐   ┌──────────┐
│ 空    │          │ OnGameEvent  │   │ true         │   │ 42       │
│      │          │ (function)   │   │ 3.14         │   │ (返回值)  │
│      │          │              │   │ 1001         │   └──────────┘
│      │          │              │   │ "PlayerDied" │
└──────┘          └──────────────┘   │ OnGameEvent  │
                   栈底 ──→ 栈顶     └──────────────┘
                                      栈底 ──→ 栈顶

                                     lua_pcall 会消耗
                                     函数和所有参数,
                                     把返回值压到栈顶

4.4 调用 Lua 模块中的函数

如果目标函数不是全局函数,而是在某个模块的 table 中:

cpp 复制代码
void CallModuleFunction(lua_State* L)
{
    // 获取模块 table
    lua_getglobal(L, "require");
    lua_pushstring(L, "GameLogic.EventSystem");
    lua_pcall(L, 1, 1, 0);  // require("GameLogic.EventSystem") → 栈顶是模块 table

    // 从 table 中获取函数
    lua_getfield(L, -1, "HandleEvent");  // 栈顶是 HandleEvent 函数

    // 压入参数并调用
    lua_pushstring(L, "OnDamage");
    lua_pcall(L, 1, 0, 0);

    lua_pop(L, 1);  // 弹出模块 table
}

五、方式四:UnLua 辅助 API

UnLua 在底层 Lua C API 之上封装了一些便捷函数,减少手动操作 Lua 栈的工作:

5.1 调用绑定对象的 Lua 方法

cpp 复制代码
#include "UnLuaBase.h"

void AMyActor::NotifyLuaSide()
{
    lua_State* L = UnLua::GetState();
    if (!L) return;

    // 检查对象是否已绑定 Lua
    if (UnLua::IsUObjectBound(this))
    {
        // 调用绑定的 Lua 实例上的方法
        // 相当于 Lua 中的 self:OnCppNotify(100, "hello")
        UnLua::Call(L, this, "OnCppNotify", 100, "hello");
    }
}

5.2 调用 Lua table 的函数

这种是全局变量

cpp 复制代码
// 调用指定模块中的函数
UnLua::CallTableFunc(L, "Utils.MathHelper", "Clamp", Value, MinVal, MaxVal);

六、各方式对比总结

复制代码
推荐程度(高→低):

  BlueprintImplementableEvent  ★★★★★  零耦合,最规范
  BlueprintNativeEvent         ★★★★★  有默认实现,灵活
  UnLua 辅助 API               ★★★☆☆  特殊场景,中等耦合
  Lua C API (lua_pcall)        ★★☆☆☆  底层操作,高耦合
维度 BlueprintImplementableEvent BlueprintNativeEvent UnLua API Lua C API
耦合度 零(不知道 Lua 存在) 中(依赖 UnLua) 高(直接操作栈)
类型安全 有(UE 反射) 有(UE 反射)
有返回值
需要 UObject
可热更新
错误处理 UE 自动处理 UE 自动处理 UnLua 处理 手动处理
适用场景 游戏逻辑接口 有默认行为的接口 特殊调用需求 全局函数/工具调用

七、最佳实践

7.1 设计原则

复制代码
C++ 负责:                          Lua 负责:
├── 引擎底层、性能敏感逻辑            ├── 游戏玩法逻辑
├── 定义接口(UFUNCTION 声明)        ├── 实现接口(覆写 BlueprintEvent)
├── 基础框架和系统                    ├── 运营活动、可热更内容
└── 调用接口(不关心谁实现)          └── UI 流程、技能配置

7.2 常见模式

模式一:事件通知(无返回值)

cpp 复制代码
// C++ 在适当时机通知 Lua
UFUNCTION(BlueprintImplementableEvent)
void OnLevelLoaded(const FString& LevelName);

// C++ 内部
void AMyGameMode::HandleLevelLoaded()
{
    // ... C++ 逻辑 ...
    OnLevelLoaded(CurrentLevelName);  // 通知 Lua
}

模式二:策略委托(有返回值)

cpp 复制代码
// C++ 向 Lua 请求决策
UFUNCTION(BlueprintNativeEvent)
bool ShouldAttackTarget(AActor* Target);

// C++ 默认实现
bool AMyAI::ShouldAttackTarget_Implementation(AActor* Target)
{
    return true;  // 默认:见谁打谁
}

// C++ 使用
if (ShouldAttackTarget(Enemy))
{
    StartAttack(Enemy);
}
lua 复制代码
-- Lua 可以实现更复杂的判断
function MyAI:ShouldAttackTarget(Target)
    if Target:HasBuff("Invisible") then
        return false  -- 隐身目标不打
    end
    if self:GetHP() < 100 then
        return false  -- 血量低不打
    end
    return true
end

模式三:数据获取

cpp 复制代码
UFUNCTION(BlueprintImplementableEvent)
TArray<FString> GetAvailableSkills();

// C++ 调用
TArray<FString> Skills = GetAvailableSkills();
for (const FString& Skill : Skills)
{
    // 处理每个技能
}

7.3 避免的做法

cpp 复制代码
// ❌ 不要在 C++ 中直接 #include Lua 头文件来调用 Lua(除非必要)
#include "lua.hpp"
lua_getglobal(L, "SomeFunction");
lua_pcall(L, 0, 0, 0);

// ✅ 应该通过 UE 反射系统间接调用
UFUNCTION(BlueprintImplementableEvent)
void SomeFunction();
cpp 复制代码
// ❌ 不要在 Tick 中频繁调用 Lua(性能问题)
void Tick(float DeltaTime)
{
    OnLuaTick(DeltaTime);  // 每帧调 Lua,开销大
}

// ✅ 用事件驱动代替每帧轮询
void OnHealthChanged(float NewHealth)
{
    OnHealthUpdate(NewHealth);  // 只在变化时调用
}

八、总结

复制代码
C++ 调用 Lua = C++ 调用 UFunction → UnLua 拦截 → 转发到 Lua 虚拟机

核心选择:
  ┌─ 需要 Lua 提供完整实现 ──→ BlueprintImplementableEvent
  ├─ 需要 C++ 有默认行为   ──→ BlueprintNativeEvent  
  ├─ 需要调用特定对象方法  ──→ UnLua::Call()
  └─ 需要调用全局 Lua 函数 ──→ lua_pcall()(尽量避免)

记住一个原则:让 C++ 不知道 Lua 的存在。通过 UE 反射系统做桥梁,C++ 定义接口、调用接口,Lua 负责实现接口。这样既解耦又支持热更新。

相关推荐
Lhan.zzZ9 小时前
笔记_2026.4.28_004
c++·ide·笔记·qt
wuminyu11 小时前
专家视角看Java字节码加载与存储指令机制
java·linux·c语言·jvm·c++
木喃的井盖11 小时前
无锁队列细节
c++·工程
王老师青少年编程12 小时前
csp信奥赛C++高频考点专项训练之字符串 --【字符串基础】:输出亲朋字符串
c++·字符串·csp·高频考点·信奥赛·专项训练·输出亲朋字符串
WBluuue12 小时前
数据结构与算法:莫队(一):普通莫队与带修莫队
c++·算法
KuaCpp13 小时前
C++面向对象(速过复习版)
开发语言·c++
智者知已应修善业16 小时前
【51单片机不用数组动态数码管显示字符和LED流水灯】2023-10-3
c++·经验分享·笔记·算法·51单片机
晴夏。16 小时前
UE Spawn出来的Actor的生命周期和管理方法
游戏·ue5·ue4·ue
AI进化营-智能译站16 小时前
ROS2 C++开发系列16-智能指针管理传感器句柄|告别ROS2节点内存泄漏与野指针
java·c++·算法·ai