你在项目中提到使用单例模式和观察者模式,能否具体说明它们的应用场景?

  • 在2D类银中使用单例模式管理全局系统,如音效管理器,技能管理器,确保全局唯一实例以及方便各处调用。观察者模式则是用于解除模块间的耦合程度,比如当玩家的血量发生变化时,UI系统会根据事件自动更新血条,而不需要直接引用玩家对象。

追问:单例模式如何避免线程安全问题?观察者模式与Unity事件系统有和异同?

  • 可以通过加锁或者静态构造函数实现。观察者模式更加灵活,可以自定义事件参数,Unity事件适合编辑器的可视化配置
维度 观察者模式(经典) Unity 事件系统
实现方式 通常通过接口定义(如 Observer/Subject),手动维护订阅列表。 基于 UnityEvent 类,可视化配置(Inspector 中可直接绑定)。
类型安全 编译期类型检查严格(需显式定义事件参数类型)。 支持泛型 UnityEvent<T>,但通过反射实现,部分错误可能在运行时暴露。
使用场景 通用设计模式,适用于任何编程语言 / 框架。 深度集成 Unity 生态,支持与组件(MonoBehaviour)直接绑定。
扩展性 需手动实现事件管理逻辑(如批量取消订阅)。 内置序列化、持久化支持,可与 Unity 编辑器无缝协作。
性能 轻量,无额外开销。 因反射和序列化,性能略低于纯代码实现的观察者模式。

C#的协程在项目中的实际应用是什么?

  • 在2D类银中使用协程来临时修改玩家的状态,比如在项目中玩家受到易伤的情况时,临时改变玩家的护甲,并且在等待一段时间后,恢复回去,使用协程可以简单实现这种状态修改的逻辑。()

追问:yield return null 和 yield return waitForSecond的区别,协程的底层原理是什么?

  • yield return null 是暂停协程,等待下一帧后继续执行后续,yield return waitForSecond等待具体时间后继续执行。协程底层通过迭代器和状态机实现,依托 Unity 帧循环机制运行,是一种 “伪并发” 而非真多线程,适合处理游戏中需要分阶段或延迟执行的逻辑。

你熟悉委托和事件,能否用代码实例说明如何实现一个简单的回调系统?

  • public class DamageSystem : MonoBehaviour
    {
        // 1. 定义委托(规定回调函数的格式)
        public delegate void DamageDelegate(int damage);
        
        // 2. 定义事件(基于委托,作为回调的触发点)
        public static event DamageDelegate OnDamageTaken;
    
        // 受到伤害时调用
        public void TakeDamage()
        {
            Debug.Log("受到伤害!");
            // 3. 触发事件(调用所有注册的回调函数)
            // ?. 确保没有订阅者时不会报错
            OnDamageTaken?.Invoke(10); 
        }
    }
    
    // 订阅者1:处理伤害数值显示
    public class UIDisplay : MonoBehaviour
    {
        private void OnEnable()
        {
            // 注册回调
            DamageSystem.OnDamageTaken += ShowDamageUI;
        }
    
        private void OnDisable()
        {
            // 取消注册(必须做,否则可能报错)
            DamageSystem.OnDamageTaken -= ShowDamageUI;
        }
    
        // 回调函数(符合DamageDelegate的格式)
        void ShowDamageUI(int damage)
        {
            Debug.Log($"UI显示:受到 {damage} 点伤害!");
        }
    }
    

追问:事件和普通委托的区别?为什么事件更加安全?

  • 事件只能在声明它的类内部触发(Invoke),外部代码无法强制调用事件,委托外部通过 = 赋值会直接清空正之前的所有订阅者。同时事件不允许外部使用 = 赋值,只能通过 +=/-= 添加 / 移除订阅者,避免了意外覆盖原有回调的风险。事件 = 委托 + 访问权限控制

你在2D-RPG项目中用有限状态机(FSM)管理角色状态,具体是如何实现的?

  • 使用一个PlayerState公共类以及PlayerStateMachine作为中间类(不同状态切换的中间件)进行管理,在PlayerStateMachine中定义一个currentState变量作为当前玩家活跃的状态,提供一个Initialize方法来实现初始化玩家的默认状态为Idle,以及实现玩家状态的切换ChangeState方法(包含当前状态的退出,根据玩家输入来设置新的活跃状态currentState,以及当前活跃状态的enter方法也就是进入该状态的方法),每个状态的update方法中实时检测玩家的输入情况,根据玩家的输入来改变人物的状态机(也就是调用PlayerStateMachine中的ChangeState方法)。PlayerState中则是定义每个状态的基本结果和行为,每个不同的状态都封装为一个类并且继承PlayState类。在检测到玩家状态发生改变时,调用PlayerStateMachine中ChangeState方法进行状态的切换,包括原来状态的退出以及新状态的进入,同时执行相应的动画播放以及行为逻辑

追问:如果状态过多导致代码臃肿,你会如何优化?状态模式(State Pattern)和FSM的区别?

  • 状态过多时改用状态模式(每个状态封装成类)层级状态模式,将不同的状态进行分类,设置父级,根据父级来实现状态机的转换,FSM适合简单逻辑,状态模式更易扩展。

Cinemachine在项目中用于相机跟随,能否说明其核心组件(如Virtual Camera)的作用?

  • 在Virtual Camera中的核心组件CinemachineVirtualCamera存在一个Follow用于实现相机跟随,将player拖入Follow中则实现了相机的跟随

追问:如何实现相机平滑跟随和边界限制?

  • Damping控制跟随延迟,Dead Zone防止微小抖动。

你提到使用NavMeshAgent实现敌人寻路,NavMesh是如何生成的?动态障碍物如何处理?

  • 通过烘焙场景静态物体生成NavMesh。动态障碍:添加NavMeshObstacle组件,设置Carve属性实时更新网格。

你在项目中使用了ScriptableObject存储物品数据,为什么选择它而不是JSON或Excel?

ScriptableObject适合存储游戏配置数据,因为:

  • 编辑器友好:可以直接在Unity Inspector中编辑,无需额外工具。

  • 运行时高效:数据直接存储在内存中,读取速度快于JSON反序列化。

  • 类型安全:强类型检查,避免Excel的字符串解析错误。

  • 持久化问题:通过EditorUtility.SetDirty()标记修改,或结合JsonUtility保存到文件

  • 追问ScriptableObject在运行时修改后如何持久化?如何避免多个实例数据不一致?

​ 避免数据不一致:用ScriptableObject.CreateInstance创建运行时实例,原始数据作为模板。

Cinemachine的Impulse功能如何实现屏幕震动?如果想让不同强度的震动叠加,你会怎么设计?

  • 给需要添加震动的CinemachineImpulseSource组件,设置Vector3变量来设置震动强度,针对不动攻击来调用不同的方法控制震动强度

问题:NavMeshAgent的Avoidance Priority和Obstacle Avoidance Type参数对AI行为有什么影响?

  • Avoidance Priority:优先级高的Agent会优先避让(0最高,99最低)。
  • Obstacle Avoidance Type
    • None:完全无避障,性能最佳。
    • Low/Med/High Quality:计算精度递增,CPU消耗递增。
  • 实战:大量敌人用Low+优先级分流,Boss用High

Unity的Addressables系统如何实现依赖加载?与AssetBundle手动管理依赖有何优劣?

Unity 的 Inspector 是如何动态显示和修改 GameObject 的 Transform 或其他组件的属性的?

  • 通过反射机制动态读取和修改组件的属性值
  • Unity Inspector 通过 反射(Reflection)动态读取/修改组件字段,结合 序列化(Serialization)持久化数据,并利用 Property Drawers 和 Custom Editors 优化渲染,最终实现动态显示和编辑 GameObject 属性。

你在CrossRoad项目中用对象池优化敌人生成,对象池的实现原理是什么?

  • 在项目中使用Dictionary<string,List>来实现对象池模块,如果需要某个对象,先从对象池中寻找空闲的对象,如果存在则直接取出服用,不存在,则创建新的对象放入缓存中。当对象不需要使用时,不需要销毁而是放入对象池中。

  • 追问:对象池的容量如何动态调整?哪些场景不适合用对象池?

  • 动态扩容 + 渐进式销毁 或者 上线控制 + 自动清理。对象需频繁创建 / 销毁状态重置简单复用率高时,对象池才能发挥最大价值;如果创建难度大,创建,销毁频率低的不建议使用。

UGUI的背包系统如何避免频繁重建(Rebuild)?

  • 减少布局重建,使用固定尺寸的背包格子 + 预制体复用,同时关闭不必要的布局组件。

  • 减少图像重建,避免频繁修改Graphic属性,对不变的UI元素使用静态

  • 追问:Canvas的渲染层级和合批规则是什么?如何减少DrawCall?

AssetBundle的同步/异步加载有什么区别?如何解决AB包的内存泄漏问题?

AssetBundle.Unload(true)释放资源。引用计数,确保无残留引用。

2D-RPG的ScriptableObject数据管理方案,相比传统Excel配置表有何优势?

无需解析Excel,直接编辑器配置;支持运行时修改([CreateAssetMenu]

  • 追问:如何实现ScriptableObject的运行时修改和持久化?
  • t通过EditorUtility.SetDirty标记修改,或结合JSON序列化。

CrossRoad的无限地形生成算法具体如何实现?随机算法是否会导致重复地形?

将各种不同的地形存入列表,根据列表的长度随机生成的不同的序号,将序号使用变量进行存储,下次生成随机地形时,比较序号与上传是否相同,相同则重新生成,这样确保不会重复地形。

  • 追问:如何优化地形加载性能(如分块加载)?
  • 将整个地形分割为多个独立的地形块(Chunk),根据玩家位置动态加载 / 卸载周围的块,只保留视野范围内的地形数据,从而减少内存占用和渲染压力。

PlayFab网络排行榜的实现中,如何保证数据安全(防作弊)?

数据校验,服务端验证分数的合理性(如时间和操作次数)

  • 追问:如果网络请求失败,本地和云端数据如何同步
  • 本地缓存临时数据,网络恢复后优先同步云端。

假设你的2D-RPG游戏在移动端运行时卡顿,如何定位和优化?

  • 考察点:Profiler分析(CPU/GPU/内存)、GC触发原因、资源加载策略。

设计一个支持多人联机的俯视角射击游戏,简述网络同步方案

  • 追问:如何处理网络延迟和玩家预测(Client-side Prediction)?

状态同步和帧同步的原理

状态同步是指服务器将所有玩家的状态进行汇总,然后广播给所有客户端。客户端收到广播后,更新自己的状态,以保持和服务器一致。状态同步适用于对实时性要求不是很高的游戏,例如策略游戏、棋牌游戏等。

帧同步是指服务器以一定的帧率(通常为每秒 30 帧或 60 帧)将游戏状态广播给所有客户端。客户端按照服务器广播的状态进行模拟,然后将自己的操作发送给服务器,服务器再将所有玩家的操作进行汇总,计算新的游戏状态并广播给所有客户端。帧同步适用于对实时性要求比较高的游戏,例如射击游戏、竞速游戏等。

在帧同步中,为了避免网络延迟导致的卡顿和掉帧,常常采用平滑插值、延迟补偿、预测等技术。平滑插值是指在两帧之间,将玩家的状态进行插值,以平滑过渡;延迟补偿是指服务器根据玩家的网络延迟,向后推迟一定的帧数来接收玩家的操作,以避免操作过期;预测是指客户端预测自己的操作结果,以提高游戏的响应速度。

上面两个在反作弊、断线重连、实时性等等场合,用哪种同步策略好

在反作弊、断线重连和实时性等场合,帧同步是更好的选择。

在帧同步中,客户端只能执行服务器发送过来的操作,减少了作弊的可能性。而状态同步是客户端主动更新自己的状态,容易被作弊者恶意利用。此外,断线重连也更适合在帧同步中实现,因为客户端可以重新连接到服务器并接收新的帧数据来进行同步,而状态同步需要重新将所有状态数据发送到客户端,增加了网络带宽的负担。

跳跃到最高点自动开枪,这个功能应该怎么做

定义一个布尔变量 jumping 表示玩家是否正在跳跃。在每帧中检测玩家是否按下了跳跃键,如果按下了,则将 jumping 设为 true。如果 jumping 为 true,则检测玩家是否已经到达最高点。可以通过判断玩家的竖直速度是否小于等于0来判断是否到达最高点。如果到达了最高点,则自动开枪。可以调用开枪的函数或发送一个开枪的指令给服务器。

角色可以穿脱装备,每个装备对角色的血量有不同的buff,该如何设计这个功能装备属性的实现

装备可以为角色提供属性加成(例如加血、加攻击力等)。可以通过设计一个装备属性接口,让装备继承该接口,并实现对应的属性加成函数。在角色穿戴或卸下装备时,调用对应的属性加成函数即可。

装备buff的实现:当角色穿戴某个装备时,可以获得该装备的buff(例如加血量)。可以在装备属性接口中添加一个获取buff的函数,在角色穿戴该装备时,调用该函数获取buff,并将buff应用到角色上。

简述TCP3次握手,四次挥手的过程

三次握手:

客户端发送 SYN:客户端向服务器发送一个 SYN 包,并随机初始化一个序列号 seq = x,以此表明客户端希望建立连接并请求同步初始序列号。

服务器回复 SYN + ACK:服务器收到 SYN 包后,会向客户端发送一个 SYN + ACK 包。其中,SYN 包的序列号 seq = y 是服务器随机生成的,ACK 包的确认号 ack = x + 1,用于确认客户端的 SYN 包。

客户端发送 ACK:客户端收到服务器的 SYN + ACK 包后,会向服务器发送一个 ACK 包,确认号 ack = y + 1,表示客户端已收到服务器的 SYN 包。此时,连接建立成功。

四次挥手:

客户端发送 FIN:客户端向服务器发送一个 FIN 包,seq = u,表示客户端想要关闭连接。

服务器回复 ACK:服务器收到 FIN 包后,会向客户端发送一个 ACK 包,ack = u + 1,确认客户端的 FIN 包。此时,服务器到客户端的连接仍处于开放状态。

服务器发送 FIN:服务器处理完数据后,向客户端发送一个 FIN 包,seq = v,表示服务器也想要关闭连接。

客户端回复 ACK:客户端收到服务器的 FIN 包后,会向服务器发送一个 ACK 包,ack = v + 1,确认服务器的 FIN 包。此时,连接彻底关闭。

为什么TCP连接需要3次握手,断开需要四次挥手

三次握手的原因:

三次握手的主要目的是同步客户端和服务器的初始序列号,确保双方都有发送和接收数据的能力。

第一次握手让服务器知道客户端有发送数据的能力;第二次握手让客户端知道服务器有接收和发送数据的能力;第三次握手让服务器知道客户端有接收数据的能力。

两次握手无法保证双方初始序列号的同步,也不能确保双方都具备数据收发能力。

四次挥手的原因:

TCP 连接是全双工的,这意味着双方可以同时进行数据的发送和接收。因此,关闭连接时需要分别关闭两个方向的连接。

四次挥手将关闭连接的过程分为两个阶段:首先由客户端请求关闭发送方向的连接,然后服务器请求关闭接收方向的连接。由于服务器在收到客户端的 FIN 包后,可能还有数据需要发送,所以 ACK 和 FIN 通常分开发送,这就导致了四次挥手的过程

客户端通过三次握手建立连接后,怎样维持这个连接?

TCP 连接建立之后,主要通过以下几种方式来维持连接:

心跳机制:应用层可以实现心跳机制,定期发送心跳包,以此确认对方是否在线。例如,在长连接的场景中,客户端和服务器会定期交换 PING/PONG 消息。

TCP Keep-Alive:TCP 协议本身提供了 Keep-Alive 机制。在连接长时间没有数据传输时,TCP 会发送 Keep-Alive 包。如果对方没有响应,经过多次重试后,TCP 会认为连接已经断开,并关闭该连接。

滑动窗口机制:通过滑动窗口协议,接收方会不断向发送方确认已接收的数据,保证数据的可靠传输,同时也间接维持了连接的状态。

超时重传:发送方发送数据后会启动定时器,如果在规定时间内没有收到确认,就会重传数据,确保连接的可靠性

lua热更新的流程

image-20250824202346312

1.集成Lua解释器

  • •选择xLua或uLua等成熟解决方案
  • •将Lua虚拟机嵌入到Unity项目中
  • •建立C#与Lua的交互桥梁

2. 导出Unity接口

  • •通过特性标记或配置文件指定需要暴露给Lua的C#类和方法
  • •生成包装代码,使Lua能够调用Unity引擎功能
  • •示例:将GameObject、Transform等常用组件接口导出

3. 开发Lua脚本

  • •使用Lua语言编写游戏逻辑:UI控制、角色行为、业务逻辑等
  • •利用导出的Unity API操作游戏对象和资源
  • •保持模块化设计,便于热更新替换

4. 编译与打包

  • •将Lua源码编译为字节码(提高加载效率和安全性)
  • •将字节码文件打包到AssetBundle中
  • •建立版本管理系统,每个AB包包含版本信息

5. 热更新执行

  • •游戏运行时检测服务器版本与本地版本差异
  • •下载更新的AssetBundle包到持久化目录
  • •加载新AB包,替换旧的Lua字节码
  • •Lua解释器执行更新后的代码,实现热更新

lua实现热更新的具体步骤

Lua 实现热更新的具体步骤

Step 1: 嵌入 Lua 解释器

  • 核心操作:将 Lua 解释器(虚拟机)集成到 Unity 宿主程序中。
  • 技术方案:使用 xLuauLua等成熟的插件,它们已经封装好了与 Unity 的交互接口,无需从零开始实现。
  • 目的:为游戏提供一个能够运行时解析和执行 Lua 脚本的环境。

Step 2: 导出 Unity 接口给 Lua

  • 核心操作:将 Unity 引擎的 C# API(如创建对象、获取组件、触发动画等)暴露给 Lua 脚本,使其能够操作游戏。
  • 技术方案
    • •在 C# 代码中使用 [LuaCallCSharp]特性标记需要暴露的类和方法。
    • •工具会自动生成这些接口的“包装代码”,作为 C# 和 Lua 之间的桥梁。
  • 目的:让 Lua 脚本拥有控制游戏逻辑和改变游戏状态的能力。

Step 3: 开发并解释执行 Lua 脚本

  • 核心操作:开发者使用 Lua 语言编写游戏逻辑(如 UI、角色行为、关卡流程等)。
  • 执行流程:Unity 内置的 Lua 解释器会加载并运行这些脚本。
  • 目的:将需要频繁变动或希望热更的业务逻辑从 C# 转移到 Lua 端。

Step 4: 编译并打包代码资源

  • 核心操作:将编写好的 Lua 源代码编译成字节码,并作为资源打包。
  • 技术流程Lua 源代码-> 编译为 Lua 字节码-> 作为 代码资源-> 打入 AssetBundle (AB 包)
  • 目的
    • 字节码:加载效率更高,并有一定的代码保护作用。
    • AB 包:Unity 官方的资源管理机制,便于对代码资源进行版本管理和增量下载。

Step 5: 运行时下载与更新

  • 核心操作:游戏运行时,从服务器检查并下载最新的代码资源 AB 包,由 Lua 解释器加载执行。

  • 完整流程

      游戏启动后,对比本地和服务器上的资源版本号。

      2.下载版本更新的 AB 包到本地持久化路径。

    1. 3.加载新的 AB 包,获取其中的 Lua 字节码。

    2. 4.通过 require等方式让 Lua 解释器执行新的代码。

  • 最终效果:游戏内容或逻辑被更新,无需重新安装应用,实现了热更新

总结:首先在Unity项目中嵌入lua解释器,比如xlua。将Unity的接口比如组件,想要修改的类脚本暴露给lua,使用lua语言进行编写并且执行,将编译好的lua脚本使用AB包进行打包,并且上传到资源服务器。运行游戏是,从资源服务器下载AB包,并且由下载的lua解释器进行编译执行,检查下载的资源与本地的版号进行比较,并且持久化,

在Unity项目中集成xLua等Lua解释器后,需通过特性标记(LuaCallCSharp)将Unity组件和业务逻辑类暴露给Lua环境。使用Lua编写动态逻辑,编译为字节码后打包成AssetBundle并上传至资源服务器。游戏运行时需执行以下严格流程:首先校验资源完整性(MD5比对),下载新版AB包至持久化存储路径,清除Lua模块缓存(package.loaded),加载并执行新脚本,最后释放旧资源(AssetBundle.Unload)。必须确保版本控制严格、内存管理规范,并建立完善的错误回滚机制,方可实现安全可靠的热更新功能。

插值

在Unity中,**插值(Interpolation)**是一种平滑过渡数值的技术,常用于动画、移动、颜色渐变等场景。Unity提供了多种插值方法,主要包括 线性插值(Lerp)球形插值(Slerp)阻尼插值(SmoothDamp),每种方法适用于不同的需求。

Lerp(线性插值)

在两个值之间进行线性过渡,速度均匀稳定,适合大多数平滑移动和颜色渐变场景。比如相机的跟随,UI颜色渐变,数值平滑过渡

Slerp(球形插值)

专门用于旋转的球面最短路径插值,避免旋转抖动,确保3D物体转向自然流畅。3D物体的自然转向

SmoothDamp(阻尼插值)

带弹簧缓冲效果的智能插值,自动计算过渡速度,特别适合摄像机跟随和物体平滑追踪。

AnimationCurve(曲线插值)

通过自定义曲线控制插值节奏,实现非线性变化,完美支持复杂动画和弹性效果。

框架模块

1. 单例模式(Singleton)

技术:使用 static变量 + DontDestroyOnLoad确保全局唯一实例。

优点

  • 提供全局访问点,避免重复创建管理器类(如 GameManager)。

  • 减少 FindObjectOfTypeGetComponent的性能开销。

    缺点

  • 滥用会导致代码耦合(如 AudioManager.Instance.Play()散落在各处)。

  • 多线程环境下需额外处理(Unity 主线程单线程,一般无需考虑)。

适用场景全局管理器(如音效、存档、场景加载)。


2. 对象池(Object Pooling)

技术:通过 Queue/Stack缓存对象,复用 GameObject而非频繁 Instantiate/Destroy

优点

  • 大幅降低GC压力,避免内存碎片。

  • 提升性能(如子弹、特效等高频创建对象)。

    缺点

  • 需手动管理对象生命周期(如 ReturnObject)。

  • 池大小不足时需动态扩容,可能引发瞬时卡顿。

适用场景高频创建/销毁的对象(如子弹、敌人、粒子特效)。

使用对象池 + 资源加载模块,在项目中提前设置好一批预制体,在场景加载前提前加载好使用Resources.Load加载好预制体资源,并且存入对象池模块中。使用时从对象池中拿取即可,不使用了,再次存入对象池,不进行销毁


3. 预加载(Preloading)

技术Resources.Load/ Addressables.LoadAssetAsync提前加载资源。

优点

  • 避免运行时卡顿(如进入战斗场景时突然加载贴图)。

  • 支持异步加载,不阻塞主线程(Addressables)。

    缺点

  • 占用额外内存,需权衡预加载量。

  • Resources文件夹难以维护,推荐 Addressables

适用场景关键资源(如角色模型、场景贴图、UI素材)。


4. 事件管理器(Event Manager)

技术Action/event或观察者模式(Dictionary<Type, Action>)。

优点

  • 彻底解耦,避免 GetComponent硬依赖(如UI与逻辑分离)。

  • 支持多订阅(如多个系统监听玩家死亡事件)。

    缺点

  • 需手动取消订阅,否则导致内存泄漏。

  • 过度使用会使调试困难(事件流难以追踪)。

适用场景跨系统通信(如成就系统、UI更新、游戏状态变更)。


5. 资源加载(Resource Loading)

技术对比

方式 优点 缺点
Resources.Load 简单快捷 内存不可控,无法热更新
AssetBundle 支持热更新 需手动管理依赖
Addressables 自动化依赖管理 学习成本较高

最佳实践

  • 小型项目用 Resources+ 对象池
  • 大型项目用 Addressables+ 异步加载

6. 空场景加载(Empty Scene Loading)

技术:先加载轻量级空场景,再异步加载目标场景。

优点

  • 避免直接切换大场景时的黑屏/卡顿。

  • 可在空场景显示加载进度条,提升体验。

    缺点

  • 增加场景切换步骤(需多一次加载)。

  • 需合理设计空场景(如仅含UI和背景)。

适用场景开放世界/大型场景切换


总结对比表

技术 核心优势 主要缺点 适用场景
单例 全局访问,避免重复创建 高耦合,难测试 管理器类
对象池 减少GC,提升性能 需手动回收 子弹/特效
预加载 避免运行时卡顿 内存占用 关键资源
事件管理器 解耦系统通信 需取消订阅 UI/逻辑交互
资源加载 动态加载/卸载 管理复杂 大型项目
空场景加载 平滑过渡 额外步骤 场景切换

核心思想

  • 性能优化:对象池、预加载、空场景。
  • 代码架构:单例、事件管理器。
  • 资源管理Addressables> AssetBundle> Resources

合理组合这些技术,可显著提升游戏性能和可维护性! 🎮

对象池实现中,对象回收时的状态重置如何处理?

这是对象池实现的关键细节。回收对象时必须彻底重置其状态,否则会产生极其诡异的Bug。

重置逻辑通常在一个 OnResetRecycle方法中完成:

以子弹对象池为例,回收时 (OnReset) 会:

  1. 1.停止所有协程StopAllCoroutines()。防止回收后,上一轮生命周期中开启的协程(如飞行协程)继续运行。
  2. 2.重置物理状态:如果使用Rigidbody,必须将速度清零 (velocity = Vector3.zero),角速度清零 (angularVelocity = Vector3.zero)。物理引擎不会自动帮你做这个。
  3. 3.重置Transform:设置 localPosition = Vector3.zero, localRotation = Quaternion.identity。有时也会设置 SetParent(null)或将其放回池的根节点下。
  4. 4.禁用所有可能的效果:取消粒子效果播放、取消拖尾渲染器 (TrailRenderer) 并清除其已有轨迹 (Clear())。
  5. 5.设置初始状态:将子弹的伤害、生命周期等业务逻辑参数重置为默认值。
  6. 6.禁用对象:最后调用 gameObject.SetActive(false)

核心思想:让对象从内到外(从数据到表现)恢复到它刚从Prefab实例化出来的那一刻的状态。

ScriptableObject存储的数据和PlayerPrefs有什么区别?分别适合存什么类型的数据?

特性 ScriptableObject (SO) PlayerPrefs
本质 配置文件,存在于项目/资源文件夹中 系统注册表(Windows)或.plist文件(Mac)
存储位置 应用包内(只读)或可读写路径 系统特定的持久化存储位置
数据类型 复杂对象(可自定义Class/Struct) 仅限int, float, string
用途 游戏配置:角色属性、技能数据、物品信息 用户偏好:音量设置、语言选择、最高分数
读写速度 快(内存中) 慢(需要读写磁盘)

简单说ScriptableObject用于配置游戏本身的数据,PlayerPrefs用于保存玩家的偏好和进度数据。

AB包分包按类型/场景/逻辑分,卸载时需考虑是否卸载资源,那如果AB包之间有依赖(如A包依赖B包的材质),卸载B包会导致A包资源失效吗?该如何处理依赖包的卸载?

会失效

如果包A依赖包B中的材质,当你卸载了包B (AssetBundle.Unload(true)),那么包A中所有使用该材质的物体会变成洋红色(丢失材质)

正确处理依赖卸载的策略

  1. 1.引用计数:为每个AssetBundle维护一个引用计数器。
  2. 2.加载依赖:加载包A时,自动加载其依赖的包B,并增加包B的引用计数。
  3. 3.卸载检查:当想要卸载包B时,检查其引用计数。
    • 如果引用计数 > 0(即还有其他包在使用它),则不能卸载。
    • 如果引用计数 == 0,则可以安全卸载。

状态机有进入/更新/退出状态,行为树有可视化节点,那在控制怪物AI时,什么时候适合用状态机,什么时候适合用行为树?比如SLG游戏中怪物巡逻-攻击,用哪种更灵活?

  • 有限状态机(FSM)
    • 适用状态明确、转换简单的AI。
    • 例子:SLG游戏中怪物的“巡逻 -> 发现敌人 -> 攻击 -> 死亡”这种线性、清晰的状态循环。FSM实现简单,直观,性能开销小。
    • 缺点:状态多了后,状态转换会变得极其复杂(“蜘蛛网”),难以维护。
  • 行为树(BT)
    • 适用复杂、需要频繁调整、需要高层决策的AI。
    • 例子:开放世界RPG中的敌人,它的决策流程可能是:“是否死亡? -> 是否发现玩家? -> 是否在攻击距离内? -> 血量低是否逃跑? -> 否则是否巡逻?”。行为树通过选择、序列、并行等节点组合,能优雅地表现这种带条件判断的复杂逻辑。
    • 优点高可复用性、可读性、可维护性,策划可以通过工具自行调整AI逻辑。
    • 缺点:实现相对复杂,性能开销比FSM稍大。

结论:对于简单的SLG怪物,FSM足够且更高效。如果预期AI会变得越来越复杂,需要频繁迭代,那么从一开始就使用行为树会更灵活。

背包界面有大量物品时如何设计优化?

  1. 1.对象池:核心优化手段。只创建可视区域内的物品UI,滚动时循环复用。
  2. 2.分帧加载:在几帧内陆续创建物品,避免一次性实例化大量UI导致的卡顿。
  3. 3.虚拟化列表:只对可视区域的物品进行数据绑定和更新,滚动时动态更新复用UI的内容。
  4. 4.合并DrawCall:确保所有物品UI使用相同的图集和材质,促进UGUI合批。
  5. 5.简化UI:减少物品UI上的组件数量(如不必要的Layout Element、Shadow等)。

UGUI常见的优化方法有哪些?Mask与Rect Mask 2D的区别?

常见优化

  • 合批:打图集,减少材质和纹理切换。
  • 填充率优化:避免全屏透明UI叠加。
  • 隐藏静态UI:将看不到的UI(如移出屏幕)的CanvasRenderer设置为cull=true
  • 分离动态/静态UI:将频繁更新的UI放在单独的Canvas下,避免触发整个Canvas的重建。
  • 禁用Raycast:对不需要交互的UI禁用Raycast Target

Mask vs Rect Mask 2D

特性 Mask Rect Mask 2D
原理 使用模板缓存(Stencil Buffer) 使用矩形裁剪,不涉及Stencil
性能 较低(每Mask增加一个DrawCall) 较高(几乎无额外开销)
形状 支持任意形状(依赖Alpha通道) 仅支持矩形
子物体 子物体需参与合批,否则失效 无此限制

结论:如果只需要矩形裁剪,无条件选择Rect Mask 2D

是否了解结构体是否可以实现接口?为什么?

可以。结构体(struct)在 C# 中是值类型,虽不能继承类,但允许实现接口,以扩展其功能。这是因为接口本质是行为契约,与类型是值类型还是引用类型无关。

在什么情况下,Awake函数可能不会执行?

  • 脚本所在的 GameObject 从未被实例化(如仅在 Assets 目录中,未放入场景)。
  • 脚本被删除或未挂载到任何 GameObject 上。
  • 游戏运行中,GameObject 在 Awake 调用前被销毁(如在其他脚本的 Awake 中销毁该对象)

请解释在Unity中,禁用脚本或物体不活动时,Start函数的执行情况

  • 若脚本被禁用(enabled = false):Start 不会执行,直至脚本被启用。
  • 若 GameObject 被设置为不活动(SetActive(false)):Start 不会执行,直至 GameObject 被激活。
    (Start 的执行需满足两个条件:对象激活且脚本启用,且尚未执行过。)

如果在Unity中隐藏一个物体,协程是否还会继续执行?

会继续执行。隐藏物体(SetActive(false))仅影响渲染和碰撞,不影响脚本逻辑(包括协程),除非脚本被禁用或物体被销毁

在Unity中销毁一个物体后,其协程是否还会继续执行?

不会。当 GameObject 被销毁(Destroy(gameObject)),其身上所有脚本的协程会被强制终止,剩余逻辑不再执行。

Unity中的Image和RawImage组件有什么区别?

  • Image:用于显示 Sprite 类型资源,支持切片(Sliced)、平铺(Tiled)等拉伸模式,适合 UI 元素(如按钮、图标)。
  • RawImage:直接显示 Texture 类型资源(如 Texture2D),无 Sprite 的拉伸优化,适合显示动态生成的纹理(如相机渲染结果)。
    核心差异:Image 依赖 Sprite(经过 UI 优化),RawImage 依赖原始纹理。

为什么热更新选择使用lua,而不是C#

热更新本身对于资源热更新是非常容易的,Unity自带的AB包就可以轻松解决,难的是代码热更新,因为Unity中的C#是编译型语言,Unity在打包后,会将C#编译

成一种中间代码,再由Mono虚拟机编译成汇编代码供各个平台执行,它打包以后就变成了二进制了,会跟着程序同时启动,就无法进行任何修改了。

LUA是解释型语言,并不需要事先编译成块,而是运行时动态解释执行的。这样LUA就和普通的游戏资源如图片,文本没有区别,因此可以在运行时直接从WEB服

务器上下载到持久化目录并被其它LUA文件调用。

资源热更新只需要打成AB包上传资源服务器提供玩家下载

热更新流程

  1. 打包热更资源的对应的md5信息(涉及到增量打包)
  2. 上传热更 ab 到热更服务器
  3. 上传版本信息到版本服务器
  4. 启动游戏。
  5. 根据当前版本号,和平台号去版本服务器上检查是否有热更。
  6. 从热更服务器上下载 MD5 文件,比对需要热更的具体文件列表。
  7. 从热更服务器上下载需要热更的资源,解压到热更资源目录。
  8. 游戏运行加载资源,优先到热更目录中加载,再到母包资源目录加载。