
2019年秋招那个节点我印象里最深的不是群面也不是面试官追问而是打开快手游戏研发A卷的那一瞬间。第一页不是普通互联网公司那种“三题算法走天下”的套路而是混合了大量C、图形学、网络同步内容的综合试卷。当时我盯着屏幕大概愣了十秒钟脑子里只有一个念头这张卷子不是在筛“会写代码的人”它是在筛“真的能去做游戏研发的人”。虽然现在距离这份卷子已经过去很久但里面很多题目和考点我依然记得很清楚也经常在指导学弟学妹校招时拿出来当案例讲。这篇文章就做一次完整的复盘把那份A卷的考察逻辑、核心题目、解题思路以及我踩过的坑都拆开说清楚。如果你正在准备游戏研发方向的校招或者只是好奇这类岗位笔试到底考什么这份复盘应该能帮你省掉很多自己摸索的时间。我会按“卷面结构—算法与数据结构—C与图形学—网络同步与架构设计—备考策略”这条线来讲中间会穿插我当时真实的答题状态和考后反思尽量把为什么这么考、怎么答才不丢分这件事讲透。1. 拿到A卷时的第一反应题型分布和考察逻辑1.1 快手的游戏研发笔试到底在筛什么人先聊一个很多同学容易忽略的点不同公司、不同岗位的笔试题背后都有一套“岗位画像”。快手游戏研发A卷给我的感觉是它希望筛选出的人至少具备三方面的能力第一扎实的编程基本功和算法思维这是所有技术岗位的底线第二对游戏引擎和底层机制有真实的理解而不是只会在Unity里拖拽预制体第三对多人游戏里的网络同步、性能优化这些“进阶但核心”的话题有自己的思考。为什么这么说因为卷子里的题目分布非常有意思。纯算法题只占一部分剩下的大头给了C内存布局、图形学坐标系变换、渲染管线、网络同步方案设计。这种组合在当年的互联网大厂校招里并不算主流更像是在模拟一个游戏研发工程师日常工作中会遇到的真实问题。你如果只刷LeetCode不碰引擎底层可能编程题能做出来但简答题和选择题会非常难受。1.2 卷面结构一览凭记忆复盘我尽量还原一下当时卷子的结构具体题号顺序可能有出入但题型分布大概是这样的题型题量分值占比考察方向单选题15题左右约30%C语法与内存、数据结构、操作系统、图形学基础多选题5题左右约15%计网、同步方案、并发、Shader相关编程题2题约30%数据结构与算法简答/设计题2题约25%网络同步、游戏逻辑架构从分值上就能看出来它不是“算法定生死”的卷子。选择题和简答题加起来占了一半以上而且这些题几乎没有标准八股文答案更多是考察你对概念的理解深度。比如有一道多选题问的是“帧同步和状态同步的区别”选项里混杂着“确定性”“服务器权威”“断线重连复杂度”“流量消耗”这些关键词如果你只是背过定义没有真正理解两种方案在实战中的取舍很容易多选或少选。这里给一个很实用的建议拿到卷子先花两分钟扫一遍所有题目心里对整张卷子的难度分布和分值分布有个底再决定答题顺序。我当时就是吃了这个亏前面选择题抠得太细导致后面编程题时间略紧。后面第五部分我会专门讲时间分配。2. 算法题复盘最容易超时的是“觉得简单”的题2.1 数组与字符串双指针的隐蔽坑编程题第一道如果我没记错的话是一道字符串相关的题大致要求是给定一个字符串要求去掉所有重复字符并且保持第一次出现的顺序。看起来非常简单很多人第一反应是用一个哈希集合记录已出现字符然后遍历字符串没出现过就加入结果出现过就跳过。这种做法的时间复杂度是O(n)空间复杂度也是O(n)本身没问题但它考察的点其实藏在题目隐含条件里。题目里如果加了“原地修改”“不能开辟额外数组”或者“只能遍历一次”这种限制解法就完全不一样了。我当时遇到的版本是允许用额外空间但要求输出顺序稳定所以哈希集合方案直接可行。不过我在写代码的时候踩了一个小坑忘记了字符范围可能是ASCII扩展字符还是Unicode字符直接用vectorbool去当哈希表用导致越界。笔试环境里编译器不一定报错但运行时会出问题。正确的做法是直接用unordered_setchar或者setchar或者先用ASCII 128的长度做标记。如果是Unicode场景unordered_set更稳妥。我当时还刻意写了一个“避免集合删除操作”的版本因为有人会用set.find加set.erase去维护顺序但那会退化到O(n·k)。真正的考点就是你能不能识别出这是一个“判重保序”问题并且用对数据结构。代码层面核心就是string removeDuplicates(string s) { unordered_setchar seen; string res; res.reserve(s.size()); for (char c : s) { if (!seen.count(c)) { seen.insert(c); res.push_back(c); } } return res; }这道题给我们的启示是笔试里的“简单题”往往不是考你会不会某种奇技淫巧而是考你在有复杂度约束和边界条件的情况下能不能快速写出无bug的代码。很多同学刷题时只追求“思路对”忽略了边界检查但笔试判题是看全部测试用例的一个字符集的坑就能让你这道题拿不到满分。2.2 树与图递归改非递归的考察点第二道编程题比第一题上了一个台阶我记得和二叉树遍历相关但不是简单的输出前序/中序/后序而是有额外条件要求用非递归方式实现某种遍历并且在遍历过程中完成一个统计任务。这种题在游戏研发笔试里出现的频率很高因为游戏里的场景管理、寻路、UI树更新经常会遇到树的遍历而实际工程中递归可能导致栈溢出所以“手写非递归”是加分项。非递归遍历的核心是用显式栈模拟系统调用栈。比如中序遍历你需要先把左子树一路压栈然后弹出访问再处理右子树。代码其实不复杂vectorint inorderTraversal(TreeNode* root) { vectorint res; stackTreeNode* st; TreeNode* cur root; while (cur || !st.empty()) { while (cur) { st.push(cur); cur cur-left; } cur st.top(); st.pop(); res.push_back(cur-val); cur cur-right; } return res; }但注意如果题目要求“只允许O(1)空间”那就得用Morris遍历这是另一个层次的问题。我当年复习的时候只准备了递归和栈模拟没有深入Morris幸好那道题没卡空间复杂度只要求非递归不然我就凉了。后来想想这种“不加解释的约束条件”其实在暗示你对每种遍历的掌握程度。如果你能在答案里写一句“如果是O(1)空间可以改用Morris遍历”会是一个很亮眼的加分项。除了二叉树图相关的搜索题几乎必考。A卷里虽然没有出现大型图搜索编程题但选择题里有一道关于BFS求最短路径的变体在二维网格上从起点到终点有障碍物问最少步数。这道题看似是模板题但选项里暗藏了两个坑一是队列里到底该存坐标还是存“坐标步数”二是访问标记的时机。很多人习惯在出队的时候标记 visited这在普通BFS里不会出问题但在某些变体里会导致同一个节点被重复入队很多次最坏情况下复杂度退化。正确做法是在入队时直接标记 visited。这是一个非常细节但很关键的点游戏里的寻路如果处理不好也会出现大量重复计算。所以建议后辈们在复习BFS时把“入队时标记”当成默认习惯而不是“出队时标记”。2.3 动态规划状态定义比转移方程更重要动态规划那道题我记得跟“背包”有点关系但加了一个限制每种物品的数量是有限的不是无限背包也不是01背包而是多重背包。经典解法是二进制拆分优化或者直接用三重循环但数据范围决定你能否用三重循环。我当时的判断是数据范围不大三重循环能过所以先写了朴素版本再特意提了一句“如果数据范围扩大可以用单调队列优化到O(n·V)”。这种“先拿稳分再展示深度”的策略在校招笔试里非常实用。不过我想说的重点不在多重背包本身而是动态规划题的一个通用原则状态定义比转移方程更重要。如果你把状态定义对了转移方程基本是顺水推舟如果状态定义错了后面所有推导都是白费。比如这道题如果你定义dp[i][j]为“前i个物品中选出总重量不超过j的最大价值”那转移就是标准的背包但如果你定义成“总重量恰好为j”初始化、转移方向、最终答案都会变。我当时做完之后复盘发现自己最开始差点把“恰好”和“不超过”搞混因为题目里有一句话描述得比较绕。这种情况在校招笔试里太常见了命题人会在题目描述里埋一些歧义考察你能不能从业务描述里提炼出准确的数学定义。我的建议是读题时把限制条件用下划线标出来尤其注意“最多”“恰好”“至少”这三类词它们的DP初始化完全不同。// 多重背包朴素版核心代码 vectorint dp(V 1, 0); for (int i 0; i n; i) { for (int j V; j 0; j--) { for (int k 1; k cnt[i] k * w[i] j; k) { dp[j] max(dp[j], dp[j - k * w[i]] k * v[i]); } } }3. C与图形学部分游戏研发特有的送分与送命3.1 内存对齐、虚函数表与智能指针选择题里C占比不低而且考法非常“工程化”不是直接问你“虚函数是什么”而是给出一个结构体让你计算sizeof或者给出一段多继承代码问对象内存布局。我印象最深的一道题是内存对齐struct A { char a; int b; char c; }; struct B { char a; char c; int b; };问sizeof(A)和sizeof(B)分别是多少。答案是12和8。原因在于默认对齐规则下每个成员的对齐数是它自身大小和编译器默认对齐数中的较小值int是4字节对齐所以A里char后面要填充3字节c后面再补充3字节凑到int的倍数而B里两个char连着放总共占2字节后面再补2字节给int对齐整体就是8字节。这道题考的不是你会不会背规则而是你是否真的理解内存布局因为游戏引擎里大量使用结构体打包顶点数据、Uniform Buffer内存对齐没弄好轻则浪费显存重则出现奇怪的渲染Bug。虚函数表那道题也很有代表性。题目大概是给了一个基类和一个派生类各有一个虚函数和一个普通成员变量问sizeof(基类)在32位和64位平台上分别是多少。这题考察的知识点是类里有虚函数时对象内存里会多一个指向虚函数表的指针也就是vptr所以32位下多4字节64位下多8字节。但要注意如果编译器开启了空基类优化某些特殊情况会有细微差别。更进阶的问法是“派生类对象的虚函数表里包含了哪些条目”这就涉及菱形继承、虚继承下的布局了。还有一个必考点是智能指针。快手这道题不是问shared_ptr和unique_ptr的区别而是给出一个场景一个对象被多个系统持有其中一个系统注册了回调另一个系统在异步线程里访问它问用哪种智能指针管理最安全。这个场景在实际游戏开发中特别常见比如战斗逻辑里的技能对象、UI系统里的资源对象。正确方向通常是shared_ptr保证生命周期再配合weak_ptr来打破循环引用同时还要考虑线程安全比如shared_ptr的控制块是线程安全的但对象本身的读写不一定是。这道题没有标准完美答案面试官想看到的是你能否分辨“所有权归属”和“访问安全性”是两件事我在答案里把这两层分开写得分应该不低。3.2 坐标系变换把“左手系”顺清楚图形学部分的选择题和简答题是A卷的重头戏。有一道选择题问的是在左手坐标系中绕Y轴正方向顺时针旋转旋转矩阵长什么样这题如果只是背过右手系的旋转矩阵非常容易选错因为左右手系的旋转正方向定义是相反的。先说结论左手坐标系下绕Y轴正方向看过去顺时针旋转对应的是正角度旋转而右手系里“正方向旋转”是逆时针。很多教材默认右手系导致Unity里用左手系的时候很多人搞混。我当时在现场用了一个笨但很稳的办法把坐标轴比划出来左手拇指指向Y轴正方向四指弯曲的方向就是旋转的正方向然后手动乘一下基向量验证。这种题在笔试里其实不是考你计算能力而是考你有没有真正在引擎里处理过坐标系。你在Unity里定义一个旋转Quaternion.Euler(0, 90, 0)代表的旋转方向和矩阵里的RotationY是同一个东西吗如果没亲手做过坐标变换很容易栽在符号上。另外一道印象很深的题是法线变换。给了一个非均匀缩放的模型矩阵问世界空间下的法线应该怎么变换。标准答案是不能直接用模型矩阵变换法线而要使用模型矩阵的逆转置矩阵。原因是非均匀缩放会改变法线方向只有用逆转置矩阵变换才能保证法线仍然垂直于切平面。这道题我记得是选择题里错误率最高的一道之一因为它需要你用数学推导来验证而不是靠“我记得好像是……”这种模糊记忆。大家可以把这个推导自己动手算一遍过程很简单切向量T经过模型矩阵M变换后是M·T法线N变换后的向量如果仍然垂直于M·T那么应该有(MN)·(MT) 0推出N^T M^T M T 0如果M^T M I正交矩阵那N直接用M变换就行但一般情况下M^T M不是单位阵所以要让N (M^{-1})^T N。这个推导过程本身就是一道完美的笔试解答题写清楚比单纯记结论有价值得多。3.3 渲染管线一次draw call的前半生简答题里有一道让我到现在还记忆犹新的题请描述一个物体从CPU提交到GPU屏幕上显示的完整流程并说明其中哪些环节会影响性能。这道题分值不低而且非常开放考的是你对渲染管线的整体理解。我当时的回答分成了几个阶段首先是CPU侧的场景剔除包括视锥体剔除和遮挡剔除这是减少draw call的第一步然后是把可见物体的顶点数据、索引数据、材质参数、变换矩阵等整理到缓冲区中绑定顶点缓冲区和索引缓冲区接下来是设置渲染状态比如深度测试、混合模式、Shader、纹理等提交draw call之后GPU进入顶点着色器阶段把模型空间坐标变换到裁剪空间然后是光栅化把图元变成像素碎片之后是片段着色器计算颜色、光照、阴影最后经过深度测试、模板测试和混合写入帧缓冲区等待呈现到屏幕。每到一个环节我都会补充一句“这里可能出现的性能瓶颈是什么”。比如CPU侧频繁切换渲染状态导致draw call无法合批顶点数据量过大导致顶点着色器压力上升过度绘制导致片段着色器执行次数爆炸纹理采样带宽受限等等。这种“流程瓶颈”的回答方式既展示了你对整个管线的理解又表明你有性能意识而性能意识恰恰是游戏研发笔试里最想看到的东西。现在回头看这道题其实是一个典型的“知识框架型”问题它不会只考你某一行代码而是考察你脑子里有没有一张完整的渲染地图。如果你只会用引擎API但不知道背后的管线流程这道题很难拿高分。4. 网络同步与架构题这部分决定了你的上限4.1 状态同步与帧同步一道题背后的架构理念A卷的简答题第二道几乎是游戏研发岗位的保留题目请对比状态同步和帧同步的优缺点并说明在什么场景下你会选择哪一种。这道题如果只看表面就是两个概念的对比但想拿高分你需要把它上升到“网络模型与游戏逻辑耦合度”的层面。先梳理一下基础定义。状态同步的核心是服务器维护权威状态客户端发送操作请求服务器计算完结果后把新的状态广播给所有客户端。这种方案安全性高、防作弊能力强逻辑也好扩展但缺点是流量大、响应有延迟感而且服务器压力集中。帧同步的核心是所有客户端同步执行同一份输入指令各自计算同样的逻辑服务器只负责收集和广播输入。这种方案流量小、逻辑表现一致适合格斗游戏、RTS这种对帧率一致性要求高的类型但缺点是对确定性要求极高任何浮点运算差异都可能导致不同步。我当时在答案里除了列出对比还写了一个实际案例MOBA类游戏通常更偏向状态同步因为需要服务器做伤害判定和防作弊但对技能释放的手感要求又很高所以客户端会做表现层插值和预测而像《王者荣耀》这种大规模同屏的玩法也考虑过帧同步的方案因为它能在相同带宽下支持更多人同时在线但需要处理断线重连、加速外挂这些问题。这个案例加分的原因在于它说明我不是在背概念而是真的知道这两种方案在商业项目里是怎么取舍的。如果让我再说一个笔试里的隐藏考点那就是“确定性”这个词。帧同步方案里不止要保证所有客户端输入顺序一致还要保证浮点运算的一致所以很多引擎会规定不能用不同平台的数学库或者统一用定点数。这道题里我特意强调了一句“逻辑层与表现层分离是实现帧同步的前提”因为只有把战斗逻辑里的随机数、时间、物理都做成可复现的多个客户端才能跑到同一个状态。4.2 延迟补偿与快照插值的计算题除了简答题选择题里还有一道关于网络同步的具体计算题当时让我犹豫了好一会儿。题目大致是客户端每隔100ms收到一个服务器的快照某个物体在快照A的位置是(0, 0)在快照B的位置是(10, 0)当前渲染时间戳位于两个快照之间且已经过去了60ms请问对该物体进行线性插值后的位置是多少。这题的数学非常简单插值位置 0 (10 - 0) * (60 / 100) 6。但选项里故意给了一些干扰项比如“10”“4”“0”对应的是插值方向搞反、时间比例算错、直接使用上一个快照位置这些常见错误。考完后我复盘时想明白了一个点这道题看似在考“lerp公式”其实在考你是否理解“渲染帧率”和“网络快照频率”是不一致的。如果快照频率是10Hz而渲染帧率是60FPS那么渲染层必须在两个快照之间通过插值生成平滑的中间帧否则画面就会出现肉眼可见的卡顿和跳变。再往深一层说如果题目加上“客户端也发送操作给服务器”的条件那还可以升级成客户端预测服务器回滚的经典方案。客户端在发送操作后不等服务器确认直接开始移动服务器发现位置不对时用权威快照纠正客户端。这里就引入了“延迟补偿”的概念服务器在判定命中时会把玩家拉回到某个历史时间点去判断而不是用当前状态。这其实是一整套面试官非常喜欢继续追问的内容笔试里用一道小计算题做引子面试时就会不断加码。5. 考后复盘如果重来一次我会怎么复习5.1 时间分配上的教训说实话如果把整张A卷的难度做一条曲线它不是从易到难而是“前半段选择题平稳中段编程题波动最后简答题突然拔高”。我当时在选择题上花了将近45分钟因为很多多选题都拿不准反复权衡。结果到了最后简答题时间只剩不到30分钟导致状态同步那道题虽然会写但写得很仓促没有充分展开。现在回想起来这是一个非常典型的失误。理想的时间分配应该是选择题控制在25到30分钟不会的先标记跳过编程题每道20到25分钟满分是60分钟左右简答题预留至少40分钟因为这类题需要组织语言、画对比表、写关键代码片段。如果选择题遇到读了30秒还没思路的直接凭第一感觉选一个并标记后面如果有多余时间再来纠结。考试不追求每道题都对而是追求总分最大化把时间花在有把握拿分的题目上永远是第一原则。5.2 对后来者的具体建议结合这张A卷我给准备游戏研发校招的同学几条具体建议第一LeetCode还是要刷但不用只刷困难题中频题才是主力。重点覆盖字符串、双指针、二叉树遍历、BFS/DFS、DFS回溯、简单DP、图的最短路。游戏研发笔试的算法题通常不会太偏门但会在边界条件上设坑所以平时做题强迫自己把所有边界想清楚比追求题数更重要。第二C不能停留在语法层面要往内存模型和对象模型上补。虚函数表、内存对齐、智能指针、move语义、const correctness这些是选择题高产区。推荐看《深度探索C对象模型》和《Effective Modern C》里智能指针那几章不用全看针对笔试考点看就行。第三图形学这块至少要理解MVP矩阵的推导、坐标系的区别、渲染管线的完整流程、光照模型的基本原理。不要求手写一个光栅化器但要有能力在白板上画出流程并标明瓶颈。如果对渲染管线的认识还停留在“调用DrawPrimitive”那笔试的时候会非常被动。第四网络同步是很多人的盲区因为它平时写业务代码很难接触到。建议自己搭一个简单的帧同步Demo哪怕只是在局域网里跑两个客户端用相同的随机种子和输入序列模拟同步逻辑这个过程能帮你真正理解确定性和状态复现。然后再看一些商业游戏的同步方案分享网上公开的技术博客很多做笔记的时候把“场景—方案—取舍”三个维度连起来记。第五笔试前最好做一两次完整的模拟。找一份往年真题或者自己给自己出一份混合卷严格按考试时间走一遍。模拟的时候用和实际考试一样的节奏不许中途刷手机这样能让你提前适应时间压力也能暴露自己哪类题最容易超时。我后来给学弟学妹做模拟时发现大多数人超时都超在选择判断上原因就是“每个选项都想搞清楚”但考试不需要你当学术研究只需要你用最少时间拿最多分。还有一个小技巧是我考完才顿悟的简答题里如果有“对比”类问题不管题目有没有要求都可以画一张表。状态同步和帧同步、AABB和OBB、RTTI和反射这些对比类知识用表格呈现阅卷人一眼就能看到你的思路结构比整页文字更容易拿分。别用太花哨的排版就是Markdown表格那种对齐方式清晰最重要。现在再看2019年这套快手游戏研发A卷其实它的风格和后来的很多游戏公司笔试题是一脉相承的不追求偏题怪题而是用常规考点组合出贴近真实工作的场景。它真正筛掉的是那些只在搜索引擎里见过“游戏研发”这个词却没有真正尝试去理解引擎、渲染、同步这些底层知识的候选人。如果你现在也在准备这一方向的校招我建议你放下“背题”的念头多去问自己一个为什么为什么状态同步需要服务器权威为什么法线变换要用逆转置矩阵为什么一次draw call会有性能开销。把这些为什么都弄明白了你再去打开任何一份游戏研发笔试题心态都会完全不一样。