吃豆人迷宫:基于数学模型的角色行为设计

18.1 经典游戏重现

吃豆人(Pac-Man)是1980年由日本南梦宫(Namco)公司发布的一款街机游戏,因其简单而富有挑战性的游戏玩法,成为了电子游戏史上最具影响力的作品之一。在这个游戏中,玩家控制一个圆形角色在迷宫中移动,吃掉所有的豆子,同时躲避四个不同颜色的幽灵怪物的追击。当吃到特殊的大力丸后,玩家暂时获得吃掉幽灵的能力,为游戏增添了战略深度。

本章将带领读者使用Unity 2021.3.8f1c1引擎,重新实现这款经典游戏,并深入探讨其背后的数学模型和算法原理。通过分析游戏中角色的移动和决策机制,我们将学习如何运用数学知识来设计游戏逻辑,以及如何利用面向对象编程的思想,构建一个结构清晰、可扩展的游戏框架。

吃豆人游戏的魅力不仅在于其简单的规则,更在于四个幽灵各具特色的AI行为,使得游戏既有挑战性又富有策略性。在本章中,我们将重点关注如何实现这些幽灵的AI逻辑,以及如何通过数学算法使它们表现出不同的追逐策略。

18.2 吃豆人游戏玩法详解

在开始实现游戏之前,我们需要全面了解吃豆人的游戏规则和机制,这些将成为我们编程的基础。

18.2.1 基本游戏元素

吃豆人游戏的主要元素包括:

  1. 迷宫:由墙壁、通道和转角组成的封闭区域,通常采用网格结构设计。
  2. 吃豆人:玩家控制的主角,可以在迷宫中上下左右移动。
  3. 豆子:分布在迷宫通道中的小点,吃豆人吃掉所有豆子即完成关卡。
  4. 大力丸:特殊的大豆子,吃掉后可以暂时赋予吃豆人吃掉幽灵的能力。
  5. 幽灵:四个具有不同颜色和行为模式的敌人,会追逐吃豆人。
  6. 水果:出现在迷宫中心的特殊奖励物品,吃掉可以获得额外分数。

18.2.2 游戏目标与计分规则

游戏的基本目标是控制吃豆人吃掉迷宫中所有的豆子,同时避开或吃掉幽灵。具体的计分规则如下:

  • 吃一个普通豆子:10分
  • 吃一个大力丸:50分
  • 吃掉幽灵(仅在吃了大力丸后):200分(第一个)、400分(第二个)、800分(第三个)、1600分(第四个)
  • 吃水果:根据水果类型获得100-5000不等的分数

18.2.3 角色行为模式

吃豆人游戏的核心在于四个幽灵的不同行为模式,这使得游戏充满了策略性和可玩性:

  1. 红色幽灵(Blinky):直接追逐吃豆人,采用最短路径算法。
  2. 粉色幽灵(Pinky):试图预判吃豆人的位置,瞄准吃豆人前方的位置。
  3. 青色幽灵(Inky):行为较为复杂,结合红色幽灵和吃豆人的位置来确定目标。
  4. 橙色幽灵(Clyde):当距离吃豆人较远时追逐,靠近后则散开,表现得较为随机。

这些不同的行为模式使得幽灵们形成了一种无意识的合作,从不同方向包围吃豆人,增加了游戏的挑战性。

18.2.4 游戏状态与转换

吃豆人游戏中的状态转换也是一个重要方面:

  1. 追逐模式(Chase):幽灵按照各自的算法追逐吃豆人。
  2. 散开模式(Scatter):幽灵各自前往迷宫的一个角落。
  3. 惊吓模式(Frightened):吃豆人吃掉大力丸后,幽灵变成蓝色并随机移动,可被吃掉。
  4. 被吃状态:幽灵被吃后变成眼睛,快速返回初始位置重生。

游戏在追逐和散开模式之间周期性切换,创造了紧张与缓和的节奏变化。

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行为是吃豆人游戏最有趣的部分之一。每个幽灵都有自己独特的追逐算法,这些算法可以用数学公式和向量计算来表示。

  1. 红色幽灵(Blinky)的直接追逐
    目标位置就是吃豆人的当前位置:target = pacmanPosition

  2. 粉色幽灵(Pinky)的预判追逐
    目标位置是吃豆人当前位置加上吃豆人面向方向的4个单位:
    target = pacmanPosition + 4 * pacmanDirection

  3. 青色幽灵(Inky)的复杂追逐
    首先计算吃豆人位置加上面向方向2个单位的中间点:
    middlePoint = pacmanPosition + 2 * pacmanDirection
    然后计算从红色幽灵位置到这个中间点的向量,并将其延长两倍:
    target = blinkyPosition + 2 * (middlePoint - blinkyPosition)

  4. 橙色幽灵(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项目和导入必要的资源。

  1. 创建一个新的Unity 2021.3.8f1c1项目,选择2D模板。
  2. 设置项目分辨率为480x640(保持经典游戏风格的纵横比)。
  3. 导入吃豆人游戏所需的精灵图片,包括:
    • 吃豆人角色(不同方向和动画帧)
    • 四个幽灵(正常、惊吓和被吃状态)
    • 迷宫墙壁和背景
    • 豆子、大力丸和水果
    • 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);
        }
    }
}

这些优化主要集中在几个方面:

  1. 使用整数运算:通过Vector2Int代替Vector2进行网格计算,避免浮点运算的开销。
  2. 曼哈顿距离:在A*算法中使用曼哈顿距离作为启发函数,这在网格移动中是一个精确的估计。
  3. 平方距离:在需要比较距离时,使用距离的平方而不是实际距离,避免开方运算。
  4. 优先队列:使用基于堆的优先队列来优化A*算法的开放列表,使得获取最小成本的节点更加高效。

这些优化可以显著提高游戏的性能,特别是在复杂迷宫和多个幽灵同时寻路的情况下。

18.6 总结与展望

在本章中,我们详细介绍了如何使用Unity 2021.3.8f1c1引擎实现一个经典的吃豆人游戏。我们从游戏规则和机制开始,通过数学模型和算法设计了游戏的核心逻辑,然后实现了具体的代码。

我们探讨了以下关键内容:

  1. 网格化地图构建:使用二维数组表示迷宫,并转换为游戏对象。
  2. 状态机理论:应用状态模式管理幽灵的不同行为状态。
  3. 幽灵AI算法:基于向量计算和A*寻路实现不同幽灵的追逐策略。
  4. 游戏流程控制:使用GameManager管理游戏的各个阶段和状态转换。
  5. 性能优化技术:通过对象池和算法改进提高游戏性能。

通过这个项目,我们不仅重现了一个经典游戏,还学习了如何将数学概念应用到游戏开发中,以及如何使用面向对象编程构建一个结构清晰的游戏框架。

未来可能的扩展方向

  1. 添加更多关卡:设计不同难度和布局的迷宫,增加游戏的多样性。
  2. 引入新的游戏机制:如传送门、移动墙壁或特殊道具。
  3. 多人模式:添加竞技或合作的多人游戏模式。
  4. 更高级的AI:使用机器学习技术训练更智能的幽灵AI。
  5. 3D版本:将游戏升级为3D版本,保持经典玩法但提供更丰富的视觉体验。

这个吃豆人游戏实现是一个很好的学习项目,通过它我们可以理解游戏开发中的许多基本概念和技术。希望这个详细的教程能够帮助你开发自己的游戏,并继续探索游戏开发的奇妙世界。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐