ue学习路线
引擎
配置UE CPP
ue对比unity项目 : https://dev.epicgames.com/documentation/en-us/unreal-engine/parrot-game-sample-for-unreal-engine X
UE入门: https://www.youtube.com/watch?v=XRmn-EYt8wI X
网易ue教程: https://zhuanlan.zhihu.com/p/647108274 √
UE路径
水曜日鸡 https://zhuanlan.zhihu.com/p/659805638
大钊老师PPT https://zhuanlan.zhihu.com/p/1937270739192505484
模块路线 https://zhuanlan.zhihu.com/p/637519288
最小实现
- 每天读2-3篇文章
- 行业趋势
- 技术类
- 持续学习新技术
- 将大目标拆解成小目标
- 复盘总结
- 知识库
- 资料、笔记整理
- 软件库
- 分享
目前在跟:
- Slate 源码
- 动画
想开坑:
TcpUdp补充
OS补充
LuaGC
Primer-CPP
优先级排序
- GP框架- Actor生命周期 UActorCompent √
- LS - 爆炸小汁 A 差不多完结 √
- Core-GC-UObject-反射-序列化 UHT √
- Lua 源码
- 数据结构 √
- 虚拟机
- GC
- Unlua
https://zhuanlan.zhihu.com/p/24806646625 https://www.cnblogs.com/ZWJ-zwj/p/18342070
UE GC √
Slate
UE蓝图虚拟机 √
C++ Primer - CPP 八股
CSAPP – 网络 OS
GAMES104+ 游戏引擎架构
UGUI源码
Flush
3C方向:
DS √
GAS √
Lyra
动画
AI 行为树 √
多线程 √
零星收集AI
- Lyra 解析
我在项目中采用 Gameplay Tag 驱动 ABP,Ability 只负责施加语义 Tag,AnimInstance 通过监听 ASC 的 Tag 事件缓存状态变量,从而实现动画与逻辑解耦、事件驱动更新以及良好的网络同步。
Lyra 的 Locomotion 不依赖离散状态机,而是基于速度、加速度和是否在地面的连续变量,通过 Blend Space 驱动动画,其中加速度用于区分起步与急停,速度控制移动强度,从而减少状态数量并提升动画平滑度和可扩展性。
Lyra 将 GAS 作为统一的行为与状态执行框架,Ability 负责行为生命周期与 Tag 施加,Gameplay Tag 作为跨系统状态语义驱动动画、输入与逻辑,从而实现高度解耦、网络友好且可扩展的架构。
Lyra 中属性同步完全依赖 GAS 的 AttributeSet 与 RepNotify,属性只通过 GameplayEffect 在服务器修改,客户端通过 ASC 的属性变化委托响应,从而实现权威同步、预测安全以及与动画和 UI 的解耦。
Lyra 的 UI 作为 GAS 的只读视图层,通过监听 Gameplay Tag 与 Attribute 的变化驱动显示逻辑,并使用 UI Extension System 实现模块化注入,从而保证 UI 与 Gameplay 的高度解耦与网络友好性。
在 Lyra 的 UI 架构中,蓝图只承担表现与布局职责,所有状态来源、数据绑定和生命周期管理都在 C++ 层完成,UI 蓝图作为 GAS 状态的被动视图层,从而保证了系统解耦、网络安全和可维护性。
Lyra 局外系统 UI 的蓝图仅负责布局与表现,所有数据、状态和事件通过 C++ Controller 或 Subsystem 管理,UI Blueprint 只响应事件显示状态,从而保持 UI 与业务逻辑的完全解耦,并支持可替换、可扩展的模块化设计。
- 局外
局外系统采用 C++ + UnLua 架构,Lua 使用比例约 70%~80%,
主要负责 UI、流程与玩法规则,核心数据与网络能力由 C++ 保证稳定性。
虽然 Lua 使用比例较高,但它不是权威层。
核心数据最终以 C++ 为准,Lua 只是读取和驱动这些数据。
局外系统整体以 Lua 作为主要开发语言,大约占 70%~80%。
C++ 主要负责底层数据结构、引擎能力封装和与服务端通信的基础设施,
Lua 层负责业务规则、流程控制和 UI 逻辑
Game-Client-Interview-2025
2025游戏客户端开发求职全纪录(C++/UE/Unity) | 涵盖 88 场面试深度复盘与高频题目统计🎮
🚀 为什么创建这个仓库?
在整个 2025 招聘季中,我一共经历了 37 场暑期实习面试和 51 场秋招面试,每一场面试结束后,我都进行了详尽的录音复盘,用穷举法对当前游戏客户端开发的常见题目进行了统计。因为我的简历上同时覆盖了 UE 实习背景与 Unity 个人项目背景,所以这些题目对于不同引擎侧重的同学都有参考性。希望这份面试题能帮助后来者少走弯路!
📊 高频面试题频率统计
题目及考点按大类分组,并依据面试中出现的频率从高到低排列。所有出现的知识点都需要提前做好准备,出现频次超过10的问题不仅要准备答案,还要深入理解相关知识点,避免被追问时卡壳。
1. 核心技术栈(C++ / Lua / 编程基础)
| 知识点分类 | 具体题目 / 考点 | 出现频率 |
|---|---|---|
| C++ 内存与多态 | 虚函数实现原理 (vptr/vtable) | 🔥15 |
| 智能指针 (shared_ptr/unique_ptr) | 🔥14 | |
| 内存对齐 (Memory Alignment) | 🔥9 | |
| 右值引用与移动语义 (Rvalue/Move) | 🔥8 | |
| C++ 多态机制 | 🔥7 | |
| 内存分区 (堆/栈/静态区) | 🔥6 | |
| C++ 类内存布局 | 🔥5 | |
| Lua 专项 | 元表 & 元方法 (Metatable) | 🔥12 |
| Lua OOP 实现原理 | 🔥12 | |
| Lua Table 底层实现 | 🔥7 | |
| Lua 基本语法与闭包 | 🔥5 | |
| C# (Unity) | 垃圾回收机制 (GC) | 🔥7 |
2. 计算机基础(系统/网络/设计模式)
| 知识点分类 | 具体题目 / 考点 | 出现频率 |
|---|---|---|
| 通用架构 | 对象池 (Object Pool) | 🔥15 |
| 协程 vs 线程 | 🔥8 | |
| 单例模式 / 观察者模式 | 🔥6 | |
| 有限状态机 (FSM) | 🔥6 | |
| 网络与系统 | 编译链接流程 | 🔥5 |
| 锁机制 (死锁/悲观锁/乐观锁) | 🔥5 | |
| 状态同步 vs 帧同步 | 🔥4 | |
| 堆 vs 栈 的区别 | 🔥4 |
3. 数据结构与算法
| 知识点分类 | 具体题目 / 考点 | 出现频率 |
|---|---|---|
| 数据结构 | 哈希表 (Hash Table) 实现及冲突 | 🔥11 |
| 红黑树 (Red-Black Tree) | 🔥10 | |
| 四叉树 (Quadtree) 空间分割 | 🔥5 | |
| 堆 (Heap) 结构 | 🔥4 | |
| 算法 | 排序算法 (快速排序/归并) | 🔥8 |
| A* 算法 (A-Star) | 🔥5 | |
| 点在三角形内判定 | 🔥4 | |
| 点乘 vs 叉乘 | 🔥4 |
4. 引擎专项(Unity / UE / 图形学)
| 知识点分类 | 具体题目 / 考点 | 出现频率 |
|---|---|---|
| Unity | Unity 协程原理 | 🔥10 |
| 生命周期函数流程 | 🔥8 | |
| AssetBundle 打包与卸载 | 🔥7 | |
| DrawCall 优化 | 🔥6 | |
| UGUI 布局与适配 | 🔥5 | |
| Unreal Engine | UE 垃圾回收 (GC) 机制 | 🔥9 |
| UE 反射机制 | 🔥7 | |
| UnLua 插件原理 | 🔥7 | |
| GAS (Gameplay Ability System) | 🔥4 | |
| 图形学 | 图形渲染管线 (GRP) | 🔥2 |
5. 实习经历与综合
| 分类 | 考点 | 出现频率 |
|---|---|---|
| 实习 | 负责模块细节深度拆解 | 🔥43 |
| 遇到的问题及解决方案 | 🔥26 | |
| 职业规划 | 想从事客户端的原因 | 🔥12 |
| 为什么选择游戏行业? | 🔥11 | |
| Offer 选择因素 | 🔥9 |
https://zhuanlan.zhihu.com/p/1890089256204092723
根据面试经验,个人建议大家学习是要有侧重点的。自己要选择一个方向去深耕,比如本人面试过三个方向。
1.角色3c方向。
2.系统方向。
3.玩法方向。
当然UE肯定不止这么多方向,方向比如关卡,战斗(部分战斗可能包含在3C)等等。
大家最好选择一个方向作为侧重点去学习。分享一下个人经验。
角色3c:
1.动画蓝图:没有经验就去看ALS和Lyra的动画蓝图就好,吃透了就差不了至于MotionMatching可以作为附加选项。
2.GAS:GAS目前似乎慢慢的流行起来了。大家可以看一下GAS的源码,以及Lyra在GAS中的应用。
3.DS网络:UE这一套网络系统还是会被经常问到的,属性同步,多播函数等,当然如果是MMO项目则不太会被问到,先熟悉应用,源码可以作为附加选项。
4.场景应用:这个就看个人积累了。
系统,玩法:系统和玩法被问到的东西差不多。
1.场景应用:说白了就是项目经验,这个最重要,不过系统和玩法就大概率不会问DS的内容,因为大多数采取的是第三方服务器。
2.UMG与Slate:比较常问。因为系统和玩法与UMG挂钩较多,Slate用于编辑器拓展。
3.脚本语言:比如Lua吧,就会问到Lua闭包原理,垃圾回收原理,lua与Cpp如何相互调用,lua如何运行等等。
4.UE反射:这个我有写过文章。偶尔会被问道。
ok,上面的内容有的我比较熟,有的一般熟,有的甚至完全不了解,所以本人打算把上述内容在两年时间(到2027年清明)内以文章的形式更新完,希望大家可以共同学习,共同努力,共同进步,其实我是想说一年的,然后感觉比较吹牛了哈哈哈哈,而且我要入职新公司了,估计前几个月压力会比较大,也没时间搞这些。唉,希望别碰到裁员,我求求了。除去上面的内容,然后就是一些通用的内容了,几乎大多数面试都会问,比如说C++的知识,虚函数多态,设计模式,MVVM,LRU等等。然后一些厂子比如说网易,字节,祖龙娱乐会让现场写算法,用的都是牛客,所以刷算法和八股的去牛客就好了。
TEMP
Lua
Lua局部变量与全局变量那个性能好一些?为什么?
查找速度差异
局部变量直接存储在寄存器式的局部变量表中,通过索引访问(一次操作)。
全局变量实际存储在_ENV表(如_G)中,每次访问至少需要两次哈希查找(先找_ENV,再找键名)
如果一个A表有一个元表B表,B表里面有个元素C,怎么在不访问B表的情况下给A表添加C元素? 了解rawset和rawget吗,在什么情况下会用到这两个?
rawget(table, key)
功能:直接从表中获取键对应的值,不触发元表的 __index元方法
参数:(表, 键)
返回值:值(如果键不存在则返回 nil)
rawset(table, key, value)
功能:直接设置表的键值对,不触发元表的 __newindex元方法
参数:(表, 键, 值)
返回值:被设置的表
C++
虚函数
如果是有虚函数的话,虚函数表的指针始终存放在内存空间的头部;
(2)除了虚函数之外,内存空间会按照类的继承顺序(父类到子类)和字段的声明顺序布局;
(3)如果有多继承,每个包含虚函数的父类都会有自己的虚函数表,并且按照继承顺序布局(虚表指针+字段);如果子类重写父类虚函数,都会在每一个相应的虚函数表中更新相应地址;如果子类有自己的新定义的虚函数或者非虚成员函数,也会加到第一个虚函数表的后面;
(4)如果有钻石继承,并采用了虚继承,则内存空间排列顺序为:各个父类(包含虚表)、子类、公共基类(最上方的父类,包含虚表),并且各个父类不再拷贝公共基类中的数据成员。
内存对齐
(1)内存对齐的原因:关键在于CPU存取数据的效率问题。为了提高效率,计算机从内存中取数据是按照一个固定长度的。比如在32位机上,CPU每次都是取32bit数据的,也就是4字节;若不进行对齐,要取出两块地址中的数据,进行掩码和移位等操作,写入目标寄存器内存,效率很低。内存对齐一方面可以节省内存,一方面可以提升数据读取的速度;
(2)内容:内存对齐指的是C++结构体中的数据成员,其内存地址是否为其对齐字节大小的倍数。
(3)对齐原则:1)结构体变量的首地址能够被其最宽基本类型成员的对齐值所整除;2)结构体内每一个成员的相对于起始地址的偏移量能够被该变量的大小整除;3)结构体总体大小能够被最宽成员大小整除;如果不满足这些条件,编译器就会进行一个填充(padding)。
(4)如何对齐:声明数据结构时,字节对齐的数据依次声明,然后小成员组合在一起,能省去一些浪费的空间,不要把小成员参杂声明在字节对齐的数据之间。
1)shared_ptr ,多个共享指针可以指向相同的对象,采用了引用计数的机制,当最后一个引用销毁时,释放内存空间;
(2)unique_ptr,保证同一时间段内只有一个智能指针能指向该对象(可通过move操作来传递unique_ptr);
(3)weak_ptr,用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
shared_ptr
1)shared_ptr是通过引用计数机制实现的,引用计数存储着有几个shared_ptr指向相同的对象,当引用计数下降至0时就会自动销毁这个对象;
(2)具体实现:
1)构造函数:将指针指向该对象,引用计数置为1;
2)拷贝构造函数:将指针指向该对象,引用计数++;
3)赋值运算符:=号左边的shared_ptr的引用计数-1,右边的shared_ptr的引用计数+1,如果左边的引用技术降为0,还要销毁shared_ptr指向对象,释放内存空间。
(3)shared_ptr的引用计数本身是安全且无锁的,但是它指向的对象的读写则不是,因此可以说shared_ptr不是线程安全的。shared_ptr是线程安全的吗? - 云+社区 - 腾讯云 (tencent.com)
Move
1)左值就是具有可寻址的存储单元,并且能由用户改变其值的量,比如常见的变量:一个int,float,class等。左值具有持久的状态,直到离开作用域才销毁;右值表示即将销毁的临时对象,具有短暂的状态,比如字面值常量“hello”,返回非引用类型的表达式int func()等,都会生成右值;
(2)右值引用就是必须绑定到右值的引用,可以通过&&(两个取地址符)来获得右值引用;右值引用只能绑定到即将销毁的对象,因此可以自由地移动其资源;
(3)右值引用是为了支持移动操作而引出的一个概念,它只能绑定到一个将要销毁的对象,使用右值引用的移动操作可以避免无谓的拷贝,提高性能。使用std::move()函数可以将一个左值转换为右值引用。(可以通过两个很长的字符串的直接赋值和移动赋值来测试一下性能的差距)。
U++
反射
简单来说,反射是程序在运行时能够了解自身结构的能力。在标准的C++中,编译器在编译后会将类型信息丢弃,运行时无法知道一个类有哪些成员变量或函数。UE通过一套由工具(Unreal Header Tool, UHT)和宏组成的系统,在编译时为C++类生成了额外的“元数据”,从而在运行时重建了这些信息。
这个系统的核心载体就是 UObject 及其派生类。一个类必须直接或间接继承自 UObject才能拥有反射能力。
宏
主要用途
UCLASS([specifiers])
标记一个可被反射的类。 Specifiers 提供了关键信息,例如:
• Blueprintable:此类可在蓝图中被继承。
• NotBlueprintable:禁止蓝图继承。
• Placeable:可在关卡编辑器中放置。
UPROPERTY([specifiers])
标记一个可被反射的成员变量。Specifiers 定义了其行为:
• EditAnywhere/VisibleAnywhere:在属性面板中编辑/查看。
• BlueprintReadWrite/BlueprintReadOnly:允许/禁止蓝图读写。
• **Replicated**:该属性会在网络上自动同步。
• Category:在编辑器中的分类。
UFUNCTION([specifiers])
标记一个可被反射的成员函数。Specifiers 定义了其调用方式:
• BlueprintCallable:蓝图可调用此函数。
• BlueprintImplementableEvent:C++声明,蓝图实现。
• BlueprintNativeEvent:C++有默认实现,蓝图可覆盖。
• **Client/Server/NetMulticast**:定义网络RPC的调用目标。
支持网络复制
多人在线游戏的同步机制极度依赖反射。
属性复制:当你在
UPROPERTY()中加入Replicated标识符,引擎的网络驱动程序在同步时就知道:“这个属性需要从服务器同步到客户端。” 每帧,它会比较服务器上该属性的值,如果发生变化,就通过反射系统获取其数据并打包发送。客户端收到后,再通过反射系统找到对应的对象和属性进行赋值。远程过程调用:
UFUNCTION()的Client、Server、NetMulticast等标识符,定义了一个函数应该在哪个端点(服务器/客户端)执行。当你调用一个RPC时,引擎利用反射信息(函数名、参数类型)将调用请求序列化成网络数据包,发送到目标端,目标端收到后再通过反射信息找到对应的函数并反序列化参数执行。预测与回滚:高级的网络功能也需要依赖反射来追踪和比较状态。
| 时刻 | A | AmmoNetSequence | UI | 服务器 | 服务器回调 | A服务器接收 | A接收后 |
|---|---|---|---|---|---|---|---|
| 0ms | 调用Fire(),并触发SpendRound() | AmmoNetSequence++ | 更新ui中子弹数为29(原有30) | - | - | - | - |
| 50ms | A本地A又开了两枪 | AmmoNetSequence = 3 | A本地ui子弹数为27 | 收到A 0ms调用Fire(),并触发SpendRound() | 通知A客户端更新Ammo子弹数为29,调用ClientUpdateAmmo(29); | - | - |
| 100ms | A相比50ms又开了两枪 | AmmoNetSequence为5 | ui子弹数为25 | - | - | 收到回调,设置Ammo为29 | AmmoNetSequence– ,AmmoNetSequence为4 |
| 50ms 不合法 | 同50ms | 同50ms | 同50ms | 收到A 0ms消息 ,判断Fire不合法 | 通知A不合法,ClientUpdateAmmo(30); | - | - |
| 100ms不合法 | 同100ms | 同100ms | A本地ui子弹数为26(0ms 的fire 不合法) | - | - | 收到回调,设置Ammo为30 | AmmoNetSequence,AmmoNetSequence为5 |
RTTI 是 C++ 的运行时类型信息(Run-Time Type Information)的缩写。它是 C++ 语言提供的一种机制,允许在程序运行时获取对象的实际类型信息。
在标准 C++ 中,RTTI 主要通过两个操作符实现:
typeid操作符:返回一个std::type_info对象,描述对象的类型。dynamic_cast操作符:用于在继承层次结构中进行安全的下行转换(将基类指针或引用转换为派生类)。
GC
UE GC 是采用标记-清除算法:
收集所有对象→标记根对象→递归标记可达对象→清理不可达对象
- 根集(Root Set):作为GC起点的对象集合,包括全局对象(如GameInstance、World)、当前活跃的Actor/组件,以及通过
AddToRoot()显式标记的对象 - 标记阶段:从根集出发,递归遍历所有被引用的UObject,标记为“活跃”(可达)
- 清除阶段:销毁所有未被标记(不可达)的对象,释放其内存
触发时机:GC会定期自动触发(切换场景强制GC 到达gc时间间隔),也可通过调用CollectGarbage()手动触发
对象存活条件: 被根节点对象引用: 若要在跨帧中保持对UObject的引用,必须在成员变量前添加UPROPERTY(),否则GC会认为该对象无引用而将其回收
对象销毁流程:调用Destroy()仅将Actor标记为“待销毁”,实际内存释放要等到GC运行时执行BeginDestroy()标记PendingKill → FinishDestroy()
GP框架
GameMode
裁判/导演
制定规则,管理游戏流程,分配“演员”和“操控器”。
仅服务器
PlayerController
玩家的“大脑”/操控器
接收玩家输入,将意图转化为指令发送给“躯体”。
服务器+每个客户端
Pawn
玩家的“躯体”/载具
存在于游戏世界中的实体,执行具体动作(移动、攻击)。
服务器+所有客户端
Character
人形“躯体”
强化版的Pawn,自带完整的角色移动和碰撞。
服务器+所有客户端
答:包括四个阶段:预处理阶段、编译阶段、汇编阶段、连接阶段。
(1)预处理阶段处理头文件包含关系,对预编译命令进行替换,生成预编译文件; c. ->.i #define
(2)编译阶段将预编译文件编译,生成汇编文件(编译的过程就是把预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码); .i->.s
(3)汇编阶段将汇编文件转换成机器码,生成可重定位目标文件(.obj文件)(汇编器是将汇编代码转变成机器可以执行的命令,每一个汇编语句几乎都对应一条机器指令。汇编相对于编译过程比较简单,根据汇编指令和机器指令的对照表一一翻译即可); .s->.obj
(4)链接阶段,将多个目标文件和所需要的库连接成可执行文件(.exe文件)。 .obj->.exe
进程是运行时的程序,是系统进行资源分配和调度的基本单位,它实现了系统的并发;
(2)线程是进程的子单位,也称为轻量级进程,它是CPU进行分配和调度的基本单位,也是独立运行的基本单位,它实现了进程内部的并发;
(3)一个程序至少拥有一个进程,一个进程至少拥有一个线程,线程依赖于进程而存在;
(4)进程拥有独立的内存空间,而线程是共享进程的内存空间的,自己不占用资源;
(5)线程的优势:线程之间的信息共享和通讯比较方便,不需要资源的切换等.
死锁就是多个进程并发执行,在各自占有一定资源的情况下,希望获得其他进程占有的资源以推进执行,但是其他资源同样也期待获得另外进程的资源,大家都不愿意释放自己的资源,从而导致了相互阻塞、循环等待,进程无法推进的情况。
(2)死锁条件:1)互斥条件(一个资源每次只能被一个进程使用);2)请求并保持条件(因请求资源而阻塞时,对已获得的资源保持不放);3)不剥夺条件(在未使用完之前,不能剥夺,只能自己释放);4)循环等待(若干进程之间形成一种头尾相接的循环等待资源关系)。
(3)死锁防止:1)死锁预防,打破四个死锁条件;2)死锁避免,使用算法来进行资源分配,防止系统进入不安全状态,如银行家算法;3)死锁检测和解除,抢占资源或者终止进程;
(4)银行家算法是一种最有代表性的避免死锁的算法。在避免死锁方法中允许进程动态地申请资源,但系统在进行资源分配之前,应先计算此次分配资源的安全性,若分配不会导致系统进入不安全状态,则分配,否则等待。为实现银行家算法,系统必须设置若干数据结构。安全的状态指的是一个进程序列{P1,P2,…Pn},对于每一个进程Pi,它以后尚需要的资源不大于当前资源剩余量和其余进程所占有的资源量之和。
业务:
ActivityMainMdt
这边会有一个时序问题,异步加载会导致时序问题。
- 创建jobList
- 获取jobid
- 在Finnish回调jobList[jobid] = nil
- 防止单帧返回 if jobisworking then jobList[jobid] = nil end
多次异步
我也不清楚,可能是透明度和绘制时序或者什么问题 原来异步做法是先改为透明颜色,加载回来才恢复颜色 我查的时候,现在数据都是对的,颜色也恢复了,但没绘制 Onpaint一直没调到 然后我本地删除所有东西,就好了 要是再深究就要把整个内容全打日志追踪了 现在做法只能先把改颜色注释掉,因为资源也加出来了,我也跟燕祖反馈了,只能先注释掉颜色
UGUIImage::AsyncLoadImageTexture UGUIImage::OnAsyncLoadImageAssetComplete 异步完成后会把颜色设回来的 有可能是你说的情况,实在不行我就只能把颜色注释掉
在GUIButton 写入反射的 bool 变量
1 | UPROPERTY(EditAnywhere, Category = "Sound", meta = (DisplayName = "bEnableSoundEvents",Tooltip = "是否触发下列按钮事件")) |
在ClickedSoundEventName 事件中多加了一个bEnableSoundEvents 判断
但是这边编译出了一点问题 MSbuild说是和cpp不匹配 or 在 EditorDir 宏没有设置? 待定
这边处理了一下VX和Btn都有的所有WBP
因为考虑到有特殊情况 :可能vx和btn多对多或者一对多是要特殊处理
现存200+ / 2000+ (outside) 的wbp处理 有25个是多对多 一对一 这边手动处理了。
如果要全自动的话,用Event 扫描一遍是否有btn的OnClick Onhover等按钮事件 并且是否连接vxcommon的按钮事件
这边最后去修改bEnableSoundEvents 的这个bool 变量 ,只处理一对一的wbp

