一、项目概述

项目信息 详情
项目名称 倭瓜射手大冒险
游戏类型 横版自动前进弹跳射击跑酷
开发引擎 Unity(2D 模式)
编程语言 C#
核心玩法 玩家操控融合倭瓜与豌豆射手特征的植物角色,通过跳跃躲避敌人、自动射击消灭敌人,收集武器道具增强火力,挑战无限递增难度

项目文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Assets/code/Scripts/
├── Background/ # 背景滚动与地面管理
│ ├── BackgroundScroll.cs
│ └── GroundManager.cs
├── Bullets/ # 子弹系统
│ └── Bullet.cs
├── Enemies/ # 敌人系统
│ ├── Enemy.cs
│ └── EnemySpawner.cs
├── Managers/ # 全局管理器
│ ├── GameManager.cs
│ └── AudioManager.cs
├── Player/ # 玩家控制
│ └── PlayerController.cs
├── Powerups/ # 道具系统
│ ├── Powerup.cs
│ └── PowerupSpawner.cs
└── UI/ # 界面系统
├── UIManager.cs
└── GameInfoDisplay.cs

二、架构设计与设计模式

2.1 单例模式(Singleton Pattern)

项目中有三个核心管理器使用了单例模式:GameManagerAudioManagerGroundManager

实现方式:在 Awake() 中进行实例检测,保证全局唯一访问点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// GameManager.cs
public static GameManager Instance { get; private set; }

private void Awake()
{
if (Instance == null)
{
Instance = this;
}
else
{
Destroy(gameObject);
return;
}
}

面试要点

  • 为什么用单例: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
2
3
public bool IsGameOver { get; private set; }
public bool IsPaused { get; private set; }
public bool IsInMainMenu { 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private bool hasGroundJump;  // 是否有地面跳跃机会
private bool hasAirJump; // 是否有空中跳跃机会

private void HandleInput()
{
bool wantJump = false;

if (Input.GetMouseButtonDown(0))
{
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
return; // 防止点击UI时触发跳跃
wantJump = true;
}
if (Input.GetKeyDown(KeyCode.Space))
wantJump = true;

if (wantJump)
{
if (hasGroundJump)
{
DoJump(jumpForce);
hasGroundJump = false;
hasAirJump = true;
}
else if (hasAirJump)
{
DoJump(jumpForce * 0.85f); // 二段跳力度衰减
hasAirJump = false;
}
}
}

面试要点

  • 双标志位设计hasGroundJumphasAirJump 两个布尔值实现二段跳,比计数器方式更清晰
  • 二段跳力度衰减jumpForce * 0.85f,让二段跳略弱于一段跳,手感更自然
  • UI 穿透防护IsPointerOverGameObject() 防止点击 UI 按钮时误触发跳跃
  • 着地重置:在 FixedUpdate 中检测着地时重置跳跃状态

3.2 自动弹跳(Auto Bounce)

1
2
3
4
5
6
7
8
9
10
11
private void HandleAutoBounce()
{
if (!isGrounded) return;

bounceTimer += Time.deltaTime;
if (bounceTimer >= bounceInterval)
{
rb.velocity = new Vector2(rb.velocity.x, bounceForce);
bounceTimer = 0f;
}
}

面试要点

  • 角色在地面时自动周期性弹跳,营造”倭瓜蹦跳”的生动感
  • 仅在 isGrounded 时触发,空中不弹跳

3.3 着地检测(FixedUpdate 物理检测)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void FixedUpdate()
{
wasGrounded = isGrounded;

if (transform.position.y <= groundY + 0.5f && rb.velocity.y <= 0.1f)
{
rb.velocity = new Vector2(rb.velocity.x, 0f);
rb.position = new Vector2(rb.position.x, groundY + 0.5f);
isGrounded = true;

if (!wasGrounded)
{
hasGroundJump = true;
hasAirJump = false;
TriggerSquash(true); // 触发着地压扁动画
}
}
else
{
isGrounded = false;
}
}

面试要点

  • 为什么用 FixedUpdate:物理检测放在 FixedUpdate 中保证与物理引擎同步,避免 Update 帧率不稳定导致的穿透问题
  • 双重条件检测position.y <= groundY + 0.5f && velocity.y <= 0.1f,位置和速度同时判断,防止上升阶段误判着地
  • wasGrounded 标志:区分”持续在地面”和”刚着地”两种状态,只在刚着地时触发压扁动画和跳跃重置

3.4 Squash & Stretch 动画(挤压拉伸)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
private void HandleSquashStretch()
{
if (!isSquashing)
{
transform.localScale = Vector3.Lerp(transform.localScale, originalScale, Time.deltaTime * 10f);
return;
}

squashTimer += Time.deltaTime;
float t = squashTimer / squashDuration;

if (t >= 1f)
{
isSquashing = false;
transform.localScale = originalScale;
return;
}

float squash = Mathf.Sin(t * Mathf.PI);
if (squashIsLand) // 着地:横向拉伸,纵向压扁
{
transform.localScale = new Vector3(
originalScale.x * (1f + squash * squashAmount),
originalScale.y * (1f - squash * squashAmount),
originalScale.z
);
}
else // 起跳:纵向拉伸,横向收缩
{
transform.localScale = new Vector3(
originalScale.x * (1f - squash * squashAmount * 0.7f),
originalScale.y * (1f + squash * squashAmount),
originalScale.z
);
}
}

面试要点

  • 动画12原则之 Squash & Stretch:经典动画原理,着地时压扁、起跳时拉伸,增强角色动态感
  • Mathf.Sin(t * Mathf.PI) 曲线:0→1→0 的平滑过渡,比线性插值更有弹性感
  • 方向区分:着地和起跳的挤压方向相反,通过 squashIsLand 标志区分
  • Lerp 回弹:非激活状态时用 Lerp 平滑恢复原始缩放,避免突变

3.5 武器系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public enum WeaponType { Pea, CornCannon, MachineGun }

private void HandleShooting()
{
fireTimer += Time.deltaTime;
float fireRate = currentWeapon == WeaponType.MachineGun ? machineGunFireRate :
currentWeapon == WeaponType.CornCannon ? cornFireRate : peaFireRate;
if (fireTimer >= fireRate)
{
GameObject prefab = currentWeapon == WeaponType.MachineGun ? machineGunBulletPrefab :
currentWeapon == WeaponType.CornCannon ? cornBulletPrefab : peaBulletPrefab;
if (prefab != null && firePoint != null)
Instantiate(prefab, firePoint.position, firePoint.rotation);
fireTimer = 0f;
}
}
武器类型 射速 子弹类型 特点
豌豆(Pea) 0.5s/发 普通子弹 基础武器,永久可用
机枪豌豆(MachineGun) 0.1s/发 普通子弹 高速连射,火力压制
玉米大炮(CornCannon) 1s/发 爆炸子弹 范围伤害,清场利器

面试要点

  • 枚举驱动WeaponType 枚举统一管理武器类型,避免魔法字符串
  • 计时器射击fireTimer 累加 Time.deltaTime,达到射速间隔时发射,实现自动射击
  • 限时机制:武器道具持续 weaponDuration(3秒)后自动切回豌豆
  • 嘴部精灵切换UpdateMouthSprite() 根据当前武器切换角色嘴部外观,增强视觉反馈

四、敌人系统

4.1 敌人行为状态机

敌人有两个行为阶段:下落阶段地面阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void Update()
{
if (!isGrounded)
{
// 阶段1:从空中下落
transform.Translate(Vector3.down * fallSpeed * Time.deltaTime);
if (transform.position.y <= groundY + 0.5f)
{
isGrounded = true;
transform.position = new Vector3(transform.position.x, groundY + 0.5f, transform.position.z);
}
}
else
{
// 阶段2:地面移动 + 空闲动画
float speed = GameManager.Instance.GetCurrentScrollSpeed() + moveSpeed;
transform.Translate(Vector3.left * speed * Time.deltaTime);
HandleIdleAnimation();
}
}

面试要点

  • 两阶段设计:敌人从空中落下(增加视觉变化),着地后随背景滚动左移
  • 速度叠加scrollSpeed + moveSpeed,敌人移动速度 = 背景滚动速度 + 自身移动速度,保证敌人始终向左移动
  • OnBecameInvisible 清理:离开屏幕左侧 15 单位后自动销毁,防止内存泄漏

4.2 受击反馈系统

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void TakeDamage(int dmg)
{
health -= dmg;
if (health <= 0)
{
GameManager.Instance.AddScore(scoreValue);
Destroy(gameObject);
}
else
{
if (mainSpriteRenderer != null)
{
mainSpriteRenderer.color = hitColor; // 变红
Invoke(nameof(ResetColor), hitFlashDuration); // 0.1秒后恢复
}
}
}

面试要点

  • 视觉反馈:受击时精灵变红,0.1 秒后恢复原色,给玩家明确的命中感
  • Invoke 延迟调用:简单实现定时恢复颜色,无需额外协程
  • 分数奖励:击杀敌人时通过 GameManager 加分,实现跨组件通信

4.3 空闲呼吸动画(Idle Squash)

1
2
3
4
5
6
7
8
9
10
11
private void HandleIdleAnimation()
{
float t = Mathf.Sin(Time.time * idleSquashSpeed);
float squashY = 1f - t * idleSquashAmount;
float squashX = 1f + t * idleSquashAmount * 0.5f;

transform.localScale = new Vector3(originalScale.x * squashX, originalScale.y * squashY, originalScale.z);

float offsetY = originalScale.y * (1f - squashY) * 0.5f;
transform.position = new Vector3(originalPosition.x, originalPosition.y + offsetY, originalPosition.z);
}

面试要点

  • 正弦波驱动Mathf.Sin(Time.time * speed) 产生周期性缩放,模拟呼吸感
  • Y轴压缩时X轴膨胀:保持体积感的经典动画技巧
  • 位置补偿:缩放时调整 Y 位置,使敌人”脚”始终贴地,不会悬浮或陷入地面

4.4 动态难度系统(EnemySpawner)

1
2
3
4
5
6
7
8
private void IncreaseDifficulty()
{
difficultyLevel++;
currentSpawnInterval = Mathf.Max(minSpawnInterval, currentSpawnInterval - spawnIntervalReduction);
currentMoveSpeedBonus += moveSpeedIncrease;
currentHealthBonus += healthIncrease;
currentScoreBonus += scoreValueIncrease;
}
难度参数 初始值 每级增量 上限
生成间隔 3s -0.3s 0.6s(下限)
移动速度加成 0 +0.3 无上限
生命加成 0 +1 无上限
分数加成 0 +50 无上限

面试要点

  • 多维难度递增:同时调整生成频率、敌人速度、敌人血量、击杀分数,避免单一维度调整导致的体验单调
  • Mathf.Max 保底:生成间隔不低于 0.6s,防止敌人过于密集无法游玩
  • 难度等级可预设SetStartingDifficulty(int level) 允许从指定难度开始,支持设置面板选择起始难度
  • 定时触发:每 difficultyInterval(20秒)提升一级,节奏可预期

五、子弹系统

5.1 子弹基础行为

1
2
3
4
private void Start()
{
rb.velocity = transform.right * speed; // 沿发射方向飞行
}

子弹沿 firePoint 的右方向发射,速度由 speed 参数控制。

5.2 爆炸子弹(AOE 伤害)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void Explode()
{
if (explosionEffect != null)
Instantiate(explosionEffect, transform.position, Quaternion.identity);

Collider2D[] hitColliders = Physics2D.OverlapCircleAll(transform.position, explosionRadius);
foreach (Collider2D collider in hitColliders)
{
if (collider.CompareTag("Enemy"))
{
Enemy enemy = collider.GetComponent<Enemy>();
if (enemy != null)
enemy.TakeDamage(damage);
}
}
Destroy(gameObject);
}

面试要点

  • Physics2D.OverlapCircleAll:圆形范围检测,获取爆炸半径内所有碰撞体,实现 AOE 伤害
  • 爆炸特效:通过 explosionEffect 预制体实例化爆炸视觉效果
  • isExplosive 标志:同一 Bullet 脚本通过布尔标志区分普通子弹和爆炸子弹,复用代码
  • OnDrawGizmosSelected:在编辑器中绘制爆炸半径辅助线,方便调参

5.3 子弹精灵动画

1
2
3
4
5
6
7
8
9
10
11
12
private void HandleAnimation()
{
if (bulletSprites == null || bulletSprites.Length == 0) return;

animationTimer += Time.deltaTime;
if (animationTimer >= animationSpeed)
{
currentSpriteIndex = (currentSpriteIndex + 1) % bulletSprites.Length;
spriteRenderer.sprite = bulletSprites[currentSpriteIndex];
animationTimer = 0f;
}
}

面试要点

  • 帧动画:通过精灵数组循环切换实现逐帧动画(如豌豆飞出效果的3帧动画)
  • 取模循环(index + 1) % length 实现无限循环播放

5.4 越界销毁

1
2
3
4
5
6
7
8
9
10
private void CheckIfOutOfBounds()
{
if (transform.position.x > Camera.main.transform.position.x + Camera.main.orthographicSize * 2f ||
transform.position.x < Camera.main.transform.position.x - Camera.main.orthographicSize * 2f ||
transform.position.y > Camera.main.transform.position.y + Camera.main.orthographicSize ||
transform.position.y < Camera.main.transform.position.y - Camera.main.orthographicSize)
{
Destroy(gameObject);
}
}

面试要点

  • 基于相机视口范围判断,而非固定坐标,适配不同分辨率
  • 水平方向使用 orthographicSize * 2(宽屏),垂直方向使用 orthographicSize

六、道具系统

6.1 道具行为(Powerup)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void Update()
{
if (!isGrounded)
{
transform.Translate(Vector3.down * fallSpeed * Time.deltaTime);
if (transform.position.y <= groundY + 0.8f)
{
isGrounded = true;
transform.position = new Vector3(transform.position.x, groundY + 0.8f, transform.position.z);
}
}
else
{
float scrollSpeed = GameManager.Instance.GetCurrentScrollSpeed();
transform.Translate(Vector3.left * scrollSpeed * Time.deltaTime);
}
}

面试要点

  • 与敌人相似的下落+滚动模式:道具从空中落下,着地后随背景滚动
  • 着地高度不同:道具着地高度 groundY + 0.8f 高于敌人的 groundY + 0.5f,视觉上道具浮在地面上方
  • weaponType 字段:每个道具预制体关联一种武器类型,玩家拾取后激活对应武器

6.2 道具生成器(PowerupSpawner)

1
2
3
4
5
6
7
8
private void SpawnPowerup()
{
GameObject powerupPrefab = powerupPrefabs[Random.Range(0, powerupPrefabs.Length)];
float spawnX = Camera.main.transform.position.x + spawnDistance;
float spawnY = Random.Range(minSpawnHeight, maxSpawnHeight);
Vector3 spawnPosition = new Vector3(spawnX, spawnY, 0f);
Instantiate(powerupPrefab, spawnPosition, Quaternion.identity);
}

面试要点

  • 随机生成间隔Random.Range(minSpawnInterval, maxSpawnInterval) 即 3~7 秒,避免固定节奏
  • 随机高度:道具在 5~8 高度生成,部分需要跳跃才能获取,增加策略性
  • 基于相机位置生成:在相机右侧 spawnDistance 处生成,保证玩家看不到生成过程

七、背景视差滚动系统

7.1 多层视差实现

1
2
3
4
5
[Header("Scroll Multipliers")]
public float cloudScrollMultiplier = 0.2f;
public float backLayerScrollMultiplier = 0.5f;
public float grassLayerScrollMultiplier = 1f;
public float dirtLayerScrollMultiplier = 1f;
层级 速度倍率 视觉效果
云层(Cloud) 0.2x 远景,缓慢移动
背景层(Back) 0.5x 中景,中速移动
草地层(Grass) 1.0x 近景,全速移动
泥土层(Dirt) 1.0x 近景,全速移动

面试要点

  • 视差滚动原理:不同层以不同速度移动,模拟景深效果,近处快、远处慢
  • 倍率驱动:通过 multiplier 参数控制每层速度,易于调整
  • 无限滚动:当某层精灵移出屏幕左侧时,将其移到最右侧精灵的右边

7.2 无限滚动算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void ScrollLayerGroup(Transform[] layers, float multiplier, float deltaTime)
{
float movement = currentScrollSpeed * multiplier * deltaTime;

foreach (Transform layer in layers)
{
layer.Translate(Vector2.left * movement);

SpriteRenderer spriteRenderer = layer.GetComponent<SpriteRenderer>();
if (spriteRenderer != null)
{
float spriteWidth = spriteRenderer.bounds.size.x;
if (layer.position.x < -spriteWidth)
{
float rightmostX = FindRightmostPosition(layers, spriteWidth);
layer.position = new Vector3(rightmostX + spriteWidth, layer.position.y, layer.position.z);
}
}
}
}

面试要点

  • 循环拼接:当精灵完全移出左侧(position.x < -spriteWidth)时,将其接到最右侧精灵后面
  • FindRightmostPosition:动态查找当前最右侧精灵位置,确保拼接无缝
  • 基于 bounds.size.x:使用实际渲染宽度而非固定值,适配不同尺寸的精灵

7.3 速度递增

1
2
3
4
private void IncreaseSpeed()
{
currentScrollSpeed = Mathf.Min(maxScrollSpeed, currentScrollSpeed + scrollSpeedIncreaseRate);
}

每 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private bool isUpdatingSpeedToggles;  // 防止递归触发

private void OnNormalSpeedChanged(bool isOn)
{
if (isUpdatingSpeedToggles) return; // 程序修改时跳过

if (isOn)
{
GameManager.Instance.SetGameSpeed(1f);
isUpdatingSpeedToggles = true;
mediumSpeedToggle.isOn = false;
fastSpeedToggle.isOn = false;
isUpdatingSpeedToggles = false;
}
else
{
isUpdatingSpeedToggles = true;
normalSpeedToggle.isOn = true; // 阻止取消选中
isUpdatingSpeedToggles = false;
}
}

面试要点

  • isUpdatingSpeedToggles 防递归:程序修改 isOn 会触发 onValueChanged 回调,使用标志位防止无限递归
  • Toggle 互斥:选中一个时取消其他,取消选中时强制保持选中(至少选一个)
  • 三档速度:1x / 1.5x / 2.25x,通过 Time.timeScale 实现

8.3 GameInfoDisplay 实时信息

1
2
3
4
5
6
7
8
9
10
11
private void UpdateJumpHint()
{
if (player != null && player.HasJumpedOnce)
{
jumpHintText.gameObject.SetActive(false); // 首次跳跃后永久隐藏
jumpHintHidden = true;
return;
}
float t = (Mathf.Sin(Time.time * pulseSpeed) + 1f) / 2f;
jumpHintText.fontSize = Mathf.RoundToInt(Mathf.Lerp(minFontSize, maxFontSize, t));
}

面试要点

  • 脉冲提示:跳跃提示文字大小在 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
2
3
4
5
6
7
8
9
10
11
启动 → 主菜单(Time.timeScale=0)
├── 开始游戏 → 游戏进行中(Time.timeScale=GameSpeed)
│ ├── 暂停 → 暂停界面(Time.timeScale=0)
│ │ ├── 继续 → 恢复游戏
│ │ ├── 重新开始 → 场景重载
│ │ └── 返回主菜单 → 场景重载
│ └── 生命归零 → 游戏结束(Time.timeScale=0)
│ ├── 重新开始 → 场景重载
│ └── 返回主菜单 → 场景重载
├── 设置 → 设置面板
└── 退出 → 关闭应用

10.2 场景重载机制

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public void RestartGame()
{
if (Score > bestScore)
{
bestScore = Score;
PlayerPrefs.SetInt("BestScore", bestScore);
}
PlayerPrefs.SetInt("SkipMainMenu", 1);
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}

public void ReturnToMainMenu()
{
Time.timeScale = 0f;
SceneManager.LoadScene(SceneManager.GetActiveScene().name);
}

面试要点

  • 统一使用场景重载:重新开始和返回主菜单都通过 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
2
3
4
if (isUpdatingSpeedToggles) return;
isUpdatingSpeedToggles = true;
// 修改其他 Toggle...
isUpdatingSpeedToggles = false;

13.3 无限滚动无缝拼接

问题:多层背景以不同速度滚动时,精灵拼接处可能出现缝隙。

解决方案:动态查找最右侧精灵位置,将移出的精灵精确接到最右侧:

1
2
float rightmostX = FindRightmostPosition(layers, spriteWidth);
layer.position = new Vector3(rightmostX + spriteWidth, layer.position.y, layer.position.z);

13.4 暂停时 UI 点击穿透

问题:点击 UI 按钮时同时触发了玩家的跳跃。

解决方案:使用 EventSystem.current.IsPointerOverGameObject() 检测点击是否在 UI 上:

1
2
if (EventSystem.current != null && EventSystem.current.IsPointerOverGameObject())
return;

十四、项目亮点总结(面试话术)

  1. 完整的游戏循环:从主菜单→游戏→暂停→结束→重开,状态管理清晰,使用 Time.timeScale 控制暂停,布尔标志控制逻辑分支

  2. 多维难度递增系统:同时调整敌人生成频率、移动速度、血量、分数四个维度,配合背景滚动加速,形成平滑的难度曲线

  3. Squash & Stretch 动画:在玩家和敌人上都实现了挤压拉伸动画,遵循动画12原则,增强角色动态表现力

  4. 多层视差滚动:4 层不同速度的背景滚动,实现景深效果,无限循环拼接算法保证无缝

  5. 武器系统设计:三种武器差异化设计(射速/伤害/范围),限时切换机制增加策略性

  6. 爆炸 AOE 机制:玉米大炮使用 Physics2D.OverlapCircleAll 实现范围伤害,配合爆炸特效

  7. 数据持久化:使用 PlayerPrefs 保存最高分和设置项,重启后保留用户偏好

  8. UI 交互细节:Toggle 互斥防递归、跳跃提示脉冲动画、武器倒计时显示、ESC 键多场景响应