1.内存中,堆和栈的区别是什么?
考察内容
这题通常不是要你死记“值类型一定在栈、引用类型一定在堆”。
- 栈(Stack):主要用于保存函数调用过程中的栈帧,例如局部变量、参数、返回地址等。函数执行结束后,对应栈帧会自动弹出,因此分配和回收很快。
- 堆(Heap):主要用于保存通过 new 创建、生命周期不完全由当前函数决定的对象实例。堆内存由 GC 负责回收。
- 引用变量本身放在哪里,要看它是局部变量、字段还是静态字段;但它指向的对象实例通常在托管堆上。
核心区别不是“类型”,而是数据的生命周期和分配方式。
标准回答
- 栈和堆主要区别在于分配方式、生命周期和回收机制。
- 栈通常用于函数调用时保存参数、局部变量和返回信息,随着函数执行结束自动释放,分配回收速度很快。
- 堆通常用于保存 new 出来的对象实例,由 GC 在对象不再被引用时统一回收。
- 在 C# 里不能简单说值类型一定在栈、引用类型一定在堆,更准确的说法是:局部值类型通常跟随栈帧,而引用类型变量保存引用,真正的对象实例通常分配在托管堆中。
- 例如局部变量 Player player = new Player() 中,player 是引用,Player 对象实例在堆上;当没有任何引用再指向它时,GC 才会回收它。
2.TCP协议和UDP协议的区别
- TCP 是面向连接、可靠的字节流协议。它通过连接建立、序号、确认应答、超时重传、流量控制和拥塞控制等机制,保证数据尽量可靠、有序且不重复地到达。缺点是协议开销更大,网络波动时可能因为重传和队头阻塞导致延迟增加。
- UDP 是无连接的数据报协议,发送前不需要建立连接,协议头更小,延迟和开销通常更低,但它不保证数据一定到达,也不保证顺序和去重。
- 在游戏开发中,登录、背包、交易、支付、任务领奖和战斗结算这类不能出错的数据通常使用 TCP;角色位置、朝向、实时操作、语音等高频且允许少量丢失的数据更适合 UDP。
- 如果需要 UDP 具备部分 TCP 的可靠能力,可以在业务层增加序列号、确认包、超时重传、去重和乱序处理等机制。
3.TCP协议的可靠性是如何达到的?
-
TCP 的可靠性主要依靠序号、确认应答、重传、校验和、流量控制和拥塞控制来保证。
-
发送方会给数据分段并编号,接收方根据序号判断是否丢包、乱序或重复;收到正确数据后会返回 ACK。发送方如果超时没有收到确认,会进行超时重传;如果收到多个重复 ACK,也会触发快速重传。
-
TCP 还会通过校验和检测数据损坏,通过接收窗口做流量控制,避免接收方缓冲区被写满;通过拥塞控制根据网络情况降低或提高发送速率。
-
因此 TCP 可以向应用层提供一个有序、无重复、尽量完整的字节流。但它的可靠性会带来额外开销,网络差时重传和队头阻塞也可能增加延迟。
4. 内存抖动指什么?如何避免内存抖动
考察内容
- GC 与托管堆分配
- Unity 中的帧率卡顿来源
- 临时对象、装箱、字符串拼接、集合扩容
- 对象池与缓存复用
标准回答
-
内存抖动通常指短时间内频繁分配和回收托管堆对象,导致 GC 频繁执行,从而造成帧时间突然升高和游戏卡顿。
-
在Unity中,常见原因包括在 Update 中频繁 new 临时对象、字符串拼接、频繁创建 List 或数组、LINQ 查询、装箱,以及频繁 Instantiate 和 Destroy 游戏对象。
-
避免方式主要是减少高频路径中的堆内存分配,例如复用 List、数组和 StringBuilder,提前设置集合容量,对子弹、特效、UI Item 等高频对象使用对象池,并避免每帧刷新没有变化的 UI 文本。
-
实际项目中我会通过 Unity Profiler 观察 GC Alloc 和 GC.Collect 的耗时,先定位具体分配来源,再针对性优化,而不是盲目地禁止所有 new。
5. buff 系统中,如何用一个 byte,记录多种buff状态标识
考察内容
- 位运算
- 位标记(Bit Flag)
- 枚举 [Flags]
- 多状态并存的表达方式
标准回答
-
可以使用位标记来记录多种 Buff 状态。因为一个 byte 有 8 个二进制位,所以可以用每一位代表一种 Buff,例如第 0 位表示中毒,第 1 位表示眩晕,第 2 位表示减速。
-
在 C# 中通常会定义一个带 [Flags] 的 enum,每个状态使用 1 < < n 表示。添加 Buff 用按位或,移除 Buff 用按位与加取反,判断 Buff 是否存在用按位与判断结果是否为 0。
-
例如角色同时有中毒和减速,可以保存为 Poisoned | Slowed。这种方式占用内存小,判断速度快,适合记录开关型状态。
-
但如果 Buff 还需要记录持续时间、叠加层数、数值、来源等信息,就需要另外维护 Buff 实例或运行时数据,不能只依赖一个 byte。
6. Unity中使用的是左手还是右手坐标系?我们需要注意什么?
-
Unity 在开发层面通常按左手坐标系理解,X 轴向右、Y 轴向上、Z 轴向前,满足 right × up = forward。
-
实际开发中需要注意几个问题:第一,使用叉乘判断左右方向时,叉乘参数顺序会影响结果方向;第二,旋转的正负方向和坐标轴方向要统一理解;第三,模型从 DCC 软件导入 Unity 时,不同软件的坐标系和轴向可能不同,需要检查模型朝向、缩放和旋转;第四,Unity 底层会根据 DirectX、OpenGL、Metal 等图形 API 做矩阵和坐标转换,所以渲染阶段不能简单套用世界坐标系结论。
7. Unity中鼠标、键盘、触屏、手柄等输入事件会在Update 之前、还是之后、还是同时执行?
-
Unity 中,常规输入会在本帧的 Update 逻辑之前被引擎处理,因此键盘、鼠标、触屏和手柄输入一般在 Update 中读取。像 GetKeyDown、GetMouseButtonDown 这类只在按下瞬间为 true 的输入,不建议主要放在 FixedUpdate 中读取,因为 FixedUpdate 的调用频率和帧率不完全一致,可能导致漏读或重复处理。
-
实际开发中通常是 Update 负责采集输入和记录操作意图,FixedUpdate 只负责执行 Rigidbody 等物理逻辑。
-
如果使用新 Input System,输入更新模式可以配置;默认 Dynamic Update 会在每次 Update 前处理输入,另外也支持 Fixed Update 和 Manual 模式。
8. Unity中场景中一个处于激活状态的物体(场景上只有这一个物体),不能被摄像机渲染出来,可能有几种情况?(至少说出3种可能的情况)
-
GameObject 处于激活状态,只说明它参与生命周期,不代表一定会被摄像机渲染。
-
常见原因包括:第一,物体不在摄像机视锥范围内,例如在摄像机后方,或者超出近远裁剪面;第二,物体所在 Layer 没有被 Camera 的 Culling Mask 勾选;第三,Renderer 被禁用,或者物体本身没有 MeshRenderer、SpriteRenderer 等可渲染组件;第四,材质、Shader、贴图异常,或者颜色 Alpha 为 0;第五,物体被深度关系、Sorting Layer、Order in Layer 或其他物体遮挡;第六,模型法线或三角形绕序反了,被背面剔除;第七,缩放或 Mesh Bounds 异常导致被错误裁剪。
-
实际排查时,我通常先检查 Camera 的 Culling Mask、物体位置和裁剪面,再检查 Renderer、材质和排序关系,最后再排查模型 Bounds、法线和 Shader 状态。
9. Unity制作物理游戏相关功能时,我们采用哪种方式处理位移?为什么?
-
Unity 中只要对象参与物理系统,例如需要碰撞、触发器、重力、摩擦或刚体推挤,我会通过 Rigidbody 来处理位移,并放在 FixedUpdate 中执行,而不是直接修改 Transform。
-
具体选择取决于玩法:需要真实受力和惯性时使用 AddForce;需要直接控制移动速度时设置 Rigidbody 的速度;需要按目标位置平滑移动、但仍参与物理系统时使用 MovePosition。
-
直接修改 Transform 会绕过物理引擎的正常模拟流程,容易造成碰撞检测异常、高速穿透、推挤和摩擦失效等问题。Transform 位移更适合不参与物理的纯表现对象,或者明确的瞬移场景。
10. Unity热更新解决方案中,Lua和ILRuntime方案的本质是什么?
-
Lua 和 ILRuntime 热更新方案的本质,都是在安装包中预先内置一个可以运行热更代码的解释环境,然后把后续需要更新的逻辑作为资源从服务器下载,在运行时执行。它们并不是直接替换已经打包好的原生 C# 代码。
-
Lua 方案是内置 Lua 虚拟机,把业务逻辑写成 Lua 脚本,通过绑定层和 C# 互相调用。它适合脚本化程度较高的项目,但需要维护 Lua 和 C# 的交互边界。
-
ILRuntime 方案则是把热更 C# 代码编译成 DLL,运行时由 ILRuntime 解释 DLL 中的 IL 指令。它的优势是热更层仍然使用 C# 开发,能更自然地复用原有 C# 的类型、工具和开发流程。
-
两种方案都需要将底层框架和热更业务分层:底层能力提前打进包里,容易变化的业务逻辑放到可下载的热更层。对于 IL2CPP 等不能直接 JIT 加载新 C# 代码的平台,这类解释执行方案尤其有意义。
Comments
评论区
欢迎在这里留言交流。