计算机软件技术实习日志项目二(二) 贪吃蛇项目实现
文章目录
前言
大家好,我将为大家介绍我实现贪吃蛇的具体细节,做软件是我之前没有接触过的领域,做得不好,大家不要见怪。本文章的思路是按照控件,参数定义,画图以及其他控制的思路讲解的。本文着重介绍项目实现,原理知识请参考《计算机软件技术实习日志项目二(一) 贪吃蛇项目准备》
一、控件
1.控件插入
我们需要两个按钮一个用来开始/暂停,一个用来重启游戏,我们还需要两个文字框用来表示速度选择和分数然后我们需要一个combox装速度,一个edit框动态显示分数,一个check框用来勾选音乐,最后需要一个picture control显示地图。
2.控件实现
2.1开始键
void CsnakeDlg::OnBnClickedstart()
{
// TODO: 在此添加控件通知处理程序代码
CString start, pause;
start = "start"; //游戏暂停时显示
pause = "pause"; //游戏开始时显示
if (is_dead == false) { //没有死才进入判断
if (is_start) { //游戏开始的话,需要暂停
is_start = false; //游戏暂停
KillTimer(1); //关闭定时器
SetDlgItemText(button_start, start); //开始键设置成“start”
}
else { //说明游戏暂停了
is_start = true; //游戏设置为开始
SetTimer(1, speed, NULL); //开启定时器
SetDlgItemText(button_start, pause); //开始键显示“pause”
}
this->GetDlgItem(game_area)->SetFocus(); //焦点设置到游戏地图
}
}
2.2重启键
void CsnakeDlg::OnBnClickedrestart()
{
// TODO: 在此添加控件通知处理程序代码
gameover();
snakeinit();
this->GetDlgItem(game_area)->SetFocus();
}
void CsnakeDlg::gameover() {
KillTimer(1);
is_start = false;
}
2.3速度
速度控件对应函数
void CsnakeDlg::OnCbnSelchangespeed()
{
// TODO: 在此添加控件通知处理程序代码
CString mode;
box.GetLBText(box.GetCurSel(), mode); //读取选中的速度等级
int tmp = 0;
tmp = mode[0] - '0';
speed = 300 / tmp; //设置周期
SetTimer(1, speed, NULL); //设置定时每一个周期发出一个WM_TIMER的信息
this->GetDlgItem(game_area)->SetFocus(); //设置焦点
}
WM_TIMER响应函数
void CsnakeDlg::OnTimer(UINT_PTR nIDEvent) //WM——TIMER消息的相应函数
{
// TODO: 在此添加消息处理程序代码和/或调用默认值
UpdateData(true);
if (is_start && is_dead == false) { //如果游戏开始且没死进入判断
if (is_live() == false) { //如果你死了
is_dead = true; //设置死亡
CString str; //设置结束窗口语句
str.Format(_T("You're died and your score is %d ~ \nIf you want to try again, please click ok and click start ~ "), score);
if (MessageBox(str, TEXT("INFO"), MB_ICONINFORMATION) == IDOK) {
OnBnClickedrestart(); //点击确定就重启
}
gameover();
}
switch (my_snake.direc) { //每到一个周期就根据贪吃蛇的方向移动
case 1:up(); break;
case 2:down(); break;
case 3:left(); break;
case 4:right(); break;
default:break;
}
eatbean(); //判断是否吃豆子了
}
CDialogEx::OnTimer(nIDEvent);
}
设置豆子
bool CsnakeDlg::setbean() {
srand((unsigned)time(NULL));
bool setB = false; //是否设置了
bool safe = true; //豆子位置是否合理
while (safe) {
pos.x = rand() % 30;
pos.y = rand() % 38;
for (int i = 0; i < my_snake.len; ++i) { //判断是否在蛇的身子上
if (pos.x == my_snake.body[i].x && pos.y == my_snake.body[i].y) {
safe = false;
break;
}
}
if (safe) { //如果合理,就设置豆子,然后退出
setB = true;
safe = false;
}
else { //否则接着生成
safe = true;
}
}
CDC* pdc = GetDlgItem(game_area)->GetWindowDC();//画豆子
pdc->SelectObject(&pen1);
pdc->SelectObject(&game_brush[3]);
pdc->Rectangle(map[pos.x][pos.y]);
return setB;
}
吃豆子
void CsnakeDlg::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);
change_score.SetWindowTextW(str); //更新分数
}
}
判断死活
bool CsnakeDlg::is_live() {
if (my_snake.body[0].x < 0 || my_snake.body[0].x > 29 || my_snake.body[0].y < 0 || my_snake.body[0].y > 37) 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;
}
2.4音乐
void CsnakeDlg::OnBnClickedmusic()
{
// TODO: 在此添加控件通知处理程序代码
int state = ((CButton*)GetDlgItem(check_music))->GetCheck(); //检查打钩情况
if (state == 0) { //如果没打勾,停止播放
PlaySound(NULL, NULL, NULL);
}
else { //如果打钩,播放
PlaySound(LPWSTR(love_me_right), GetModuleHandle(NULL), SND_ASYNC | SND_LOOP);
}
}
二、参数定义
主要罗列了一些我自己定义的参数。
部分函数在前面由相关的控件引出讲过了
1.对话框头文件:
CEdit change_score;
CComboBox box; //控制速度
CBrush game_brush[5]; //画刷
CRect map[30][38]; //地图
CPen pen1; //画笔
CFont showfont1; //字体1
CFont showfont2; //字体2
CFont showfont3; //字体3
Point pos = { 0,0 }; //豆子
gamesnake my_snake; //蛇
int speed = 150; //初始速度
int score = 0; //分数
bool is_start = false; //游戏开始状态
bool is_dead = false; //游戏结束状态
bool setbean(); //放豆子
void snakeinit(); //初始蛇身
void drawsnake(); //画
void up();
void down();
void left();
void right();
void eatbean(); //吃
bool is_live(); //判断是否生存
void gameover(); //游戏结束
首先在对话框里写了四个移动函数
以up为例:
oid CsnakeDlg::up() {
CDC* pdc = GetDlgItem(game_area)->GetWindowDC();
pdc->SelectObject(&pen1);
pdc->SelectObject(&game_brush[4]);
pdc->Rectangle(map[my_snake.body[my_snake.len - 1].x][my_snake.body[my_snake.len - 1].y]); //蛇尾画为背景
my_snake.up(); //蛇向上移动
drawsnake(); //画蛇
}
自定义了一个蛇类
自定义头文件:
struct Point { //点类
int x;
int y;
};
class gamesnake { //蛇类
public:
gamesnake();
~gamesnake();
int len;
Point body[1255];
int direc;
void up();
void down();
void left();
void right();
void init();
};
蛇的移动
上:
void gamesnake::up(){
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; //方向设为1
}
蛇的初始化:
void gamesnake::init(){
srand((unsigned)time(NULL));
body[0].x = rand()%10+10;
body[0].y = rand()%10+10;
len = 1;
direc = rand()%4+1;
}
三、画图
1.初始化
首先我配置画图用到的对象
void CsnakeDlg::OnPaint() {
CRect re;
(this->GetDlgItem(game_area))->GetWindowRect(&re); //获取相对屏幕的大小
ScreenToClient(re); //相对屏幕转换到相对客户区
GetDlgItem(game_area)->MoveWindow(re.left, re.top , 760, 600, false); //设置客户区大小
CDC* clientdc = GetDC(); //设备环境
(this->GetDlgItem(game_area))->GetWindowRect(&re);
ScreenToClient(re);
clientdc->Rectangle(re);
re.left -= 5; //扩一个框
re.right += 5;
re.top -= 5;
re.bottom += 5;
clientdc->SelectObject(&pen1); //选中画笔
clientdc->Rectangle(re); //画
clientdc->DeleteDC();
CBitmap bodybmp, headbmp, beanbmp, bgbmp;
bgbmp.LoadBitmapW(bmp_bg);
beanbmp.LoadBitmapW(bmp_bean);
bodybmp.LoadBitmapW(bmp_body);
headbmp.LoadBitmapW(bmp_head);
game_brush[1].CreatePatternBrush(&bodybmp); //身子,用位图创建位图画刷
game_brush[2].CreatePatternBrush(&headbmp); //头
game_brush[3].CreatePatternBrush(&beanbmp); //豆子
game_brush[4].CreatePatternBrush(&bgbmp); //背景
box.SetCurSel(1);
change_score.SetReadOnly(1);
showfont1.CreatePointFont(200,L"黑体"); //设置字体格式
GetDlgItem(edit_score)->SetFont(&showfont1); //设置控件字体
GetDlgItem(button_start)->SetFont(&showfont1);
GetDlgItem(button_restart)->SetFont(&showfont1);
showfont2.CreatePointFont(150, L"Consolas");
GetDlgItem(word_score)->SetFont(&showfont2);
GetDlgItem(word_speed)->SetFont(&showfont2);
showfont3.CreatePointFont(100, L"Consolas");
((CButton*)GetDlgItem(check_music))->SetFont(&showfont3);
((CButton*)GetDlgItem(check_music))->SetCheck(BST_CHECKED); //把按钮设置为选中状态
PlaySound(LPWSTR(love_me_right), GetModuleHandle(NULL), SND_RESOURCE | SND_ASYNC | SND_LOOP);
//设置播放模式,循环播放
snakeinit();
CPaintDC pDC(this);
}
2.作画
void CsnakeDlg::snakeinit() {
CDC* pdc = GetDlgItem(game_area)->GetWindowDC(); //生成设备环境
CPen* p01dPen = pdc->SelectObject(&pen1); //设置画笔花边框
CBrush* p01dbrs = pdc->SelectObject(&game_brush[4]); //设置背景位图画地图块内部,
for (int i = 0; i < 30; ++i) { //地图设置成地图块组成的
for (int j = 0; j < 38; ++j) { //大小设置成20*20
map[i][j].left = 0 + j * 20;
map[i][j].right = 20 + j * 20;
map[i][j].top = 0 + i * 20;
map[i][j].bottom = 20 + i * 20;
pdc->Rectangle(map[i][j]);
}
}
my_snake.init(); //初始蛇
score = 0; //参数初始化
is_start = 0;
is_dead = false;
drawsnake(); //画蛇
setbean(); //画豆子
change_score.SetWindowTextW(_T("0")); //分数显示
SetDlgItemText(button_start, _T("start"));
}
void CsnakeDlg::drawsnake() {
CDC* pdc = GetDlgItem(game_area)->GetWindowDC();
CBrush* poldbrs = pdc->SelectObject(&game_brush[1]);
for (int i = 1; i < my_snake.len; ++i) { //画蛇身
pdc->SelectObject(game_brush[1]);
pdc->SelectObject(&pen1);
pdc->Rectangle(map[my_snake.body[i].x][my_snake.body[i].y]);
}
pdc->SelectObject(&game_brush[2]); //花蛇头
pdc->SelectObject(&pen1);
pdc->Rectangle(map[my_snake.body[0].x][my_snake.body[0].y]);
pdc->DeleteDC();
}
四、其他控制
键盘控制:
BOOL CsnakeDlg::PreTranslateMessage(MSG* pMsg)
{
// TODO: 在此添加专用代码和/或调用基类
if (is_start && is_dead == false) { //pMsg->wParam表示按的键
if (my_snake.direc != 2 && (pMsg->wParam == VK_UP ) )my_snake.direc = 1; //上
else if (my_snake.direc != 1 && (pMsg->wParam == VK_DOWN )) my_snake.direc = 2; //下
else if (my_snake.direc != 4 && (pMsg->wParam == VK_LEFT )) my_snake.direc = 3; //左
else if (my_snake.direc != 3 && (pMsg->wParam == VK_RIGHT )) my_snake.direc = 4; //右
}
//return CDialogEx::PreTranslateMessage(pMsg);
return false;
}
窗口总保持在屏幕内:
void CsnakeDlg::OnWindowPosChanging(WINDOWPOS* lpwndpos)
{
CDialogEx::OnWindowPosChanging(lpwndpos);
// TODO: 在此处添加消息处理程序代码
//获取屏幕宽度
int wth = GetSystemMetrics(SM_CXSCREEN);
// 获取屏幕高度
int hgih = GetSystemMetrics(SM_CYSCREEN);
// 判断窗口X坐标有无超过左边桌面。
if (lpwndpos->x < 0)
{
lpwndpos->x = 0;
}
// 判断窗口X坐标有无超过右边桌面。
if (lpwndpos->x + lpwndpos->cx > wth)
{
lpwndpos->x = wth - lpwndpos->cx;
}
// 判断窗口Y坐标有无超过顶部桌面。
if (lpwndpos->y < 0)
{
lpwndpos->y = 0;
}
// 判断窗口Y坐标有无超过底部桌面。
if (lpwndpos->y + lpwndpos->cy > hgih)
{
lpwndpos->y = hgih - lpwndpos->cy;
}
}
五、AI贪吃蛇
我是现在项目三里学习的A星,回过来写的AI贪吃蛇,AI贪吃蛇是A星寻路的,所以大家参考项目三的文章,这里直贴代码了
//定时器响应函数里将switch语句换为下面的句子
//请将fax,fay理解为要去的下一个结点,不会父节点
int nowx = my_snake.body[0].x, nowy = my_snake.body[0].y;
int fax = ma[nowx][nowy].fax, fay = ma[nowx][nowy].fay;
if (fax == nowx - 1 && fay == nowy) up();
if (fax == nowx + 1 && fay == nowy) down();
if (fax == nowx && fay == nowy-1) left();
if (fax == nowx && fay == nowy+1) right();
void CsnakeDlg::astar() {
/*
和迷宫的A星基本相同不做具体讲解。
该函数在setbean里调用,然后通过深搜找路径
这里是从豆子找到蛇,通过fa回溯,比正着来方便
*/
init();
int tx, ty,ttx,tty;
node tmp;
int ex = my_snake.body[0].x,ey=my_snake.body[0].y ;
int sx = pos.x, sy = pos.y;
ma[sx][sy].x = sx, ma[sx][sy].y = sy;
ma[sx][sy].in_open = 1;
open_queue.push(ma[sx][sy]);
while (!ma[ex][ey].in_open) {
tmp = open_queue.top();
tx = tmp.x, ty = tmp.y;
ma[tx][ty].in_close = 1;
open_queue.pop();
for (int i = 1; i <= 4; ++i) {
ttx = tx + dr[i][0], tty = ty + dr[i][1];
if (!(ttx >= 0 && ttx <30 && tty >= 0 && tty <38)) continue;
bool ok = 1;
ok = !vis[ttx][tty]; //vis在蛇身移动时,蛇身的地图块会赋为1
if (ok && !ma[ttx][tty].in_close) {
if (!ma[ttx][tty].in_open || ma[ttx][tty].g > ma[tx][ty].g + 1) {
ma[ttx][tty].x = ttx, ma[ttx][tty].y = tty;
ma[ttx][tty].g = ma[tx][ty].g + 1;
ma[ttx][tty].h = abs(ttx - ex) + abs(tty - ey);
ma[ttx][tty].f = ma[ttx][tty].g + ma[ttx][tty].h;
ma[ttx][tty].in_open = 1;
ma[ttx][tty].fax = tx, ma[ttx][tty].fay = ty;
open_queue.push(ma[ttx][tty]);
}
}
}
}
while (!open_queue.empty()) {
open_queue.pop();
}
}
六、演示
开始暂停重启调速死亡提示演示
AI贪吃蛇(重调了一下背景,还没设置手动模式和自动模式按键)
七、总结
这一次通过学习贪吃蛇我学习了一些关于Windows窗口编程的深层原理,对我理解运MFC有很大的帮助。贪吃蛇的界面设置的很low,还可以再丰富游戏性。
八、参考资料
【1】 B站视频 MFC 贪吃蛇小程序(从创建项目开始)
https://www.bilibili.com/video/BV1Q741187fo?from=search&seid=9242306747617284419