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 文件UCLASS()classAMyCharacter:publicACharacter{GENERATED_BODY()public:// 声明一个蓝图可实现事件// C 不写实现体由 Lua或蓝图来实现UFUNCTION(BlueprintImplementableEvent,CategorySkill)voidOnSkillActivated(int32 SkillId,floatCooldownTime);};C 调用.cpp 文件voidAMyCharacter::UseSkill(int32 SkillId){// 像调用普通函数一样调用不需要任何 Lua 相关代码OnSkillActivated(SkillId,3.0f);}Lua 覆写-- Characters/MyCharacter.lualocalMyCharacterUnLua.Class()functionMyCharacter:OnSkillActivated(SkillId,CooldownTime)print(string.format(技能 %d 激活冷却 %.1f 秒,SkillId,CooldownTime))-- 在这里写 Lua 侧的逻辑self:StartCooldownTimer(CooldownTime)endreturnMyCharacter2.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 执行完毕从栈上取返回值如果有传回 C2.4 带返回值的情况// C 声明UFUNCTION(BlueprintImplementableEvent)floatCalculateDamageMultiplier(int32 SkillLevel);// C 使用返回值floatMultiplierCalculateDamageMultiplier(5);floatFinalDamageBaseDamage*Multiplier;functionMyCharacter:CalculateDamageMultiplier(SkillLevel)return1.0SkillLevel*0.2-- 等级5 → 2.0倍end2.5 适用场景游戏逻辑事件技能激活、角色死亡、任务完成等UI 更新通知血量变化、经验值变化等流程控制关卡开始、关卡结束、存档等三、方式二BlueprintNativeEvent有默认实现3.1 与方式一的区别BlueprintImplementableEventBlueprintNativeEventC 默认实现无有_Implementation后缀Lua 未覆写时什么都不做执行 C 默认实现Lua 覆写后执行 Lua执行 Lua可选调用 C 默认实现适用场景纯 Lua 实现的逻辑有合理默认行为Lua 可选择定制3.2 代码示例C 声明 默认实现// .h 文件UFUNCTION(BlueprintNativeEvent,CategoryCombat)floatCalculateDamage(floatBaseDamage,int32 ArmorLevel);// .cpp 文件 —— 注意函数名加 _Implementation 后缀floatAMyCharacter::CalculateDamage_Implementation(floatBaseDamage,int32 ArmorLevel){// C 默认实现简单的护甲减伤floatArmorReductionArmorLevel*5.0f;returnFMath::Max(BaseDamage-ArmorReduction,0.0f);}C 调用voidAMyCharacter::ApplyDamage(floatBaseDamage){// 直接调用不带 _Implementation 后缀floatFinalDamageCalculateDamage(BaseDamage,CurrentArmorLevel);Health-FinalDamage;}Lua 覆写可选functionMyCharacter:CalculateDamage(BaseDamage,ArmorLevel)-- 自定义伤害计算百分比减伤localReduction1.0-(ArmorLevel*0.05)returnBaseDamage*math.max(Reduction,0.1)end3.3 在 Lua 中调用 C 默认实现如果 Lua 想在自定义逻辑的基础上也执行 C 的默认实现functionMyCharacter:CalculateDamage(BaseDamage,ArmorLevel)-- 先执行 C 默认实现localDefaultDamageself.Overridden.CalculateDamage(self,BaseDamage,ArmorLevel)-- 在默认结果上做额外处理ifself:HasBuff(IronSkin)thenreturnDefaultDamage*0.5-- 铁皮 buff 再减半endreturnDefaultDamageendself.Overridden.XXX是 UnLua 提供的语法用于调用被覆写前的原始 C 实现。四、方式三直接操作 Lua C API底层方式4.1 什么时候需要当你需要调用 Lua 全局函数不属于任何 UObject调用指定 Lua 模块中的函数在非 UObject 上下文中与 Lua 交互4.2 代码示例C 侧#includelua.hpp#includeUnLuaBase.hvoidAMyManager::CallLuaGlobalFunction(){// 1. 获取 Lua 虚拟机 lua_State*LUnLua::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: 事件名stringlua_pushinteger(L,1001);// 参数2: 玩家IDintegerlua_pushnumber(L,3.14);// 参数3: 某个数值numberlua_pushboolean(L,true);// 参数4: 是否重生boolean// 4. 调用函数 // lua_pcall(L, 参数个数, 返回值个数, 错误处理函数索引)// 错误处理函数索引传 0 表示使用默认错误处理intResultlua_pcall(L,4,1,0);if(Result!LUA_OK){// 调用失败栈顶是错误信息constchar*ErrorMsglua_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)){intReturnValuelua_tointeger(L,-1);UE_LOG(LogTemp,Log,TEXT(Lua returned: %d),ReturnValue);}lua_pop(L,1);// 弹出返回值清理栈}Lua 侧-- 全局函数functionOnGameEvent(EventName,PlayerId,Value,bRespawn)print(string.format(事件: %s, 玩家: %d, 值: %.2f, 重生: %s,EventName,PlayerId,Value,tostring(bRespawn)))return42-- 返回值end4.3 Lua 栈操作图解操作过程中 Lua 栈的变化 初始状态: getglobal 后: push 参数后: pcall 后: ┌──────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐ │ 空 │ │ OnGameEvent │ │ true │ │ 42 │ │ │ │ (function) │ │ 3.14 │ │ (返回值) │ │ │ │ │ │ 1001 │ └──────────┘ │ │ │ │ │ PlayerDied │ └──────┘ └──────────────┘ │ OnGameEvent │ 栈底 ──→ 栈顶 └──────────────┘ 栈底 ──→ 栈顶 lua_pcall 会消耗 函数和所有参数 把返回值压到栈顶4.4 调用 Lua 模块中的函数如果目标函数不是全局函数而是在某个模块的 table 中voidCallModuleFunction(lua_State*L){// 获取模块 tablelua_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 辅助 APIUnLua 在底层 Lua C API 之上封装了一些便捷函数减少手动操作 Lua 栈的工作5.1 调用绑定对象的 Lua 方法#includeUnLuaBase.hvoidAMyActor::NotifyLuaSide(){lua_State*LUnLua::GetState();if(!L)return;// 检查对象是否已绑定 Luaif(UnLua::IsUObjectBound(this)){// 调用绑定的 Lua 实例上的方法// 相当于 Lua 中的 self:OnCppNotify(100, hello)UnLua::Call(L,this,OnCppNotify,100,hello);}}5.2 调用 Lua table 的函数这种是全局变量// 调用指定模块中的函数UnLua::CallTableFunc(L,Utils.MathHelper,Clamp,Value,MinVal,MaxVal);六、各方式对比总结推荐程度高→低 BlueprintImplementableEvent ★★★★★ 零耦合最规范 BlueprintNativeEvent ★★★★★ 有默认实现灵活 UnLua 辅助 API ★★★☆☆ 特殊场景中等耦合 Lua C API (lua_pcall) ★★☆☆☆ 底层操作高耦合维度BlueprintImplementableEventBlueprintNativeEventUnLua APILua C API耦合度零不知道 Lua 存在零中依赖 UnLua高直接操作栈类型安全有UE 反射有UE 反射弱无有返回值✅✅✅✅需要 UObject✅✅✅❌可热更新✅✅✅✅错误处理UE 自动处理UE 自动处理UnLua 处理手动处理适用场景游戏逻辑接口有默认行为的接口特殊调用需求全局函数/工具调用七、最佳实践7.1 设计原则C 负责 Lua 负责 ├── 引擎底层、性能敏感逻辑 ├── 游戏玩法逻辑 ├── 定义接口UFUNCTION 声明 ├── 实现接口覆写 BlueprintEvent ├── 基础框架和系统 ├── 运营活动、可热更内容 └── 调用接口不关心谁实现 └── UI 流程、技能配置7.2 常见模式模式一事件通知无返回值// C 在适当时机通知 LuaUFUNCTION(BlueprintImplementableEvent)voidOnLevelLoaded(constFStringLevelName);// C 内部voidAMyGameMode::HandleLevelLoaded(){// ... C 逻辑 ...OnLevelLoaded(CurrentLevelName);// 通知 Lua}模式二策略委托有返回值// C 向 Lua 请求决策UFUNCTION(BlueprintNativeEvent)boolShouldAttackTarget(AActor*Target);// C 默认实现boolAMyAI::ShouldAttackTarget_Implementation(AActor*Target){returntrue;// 默认见谁打谁}// C 使用if(ShouldAttackTarget(Enemy)){StartAttack(Enemy);}-- Lua 可以实现更复杂的判断functionMyAI:ShouldAttackTarget(Target)ifTarget:HasBuff(Invisible)thenreturnfalse-- 隐身目标不打endifself:GetHP()100thenreturnfalse-- 血量低不打endreturntrueend模式三数据获取UFUNCTION(BlueprintImplementableEvent)TArrayFStringGetAvailableSkills();// C 调用TArrayFStringSkillsGetAvailableSkills();for(constFStringSkill:Skills){// 处理每个技能}7.3 避免的做法// ❌ 不要在 C 中直接 #include Lua 头文件来调用 Lua除非必要#includelua.hpplua_getglobal(L,SomeFunction);lua_pcall(L,0,0,0);// ✅ 应该通过 UE 反射系统间接调用UFUNCTION(BlueprintImplementableEvent)voidSomeFunction();// ❌ 不要在 Tick 中频繁调用 Lua性能问题voidTick(floatDeltaTime){OnLuaTick(DeltaTime);// 每帧调 Lua开销大}// ✅ 用事件驱动代替每帧轮询voidOnHealthChanged(floatNewHealth){OnHealthUpdate(NewHealth);// 只在变化时调用}八、总结C 调用 Lua C 调用 UFunction → UnLua 拦截 → 转发到 Lua 虚拟机 核心选择 ┌─ 需要 Lua 提供完整实现 ──→ BlueprintImplementableEvent ├─ 需要 C 有默认行为 ──→ BlueprintNativeEvent ├─ 需要调用特定对象方法 ──→ UnLua::Call() └─ 需要调用全局 Lua 函数 ──→ lua_pcall()尽量避免记住一个原则让 C 不知道 Lua 的存在。通过 UE 反射系统做桥梁C 定义接口、调用接口Lua 负责实现接口。这样既解耦又支持热更新。