面经-ue知识

本文最后更新于:2026年4月27日 凌晨

智能指针

UE 里常见的「类智能指针」各干什么?

类型 作用(白话) 典型用途
TObjectPtr<UXXX> 参与 UPROPERTY 的强引用,配合增量 GC、写屏障 类成员里引用另一个 UObject(替代裸 UObject*,新项目/UE5 规范向)
TWeakObjectPtr<UXXX> 弱引用:不延长 UObject 生命;对象没了会变成无效 缓存「可能已销毁」的对象、解耦循环引用、回调里判断 IsValid
TSoftObjectPtr<T> / FSoftObjectPath 软引用:默认不强制加载;需要时再异步加载 资源按需加载、减小内存与启动成本
TStrongObjectPtr<T>(非 UObject 成员时常用) 在非 UObject 或原生代码里强引用住一个 UObject 临时持有、非反射路径的强引用(需包含头文件并遵守用法)

TWeakObjectPtr

缓存、回调里「顺便记一下」、打断循环引用(A 强引用 B,B 用弱引用指回 A)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
TWeakObjectPtr<AActor> WeakActor;

void Foo(AActor* Actor)
{
WeakActor = Actor; // 只缓存,不保证 Actor 永远活着
}

void Bar()
{
if (AActor* A = WeakActor.Get()) // 或 WeakActor.IsValid()
{
// 还活着再用
}
}

TObject和SoftObject的区别?垃圾回收有什么区别这两个

硬引用(Hard Reference)

定义:

  • 直接指针引用,如 UObject、UClass

  • 资源在包含它的对象加载时会被自动加载到内存

1
2
3
4
5
6
// 硬引用示例
UPROPERTY(EditAnywhere)
UAnimMontage* HardReferenceMontage; // 直接指针

UPROPERTY(EditAnywhere)
UClass* HardReferenceClass; // 直接类指针

优点:

  • 使用简单,直接访问

  • 自动管理,无需手动加载

  • 类型安全,编译时检查

缺点:

  • 内存占用:资源会立即加载

  • 加载时间:形成依赖链,导致大量资源一起加载

  • 循环依赖风险:可能导致资源无法卸载

使用场景:

  • 核心资源,必须立即可用

  • 小资源,内存占用可接受

  • 频繁使用的资源

软引用

定义:

  • 只存储资源路径字符串,不立即加载资源

  • 使用 TSoftObjectPtr、TSoftClassPtr 或 FSoftObjectPath

1
2
3
4
5
6
7
8
9
10
11
// 软引用示例(来自你的代码库)
UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UAnimMontage> DefaultExpression; // 软引用动画蒙太奇

UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<UStaticMesh> ExpressionPlaneStaticMeshSoftPtr; // 软引用静态网格

// 使用时需要手动加载
UAnimMontage* Montage = DefaultExpression.LoadSynchronous();
// 或异步加载
DefaultExpression.LoadAsync();

优点:

  • 延迟加载:只在需要时加载

  • 减少内存占用:不立即加载到内存

  • 加快启动速度:避免加载不必要的资源

  • 更好的资源管理:可以控制加载时机

缺点:

  • 需要手动加载:代码更复杂

  • 可能为空:需要检查资源是否加载成功

  • 异步加载:需要处理回调

使用场景:

  • 可选资源:可能不使用的资源

  • 大型资源:纹理、模型等

  • 按需加载:根据游戏状态动态加载

  • 减少启动时间:延迟加载非关键资源

GC区别

  • 硬引用:阻止 GC,对象会一直保留在内存中

  • 软引用:不阻止 GC,对象可以被回收,需要时重新加载

  • 弱引用:不阻止 GC,用于避免循环引用

对象销毁后行为
FSoftObjectPtr 在一定时间内仍然可以访问已销毁的对象,直到垃圾回收期间。FWeakObjectPtr 会自动失效(置为空指针),不会访问到已销毁的对象。

UMG和SlateUI的区别?

UMG是蓝图,可以绑脚本

Slateui更底层,就是编辑器那些的ui

手柄

手柄的组合键是什么做的?

动态修改InputSetting里的ActionMapping按下技能组合键时,把原先 ”X键映射Attack” 删掉,新增一条 ”X键映射Skill1”松开技能组合键时,把 ”X键映射Skill1” 删掉,重新加入 ”X键映射Attack”

这样做有个问题,有可能出现时序问题

时序

解决办法:加一道工序,如果正在按下,就松开的时候再替换,绑一个回调

image-20260123143905157

思路就是修饰键

  • 组合键通常需要一个“修饰键”(如 GamepadUseSkill)作为触发条件

  • 按下修饰键时,临时改变其他按键的行为

  • 释放修饰键时,恢复原始映射

这样可以实现:

  • 单独按 y:执行原始功能

  • 按住 x + 按 y:执行组合功能(如技能1)

  • 按住 x + 按其他键:执行对应的组合功能

这是典型的手柄组合键实现方式,类似键盘的 Ctrl/Cmd + 其他键的组合。

enhance输入什么的

增强输入:

https://dev.epicgames.com/documentation/zh-cn/unreal-engine/enhanced-input-in-unreal-engine

IMC Mapping和Action的区别是什么

这是 UE 增强输入(Enhanced Input) 里两个不同层的东西。

Input Action(动作)

  • 含义:逻辑上的「玩家意图」,例如:跳跃、开火、打开背包、确认。

  • 特点

    • 和具体按键无关,只定义「游戏里要响应什么」。
    • 可以带数值类型:布尔、一维轴、二维轴(例如移动向量)。
  • 作用:给玩法 / UI 代码一个稳定接口(监听某个 Action 触发),底层是键盘还是手柄由别处决定。

Input Mapping Context(映射上下文)

  • 含义:一张(或多张)把物理输入映射到 Action 的表,例如:WASD → IA_Move,空格 → IA_Jump

  • 特点

    • 可以有多份 IMC(战斗、载具、菜单),用优先级叠在一起,实现「菜单打开时覆盖移动」等。
    • 通常按模式整体启用/禁用(切场景、打开界面时换一套映射)。
  • 作用:在不改 Action 含义的前提下,换键位、换手柄布局、做多语言/多平台差异。

一句话对比

Action IMC
回答的问题 「游戏里要识别哪种行为?」 「用哪些键/摇杆去触发这些行为?」
类比 API 名 / 事件名 快捷键配置表

关系:IMC 里引用并绑定 Action;Action 是抽象,IMC 是「硬件 → Action」的配置层。我们项目里一般是:玩法只绑 Action,策划/程序在 IMC 里配键位和上下文切换,这样换键、加手柄映射不用改 C++/Lua 里的逻辑名。

按键响应的顺序是什么?

输入过程

AActor生命周期

浅析 UE Actor 生命周期管理

  1. PostLoad/PostActorCreated - 执行构建 Actor 所需的任何设置。PostLoad 用于序列化 Actor,PostActorCreated 用于生成 Actor。
  2. AActor::OnConstruction - Actor 的构造函数,蓝图 Actor 在此函数中创建其组件并初始化蓝图变量。
  3. AActor::PreInitializeComponents - 在对 Actor 的组件调用 InitializeComponent 之前调用
  4. UActorComponent::InitializeComponent - actor 的 components 数组中的每个组件都会被调用初始化函数(如果该组件的 bWantsInitializeComponent 为 true)。
  5. AActor::PostInitializeComponents - 在 Actor 的组件初始化完成后调用
  6. Actor::BeginPlay - 关卡开始时调用

UE官方文档Actor生命周期

UE 动画系统框架介绍及使用 - TurBo强的文章 - 知乎

IK

Two Bone IK - silvergp的文章 - 知乎

其实很简单,就是想象最后是一个三角形,所以可以这样就知道了三个边的长度(因为ac=at),然后就可以求出角的大小,角的大小就是bat-bac,然后就可以旋转过去,这里用个旋转矩阵就可以了,然后就又可以求出其他的了

img

动画系统

UE 动画系统框架源码解析 - TurBo强的文章 - 知乎

这个文章还介绍了许多大佬的文章

Motion Mathcing

Motion Matching 中的代码驱动移动和动画驱动移动 - 小木子的文章 - 知乎

GC

UE4 垃圾回收 - 南京周润发的文章 - 知乎

增量回收

增量可达性分析

UE使用增量可达性分析对其进行了改进。 现在用户能够将垃圾回收器的可达性分析阶段分散到多帧,并可配置每帧的软时间限制。 引擎通过TObjectPtr属性跟踪可达性迭代之间的UObject引用。 这意味着对暴露了TObjectPtr的UPROPERTY进行任何赋值,都会在垃圾回收进行时立即将对象标记为可达。 这也被称为垃圾回收器写屏障。

引擎已转换为在将UObject暴露给垃圾回收器的所有地方使用TObjectPtr而不是原始C++指针,包括所有UObject或FGCObject AddReferencedObjects函数。 要在使用虚幻引擎编译的项目中使用增量可达性分析,必须将所有UPROPERTY实例转换为使用TObjectPtr而不是原始C++,否则垃圾回收可能过早回收一些UObject的内存。 我们目前初步将此功能发布为试验性的功能,因为可达性分析阶段仍有可能超出指定的时间限制。

传统做法:GC 的可达性分析(Mark)在一帧里尽量做完,引用图一大就容易卡顿。

增量可达性分析:把 Mark 拆到多帧里做,每帧只干一小段时间(软时间上限,可配)。这样单帧压力小,但会带来新问题:

  • 第 1 帧扫到一半时,第 2 帧里你又给某个 UPROPERTY 赋了新 UObject;
  • 若 GC 看不到这次赋值,可能误以为新指向的对象不可达 → 误删(过早回收)。

所以引擎要一种机制:赋值一旦发生,就立刻让 GC「知道」这个对象至少暂时是可达的。这就是文档里的 写屏障(Write Barrier) 和 TObjectPtr 的配合。

GC优化方法?

这篇有优化相关的介绍

UE4 垃圾收集大杂烩 - Jiff的文章 - 知乎

通常GC会引起问题都是在LevelStreaming的时候,不过如果你在代码中申请了大规模的UObject,在不再使用的时候也不删除。或者频繁而无意义的调用ForceGC,自然也可能会造成性能问题。

为了避免LevelStreaming造成的GC瞬间成本,可以通过缩小细分关卡的规模来进行。

这样的话在进行角色的移动时,就可以形成小规模的载入和移除,而不是一次性的大规模变更。

设置这个可以减少遍历成本

image-20260204160723179

Cluster

UE机关同步,上下线怎么同步?

比如一个门打开了, 再上线怎么直接是打开?

常见实现方式(任选或组合)

方式 适用
动画跳到最后一帧 序列帧 / 关卡 Sequence:SetPosition(1.f)JumpToEnd()、或 AnimMontage SetPosition(Length)
直接改 Transform 门就是旋转/位移:算出「开」的 RelativeRotation/LocationSet 过去,不播 Timeline。
两个状态机节点 动画蓝图里「Opened」是独立 Pose,直接 SetBool bOpened 为 true,不经过过渡。
Static Mesh 换姿态 简单门:关/开两套 Mesh 或一个 Mesh 直接设最终矩阵。

RepNotify / OnRep 里怎么写(避免再播一次

1
2
3
4
5
若 NewOpenState == true:
若 本地之前是关的 且 是“远程同步来的”(非本机刚点的):
ApplyOpenVisualInstant() // 直接终态
否则若 是本机交互触发的开门:
PlayOpenAnim() // 可播动画

lua

怎么做一个eventmanager?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
local M = {}

M.EventID = {
OnTest = "OnTest",
}

local EventDic = {}

local function getOrCreateEvent(eventName)
local ev = EventDic[eventName]
if not ev then
ev = {subscribers = {}}
EventDic[eventName] = ev
end
return ev
end

function M.AddEvent(eventName, obj, func)
local ev = getOrCreateEvent(eventName)
local list = ev.subscribers[obj]
if not list then
list = {}
ev.subscribers[obj] = list
end
list[#list + 1] = func
end

function M.RemoveEvent(eventName, obj)
local ev = getOrCreateEvent(eventName)
local list = ev.subscribers[obj]
if not list then
return
end
if not ev then
return
end
list[obj] = nil
end

function M.FireEvent(eventName, ...)
local ev = getOrCreateEvent(eventName)
if not ev then
return
end
local args = {...}
for object, funcs in pairs(ev.subscribers) do
for i = 1, #funcs do
funcs[i](object, table.unpack(args, 1, #args))
end
end
end

lua元方法

定义(面试口径)

在 Lua 里,元表(metatable) 是一张普通表,用来描述「另一张表在特定操作下该怎么表现」。
元表里以双下划线开头的特殊键,叫做 元方法(metamethod)。当 Lua 对某张表做某种运算或内建操作时,如果发现该表有对应元方法,就会改走元方法,而不是默认行为。

可以一句话记:元方法 = 通过元表给表“重载”语言内置行为的一组钩子。

常见元方法(按用途记)

  • __index:用 t.kt[k] 取值时,t 上没有这个键 → 去 __index 里找(可以是表或函数)。面向对象、继承、读默认值几乎都靠它。
  • __newindex:给 t.k 赋值且 t 上原本没有该键时触发;常用来做只读代理、属性校验、懒创建子表。
  • __callt(...) 把表当函数调。
  • 算术:__add __sub __mul __div __mod __pow __unm 等。
  • 比较:__eq __lt __le(注意 ~=``、>>=` 由引擎用这些组合出来,有细节)。
  • __tostringtostring(t)
  • __pairs / __ipairs(Lua 5.2+ 起 ipairs 行为有变化,具体看版本):自定义遍历。
  • __gc:userdata 回收时(Lua 5.2+ 对 table 的 __gc 支持因版本而异,面试提 userdata 更稳)。

和「用过」怎么对应到项目

在你们这种 EM + UnLua 项目里:

  1. 日常业务 Lua
    大量是 Class({ ... })、组件、self:XXX,底层往往是 用元表 + __index(以及可能的 __newindex) 做类、继承、Super 调用。你不一定每张表手写 setmetatable,但语义上就是在用元方法。
  2. 自己显式写元表
    相对少,但会出现,例如:
    • 做一层 配置 / 只读代理;
    • 缓存 + 懒加载的访问器;
    • 极少数 运算符重载(比如向量式数据在纯 Lua 里模拟)。
  3. 和 UE 的关系
    UObject 从 Lua 访问时,字段、函数很多也是通过绑定层和元表机制接到 C++ 上的;面试可以说:脚本侧的“点出来一个 UFunction”背后,和元方法式的分发是同一类思路(具体实现是引擎/插件细节)。

1. __index 是「表」和是「函数」时分别发生什么?

共同点:访问 t.k 时,若 t 自身没有键 k,Lua 会去看 t 的元表里有没有 __index

1
__index` 是表 `M
  • 语义:t.k 等价于「若 t 没有 k,就去 M 里取 M.k」。
  • 典型用途:原型 / 单继承:实例表很薄,方法都放在 M 上,所有实例共享一份方法表,省内存。
  • 链式查找:若 M 里也没有 k,不会自动再对 M__index(那是另一张表的元表的事);要多层继承需要自己设计(例如 M 的元表再指向父类方法表)。
1
__index` 是函数 `f(t, k)
  • 每次「实例上没有 k」都会调用 f,你可以在函数里写任意逻辑:多级查找、getter、缓存、按 k 拼路径懒加载等。
  • 代价:每次未命中都要进一次 Lua 函数,比表查找慢。

项目语境(UnLua)
类体系里常见的是「方法表 + __index 指向方法表或带查找逻辑的函数」; UObject 上点成员时,绑定层也可能在「查不到」时走 C++ 侧解析,思路和「函数式 __index」类似:灵活但比纯表字段贵。


2. 性能直觉(面试够用)

  • 热路径(每帧、列表滚动里大量访问):尽量让数据直接挂在对象上,或让 __index 指向一张固定的父表,避免函数式 __index 里再做大字符串拼接或表创建。
  • 函数式 __index:适合不频繁的键、或需要动态规则的地方;可配合结果写回实例(见下)摊销成本。
  • __newindex:每次给「原本没有的键」赋值都可能触发,滥用会做大量额外调用;属性系统要小心。

3. rawget(t, k) / rawset(t, k, v) 和元表的关系

  • rawget:只读 t 自身的槽位,完全忽略 __index。用来打破递归(例如在 __index 函数里查实例字段)、或避免触发 getter。
  • rawset:只写 t 自身,忽略 __newindex。用来避免赋值被拦截、或同样在元方法里避免死循环。

常见坑:在 __newindex 里对同一 t 做普通赋值想「绕过」,若逻辑不对仍会再进 __newindex;这时往往要用 rawset(t, k, v) 真正把值落到实例上。

一个小模式(缓存 + 函数 __index
第一次用 __index 算出一个值后 rawset(t, k, value),以后访问就走实例自身,等价于把动态查找变成一次性成本(要注意是否允许后续失效,若配置会变要配套清理)。

深拷贝怎么写?

1
2
3
4
5
6
7
8
9
10
11
12
13
function CommonUtils.DeepCopy(Object)
local function _copy(object)
if type(object) ~= "table" then
return object
end
local new_table = {}
for key, value in pairs(object) do
new_table[_copy(key)] = _copy(value)
end
return setmetatable(new_table, getmetatable(object))
end
return _copy(Object)
end

visited版本,防止循环引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function DeepCopy(Object)
local visited = {}
local function _copy(obj)
if type(obj) ~= "table" then
return obj
end
if visited[obj] then
return visited[obj]
end
local new_table = {}
visited[obj] = new_table -- 必须先登记,再填子字段,才能断环

for key, value in pairs(obj) do
new_table[_copy(key)] = _copy(value)
end

return setmetatable(new_table, getmetatable(obj))
end
return _copy(Object)
end

UProperty

用过。在我们 EM 工程 C++ 里很常见,和 编辑器细节面板、蓝图、反射、GC、网络复制 都绑在一起;脚本侧 UnLua 能直接 self.SomeProp,背后也依赖这些成员被反射暴露出来(该用 UPROPERTY / UFUNCTION 的地方要对上)。


定义(面试怎么说)

UPROPERTY 是 UnrealHeaderTool(UHT) 识别的宏,给 UObject 派生类里的 C++ 成员变量打上一组 元数据,让引擎做几件事:

  1. 注册到反射系统:生成 FProperty / FField,名字、类型、容器、嵌套结构都能被 序列化、编辑器、蓝图、详情面板 查询。
  2. 垃圾回收(GC):对 UObject*TSubclassOfFSoftObjectPath 等 引用关系 参与 GC 扫描;没标 UPROPERTY 的裸 UObject* 可能被误回收(经典坑)。
  3. 可选能力:复制(Replicated)、保存到关卡/配置(SaveGame)、编辑器可编辑(EditAnywhere)、蓝图读写(BlueprintReadWrite)等,都由 Specifiers 组合决定。

没有 UPROPERTY 的普通 C++ 变量,对 UObject 体系来说通常是 “引擎看不见的字段”。


常见 Specifiers(按场景记)

方向 例子 作用
编辑器 EditAnywhere / VisibleAnywhere 是否在细节面板可改 / 只读
蓝图 BlueprintReadOnly / BlueprintReadWrite 蓝图图里能否读/写
生命周期 Transient 默认不序列化到磁盘(运行时缓存常用)
网络 Replicated + DOREPLIFETIME 属性复制
容器 TArray / TMap + UPROPERTY 同样参与反射与 GC

具体组合以你们引擎版本文档为准,面试里强调 “Specifiers 决定可见性与是否进存档/复制” 即可。


和项目相关的例子

例如 Source/EMUIManagerComponent.h 里就有典型写法:VisibleAnywhere 给编辑器只读展示、EditAnywhere, BlueprintReadWrite, Category="EM" 给可调参数、Transient 标运行时临时引用等——这就是日常 UI / 管理器组件 里 UPROPERTY + Category 组织细节面板 的用法。


易错点(加分项)

  • UObject* 成员:该标就标,避免 GC 回收仍在用的对象。
  • TObjectPtr / 裸指针:UE5 里推荐用引擎指导的类型;面试可提 追踪生命周期、和 GC 的配合。
  • UFUNCTION 分工:UPROPERTY 管 状态字段;行为用 UFUNCTION 暴露给蓝图/Lua。
  • Lua:属性要在反射里可见,Lua 才能稳定访问;纯 C++ 未反射成员脚本侧通常碰不到。

一句话收尾:UPROPERTY = 把 C++ 成员挂进 UObject 反射与 GC 体系,并用 Specifiers 控制编辑器、蓝图、序列化、复制等行为;我们项目在 UI 组件、Manager 等头文件里一直在用。

lua和C++互相调用

下面只谈 原生 Lua 官方 C API 和 C/C++ 互调,不涉及 UnLua。


1. 核心模型:虚拟机 + 栈

嵌入 Lua 时,C++ 侧先 luaL_newstate() 得到 lua_State*
Lua 和 C 之间传参、返回值,几乎都走 虚拟栈(stack):

  • C 压入参数 → 调用 Lua 函数 → 从栈上 取出返回值。
  • Lua 调 C 时,运行时会把参数压在栈上,C 函数用 索引(正数从底往上,负数从顶往下)读写栈。

所以「互相调用」= 一方往栈上摆数据,另一方按约定读写栈。


2. C++ 调 Lua

典型步骤:

  1. lua_getglobal(L, "foo"):把全局函数 foo 压栈。
  2. 依次 lua_push*lua_pushnumberlua_pushstringlua_pushboolean 等)压入参数。
  3. lua_pcall(L, nargs, nresults, errfunc) 执行;nargs/nresults 是参数个数和期望返回值个数。
  4. lua_tonumber / lua_tostring / lua_toboolean 等从栈顶取返回值。
  5. 出错时 lua_pcall 返回非 0,错误信息在栈顶,要 lua_tostring(L, -1)lua_pop

跑文件/字符串:luaL_dofile / luaL_dostring(内部也是 load + pcall)。


3. Lua 调 C++:注册 C 函数

C 侧写一个符合签名的函数:

typedef int (*lua_CFunction)(lua_State *L);

约定:

  • 从栈上 lua_tointeger(L, 1) 等读 第 1、2… 个参数(Lua 传的)。
  • 把返回值 lua_push* 压栈。
  • return n 表示 返回给 Lua 的返回值个数。

lua_pushcfunction(L, my_c_fn)lua_setglobal(L, "myfn"),Lua 里就能 myfn(...)
也可以用 luaL_Reg + luaL_newlib 做成 table 模块(Lua 5.2+ 常用 require 加载 .dll/.so 里的 luaopen_xxx)。


4. 在 Lua 里表示「C++ 对象」:userdata + 元表

Lua 没有指针类型,一般用:

  • full userdata:lua_newuserdata 分配一块内存,常用来 嵌入 T* 或 struct 副本;
  • light userdata:只传一个 void*,不参与 GC,适合「由 C++ 保证生命周期」的句柄。

再在 userdata 上 setmetatable__index 里挂 方法表,Lua 里就变成 obj:Method()(语法糖等价 obj.Method(obj))。

销毁、GC 时可用 __gc 元方法(userdata)通知 C++(注意 Lua 版本对 table 的 __gc 支持)。

UnLua怎么通信?

UnLua原理详解 - mike的文章 - 知乎

UnLua解析(一)Object绑定lua - 南京周润发的文章 - 知乎

至此,我们就回答了第一个问题:为什么在Lua中调用UE4.UWidgetBlueprintLibrary.Create,可以最终调用到C++的Create函数?,这里我们总结一下回答:

  1. Lua代码执行到UE4.UWdigetBlueprintLibrary的时候,执行了RegisterClass(“UWdigetBlueprintLibrary”)函数,做了这样几件事: (1) UnLua框架代码在G表里创建了一个名为【“UWdigetBlueprintLibrary”的表】 (2)为这个表定义了【C++类型的__Index元方法】 (3)将这个类型的【FClassDesc】记录到了UnLua的反射信息库中
  2. Lua代码执行到UE4.UWdigetBlueprintLibrary.Create的时候,执行了【C++类型的__Index元方法】,做了这样几件事: (1)通过【FClassDesc】和函数名“Create”注册Field,组装了【UFunctionDesc】,记录到了UnLua的反射信息库中 (2)在【“UWdigetBlueprintLibrary”的表】创建了key为“Create”,value为【C++函数ClassCallUFunction + 函数反射信息UFunctionDesc指针的闭包】 (3)将【C++函数ClassCallUFunction + 函数反射信息UFunctionDesc指针的闭包】返回给Lua层
  3. Lua代码执行到UE4.UWidgetBlueprintLibrary.Create(_G.GetCurrentWorld(), prefab, nil)的时候,执行了【C++函数ClassCallUFunction + 函数反射信息UFunctionDesc指针的闭包】,也就是执行了C++函数ClassCallUFunction,做了这样几件事: (1)执行PreCall,根据【UFunctionDesc】和Lua参数,将Lua参数转换成C++参数,再根据【UFunctionDesc】反射信息,将C++参数写入到一个缓存区中 (2)执行 UObject::ProcessEvent(FinalFunction, Params),参数FinalFunction是UFunction,参数Params是前面保存有C++参数的缓存区。ProcessEvent执行时,即调用了真正我们想调用的C++ Create函数,然后把C++返回值放入了Params缓存区中 (3)执行PostCall,从Params缓存区中读出C++返回值,转换成Lua返回值,Push进Lua栈,返回给Lua。

UnLua监听了新UObject被创建出来的事件,回调函数是上图的NotifyUObjectCreated,因此,当C++ Create函数创建出一个新的UserWidget的时候,会调到NotifyUObjectCreated,并把新UObject传入,我们以创建出了HelloWorldUMG为例,这个UserWidget触发了回调,下面我们看看它做了什么事情。

至此,Manager->Bind函数全部执行完毕,而新UObject创建后触发NotifyUObjectCreated调用的内容,其实就是从新UObject中,尝试获取UObject绑定的Lua脚本路径,然后进行Manager->Bind。总结一下Manager->Bind做了哪些事情

(1)Manager->Bind绑定的是:新创建的UObject + Lua 对象(根据新创建的UObject找到lua路径后创建的) (2)为新UObject 注册(RegisterClass)元表 (3)根据Lua模块,重写新UObject中可被重写的的UFunction的反射信息,注意这里是直接在UFunction上改写 (4)创建Lua对象,并做设元表、设Object等变量、放入全局引用等初始化,并把Lua对象和lightuserdata(UObject指针)的映射放入ObjectMap中,ObjectMap是Lua中的对象 (5)使新UObject被全局持有,并把Lua对象索引和UObject映射放入AttachedObjects中,AttachedObjects是C++中的对象,和Lua中的ObjectMap对象相似

可以知道,当新的UObject实例被创建时,UnLua做了这些准备工作,然后把Lua对象和C++对象的映射保存了起来,C++存在AttachedObjects中,Lua存在ObjectMap中,容易猜到这是在后面的转换做准备。

问题2:C++的Create函数返回的是一个UserWidget对象,而Lua这边用”local UMG”接住的是一个Lua对象,是怎么做到C++这边Return一个UObject,Lua这边接住的是一个Lua Table的呢,这个C++对象和Lua对象之间又有什么关系?

  1. 在lua CallUE函数执行到 UObject::ProcessEvent(FinalFunction, Params)时,调用了C++Create函数,这时创建了一个新的UObject,触发了NotifyUObjectCreated回调,在这个回调中,根据UObject创建了Lua对象,并将Lua对象和UObject的映射关系保存在了Lua中的ObjectMap中
  2. 在Lua CallUE函数执行到PostCall的时候,根据Params缓存区的返回值,在ObjectMap中索引到了对应的Lua对象,将这个Lua对象返回给了Lua

以此实现了Lua对象和C++对象之间的转换,Lua对象和C++对象之间的关系:

Lua对象的元表是“UAGame.Utils.HelloWorldUMG”Lua模块,该Lua模块的元表是通过RegisterClass创建出的UObject元表,它里面包含了UObject的反射信息、Class_Index元方法等内容可供与C++交互,同时Lua对象的Object对象存有UObject的二级指针,可通过该Lua对象取到UObject。

问题3:为什么执行“UMG.centerText”,就直接可以对它进行SetText操作了?

3)问题3回答总结:

因为上面Create返回的UMG是一个LuaInstance,之前设置了它的元表,并且在Lua的ObjectMap中将它和相应UObject进行了绑定,调用到centerText时,通过反射和映射关系,找到了对应UObject和属性信息,然后又创建了一次新的Userdata以及对应关系放回给了Lua,最后再用这个Userdata调用SetText,和之前调用Create函数一样,触发元方法,根据FieldName找到UFunction,根据userdata去ObjectMap中找到UObject,然后返回一个包含函数和反射信息的闭包,然后PreCall、UObject::ProcessEvent、PostCall,最后将结果返回给Lua,完成一整套调用。

公司真题随手记录

网易

实现一个队列,要求不重复怎么搞?

listview怎么优化的?

恢复这个任务怎么恢复的?会不会影响场景中的东西?

深蓝

植被随机变稀疏是怎么搞得?

样条曲线实现逻辑

节点怎么跳到任务?

萨罗斯

Ue的game play框架?Ds、基础的类分别有什么用?

C++怎么到lua?lua怎么到C++?

反射机制如果暴露给蓝图的?

自研引擎和ue有啥区别?

TCPheUDP区别?射击游戏一般用哪种?PUBG用了哪种?UE用的哪种?

UDP如何实现握手?几种方式?可靠性如何保证?

永星

ue常用的反射宏有哪些?绑定的时候有没有踩什么坑?

native和一个什么没听清有什么区别

Uboject的生命周期?会遇到什么函数


面经-ue知识
https://rorschachandbat.github.io/找工作/面经-ue知识/
作者
R
发布于
2026年3月26日
许可协议