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# 代码的平台,这类解释执行方案尤其有意义。