贪吃蛇游戏开发
一、前言
设计一个简单的贪吃蛇游戏,实现其基本功能:
- 屏幕上随机出现一个“食物”,称为豆子,上下左右控制“蛇”的移动,吃到“豆子”以后“蛇”的身体加长一点。
- “蛇”碰到边界或蛇头与蛇身相撞,蛇死亡,游戏结束。
- 为游戏设计友好的交互界面;例如欢迎界面,游戏界面,游戏结束界面。要有开始键、暂停键和停止退出的选项。
- 对蛇吃到豆子进行分值计算,可以设置游戏速度,游戏音乐等拓展元素。
二、基本流程
三、游戏界面设计
1. 界面构造
这里我用到了MFC,加入了多个button控件来控制游戏的开始、停止与结束,以及贪吃蛇的上下左右移动;并加入了edit控件来显示当前等级与统计得分情况。如下图所示:
并在dlg文件中对界面的地图和单个格子的大小进行调试
#define LENGTH 10
#define SIZE_H 60
#define SIZE_V 60
在OnPaint函数中将背景颜色改为白色
CClientDC dc(this);
dc.Rectangle(0, 0, LENGTH * SIZE_H, LENGTH * SIZE_V);
调试程序得到界面
完成了游戏主页面才发现,初始欢迎界面还没有设置。
首先在资源列表中添加资源,命名为CWelcomeDlg,并添加一个对应的类;增加button控件跳转进入正式游戏界面。此时发现调试程序只能出现主界面无法出现欢迎界面;
在网上查找资料后,进入主界面的无Dlg后缀的同名文件,将
CMFCGreedySnakeDlg dlg;
修改为刚刚新增的界面
CWelcomeDlg dlg;
并在CWelcomeDlg的Button控件中加入触发响应函数
void CWelcomeDlg::OnBnClickedWelcome()
{
CDialogEx::OnOK(); //关闭主界面
CMFCGreedySnakeDlg dlg;
dlg.DoModal();
}
如此,就实现了点击欢迎界面进入游戏主界面。
2. 蛇的构造
创建一个snake类
Snake.h
#pragma once
class Snake
{
public:
Snake();
private:
CList<CPoint> snake_body;
CPoint snake_direction;
public:
void DrawSnake();
CList<CPoint>* GetBody();
};
Snake.cpp
#include "pch.h"
#include "Snake.h"
Snake::Snake()
{
//初始长度为3,放在正中间
snake_body.AddTail(CPoint(30, 29));
snake_body.AddTail(CPoint(30, 30));
snake_body.AddTail(CPoint(30, 31));
//初始化方向向上
snake_direction.SetPoint(0, -1);
}
void Snake::DrawSnake()
{
}
CList<CPoint>* Snake::GetBody()
{
return &snake_body;
}
在Dlg文件中调用
void CMFCGreedySnakeDlg::DrawSnake()
{
CClientDC dc(this);
CBrush red(RGB(255, 0, 0));
dc.SelectObject(red);
CList<CPoint>* pBody = m_snake.GetBody();
//遍历整个蛇身,画出整条蛇
POSITION p = pBody->GetHeadPosition();
while (p)
{
CPoint point = pBody->GetNext(p);
dc.Rectangle(point.x * LENGTH, point.y * LENGTH,
(point.x + 1) * LENGTH, (point.y + 1) * LENGTH);
}
}
并将开始游戏的Button绑定创建蛇身
void CMFCGreedySnakeDlg::OnBnClickedStart()
{
DrawSnake();
}
此时,进入主界面点击开始游戏,则出现以下页面
3. 食物构造
在Dlg中加入setBean函数。考虑到随机生成的食物不能与蛇身重合,还要加入一层循环检测。
在Snake类中新增函数
bool Snake::IsInBody(CPoint point) {
POSITION p = snake_body.GetHeadPosition();
while (p)
{
if (point == snake_body.GetNext(p))
return true;
}
return false;
}
用以判断豆子是否落在蛇身上
void CMFCGreedySnakeDlg::setBean()
{
srand(time(NULL)); //初始化rand函数
m_ptFood.x = rand() % 58 + 1;//随机产生x坐标
m_ptFood.y = rand() % 58 + 1;
while (m_snake.IsInBody(m_ptFood)) {
m_ptFood.x = rand() % 58 + 1;//随机产生x坐标
m_ptFood.y = rand() % 58 + 1;
}
CClientDC dc(this);
CBrush blue(RGB(0, 0, 255));
dc.SelectObject(blue);
dc.Rectangle(m_ptFood.x * LENGTH, m_ptFood.y * LENGTH,
(m_ptFood.x + 1) * LENGTH, (m_ptFood.y + 1) * LENGTH);
}
并通过“开始游戏”触发响应
void CMFCGreedySnakeDlg::OnBnClickedStart()
{
DrawSnake();
setBean();
}
调试游戏界面,开始游戏后
四、游戏过程
1. 蛇的移动
在类向导中添加定时器消息的响应函数WM_TIMER,并在Dlg中新建StartMove函数,添加在OnTimer函数中,让蛇每隔1000毫秒运动一次,并添加在“开始游戏”控件中。
void CMFCGreedySnakeDlg::StartMove()
{
setBean();
SetTimer(1, 1000, NULL);
}
顺便增加了游戏暂停的控件触发
void CMFCGreedySnakeDlg::OnBnClickedStop()
{
KillTimer(1);
}
为Button控件的上下左右设置触发
void CMFCGreedySnakeDlg::OnBnClickedUp()
{
// 上
m_snake.DirectionChange(CPoint(0, -1));
}
void CMFCGreedySnakeDlg::OnBnClickedDown()
{
// 下
m_snake.DirectionChange(CPoint(0, 1));
}
void CMFCGreedySnakeDlg::OnBnClickedLeft()
{
// 左
m_snake.DirectionChange(CPoint(-1, 0));
}
void CMFCGreedySnakeDlg::OnBnClickedRight()
{
// 右
m_snake.DirectionChange(CPoint(1, 0));
}
2. 蛇吃食物与碰撞检测
使用链表来表示蛇的移动,这样只需要改变蛇头与蛇尾的位置,更加简便。
bool Snake::MoveSnake(CPoint food)
{
CPoint newHead = snake_body.GetHead() + snake_direction;//新的蛇头位置
if (IsInBody(newHead))//撞到自己
return false;
else if (newHead.x < 0 || newHead.x>60 || newHead.y < 0 || newHead.y>60)
return false;
if (newHead != food) //没吃到豆子
{
snake_body.RemoveTail();//删掉蛇尾
}
snake_body.AddHead(newHead);//新的蛇头在最前面
return true;
}
在Dlg中加入SnakeMove函数
void CMFCGreedySnakeDlg::SnakeMove()
{
CClientDC dc(this);
CPoint tail = m_snake.GetBody()->GetTail();//获取之前的蛇尾
if (m_snake.Move(m_ptFood))//移动成功
{
CPoint head = m_snake.GetBody()->GetHead();//获取新的蛇头
CBrush red(RGB(255, 0, 0));
CBrush* old = dc.SelectObject(&red);
dc.Rectangle(head.x * LENGTH, head.y * LENGTH,
(head.x + 1) * LENGTH, (head.y + 1) * LENGTH);
dc.SelectObject(old);
if (head == m_ptFood)//如果吃到豆子,则产生新豆子
{
setBean();
}
else//否则删去蛇尾
{
CPen white(PS_SOLID, 1, RGB(255, 255, 255));
//CBrush white(RGB(255, 255, 255));
dc.SelectObject(white);
dc.Rectangle(tail.x * LENGTH, tail.y * LENGTH,
(tail.x + 1) * LENGTH, (tail.y + 1) * LENGTH);
}
}
}
五、游戏结束
1. 结束界面
如果游戏途中蛇头碰到蛇身,或者蛇头碰到边框,则会出现以下界面:
点击退出游戏窗口,则提示游戏结束,并自动退出程序。
void CMFCGreedySnakeDlg::OnBnClickedExit()
{
KillTimer(1);
MessageBox(_T("游戏结束!"));
EndDialog(0);
}
六、扩展功能
1. 移动速度
在键盘输入数字,点击等级选择,就可改变移动速度
void CMFCGreedySnakeDlg::OnBnClickedLevel()
{
CString str;
m_level.GetWindowText(str);
int num;
num = _ttoi(str);
switch (num)
{
case 1:
SetTimer(1, 1000, NULL);
break;
case 2:
SetTimer(1, 900, NULL);
break;
case 3:
SetTimer(1, 800, NULL);
break;
case 4:
SetTimer(1, 700, NULL);
break;
case 5:
SetTimer(1, 600, NULL);
break;
case 6:
SetTimer(1, 500, NULL);
break;
case 7:
SetTimer(1, 400, NULL);
break;
case 8:
SetTimer(1, 300, NULL);
break;
case 9:
SetTimer(1, 200, NULL);
break;
default:
SetTimer(1, 1000, NULL);
m_level.SetWindowText(_T("默认1"));
break;
}
}
2. 得分记录
首先为得分的edit框绑定一个mScore参数,随后在函数中加入参数的屏幕显示即可。并且在Dlg中新增eat_bean的变量记录吃到豆子的个数。
CString b;
if (head == m_ptFood)//如果吃到豆子,则产生新豆子
{
setBean();
eat_bean++;
b.Format(_T("% d"), eat_bean);
mScore.SetWindowText(b);
}
此时吃到四颗豆子之后如图