Unity开发方向暑期实习考核
一、项目概述
| 项目信息 | 详情 |
|---|---|
| 项目名称 | 倭瓜射手大冒险 |
| 游戏类型 | 横版自动前进弹跳射击跑酷 |
| 开发引擎 | Unity(2D 模式) |
| 编程语言 | C# |
| 核心玩法 | 玩家操控融合倭瓜与豌豆射手特征的植物角色,通过跳跃躲避敌人、自动射击消灭敌人,收集武器道具增强火力,挑战无限递增难度 |
项目文件结构
1 | Assets/code/Scripts/ |
二、架构设计与设计模式
2.1 单例模式(Singleton Pattern)
项目中有三个核心管理器使用了单例模式:GameManager、AudioManager、GroundManager。
实现方式:在 Awake() 中进行实例检测,保证全局唯一访问点。
1 | // GameManager.cs |
面试要点:
- 为什么用单例:GameManager 需要全局访问游戏状态(分数、生命、暂停等),AudioManager 需要跨场景保持音乐播放,GroundManager 提供全局地面 Y 坐标引用
Destroy(gameObject)的作用:防止场景中存在多个单例实例,确保唯一性- AudioManager 额外使用
DontDestroyOnLoad:音乐需要在场景切换时不被销毁,实现跨场景持续播放 { get; private set; }的意义:外部只能读取实例引用,不能修改,保证单例的不可变性和安全性
2.2 组件化架构(Component-Based Architecture)
项目遵循 Unity 的组件化设计理念,每个脚本职责单一:
| 组件 | 职责 |
|---|---|
GameManager |
游戏状态管理、分数、生命、暂停/恢复 |
PlayerController |
玩家输入处理、跳跃、射击、武器切换 |
Enemy |
敌人个体行为(移动、受击、动画) |
EnemySpawner |
敌人生成逻辑与难度递增 |
Bullet |
子弹飞行、碰撞检测、爆炸逻辑 |
Powerup |
道具下落与滚动 |
PowerupSpawner |
道具随机生成 |
BackgroundScroll |
多层视差滚动 |
GroundManager |
全局地面坐标提供 |
UIManager |
所有 UI 面板的显示/隐藏与交互 |
GameInfoDisplay |
游戏内实时信息展示 |
面试要点:
- 职责分离使得每个组件可独立测试和修改
- Spawner 与实体分离,生成逻辑与个体行为解耦
- UI 层与逻辑层分离,UIManager 仅负责显示,不包含业务逻辑
2.3 状态机思想(State Management)
GameManager 通过布尔标志管理游戏状态:
1 | public bool IsGameOver { get; private set; } |
所有游戏对象在 Update() 开头检查状态,决定是否执行逻辑:
1 | if (GameManager.Instance.IsGameOver || GameManager.Instance.IsPaused) return; |
面试要点:
- 这是一种轻量级的状态控制,避免了正式状态机的复杂度
- 通过
Time.timeScale = 0f实现暂停效果,所有基于Time.deltaTime的逻辑自动停止 - 状态属性使用
{ get; private set; },外部只读,仅 GameManager 内部可修改
三、玩家系统
3.1 跳跃系统(二段跳 + 自动弹跳)
核心实现:
1 | private bool hasGroundJump; // 是否有地面跳跃机会 |
面试要点:
- 双标志位设计:
hasGroundJump和hasAirJump两个布尔值实现二段跳,比计数器方式更清晰 - 二段跳力度衰减:
jumpForce * 0.85f,让二段跳略弱于一段跳,手感更自然 - UI 穿透防护:
IsPointerOverGameObject()防止点击 UI 按钮时误触发跳跃 - 着地重置:在
FixedUpdate中检测着地时重置跳跃状态
3.2 自动弹跳(Auto Bounce)
1 | private void HandleAutoBounce() |
面试要点:
- 角色在地面时自动周期性弹跳,营造”倭瓜蹦跳”的生动感
- 仅在
isGrounded时触发,空中不弹跳
3.3 着地检测(FixedUpdate 物理检测)
1 | private void FixedUpdate() |
面试要点:
- 为什么用
FixedUpdate:物理检测放在FixedUpdate中保证与物理引擎同步,避免Update帧率不稳定导致的穿透问题 - 双重条件检测:
position.y <= groundY + 0.5f && velocity.y <= 0.1f,位置和速度同时判断,防止上升阶段误判着地 wasGrounded标志:区分”持续在地面”和”刚着地”两种状态,只在刚着地时触发压扁动画和跳跃重置
3.4 Squash & Stretch 动画(挤压拉伸)
1 | private void HandleSquashStretch() |
面试要点:
- 动画12原则之 Squash & Stretch:经典动画原理,着地时压扁、起跳时拉伸,增强角色动态感
Mathf.Sin(t * Mathf.PI)曲线:0→1→0 的平滑过渡,比线性插值更有弹性感- 方向区分:着地和起跳的挤压方向相反,通过
squashIsLand标志区分 - Lerp 回弹:非激活状态时用
Lerp平滑恢复原始缩放,避免突变
3.5 武器系统
1 | public enum WeaponType { Pea, CornCannon, MachineGun } |
| 武器类型 | 射速 | 子弹类型 | 特点 |
|---|---|---|---|
| 豌豆(Pea) | 0.5s/发 | 普通子弹 | 基础武器,永久可用 |
| 机枪豌豆(MachineGun) | 0.1s/发 | 普通子弹 | 高速连射,火力压制 |
| 玉米大炮(CornCannon) | 1s/发 | 爆炸子弹 | 范围伤害,清场利器 |
面试要点:
- 枚举驱动:
WeaponType枚举统一管理武器类型,避免魔法字符串 - 计时器射击:
fireTimer累加Time.deltaTime,达到射速间隔时发射,实现自动射击 - 限时机制:武器道具持续
weaponDuration(3秒)后自动切回豌豆 - 嘴部精灵切换:
UpdateMouthSprite()根据当前武器切换角色嘴部外观,增强视觉反馈
四、敌人系统
4.1 敌人行为状态机
敌人有两个行为阶段:下落阶段 和 地面阶段。
1 | private void Update() |
面试要点:
- 两阶段设计:敌人从空中落下(增加视觉变化),着地后随背景滚动左移
- 速度叠加:
scrollSpeed + moveSpeed,敌人移动速度 = 背景滚动速度 + 自身移动速度,保证敌人始终向左移动 OnBecameInvisible清理:离开屏幕左侧 15 单位后自动销毁,防止内存泄漏
4.2 受击反馈系统
1 | public void TakeDamage(int dmg) |
面试要点:
- 视觉反馈:受击时精灵变红,0.1 秒后恢复原色,给玩家明确的命中感
Invoke延迟调用:简单实现定时恢复颜色,无需额外协程- 分数奖励:击杀敌人时通过
GameManager加分,实现跨组件通信
4.3 空闲呼吸动画(Idle Squash)
1 | private void HandleIdleAnimation() |
面试要点:
- 正弦波驱动:
Mathf.Sin(Time.time * speed)产生周期性缩放,模拟呼吸感 - Y轴压缩时X轴膨胀:保持体积感的经典动画技巧
- 位置补偿:缩放时调整 Y 位置,使敌人”脚”始终贴地,不会悬浮或陷入地面
4.4 动态难度系统(EnemySpawner)
1 | private void IncreaseDifficulty() |
| 难度参数 | 初始值 | 每级增量 | 上限 |
|---|---|---|---|
| 生成间隔 | 3s | -0.3s | 0.6s(下限) |
| 移动速度加成 | 0 | +0.3 | 无上限 |
| 生命加成 | 0 | +1 | 无上限 |
| 分数加成 | 0 | +50 | 无上限 |
面试要点:
- 多维难度递增:同时调整生成频率、敌人速度、敌人血量、击杀分数,避免单一维度调整导致的体验单调
Mathf.Max保底:生成间隔不低于 0.6s,防止敌人过于密集无法游玩- 难度等级可预设:
SetStartingDifficulty(int level)允许从指定难度开始,支持设置面板选择起始难度 - 定时触发:每
difficultyInterval(20秒)提升一级,节奏可预期
五、子弹系统
5.1 子弹基础行为
1 | private void Start() |
子弹沿 firePoint 的右方向发射,速度由 speed 参数控制。
5.2 爆炸子弹(AOE 伤害)
1 | private void Explode() |
面试要点:
Physics2D.OverlapCircleAll:圆形范围检测,获取爆炸半径内所有碰撞体,实现 AOE 伤害- 爆炸特效:通过
explosionEffect预制体实例化爆炸视觉效果 isExplosive标志:同一 Bullet 脚本通过布尔标志区分普通子弹和爆炸子弹,复用代码OnDrawGizmosSelected:在编辑器中绘制爆炸半径辅助线,方便调参
5.3 子弹精灵动画
1 | private void HandleAnimation() |
面试要点:
- 帧动画:通过精灵数组循环切换实现逐帧动画(如豌豆飞出效果的3帧动画)
- 取模循环:
(index + 1) % length实现无限循环播放
5.4 越界销毁
1 | private void CheckIfOutOfBounds() |
面试要点:
- 基于相机视口范围判断,而非固定坐标,适配不同分辨率
- 水平方向使用
orthographicSize * 2(宽屏),垂直方向使用orthographicSize
六、道具系统
6.1 道具行为(Powerup)
1 | private void Update() |
面试要点:
- 与敌人相似的下落+滚动模式:道具从空中落下,着地后随背景滚动
- 着地高度不同:道具着地高度
groundY + 0.8f高于敌人的groundY + 0.5f,视觉上道具浮在地面上方 weaponType字段:每个道具预制体关联一种武器类型,玩家拾取后激活对应武器
6.2 道具生成器(PowerupSpawner)
1 | private void SpawnPowerup() |
面试要点:
- 随机生成间隔:
Random.Range(minSpawnInterval, maxSpawnInterval)即 3~7 秒,避免固定节奏 - 随机高度:道具在 5~8 高度生成,部分需要跳跃才能获取,增加策略性
- 基于相机位置生成:在相机右侧
spawnDistance处生成,保证玩家看不到生成过程
七、背景视差滚动系统
7.1 多层视差实现
1 | [] |
| 层级 | 速度倍率 | 视觉效果 |
|---|---|---|
| 云层(Cloud) | 0.2x | 远景,缓慢移动 |
| 背景层(Back) | 0.5x | 中景,中速移动 |
| 草地层(Grass) | 1.0x | 近景,全速移动 |
| 泥土层(Dirt) | 1.0x | 近景,全速移动 |
面试要点:
- 视差滚动原理:不同层以不同速度移动,模拟景深效果,近处快、远处慢
- 倍率驱动:通过
multiplier参数控制每层速度,易于调整 - 无限滚动:当某层精灵移出屏幕左侧时,将其移到最右侧精灵的右边
7.2 无限滚动算法
1 | private void ScrollLayerGroup(Transform[] layers, float multiplier, float deltaTime) |
面试要点:
- 循环拼接:当精灵完全移出左侧(
position.x < -spriteWidth)时,将其接到最右侧精灵后面 FindRightmostPosition:动态查找当前最右侧精灵位置,确保拼接无缝- 基于
bounds.size.x:使用实际渲染宽度而非固定值,适配不同尺寸的精灵
7.3 速度递增
1 | private void IncreaseSpeed() |
每 30 秒速度增加 0.05,上限 8,配合敌人难度递增形成整体难度曲线。
八、UI 系统
8.1 UIManager 面板管理
UIManager 管理五个主要界面:
| 界面 | 功能 |
|---|---|
| 主菜单(MainMenu) | 开始游戏、设置、退出 |
| 设置面板(Settings) | 难度、速度、音乐、音量 |
| 游戏 HUD | 分数、生命值 |
| 暂停界面(Pause) | 继续、重新开始、返回主菜单 |
| 游戏结束(GameOver) | 最终分数、重新开始、返回主菜单 |
面试要点:
- 面板激活/隐藏:通过
SetActive(true/false)控制面板显示,简单高效 - 按钮事件绑定:在
Start()中通过onClick.AddListener统一绑定,避免 Inspector 拖拽遗漏 - ESC 键处理:
Update()中监听KeyCode.Escape,主菜单下关闭设置,游戏中切换暂停
8.2 设置面板交互
速度选择 Toggle 互斥逻辑:
1 | private bool isUpdatingSpeedToggles; // 防止递归触发 |
面试要点:
isUpdatingSpeedToggles防递归:程序修改isOn会触发onValueChanged回调,使用标志位防止无限递归- Toggle 互斥:选中一个时取消其他,取消选中时强制保持选中(至少选一个)
- 三档速度:1x / 1.5x / 2.25x,通过
Time.timeScale实现
8.3 GameInfoDisplay 实时信息
1 | private void UpdateJumpHint() |
面试要点:
- 脉冲提示:跳跃提示文字大小在 24~32 之间正弦波动,吸引注意力
- 一次性提示:玩家首次跳跃后永久隐藏,不再干扰
- 武器倒计时:实时显示当前武器名称和剩余秒数
- 难度信息:显示当前难度等级、各项加成和下次加强倒计时
九、数据持久化
项目使用 PlayerPrefs 实现简单的本地持久化:
| 存储键 | 类型 | 用途 |
|---|---|---|
BestScore |
int | 历史最高分 |
GameSpeed |
float | 游戏速度设置 |
StartingDifficulty |
int | 起始难度设置 |
MusicEnabled |
int (0/1) | 音乐开关 |
MusicVolume |
float | 音乐音量 |
SkipMainMenu |
int (0/1) | 重新开始时跳过主菜单 |
面试要点:
PlayerPrefs适用场景:简单的键值对存储,适合设置项和最高分SkipMainMenu技巧:重新开始时设置标记,场景加载后检测标记直接开始游戏,避免回到主菜单- 局限性:
PlayerPrefs不适合复杂数据结构,生产环境应考虑 JSON 文件或数据库
十、游戏流程与状态管理
10.1 完整游戏流程
1 | 启动 → 主菜单(Time.timeScale=0) |
10.2 场景重载机制
1 | public void RestartGame() |
面试要点:
- 统一使用场景重载:重新开始和返回主菜单都通过
SceneManager.LoadScene实现,简洁可靠 SkipMainMenu区分:重载后通过此标记决定是进入主菜单还是直接开始游戏- 最高分保存:每次场景重载前检查并保存最高分
十一、碰撞与物理系统
11.1 碰撞层设计
| 碰撞体 | Tag | 行为 |
|---|---|---|
| 玩家 | Player | 检测与 Enemy/Powerup 的触发器碰撞 |
| 敌人 | Enemy | 检测与 Player 的触发器碰撞(造成伤害) |
| 子弹 | 无特定 Tag | 检测与 Enemy 的触发器碰撞(造成伤害) |
| 道具 | Powerup | 被玩家检测拾取 |
11.2 双向碰撞处理
Enemy 与 Player 的碰撞在两侧都有处理:
Enemy.OnTriggerEnter2D:敌人碰到玩家 → 玩家受伤,敌人销毁PlayerController.OnTriggerEnter2D:玩家碰到敌人 → 玩家受伤,敌人销毁
面试要点:
- 双侧处理保证无论哪一方先检测到碰撞都能正确响应
- 子弹与敌人的碰撞仅在
Bullet.OnTriggerEnter2D中处理(单向)
十二、性能优化要点
12.1 对象清理
| 清理方式 | 适用对象 | 触发条件 |
|---|---|---|
OnBecameInvisible |
敌人、道具 | 移出屏幕左侧 15 单位 |
CheckIfOutOfBounds |
子弹 | 移出相机视口范围 |
Destroy |
敌人(被击杀/碰到玩家)、子弹(命中)、道具(被拾取) | 逻辑触发 |
12.2 状态检查短路
所有 Update() 方法开头检查游戏状态:
1 | if (GameManager.Instance.IsGameOver || GameManager.Instance.IsPaused) return; |
面试要点:
- 暂停/结束时跳过所有逻辑运算,减少不必要的 CPU 开销
Time.timeScale = 0本身会停止物理更新,但Update()仍会执行(使用真实时间),所以需要手动检查
12.3 潜在优化方向(面试加分项)
| 优化方向 | 当前方案 | 改进方案 |
|---|---|---|
| 对象池 | Instantiate/Destroy |
对象池复用敌人、子弹、道具 |
| FindObjectOfType | GameInfoDisplay.Start() |
Inspector 引用或单例访问 |
| 帧动画 | 手动计时器切换精灵 | Unity Animator 或 Sprite Sheet 动画 |
| 碰撞检测 | 多次 GetComponent |
缓存组件引用 |
十三、技术难点与解决方案
13.1 着地检测精度问题
问题:使用 OnCollisionEnter2D 检测着地容易出现”粘地”或”穿地”问题,特别是在高速下落时。
解决方案:在 FixedUpdate 中基于位置和速度双重判断:
1 | if (transform.position.y <= groundY + 0.5f && rb.velocity.y <= 0.1f) |
- 位置判断确保角色不低于地面
- 速度判断确保角色在下降阶段才判定着地
- 直接修正位置和速度,避免物理引擎累积误差
13.2 Toggle 互斥递归问题
问题:代码修改 Toggle 的 isOn 属性会触发 onValueChanged 回调,导致无限递归。
解决方案:引入 isUpdatingSpeedToggles 布尔标志:
1 | if (isUpdatingSpeedToggles) return; |
13.3 无限滚动无缝拼接
问题:多层背景以不同速度滚动时,精灵拼接处可能出现缝隙。
解决方案:动态查找最右侧精灵位置,将移出的精灵精确接到最右侧:
1 | float rightmostX = FindRightmostPosition(layers, spriteWidth); |
13.4 暂停时 UI 点击穿透
问题:点击 UI 按钮时同时触发了玩家的跳跃。
解决方案:使用 EventSystem.current.IsPointerOverGameObject() 检测点击是否在 UI 上:
1 | if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject()) |
十四、项目亮点总结(面试话术)
完整的游戏循环:从主菜单→游戏→暂停→结束→重开,状态管理清晰,使用
Time.timeScale控制暂停,布尔标志控制逻辑分支多维难度递增系统:同时调整敌人生成频率、移动速度、血量、分数四个维度,配合背景滚动加速,形成平滑的难度曲线
Squash & Stretch 动画:在玩家和敌人上都实现了挤压拉伸动画,遵循动画12原则,增强角色动态表现力
多层视差滚动:4 层不同速度的背景滚动,实现景深效果,无限循环拼接算法保证无缝
武器系统设计:三种武器差异化设计(射速/伤害/范围),限时切换机制增加策略性
爆炸 AOE 机制:玉米大炮使用
Physics2D.OverlapCircleAll实现范围伤害,配合爆炸特效数据持久化:使用
PlayerPrefs保存最高分和设置项,重启后保留用户偏好UI 交互细节:Toggle 互斥防递归、跳跃提示脉冲动画、武器倒计时显示、ESC 键多场景响应



