Skip to content

最容易丢分的回答

只说 API,不说原理

标准回答

对,面试里“只说 API,不说原理”是很典型的扣分点。 API 只能说明你会用接口,原理、成本、边界和验证,才能说明你真的能解决项目问题。

interview-api-vs-principle-answer

正确回答模板

不要只说:

用 Instantiate 创建对象。用 Addressables.LoadAssetAsync 加载资源。用 StartCoroutine 开协程。用 Profiler 看性能。

要升级成:

这个 API 解决什么问题。 它底层大概怎么工作。 它有什么性能或内存代价。 它在项目里适合什么场景。 它有哪些坑。 我怎么验证它真的有效。

举例

如果问对象池,不要只说“我写了 Get 和 Release”。 要说:对象池是为了减少频繁创建销毁带来的 CPU 开销和 GC;对象取出时要重置状态,归还时要防止重复回收,切场景时要决定池子清空还是常驻;最后用 Profiler 对比优化前后的 GC Alloc 和 Instantiate 峰值。

如果问资源加载,不要只说“用 Addressables 异步加载”。 要说:异步加载避免主线程长时间卡住,但依赖资源要一起处理;多个模块引用同一资源要做引用计数;释放时不能只 Release 实例,还要确保没有其他对象持有引用;最后用 Memory Profiler 看资源是否真的释放。

面试加分说法

WARNING

我回答 API 类问题时一般不会停在“怎么调用”,而会补充它的底层机制、使用代价、适用场景、常见坑和验证方式。因为项目里真正难的不是调 API,而是知道什么时候该用、什么时候不该用,以及出了问题怎么定位。

只说用了对象池,不说回收和状态重置

标准回答

对,面试里只说“我用了对象池”是不够的。 对象池真正考的是:对象什么时候回收、回收时如何重置状态、如何防止重复回收、切场景时怎么清理。

object-pool-recycle-reset

正确说法

我用了对象池后,会给对象定义完整生命周期: 取出时初始化,使用中监听回收条件,回收前重置状态,最后隐藏并放回池子。

什么时候回收

子弹:命中目标、飞出范围、超过生命周期。 特效:播放结束、被打断、切场景。 怪物:死亡、离开活动区域、波次结束。 UI Item:滑出虚拟列表可见区域。 音效对象:播放完成后回收。

回收时要重置什么

位置、旋转、缩放。 速度、加速度、方向。 血量、伤害、阵营、目标。 动画状态、粒子状态、拖尾状态。 碰撞体开关、刚体速度。 计时器、协程、延迟回调。 事件订阅、委托回调。 owner、target、callback 等引用。

常见坑

重复回收: 同一个对象可能因为命中和超时同时触发回收,所以要有 inPoolisReleased 标记。

脏状态残留: 上一颗子弹的速度、目标、伤害没清干净,下一次复用就会出现奇怪 bug。

事件没取消: 对象回池后还响应事件,可能导致对象被错误激活,也可能造成内存泄漏。

协程没停止: 对象已经回池,旧协程还在跑,过几秒又改了新对象的状态。

切场景没清理: 临时战斗池应该清空;跨场景常驻池要明确归属,避免引用旧场景对象。

面试加分说法

CAUTION

我不会只说“用了对象池”,我会强调对象池管理的是对象生命周期。对象取出时要初始化本次使用的数据,回收时要停止协程、取消事件、清理引用、重置物理和表现状态,并用标记防止重复回收。最后我会用 Profiler 对比 Instantiate / Destroy 峰值和 GC Alloc,证明对象池确实减少了运行时抖动。

只说 Profiler,不说具体看哪个指标

标准回答

对,面试里只说“我会用 Profiler 看”不够。 要说清楚:你遇到什么现象、打开 Profiler 后看哪个模块、哪个指标异常、下一步怎么继续拆。

profiler-specific-metrics

正确说法

如果是卡顿掉帧,我会先看 CPU Usage / Timeline,找尖峰帧,然后看是 Main ThreadRender Thread 还是 GC.Collect 导致的。

如果是 GC 问题,我会看 GC AllocGC.CollectManaged Heap Used,再打开调用栈找是哪段代码分配了内存。

如果是渲染问题,我会看 Rendering 里的 BatchesSetPass CallsTrianglesVertices,还会结合 Frame Debugger 看具体是哪批 DrawCall。

如果是 UI 卡,我会重点看 Canvas.BuildBatchLayout RebuildGraphic RaycasterCanvas.SendWillRenderCanvases

如果是物理卡,我会看 Physics.Simulate、Raycast 数量、碰撞体数量、刚体数量。

如果是加载卡,我会看 Loading、资源加载耗时、AsyncUpload、主线程是否被同步加载阻塞。

如果是内存问题,我会看 Memory Profiler 里的 Managed MemoryNative MemoryTexture MemoryMesh Memory、AudioClip 和对象数量。

面试加分说法

WARNING

我不会只说“用 Profiler 看”。我会先根据现象判断方向:卡顿先看 Timeline 和帧时间,GC 看 GC Alloc 和 GC.Collect,UI 卡看 Canvas.BuildBatch 和 Layout,渲染卡看 Render Thread、SetPass、DrawCall 和 GPU Time,内存问题用 Memory Profiler 看 Texture、Mesh、Managed 和 Native。最后用优化前后的毫秒数、内存值或 GC Alloc 数据证明优化有效。

只说 DrawCall 多,不区分 CPU/GPU

标准回答

对,只说“DrawCall 多”还不够。 DrawCall 多通常更偏向 CPU 提交渲染命令 的问题,但最终卡不卡,还要区分是 CPU 瓶颈还是 GPU 瓶颈。

drawcall-cpu-gpu-distinction

正确说法

我不会只看 DrawCall 数量。 DrawCall 多主要会增加 CPU 向 GPU 提交渲染命令的成本,也会让 Render Thread 压力上升。但如果真正慢的是 GPU,比如 Shader 太复杂、Overdraw 高、阴影贵、后处理重,那单纯降 DrawCall 未必能解决问题。

怎么区分 CPU/GPU

如果是 CPU 瓶颈,常见表现是:

Main ThreadRender Thread 时间高。 Batches / SetPass Calls 多。 降低分辨率后帧率提升不明显。 合批、图集、SRP Batcher、GPU Instancing 后有改善。

如果是 GPU 瓶颈,常见表现是:

GPU Time 高。 降低分辨率后帧率明显提升。 Overdraw、透明物体、阴影、后处理、复杂 Shader 比较重。 DrawCall 不多,但画面依然卡。

Unity 里看哪些指标

Profiler

Main Thread 高,偏 CPU 逻辑或主线程问题。 Render Thread 高,偏提交渲染命令、DrawCall、SetPass。 GPU Time 高,偏 GPU 渲染压力。 WaitForPresent 高,可能是等待 GPU 或 VSync。

Frame Debugger

每个 DrawCall 是谁产生的。 材质和 Shader 是否频繁切换。 透明队列、阴影 Pass、后处理 Pass 是否很多。

RenderDoc

具体 GPU Pass。 Overdraw。 Shader 采样和带宽压力。

面试加分说法

TIP

我不会直接说 DrawCall 多就一定是 GPU 问题。DrawCall 多更常见是 CPU 提交成本高,主要看 Main Thread 和 Render Thread。如果 GPU Time 高,那就要继续查 Overdraw、Shader、阴影、后处理和分辨率。优化时要先定位瓶颈,再决定是做合批、Instancing、SRP Batcher,还是降低透明、阴影和后处理成本。

只说热更新,不说版本、回滚、校验

标准回答

对,只说“我做了热更新”是不够的。 热更新真正考的是:版本怎么管理、补丁怎么校验、失败怎么回滚、线上怎么灰度和监控。

hot-update-version-rollback-verify

正确说法

我做热更新时,不只是客户端下载补丁,而是有一套完整流程: 客户端先拉取版本清单 Manifest,对比本地版本和远端版本,算出需要下载的差异文件;下载完成后校验 Hash、大小和依赖完整性;校验通过后再切换到新版本;如果下载、解压、校验或启动失败,就回滚到上一个稳定版本。

版本怎么管

一般会区分:

AppVersion:安装包版本,涉及代码和底层能力。 ResVersion:资源版本,资源热更新用。 PatchVersion:补丁版本,用于灰度、回滚和追踪问题。

如果热更资源依赖新代码,而用户 App 版本太低,就不能只下资源,要提示强更。

Manifest 里有什么

文件路径。 文件 Hash,比如 MD5 或 SHA。 文件大小。 资源依赖。 资源分组。 版本号。 是否必须更新。 平台和渠道信息。

Manifest 的意义是让客户端知道:哪些文件变了、哪些要下载、下载后怎么校验、当前资源是否完整。

校验怎么做

下载后不能直接使用,要校验:

文件大小是否一致。 Hash 是否一致。 解压是否成功。 依赖是否齐全。 Manifest 签名是否可信。 资源版本和代码版本是否兼容。

否则用户网络中断、CDN 文件异常、下载到脏文件,都可能导致启动崩溃或资源丢失。

回滚怎么做

热更新不能覆盖掉唯一可用版本。 常见做法是保留一个 last good version,也就是上一次稳定可启动的资源目录。

流程是:

先下载到临时目录。 校验通过后标记为候选版本。 启动成功后再切换为当前版本。 如果失败,就切回旧版本。 确认新版本稳定后,再清理旧补丁。

线上还要考虑

灰度发布: 先给少量用户或指定渠道更新,观察错误率。

断点续传: 网络断了不要全部重下,继续下载未完成部分。

失败重试: 下载失败、校验失败要有重试次数和错误提示。

日志上报: 上报下载失败率、校验失败率、启动失败率、回滚次数。

紧急回滚: 线上补丁出问题时,服务端能把目标版本切回旧 Manifest。

面试加分说法

NOTE

我不会只说热更新能下载补丁。我会强调热更新是一个版本管理系统:用 Manifest 描述文件、Hash、大小和依赖;下载后做完整性校验;切换前保留旧版本;失败时回滚;上线时走灰度发布,并监控下载失败率、启动失败率和回滚次数。这样热更新才是可控、可追踪、可恢复的。

只说状态机,不说状态切换边界

标准回答

对,只说“我用了状态机”是不够的。 状态机真正重要的是:状态之间什么时候能切、什么时候不能切、谁优先级更高、切换时怎么清理旧状态。

state-machine-transition-boundaries

正确说法

我设计状态机时,不只是定义 Idle / Move / Attack / Hit / Die 这些状态,还会定义每条状态切换边的规则。

比如:

Idle -> Move:有移动输入。 Move -> Attack:按下攻击键,并且角色当前可攻击。 Attack -> Move:攻击后摇结束,或者允许取消。 Attack -> Hit:受到可打断攻击。 Any -> Die:血量小于等于 0,死亡优先级最高。

状态切换边界要说什么

第一,切换条件。 比如是否在地面、CD 是否结束、动画是否到达可取消帧、当前是否处于硬直。

第二,不可切规则。 比如死亡状态不能切回移动;攻击前摇不能被普通移动打断;受击硬直期间不能释放技能。

第三,优先级。 死亡通常最高,其次是受击、击退、打断、技能、普通移动。否则多个条件同时满足时会乱切。

第四,Enter 和 Exit。 进入状态时初始化计时器、动画参数、速度、碰撞盒。退出状态时清理输入缓存、关闭攻击判定、停止特效、取消临时计时器。

第五,非法切换处理。 如果当前状态不允许切换,要拒绝并记录日志,比如 from=Attack, to=Move, reason=not cancelable,方便调试。

Unity 项目里怎么落地

逻辑状态最好驱动动画参数,而不是完全依赖 Animator 自己跳。 Animator 负责表现,逻辑状态机负责规则。否则容易出现动画已经切了,但逻辑还在旧状态的情况。

战斗状态要特别注意:

攻击前摇。 攻击生效帧。 攻击后摇。 可取消窗口。 受击打断。 翻滚无敌帧。 死亡强制切换。

这些都是“状态切换边界”,不说清楚就很容易出现技能连招错乱、攻击盒没关、角色死亡后还能移动等问题。

面试加分说法

CAUTION

我不会只说用了 FSM。我会说明每条状态边都有 Guard 条件、优先级和 Enter/Exit 生命周期。比如攻击状态不是任何时候都能切走,只有到达可取消帧,或者受到更高优先级的受击、死亡事件时才允许切换。切换时还要清理攻击判定、输入缓存、计时器和特效,避免旧状态污染新状态。

只说资源异步加载,不说依赖和卸载

标准回答

对,只说“资源异步加载”还不够。 异步加载只解决“加载过程不要卡主线程”,但资源系统真正要解决的是:依赖是否完整、是否重复加载、谁在引用、什么时候释放、什么时候真正卸载。

async-resource-load-deps-unload

正确说法

我做资源异步加载时,不只是调用 LoadAssetAsync。 我会先根据资源表或 Manifest 查依赖,比如 Prefab 依赖材质、贴图、Shader、动画、音频等;依赖加载完成后再返回主资源。资源被多个模块使用时,会通过句柄或引用计数管理,业务结束后必须对称释放。只有引用计数归零,才允许卸载资源或 Bundle。

依赖怎么处理

比如加载一个角色 Prefab,它可能依赖:

材质。 贴图。 Shader。 动画。 Avatar。 音效。 特效 Prefab。

如果只异步加载主 Prefab,但依赖没加载完整,可能会出现材质丢失、Shader 变粉、贴图空白、动画缺失等问题。

所以加载流程应该是:

先查依赖。 依赖异步加载。 主资源异步加载。 全部成功后回调业务。 任意依赖失败则整体失败或走降级资源。

引用怎么管理

同一个资源可能被多个系统使用。 比如一个怪物 Prefab,战斗系统、对象池、预加载系统都可能引用它。

如果每个模块都自己加载,会造成重复加载和内存浪费。 如果某个模块提前卸载,又可能导致其他模块还在使用时资源丢失。

所以要做引用计数或句柄管理:

第一次请求时真正加载。 后续请求复用已加载资源。 每个使用者增加引用。 使用结束时释放引用。 引用归零后才进入可卸载状态。

卸载怎么做

Addressables 里要注意 Addressables.Release(handle) 或释放实例。 AssetBundle 里要区分卸载 Bundle 文件和卸载已经加载出来的 Asset。 UnloadUnusedAssets 可以清理未被引用的资源,但代价比较高,不适合频繁调用。

常见卸载时机:

切场景。 战斗结束。 副本退出。 UI 关闭。 对象池清空。 内存压力过高。 资源引用计数归零后一段时间。

常见坑

只加载不释放。 资源会一直常驻,Memory Profiler 里能看到残留。

释放太早。 实例还在场景里用,贴图、材质或依赖资源被卸掉。

依赖没有一起管理。 主资源释放了,但依赖还在;或者依赖被释放,主资源还在用。

重复加载同一资源。 不同系统各自加载一份,导致内存翻倍。

切场景峰值过高。 新场景资源还没加载完,旧场景资源还没释放,峰值内存瞬间上去。

面试加分说法

WARNING

我不会只说“用了异步加载”。我会补充:异步加载解决的是不卡主线程,但资源管理还需要依赖解析、引用计数、句柄释放和卸载策略。加载前先查依赖,多个模块共用同一资源时只加载一份,业务释放时减少引用,引用归零后延迟卸载,并用 Memory Profiler 检查资源是否残留。

只说会 Shader,不懂坐标空间

标准回答

对,只说“我会 Shader”不够。 Shader 真正绕不开的是坐标空间:顶点在哪个空间、法线在哪个空间、光照方向在哪个空间,如果混着算,结果一定会错。

shader-coordinate-space-understanding

正确说法

我写 Shader 时会先确认计算发生在哪个空间。 顶点通常从 Object Space 经过 Model 矩阵变到 World Space,再经过 View 矩阵变到相机空间,最后经过 Projection 矩阵变到裁剪空间,也就是常说的 MVP

c
MVP = Projection * View * Model

光照计算里,法线 N、光照方向 L、视线方向 V 必须在同一个空间里才能点乘。 如果拿对象空间法线去点乘世界空间光照方向,结果就会随着物体旋转、缩放出现错误。

几个重要空间

Object Space: 模型自己的局部空间,顶点原始位置和原始法线通常在这里。

World Space: 场景统一空间,灯光方向、相机位置、角色位置通常会在这里计算。

View Space: 以相机为原点的空间,深度、雾效、某些屏幕效果会用到。

Clip Space: 投影后的裁剪空间,GPU 会用它做裁剪和透视除法。

Screen Space: 屏幕坐标或屏幕 UV,后处理、描边、屏幕特效常用。

Tangent Space: 法线贴图常用空间。法线贴图采样出来的法线通常不是世界空间法线,而是切线空间法线,需要通过 TBN 矩阵转到世界空间或视图空间。

常见坑

法线变换不能简单当成位置变换。 如果模型有非等比缩放,法线要用逆转置矩阵,否则光照方向会歪。

光照向量和法线不在同一空间。 比如 normalOSlightDirWS 直接点乘,就是错的。

法线贴图采样后直接拿来当世界法线。 法线贴图结果一般在 Tangent Space,要用切线、副切线、法线组成的 TBN 转换。

屏幕空间 UV 和普通模型 UV 搞混。 后处理用的是屏幕坐标,普通贴图采样用的是模型 UV。

面试加分说法

IMPORTANT

我不会只说会写 Shader 语法。我会先说明坐标空间链路:顶点从 Object 到 World,再到 View、Clip、Screen;光照计算必须保证法线、光方向、视线方向在同一空间;法线贴图一般在 Tangent Space,需要用 TBN 矩阵转换;法线遇到非等比缩放时还要用逆转置矩阵。这些才是 Shader 正确性的基础。

只说 C++ 智能指针,不懂循环引用

标准回答

对,只说“我会用 C++ 智能指针”不够。 真正要说清楚:shared_ptr 是引用计数,引用计数能自动释放对象,但如果形成循环引用,计数永远降不到 0,对象就不会析构。

cpp-smart-pointer-cycle-reference

核心原理

shared_ptr 表示共享所有权。 每多一个 shared_ptr 指向对象,强引用计数就加 1。 当强引用计数变成 0,对象才会析构。

但如果:

A 里面有 shared_ptr<B>B 里面又有 shared_ptr<A>

那即使外部变量销毁了,AB 仍然互相持有对方,引用计数都不是 0,所以析构函数不会执行,这就是循环引用。

错误写法

c
#include <memory> // 引入 shared_ptr 和 make_shared
struct B; // 前向声明 B,因为 A 里要用 B
struct A { // 定义结构体 A
    std::shared_ptr<B> b; // A 强引用 B
}; // A 定义结束
struct B { // 定义结构体 B
    std::shared_ptr<A> a; // B 强引用 A,和 A::b 形成循环引用
}; // B 定义结束
void Bad() { // 错误示例函数
    auto a = std::make_shared<A>(); // 创建 A,对 A 的引用计数为 1
    auto b = std::make_shared<B>(); // 创建 B,对 B 的引用计数为 1
    a->b = b; // A 持有 B,B 的引用计数增加
    b->a = a; // B 持有 A,A 的引用计数增加
} // 函数结束后外部 a 和 b 销毁,但 A 与 B 互相持有,无法释放

正确思路

一边表示“拥有”,用 shared_ptr。 另一边只是“观察”或“反向访问”,用 weak_ptr

c
#include <memory> // 引入 shared_ptr、weak_ptr 和 make_shared
struct A; // 前向声明 A,因为 B 里要弱引用 A
struct B { // 定义结构体 B
    std::weak_ptr<A> a; // B 不拥有 A,只观察 A
}; // B 定义结束
struct A { // 定义结构体 A
    std::shared_ptr<B> b; // A 拥有 B
}; // A 定义结束
void Good() { // 正确示例函数
    auto a = std::make_shared<A>(); // 创建 A,A 的强引用计数为 1
    auto b = std::make_shared<B>(); // 创建 B,B 的强引用计数为 1
    a->b = b; // A 强引用 B,表示 A 拥有 B
    b->a = a; // B 弱引用 A,不增加 A 的强引用计数
} // 函数结束后强引用计数能归零,A 和 B 可以正常析构

面试加分说法

TIP

我不会说智能指针一定不会内存泄漏。shared_ptr 只能解决普通所有权释放问题,解决不了循环引用。父子关系、观察者关系、缓存回调、双向链表这类场景,如果双方都用 shared_ptr,就可能形成所有权环。通常做法是明确谁拥有谁,反向引用用 weak_ptr,使用前通过 lock() 临时获取 shared_ptr

项目只讲功能,不讲问题和取舍

标准回答

项目只讲功能会像“功能流水账”,面试官听不出你的技术深度。更好的讲法是:功能只是入口,重点讲问题、约束、方案取舍、结果数据和复盘

project-story-problem-tradeoff

回答框架

  1. 项目背景:做了什么游戏或系统,面向什么平台。
  2. 我的职责:我负责哪些模块,边界是什么。
  3. 技术问题:当时遇到什么难点,比如卡顿、GC、资源泄漏、状态混乱。
  4. 方案取舍:为什么选这个方案,不选另一个方案,牺牲了什么,换来了什么。
  5. 实现细节:数据结构、流程、边界处理、异常处理。
  6. 结果数据:帧率、内存、加载时间、GC Alloc、复用效率。
  7. 复盘反思:如果重做一次,会怎么改。

面试说法示例

NOTE

我负责背包和资源加载模块。最开始背包打开会卡顿,因为一次性创建大量 UI Item,导致 Canvas.BuildBatch 和 GC Alloc 升高。后来我把数据层和 UI 表现层拆开,使用虚拟列表加对象池,只创建可见区域的 Item;资源加载改成异步加载,并做引用计数管理。 这个方案的取舍是实现复杂度更高,但换来了更稳定的滚动体验和更低的内存抖动。优化后背包打开耗时从约 400ms 降到 60ms,滚动过程 GC Alloc 基本为 0。复盘的话,我会更早建立 UI 性能规范和自动检测工具。

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

本站访客数0总站访问量0本页访问量0