C++面向对象 第五次实验报告

本文介绍了使用MFC开发迷宫生成器的过程,包括设计类结构、实现算法、遇到的问题及解决方案。作者分享了从基础功能到美术优化的版本迭代,并提到了对代码可视化和离散数学理解的深入。在开发中遇到了性能优化、资源管理、事件处理等问题,通过不断调试和学习解决了这些问题。此外,还探讨了基于DFS的迷宫求解方法和存在的局限性。
摘要由CSDN通过智能技术生成

以下实验题目任选一个(用控制台应用程序、Windows API或MFC实现均可):

  1. 编写一本通信录
  2. 模拟简单计算器
  3. 简单的管理系统的设计:如人事、工资、学生成绩等。

4、迷宫生成器

要求:

  1. 单文档界面。
  2. 加入相关资源。
  3. 在视图中绘制随机产生的迷宫图案,并可求解。

五、实验报告要求

1、实验步骤

1)设计确定类的结构及各类之间的关系,注意成员变量和函数的性质(共有、私有或保护),哪些函数需要动态(定义为虚拟函数)。

2)创建系统项目(解决方案, Project)。

3)按上述结构定义各类,在构造函数中对成员变量进行初始化。

4)定义各类中的成员函数

5)对用到的算法进行描述

2、完成编写相关实验代码

3、对实验结果进行分析(有截图)

4、对本次实验要有总结

注意:要求在系统设计阶段对数据结构(主要是类的结构及类之间的联系)进行分析研究,充分利用面向对象的特性,使类结构尽可能合理和高效。


选择了迷宫生成器的制作

目前项目的功能基本完备,源码放在了GitHub上(视网络情况更新)

suk1ran/-MFC- (github.com)


项目做下来对“离散”有了更深的认识,以前无法想象代码是怎么做到可视化的,现在对底层的实现有了一定的了解。

迷宫项目本身复杂度不高,关键点在于如何把生成函数和迷宫的可视化联系在一起。

如果是制作植物大战僵尸等游戏,还需要引入画面刷新机制,这会是更大的挑战。

对于实验报告中充分利用所学知识的要求还算契合,只不过没有虚函数,没有多态,没有重载运算符。


1.0版

实现基本功能

 2.0版

由于水平不足,迟迟做不出来连续的墙,于是突发奇想,不如化缺点为优点,寻找适合离散的墙的呈现形式,于是在美术方面做了观感上极大的优化

 3.0版

进一步优化美术效果,添加了初始界面


对我帮助很大的三个视频:

12_菜单栏设置_哔哩哔哩_bilibili

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
}

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值