先看效果——
一、项目构思
- 能自动生成迷宫,并且可以自定义迷宫的大小,每一次的迷宫都不重样;
- 迷宫要有简单模式和困难模式,难度一定能要有明显的变化;
- 使用 wxWidgets 把迷宫绘制出来;
- 能通过上下左右键盘按键进行操作。
二、思路分析
(一) 生成迷宫
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);
MyApp
为程序入口类,有且仅有一个,在以往的项目和教程中我都有介绍,这里就不细说了。wxIMPLEMENT_APP
为程序入口宏,内部封装了main
函数,其目的是为了更好的跨平台。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()
-
MazePanel
类继承自wxWindow
。 -
在该类中,定义了两个类型:
Element
和Path
,分别表示迷宫的元素和迷宫中的位置。 -
该类的构造函数中,调用了
GenerateMaze
函数生成了迷宫,并根据用户选择难度进行初始化。 -
提供了事件处理函数,用于处理重绘、按键和游戏胜利事件。
-
定义了两个移动函数,用于处理玩家的移动操作。
-
还提供了一个难度选择函数
ChooseDifficulty
,用于提示用户选择难度。 -
GenerateMaze
函数用于生成迷宫。 -
还定义了一些辅助函数,如
IsInMaze
和IsPassable
,用于检查指定位置是否在迷宫范围内和是否可通行。 -
为该控件定义了事件表,将事件与相应的处理函数进行绑定。
这样就实现了一个简单的迷宫控件,通过对该控件进行相应的操作,可以实现迷宫游戏的功能。
(四) 迷宫控件类:迷宫生成
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_DX
和s_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 库中的一个绘图上下文类,用于在窗口上进行绘制操作。 - 函数体:
- 定义迷宫的行数和列数(
mazeCols
和mazeRows
)。 - 使用
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轴 上要移动的距离。 - 函数功能:
- 首先计算玩家的新位置
nextX
和nextY
。 - 检查新位置是否在迷宫范围内。如果不在范围内,则直接返回。
- 如果新位置是出口,发送游戏通关事件。
- 如果新位置是路径,则更新玩家位置:
- 将原始位置设置为路径(移除玩家标记)。
- 将新位置设置为玩家。
- 调用
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. 其他:
wxCommandEvent
和wxPostEvent
用于自定义事件和事件处理。wxMessageBox
用于显示消息框。std::random_device()
、std::mt19937
和std::shuffle
用于产生真随机数(通过硬件的实时参数,Linux下会访问/dev/random
以获取真随机数,Windows 我不知道)、生成随机数和随机打乱方向。
(六) 总结
这是一个迷宫游戏,在该游戏中,玩家通过控制一个红色圆点在迷宫中移动,从入口开始寻找到出口,完成游戏。项目通过使用深度优先搜索算法生成迷宫,并通过监听用户的键盘输入事件来控制玩家的移动。迷宫的生成和绘制都基于 wxWidgets 库,并在游戏结束后会弹出提示框通知用户。整体来看,代码运用了深度优先搜索、随机数生成器、wxWidgets 库等技术实现了一个简单的迷宫游戏。
【wxWidgets实战】迷宫游戏 实战至此结束,感谢大家的热情关注和鼎力支持!如有任何改进建议或疑问,欢迎随时向我提出。在此,我真诚地祝愿大家在技术学习的道路上不断突破,事业发展一路向前,每天都充实而有所收获!