【wxWidgets实战】迷宫游戏

本文介绍了使用wxWidgets库实现一个迷宫游戏的过程,包括自动生成不重复的迷宫、设置简单和困难模式、使用键盘控制玩家移动。迷宫生成采用深度优先搜索算法,游戏界面通过自定义控件绘制。文章详细讲解了迷宫生成、控件创建、事件处理和玩家移动的实现细节,并提供了完整的代码分析。
摘要由CSDN通过智能技术生成

先看效果——
演示

一、项目构思

  1. 能自动生成迷宫,并且可以自定义迷宫的大小,每一次的迷宫都不重样
  2. 迷宫要有简单模式困难模式,难度一定能要有明显的变化;
  3. 使用 wxWidgets 把迷宫绘制出来;
  4. 能通过上下左右键盘按键进行操作。

二、思路分析

(一) 生成迷宫

1. 迷宫每次生成都不重样

随机数——C++的 random 头文件。

2. 迷宫的元素

  • 墙,即黑黑的那些东西;
  • 通道,白色部分,可以通行的地方;
  • 出口,右下角绿色那块;
  • 玩家,左上角红色的实心圆圈。

3. 生成迷宫的方式

  • 通过“打通墙壁”这种招式来创建通道
  • 使用“深度优先搜索算法”来找下一个打通的位置;
  • 迷宫生成好后,在迷宫的左上方和右下方分别标注玩家出口
  • 生成好的迷宫使用一个二维容器进行存储。

(二) 进行迷宫游戏

1. 迷宫控件

  • 采用继承 wxWindow 的方式进行创建;
  • 通过 wxEVT_PAINT 事件对迷宫进行绘制;
  • 绘制风格:墙->黑色通道->白色(看不见了吧);出口->绿色玩家->红色圆圈

2. 控件使用

使用一个继承于 wxFrame 的主窗口 MyFrame 来显示迷宫控件。

3. 进行游戏

既然是迷宫游戏,自然是要能通过键盘进行操作的。我们通过键盘按键按下事件来对“玩家”进行操纵,控制它行走在通道上,并成功到达终点。

三、详细分解

(一) 入口以及主窗口代码

class MyFrame : public wxFrame
{
public:
    MyFrame(const wxString &title, const wxPoint &pos, const wxSize &size) :
        wxFrame(nullptr, wxID_ANY, title, pos, size)
    {
        SetClientSize(GetSize());
        new MazePanel(this);
    }
};

class MyApp : public wxApp
{
public:
    bool OnInit() override
    {
        auto *frame = new MyFrame("Maze Game", wxDefaultPosition, wxSize(816, 816));
        frame->Show(true);
        frame->Center();
        return true;
    }
};

wxIMPLEMENT_APP(MyApp);
  1. MyApp 为程序入口类,有且仅有一个,在以往的项目和教程中我都有介绍,这里就不细说了。
  2. wxIMPLEMENT_APP 为程序入口宏,内部封装了 main 函数,其目的是为了更好的跨平台
  3. MyFrame 为主窗口类,其构造函数中还 new 了一个控件 MazePanel,这个控件就是迷宫控件

(二) 事件自定义

wxDECLARE_EVENT(ctmEVT_GAME_WIN, wxCommandEvent); // 声明
wxDEFINE_EVENT(ctmEVT_GAME_WIN, wxCommandEvent);  // 定义

这里声明定义了一个 ctmEVT_GAME_WIN 事件,当游戏胜利时该控件应被触发。

(三) 迷宫控件类:声明

class MazePanel : public wxWindow
{
protected:
    // 元素枚举
    enum class Element
    {
        Wall   = 0,  // 墙
        Path   = 1,  // 路
        Exit   = 2,  // 出口
        Person = 3,  // 玩家
    };

    // 迷宫位置
    struct Path
    {
        int x;
        int y;
    };

public:
    explicit MazePanel(wxFrame *parent);

protected:
    // 事件处理函数
    void OnPaint(wxPaintEvent &evt);
    void OnKeyDown(wxKeyEvent &evt);
    void OnGameWin(wxCommandEvent &evt);

    // 移动操作
    void Move(int dx, int dy);
    void Move(int keyCode);

    // 难度选择
    static bool ChooseDifficulty();

    // 迷宫生成
    wxVector<wxVector<MazePanel::Element>> GenerateMaze(bool isHard = true);

private:
    // 绘制迷宫
    void DrawMaze(wxDC &dc);

    // 检查位置是否在迷宫内
    bool IsInMaze(const Path &path) const;

    // 检查位置是否可通行
    bool IsPassable(const Path &path) const;

    int m_size = 51; 					// 迷宫大小
    int m_moveInterval = 50; 			// 移动定时器间隔
    int m_moveDelay = 100; 				// 移动延迟
    std::mt19937 m_gen; 				// 随机数生成器
    wxVector<wxVector<Element>> m_maze; // 迷宫
    Path m_personPath {}; 				// 玩家的位置

	wxDECLARE_EVENT_TABLE();
};

wxBEGIN_EVENT_TABLE(MazePanel, wxPanel)
    EVT_PAINT(MazePanel::OnPaint)
    EVT_KEY_DOWN(MazePanel::OnKeyDown)
    EVT_COMMAND(wxID_ANY, ctmEVT_GAME_WIN, MazePanel::OnGameWin)
wxEND_EVENT_TABLE()
  1. MazePanel 类继承自 wxWindow

  2. 在该类中,定义了两个类型:ElementPath,分别表示迷宫的元素和迷宫中的位置。

  3. 该类的构造函数中,调用了 GenerateMaze 函数生成了迷宫,并根据用户选择难度进行初始化。

  4. 提供了事件处理函数,用于处理重绘按键游戏胜利事件。

  5. 定义了两个移动函数,用于处理玩家的移动操作。

  6. 还提供了一个难度选择函数 ChooseDifficulty,用于提示用户选择难度

  7. GenerateMaze 函数用于生成迷宫。

  8. 还定义了一些辅助函数,如 IsInMazeIsPassable,用于检查指定位置是否在迷宫范围内是否可通行

  9. 为该控件定义了事件表,将事件与相应的处理函数进行绑定

这样就实现了一个简单的迷宫控件,通过对该控件进行相应的操作,可以实现迷宫游戏的功能。

(四) 迷宫控件类:迷宫生成

wxVector<wxVector<MazePanel::Element>> MazePanel::GenerateMaze(bool isHard)
{
    wxVector<wxVector<Element>> maze(m_size, wxVector<Element>(m_size, Element::Wall));
    wxVector<wxVector<bool>> visited(m_size, wxVector<bool>(m_size, false));
    constexpr static const int s_DX[] = {-2, 2, 0, 0};
    constexpr static const int s_DY[] = {0, 0, -2, 2};
    std::stack<Path> stack;
    stack.push(isHard ? (Path) {m_size - 2, m_size - 2} : (Path) {1, 1});

    while (!stack.empty()) {
        int arrDir[] = {0, 1, 2, 3};
        std::shuffle(arrDir, arrDir + 4, m_gen);
        Path lastPath = stack.top();

        // 遍历四个方向,如果有一个方向可以走,就压入栈
        bool find = false;
        for (int i : arrDir) {
            int newX = lastPath.x + s_DX[i];
            int newY = lastPath.y + s_DY[i];
            if (!IsInMaze({newX, newY}) || visited[newX][newY])
                continue;

            // 找到了可以走的方向
            find = true;
            stack.push({newX, newY});
            visited[newX][newY]        = true;
            Path midPath               = {lastPath.x + s_DX[i] / 2, lastPath.y + s_DY[i] / 2};
            maze[midPath.x][midPath.y] = Element::Path;
            maze[newX][newY]           = Element::Path;
            break;
        }

        // 回溯
        if (!find) stack.pop();
    }

    // 生成入口和出口
    Path ptPlay = {1, 0};
    while (maze[ptPlay.x][ptPlay.y + 1] == Element::Wall) ++ptPlay.x;
    Path ptExit = {m_size - 2, m_size - 1};
    while (maze[ptExit.x][ptExit.y - 1] == Element::Wall) --ptExit.x;
    maze[ptPlay.x][ptPlay.y] = Element::Person;
    maze[ptExit.x][ptExit.y] = Element::Exit;
    m_personPath             = ptPlay;  // 玩家的位置

    return maze;
}

这段代码定义了一个函数 GenerateMaze,该函数是 MazePanel 类的成员。这个函数生成一个迷宫并返回其数据结构。

1. 函数参数与返回值:

  • 参数:bool isHard,该参数确定是否要生成一个难度较高的迷宫。
  • 返回值:函数返回一个 wxVector<wxVector<MazePanel::Element>> 类型的数据结构,表示迷宫。

2. 函数体:

  • 首先,初始化一个大小为 m_size * m_size 的迷宫矩阵,并将所有格子设置为墙Element::Wall)。

  • 初始化一个大小为 m_size * m_size 的访问矩阵,记录每个格子是否被访问过,并初始化为 false

  • 定义两个常量数组 s_DXs_DY,它们分别表示沿 X轴Y轴 的方向移动的距离

  • 创建一个 std::stack<Path> 类型的栈 stack,用于迷宫生成过程中的回溯

  • 根据难度 isHard 选择起点,并将其压入栈

  • 使用一个 while 循环,当栈不为空时执行以下操作:

    • 初始化一个方向数组 arrDir随机打乱
    • 获取栈顶元素 lastPath不弹出)。
    • 遍历四个方向,如果有一个方向可以走(即未访问在迷宫范围内),就压入栈。
    • 该方向标记为已访问,并将与当前格子相邻的墙(注意方向)打通(将其设置为路径),同时打通新路径
    • 如果所有方向都已访问过,则弹出栈顶元素(回溯),回到上一个位置,再继续循环查找可以通行的新方向
  • 生成迷宫的入口出口,并设置玩家的位置:

    • 找到迷宫左侧第一个可走的位置,作为入口。
    • 找到迷宫右侧第一个可走的位置,作为出口。
    • 将入口位置设置为玩家Element::Person)。
    • 将出口位置设置为出口Element::Exit)。
    • 记录玩家的初始位置
  • 返回生成的迷宫。

这个函数使用了深度优先搜索算法来生成迷宫。生成的迷宫的入口和出口位置根据生成的路径来确定。

(五) 迷宫控件类:迷宫绘制

void MazePanel::OnPaint(wxPaintEvent &evt)
{
    wxBufferedPaintDC dc(this);
    dc.Clear();
    DrawMaze(dc);
}

void MazePanel::DrawMaze(wxDC &dc)
{
    int mazeCols = m_size;
    int mazeRows = m_size;

    // 根据窗口大小计算出每个迷宫格子的大小
    int width, height;
    GetClientSize(&width, &height);
    int cellSize = width < height ? width / mazeCols : height / mazeRows;

    dc.SetPen(*wxTRANSPARENT_PEN);

    for (int row = 0; row < mazeRows; ++row) {
        for (int col = 0; col < mazeCols; ++col) {
            int x = col * cellSize;
            int y = row * cellSize;

            switch (m_maze[row][col]) {
                case Element::Wall:
                    dc.SetBrush(*wxBLACK_BRUSH);
                    dc.DrawRectangle(x, y, cellSize, cellSize);
                    break;
                case Element::Path:
                    dc.SetBrush(*wxWHITE_BRUSH);
                    dc.DrawRectangle(x, y, cellSize, cellSize);
                    break;
                case Element::Exit:
                    dc.SetBrush(*wxGREEN_BRUSH);
                    dc.DrawRectangle(x, y, cellSize, cellSize);
                    break;
                case Element::Person:
                    dc.SetBrush(*wxWHITE_BRUSH);
                    dc.DrawRectangle(x, y, cellSize, cellSize);
                    dc.SetBrush(*wxRED_BRUSH);
                    dc.DrawCircle(x + cellSize / 2, y + cellSize / 2, cellSize / 2);
                    break;
            }
        }
    }
}

1. OnPaint(wxPaintEvent &evt) 函数:

  • 函数参数:函数的参数 evt 是一个 wxPaintEvent 对象的引用。wxPaintEvent 是 wxWidgets 库中的一个事件类,用于处理窗口绘图事件。
  • 函数体:
    • 创建一个 wxBufferedPaintDC 对象 dc,该对象是双缓冲绘图上下文,可以有效防止屏幕闪烁
    • 使用 Clear 方法清空窗口的绘图上下文
    • 调用 DrawMaze 函数,并将 dc 传递作为参数,用于在窗口上绘制迷宫

2. DrawMaze(wxDC &dc) 函数:

  • 函数参数:函数的参数 dc 是一个 wxDC 对象的引用。wxDC 是 wxWidgets 库中的一个绘图上下文类,用于在窗口上进行绘制操作。
  • 函数体:
    • 定义迷宫的行数列数mazeColsmazeRows)。
    • 使用 GetClientSize 方法获取窗口的宽度高度
    • 计算每个迷宫格子的大小 cellSize,这是根据窗口大小和迷宫的行数列数进行计算的。
    • 设置绘图上下文的画笔为透明画笔,以便只填充矩形,而不绘制边框
    • 使用两个嵌套的 for 循环遍历迷宫的每个元素,并进行绘制。
    • 根据迷宫的元素类型路径出口玩家),设置不同的画刷颜色,并绘制矩形圆形
      • :使用黑色画刷绘制一个填充的矩形。
      • 路径:使用白色画刷绘制一个填充的矩形。
      • 出口:使用绿色画刷绘制一个填充的矩形。
      • 玩家:首先使用白色画刷绘制一个填充的矩形(作为背景),然后使用红色画刷绘制一个填充的圆形(代表玩家)。

这段代码的目的是根据迷宫的数据结构,在窗口上绘制迷宫的可视化表示。每个迷宫格子都根据其类型使用不同的颜色进行绘制。当窗口接收到绘图事件(例如窗口大小更改窗口内容需要重绘)时,将触发 OnPaint 函数。

(六) 迷宫控件类:玩家移动

void MazePanel::OnKeyDown(wxKeyEvent &evt)
{
    switch (evt.GetKeyCode()) {
        case WXK_LEFT:
            Move(0, -1);
            break;
        case WXK_RIGHT:
            Move(0, 1);
            break;
        case WXK_UP:
            Move(-1, 0);
            break;
        case WXK_DOWN:
            Move(1, 0);
            break;
    }
}

void MazePanel::Move(int dx, int dy)
{
    int nextX = m_personPath.x + dx;
    int nextY = m_personPath.y + dy;
    if (!IsInMaze({nextX, nextY})) return;

    if (m_maze[nextX][nextY] == Element::Exit) {
        wxCommandEvent evt(ctmEVT_GAME_WIN);
        wxPostEvent(this, evt);  // 发送游戏通关事件
        return;
    }
    if (m_maze[nextX][nextY] == Element::Path) {
        m_maze[m_personPath.x][m_personPath.y]                 = Element::Path;
        m_maze[m_personPath.x = nextX][m_personPath.y = nextY] = Element::Person;
        Refresh();
    }
}

void MazePanel::Move(int keyCode)
{
    switch (keyCode) {
        case WXK_LEFT:
            Move(0, -1);
            break;
        case WXK_RIGHT:
            Move(0, 1);
            break;
        case WXK_UP:
            Move(-1, 0);
            break;
        case WXK_DOWN:
            Move(1, 0);
            break;
        default:
            break;
    }
}

这段代码主要处理 MazePanel 类中的玩家移动。有三个函数:OnKeyDown, Move 和另一个 Move 重载函数。

1. OnKeyDown(wxKeyEvent &) 函数:

  • 函数参数:wxKeyEvent &evt,表示发生的键盘事件
  • 函数功能:根据键盘事件中的按键代码(上、下、左、右),调用 Move 函数使玩家沿相应方向移动

2. Move(int dx, int dy) 函数:

  • 函数参数:int dx, int dy,分别表示玩家在 X轴Y轴 上要移动的距离。
  • 函数功能:
    • 首先计算玩家的新位置 nextXnextY
    • 检查新位置是否在迷宫范围内。如果不在范围内,则直接返回
    • 如果新位置是出口,发送游戏通关事件
    • 如果新位置是路径,则更新玩家位置
      • 原始位置设置为路径移除玩家标记)。
      • 新位置设置为玩家
      • 调用 Refresh() 函数刷新迷宫的绘制
  • 注意:这个函数只在按下上、下、左、右方向键时被调用。

3. Move(int keyCode) 函数(重载):

  • 函数参数:int keyCode,表示按下的键盘按键代码
  • 函数功能:根据按键代码,调用之前定义的 Move 函数并传递相应的移动方向。
  • 这个重载函数与 OnKeyDown 函数非常类似,但它接受按键代码而不是移动距离作为参数。这可以方便地在其他地方调用移动操作。

综上所述,这些函数都涉及处理玩家在迷宫中的移动,包括对按键事件的响应计算新位置检查新位置的合法性更新迷宫状态

四、完整代码

#include <ctime>
#include <random>
#include <stack>
#include <wx/wx.h>
#include <wx/dcbuffer.h>

// 游戏通关事件
wxDECLARE_EVENT(ctmEVT_GAME_WIN, wxCommandEvent);
wxDEFINE_EVENT(ctmEVT_GAME_WIN, wxCommandEvent);

class MazePanel : public wxWindow
{
protected:
    /// 元素
    enum class Element
    {
        Wall   = 0,  ///< 墙
        Path   = 1,  ///< 路
        Exit   = 2,  ///< 出口
        Person = 3,  ///< 玩家
    };

    /// 迷宫位置
    struct Path
    {
        int x;
        int y;
    };

public:
    explicit MazePanel(wxFrame *parent);

protected:
    void OnPaint(wxPaintEvent &evt);
    void OnKeyDown(wxKeyEvent &evt);
    void OnGameWin(wxCommandEvent &evt);

    /// 移动一段距离(dx和dy分别表示X和Y方向的移动距离)
    void Move(int dx, int dy);
    /// 往按键方向移动一步
    void Move(int keyCode);

    /// 难度选择(返回true表示选择了困难)
    static bool ChooseDifficulty();

    /// 生成迷宫(采用深度优先搜索算法)
    wxVector<wxVector<MazePanel::Element>> GenerateMaze(bool isHard = true);

private:
    void DrawMaze(wxDC &dc);

    /// 给定的 Path 对象是否在迷宫范围内
    bool IsInMaze(const Path &path) const { return path.x >= 0 && path.x < m_size && path.y >= 0 && path.y < m_size; }

    /// 给定的 Path 对象是否是可通行的(Path 或 Exit)
    bool IsPassable(const Path &path) const
    {
        return IsInMaze(path) && (m_maze[path.x][path.y] == Element::Path || m_maze[path.x][path.y] == Element::Exit);
    }

    int m_size         = 51;             ///< 迷宫大小
    int m_moveInterval = 50;             ///< 移动定时器间隔
    int m_moveDelay    = 100;            ///< 移动延迟
    std::mt19937 m_gen;                  ///< 随机数生成器
    wxVector<wxVector<Element>> m_maze;  ///< 迷宫
    Path m_personPath {};                ///< 玩家的位置

    wxDECLARE_EVENT_TABLE();
};


wxBEGIN_EVENT_TABLE(MazePanel, wxPanel)
    EVT_PAINT(MazePanel::OnPaint)
    EVT_KEY_DOWN(MazePanel::OnKeyDown)
    EVT_COMMAND(wxID_ANY, ctmEVT_GAME_WIN, MazePanel::OnGameWin)
wxEND_EVENT_TABLE()


MazePanel::MazePanel(wxFrame *parent) :
    wxWindow(parent, wxID_ANY),
    m_gen(std::random_device()())
{
    m_maze = GenerateMaze(ChooseDifficulty());
}


void MazePanel::OnPaint(wxPaintEvent &evt)
{
    wxBufferedPaintDC dc(this);
    dc.Clear();
    DrawMaze(dc);
}


void MazePanel::OnKeyDown(wxKeyEvent &evt)
{
    switch (evt.GetKeyCode()) {
        case WXK_LEFT:
            Move(0, -1);
            break;
        case WXK_RIGHT:
            Move(0, 1);
            break;
        case WXK_UP:
            Move(-1, 0);
            break;
        case WXK_DOWN:
            Move(1, 0);
            break;
    }
}


void MazePanel::DrawMaze(wxDC &dc)
{
    // 迷宫的行数和列数
    int mazeCols = m_size;
    int mazeRows = m_size;

    // 根据窗口大小计算出每个迷宫格子的大小
    int width, height;
    GetClientSize(&width, &height);
    // 每个迷宫格子的大小
    int cellSize = width < height ? width / mazeCols : height / mazeRows;

    dc.SetPen(*wxTRANSPARENT_PEN);

    for (int row = 0; row < mazeRows; ++row) {
        for (int col = 0; col < mazeCols; ++col) {
            int x = col * cellSize;  // 格子的左上角坐标X
            int y = row * cellSize;  // 格子的左上角坐标Y

            // 根据迷宫的元素绘制
            switch (m_maze[row][col]) {
                case Element::Wall:  // 墙
                    dc.SetBrush(*wxBLACK_BRUSH);
                    dc.DrawRectangle(x, y, cellSize, cellSize);
                    break;
                case Element::Path:  // 路
                    dc.SetBrush(*wxWHITE_BRUSH);
                    dc.DrawRectangle(x, y, cellSize, cellSize);
                    break;
                case Element::Exit:  // 出口
                    dc.SetBrush(*wxGREEN_BRUSH);
                    dc.DrawRectangle(x, y, cellSize, cellSize);
                    break;
                case Element::Person:  // 玩家
                    dc.SetBrush(*wxWHITE_BRUSH);
                    dc.DrawRectangle(x, y, cellSize, cellSize);
                    dc.SetBrush(*wxRED_BRUSH);
                    dc.DrawCircle(x + cellSize / 2, y + cellSize / 2, cellSize / 2);
                    break;
            }
        }
    }
}


void MazePanel::Move(int dx, int dy)
{
    int nextX = m_personPath.x + dx;
    int nextY = m_personPath.y + dy;
    if (!IsInMaze({nextX, nextY})) return;

    if (m_maze[nextX][nextY] == Element::Exit) {  // 走出迷宫
        wxCommandEvent evt(ctmEVT_GAME_WIN);
        wxPostEvent(this, evt);  // 发送游戏通关事件(此处不涉及到多线程,所以可以直接调用)
        return;
    }
    if (m_maze[nextX][nextY] == Element::Path) {  // 可以走
        m_maze[m_personPath.x][m_personPath.y]                 = Element::Path;
        m_maze[m_personPath.x = nextX][m_personPath.y = nextY] = Element::Person;
        Refresh();
    }
}


void MazePanel::Move(int keyCode)
{
    switch (keyCode) {
        case WXK_LEFT:
            Move(0, -1);
            break;
        case WXK_RIGHT:
            Move(0, 1);
            break;
        case WXK_UP:
            Move(-1, 0);
            break;
        case WXK_DOWN:
            Move(1, 0);
            break;
        default:
            break;
    }
}


bool MazePanel::ChooseDifficulty()
{
    wxMessageDialog dialog(nullptr, "Choose Difficulty", "Maze Game", wxYES_NO | wxICON_QUESTION);
    dialog.SetYesNoLabels("Easy", "Hard");
    return dialog.ShowModal() == wxID_NO;
}


wxVector<wxVector<MazePanel::Element>> MazePanel::GenerateMaze(bool isHard)
{
    wxVector<wxVector<Element>> maze(m_size, wxVector<Element>(m_size, Element::Wall));  // 迷宫,全部是墙(后面会打通)
    wxVector<wxVector<bool>> visited(m_size, wxVector<bool>(m_size, false));             // 记录是否访问过
    constexpr static const int s_DX[] = {-2, 2, 0, 0};                                   // 用于生成迷宫的X方向
    constexpr static const int s_DY[] = {0, 0, -2, 2};                                   // 用于生成迷宫的Y方向
    std::stack<Path> stack;                                                              // 用于生成迷宫的栈
    stack.push(isHard ? (Path) {m_size - 2, m_size - 2} : (Path) {1, 1});                // 根据难度选择起点(难度越高,起点越靠近迷宫

    while (!stack.empty()) {
        int arrDir[] = {0, 1, 2, 3};  // 用于生成迷宫的方向
        std::shuffle(arrDir, arrDir + 4, m_gen);
        Path lastPath = stack.top();

        // 遍历四个方向,如果有一个方向可以走,就压入栈
        bool find = false;
        for (int i : arrDir) {
            int newX = lastPath.x + s_DX[i];
            int newY = lastPath.y + s_DY[i];
            // 如果不在迷宫范围内或者已经访问过了,就跳过
            if (!IsInMaze({newX, newY}) || visited[newX][newY])
                continue;

            // 找到了可以走的方向
            find = true;
            stack.push({newX, newY});
            visited[newX][newY]        = true;                                                  // 访问过了
            Path midPath               = {lastPath.x + s_DX[i] / 2, lastPath.y + s_DY[i] / 2};  // 记录中间路径
            maze[midPath.x][midPath.y] = Element::Path;                                         // 打通新旧路径之间的墙
            maze[newX][newY]           = Element::Path;                                         // 打通新路径
            break;
        }

        // 如果都访问过了,就弹出,反问上一个(回溯)
        if (!find) stack.pop();
    }

    // 生成入口和出口
    Path ptPlay = {1, 0};
    while (maze[ptPlay.x][ptPlay.y + 1] == Element::Wall) ++ptPlay.x;  // 从左边开始找入口,找到第一个可以走的位置
    Path ptExit = {m_size - 2, m_size - 1};
    while (maze[ptExit.x][ptExit.y - 1] == Element::Wall) --ptExit.x;  // 从右边开始找出口,找到第一个可以走的位置
    maze[ptPlay.x][ptPlay.y] = Element::Person;
    maze[ptExit.x][ptExit.y] = Element::Exit;
    m_personPath             = ptPlay;  // 玩家的位置

    return maze;
}


void MazePanel::OnGameWin(wxCommandEvent &evt)
{
    // 重新生成迷宫
    wxMessageBox("You Win!", "Maze Game", wxOK | wxICON_INFORMATION);
    m_maze = GenerateMaze(ChooseDifficulty());
    Refresh();
}


class MyFrame : public wxFrame
{
public:
    MyFrame(const wxString &title, const wxPoint &pos, const wxSize &size) :
        wxFrame(nullptr, wxID_ANY, title, pos, size)
    {
        SetClientSize(GetSize());
        new MazePanel(this);
    }
};


class MyApp : public wxApp
{
public:
    bool OnInit() override
    {
        auto *frame = new MyFrame("Maze Game", wxDefaultPosition, wxSize(816, 816));
        frame->Show(true);
        frame->Center();
        return true;
    }
};

wxIMPLEMENT_APP(MyApp);  // NOLINT

至于怎么创建 wxWdigets 项目,可参考上一期——【wxWidgets实战】跳动的泡泡

五、要点分析

1. 类结构与成员变量:

  • MazePanel 类是迷宫游戏的主要实现类,它是 wxWindow 的子类,代表了游戏的迷宫控件
  • Element 是一个枚举类型,表示迷宫中的四种元素:墙(Wall)路(Path)出口(Exit)玩家(Person)
  • Path 结构体用于表示迷宫中的位置(坐标)。
  • 类成员变量包括迷宫大小移动间隔移动延迟随机数生成器迷宫数据玩家位置等。

2. 事件处理与绘制:

  • 事件表(Event Table)用于将特定事件(如键盘按键、画图事件)绑定到对应的处理函数
  • OnPaint 函数处理画图事件,使用 wxBufferedPaintDC 对象进行双缓冲绘制减少画面闪烁
  • DrawMaze 函数根据迷宫数据绘制迷宫的各个元素
  • OnKeyDown 函数处理键盘按键事件,根据按键调用 Move 函数来移动玩家。

3. 迷宫生成与移动:

  • 迷宫是通过 GenerateMaze 函数生成的,使用深度优先搜索(DFS)算法来生成迷宫。该算法首先选择一个起点,然后随机选择一个方向前进,不断拓展新的路径。如果遇到无法前进的情况,则回溯之前的位置
  • Move 函数用于玩家移动。当玩家尝试移动到某个位置时,会先检查该位置是否在迷宫范围内。如果玩家移动到出口,则触发游戏通关事件

4. 难度选择与游戏通关:

  • ChooseDifficulty 函数用于选择游戏难度。玩家可以选择简单困难两种难度。
  • OnGameWin 函数处理游戏通关事件。当玩家走出迷宫时,会弹出提示框,并重新生成迷宫

5. 其他:

  • wxCommandEventwxPostEvent 用于自定义事件事件处理
  • wxMessageBox 用于显示消息框
  • std::random_device()std::mt19937std::shuffle 用于产生真随机数(通过硬件的实时参数,Linux下会访问 /dev/random 以获取真随机数,Windows 我不知道)生成随机数随机打乱方向

(六) 总结

这是一个迷宫游戏,在该游戏中,玩家通过控制一个红色圆点在迷宫中移动,从入口开始寻找到出口,完成游戏。项目通过使用深度优先搜索算法生成迷宫,并通过监听用户的键盘输入事件来控制玩家的移动。迷宫的生成绘制都基于 wxWidgets 库,并在游戏结束后会弹出提示框通知用户。整体来看,代码运用了深度优先搜索随机数生成器wxWidgets 库等技术实现了一个简单的迷宫游戏。


【wxWidgets实战】迷宫游戏 实战至此结束,感谢大家的热情关注和鼎力支持!如有任何改进建议或疑问,欢迎随时向我提出。在此,我真诚地祝愿大家在技术学习的道路上不断突破,事业发展一路向前,每天都充实而有所收获!

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xiao_Ley

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值