计算机软件实验2-基于MFC的贪吃蛇游戏制作
课前预习
课程要求
- 实现贪吃蛇游戏 基本功能, 屏幕上随机出现一个 食物 称为豆子, 上下左右控制 “蛇 ”的移动 吃到 豆子 ”以后 “蛇 ”的身体加长 一点。
- “蛇 ”碰到边界 或蛇头与蛇身相撞 蛇死亡, 游戏结束。
- 为游戏设计友好的交互界面;例如欢迎界面,游戏界面,游戏结束界面。要有开始键、暂停键和停止退出的选项。
- 对蛇吃到豆子进行分值计算,可以设置游戏速度,游戏音乐等拓展元素。
题目难点
- 要实现随机生成豆子(豆子不能和蛇头出现在同一位置)。
- 蛇身要自主增长(蛇头吃到豆子以后变长,不能直接在蛇尾增加长度,这个很可能撞墙,要在蛇头增加长度,并且更新蛇长)。
- 界面设计,对于游戏的不同状况要对进行分类,出现不同的提示。
贪吃蛇基本原理
我们先看一下制作贪吃蛇我们需要哪些部分:
对于用户界面设计:
afx_msg void OnBnClickedButtonStop();//stop按键
afx_msg void OnBnClickedButtonStart();//start按键
afx_msg void OnTimer(UINT_PTR nIDEvent);//界面初始化设计
CComboBox Box;//在屏幕上中修改游戏速度选项
CEdit EDIT_Score;//在屏幕展现游戏分数
virtual BOOL PreTranslateMessage(MSG* pMsg);//传送消息
对于游戏区设计:
afx_msg void OnCbnSelchangeCombospeed();//游戏界面蛇的运动速度改变
afx_msg void OnWindowPosChanging(WINDOWPOS* lpwndpos);//游戏界面改变豆子的位置
CRect m_map[30][38];//确定游戏地图大小
POINT pos = { 0,0 };//定义豆子的位置的指针
CPen pen1;//画笔1,用于画出游戏界面的格子
void gameOver();//游戏结束
int speed = 150; // 设置速度
int score = 0;//计分
bool INIT = true;//初始化是否成功
bool START = false;//判断游戏是否开始
bool DEAD = false;//判断蛇是否死亡
bool setBean();//随机生成豆子
豆子的生成要求为:一颗豆子被吃掉后立刻生成另一颗豆子,并且豆子不能与蛇头重合。
我们选择随机生成函数进行随机生成豆子。由于豆子是时刻展示在屏幕上的,需要画刷随时将其绘制出来。
对于蛇的设定:
CSnake my_snake;//定义蛇
void SnackInit();//蛇的初始化
void drawSnake();//画蛇增加长度
void MvUp();//上移
void MvDown();//下移
void MvLeft();//左移
void MvRight();右移
void eatBean();//吃豆子
bool checkLive();//检测蛇是否活着
蛇身的生长是贪吃蛇游戏的一个难点,当蛇吃到豆子之后,我们选择for循环时刻更新蛇的身体,并通过画刷展示在屏幕界面上。(另一种做法:通过链表的方式修改蛇身,通过修改指针来修改蛇身)。
整个过程的设计:
基于MFC制作贪吃蛇(环境VS2019)
利用MFC制作界面
用户界面及游戏界面的设计:
首先我们像实验1一样添加按钮,并修改按键的ID和按键的显示名称。(修改按键的ID是为了方便后面设计程序时找到相对应的按键)
然后在界面上添加一个BOX,这个是用来更换游戏速度的下拉选择框,同样的修改按键ID。
BOX下拉框是用来修改游戏速度,因此需要在属性界面设定数据,具体内容如图所示。(图片上显示的就是将该游戏设定为四个速度,分别为1,2,3,4)
编辑框的作用是显示用户游戏得分,同样的在属性界面修改 ID及相关信息。
下面展示的是在用户界面上文字提示,同样的在属性界面修改相对应的内容。
最后一部分展示的是游戏区,用的是编辑框,同样也进行修改相关内容。
确定界面的设计:
该界面的设计主要是负责蛇死亡之后的界面展示,判断是否要继续游戏。
功能实现
下面我们展示一下个部分功能的实现的代码:
蛇的主要功能展现:
- 蛇身的定义:
蛇身的定义是专门添加的一个类Snake来写的,这个类辅助其他的类的运行(其他部分的内容都是在主程序中)。
include "pch.h"
#include "framework.h"
#include "MfcSnake.h"
#include "MfcSnakeDlg.h"
#include "Snack.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
CSnake::CSnake()//构造函数
{
// 初始化蛇的长度为1, 向右运动, 头的位置为(10, 10)
len = 1;
direc = 4; // 上下左右1,2,3,4
body[0].x = 10;
body[0].y = 10;
}
CSnake::~CSnake()//析构函数
{
}
//设定蛇的运动
void CSnake::UpMove()
{
for (int i = len - 1; i > 0; --i) {
body[i].x = body[i - 1].x;
body[i].y = body[i - 1].y;
}
body[0].x--;
direc = 1;
}
void CSnake::DownMove()
{
for (int i = len - 1; i > 0; --i) {
body[i].x = body[i - 1].x;
body[i].y = body[i - 1].y;
}
body[0].x++;
direc = 2;
}
void CSnake::LeftMove()
{
for (int i = len - 1; i > 0; --i) {
body[i].x = body[i - 1].x;
body[i].y = body[i - 1].y;
}
body[0].y--;
direc = 3;
}
void CSnake::RightMove()
{
for (int i = len - 1; i > 0; --i) {
body[i].x = body[i - 1].x;
body[i].y = body[i - 1].y;
}
body[0].y++;
direc = 4;
}
void CSnake::init()
{
body[0].x = 10;
body[0].y = 10;
len = 1;
direc = 4;
}
- 蛇的初始化:
蛇的初始化这部分内容是初始化整个游戏,相当于重新开始游戏。
void CMfcSnakeDlg::SnackInit()
{
// 游戏区
CDC* pdc = GetDlgItem(IDC_game)->GetWindowDC();
// 棋盘初始化
CBrush* pOldBrs = pdc->SelectObject(&m_brush[3]);
CPen* pOldPen = pdc->SelectObject(&pen1);
for (int i = 0; i < 30; i++) {
for (int j = 0; j < 38; j++) {
m_map[i][j].left = 0 + j * 20;//map是一个矩形框
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(&pen1);//用画笔把画框的周围上色,避免周围没上色
//pdc->Rectangle(m_map[i][j]);
}
}
pdc->SelectObject(&pOldBrs);//切换回原来的画刷
// 蛇初始化
my_snake.init();
score = 0;
START = false;
DEAD = false;
srand((unsigned)time(NULL));
drawSnake();
setBean();//设置豆子位置
// 控件初始化
EDIT_Score.SetWindowTextW(_T("0"));
SetDlgItemText(IDC_BUTTON_Start, _T("start"));
}
- 蛇身增长:
蛇身增长主要是通过画刷和循环来完成(并且要保证蛇身不能出边框),并实时展现在屏幕上。
void CMfcSnakeDlg::drawSnake(){
CDC* pdc = GetDlgItem(IDC_game)->GetWindowDC();//修改内容展示在游戏区屏幕上
CBrush* pOldBrs = pdc->SelectObject(&m_brush[0]);
for (int i = 1; i < my_snake.len; i++) {//通过循环来增长蛇身
pdc->SelectObject(&m_brush[0]);
pdc->Rectangle(m_map[my_snake.body[i].x][my_snake.body[i].y]);
pdc->SelectObject(&pen1);
pdc->Rectangle(m_map[my_snake.body[i].x][my_snake.body[i].y]);
}
pdc->SelectObject(&m_brush[1]);
pdc->Rectangle(m_map[my_snake.body[0].x][my_snake.body[0].y]);
pdc->SelectObject(&pen1);
pdc->Rectangle(m_map[my_snake.body[0].x][my_snake.body[0].y]);
pdc->SelectObject(&pOldBrs);
pdc->DeleteDC();//画刷用完删除
}
- 蛇吃豆子:
蛇吃完豆子之后蛇身要增长,同时还要立刻生成一个新的豆子,并且还要更新游戏界面上的分数。
void CMfcSnakeDlg::eatBean()
{
if (pos.x == my_snake.body[0].x && pos.y == my_snake.body[0].y) {//当头和豆子的位置是一样的
my_snake.len++;//蛇的长度加加
setBean();//重新画一个豆子
score = score + (abs(pos.x - my_snake.body[0].x) + abs(pos.y - my_snake.body[0].y));
CString str;
str.Format(_T("%d"), score);
EDIT_Score.SetWindowTextW(str);//在编辑框中更新分数
}
}
- 蛇的移动:
蛇身的移动包括上下左右,由于是蛇的移动,因此主程序还要调用类Snake的内容。主程序的作用主要是将该操作展现在游戏界面,真正动作执行是在Snake类。
void CMfcSnakeDlg::MvUp()//上移
{
CDC* pdc = GetDlgItem(IDC_game)->GetWindowDC();
// 将最后一个恢复背景色
CBrush* pOldBrs = pdc->SelectObject(&m_brush[3]);
pdc->Rectangle(m_map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]);
pdc->SelectObject(&pen1);
pdc->Rectangle(m_map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]);
pdc->SelectObject(pOldBrs);
my_snake.UpMove();
drawSnake();
}
void CMfcSnakeDlg::MvDown()//下移
{
CDC* pdc = GetDlgItem(IDC_game)->GetWindowDC();
CBrush* pOldBrs = pdc->SelectObject(&m_brush[3]);
pdc->Rectangle(m_map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]);
pdc->SelectObject(&pen1);
pdc->Rectangle(m_map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]);
pdc->SelectObject(pOldBrs);
my_snake.DownMove();
drawSnake();
}
void CMfcSnakeDlg::MvLeft()//左移
{
CDC* pdc = GetDlgItem(IDC_game)->GetWindowDC();
CBrush* pOldBrs = pdc->SelectObject(&m_brush[3]);
pdc->Rectangle(m_map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]);
pdc->SelectObject(&pen1);
pdc->Rectangle(m_map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]);
pdc->SelectObject(pOldBrs);
my_snake.LeftMove();
drawSnake();
}
void CMfcSnakeDlg::MvRight()//右移
{
CDC* pdc = GetDlgItem(IDC_game)->GetWindowDC();
CBrush* pOldBrs = pdc->SelectObject(&m_brush[3]);
pdc->Rectangle(m_map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]);
pdc->SelectObject(&pen1);
pdc->Rectangle(m_map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]);
pdc->SelectObject(pOldBrs);
my_snake.RightMove();
drawSnake();
}
- 蛇是否死亡
bool CMfcSnakeDlg::checkLive()
{
// 判断是否出界
if (my_snake.body[0].x < 0 || my_snake.body[0].x >= 30 || my_snake.body[0].y < 0 || my_snake.body[0].y >= 38) return false;
// 判断是否撞到了自己
for (int i = 1; i < my_snake.len; i++) {
if (my_snake.body[0].x == my_snake.body[i].x && my_snake.body[0].y == my_snake.body[i].y) return false;
}
return true;
}
用户界面的功能展示:
- Stop按键
点击完Stop按键之后,游戏直接结束。
void CMfcSnakeDlg::OnBnClickedButtonStop()
{
// TODO: 在此添加控件通知处理程序代码
// 重新开始
gameOver();
SnackInit();
this->GetDlgItem(IDC_game)->SetFocus();
}
- Start按键
Start按键处除了右开始游戏的功能,还有终止游戏的功能,因此,点击该按键要判断是蛇死之后重新开始游戏。如果是游戏中点击,则会出现pause。
void CMfcSnakeDlg::OnBnClickedButtonStart()
{
// TODO: 在此添加控件通知处理程序代码
CString start, pause;
start = "start";
pause = "pause";
if (DEAD == false) {
if (START) {
START = false;//如果已经开始了,则关掉
KillTimer(1);//关掉计时器
SetDlgItemText(IDC_BUTTON_Start, start);
}
else {
if (INIT) {//判断是否为第一次
INIT = false;
SnackInit();//初始化为第一次(这个是为了界面面框的一个美观)
}
START = true;//重新开始
SetTimer(1, speed, NULL);
SetDlgItemText(IDC_BUTTON_Start, pause);
}
this->GetDlgItem(IDC_game)->SetFocus();
}
}
- 游戏结束
关闭计时器,结束游戏。
void CMfcSnakeDlg::gameOver()
{
KillTimer(1);//关闭计时器
START = false;//START不在运行
}
成果展示
本次实验实现了贪食蛇的一下功能:
- 屏幕上随机出现一个豆子, 上下左右控制 “蛇 ”的移动吃到 豆子 ”以后 “蛇 ”的身体加长一点。
- “蛇 ”碰到边界 或蛇头与蛇身相撞 蛇死亡, 游戏结束。
- 界面展示有游戏界面,游戏结束界面。有开始键、暂停键和停止退出的选项。
- 对蛇吃到豆子进行分值计算,可以设置游戏速度。
视频展示:
本次学习参考视频:添加链接描述