Skip to content

Unity 客户端岗

C# 基础是否扎实

csharp-basics-solidness

面试回答: C# 基础扎实,不是只会写语法,而是能做到四层:语法会写、原理能讲、常见坑能避开、能结合 Unity 项目落地

面试官常看这些: 类型系统:struct / class、值类型 / 引用类型、装箱拆箱、string 不可变、nullableenumreadonlyconststatic。 集合泛型:List 扩容、Dictionary 哈希表、HashSet 去重、泛型约束、遍历中修改集合为什么报错。 委托事件:delegateeventActionFunc、闭包、多播委托、事件不取消订阅导致泄漏。 内存和 Unity:GC、IDisposableusing、协程、async/await、LINQ 和闭包带来的 GC、Profiler 定位问题。

WARNING

比较稳的回答: “我理解 C# 基础扎实不是背 API,而是知道代码背后的运行机制。比如我知道 List<T> 底层是数组,扩容会复制;Dictionary 靠哈希定位;event 能限制外部直接赋值;闭包和 LINQ 在 Unity 热路径里可能产生 GC。所以我写代码时会结合性能、可维护性和 Unity 场景做取舍。”

加分句: “如果面试官追问,我可以从语法讲到底层,再讲到 Unity 项目里的实际坑。”

Unity 生命周期、组件、协程、资源加载是否熟

unity-lifecycle-component-coroutine-resource

面试回答: 可以这样说:我对 Unity 的生命周期、组件模型、协程和资源加载比较熟。我的理解不是只会用 API,而是知道对象什么时候创建、脚本什么时候执行、协程怎么恢复、资源什么时候加载和释放

生命周期: 我知道常见顺序是 AwakeOnEnableStart,然后进入 UpdateFixedUpdateLateUpdateAwake 适合做自身初始化,Start 适合依赖其他对象初始化后的逻辑,OnEnable / OnDisable 常用来注册和取消事件,OnDestroy 做最终清理。

组件模型:GameObject 更像一个容器,真正的能力来自 Component,比如 TransformAnimatorRigidbody、自定义脚本。开发时我会缓存常用组件引用,避免在 Update 中频繁 FindGetComponent

协程: 协程本质是 IEnumerator 状态机,不是线程,仍然在 Unity 主线程执行。yield return null 表示下一帧继续,WaitForSecondstimeScale 影响,WaitForSecondsRealtime 不受暂停影响。

IMPORTANT

资源加载: 我熟悉同步加载和异步加载的区别。同步加载简单但可能卡主线程;异步加载适合场景、Prefab、大贴图、音频等资源。资源管理上要关注依赖、引用计数、重复加载、释放时机,以及 UnloadUnusedAssets 的代价。

UI、动画、物理、输入是否能落地

unity-ui-animation-physics-input-landing

面试回答: 可以这样说:我能把 UI、动画、物理、输入做成完整功能,而不是只会单个 API。比如一个角色攻击流程:输入触发攻击,状态机判断能不能攻击,Animator 播放攻击动画,动画关键帧开启碰撞或射线检测,命中后结算伤害,最后 UI 刷新血条和飘字

UI 落地: 能做界面打开关闭、按钮事件、弹窗栈、ScrollView 虚拟列表、Canvas Scaler 适配、事件穿透处理、Raycast Target 优化、UI 管理器和界面资源加载。

动画落地: 能用 Animator Controller、State、Parameter、Blend Tree、Layer、Avatar Mask 控制角色表现。也知道动画不能完全替代逻辑,攻击判定、打断、前后摇、受击、硬直这些最好由逻辑状态机统一管理,动画只负责表现和关键帧同步。

物理落地: 能区分 ColliderRigidbodyTriggerCollision,知道角色移动什么时候用 CharacterController,什么时候用 Rigidbody。会用 Raycast 做点击地面、技能目标筛选、视野检测,也知道 LayerMask 和碰撞矩阵要配置好。

输入落地: 能处理键盘、鼠标、触屏、虚拟摇杆、手柄。一般在 Update 读取输入,把输入转成“移动、攻击、跳跃、交互”等意图,再交给角色状态机处理。物理移动相关逻辑再放到 FixedUpdate 或物理模块里。

TIP

加分句: “我理解落地能力不是把功能写出来就结束,还要处理边界和性能,比如输入禁用、UI 遮挡、动画打断、碰撞层级、重复点击、UI 重建、Animator 数量和物理检测频率。”

是否做过完整游戏 Demo

complete-game-demo-experience

面试回答: 可以这样说:我做过完整游戏 Demo,不只是单独做角色移动或一个 UI,而是做了一个可玩的闭环:主菜单进入关卡,玩家操作角色,击败敌人或达成目标,触发胜负结算,发放奖励或保存进度,然后可以重新开始。

什么叫完整 Demo: 完整 Demo 至少要有:核心玩法、角色控制、怪物或目标、UI 反馈、音效或特效、配置表、资源加载、对象池、胜负结算、存档或进度记录。重点不是功能多,而是流程能跑通,系统之间能协作。

如果做过,可以这样讲: “我做过一个小型动作 Demo,包含角色移动、攻击、怪物 AI、技能 CD、血条 UI、对象池、场景切换和结算流程。我主要负责角色控制、战斗逻辑、UI 管理和资源加载。过程中遇到过 GC、对象频繁创建、动画和攻击判定不同步的问题,后来通过对象池、动画事件和状态机拆分解决。”

NOTE

如果没做过大型项目,也可以稳住: “我没有参与过商业级完整项目,但我做过小型闭环 Demo。它能从开始到结束完整运行,我能讲清楚每个模块的职责、系统之间怎么通信,以及后续如果扩展背包、任务、技能、资源管理会怎么设计。”

是否知道移动端性能优化

mobile-performance-optimization-knowledge

面试回答: 知道。移动端性能优化不能只说“少创建对象、少用 Update”,要先用真机和 Profiler 定位瓶颈,然后按 CPU、GPU、内存、加载、发热、包体 分类处理。

CPU 优化:Main ThreadBehaviourUpdate、脚本耗时。常见手段是减少大量 Update,用 Update Manager;AI、寻路、碰撞检测分帧或降频;缓存组件引用;减少反射、LINQ、闭包和频繁字符串拼接;对象池减少创建销毁。

GPU 优化:Camera.Render、DrawCall、SetPass、Overdraw。常见手段是合批、图集、GPU Instancing、SRP Batcher;减少透明物体和粒子 Overdraw;控制实时阴影、后处理、分辨率和 Shader Variant。

内存优化: 看 Managed Memory、Native Memory、Texture、Mesh、AudioClip。常见手段是贴图压缩、Mesh 压缩、音频按场景选择加载方式;资源用引用计数管理;不用的资源及时释放;避免 AssetBundle 或 Addressables 常驻不卸载。

加载和发热: 大资源尽量异步加载,场景分块加载,必要资源预加载;首包和热更包拆开。发热方面要控制持续高负载,比如限制最高帧率、降低渲染分辨率、减少后处理和实时阴影。

CAUTION

加分句: “我做优化一定会先记录优化前数据,再对比优化后数据,比如帧耗时、GC Alloc、内存峰值、加载耗时、卡顿率。移动端优化不只是追 FPS,还要看发热、耗电、低端机和长时间运行稳定性。”

是否能定位 GC、卡顿、资源泄漏

unity-gc-stutter-resource-leak-diagnosis

面试回答: 能定位。我的思路不是先猜原因,而是先稳定复现,再用 Profiler 把问题分类:GC 看分配,卡顿看尖峰帧,资源泄漏看内存快照差异

GC 怎么定位: 看 Profiler 里的 GC AllocManaged Heap,找到是哪段代码每帧分配。常见原因有 Updatenew 对象、字符串拼接、LINQ、闭包、装箱、频繁 Instantiate/Destroy、事件临时注册等。

卡顿怎么定位: 先看尖峰帧发生在 Main ThreadRender Thread 还是加载线程。Main Thread 高就看脚本、AI、寻路、UI 重建;Camera.Render 高就看 DrawCall、Overdraw、后处理、阴影;如果是加载卡顿,就查同步加载、解压、IO、Shader 编译。

资源泄漏怎么定位: 用 Memory Profiler 做快照对比,比如进入场景前一次、退出场景后一次,看 Texture、Mesh、AudioClip、GameObject、AssetBundle 是否没有下降。常见原因是事件没取消订阅、静态引用没清、对象池只入池不释放引用、Addressables 或 AssetBundle 没正确 Release。

简单监控脚本:

c
using UnityEngine; // 引入 Unity 基础 API。
public class RuntimeSpikeMonitor : MonoBehaviour // 定义一个运行时尖峰帧监控组件。
{ // 类体开始。
    private const float SpikeMs = 50f; // 定义超过 50 毫秒的一帧为明显卡顿。
    private void Update() // 每帧执行一次采样逻辑。
    { // 方法体开始。
        float ms = Time.unscaledDeltaTime * 1000f; // 把真实帧间隔转换成毫秒。
        if (ms < SpikeMs) return; // 如果没有超过阈值,就直接返回。
        Debug.Log($"Frame spike: {ms:F1} ms"); // 输出尖峰帧日志,方便结合 Profiler 时间点排查。
    } // 方法体结束。
} // 类体结束。

WARNING

加分句: “修复后我会用同一机型、同一路径、同一数据量重新跑一遍,对比 GC、帧耗时和内存曲线,确认问题是真的消失了。”

是否能设计常见业务系统

common-business-system-design-capability

面试回答: 能。我设计常见业务系统时,不会先从 UI 开始堆功能,而是先拆成:配置表、运行数据、业务服务、事件通知、UI 表现、存档或网络同步。这样背包、任务、商店、邮件、红点、成就、排行榜、新手引导、对话系统都能套同一套思路。

核心设计思路: 配置表负责规则,比如物品 ID、任务条件、奖励内容;运行数据负责状态,比如背包里有多少物品、任务进度是多少;Service 负责改数据和校验规则;UI 只负责显示和点击;数据变化后通过事件通知 UI 刷新。这样不会把 UI、数据、逻辑混在一起。

简单业务系统代码模板:

c
using System; // 引入 Action 事件类型。
using System.Collections.Generic; // 引入 Dictionary 集合类型。
public interface IGameSystem // 定义所有业务系统都可以遵守的基础接口。
{ // 接口开始。
    void Init(); // 初始化系统,例如加载配置或恢复数据。
    void Dispose(); // 释放系统,例如取消事件或清理缓存。
} // 接口结束。
public sealed class InventoryService : IGameSystem // 定义一个简单的背包业务服务。
{ // 类开始。
    public event Action<int> OnItemChanged; // 定义物品变化事件,参数是变化的物品 ID。
    private readonly Dictionary<int, int> itemCounts = new Dictionary<int, int>(); // 保存物品 ID 到数量的映射。
    public void Init() // 初始化背包系统。
    { // 方法开始。
        itemCounts.Clear(); // 清空旧数据,避免重复初始化残留。
    } // 方法结束。
    public void AddItem(int itemId, int count) // 增加指定物品数量。
    { // 方法开始。
        if (!itemCounts.ContainsKey(itemId)) itemCounts[itemId] = 0; // 如果物品不存在,就先创建记录。
        itemCounts[itemId] += count; // 累加物品数量。
        OnItemChanged?.Invoke(itemId); // 通知 UI 或其他系统刷新。
    } // 方法结束。
    public int GetCount(int itemId) // 查询指定物品数量。
    { // 方法开始。
        return itemCounts.TryGetValue(itemId, out int count) ? count : 0; // 有记录返回数量,没有记录返回 0。
    } // 方法结束。
    public void Dispose() // 释放背包系统。
    { // 方法开始。
        OnItemChanged = null; // 清空事件引用,避免对象无法释放。
        itemCounts.Clear(); // 清空背包运行数据。
    } // 方法结束。
} // 类结束。

IMPORTANT

加分句: “业务系统最怕 UI、数据、逻辑混在一起。我的设计习惯是让配置定规则,数据存状态,Service 改状态,事件通知 UI,存档和同步做兜底。”

是否能说明项目架构和模块边界

project-architecture-module-boundary

面试回答: 能说明项目架构,关键是讲清楚:项目怎么分层、每层负责什么、模块之间怎么通信、哪些事情不能越界。比如 Unity 客户端常见可以分成 UI 表现层、业务服务层、数据配置层、基础设施层。

模块边界怎么讲: UI 只负责显示和输入,不直接改背包、任务、战斗数据;业务 Service 负责规则校验和状态修改;数据层保存配置和运行时状态;资源、音频、网络、对象池、事件系统属于基础设施层,给各业务模块提供能力,但不写具体玩法规则。

简单代码示例:

c
public interface IGameModule // 定义所有游戏模块都遵守的统一接口。
{ // 接口开始。
    void Init(); // 模块初始化,例如注册事件、加载数据。
    void Dispose(); // 模块释放,例如取消订阅、清理引用。
} // 接口结束。
public interface IEventBus // 定义事件总线接口,用来降低模块之间的直接依赖。
{ // 接口开始。
    void Publish(string eventName); // 发布事件,让关心这个事件的模块自己响应。
} // 接口结束。
public sealed class BagModule : IGameModule // 定义背包模块,负责背包自己的业务边界。
{ // 类开始。
    private readonly IEventBus eventBus; // 保存事件总线引用,用来通知外部背包状态变化。
    public BagModule(IEventBus eventBus) // 通过构造函数传入依赖,避免模块自己到处查找。
    { // 构造函数开始。
        this.eventBus = eventBus; // 保存传入的事件总线对象。
    } // 构造函数结束。
    public void Init() // 初始化背包模块。
    { // 方法开始。
        eventBus.Publish("BagReady"); // 发布背包准备完成事件,而不是直接操作 UI。
    } // 方法结束。
    public void Dispose() // 释放背包模块。
    { // 方法开始。
        eventBus.Publish("BagClosed"); // 发布背包关闭事件,让外部自行处理表现。
    } // 方法结束。
} // 类结束。

NOTE

加分句: “我理解模块边界的核心是高内聚、低耦合。模块内部状态自己维护,对外只暴露接口或事件。这样后续扩展、测试、排查 Bug、替换实现都会更容易。”

是否了解热更新和资源分包

hotupdate-resource-bundling-knowledge

面试回答: 了解。热更新主要解决线上修 Bug、更新配置、上活动资源、减少整包发版成本;资源分包主要解决首包过大、资源按需下载、不同玩法资源隔离、更新体积过大的问题。

热更新大概流程: 客户端启动时先请求远程 Manifest,里面记录资源路径、版本、Hash、大小、依赖。然后和本地 Manifest 对比,只下载 Hash 变化或本地没有的资源。下载完成后做 Hash 校验,校验通过再切换到新版本;如果失败就重试或回滚旧版本。

资源分包怎么拆: 首包放登录、基础 UI、公共 Shader、必要配置;公共包放通用图集、音效、角色基础资源;场景、活动、语音、剧情、皮肤这些按需拆包。分包不能只看目录,还要看依赖关系、复用率、更新频率和平台差异,避免重复打包。

简单 Manifest 对比代码:

c
using System.Collections.Generic; // 引入 Dictionary 和 List 集合类型。
public sealed class ResourceInfo // 定义资源清单中的单个资源信息。
{ // 类开始。
    public string Hash; // 保存资源 Hash,用来判断资源内容是否变化。
    public long Size; // 保存资源大小,用来统计下载体积。
} // 类结束。
public static class HotUpdateChecker // 定义热更新检查工具类。
{ // 类开始。
    public static List<string> GetNeedDownloadList(Dictionary<string, ResourceInfo> local, Dictionary<string, ResourceInfo> remote) // 对比本地和远程清单,返回需要下载的资源路径。
    { // 方法开始。
        List<string> result = new List<string>(); // 创建结果列表,用来保存需要下载的资源。
        foreach (KeyValuePair<string, ResourceInfo> pair in remote) // 遍历远程清单中的每一个资源。
        { // 循环开始。
            string path = pair.Key; // 取出资源路径。
            ResourceInfo remoteInfo = pair.Value; // 取出远程资源信息。
            bool notExists = !local.ContainsKey(path); // 判断本地是否没有这个资源。
            bool hashChanged = !notExists && local[path].Hash != remoteInfo.Hash; // 判断本地资源 Hash 是否和远程不同。
            if (notExists || hashChanged) // 如果资源不存在或内容变化,就需要下载。
            { // 判断开始。
                result.Add(path); // 把资源路径加入下载列表。
            } // 判断结束。
        } // 循环结束。
        return result; // 返回最终需要下载的资源列表。
    } // 方法结束。
} // 类结束。

TIP

加分句: “热更新不能只考虑成功路径,还要考虑下载失败、Hash 校验失败、断点续传、灰度发布、版本兼容和回滚。资源分包也不能只为了拆而拆,要避免公共依赖重复打包,并且要有清晰的引用计数和卸载策略。”

是否能讲清楚上线质量问题

online-quality-issue-explanation

面试回答: 能。上线质量问题我会按一套流程讲:发现问题、判断影响、定级止血、定位根因、修复验证、复盘沉淀。上线问题最重要的不是先追责,而是先保护玩家体验,尽快降低影响面。

常见上线质量问题: 比如崩溃、ANR、黑屏、卡死、掉帧、发热、资源下载失败、热更失败、配置错误、存档兼容问题、弱网异常、活动入口异常、支付或登录链路异常。不同问题优先级不一样,登录、支付、战斗、更新失败通常优先级最高。

处理思路: 先看监控和告警,确认影响范围,比如哪个版本、哪个渠道、哪些机型、哪个场景、多少用户。然后先止血:关远程开关、回滚配置、回滚 Manifest、关闭活动入口、灰度暂停、必要时强更。等影响控制住,再根据日志、堆栈、错误码、资源 Hash、埋点路径定位根因。

NOTE

加分说法: “线上问题处理要先止血,再定位根因。修复后我会观察指标是否回到基线,比如崩溃率、卡顿率、下载失败率、投诉量是否下降。最后复盘时要补自动化检查、测试用例、灰度策略或告警规则,避免同类问题再次发生。”

文章评价

读完这篇,留下你的看法

暂无审核通过的评价。

登录账号后才能评价。

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