乐信圣文面经
帧同步和状态同步区别
帧同步只同步玩家输入指令,所有客户端运行相同的确定性逻辑,保证相同输入产生相同结果,适合RTS、MOBA类游戏;状态同步则直接同步游戏对象的状态数据(如位置、血量等),服务器作为权威来源,客户端主要负责表现,适合MMORPG、FPS等游戏。两者核心区别在于同步的内容不同:帧同步同步”操作”,状态同步同步”结果”。
帧同步注意事项及不同步原因
帧同步必须保证所有客户端逻辑代码完全一致,使用定点数避免浮点精度问题,统一随机数种子,避免使用本地时间。不同步原因包括:浮点数计算精度差异、随机数未同步种子、逻辑代码版本不一致、网络丢包导致指令丢失、多线程执行顺序差异等,在实际项目中经常遇到因浮点数运算或时间戳使用不当导致的同步问题。
预测、插值与校正实现方式
客户端预测先在本地执行操作并显示结果,服务器验证后发送权威状态;插值通过平滑过渡 between 历史状态来消除网络抖动,保持画面流畅;校正当客户端预测与服务器状态不一致时,以服务器数据为准进行位置修正,并通过插值平滑过渡到正确状态,避免瞬间跳跃。
逻辑层与表现层分离原因
分离逻辑层和表现层主要为了确保游戏逻辑的确定性和可维护性,逻辑层处理核心计算和规则,保证所有客户端计算结果一致;表现层负责视觉效果和动画,允许各客户端有差异。这种分离架构便于调试、回放功能实现,并能有效防止视觉表现影响游戏逻辑的正确性。
心跳机制检测实现
心跳机制通过客户端定期向服务器发送微小数据包维持连接,服务器检测心跳间隔判断连接状态。通常设置心跳频率为每2-5秒一次,超时阈值设为2-3倍心跳间隔,连续多次超时则判定为断连,触发重连机制,同时心跳包可携带基础状态信息用于连接质量监测。
通信协议选择
游戏通信通常采用UDP协议为主,TCP为辅的混合方案。UDP用于实时性要求高的游戏状态同步,如位置更新、技能释放等;TCP用于可靠性要求高的数据,如登录验证、物品交易等。KCP等可靠UDP协议也在游戏中广泛应用,在保证实时性的同时提供更好的可靠性。
重连机制实现
重连机制首先检测连接状态,断连时尝试重新建立连接;成功后向服务器请求当前游戏状态快照,服务器发送完整的游戏状态数据;客户端根据服务器数据重置本地状态,同步到当前游戏进度,并重新开始接收后续的同步消息,确保玩家无缝重回游戏。
TCP和UDP区别
TCP是面向连接、可靠的传输协议,保证数据有序到达且不丢失,适合文件传输等场景;UDP是无连接、不可靠的协议,不保证数据顺序和完整性,但传输效率高,适合实时音视频和游戏数据同步。核心差异在于TCP通过三次握手建立连接并自带重传机制,而UDP直接发送数据包不作验证。
分包黏包解决方案
采用消息头+消息体的格式,消息头固定包含数据长度字段(如4字节int)。接收方先读取长度字段,再按指定长度截取完整消息。若剩余数据不足则等待下次接收(分包),若有多余数据则拆分处理(黏包)。Protobuf等序列化工具常内置此机制。
UDP面向报文段,TCP面向?
TCP是面向字节流的协议,数据被视为无结构的连续字节序列,传输层会自动拆分和重组。这正是TCP需要处理黏包问题的根本原因,而UDP的报文段边界在传输中保持不变。
常用数据结构
基础数据结构包括:栈(LIFO)、队列(FIFO)、数组(连续内存)、链表(动态节点)、二叉树(分层存储)。游戏开发中常用衍生结构如红黑树(高效查找)、跳表(快速范围查询)、对象池(内存复用)等。
数组和链表区别
数组内存连续,支持O(1)随机访问但增删成本高;链表通过指针连接节点,增删效率O(1)但查找需遍历。Unity中数组适合静态数据(如配置表),链表适合频繁修改的数据(如技能冷却队列)。
链表实现方式
C#中通过类实现节点结构,包含Value字段存储数据,Next字段指向下一节点。优化时可添加Prev字段构成双向链表,或引入头尾指针提升操作效率。实际开发建议直接使用LinkedList
数组和List区别
List本质是动态数组,内部通过数组实现并自动处理扩容(默认2倍增长)。相比原生数组,支持增删元素、泛型类型安全,但插入性能低于链表。Unity中频繁增删建议使用LinkedList。
反射机制
反射允许运行时动态获取类型信息(字段/方法/属性),通过System.Reflection实现。典型应用是配置表自动加载,但会带来性能开销(比直接调用慢100倍以上),应避免高频使用。
抽象类与接口
抽象类可包含实现代码和字段,适合is-a关系;接口纯定义契约,支持多继承。游戏开发中,抽象类常用于角色基类,接口用于技能系统等需要多重特性的场景。
装箱与拆箱
装箱将值类型(如int)转为引用类型(object),引发堆内存分配;拆箱逆向操作可能抛出异常。频繁操作会导致GC压力,解决方案是使用泛型集合(如List
GC原理与优化
Unity使用分代式GC,当堆内存不足或手动调用GC.Collect()时触发。优化方案包括:减少引用分配、使用结构体、对象池复用。关键指标是GC频率控制在每10秒1次以内。
Animator与Animation
Animator基于状态机驱动,支持混合动画和参数控制;Animation直接播放单一动画文件。3D角色用Animator,2D简单动画可用Animation组件。性能敏感场景应优化状态机层级。
四元数与欧拉角
四元数(Quaternion)用4个浮点数表示旋转,避免万向节锁且插值平滑;欧拉角直观但存在轴向缠绕问题。Unity中旋转运算一律使用四元数,仅在编辑器暴露欧拉角方便调整。
结构体与类
结构体是值类型(栈内存),适合小型数据(如坐标);类是引用类型(堆内存),支持继承和多态。战斗系统中的伤害计算等高频操作应优先使用结构体。
点乘与叉乘
点乘结果反映向量夹角余弦值,用于光照计算和投影;叉乘生成正交向量,用于求平面法线或角色转向。Unity中Vector3.Dot()和Vector3.Cross()已作底层优化。
协程与线程
协程在Unity主线程执行,通过yield分帧处理异步任务;线程并行执行但需手动同步。协程适合动画序列等轻量任务,耗时计算(如A*寻路)应交给线程池。
哈希的底层原理,哈希冲突的解决方法,以及哈希冲突的具体查找过程
(索引上的哈希值是否相等?)
哈希表通过哈希函数将键映射到数组索引,理想情况下实现O(1)访问。哈希冲突常用解决方法:开放定址法(线性探测/二次探测)、链地址法(桶结构)、再哈希法。查找时先计算键的哈希值定位索引,若该位置非空则比较键值:链地址法遍历链表比较,开放定址法则按探测序列比较。关键是比较实际键值而非哈希值。
数据结构:特殊二叉树,满二叉树的性质
满二叉树所有非叶子节点都有两个子节点,且叶子都在最后一层,节点数满足2^h-1(h为深度)。完全二叉树除最后一层外全满,且最后一层节点左对齐。二叉搜索树左子树所有节点值小于根,右子树所有节点值大于根,支持高效查找。
数据结构:二叉树的层序遍历,二叉树的深度遍历
(有几种顺序,非递归的结束条件)
层序遍历使用队列,按深度逐层输出;深度遍历分前序(根左右)、中序(左根右)、后序(左右根)三种顺序。非递归实现用栈模拟递归,前序/中序的结束条件为栈空且当前节点为空;后序需记录上次访问节点以避免重复处理
数据结构:有向图和无向图,邻接矩阵和邻接表
邻接矩阵用二维数组存储边权值,适合稠密图;邻接表用数组+链表存储邻接节点,适合稀疏图。无向图的邻接矩阵对称,邻接表每条边存两次;有向图则需区分出度入度
数据结构:数组实现栈
定义类包含数组容器、容量限制、栈顶指针top(初始-1)。Push时top++并赋值,Pop时返回top位置值后top–。需处理栈满(top==capacity-1)和栈空(top==-1)边界条件,C#中常用泛型数组实现类型安全
CSharp:接口和抽象类的区别,抽象类的抽象方法在派生类中不实现是被允许的吗?
抽象类可包含实现代码和字段,适合is-a关系;接口纯定义契约,支持多继承。游戏开发中,抽象类常用于角色基类,接口用于技能系统等需要多重特性的场景。
CSharp:装箱和拆箱
装箱将值类型(如int)转为引用类型(object),引发堆内存分配;拆箱逆向操作可能抛出异常。频繁操作会导致GC压力,解决方案是使用泛型集合(如List
CSharp:.NET 垃圾回收机制
(这里向面试官申请去说了一下 Unity 中 Mono 和 IL2CPP 的不同实现,说完后面试官也进一步向我科普了一下 Unity6 的贝姆默认开的是增量)
Unity的Mono使用分代GC,分Young/Old两代;IL2CPP转换为C++代码后使用更高效的内存管理。Unity6默认启用增量GC,将GC工作分摊到多帧减少卡顿。优化关键:减少堆分配、使用结构体、对象池复用。
设计模式:说一下 SOLID 原则
单一职责(类功能专注)、开闭原则(扩展开放修改关闭)、里氏替换(子类可替换父类)、接口隔离(接口细化)、依赖倒置(依赖抽象而非实现)。在技能系统中,每个技能类独立实现(单一职责),通过接口扩展新效果(开闭原则)。
设计模式:里氏替换原则的实际应用
角色基类定义移动接口,派生类(战士/法师)重写移动逻辑但保持相同方法签名。客户端代码依赖基类接口,可无缝替换不同子类。违反案例:子类抛出父类未声明的异常或强化前置条件。
Unity:Canvas 的三种渲染模式,overlay 这种模式下为什么不需要相机
Overlay模式渲染到屏幕空间,无需相机直接覆盖整个视图;Camera模式通过指定相机渲染;World Space作为3D物体嵌入场景。Overlay利用UI内置投影矩阵,直接映射到屏幕坐标故无需相机参与
Unity:RectTransform 和 Transform 的区别,RectTransform 比 Transform 多了一个什么位置
相比Transform,RectTransform增加锚点(Anchors)、轴心(Pivot)、尺寸(SizeDelta)等UI布局属性。核心多出的位置概念是相对锚点的标准化位置(0-1范围),使UI能自适应不同分辨率
Unity:UI 的排列展示组件(Layout Group),Grid 和另外两个的区别
Horizontal/Vertical Layout Group按行列排列,Grid Layout Group网格排列支持动态行列数。Grid独特在可定义单元格大小和间距,自动换行布局,适合背包/图鉴系统。
Unity:UI 的滑动组件,超出滑动区域的元素如何隐藏或裁剪
通过Mask组件裁剪超出区域的内容,Mask利用模板缓冲(Stencil Buffer)实现像素级遮挡。优化方案:RectMask2D性能优于Mask(仅矩形裁剪),动态列表采用对象池减少渲染开销。
Unity:三个 Mask 的底层原理
Mask通过模板测试实现裁剪:首先将遮罩形状写入模板缓冲,子元素渲染时比较模板值决定是否绘制。Transitional Mask添加Alpha检测实现软边缘,但性能开销较大。
Unity:场景题,用户上传的图片都是方形的,如何实现圆形头像的显示
方案1:Shader采样时丢弃方形纹理边缘像素(使用圆形Alpha贴图);方案2:渲染到RenderTexture后应用圆形Mask;方案3:UI层叠圆形遮罩图(性能最优)。推荐方案2+缓存机制避免重复处理。
项目:MMO 最重要的模块是哪些,网络的实现,客户端之间的同步
(协议广播 + 反射处理)
网络模块(TCP长连接+KCP可靠UDP)、实体管理、场景同步、技能系统、任务系统。同步采用状态同步+帧同步混合:移动用UDP状态同步,技能用TCP保证可靠性。反射处理网络消息路由到对应Handler。
项目:角色控制的实现
(新版 Input + Cinemachine)
新版Input System定义Action Map分离输入逻辑,Cinemachine提供相机智能跟随(Impulse源实现打击感)。关键优化:输入缓冲(Input Buffer)缓解网络延迟影响,相机碰撞检测避免穿墙。
项目:对话系统的实现(UI Toolkit + IMGUI),为什么没有用 UGUI
采用UI Toolkit(数据绑定+样式分离)结合IMGUI(编辑器工具快速原型)。未选UGUI因:UI Toolkit更适合动态文本布局(RTL语言支持),内存开销更低,且与Data-Oriented设计理念契合。
