目录
(二)现在有一个 mesh A 和一个 mesh B,在视觉上 mesh A 能完全挡住 B,那么有什么办法可以快速对 B 做一个剔除呢
(三)讲一下 c++ 相对于 c 的优势(当时觉得这里问得很抽象,全面讲了讲两者区别和 cpp 一些 features)
(五)你刚刚说到了多线程下智能指针使用不当会出现问题,这里是指什么呢
(六)unity 的 gc 有了解过吗(我说只知道 c# 的,面试官直接跳过了)
(七)cpp 多态怎么实现的(经典八股吟唱,最难绷的是吟唱到后面他直接打断我喊我别说了)
(十)手撕:有一个 Fibonacci 数列 0, 1, 1, 2, 3....,求 m = Fibonacci (n) % 1e7,数据 n < 1e6。
一、前言
游戏开发领域的面试通常涵盖多个方面的知识,包括编程语言、数据结构、算法、网络通信、游戏开发相关技术等。本文将结合多益网络游戏客户端面经以及其他相关面经,详细分析这些面试中出现的问题及解答思路,为求职者提供参考。
二、多益网络游戏客户端面经解析
(一)自我介绍
简要介绍自己的教育背景、项目经验和技能特长。
(二)项目相关
详细介绍自己参与的项目,包括项目的背景、目标、技术架构、实现过程以及遇到的问题和解决方案。
(三)java 三大特性
- 封装:将数据和操作封装在类中,通过访问修饰符控制对类成员的访问,提高代码的安全性和可维护性。
- 继承:子类继承父类的属性和方法,实现代码的复用,同时可以在子类中进行扩展和重写。
- 多态:同一操作作用于不同的对象可以有不同的表现形式,通过方法重写和方法重载实现。
(四)多态实现原理
多态主要通过方法重写和方法重载实现。在继承关系中,子类重写父类的方法,通过父类引用调用重写后的方法时,实际执行的是子类的方法,这就是多态的一种表现形式。方法重载是在同一个类中,定义多个同名但参数不同的方法,根据传入的参数不同来确定调用哪个方法。
(五)java 垃圾回收算法
- 标记 - 清除算法:首先标记出所有需要回收的对象,然后统一回收这些对象所占用的内存空间。缺点是会产生内存碎片。
- 复制算法:将内存分为两块,每次只使用其中一块,当这块内存用完后,将还存活的对象复制到另一块内存中,然后清理已使用的那块内存。优点是不会产生内存碎片,缺点是内存利用率只有一半。
- 标记 - 整理算法:标记出所有需要回收的对象,然后将所有存活的对象向一端移动,最后清理掉端边界以外的内存。
- 分代收集算法:根据对象的生命周期将内存分为新生代和老年代,针对不同代采用不同的垃圾回收算法。新生代通常采用复制算法,老年代通常采用标记 - 清除或标记 - 整理算法。
(六)链表使用场景优缺点
- 使用场景:
- 动态数据结构:链表的长度可以动态变化,适合存储数量不确定的数据。
- 频繁插入和删除:在链表中进行插入和删除操作只需要修改指针,时间复杂度为 O (1),比数组高效。
- 优点:
- 动态大小:可以根据需要随时增加或减少节点数量。
- 高效的插入和删除:不需要移动大量数据,只需修改指针。
- 缺点:
- 随机访问困难:不像数组可以通过索引直接访问元素,链表需要从头节点开始遍历才能找到特定的节点。
- 占用额外的内存空间:每个节点都需要存储指向下一个节点的指针,增加了内存开销。
(七)进程线程协程
- 进程:是操作系统进行资源分配和调度的基本单位,拥有独立的内存空间和系统资源。
- 线程:是进程中的一个执行单元,共享进程的内存空间和系统资源。线程的创建和切换比进程轻量级。
- 协程:是一种用户态的轻量级线程,由程序员自己控制协程的切换,不需要操作系统进行调度。协程的切换开销非常小,可以在一个线程中同时运行多个协程。
(八)线程安全
线程安全是指多个线程同时访问一个对象或方法时,不会出现数据不一致或错误的情况。实现线程安全的方法有:
- 使用同步机制,如锁(互斥锁、读写锁等)、信号量等。
- 使用不可变对象。
- 使用线程安全的集合类,如 ConcurrentHashMap、CopyOnWriteArrayList 等。
(九)知道的排序算法,说了几个以后问了归并
- 常见排序算法:冒泡排序、插入排序、选择排序、快速排序、归并排序、堆排序等。
- 归并排序原理:采用分治的思想,将数组分成两个子数组,分别对两个子数组进行排序,然后将两个有序的子数组合并成一个有序的数组。
(十)手撕反转链表
- 思路:使用三个指针,分别指向当前节点、前一个节点和下一个节点。遍历链表,将当前节点的 next 指针指向前一个节点,然后移动三个指针到下一个位置,直到遍历完整个链表。
- 代码示例(Java):
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
ListNode next = null;
while (curr!= null) {
next = curr.next;
curr.next = prev;
prev = curr;
curr = next;
}
return prev;
}
(十一)游戏开发相关了解
可以介绍一些游戏开发的知识,如游戏引擎、游戏开发流程、游戏设计原则等。
(十二)互联网工作强度等问题
根据自己的了解和接受程度回答,同时可以询问公司的工作强度和加班情况。
(十三)反问
可以询问关于岗位的具体工作内容、团队技术栈、发展机会等问题。
三、游卡游戏服务端开发面经解析
(一)项目问题(双端检锁等)
详细介绍项目中双端检锁的实现方式、作用以及遇到的问题和解决方案。
(二)项目中对于登录的用户 token 储存在哪里?
根据项目实际情况回答,可能存储在数据库、缓存(如 Redis)、客户端本地存储等地方。
(三)hashmap 的底层实现
HashMap 在 Java 中的底层实现是数组 + 链表(或红黑树)。通过哈希函数将键映射到数组的索引位置,如果多个键映射到同一个索引位置,就形成链表(当链表长度超过一定阈值时会转换为红黑树)。
(四)乐观锁,悲观锁知道吗,详细讲一下
- 悲观锁:假定会发生并发冲突,在访问数据时先获取锁,确保数据不会被其他线程修改。例如在 Java 中使用 synchronized 关键字或 ReentrantLock 实现悲观锁。
- 乐观锁:假定不会发生并发冲突,在更新数据时先检查数据是否被其他线程修改,如果没有被修改则进行更新,否则重试或采取其他策略。例如在 Java 中可以使用 AtomicInteger 等原子类实现乐观锁。
(五)https 怎么保证安全性
HTTPS 通过以下方式保证安全性:
- 加密:使用 SSL/TLS 协议对数据进行加密,防止数据在传输过程中被窃取。
- 身份验证:通过数字证书验证服务器的身份,确保连接的是合法的服务器。
- 完整性校验:使用消息验证码(MAC)对数据进行完整性校验,防止数据在传输过程中被篡改。
(六)对称加密和非对称加密
- 对称加密:加密和解密使用相同的密钥,速度快,但密钥的分发和管理比较困难。
- 非对称加密:使用一对密钥,公钥用于加密,私钥用于解密。密钥的分发和管理相对容易,但速度较慢。
(七)HTTP 报文格式
HTTP 报文分为请求报文和响应报文。
- 请求报文:包括请求行(方法、URL、协议版本)、请求头、空行和请求体。
- 响应报文:包括状态行(协议版本、状态码、状态描述)、响应头、空行和响应体。
(八)看过什么源码
根据自己实际情况回答,可以介绍一些自己看过的优秀开源项目的源码,如 Spring、MyBatis 等,并分享自己从中学到的知识和经验。
四、友塔游戏一面面经解析
(一)自我介绍
简要介绍自己的教育背景、项目经验和技能特长。
(二)先是一串八股吟唱
可能是一些常见的基础知识问题,如数据结构、算法、编程语言特性等。
(三)tcp 和 udp 区别
- 连接性:TCP 是面向连接的协议,在通信之前需要建立连接;UDP 是无连接的协议,不需要建立连接。
- 可靠性:TCP 提供可靠的数据传输,通过确认机制、重传机制和流量控制保证数据的正确传输;UDP 不保证数据的可靠传输,可能会出现丢包、乱序等情况。
- 传输效率:UDP 传输效率高,因为它不需要建立连接和进行复杂的确认机制;TCP 传输效率相对较低,但保证了数据的可靠性。
- 头部开销:TCP 头部开销大,因为它需要包含更多的控制信息;UDP 头部开销小。
(四)tcp 保证可靠传输,拥塞控制
- 可靠传输:
- 确认机制:接收方收到数据后会发送确认信息,发送方收到确认信息后才会继续发送下一部分数据。
- 重传机制:如果发送方在一定时间内没有收到确认信息,会重传数据。
- 流量控制:通过接收方反馈的窗口大小来控制发送方的数据发送速度,避免接收方缓冲区溢出。
- 拥塞控制:
- 慢启动:在开始传输数据时,逐渐增加发送数据的速度,避免网络拥塞。
- 拥塞避免:当网络拥塞时,减少发送数据的速度,避免进一步加重网络拥塞。
- 快重传和快恢复:当接收方连续收到三个重复的确认信息时,立即重传丢失的数据包,而不是等待超时重传。同时,调整拥塞窗口的大小,避免网络拥塞。
(五)tcp 连接建立之后服务器断电会发生什么
如果服务器断电,客户端会在一段时间后检测到连接断开,然后尝试重新建立连接。如果服务器在断电后重新启动,并且使用相同的 IP 地址和端口号,客户端可以重新建立连接。如果服务器在断电后无法重新启动,或者使用了不同的 IP 地址和端口号,客户端将无法重新建立连接。
(六)进程和线程的区别
- 定义:进程是操作系统进行资源分配和调度的基本单位,拥有独立的内存空间和系统资源;线程是进程中的一个执行单元,共享进程的内存空间和系统资源。
- 创建和切换开销:进程的创建和切换开销比线程大,因为进程需要分配独立的内存空间和系统资源,而线程只需要在进程的内存空间中进行切换。
- 通信方式:进程间通信相对复杂,需要使用管道、消息队列、共享内存等方式;线程间通信相对简单,可以直接访问共享的内存空间。
(七)进程间通信方式
- 管道:一种半双工的通信方式,只能在有亲缘关系的进程之间使用。
- 消息队列:一种独立于进程的消息传递机制,可以在不同的进程之间传递消息。
- 共享内存:多个进程可以共享一块内存区域,实现快速的数据交换。
- 信号量:用于实现进程间的同步和互斥。
- 套接字:一种网络通信方式,可以在不同的主机上的进程之间进行通信。
(八)单例模式,线程安全的如何实现
- 饿汉式单例模式:在类加载时就创建实例,保证线程安全。
- 懒汉式单例模式:在第一次使用时才创建实例,需要使用同步机制(如锁)来保证线程安全。
- 双重检查锁单例模式:在懒汉式单例模式的基础上,通过双重检查锁来减少同步的开销。
- 静态内部类单例模式:利用静态内部类的特性,在第一次使用时才创建实例,保证线程安全。
(九)C++ 动态多态和静态多态如何实现
- 动态多态:通过虚函数实现。在基类中声明虚函数,在派生类中重写虚函数,通过基类指针或引用调用虚函数时,根据实际对象的类型决定调用哪个函数。
- 静态多态:通过模板和函数重载实现。模板可以根据不同的类型参数生成不同的函数实现,函数重载可以根据不同的参数类型或参数个数实现不同的函数功能。
(十)重写,重载和隐藏
- 重写(override):在派生类中重新定义基类的虚函数,实现多态。重写的函数必须与基类的函数具有相同的函数名、参数列表和返回类型。
- 重载(overload):在同一个类中定义多个同名但参数不同的函数,根据传入的参数不同来确定调用哪个函数。
- 隐藏:在派生类中定义与基类同名的函数,但参数列表不同。这种情况下,派生类的函数会隐藏基类的函数,通过派生类对象调用该函数时,只会调用派生类的函数,而不会调用基类的函数。
(十一)数据库联合索引如何匹配
联合索引是由多个字段组成的索引。在查询时,如果查询条件中包含联合索引的最左前缀字段,并且查询条件的顺序与联合索引的字段顺序一致,数据库可以使用联合索引进行查询优化。例如,有一个联合索引(A, B, C),如果查询条件是 WHERE A =? AND B =? AND C =? 或者 WHERE A =? AND B =?,数据库可以使用联合索引进行查询。如果查询条件的顺序与联合索引的字段顺序不一致,或者只包含部分字段,数据库可能无法使用联合索引进行查询优化。
(十二)解释一下脏读和幻读
- 脏读:一个事务读取了另一个事务未提交的数据,然后另一个事务回滚了,导致第一个事务读取到的数据是无效的。
- 幻读:一个事务在两次查询之间,另一个事务插入了新的数据,导致第一个事务第二次查询的结果与第一次查询的结果不同,就像出现了 “幻觉” 一样。
(十三)哈希冲突如何解决
- 开放地址法:当发生哈希冲突时,通过一定的探测方法(如线性探测、二次探测等)在哈希表中寻找下一个空闲位置。
- 链地址法:将哈希值相同的元素存储在一个链表中,哈希表中的每个位置存储一个链表的头指针。
- 再哈希法:当发生哈希冲突时,使用另一个哈希函数重新计算哈希值,直到找到一个空闲位置。
(十四)口述算法题
根据题目要求进行算法设计和描述。
(十五)力扣 983 最低票价
力扣 983 题目描述:在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 days
的数组给出。每一项是一个从 1
到 365
的整数。
火车票有三种不同的销售方式:
- 一张为期一天的通行证售价为
costs[0]
美元; - 一张为期七天的通行证售价为
costs[1]
美元; - 一张为期三十天的通行证售价为
costs[2]
美元。
通行证允许数天无限制的旅行。 例如,如果我们在第 2
天获得一张为期 7
天的通行证,那么我们可以连着旅行 7
天:第 2
、3
、4
、5
、6
、7
和 8
天。
返回你想要完成在给定的列表 days
中列出的每一天的旅行所需要的最低消费。
思路:动态规划。定义 dp[i]
表示到第 i
天的最低花费。对于第 i
天,如果这一天不需要旅行,那么 dp[i] = dp[i - 1]
;如果这一天需要旅行,那么可以考虑三种购买通行证的方式,分别计算出三种方式的花费,取最小值作为 dp[i]
的值。
代码示例(Python):
def mincostTickets(days, costs):
n = days[-1]
dp = [0] * (n + 1)
for i in range(1, n + 1):
if i not in days:
dp[i] = dp[i - 1]
else:
one_day = dp[i - 1] + costs[0]
seven_day = dp[max(0, i - 7)] + costs[1]
thirty_day = dp[max(0, i - 30)] + costs[2]
dp[i] = min(one_day, seven_day, thirty_day)
return dp[n]
(十六)力扣 207 课程表
力扣 207 题目描述:你这个学期必须选修 numCourses
门课程,记为 0
到 numCourses - 1
。
在选修某些课程之前需要一些先修课程。先修课程
(十七)力扣 64 最小路径和
力扣 64 题目描述:给定一个包含非负整数的 m x n 网格 grid
,请找出一条从左上角到右下角的路径,使得路径上的数字总和最小。
思路:动态规划。定义dp[i][j]
表示到达(i,j)
位置的最小路径和。对于第一行和第一列的位置,只能从左边或上边过来,所以dp[i][0]
和dp[0][j]
可以直接计算。对于其他位置,可以从左边或上边过来,取两者中的较小值加上当前位置的值作为dp[i][j]
。
代码示例(Python):
def minPathSum(grid):
m, n = len(grid), len(grid[0])
dp = [[0] * n for _ in range(m)]
dp[0][0] = grid[0][0]
for i in range(1, m):
dp[i][0] = dp[i - 1][0] + grid[i][0]
for j in range(1, n):
dp[0][j] = dp[0][j - 1] + grid[0][j]
for i in range(1, m):
for j in range(1, n):
dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
return dp[m - 1][n - 1]
(十八)然后问了几个场景题
根据具体的场景题进行分析和回答,可能涉及游戏开发中的实际问题,如游戏性能优化、网络通信、用户体验等方面。
(十九)反问
可以询问关于岗位的具体工作内容、团队技术栈、发展机会等问题。
五、光子游戏客户端一面凉经解析
(一)讲一下 games101 里面做光栅化代码部分的流程
根据 games101 课程中的内容,光栅化的流程大致如下:
- 顶点处理:将三维空间中的顶点坐标转换为屏幕坐标。
- 三角形设置:确定三角形的边界像素。
- 三角形遍历:对三角形内部的像素进行插值计算。
- 着色:根据插值得到的属性对像素进行着色。
(二)现在有一个 mesh A 和一个 mesh B,在视觉上 mesh A 能完全挡住 B,那么有什么办法可以快速对 B 做一个剔除呢
可以使用遮挡剔除技术。一种常见的方法是层次包围盒(Bounding Volume Hierarchy,BVH)。首先为每个 mesh 构建一个包围盒,然后在渲染过程中,根据摄像机的位置和方向,快速判断一个 mesh 的包围盒是否被另一个 mesh 的包围盒完全遮挡。如果是,则可以剔除被遮挡的 mesh,减少渲染开销。
(三)讲一下 c++ 相对于 c 的优势(当时觉得这里问得很抽象,全面讲了讲两者区别和 cpp 一些 features)
- 面向对象编程:C++ 支持面向对象编程,包括封装、继承和多态,可以更好地组织和管理代码,提高代码的可维护性和可扩展性。
- 模板:C++ 的模板机制可以实现泛型编程,提高代码的复用性。
- 异常处理:C++ 提供了异常处理机制,可以更好地处理程序中的错误情况。
- 标准库:C++ 拥有丰富的标准库,提供了很多实用的功能,如容器、算法、输入输出等。
- 函数重载:C++ 允许函数重载,可以根据不同的参数类型或参数个数定义多个同名函数,提高代码的灵活性。
(四)cpp 的智能指针有缺点吗,它一定是内存安全的吗
- 智能指针的缺点:
- 可能会带来一定的性能开销,因为智能指针需要进行一些额外的管理操作。
- 在某些复杂的情况下,可能会出现循环引用的问题,导致内存泄漏。
- 智能指针不一定是内存安全的:虽然智能指针在很多情况下可以帮助管理内存,避免内存泄漏,但如果使用不当,仍然可能出现问题。例如,在循环引用的情况下,智能指针可能无法正确释放内存。此外,如果智能指针指向的对象被手动释放,而智能指针没有及时更新,也可能导致错误。
(五)你刚刚说到了多线程下智能指针使用不当会出现问题,这里是指什么呢
在多线程环境下,智能指针可能会出现以下问题:
- 竞争条件:如果多个线程同时访问和修改同一个智能指针,可能会导致竞争条件,出现未定义的行为。
- 线程安全问题:智能指针的一些操作可能不是线程安全的,例如
shared_ptr
的引用计数的更新。如果在多线程环境下没有正确地同步这些操作,可能会导致错误。
(六)unity 的 gc 有了解过吗(我说只知道 c# 的,面试官直接跳过了)
如果了解 Unity 的垃圾回收机制,可以介绍一下 Unity 中 C# 的垃圾回收是如何工作的,包括垃圾回收的触发条件、回收算法等。如果不了解,可以在面试后进行学习,了解游戏引擎中的垃圾回收机制对于游戏开发是很有帮助的。
(七)cpp 多态怎么实现的(经典八股吟唱,最难绷的是吟唱到后面他直接打断我喊我别说了)
C++ 多态主要通过虚函数实现。在基类中声明虚函数,在派生类中重写虚函数,通过基类指针或引用调用虚函数时,根据实际对象的类型决定调用哪个函数。
(八)c++ 虚表指针占用多少
在 32 位系统中,通常虚表指针占用 4 个字节;在 64 位系统中,通常虚表指针占用 8 个字节。
(九)字节对齐代码题,32 位上这里占用多少字节:
struct A { char c; bool b; int a; float f; void* p; };
- 字节对齐规则:
- 基本数据类型按照其自身大小进行对齐。
- 结构体的总大小是其最大成员大小的整数倍。
- 分析结构体 A:
char c
占用 1 个字节。bool b
在大多数系统中也占用 1 个字节。int a
占用 4 个字节。float f
占用 4 个字节。void* p
在 32 位系统中占用 4 个字节。
- 计算结构体 A 的大小:
- 首先按照成员顺序排列,
c
和b
可以放在一起,占用 2 个字节(为了满足int a
的对齐要求,需要空出 2 个字节)。 - 然后是
int a
占用 4 个字节。 - 接着是
float f
占用 4 个字节。 - 最后是
void* p
占用 4 个字节。 - 总共占用 16 个字节。
- 首先按照成员顺序排列,
(十)手撕:有一个 Fibonacci 数列 0, 1, 1, 2, 3....,求 m = Fibonacci (n) % 1e7,数据 n < 1e6。
- 思路:可以使用动态规划的思想来求解。定义一个数组
dp
,dp[i]
表示第i
个 Fibonacci 数。dp[0] = 0
,dp[1] = 1
,对于i > 1
,dp[i] = dp[i - 1] + dp[i - 2]
。最后返回dp[n] % 1e7
。 - 代码示例(C++):
#include <iostream>
using namespace std;
int fibonacci(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
int dp[n + 1];
dp[0] = 0;
dp[1] = 1;
for (int i = 2; i <= n; i++) {
dp[i] = (dp[i - 1] + dp[i - 2]) % 10000000;
}
return dp[n];
}
(十一)手撕拓展:数据是 n < 1e18 如何处理
当数据范围增大到n < 1e18
时,使用普通的整数类型可能会溢出。可以考虑使用高精度计算或者矩阵快速幂的方法来求解 Fibonacci 数列。
- 高精度计算:
- 定义一个数组来存储 Fibonacci 数的每一位。
- 模拟加法运算来计算 Fibonacci 数。
- 矩阵快速幂:
- 构造一个矩阵
[[1,1],[1,0]]
,通过矩阵快速幂可以快速计算出第n
个 Fibonacci 数。 - 矩阵快速幂的时间复杂度为
O(log n)
,比普通的动态规划方法效率更高。
- 构造一个矩阵
六、总结
通过对这些面经的分析,可以看出游戏开发岗位的面试涵盖了多个方面的知识,包括编程语言、数据结构、算法、网络通信、游戏开发相关技术等。在准备面试时,需要全面复习这些知识,并结合实际项目经验进行思考和总结。同时,要注意算法题的练习和代码的规范性,提高自己的编程能力和解决问题的能力。希望本文对求职者有所帮助。