以下实验题目任选一个(用控制台应用程序、Windows API或MFC实现均可):
- 编写一本通信录
- 模拟简单计算器
- 简单的管理系统的设计:如人事、工资、学生成绩等。
4、迷宫生成器
要求:
- 单文档界面。
- 加入相关资源。
- 在视图中绘制随机产生的迷宫图案,并可求解。
五、实验报告要求
1、实验步骤
1)设计确定类的结构及各类之间的关系,注意成员变量和函数的性质(共有、私有或保护),哪些函数需要动态(定义为虚拟函数)。
2)创建系统项目(解决方案, Project)。
3)按上述结构定义各类,在构造函数中对成员变量进行初始化。
4)定义各类中的成员函数
5)对用到的算法进行描述
2、完成编写相关实验代码
3、对实验结果进行分析(有截图)
4、对本次实验要有总结
注意:要求在系统设计阶段对数据结构(主要是类的结构及类之间的联系)进行分析研究,充分利用面向对象的特性,使类结构尽可能合理和高效。
选择了迷宫生成器的制作
目前项目的功能基本完备,源码放在了GitHub上(视网络情况更新)
项目做下来对“离散”有了更深的认识,以前无法想象代码是怎么做到可视化的,现在对底层的实现有了一定的了解。
迷宫项目本身复杂度不高,关键点在于如何把生成函数和迷宫的可视化联系在一起。
如果是制作植物大战僵尸等游戏,还需要引入画面刷新机制,这会是更大的挑战。
对于实验报告中充分利用所学知识的要求还算契合,只不过没有虚函数,没有多态,没有重载运算符。
1.0版
实现基本功能
2.0版
由于水平不足,迟迟做不出来连续的墙,于是突发奇想,不如化缺点为优点,寻找适合离散的墙的呈现形式,于是在美术方面做了观感上极大的优化
3.0版
进一步优化美术效果,添加了初始界面
对我帮助很大的三个视频:
04、游戏鼠标响应与键盘响应_转_哔哩哔哩_bilibili
MFC 贪吃蛇小程序(从创建项目开始)_哔哩哔哩_bilibili
MFC本身算得上是一个比较老的东西,观看视频学习与实际操作时往往会出现一些奇妙的偏差。
以下是遇到过的一些问题以及解决方案:
1.越用越卡,性能差
①有一些东西是配套使用的(类似new和delete),比如
CDC* pdc = GetDlgItem(IDC_MAZEPIC)->GetWindowDC();
之后就一定要
ReleaseDC(pdc);
②画笔画刷等工具不要反复创建
创建画刷
CBrush* poldBrs = pdc->SelectObject(&m_brush[3]);
需要切换颜料时要先复位
pdc->SelectObject(&poldBrs);
然后再选择另一个颜料
pdc->SelectObject(&m_brush[1]);
最后记得要及时回收
poldBrs->DeleteObject();
③数据过大时,可以在堆区放一些数据,防止栈区数据溢出
2.图标更改失败
把要用的图标的资源ID改成比128小的值(在Resource.h里面改)
在OnCreat()中输入以下代码:
HICON ico = AfxGetApp()->LoadIcon(IDI_ICON2);
SetIcon(ico, TRUE);//设置大图标
SetIcon(ico, FALSE);//设置小图标
3.迷宫的墙不连续
把核心生成算法换了(还没换)
4.用并查集算法生成迷宫失败
重新写一遍(还没写)
5.重写OnKeyDown函数没反应
重写PreTranslateMessage函数,把OnKeyDown的实现放里面
框架:
if (pMsg->message == WM_KEYDOWN)
{
switch (pMsg->wParam)
{
}
}
6.点击重写OnTimer等函数时弹出异常窗口
其实已经创建了,再点反而会多创建几个,记得删掉
7.重写OnTimer函数没反应
检查三件套是否有缺漏(最可能是消息缺漏):
afx_msg void OnTimer(UINT nIDEvent);
ON_WM_TIMER()
void CXXXDlg::OnTimer(UINT nIDEvent)
{
CDialog::OnTimer(nIDEvent);
// TODO:
}
8.二维数组越界
在判断边界时数组下标的+-可能导致越界问题,这时可以把数组扩大一圈,让外围存在一条隐形的路,也可以把游玩的区域缩小一圈,如果非要让最外圈是墙的话就要根据实际情况改条件或加条件了
9.剩下的忘了
未完待续...
迷宫生成算法第一代(Prim算法的超级简化版,缺点是迷宫有效路径单一)
void CMazeDlg::PrimGenerate()
{
// 游戏区
CDC* pdc = GetDlgItem(IDC_MAZEPIC)->GetWindowDC();
// 迷宫初始化,绘制背景
CBrush* poldBrs = pdc->SelectObject(&m_brush[3]);
for (int i = 0; i < 41; i++)
{
for (int j = 0; j < 41; j++)
{
m_map[i][j].left = 0 + j * 20;
m_map[i][j].right = 20 + j * 20;
m_map[i][j].top = 0 + i * 20;
m_map[i][j].bottom = 20 + i * 20;
//pdc->SelectObject(m_brush[3]);
pdc->Rectangle(m_map[i][j]);
//pdc->SelectObject(&poldBrs);
}
}
pdc->SelectObject(&poldBrs);
//ReleaseDC(pdc);
int Maze[L][L] = { 0 };//0为墙,1为路,-1为边界(无法破坏)
//最外围设置为边界
for (int i = 0; i < L; i++)
{
Maze[i][0] = -1;
Maze[0][i] = -1;
Maze[L - 1][i] = -1;
Maze[i][L - 1] = -1;
}
//墙队列,包括X , Y
std::vector<std::pair<int, int>> Wall;
//设置迷宫入口
Maze[0][1] = 1;
//把起点的邻居放入墙队列
Wall.emplace_back(1, 1);
//当墙队列为空时结束循环
while (Wall.size())
{
//在墙队列中随机取一点
int idx = rand() % Wall.size();
int x = Wall[idx].first;
int y = Wall[idx].second;
//判读上下左右四个方向是否为路
int count = 0;
for (int i = x - 1; i < x + 2; i++) {
for (int j = y - 1; j < y + 2; j++) {
if (abs(x - i) + abs(y - j) == 1 && Maze[i][j] > 0) {
++count;
}
}
}
if (count <= 1)
{
Maze[x][y] = 1;
//在墙队列中插入新的墙
for (int i = x - 1; i < x + 2; i++) {
for (int j = y - 1; j < y + 2; j++) {
if (abs(x - i) + abs(y - j) == 1 && Maze[i][j] == 0) {
Wall.emplace_back(i, j);
}
}
}
}
//删除当前墙
Wall.erase(Wall.begin() + idx);
}
if (Maze[L - 2][L - 2] != 1)//确保迷宫有解
{
PrimGenerate();
return;
}
//标记可走路线,用于迷宫求解
for (int i = 0; i < L; i++)
{
for (int j = 0; j < L; j++)
{
BOOK[i][j] = Maze[i][j];
}
}
step = 0;
min_step = 999;
//画迷宫
for (int i = 0; i < L; i++)
{
for (int j = 0; j < L; j++)
{
if (Maze[i][j] == 1) // 路
{
continue;
}
else // 墙或边界
{
CDC* pdc = GetDlgItem(IDC_MAZEPIC)->GetWindowDC();
CBrush* poldBrs = pdc->SelectObject(&m_brush[2]);
m_map[i][j].left = 0 + j * 20;
m_map[i][j].right = 20 + j * 20;
m_map[i][j].top = 0 + i * 20;
m_map[i][j].bottom = 20 + i * 20;
pdc->SelectObject(m_brush[2]);
pdc->Rectangle(m_map[i][j]);
pdc->SelectObject(&poldBrs);
//ReleaseDC(pdc);
}
}
}
/*CDC* pdc = GetDlgItem(IDC_MAZEPIC)->GetWindowDC();
CBrush* poldBrs = pdc->SelectObject(&m_brush[1]);*/
//画人物
pdc->SelectObject(&m_brush[1]);
pdc->Rectangle(m_map[0][1]);
pdc->SelectObject(&poldBrs);
//画终点
pdc->SelectObject(&m_brush[0]);
pdc->Rectangle(m_map[L - 1][L - 2]);
pdc->SelectObject(&poldBrs);
ReleaseDC(pdc); // 释放设备上下文对象
}
迷宫求解函数第一代(基于DFS,缺点在于过于暴力,迷宫规模过大时不适合)
void CMazeDlg::DfsSolve(int x, int y)
{
int direction[4][2] =
{
{-1, 0}, // 上
{1, 0}, // 下
{0, -1}, // 左
{0, 1} // 右
};
BOOK[x][y] = 2; // 标记当前节点为已访问
if (x == L - 2 && y == L - 2)
{
//更新最短路径
if (step < min_step)
{
min_step = step;
}
// 画路径
for (int i = 1; i < L; i++)
{
for (int j = 1; j < L; j++)
{
CDC* pDC = GetDlgItem(IDC_MAZEPIC)->GetWindowDC();
if (pDC != nullptr)
{
if (BOOK[i][j] == 2)
{
pDC->SelectObject(&m_brush[4]);
CRect rect(j * 20 + 1, i * 20 + 1, (j + 1) * 20 - 1, (i + 1) * 20 - 1);
pDC->FillRect(rect, &m_brush[4]);
ReleaseDC(pDC);
}
}
}
}
// 找到出口
MessageBox(_T("恭喜您,成功找到出口!"), _T("迷宫解决成功"), MB_OK);
return;
}
for (int i = 0; i < 4; i++)
{
int nx = x + direction[i][0];
int ny = y + direction[i][1];
if (nx >= 0 && nx <= L - 1 && ny >= 0 && ny <= L - 1 && BOOK[nx][ny] == 1)
{
// 标记下一个点为已经走过
BOOK[nx][ny] = 2;
step++;
// 递归执行下一步
DfsSolve(nx, ny);
// 取消下一个点的标记并回溯
BOOK[nx][ny] = 1;
step--;
}
}
return;
}
OnPaint函数第一代(缺点在于资源浪费严重,调用次数多后很容易卡)
void CMazeDlg::OnPaint()
{
CPaintDC dc(this); // device context for painting
// TODO: 在此处添加消息处理程序代码
// 不为绘图消息调用 CDialogEx::OnPaint()
CRect rect;
CDC* pClientDC = GetDC();
(this->GetDlgItem(IDC_MAZEPIC))->GetWindowRect(&rect); // 获取控件相对于屏幕的位
ScreenToClient(rect); // 转化为相对于客户区的位置
CPen pen(PS_SOLID, 6, RGB(255, 160, 122));
CPen pen2(PS_SOLID, 18, RGB(255, 160, 122));
CPen* oldPen = pClientDC->SelectObject(&pen2);//描绘边缘
pClientDC->Rectangle(rect);
rect.left -= 5;
rect.right += 5;
rect.top -= 5;
rect.bottom += 5;
pClientDC->SelectObject(&pen);//描绘外围框
pClientDC->Rectangle(rect);
pen.DeleteObject();
pen2.DeleteObject();
ReleaseDC(pClientDC);
// 游戏区
CDC* pdc = GetDlgItem(IDC_MAZEPIC)->GetWindowDC();
// 迷宫初始化
CBrush* poldBrs = pdc->SelectObject(&m_brush[3]);
for (int i = 0; i < 41; i++)
{
for (int j = 0; j < 41; j++)
{
m_map[i][j].left = 0 + j * 20;
m_map[i][j].right = 20 + j * 20;
m_map[i][j].top = 0 + i * 20;
m_map[i][j].bottom = 20 + i * 20;
pdc->SelectObject(m_brush[3]);
pdc->Rectangle(m_map[i][j]);
}
}
ReleaseDC(pdc);
}
OnInitDialog函数第一代(缺点未知)
BOOL CMazeDlg::OnInitDialog()
{
CDialogEx::OnInitDialog();
CRect rect;
(this->GetDlgItem(IDC_MAZEPIC))->GetWindowRect(&rect); // 获取控件相对于屏幕的位置
ScreenToClient(&rect); // 转化为相对于客户区的位置
GetDlgItem(IDC_MAZEPIC)->MoveWindow(rect.left, rect.top - 30, 820, 820, false);
// 画刷初始化
CBitmap herobmp, wallbmp, bgbmp, ptbmp, swbmp;
bgbmp.LoadBitmapW(IDB_BACKGROUND);
wallbmp.LoadBitmapW(IDB_WALL);
herobmp.LoadBitmapW(IDB_HERO);
ptbmp.LoadBitmapW(IDB_POINT);
swbmp.LoadBitmapW(IDB_SHOWWAY);
m_brush[0].CreatePatternBrush(&ptbmp);
m_brush[1].CreatePatternBrush(&herobmp);
m_brush[2].CreatePatternBrush(&wallbmp);
m_brush[3].CreatePatternBrush(&bgbmp);
m_brush[4].CreatePatternBrush(&swbmp);
return TRUE; // return TRUE unless you set the focus to a control
// 异常: OCX 属性页应返回 FALSE
}