吃豆人迷宫:基于数学模型的角色行为设计
本文详细介绍了使用Unity引擎实现经典吃豆人游戏的全过程。重点分析了游戏核心机制的设计与实现,包括:1. 迷宫网格化构建技术;2. 角色状态机设计;3. 幽灵AI的四种差异化行为算法;4. 游戏流程控制与管理。文章深入探讨了A*寻路、向量计算等数学原理在游戏中的应用,并提供了性能优化方案。通过完整的代码示例,展示了如何将数学模型转化为游戏逻辑,实现了一个结构清晰、可扩展的游戏框架。最后还提出了多
吃豆人迷宫:基于数学模型的角色行为设计
18.1 经典游戏重现
吃豆人(Pac-Man)是1980年由日本南梦宫(Namco)公司发布的一款街机游戏,因其简单而富有挑战性的游戏玩法,成为了电子游戏史上最具影响力的作品之一。在这个游戏中,玩家控制一个圆形角色在迷宫中移动,吃掉所有的豆子,同时躲避四个不同颜色的幽灵怪物的追击。当吃到特殊的大力丸后,玩家暂时获得吃掉幽灵的能力,为游戏增添了战略深度。
本章将带领读者使用Unity 2021.3.8f1c1引擎,重新实现这款经典游戏,并深入探讨其背后的数学模型和算法原理。通过分析游戏中角色的移动和决策机制,我们将学习如何运用数学知识来设计游戏逻辑,以及如何利用面向对象编程的思想,构建一个结构清晰、可扩展的游戏框架。
吃豆人游戏的魅力不仅在于其简单的规则,更在于四个幽灵各具特色的AI行为,使得游戏既有挑战性又富有策略性。在本章中,我们将重点关注如何实现这些幽灵的AI逻辑,以及如何通过数学算法使它们表现出不同的追逐策略。
18.2 吃豆人游戏玩法详解
在开始实现游戏之前,我们需要全面了解吃豆人的游戏规则和机制,这些将成为我们编程的基础。
18.2.1 基本游戏元素
吃豆人游戏的主要元素包括:
- 迷宫:由墙壁、通道和转角组成的封闭区域,通常采用网格结构设计。
- 吃豆人:玩家控制的主角,可以在迷宫中上下左右移动。
- 豆子:分布在迷宫通道中的小点,吃豆人吃掉所有豆子即完成关卡。
- 大力丸:特殊的大豆子,吃掉后可以暂时赋予吃豆人吃掉幽灵的能力。
- 幽灵:四个具有不同颜色和行为模式的敌人,会追逐吃豆人。
- 水果:出现在迷宫中心的特殊奖励物品,吃掉可以获得额外分数。
18.2.2 游戏目标与计分规则
游戏的基本目标是控制吃豆人吃掉迷宫中所有的豆子,同时避开或吃掉幽灵。具体的计分规则如下:
- 吃一个普通豆子:10分
- 吃一个大力丸:50分
- 吃掉幽灵(仅在吃了大力丸后):200分(第一个)、400分(第二个)、800分(第三个)、1600分(第四个)
- 吃水果:根据水果类型获得100-5000不等的分数
18.2.3 角色行为模式
吃豆人游戏的核心在于四个幽灵的不同行为模式,这使得游戏充满了策略性和可玩性:
- 红色幽灵(Blinky):直接追逐吃豆人,采用最短路径算法。
- 粉色幽灵(Pinky):试图预判吃豆人的位置,瞄准吃豆人前方的位置。
- 青色幽灵(Inky):行为较为复杂,结合红色幽灵和吃豆人的位置来确定目标。
- 橙色幽灵(Clyde):当距离吃豆人较远时追逐,靠近后则散开,表现得较为随机。
这些不同的行为模式使得幽灵们形成了一种无意识的合作,从不同方向包围吃豆人,增加了游戏的挑战性。
18.2.4 游戏状态与转换
吃豆人游戏中的状态转换也是一个重要方面:
- 追逐模式(Chase):幽灵按照各自的算法追逐吃豆人。
- 散开模式(Scatter):幽灵各自前往迷宫的一个角落。
- 惊吓模式(Frightened):吃豆人吃掉大力丸后,幽灵变成蓝色并随机移动,可被吃掉。
- 被吃状态:幽灵被吃后变成眼睛,快速返回初始位置重生。
游戏在追逐和散开模式之间周期性切换,创造了紧张与缓和的节奏变化。
18.3 游戏设计理念
18.3.1 网格化地图构建
吃豆人游戏的迷宫是一个经典的网格结构,我们可以使用二维数组来表示。每个格子可以是墙壁、通道、豆子或大力丸。这种数据结构使得角色移动和碰撞检测变得简单高效。
在数学上,我们可以将迷宫表示为一个图(Graph),其中节点是可移动的格子,边是连接相邻格子的通道。这种表示方法对于实现幽灵的寻路算法非常有用。
csharp
public class MazeGenerator
{
// 迷宫单元类型
public enum CellType
{
Wall,
Path,
Dot,
PowerPellet,
Empty
}
// 迷宫大小
private int width;
private int height;
// 迷宫数据
private CellType[,] maze;
// 构造函数
public MazeGenerator(int width, int height)
{
this.width = width;
this.height = height;
this.maze = new CellType[width, height];
// 初始化迷宫(全部设为墙)
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
maze[x, y] = CellType.Wall;
}
}
}
// 生成标准吃豆人迷宫
public CellType[,] GenerateClassicMaze()
{
// 设置基础路径
SetBasicLayout();
// 放置豆子和大力丸
PlaceDotsAndPowerPellets();
return maze;
}
// 设置基础布局
private void SetBasicLayout()
{
// 这里实现经典吃豆人迷宫的布局
// 为简化示例,我们使用硬编码的方式
// 创建水平通道
for (int x = 1; x < width - 1; x++)
{
maze[x, 1] = CellType.Empty;
maze[x, height - 2] = CellType.Empty;
maze[x, height / 2] = CellType.Empty;
}
// 创建垂直通道
for (int y = 1; y < height - 1; y++)
{
maze[1, y] = CellType.Empty;
maze[width - 2, y] = CellType.Empty;
maze[width / 2, y] = CellType.Empty;
}
// 添加一些额外的通道和障碍物
AddAdditionalPaths();
}
// 添加额外的通道和障碍物
private void AddAdditionalPaths()
{
// 这里可以添加更复杂的迷宫结构
// 根据经典吃豆人游戏的迷宫布局
}
// 放置豆子和大力丸
private void PlaceDotsAndPowerPellets()
{
// 在空通道上放置普通豆子
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
if (maze[x, y] == CellType.Empty)
{
maze[x, y] = CellType.Dot;
}
}
}
// 在特定位置放置大力丸
if (width >= 3 && height >= 3)
{
maze[1, 1] = CellType.PowerPellet;
maze[width - 2, 1] = CellType.PowerPellet;
maze[1, height - 2] = CellType.PowerPellet;
maze[width - 2, height - 2] = CellType.PowerPellet;
}
// 幽灵初始位置不放豆子
int centerX = width / 2;
int centerY = height / 2;
maze[centerX, centerY] = CellType.Empty;
maze[centerX - 1, centerY] = CellType.Empty;
maze[centerX + 1, centerY] = CellType.Empty;
maze[centerX, centerY - 1] = CellType.Empty;
maze[centerX, centerY + 1] = CellType.Empty;
}
// 从文件加载迷宫布局
public void LoadMazeFromFile(string filePath)
{
try
{
string[] lines = System.IO.File.ReadAllLines(filePath);
height = lines.Length;
width = lines[0].Length;
maze = new CellType[width, height];
for (int y = 0; y < height; y++)
{
string line = lines[y];
for (int x = 0; x < width && x < line.Length; x++)
{
switch (line[x])
{
case '#': // 墙
maze[x, y] = CellType.Wall;
break;
case '.': // 豆子
maze[x, y] = CellType.Dot;
break;
case 'O': // 大力丸
maze[x, y] = CellType.PowerPellet;
break;
case ' ': // 空通道
maze[x, y] = CellType.Empty;
break;
default:
maze[x, y] = CellType.Wall;
break;
}
}
}
}
catch (System.Exception e)
{
UnityEngine.Debug.LogError($"Failed to load maze from file: {e.Message}");
}
}
// 获取迷宫数据
public CellType[,] GetMaze()
{
return maze;
}
}
上面的代码展示了如何创建一个基本的迷宫生成器,它可以通过硬编码或从文件加载的方式生成吃豆人游戏的迷宫布局。在实际项目中,我们可以扩展这个类,添加更多自定义的迷宫生成算法。
18.3.2 状态机理论应用
状态机(State Machine)是游戏开发中常用的一种设计模式,特别适合用来管理角色的行为状态。在吃豆人游戏中,幽灵的行为就可以通过状态机来实现。
从数学角度看,状态机可以表示为一个五元组(Q, Σ, δ, q0, F),其中Q是所有可能状态的集合,Σ是所有可能输入的集合,δ是状态转移函数,q0是初始状态,F是接受状态的集合。在游戏中,我们可以简化这个概念,将其实现为一系列状态类和状态转换逻辑。
csharp
// 幽灵状态基类
public abstract class GhostState
{
protected Ghost ghost;
public GhostState(Ghost ghost)
{
this.ghost = ghost;
}
// 进入状态时调用
public virtual void Enter() {}
// 状态更新逻辑
public abstract void Update();
// 退出状态时调用
public virtual void Exit() {}
}
// 追逐状态
public class ChaseState : GhostState
{
public ChaseState(Ghost ghost) : base(ghost) {}
public override void Enter()
{
// 设置追逐状态的外观和速度
ghost.SetColor(ghost.originalColor);
ghost.SetSpeed(ghost.normalSpeed);
}
public override void Update()
{
// 根据幽灵类型计算目标位置
Vector2 targetPosition = ghost.CalculateTargetPosition();
// 使用寻路算法移动向目标
ghost.MoveTowards(targetPosition);
// 检查是否应该切换到散开状态
if (ghost.ShouldSwitchToScatter())
{
ghost.ChangeState(new ScatterState(ghost));
}
// 检查是否吃到了大力丸
if (ghost.gameManager.IsPowerPelletActive())
{
ghost.ChangeState(new FrightenedState(ghost));
}
}
}
// 散开状态
public class ScatterState : GhostState
{
// 实现类似于ChaseState
}
// 惊吓状态
public class FrightenedState : GhostState
{
private float duration;
private float timer;
public FrightenedState(Ghost ghost) : base(ghost)
{
duration = ghost.gameManager.GetPowerPelletDuration();
timer = 0;
}
public override void Enter()
{
// 设置惊吓状态的外观和速度
ghost.SetColor(Color.blue);
ghost.SetSpeed(ghost.frightenedSpeed);
}
public override void Update()
{
// 增加计时器
timer += Time.deltaTime;
// 闪烁提示即将结束
if (timer > duration * 0.7f)
{
ghost.Blink();
}
// 随机移动
ghost.MoveRandomly();
// 检查是否应该返回正常状态
if (timer >= duration)
{
ghost.ChangeState(new ChaseState(ghost));
}
}
}
// 被吃状态
public class EatenState : GhostState
{
// 实现被吃后的返回出生点逻辑
}
以上代码展示了如何使用状态模式来实现幽灵的不同行为状态。每个状态都有自己的进入、更新和退出逻辑,幽灵对象可以在不同状态之间切换,展现不同的行为。
18.3.3 幽灵AI数学模型
幽灵的AI行为是吃豆人游戏最有趣的部分之一。每个幽灵都有自己独特的追逐算法,这些算法可以用数学公式和向量计算来表示。
-
红色幽灵(Blinky)的直接追逐:
目标位置就是吃豆人的当前位置:target = pacmanPosition -
粉色幽灵(Pinky)的预判追逐:
目标位置是吃豆人当前位置加上吃豆人面向方向的4个单位:
target = pacmanPosition + 4 * pacmanDirection -
青色幽灵(Inky)的复杂追逐:
首先计算吃豆人位置加上面向方向2个单位的中间点:
middlePoint = pacmanPosition + 2 * pacmanDirection
然后计算从红色幽灵位置到这个中间点的向量,并将其延长两倍:
target = blinkyPosition + 2 * (middlePoint - blinkyPosition) -
橙色幽灵(Clyde)的距离敏感追逐:
如果与吃豆人的距离超过8个单位,则像红色幽灵一样直接追逐:
target = pacmanPosition
否则前往自己的散开角落:
target = scatterTarget
我们可以在Ghost类中实现这些算法:
csharp
public class Ghost : MonoBehaviour
{
public enum GhostType
{
Blinky, // 红色
Pinky, // 粉色
Inky, // 青色
Clyde // 橙色
}
// 幽灵类型
public GhostType ghostType;
// 颜色和速度
public Color originalColor;
public float normalSpeed = 3.5f;
public float frightenedSpeed = 2.5f;
public float eatenSpeed = 5f;
// 散开时的目标点
public Vector2 scatterTarget;
// 游戏管理器引用
public GameManager gameManager;
// 当前状态
private GhostState currentState;
// 初始化
void Start()
{
// 设置初始状态为散开
ChangeState(new ScatterState(this));
}
// 更新
void Update()
{
// 更新当前状态
if (currentState != null)
{
currentState.Update();
}
}
// 计算目标位置
public Vector2 CalculateTargetPosition()
{
// 获取吃豆人位置和方向
Vector2 pacmanPosition = gameManager.GetPacmanPosition();
Vector2 pacmanDirection = gameManager.GetPacmanDirection();
switch (ghostType)
{
case GhostType.Blinky:
// 红色幽灵:直接追踪吃豆人
return pacmanPosition;
case GhostType.Pinky:
// 粉色幽灵:瞄准吃豆人前方4个单位
return pacmanPosition + 4 * pacmanDirection;
case GhostType.Inky:
// 青色幽灵:复杂的位置计算
Vector2 middlePoint = pacmanPosition + 2 * pacmanDirection;
Vector2 blinkyPosition = gameManager.GetGhostPosition(GhostType.Blinky);
return blinkyPosition + 2 * (middlePoint - blinkyPosition);
case GhostType.Clyde:
// 橙色幽灵:根据距离决定是追逐还是散开
float distanceToPacman = Vector2.Distance(transform.position, pacmanPosition);
if (distanceToPacman > 8f)
{
return pacmanPosition;
}
else
{
return scatterTarget;
}
default:
return pacmanPosition;
}
}
// 向目标移动
public void MoveTowards(Vector2 targetPosition)
{
// 实现A*寻路算法,向目标移动
// 这里简化为向目标方向移动
Vector2 currentPosition = transform.position;
// 在网格上,我们只能向上、下、左、右四个方向移动
// 根据当前位置,计算可以移动的方向
List<Vector2> possibleDirections = GetPossibleDirections();
if (possibleDirections.Count == 0)
{
// 无路可走,保持原位
return;
}
// 选择最接近目标的方向
Vector2 bestDirection = Vector2.zero;
float bestDistance = float.MaxValue;
foreach (Vector2 direction in possibleDirections)
{
// 避免180度转弯(除非别无选择)
if (direction == -GetCurrentDirection() && possibleDirections.Count > 1)
{
continue;
}
Vector2 newPosition = currentPosition + direction;
float distance = Vector2.Distance(newPosition, targetPosition);
if (distance < bestDistance)
{
bestDistance = distance;
bestDirection = direction;
}
}
// 移动到最佳方向
MoveInDirection(bestDirection);
}
// 随机移动(惊吓状态使用)
public void MoveRandomly()
{
List<Vector2> possibleDirections = GetPossibleDirections();
// 过滤掉180度转弯的方向
Vector2 currentDirection = GetCurrentDirection();
possibleDirections.RemoveAll(dir => dir == -currentDirection && possibleDirections.Count > 1);
if (possibleDirections.Count > 0)
{
// 随机选择一个方向
int randomIndex = Random.Range(0, possibleDirections.Count);
MoveInDirection(possibleDirections[randomIndex]);
}
}
// 获取可能的移动方向
private List<Vector2> GetPossibleDirections()
{
List<Vector2> directions = new List<Vector2>();
Vector2 currentPosition = transform.position;
// 四个基本方向
Vector2[] basicDirections = new Vector2[] {
Vector2.up, Vector2.down, Vector2.left, Vector2.right
};
foreach (Vector2 direction in basicDirections)
{
Vector2 newPosition = currentPosition + direction;
if (!gameManager.IsWall(newPosition))
{
directions.Add(direction);
}
}
return directions;
}
// 获取当前方向
private Vector2 GetCurrentDirection()
{
// 实现获取当前移动方向的逻辑
return Vector2.right; // 默认向右,实际应根据幽灵当前移动方向返回
}
// 沿着指定方向移动
private void MoveInDirection(Vector2 direction)
{
// 实现沿指定方向移动的逻辑
float speed = normalSpeed;
if (currentState is FrightenedState)
{
speed = frightenedSpeed;
}
else if (currentState is EatenState)
{
speed = eatenSpeed;
}
transform.position = (Vector2)transform.position + direction * speed * Time.deltaTime;
}
// 改变状态
public void ChangeState(GhostState newState)
{
if (currentState != null)
{
currentState.Exit();
}
currentState = newState;
if (currentState != null)
{
currentState.Enter();
}
}
// 检查是否应该切换到散开状态
public bool ShouldSwitchToScatter()
{
// 根据游戏时间和规则判断是否应该进入散开状态
return gameManager.IsScatterMode();
}
// 设置颜色
public void SetColor(Color color)
{
// 设置幽灵的颜色
GetComponent<SpriteRenderer>().color = color;
}
// 设置速度
public void SetSpeed(float speed)
{
// 当前使用的是在MoveInDirection方法中动态获取速度
}
// 闪烁效果(惊吓状态即将结束时)
public void Blink()
{
// 实现闪烁效果
float blinkRate = 0.2f; // 每0.2秒切换一次颜色
bool isWhite = Mathf.FloorToInt(Time.time / blinkRate) % 2 == 0;
if (isWhite)
{
SetColor(Color.white);
}
else
{
SetColor(Color.blue);
}
}
}
以上代码展示了如何实现幽灵的基本行为和特定的追逐算法。通过向量数学和条件判断,我们可以重现经典吃豆人游戏中幽灵的行为模式。
18.3.4 游戏流程控制图
游戏流程是通过一系列状态转换和事件触发来控制的。我们可以用流程图来描述吃豆人游戏的主要流程:
初始化游戏
↓
加载迷宫和角色
↓
游戏开始
↓
主游戏循环 ←────┐
↓ │
玩家输入处理 │
↓ │
吃豆人移动 │
↓ │
检测碰撞和吃豆 │
↓ │
幽灵AI更新 │
↓ │
检查胜利/失败条件│
↓ │
更新UI和音效 │
↓ │
等待下一帧 ─────┘
↓
游戏结束
↓
显示结果和分数
这个流程图描述了游戏的基本运行逻辑,从初始化到主循环再到结束。在实际实现中,我们需要通过GameManager类来管理这个流程:
csharp
public class GameManager : MonoBehaviour
{
// 游戏状态
public enum GameState
{
Ready, // 准备开始
Playing, // 游戏中
Paused, // 暂停
GameOver // 游戏结束
}
// 当前游戏状态
private GameState currentState = GameState.Ready;
// 迷宫引用
public MazeController mazeController;
// 角色引用
public PacmanController pacman;
public Ghost[] ghosts;
// UI引用
public UIManager uiManager;
// 游戏参数
private int score = 0;
private int lives = 3;
private int level = 1;
private int dotsRemaining = 0;
// 大力丸状态
private bool powerPelletActive = false;
private float powerPelletTimer = 0f;
private float powerPelletDuration = 8f;
// 幽灵模式切换
private bool isScatterMode = true;
private float modeTimer = 0f;
private float[] chaseDurations = { 20f, 20f, 20f, 20f, 5f, 20f, 5f }; // 第7个后永久追逐
private float[] scatterDurations = { 7f, 7f, 5f, 5f, 5f, 5f, 5f }; // 持续时间
private int modeIndex = 0;
// 游戏初始化
void Start()
{
// 初始化迷宫
InitializeMaze();
// 初始化角色
InitializeCharacters();
// 开始游戏准备阶段
StartCoroutine(ReadySequence());
}
// 初始化迷宫
private void InitializeMaze()
{
mazeController.Initialize();
dotsRemaining = mazeController.GetTotalDots();
}
// 初始化角色
private void InitializeCharacters()
{
// 设置吃豆人初始位置
pacman.Initialize();
// 设置幽灵初始位置和目标
foreach (Ghost ghost in ghosts)
{
ghost.gameManager = this;
ghost.scatterTarget = GetGhostScatterTarget(ghost.ghostType);
// 设置初始状态
ghost.ChangeState(new ScatterState(ghost));
}
}
// 获取幽灵的散开目标点
private Vector2 GetGhostScatterTarget(Ghost.GhostType ghostType)
{
// 分配迷宫四个角落作为散开目标
switch (ghostType)
{
case Ghost.GhostType.Blinky:
return new Vector2(mazeController.width - 2, 1);
case Ghost.GhostType.Pinky:
return new Vector2(1, 1);
case Ghost.GhostType.Inky:
return new Vector2(mazeController.width - 2, mazeController.height - 2);
case Ghost.GhostType.Clyde:
return new Vector2(1, mazeController.height - 2);
default:
return Vector2.zero;
}
}
// 准备开始序列
private IEnumerator ReadySequence()
{
uiManager.ShowReadyText(true);
yield return new WaitForSeconds(2f);
uiManager.ShowReadyText(false);
currentState = GameState.Playing;
// 启动模式计时器
StartCoroutine(ModeSwitchingRoutine());
}
// 更新函数
void Update()
{
if (currentState != GameState.Playing)
{
return;
}
// 更新大力丸计时器
if (powerPelletActive)
{
powerPelletTimer -= Time.deltaTime;
if (powerPelletTimer <= 0f)
{
DeactivatePowerPellet();
}
}
// 更新UI
uiManager.UpdateScore(score);
uiManager.UpdateLives(lives);
}
// 幽灵模式切换协程
private IEnumerator ModeSwitchingRoutine()
{
while (currentState == GameState.Playing)
{
if (isScatterMode)
{
// 散开模式
yield return new WaitForSeconds(scatterDurations[Mathf.Min(modeIndex, scatterDurations.Length - 1)]);
// 切换到追逐模式
isScatterMode = false;
SwitchGhostsToChaseMode();
}
else
{
// 追逐模式
yield return new WaitForSeconds(chaseDurations[Mathf.Min(modeIndex, chaseDurations.Length - 1)]);
// 切换到散开模式
isScatterMode = true;
SwitchGhostsToScatterMode();
// 增加模式索引
modeIndex = Mathf.Min(modeIndex + 1, chaseDurations.Length - 1);
}
}
}
// 切换幽灵到追逐模式
private void SwitchGhostsToChaseMode()
{
foreach (Ghost ghost in ghosts)
{
// 只有当幽灵不处于惊吓或被吃状态时才切换
if (!(ghost.currentState is FrightenedState) &&
!(ghost.currentState is EatenState))
{
ghost.ChangeState(new ChaseState(ghost));
}
}
}
// 切换幽灵到散开模式
private void SwitchGhostsToScatterMode()
{
foreach (Ghost ghost in ghosts)
{
// 只有当幽灵不处于惊吓或被吃状态时才切换
if (!(ghost.currentState is FrightenedState) &&
!(ghost.currentState is EatenState))
{
ghost.ChangeState(new ScatterState(ghost));
}
}
}
// 吃豆人吃到大力丸
public void ActivatePowerPellet()
{
powerPelletActive = true;
powerPelletTimer = powerPelletDuration;
// 将所有幽灵切换到惊吓状态
foreach (Ghost ghost in ghosts)
{
// 只有未被吃的幽灵才切换到惊吓状态
if (!(ghost.currentState is EatenState))
{
ghost.ChangeState(new FrightenedState(ghost));
}
}
// 播放吃大力丸音效
AudioManager.Instance.PlayPowerPelletSound();
}
// 大力丸效果结束
private void DeactivatePowerPellet()
{
powerPelletActive = false;
// 将幽灵恢复到正常状态
foreach (Ghost ghost in ghosts)
{
// 只有处于惊吓状态的幽灵才需要恢复
if (ghost.currentState is FrightenedState)
{
// 根据当前模式恢复到追逐或散开
if (isScatterMode)
{
ghost.ChangeState(new ScatterState(ghost));
}
else
{
ghost.ChangeState(new ChaseState(ghost));
}
}
}
}
// 吃豆人吃到豆子
public void EatDot(Vector2 position)
{
// 增加分数
score += 10;
// 减少剩余豆子数量
dotsRemaining--;
// 更新迷宫状态
mazeController.RemoveDot(position);
// 检查是否通关
if (dotsRemaining <= 0)
{
LevelComplete();
}
// 播放吃豆子音效
AudioManager.Instance.PlayDotSound();
}
// 吃豆人吃到大力丸
public void EatPowerPellet(Vector2 position)
{
// 增加分数
score += 50;
// 减少剩余豆子数量
dotsRemaining--;
// 更新迷宫状态
mazeController.RemovePowerPellet(position);
// 激活大力丸效果
ActivatePowerPellet();
// 检查是否通关
if (dotsRemaining <= 0)
{
LevelComplete();
}
}
// 吃豆人被幽灵抓到
public void PacmanCaught()
{
if (powerPelletActive)
{
// 大力丸激活时,幽灵应该被吃掉,不是吃豆人被抓
return;
}
// 减少生命
lives--;
if (lives <= 0)
{
GameOver(false);
}
else
{
// 重置角色位置
ResetPositions();
// 暂停一下游戏
StartCoroutine(PauseBeforeResuming());
}
// 播放死亡音效
AudioManager.Instance.PlayDeathSound();
}
// 幽灵被吃豆人吃掉
public void GhostEaten(Ghost ghost)
{
// 计算得分(每个连续吃掉的幽灵得分翻倍)
int ghostScore = 200 * (int)Mathf.Pow(2, CountEatenGhosts());
score += ghostScore;
// 显示得分UI
uiManager.ShowGhostScore(ghost.transform.position, ghostScore);
// 切换幽灵到被吃状态
ghost.ChangeState(new EatenState(ghost));
// 播放吃幽灵音效
AudioManager.Instance.PlayGhostEatenSound();
}
// 计算已经吃掉的幽灵数量
private int CountEatenGhosts()
{
int count = 0;
foreach (Ghost ghost in ghosts)
{
if (ghost.currentState is EatenState)
{
count++;
}
}
return count;
}
// 通关
private void LevelComplete()
{
currentState = GameState.Ready;
// 停止所有协程
StopAllCoroutines();
// 显示通关UI
uiManager.ShowLevelComplete();
// 准备下一关
StartCoroutine(PrepareNextLevel());
}
// 准备下一关
private IEnumerator PrepareNextLevel()
{
yield return new WaitForSeconds(3f);
// 增加关卡
level++;
// 重置游戏状态但保留得分
ResetGame(true);
// 重新开始游戏
StartCoroutine(ReadySequence());
}
// 游戏结束
private void GameOver(bool win)
{
currentState = GameState.GameOver;
// 停止所有协程
StopAllCoroutines();
// 显示游戏结束UI
uiManager.ShowGameOver(win);
}
// 暂停后继续
private IEnumerator PauseBeforeResuming()
{
currentState = GameState.Paused;
yield return new WaitForSeconds(2f);
currentState = GameState.Playing;
}
// 重置角色位置
private void ResetPositions()
{
// 重置吃豆人位置
pacman.ResetPosition();
// 重置幽灵位置
foreach (Ghost ghost in ghosts)
{
ghost.ResetPosition();
// 重置状态
ghost.ChangeState(new ScatterState(ghost));
}
// 重置模式计时
isScatterMode = true;
modeIndex = 0;
// 关闭大力丸效果
powerPelletActive = false;
}
// 重置游戏
private void ResetGame(bool keepScore)
{
// 重置游戏参数
if (!keepScore)
{
score = 0;
}
lives = 3;
// 重置迷宫
InitializeMaze();
// 重置角色位置
ResetPositions();
// 重置大力丸状态
powerPelletActive = false;
// 重置模式切换
isScatterMode = true;
modeIndex = 0;
}
// 获取吃豆人位置
public Vector2 GetPacmanPosition()
{
return pacman.transform.position;
}
// 获取吃豆人方向
public Vector2 GetPacmanDirection()
{
return pacman.GetDirection();
}
// 获取幽灵位置
public Vector2 GetGhostPosition(Ghost.GhostType ghostType)
{
foreach (Ghost ghost in ghosts)
{
if (ghost.ghostType == ghostType)
{
return ghost.transform.position;
}
}
return Vector2.zero;
}
// 检查位置是否是墙
public bool IsWall(Vector2 position)
{
return mazeController.IsWall(position);
}
// 检查是否处于散开模式
public bool IsScatterMode()
{
return isScatterMode;
}
// 检查大力丸是否激活
public bool IsPowerPelletActive()
{
return powerPelletActive;
}
// 获取大力丸持续时间
public float GetPowerPelletDuration()
{
return powerPelletDuration;
}
}
这个GameManager类实现了整个游戏的流程控制,包括初始化、状态管理、角色交互以及胜负判定等功能。通过这个类,我们可以将游戏的各个组件联系起来,形成一个完整的系统。
18.4 实际代码实现
18.4.1 环境准备与项目设置
在开始实现代码之前,我们需要做一些准备工作,包括设置Unity项目和导入必要的资源。
- 创建一个新的Unity 2021.3.8f1c1项目,选择2D模板。
- 设置项目分辨率为480x640(保持经典游戏风格的纵横比)。
- 导入吃豆人游戏所需的精灵图片,包括:
- 吃豆人角色(不同方向和动画帧)
- 四个幽灵(正常、惊吓和被吃状态)
- 迷宫墙壁和背景
- 豆子、大力丸和水果
- UI元素(分数、生命图标等)
设置完成后,我们可以创建基本的场景结构:
csharp
// 场景初始化脚本
public class SceneInitializer : MonoBehaviour
{
// 预制体引用
public GameObject mazeControllerPrefab;
public GameObject pacmanPrefab;
public GameObject[] ghostPrefabs;
public GameObject gameManagerPrefab;
public GameObject uiManagerPrefab;
// 初始化
void Awake()
{
// 创建游戏管理器
GameObject gameManagerObj = Instantiate(gameManagerPrefab);
GameManager gameManager = gameManagerObj.GetComponent<GameManager>();
// 创建迷宫控制器
GameObject mazeControllerObj = Instantiate(mazeControllerPrefab);
MazeController mazeController = mazeControllerObj.GetComponent<MazeController>();
// 创建吃豆人
GameObject pacmanObj = Instantiate(pacmanPrefab);
PacmanController pacman = pacmanObj.GetComponent<PacmanController>();
// 创建幽灵
List<Ghost> ghosts = new List<Ghost>();
for (int i = 0; i < ghostPrefabs.Length; i++)
{
GameObject ghostObj = Instantiate(ghostPrefabs[i]);
Ghost ghost = ghostObj.GetComponent<Ghost>();
ghost.ghostType = (Ghost.GhostType)i;
ghosts.Add(ghost);
}
// 创建UI管理器
GameObject uiManagerObj = Instantiate(uiManagerPrefab);
UIManager uiManager = uiManagerObj.GetComponent<UIManager>();
// 设置引用
gameManager.mazeController = mazeController;
gameManager.pacman = pacman;
gameManager.ghosts = ghosts.ToArray();
gameManager.uiManager = uiManager;
// 启动游戏
gameManager.StartGame();
}
}
这个脚本负责创建游戏所需的所有对象,并设置它们之间的引用关系。通过这种方式,我们可以确保游戏初始化的顺序是正确的。
18.4.2 迷宫和游戏场景设计
迷宫是吃豆人游戏的核心元素,我们需要一个专门的控制器来管理迷宫的创建和状态更新:
csharp
public class MazeController : MonoBehaviour
{
// 迷宫尺寸
public int width = 28;
public int height = 31;
// 迷宫单元格大小
public float cellSize = 1f;
// 迷宫预制体
public GameObject wallPrefab;
public GameObject dotPrefab;
public GameObject powerPelletPrefab;
// 迷宫数据
private MazeGenerator.CellType[,] maze;
// 游戏对象引用
private Dictionary<Vector2, GameObject> mazeObjects = new Dictionary<Vector2, GameObject>();
// 总豆子数量
private int totalDots = 0;
// 初始化迷宫
public void Initialize()
{
// 清理现有迷宫
ClearMaze();
// 生成迷宫数据
MazeGenerator mazeGenerator = new MazeGenerator(width, height);
maze = mazeGenerator.GenerateClassicMaze();
// 或者从文件加载经典迷宫
// mazeGenerator.LoadMazeFromFile("ClassicMaze.txt");
// maze = mazeGenerator.GetMaze();
// 创建迷宫游戏对象
CreateMazeObjects();
// 计算总豆子数量
CountTotalDots();
}
// 清理迷宫
private void ClearMaze()
{
// 销毁所有迷宫对象
foreach (GameObject obj in mazeObjects.Values)
{
Destroy(obj);
}
mazeObjects.Clear();
}
// 创建迷宫游戏对象
private void CreateMazeObjects()
{
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
Vector2 position = new Vector2(x * cellSize, y * cellSize);
switch (maze[x, y])
{
case MazeGenerator.CellType.Wall:
CreateWall(position);
break;
case MazeGenerator.CellType.Dot:
CreateDot(position);
break;
case MazeGenerator.CellType.PowerPellet:
CreatePowerPellet(position);
break;
}
}
}
}
// 创建墙壁
private void CreateWall(Vector2 position)
{
GameObject wall = Instantiate(wallPrefab, position, Quaternion.identity);
wall.transform.parent = transform;
mazeObjects[position] = wall;
}
// 创建豆子
private void CreateDot(Vector2 position)
{
GameObject dot = Instantiate(dotPrefab, position, Quaternion.identity);
dot.transform.parent = transform;
mazeObjects[position] = dot;
}
// 创建大力丸
private void CreatePowerPellet(Vector2 position)
{
GameObject powerPellet = Instantiate(powerPelletPrefab, position, Quaternion.identity);
powerPellet.transform.parent = transform;
mazeObjects[position] = powerPellet;
}
// 计算总豆子数量
private void CountTotalDots()
{
totalDots = 0;
for (int x = 0; x < width; x++)
{
for (int y = 0; y < height; y++)
{
if (maze[x, y] == MazeGenerator.CellType.Dot ||
maze[x, y] == MazeGenerator.CellType.PowerPellet)
{
totalDots++;
}
}
}
}
// 移除豆子
public void RemoveDot(Vector2 position)
{
// 将位置转换为网格坐标
int x = Mathf.RoundToInt(position.x / cellSize);
int y = Mathf.RoundToInt(position.y / cellSize);
if (x >= 0 && x < width && y >= 0 && y < height)
{
if (maze[x, y] == MazeGenerator.CellType.Dot)
{
maze[x, y] = MazeGenerator.CellType.Empty;
// 移除豆子游戏对象
Vector2 exactPosition = new Vector2(x * cellSize, y * cellSize);
if (mazeObjects.ContainsKey(exactPosition))
{
Destroy(mazeObjects[exactPosition]);
mazeObjects.Remove(exactPosition);
}
}
}
}
// 移除大力丸
public void RemovePowerPellet(Vector2 position)
{
// 将位置转换为网格坐标
int x = Mathf.RoundToInt(position.x / cellSize);
int y = Mathf.RoundToInt(position.y / cellSize);
if (x >= 0 && x < width && y >= 0 && y < height)
{
if (maze[x, y] == MazeGenerator.CellType.PowerPellet)
{
maze[x, y] = MazeGenerator.CellType.Empty;
// 移除大力丸游戏对象
Vector2 exactPosition = new Vector2(x * cellSize, y * cellSize);
if (mazeObjects.ContainsKey(exactPosition))
{
Destroy(mazeObjects[exactPosition]);
mazeObjects.Remove(exactPosition);
}
}
}
}
// 检查位置是否是墙
public bool IsWall(Vector2 position)
{
// 将位置转换为网格坐标
int x = Mathf.RoundToInt(position.x / cellSize);
int y = Mathf.RoundToInt(position.y / cellSize);
// 如果超出边界,视为墙
if (x < 0 || x >= width || y < 0 || y >= height)
{
return true;
}
return maze[x, y] == MazeGenerator.CellType.Wall;
}
// 获取总豆子数量
public int GetTotalDots()
{
return totalDots;
}
// 获取单元格类型
public MazeGenerator.CellType GetCellType(Vector2 position)
{
// 将位置转换为网格坐标
int x = Mathf.RoundToInt(position.x / cellSize);
int y = Mathf.RoundToInt(position.y / cellSize);
if (x >= 0 && x < width && y >= 0 && y < height)
{
return maze[x, y];
}
return MazeGenerator.CellType.Wall;
}
}
这个MazeController类负责创建和管理迷宫,包括墙壁、豆子和大力丸的放置与移除。它使用MazeGenerator类生成迷宫数据,然后将其转换为游戏对象。
18.4.3 吃豆人移动控制实现
吃豆人的移动是游戏的核心交互部分,我们需要实现平滑的移动和转向逻辑:
csharp
public class PacmanController : MonoBehaviour
{
// 移动速度
public float moveSpeed = 3.5f;
// 初始位置
public Vector2 startPosition = new Vector2(14f, 7f);
// 当前方向和请求方向
private Vector2 currentDirection = Vector2.zero;
private Vector2 nextDirection = Vector2.zero;
// 动画控制
private Animator animator;
// 游戏管理器引用
private GameManager gameManager;
// 初始化
public void Initialize()
{
animator = GetComponent<Animator>();
gameManager = FindObjectOfType<GameManager>();
// 设置初始位置
ResetPosition();
}
// 设置游戏管理器引用
public void SetGameManager(GameManager manager)
{
gameManager = manager;
}
// 重置位置
public void ResetPosition()
{
transform.position = startPosition;
currentDirection = Vector2.zero;
nextDirection = Vector2.zero;
// 重置动画
UpdateAnimation();
}
// 更新
void Update()
{
// 只在游戏进行时处理输入和移动
if (gameManager != null &&
gameManager.GetCurrentState() == GameManager.GameState.Playing)
{
// 处理输入
ProcessInput();
// 移动
Move();
}
}
// 处理输入
private void ProcessInput()
{
// 检测方向键输入
if (Input.GetKeyDown(KeyCode.UpArrow) || Input.GetKeyDown(KeyCode.W))
{
nextDirection = Vector2.up;
}
else if (Input.GetKeyDown(KeyCode.DownArrow) || Input.GetKeyDown(KeyCode.S))
{
nextDirection = Vector2.down;
}
else if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.A))
{
nextDirection = Vector2.left;
}
else if (Input.GetKeyDown(KeyCode.RightArrow) || Input.GetKeyDown(KeyCode.D))
{
nextDirection = Vector2.right;
}
}
// 移动
private void Move()
{
// 尝试切换到请求的方向
if (nextDirection != Vector2.zero)
{
if (CanMove(nextDirection))
{
currentDirection = nextDirection;
nextDirection = Vector2.zero;
// 更新动画
UpdateAnimation();
}
}
// 如果无法继续当前方向,停下来
if (!CanMove(currentDirection))
{
return;
}
// 移动
transform.position = (Vector2)transform.position + currentDirection * moveSpeed * Time.deltaTime;
// 检测吃豆
CheckDotCollection();
// 检测幽灵碰撞
CheckGhostCollision();
// 处理穿过隧道
HandleTunnelWarp();
}
// 检查是否可以向指定方向移动
private bool CanMove(Vector2 direction)
{
// 如果方向为零,不能移动
if (direction == Vector2.zero)
{
return false;
}
// 计算下一个位置
Vector2 nextPos = (Vector2)transform.position + direction * 0.5f;
// 检查是否是墙
return !gameManager.IsWall(nextPos);
}
// 更新动画
private void UpdateAnimation()
{
if (animator != null)
{
// 设置动画参数
animator.SetFloat("DirX", currentDirection.x);
animator.SetFloat("DirY", currentDirection.y);
// 设置移动状态
bool isMoving = currentDirection != Vector2.zero;
animator.SetBool("IsMoving", isMoving);
}
}
// 检测吃豆
private void CheckDotCollection()
{
// 获取当前位置的单元格类型
MazeGenerator.CellType cellType = gameManager.GetCellTypeAtPosition(transform.position);
if (cellType == MazeGenerator.CellType.Dot)
{
// 吃到普通豆子
gameManager.EatDot(transform.position);
}
else if (cellType == MazeGenerator.CellType.PowerPellet)
{
// 吃到大力丸
gameManager.EatPowerPellet(transform.position);
}
}
// 检测幽灵碰撞
private void CheckGhostCollision()
{
Ghost[] ghosts = FindObjectsOfType<Ghost>();
foreach (Ghost ghost in ghosts)
{
float distance = Vector2.Distance(transform.position, ghost.transform.position);
// 如果距离小于阈值,发生碰撞
if (distance < 0.5f)
{
// 如果幽灵处于惊吓状态,吃掉幽灵
if (ghost.currentState is FrightenedState)
{
gameManager.GhostEaten(ghost);
}
// 如果幽灵不是被吃状态,吃豆人被抓
else if (!(ghost.currentState is EatenState))
{
gameManager.PacmanCaught();
}
// 每帧只处理一个碰撞
break;
}
}
}
// 处理穿过隧道
private void HandleTunnelWarp()
{
// 获取迷宫宽度
int mazeWidth = gameManager.GetMazeWidth();
// 如果超出左边界,从右边出现
if (transform.position.x < 0)
{
transform.position = new Vector2(mazeWidth - 0.5f, transform.position.y);
}
// 如果超出右边界,从左边出现
else if (transform.position.x >= mazeWidth)
{
transform.position = new Vector2(0, transform.position.y);
}
}
// 获取当前方向
public Vector2 GetDirection()
{
return currentDirection;
}
}
这个PacmanController类负责处理吃豆人的移动、转向和碰撞检测。它实现了经典吃豆人游戏中的网格移动机制,以及在遇到墙壁时的停止和转向行为。
18.4.4 豆子消失效果实现
当吃豆人吃到豆子时,我们希望有一个消失的效果,而不是豆子直接消失。这可以通过动画或淡出效果来实现:
csharp
public class DotController : MonoBehaviour
{
// 豆子类型
public enum DotType
{
Regular,
PowerPellet
}
public DotType dotType = DotType.Regular;
// 大力丸闪烁效果
public float blinkRate = 0.5f;
private SpriteRenderer spriteRenderer;
// 初始化
void Start()
{
spriteRenderer = GetComponent<SpriteRenderer>();
// 如果是大力丸,启动闪烁效果
if (dotType == DotType.PowerPellet)
{
StartCoroutine(BlinkEffect());
}
}
// 大力丸闪烁效果
private IEnumerator BlinkEffect()
{
while (true)
{
spriteRenderer.enabled = !spriteRenderer.enabled;
yield return new WaitForSeconds(blinkRate);
}
}
// 消失效果
public void Disappear()
{
StartCoroutine(DisappearEffect());
}
// 消失动画
private IEnumerator DisappearEffect()
{
// 创建粒子效果
ParticleSystem particles = GetComponentInChildren<ParticleSystem>();
if (particles != null)
{
particles.Play();
}
// 淡出效果
float duration = 0.2f;
float elapsed = 0;
Color originalColor = spriteRenderer.color;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float alpha = Mathf.Lerp(1, 0, elapsed / duration);
spriteRenderer.color = new Color(originalColor.r, originalColor.g, originalColor.b, alpha);
yield return null;
}
// 禁用渲染器
spriteRenderer.enabled = false;
// 等待粒子效果完成
if (particles != null)
{
yield return new WaitForSeconds(particles.main.duration);
}
// 销毁游戏对象
Destroy(gameObject);
}
}
这个DotController类为豆子和大力丸添加了视觉效果,包括大力丸的闪烁效果和豆子被吃时的消失动画。通过这些效果,游戏会更加生动有趣。
18.4.5 幽灵移动算法详解
幽灵的移动是吃豆人游戏中最复杂的部分,不同的幽灵有不同的追逐策略,下面我们将深入探讨这些算法的数学原理:
csharp
// 幽灵AI移动控制
public class GhostAI : MonoBehaviour
{
// 引用Ghost组件
private Ghost ghost;
// A*寻路算法实现
private AStarPathfinding pathfinding;
// 当前路径
private List<Vector2> currentPath = new List<Vector2>();
private int currentPathIndex = 0;
// 随机移动参数
private float randomDirectionTimer = 0f;
private float randomDirectionInterval = 0.5f;
// 初始化
public void Initialize(Ghost ghostRef)
{
ghost = ghostRef;
pathfinding = new AStarPathfinding(ghost.gameManager);
}
// 计算路径到目标
public void CalculatePathToTarget(Vector2 targetPosition)
{
Vector2 currentPosition = GridPosition(ghost.transform.position);
Vector2 targetGridPos = GridPosition(targetPosition);
// 使用A*算法计算路径
currentPath = pathfinding.FindPath(currentPosition, targetGridPos);
currentPathIndex = 0;
// 如果找不到路径,使用简单的方向选择
if (currentPath.Count == 0)
{
// 尝试直接移动到目标
FallbackPathfinding(targetPosition);
}
}
// 备用寻路方法
private void FallbackPathfinding(Vector2 targetPosition)
{
List<Vector2> possibleDirections = GetPossibleDirections();
// 如果没有可行的方向,返回
if (possibleDirections.Count == 0)
{
return;
}
// 选择最接近目标的方向
Vector2 bestDirection = Vector2.zero;
float bestDistance = float.MaxValue;
foreach (Vector2 direction in possibleDirections)
{
// 避免180度转弯
if (direction == -ghost.GetCurrentDirection() && possibleDirections.Count > 1)
{
continue;
}
Vector2 newPosition = ghost.transform.position + (Vector3)direction;
float distance = Vector2.Distance(newPosition, targetPosition);
if (distance < bestDistance)
{
bestDistance = distance;
bestDirection = direction;
}
}
// 创建一个简单的路径
currentPath = new List<Vector2>
{
GridPosition(ghost.transform.position) + bestDirection
};
currentPathIndex = 0;
}
// 随机移动
public void MoveRandomly()
{
randomDirectionTimer -= Time.deltaTime;
// 定期重新选择随机方向
if (randomDirectionTimer <= 0 || currentPath.Count == 0 ||
currentPathIndex >= currentPath.Count)
{
RandomizeDirection();
randomDirectionTimer = randomDirectionInterval;
}
else
{
// 继续沿当前路径移动
FollowPath();
}
}
// 随机选择方向
private void RandomizeDirection()
{
List<Vector2> possibleDirections = GetPossibleDirections();
// 过滤掉180度转弯
Vector2 currentDirection = ghost.GetCurrentDirection();
possibleDirections.RemoveAll(dir => dir == -currentDirection && possibleDirections.Count > 1);
if (possibleDirections.Count > 0)
{
// 随机选择一个方向
int randomIndex = Random.Range(0, possibleDirections.Count);
Vector2 randomDirection = possibleDirections[randomIndex];
// 创建一个简单的路径
currentPath = new List<Vector2>
{
GridPosition(ghost.transform.position) + randomDirection
};
currentPathIndex = 0;
}
}
// 跟随当前路径
public void FollowPath()
{
if (currentPath.Count == 0 || currentPathIndex >= currentPath.Count)
{
return;
}
// 获取下一个路径点
Vector2 nextPathPoint = currentPath[currentPathIndex];
Vector2 currentPosition = ghost.transform.position;
// 计算到下一个路径点的方向
Vector2 direction = (nextPathPoint - currentPosition).normalized;
// 将对角线方向转换为基本方向(上下左右)
if (Mathf.Abs(direction.x) > Mathf.Abs(direction.y))
{
direction = new Vector2(Mathf.Sign(direction.x), 0);
}
else
{
direction = new Vector2(0, Mathf.Sign(direction.y));
}
// 移动
ghost.MoveInDirection(direction);
// 检查是否到达路径点
if (Vector2.Distance(currentPosition, nextPathPoint) < 0.1f)
{
currentPathIndex++;
}
}
// 获取可能的移动方向
private List<Vector2> GetPossibleDirections()
{
List<Vector2> directions = new List<Vector2>();
Vector2 currentPosition = ghost.transform.position;
// 四个基本方向
Vector2[] basicDirections = new Vector2[]
{
Vector2.up, Vector2.down, Vector2.left, Vector2.right
};
foreach (Vector2 direction in basicDirections)
{
Vector2 newPosition = currentPosition + direction * 0.5f; // 半个单位的检测距离
if (!ghost.gameManager.IsWall(newPosition))
{
directions.Add(direction);
}
}
return directions;
}
// 将世界坐标转换为网格坐标
private Vector2 GridPosition(Vector2 worldPosition)
{
return new Vector2(
Mathf.Round(worldPosition.x),
Mathf.Round(worldPosition.y)
);
}
}
// A*寻路算法
public class AStarPathfinding
{
// 游戏管理器引用
private GameManager gameManager;
// 构造函数
public AStarPathfinding(GameManager manager)
{
gameManager = manager;
}
// 节点类
private class Node
{
public Vector2 position;
public Node parent;
public float gCost; // 从起点到当前节点的成本
public float hCost; // 从当前节点到目标的估计成本
// 总成本
public float fCost
{
get { return gCost + hCost; }
}
public Node(Vector2 position)
{
this.position = position;
}
}
// 寻找路径
public List<Vector2> FindPath(Vector2 startPos, Vector2 targetPos)
{
// 创建开始和结束节点
Node startNode = new Node(startPos);
Node targetNode = new Node(targetPos);
// 创建开放和关闭列表
List<Node> openList = new List<Node>();
HashSet<Vector2> closedSet = new HashSet<Vector2>();
// 添加起始节点到开放列表
openList.Add(startNode);
// 循环直到开放列表为空
while (openList.Count > 0)
{
// 获取成本最低的节点
Node currentNode = openList[0];
for (int i = 1; i < openList.Count; i++)
{
if (openList[i].fCost < currentNode.fCost ||
(openList[i].fCost == currentNode.fCost &&
openList[i].hCost < currentNode.hCost))
{
currentNode = openList[i];
}
}
// 从开放列表移除并添加到关闭集
openList.Remove(currentNode);
closedSet.Add(currentNode.position);
// 如果到达目标,构建路径并返回
if (currentNode.position == targetNode.position)
{
return ReconstructPath(currentNode);
}
// 检查相邻节点
foreach (Vector2 direction in new Vector2[] {Vector2.up, Vector2.right, Vector2.down, Vector2.left})
{
Vector2 neighborPos = currentNode.position + direction;
// 跳过不可行走的和已经在关闭集中的节点
if (gameManager.IsWall(neighborPos) || closedSet.Contains(neighborPos))
{
continue;
}
float newGCost = currentNode.gCost + 1; // 相邻格子距离为1
// 创建邻居节点
Node neighborNode = openList.Find(n => n.position == neighborPos);
// 如果节点不在开放列表中或找到了更好的路径
if (neighborNode == null || newGCost < neighborNode.gCost)
{
if (neighborNode == null)
{
neighborNode = new Node(neighborPos);
openList.Add(neighborNode);
}
neighborNode.gCost = newGCost;
neighborNode.hCost = CalculateHCost(neighborPos, targetPos);
neighborNode.parent = currentNode;
}
}
}
// 没有找到路径
return new List<Vector2>();
}
// 计算启发式成本(曼哈顿距离)
private float CalculateHCost(Vector2 from, Vector2 to)
{
return Mathf.Abs(from.x - to.x) + Mathf.Abs(from.y - to.y);
}
// 重构路径
private List<Vector2> ReconstructPath(Node endNode)
{
List<Vector2> path = new List<Vector2>();
Node currentNode = endNode;
// 从目标节点回溯到起始节点
while (currentNode != null)
{
path.Add(currentNode.position);
currentNode = currentNode.parent;
}
// 反转路径,使其从起点到终点
path.Reverse();
return path;
}
}
这段代码实现了幽灵的AI移动控制,包括使用A算法进行寻路和在惊吓状态下的随机移动。A算法是一种启发式搜索算法,它通过评估每个节点的成本和到目标的估计距离,找出从起点到终点的最短路径。
在吃豆人游戏中,不同的幽灵使用相同的寻路算法,但目标点的计算方式不同,这就创造了不同的行为模式。
18.4.6 游戏UI和音效系统
一个完整的游戏需要UI界面和音效系统,下面我们来实现这些组件:
csharp
// UI管理器
public class UIManager : MonoBehaviour
{
// UI元素引用
public Text scoreText;
public Text readyText;
public Text gameOverText;
public Text levelCompleteText;
public Image[] lifeImages;
// 幽灵得分预制体
public GameObject ghostScorePrefab;
// 初始化
void Start()
{
// 隐藏状态文本
readyText.gameObject.SetActive(false);
gameOverText.gameObject.SetActive(false);
levelCompleteText.gameObject.SetActive(false);
}
// 更新分数显示
public void UpdateScore(int score)
{
scoreText.text = $"SCORE: {score}";
}
// 更新生命显示
public void UpdateLives(int lives)
{
for (int i = 0; i < lifeImages.Length; i++)
{
lifeImages[i].gameObject.SetActive(i < lives);
}
}
// 显示准备文本
public void ShowReadyText(bool show)
{
readyText.gameObject.SetActive(show);
}
// 显示游戏结束
public void ShowGameOver(bool win)
{
gameOverText.gameObject.SetActive(true);
gameOverText.text = win ? "YOU WIN!" : "GAME OVER";
}
// 显示通关文本
public void ShowLevelComplete()
{
levelCompleteText.gameObject.SetActive(true);
}
// 显示幽灵得分
public void ShowGhostScore(Vector2 position, int score)
{
GameObject scoreObj = Instantiate(ghostScorePrefab, position, Quaternion.identity);
Text scoreText = scoreObj.GetComponent<Text>();
if (scoreText != null)
{
scoreText.text = score.ToString();
// 设置动画
StartCoroutine(AnimateGhostScore(scoreObj));
}
}
// 幽灵得分动画
private IEnumerator AnimateGhostScore(GameObject scoreObj)
{
float duration = 1.0f;
float elapsed = 0f;
Vector3 startPos = scoreObj.transform.position;
Vector3 endPos = startPos + Vector3.up * 2f;
while (elapsed < duration)
{
elapsed += Time.deltaTime;
float t = elapsed / duration;
// 上升和淡出
scoreObj.transform.position = Vector3.Lerp(startPos, endPos, t);
Text text = scoreObj.GetComponent<Text>();
if (text != null)
{
Color color = text.color;
color.a = Mathf.Lerp(1, 0, t);
text.color = color;
}
yield return null;
}
Destroy(scoreObj);
}
}
// 音效管理器
public class AudioManager : MonoBehaviour
{
// 单例实例
public static AudioManager Instance { get; private set; }
// 音效片段
public AudioClip startGameSound;
public AudioClip dotEatSound;
public AudioClip powerPelletSound;
public AudioClip ghostEatenSound;
public AudioClip deathSound;
public AudioClip fruitEatSound;
public AudioClip sirenSound; // 游戏背景音
public AudioClip frightenedSound; // 幽灵惊吓状态音效
// 音源
private AudioSource effectsSource;
private AudioSource musicSource;
// 初始化
void Awake()
{
// 单例设置
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
return;
}
// 创建音源
effectsSource = gameObject.AddComponent<AudioSource>();
musicSource = gameObject.AddComponent<AudioSource>();
// 设置音乐循环
musicSource.loop = true;
}
// 播放开始游戏音效
public void PlayStartGameSound()
{
effectsSource.PlayOneShot(startGameSound);
}
// 播放吃豆子音效
public void PlayDotSound()
{
effectsSource.PlayOneShot(dotEatSound);
}
// 播放吃大力丸音效
public void PlayPowerPelletSound()
{
effectsSource.PlayOneShot(powerPelletSound);
}
// 播放吃幽灵音效
public void PlayGhostEatenSound()
{
effectsSource.PlayOneShot(ghostEatenSound);
}
// 播放死亡音效
public void PlayDeathSound()
{
effectsSource.PlayOneShot(deathSound);
}
// 播放吃水果音效
public void PlayFruitEatSound()
{
effectsSource.PlayOneShot(fruitEatSound);
}
// 播放游戏背景音乐
public void PlaySirenMusic()
{
musicSource.clip = sirenSound;
musicSource.Play();
}
// 播放幽灵惊吓状态音乐
public void PlayFrightenedMusic()
{
musicSource.clip = frightenedSound;
musicSource.Play();
}
// 恢复正常音乐
public void ResumeNormalMusic()
{
musicSource.clip = sirenSound;
musicSource.Play();
}
// 停止所有音乐
public void StopMusic()
{
musicSource.Stop();
}
}
UI管理器负责显示分数、生命和状态文本,并提供一些动画效果。音效管理器则使用单例模式,提供了一个全局访问点,用于播放各种游戏音效和背景音乐。
18.5 进阶技术与优化
18.5.1 性能优化技巧
在开发商业游戏时,性能优化是一个重要的考虑因素。以下是一些适用于吃豆人游戏的优化技巧:
csharp
// 对象池管理器
public class ObjectPoolManager : MonoBehaviour
{
// 单例实例
public static ObjectPoolManager Instance { get; private set; }
// 对象池字典
private Dictionary<string, Queue<GameObject>> objectPools = new Dictionary<string, Queue<GameObject>>();
// 初始化
void Awake()
{
// 单例设置
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
}
// 创建对象池
public void CreatePool(string poolName, GameObject prefab, int initialSize)
{
if (objectPools.ContainsKey(poolName))
{
Debug.LogWarning($"Pool {poolName} already exists!");
return;
}
Queue<GameObject> pool = new Queue<GameObject>();
// 创建初始对象
for (int i = 0; i < initialSize; i++)
{
GameObject obj = Instantiate(prefab);
obj.SetActive(false);
obj.transform.parent = transform;
pool.Enqueue(obj);
}
objectPools.Add(poolName, pool);
}
// 从池中获取对象
public GameObject GetObjectFromPool(string poolName)
{
if (!objectPools.ContainsKey(poolName))
{
Debug.LogError($"Pool {poolName} doesn't exist!");
return null;
}
// 如果池为空,创建新对象
if (objectPools[poolName].Count == 0)
{
Debug.LogWarning($"Pool {poolName} is empty. Consider increasing initial size.");
// 获取池中第一个对象的预制体
GameObject prefab = objectPools[poolName].Peek();
// 创建新对象
GameObject newObj = Instantiate(prefab);
return newObj;
}
// 获取对象并激活
GameObject obj = objectPools[poolName].Dequeue();
obj.SetActive(true);
return obj;
}
// 将对象返回池中
public void ReturnObjectToPool(string poolName, GameObject obj)
{
if (!objectPools.ContainsKey(poolName))
{
Debug.LogError($"Pool {poolName} doesn't exist!");
return;
}
// 重置对象状态
obj.SetActive(false);
// 返回到池中
objectPools[poolName].Enqueue(obj);
}
// 清空对象池
public void ClearPool(string poolName)
{
if (!objectPools.ContainsKey(poolName))
{
Debug.LogError($"Pool {poolName} doesn't exist!");
return;
}
// 销毁所有对象
while (objectPools[poolName].Count > 0)
{
GameObject obj = objectPools[poolName].Dequeue();
Destroy(obj);
}
// 移除池
objectPools.Remove(poolName);
}
}
// 优化的粒子系统管理器
public class ParticleSystemManager : MonoBehaviour
{
// 单例实例
public static ParticleSystemManager Instance { get; private set; }
// 粒子系统预制体
public GameObject dotEatParticlePrefab;
public GameObject powerPelletParticlePrefab;
public GameObject ghostEatParticlePrefab;
// 初始化
void Awake()
{
// 单例设置
if (Instance == null)
{
Instance = this;
DontDestroyOnLoad(gameObject);
}
else
{
Destroy(gameObject);
}
// 创建对象池
ObjectPoolManager.Instance.CreatePool("DotParticles", dotEatParticlePrefab, 20);
ObjectPoolManager.Instance.CreatePool("PowerPelletParticles", powerPelletParticlePrefab, 4);
ObjectPoolManager.Instance.CreatePool("GhostEatParticles", ghostEatParticlePrefab, 4);
}
// 播放豆子吃掉特效
public void PlayDotEatEffect(Vector3 position)
{
GameObject particleObj = ObjectPoolManager.Instance.GetObjectFromPool("DotParticles");
particleObj.transform.position = position;
ParticleSystem particles = particleObj.GetComponent<ParticleSystem>();
particles.Play();
// 自动返回对象池
StartCoroutine(ReturnParticleToPool("DotParticles", particleObj, particles.main.duration));
}
// 播放大力丸吃掉特效
public void PlayPowerPelletEatEffect(Vector3 position)
{
GameObject particleObj = ObjectPoolManager.Instance.GetObjectFromPool("PowerPelletParticles");
particleObj.transform.position = position;
ParticleSystem particles = particleObj.GetComponent<ParticleSystem>();
particles.Play();
// 自动返回对象池
StartCoroutine(ReturnParticleToPool("PowerPelletParticles", particleObj, particles.main.duration));
}
// 播放幽灵吃掉特效
public void PlayGhostEatEffect(Vector3 position)
{
GameObject particleObj = ObjectPoolManager.Instance.GetObjectFromPool("GhostEatParticles");
particleObj.transform.position = position;
ParticleSystem particles = particleObj.GetComponent<ParticleSystem>();
particles.Play();
// 自动返回对象池
StartCoroutine(ReturnParticleToPool("GhostEatParticles", particleObj, particles.main.duration));
}
// 返回粒子到对象池
private IEnumerator ReturnParticleToPool(string poolName, GameObject particleObj, float duration)
{
yield return new WaitForSeconds(duration);
ObjectPoolManager.Instance.ReturnObjectToPool(poolName, particleObj);
}
}
这两个类实现了对象池模式,这是游戏开发中常用的性能优化技术。对象池通过重用游戏对象而不是频繁地创建和销毁它们,减少了垃圾回收的压力,提高了游戏的性能。
18.5.2 数学优化与算法改进
除了对象池之外,我们还可以通过优化数学计算和改进算法来提高游戏性能:
csharp
// 优化的网格位置转换
public static class GridUtils
{
// 世界坐标到网格坐标的转换(使用整数运算)
public static Vector2Int WorldToGrid(Vector2 worldPos, float cellSize)
{
return new Vector2Int(
Mathf.RoundToInt(worldPos.x / cellSize),
Mathf.RoundToInt(worldPos.y / cellSize)
);
}
// 网格坐标到世界坐标的转换
public static Vector2 GridToWorld(Vector2Int gridPos, float cellSize)
{
return new Vector2(
gridPos.x * cellSize,
gridPos.y * cellSize
);
}
// 快速距离计算(曼哈顿距离)
public static int ManhattanDistance(Vector2Int a, Vector2Int b)
{
return Mathf.Abs(a.x - b.x) + Mathf.Abs(a.y - b.y);
}
// 欧几里得距离的平方(避免开方运算)
public static float SqrDistance(Vector2 a, Vector2 b)
{
float dx = a.x - b.x;
float dy = a.y - b.y;
return dx * dx + dy * dy;
}
}
// 优化的A*寻路算法
public class OptimizedAStarPathfinding
{
// 游戏管理器引用
private GameManager gameManager;
// 网格大小
private float cellSize;
// 构造函数
public OptimizedAStarPathfinding(GameManager manager, float cellSize)
{
gameManager = manager;
this.cellSize = cellSize;
}
// 节点类
private class PathNode : IComparable<PathNode>
{
public Vector2Int position;
public PathNode parent;
public int gCost;
public int hCost;
public int fCost
{
get { return gCost + hCost; }
}
public PathNode(Vector2Int position)
{
this.position = position;
}
// 实现比较接口,用于优先队列
public int CompareTo(PathNode other)
{
int compare = fCost.CompareTo(other.fCost);
if (compare == 0)
{
compare = hCost.CompareTo(other.hCost);
}
return compare;
}
}
// 寻找路径
public List<Vector2> FindPath(Vector2 startWorldPos, Vector2 targetWorldPos)
{
// 转换为网格坐标
Vector2Int startPos = GridUtils.WorldToGrid(startWorldPos, cellSize);
Vector2Int targetPos = GridUtils.WorldToGrid(targetWorldPos, cellSize);
// 创建开始和结束节点
PathNode startNode = new PathNode(startPos);
PathNode targetNode = new PathNode(targetPos);
// 使用优先队列优化开放列表
PriorityQueue<PathNode> openSet = new PriorityQueue<PathNode>();
Dictionary<Vector2Int, PathNode> allNodes = new Dictionary<Vector2Int, PathNode>();
HashSet<Vector2Int> closedSet = new HashSet<Vector2Int>();
// 初始化起始节点
startNode.gCost = 0;
startNode.hCost = GridUtils.ManhattanDistance(startPos, targetPos);
openSet.Enqueue(startNode);
allNodes.Add(startPos, startNode);
// 主循环
while (openSet.Count > 0)
{
// 获取成本最低的节点
PathNode currentNode = openSet.Dequeue();
// 如果到达目标,构建路径并返回
if (currentNode.position == targetPos)
{
return ReconstructPath(currentNode);
}
// 添加到关闭集
closedSet.Add(currentNode.position);
// 检查四个方向的邻居
Vector2Int[] directions = new Vector2Int[]
{
new Vector2Int(0, 1), // 上
new Vector2Int(1, 0), // 右
new Vector2Int(0, -1), // 下
new Vector2Int(-1, 0) // 左
};
foreach (Vector2Int dir in directions)
{
Vector2Int neighborPos = currentNode.position + dir;
// 跳过不可行走的和已经在关闭集中的节点
if (IsWall(neighborPos) || closedSet.Contains(neighborPos))
{
continue;
}
// 计算新的g值
int newGCost = currentNode.gCost + 1; // 相邻格子距离为1
// 获取或创建邻居节点
PathNode neighborNode;
bool isInOpenSet = allNodes.TryGetValue(neighborPos, out neighborNode);
// 如果节点不在开放列表中或找到了更好的路径
if (!isInOpenSet || newGCost < neighborNode.gCost)
{
if (!isInOpenSet)
{
neighborNode = new PathNode(neighborPos);
allNodes.Add(neighborPos, neighborNode);
}
// 更新节点值
neighborNode.gCost = newGCost;
neighborNode.hCost = GridUtils.ManhattanDistance(neighborPos, targetPos);
neighborNode.parent = currentNode;
// 添加到开放列表
if (!isInOpenSet)
{
openSet.Enqueue(neighborNode);
}
else
{
// 更新队列中的节点优先级
openSet.Update(neighborNode);
}
}
}
}
// 没有找到路径
return new List<Vector2>();
}
// 检查是否是墙
private bool IsWall(Vector2Int gridPos)
{
Vector2 worldPos = GridUtils.GridToWorld(gridPos, cellSize);
return gameManager.IsWall(worldPos);
}
// 重构路径
private List<Vector2> ReconstructPath(PathNode endNode)
{
List<Vector2> path = new List<Vector2>();
PathNode currentNode = endNode;
// 从目标节点回溯到起始节点
while (currentNode != null)
{
// 转换为世界坐标
path.Add(GridUtils.GridToWorld(currentNode.position, cellSize));
currentNode = currentNode.parent;
}
// 反转路径,使其从起点到终点
path.Reverse();
return path;
}
}
// 优先队列实现
public class PriorityQueue<T> where T : IComparable<T>
{
private List<T> data;
public int Count
{
get { return data.Count; }
}
public PriorityQueue()
{
data = new List<T>();
}
// 入队
public void Enqueue(T item)
{
data.Add(item);
int childIndex = data.Count - 1;
while (childIndex > 0)
{
int parentIndex = (childIndex - 1) / 2;
if (data[childIndex].CompareTo(data[parentIndex]) >= 0)
{
break;
}
// 交换父子节点
T tmp = data[childIndex];
data[childIndex] = data[parentIndex];
data[parentIndex] = tmp;
childIndex = parentIndex;
}
}
// 出队
public T Dequeue()
{
int lastIndex = data.Count - 1;
T frontItem = data[0];
// 将最后一项移到前面
data[0] = data[lastIndex];
data.RemoveAt(lastIndex);
// 重新平衡堆
lastIndex--;
if (lastIndex >= 0)
{
ShiftDown(0);
}
return frontItem;
}
// 向下移动元素以保持堆性质
private void ShiftDown(int parentIndex)
{
int lastIndex = data.Count - 1;
int leftChildIndex = parentIndex * 2 + 1;
while (leftChildIndex <= lastIndex)
{
int rightChildIndex = leftChildIndex + 1;
int minIndex = leftChildIndex;
if (rightChildIndex <= lastIndex &&
data[rightChildIndex].CompareTo(data[leftChildIndex]) < 0)
{
minIndex = rightChildIndex;
}
if (data[parentIndex].CompareTo(data[minIndex]) <= 0)
{
break;
}
// 交换
T tmp = data[parentIndex];
data[parentIndex] = data[minIndex];
data[minIndex] = tmp;
parentIndex = minIndex;
leftChildIndex = parentIndex * 2 + 1;
}
}
// 更新队列中元素的优先级
public void Update(T item)
{
// 简单实现:重新入队(实际应用中可以优化)
int index = data.FindIndex(x => x.Equals(item));
if (index != -1)
{
data.RemoveAt(index);
Enqueue(item);
}
}
}
这些优化主要集中在几个方面:
- 使用整数运算:通过Vector2Int代替Vector2进行网格计算,避免浮点运算的开销。
- 曼哈顿距离:在A*算法中使用曼哈顿距离作为启发函数,这在网格移动中是一个精确的估计。
- 平方距离:在需要比较距离时,使用距离的平方而不是实际距离,避免开方运算。
- 优先队列:使用基于堆的优先队列来优化A*算法的开放列表,使得获取最小成本的节点更加高效。
这些优化可以显著提高游戏的性能,特别是在复杂迷宫和多个幽灵同时寻路的情况下。
18.6 总结与展望
在本章中,我们详细介绍了如何使用Unity 2021.3.8f1c1引擎实现一个经典的吃豆人游戏。我们从游戏规则和机制开始,通过数学模型和算法设计了游戏的核心逻辑,然后实现了具体的代码。
我们探讨了以下关键内容:
- 网格化地图构建:使用二维数组表示迷宫,并转换为游戏对象。
- 状态机理论:应用状态模式管理幽灵的不同行为状态。
- 幽灵AI算法:基于向量计算和A*寻路实现不同幽灵的追逐策略。
- 游戏流程控制:使用GameManager管理游戏的各个阶段和状态转换。
- 性能优化技术:通过对象池和算法改进提高游戏性能。
通过这个项目,我们不仅重现了一个经典游戏,还学习了如何将数学概念应用到游戏开发中,以及如何使用面向对象编程构建一个结构清晰的游戏框架。
未来可能的扩展方向
- 添加更多关卡:设计不同难度和布局的迷宫,增加游戏的多样性。
- 引入新的游戏机制:如传送门、移动墙壁或特殊道具。
- 多人模式:添加竞技或合作的多人游戏模式。
- 更高级的AI:使用机器学习技术训练更智能的幽灵AI。
- 3D版本:将游戏升级为3D版本,保持经典玩法但提供更丰富的视觉体验。
这个吃豆人游戏实现是一个很好的学习项目,通过它我们可以理解游戏开发中的许多基本概念和技术。希望这个详细的教程能够帮助你开发自己的游戏,并继续探索游戏开发的奇妙世界。
更多推荐

所有评论(0)