个人Unity自用面经(未完)
目录标题
- 1.在 2D 平台跳跃游戏项目中,你使用了对象池来生成和回收怪物包含阵亡的动画预制件。在对象池回收对象时,如何确保动画状态被正确重置,避免下次使用时出现异常?
- 2.在僵尸吃脑子模拟项目中,你创建了继承于IAspect的aspect,并通过RefRW和RefRO定义访问的属性。能详细说说在实际使用中,RefRW和RefRO有什么区别吗?在什么场景下分别使用它们?
- 3.在 2D 平台跳跃游戏项目中,你使用对象池来生成和回收怪物预制件。在对象池的设计与实现过程中,如何确定对象池的初始容量和最大容量呢?
- 4.在 3D 回合制策略游戏中,你使用了状态机实现敌人的 AI,并且 AI 的行为带权。在不同情况下,不同动作的权值是如何确定的呢?
- 5.在僵尸吃脑子模拟项目里运用了 ECS 架构,你也清楚 ECS 架构适合并行计算。那在实际开发中,是怎样去利用 ECS 架构实现并行计算,进而提升游戏性能的呢?
- 6.在 2D 平台跳跃游戏项目中,你使用了 TileMap 瓦砾地图系统构建地图。在使用过程中,如何实现地图的动态加载与卸载,以优化游戏内存占用?
- 7.在 3D 回合制策略游戏中,你使用了状态机来管理角色的行为状态。当角色的状态比较复杂,存在多种状态之间的相互转换,并且有一些状态还存在子状态时,你是如何设计状态机以确保逻辑清晰,易于维护和扩展的呢?
- 8.在僵尸吃脑子模拟项目里,你运用了 ECS 架构。当游戏场景中僵尸数量极多,比如上万只时,可能会面临性能瓶颈。你打算采取哪些策略来优化 ECS 架构下大规模僵尸的性能表现呢?
- 9.在你参与的 “3D 回合制策略游戏” 项目里,你使用了状态机实现敌人的 AI 且行为带权。在游戏运行过程中,如果玩家做出了一个游戏设计师未曾预料到的操作,导致敌人 AI 的权值计算出现异常,你会如何排查和解决这个问题?
- 10.在 “僵尸吃脑子模拟” 项目里,你使用了 ECS 架构并创建了多个继承于ISystem的系统。当多个系统之间存在数据依赖关系时,你是如何管理和协调这些依赖关系,以确保系统执行顺序正确且数据一致性的?
豆包 中等偏上—优秀 评价
1.在 2D 平台跳跃游戏项目中,你使用了对象池来生成和回收怪物包含阵亡的动画预制件。在对象池回收对象时,如何确保动画状态被正确重置,避免下次使用时出现异常?
答:我在泛型对象池定义之初就设置了一个私有的抽象Reset函数,对于后续继承它的对象池必须要实现它,然后在对外公开的回收预制体函数ReturnToPool中调用这个Reset函数,这样来保证他们状态复原,避免异常。比如我在Reset中重新设定这个预制体的动画机中所有的转换条件参数复原,Transform属性复原,并且在Reset函数里使用了Try和catch来捕获重置失败的预制体。
2.在僵尸吃脑子模拟项目中,你创建了继承于IAspect的aspect,并通过RefRW和RefRO定义访问的属性。能详细说说在实际使用中,RefRW和RefRO有什么区别吗?在什么场景下分别使用它们?
答:Ref RW对应的就是ref关键字, read and write,RO 对应的就是 read only,意思就是前者是一个可读写的属性,后者是一个只可读的属性,这要根据需求来用,比如我项目里写了僵尸的行走速度,我不希望他改变所以我把aspect里面对僵尸速度属性的引用设置为只读的意味着我不希望改变他,比如项目中墓碑的Transform属性,我希望每个墓碑生成的位置角度是不一样的所以我设置他为可读写的。
3.在 2D 平台跳跃游戏项目中,你使用对象池来生成和回收怪物预制件。在对象池的设计与实现过程中,如何确定对象池的初始容量和最大容量呢?
答:初步预估对象池容量
统计游戏数据:在设计阶段,仔细分析游戏场景和玩法,统计出游戏中不同类型对象(如怪物、子弹、道具等)同时出现的最大数量和常见数量。例如在一个射击游戏中,统计一场战斗里最多会同时存在多少颗子弹,一般情况下又会有多少颗子弹。
考虑扩展系数:为了应对游戏中可能出现的特殊情况,如玩家触发特殊技能、进入特殊关卡等导致对象数量突然增加,需要在最大数量的基础上乘以一个扩展系数(通常为 1.2 - 1.5),以此确定对象池的最大容量。初始容量可以设置为常见数量的均值。
游戏测试阶段的性能监测
选择合适的监测工具:在 Unity 中,可以使用内置的 Profiler 工具来监测对象池的使用情况,它能提供详细的内存使用信息、对象创建和销毁的频率等数据。
关注关键指标:重点关注对象池的扩容次数、闲置对象数量、对象创建和销毁的时间等指标。如果对象池频繁扩容,说明初始容量设置过小;如果大部分时间对象池都有大量闲置对象,说明最大容量设置过大。
根据监测数据进行动态调整
增大初始容量:如果发现对象池频繁扩容,导致游戏性能下降,可以适当增大初始容量。例如,将初始容量增加 20% - 30%,然后再次进行测试,观察性能是否有所改善。
减小最大容量:如果对象池大部分时间都有大量闲置对象,占用了过多的内存,可以考虑减小最大容量。每次减小的幅度不宜过大,建议在 10% - 20% 之间,然后重新测试,确保不会影响游戏的正常运行。
4.在 3D 回合制策略游戏中,你使用了状态机实现敌人的 AI,并且 AI 的行为带权。在不同情况下,不同动作的权值是如何确定的呢?
答:
我的项目中敌人和友军都是继承自player这个父类的,所以他们能使用的动作是一样的,对于敌人他有移动,射击,手雷。3个动作,我把移动的权值设置成10 - 可攻击范围内敌人的数量 * 3,这样如果范围内有敌人那么他们就不更容易去移动,对于射击权值就是 8,手雷权值是8 + (可攻击范围内敌人的数量 - 1) * 2,于是在有敌人且只有一个的情况下会使用射击,有敌人且有多个的情况下会使用手雷,没有敌人的情况下会使用移动,同时攻击还跟敌人剩余的血量有关,对于移动还要计算每个可到达的地点的权值不同于之前的权值系统,它会优先选择更多可接触敌人的地点到达,这些其实都只和设计者的需求来设定。
5.在僵尸吃脑子模拟项目里运用了 ECS 架构,你也清楚 ECS 架构适合并行计算。那在实际开发中,是怎样去利用 ECS 架构实现并行计算,进而提升游戏性能的呢?
答:
首先建立一个Job : IEntityJob,把这个job需要用到的所有东西比如EntityCommandBuffer.ParallelWriter如果不是并发的那么不用使用ParallelWriter(ecb用于更改实体组件,生成实体等)可以暂存某些指令等到某个时间点一起执行以免出现并发的线程争夺和数据错读问题,还有一些数据,或者实体。然后实现这个Job的Execute方法,如有需要还要提供sortKey这个参数,在调用这个作业的时候会自动检索含有Execute带有的组件的实体,并且在运行的时候给他发配一个sortKey。最后只需要在调用这个任务的system中new这个job,然后给他提供所需要的参数比如使用SystemAPI.Getsingleton,GetComponent等等,用来new这个job并且调用它,需要注意的是在EntityCommandBufferSystem创建EntityCommandBuffer的时候要AsParallelWriter否则会报错,这里state作用是在非托管世界申请一个命令缓冲区并且视为并发写者来缓存并执行并发的命令,因为要并发执行,最后在new的尾处.ScheduleParallel()进行并发执行任务。
避免在一个任务中修改另一个任务需要读取的数据
6.在 2D 平台跳跃游戏项目中,你使用了 TileMap 瓦砾地图系统构建地图。在使用过程中,如何实现地图的动态加载与卸载,以优化游戏内存占用?
答:
地图分块处理
划分地图区域:把整个游戏地图按照一定规则划分成多个小的地图块,比如以正方形或矩形区域为单位。每个地图块都可以独立加载和卸载。例如,在一个大型的 2D 平台跳跃游戏世界中,可以将其划分为多个 10×10 个 Tile 的地图块。
确定加载范围:依据玩家当前所在的位置和视野范围,确定需要加载的地图块。一般来说,除了玩家当前所在的地图块,还需要加载其周围的一些地图块,以保证玩家移动时地图的连贯性。比如,玩家在一个地图块中,那么可以同时加载其上下左右以及四个对角方向相邻的地图块。
实现动态加载与卸载机制
加载逻辑:当玩家接近某个未加载的地图块时,触发加载操作。可以使用 Unity 的场景加载 API(如 SceneManager.LoadSceneAsync )来异步加载地图块。异步加载不会阻塞主线程,能保证游戏的流畅性。在加载时,还可以显示一个加载提示,提升用户体验。
卸载逻辑:当某个地图块远离玩家的视野范围,并且在一段时间内不会被玩家访问到时,触发卸载操作。使用 SceneManager.UnloadSceneAsync 来异步卸载地图块,释放内存。
缓存机制
临时缓存:为了避免频繁的加载和卸载操作,可以设置一个缓存区,将最近使用过的地图块暂时保留在内存中。当玩家再次需要访问这些地图块时,可以直接从缓存中获取,而不需要重新加载。
缓存淘汰策略:由于缓存区的容量是有限的,当缓存区达到最大容量时,就需要移除一些数据来为新的数据腾出空间。常见的淘汰策略有先进先出(FIFO)、最近最少使用(LRU)等。
7.在 3D 回合制策略游戏中,你使用了状态机来管理角色的行为状态。当角色的状态比较复杂,存在多种状态之间的相互转换,并且有一些状态还存在子状态时,你是如何设计状态机以确保逻辑清晰,易于维护和扩展的呢?
答:我使用状态机系统来维护我的状态,状态机内部只有 state currentstate,两个公开的方法public void changeState(State stateName)和public void Initial,然后每个state都继承自Istate必须实现Istate里的抽象方法,Enter,Exit,Update,然后在FSM类里创建一个枚举类State用于枚举每个状态的名字,和一个字典关联 State state 和 Istate theState,然后FSM里包含对状态机的声明,并且有AddState方法用于往字典里添加状态,在声明fsm变量的脚本中的Onupdate调用currenState.Update,这样就可以确保状态之间的模块化和解耦而且扩展方便,维护方便。子状态也同样可以这样使用和添加,确保同一个时间只有一个状态在执行。
8.在僵尸吃脑子模拟项目里,你运用了 ECS 架构。当游戏场景中僵尸数量极多,比如上万只时,可能会面临性能瓶颈。你打算采取哪些策略来优化 ECS 架构下大规模僵尸的性能表现呢?
答:
空间分区与裁剪:
将游戏场景划分为多个空间区域,比如使用**四叉树(2D 场景)或八叉树(3D 场景)**结构。为每个僵尸分配所属的区域,当进行渲染、碰撞检测或 AI 计算时,只处理玩家所在区域及相邻区域内的僵尸。例如,玩家在一个特定房间内,只计算该房间及其相邻房间内的僵尸,避免对整个场景中所有僵尸进行不必要的操作。
对于视野范围外的僵尸,不进行渲染和复杂的 AI 计算,只保留基本的位置追踪等轻量级信息,当僵尸进入玩家视野范围时再进行完整的处理。
批处理与合并:
在渲染方面,将多个僵尸的渲染数据合并成一个批次进行绘制。利用 GPU 的批处理功能,减少 Draw Call 的数量。比如,可以将相同类型、相同材质的僵尸的顶点数据、索引数据等合并,一次性提交给 GPU 进行渲染,提高渲染效率。
对于僵尸的物理碰撞检测,可以将多个僵尸分组,对组内的僵尸进行统一的碰撞检测处理,而不是逐个僵尸进行检测,减少碰撞检测的计算量。
优化组件与数据结构:
确保僵尸的组件设计简洁,只包含必要的数据和功能。避免在组件中存储过多冗余或不常用的数据。例如,如果僵尸的某个属性只在特定情况下使用,可以考虑在需要时动态获取,而不是一直存储在组件中。
选择合适的数据结构来存储僵尸相关的数据。例如,使用数组而不是链表来存储僵尸列表,因为数组在随机访问和遍历方面通常具有更好的性能。同时,可以对数据进行预排序,以便在进行某些计算(如距离排序)时提高效率。
并行计算与多线程优化:
充分利用 ECS 架构的并行计算优势,将僵尸的 AI 计算、物理模拟等任务拆分成多个并行任务,利用多核 CPU 进行加速。例如,为每个僵尸的 AI 行为计算创建一个 Job,通过 ScheduleParallel() 方法并行执行这些 Job。
注意避免多线程之间的资源竞争和数据冲突。合理使用锁机制或无锁数据结构来保护共享资源。例如,对于僵尸的生命值等共享数据的修改,使用线程安全的方式进行操作。
LOD(Level of Detail)技术:
为僵尸设置不同的细节层次模型。当僵尸距离玩家较远时,使用低细节模型,减少模型的顶点数量和纹理复杂度,降低渲染成本。当僵尸靠近玩家时,切换到高细节模型,提供更好的视觉效果。
同样,对于僵尸的 AI 行为也可以设置不同的细节层次。例如,远处的僵尸可以采用简单的巡逻行为,而近处的僵尸则执行更复杂的追逐和攻击行为。
9.在你参与的 “3D 回合制策略游戏” 项目里,你使用了状态机实现敌人的 AI 且行为带权。在游戏运行过程中,如果玩家做出了一个游戏设计师未曾预料到的操作,导致敌人 AI 的权值计算出现异常,你会如何排查和解决这个问题?
答:
我应该会在敌人AI采取行动的使用使用Try和Catch去捕捉错误,如果没有捕捉到说明没有语法上或者访问越界等的问题而是逻辑上的问题,那么如果是后者只能去跟随敌人状态机进行一步一步的调试,如果它引起了空间或者性能问题那么可以使用性能检测工具profile来检测哪个函数或者哪个部分出现了内存性能异常。
10.在 “僵尸吃脑子模拟” 项目里,你使用了 ECS 架构并创建了多个继承于ISystem的系统。当多个系统之间存在数据依赖关系时,你是如何管理和协调这些依赖关系,以确保系统执行顺序正确且数据一致性的?
答:
在ECS架构中不像Unity自带一个脚本执行顺序的调整功能。在ECS架构中你运行项目的时候如果先后顺序有问题那么大概率会报错并且无法运行,届时你可以到Windows->Entities->System中查看每个系统的执行时间,但是这里不像unity原生开发框架可以直接拖动,你必须到每个系统中使用UpdateInGroup(typeof(InitializationSystemGroup))此处可以替换为很多ECS内定好的系统组或者UpdateAfter,Before某个System来调整它们的执行顺序。通过这个动作来保证依赖的关系不会出现问题,比如我项目中僵尸生成系统就定义了[UpdateAftertypedef(TombStoneSpawnSystem)]因为我僵尸生成的位置依赖于墓碑生成的位置。
避免循环引用导致相互依赖,可以设置中间层来接收相互依赖之间的数据避免直接依赖来解耦,或者调整循环依赖之间的关系,让其中一个不再引用环中的数据。