文章目录
- 前言
- 一、工具准备
- 1.1游戏开发框架
- 1.2visual studio2022下载
- 1.3easyX下载
- 1.4图片素材
- 二、逻辑分析
- 2.1数据结构
- 2.2蛇的移动
- 2.3吃食物
- 2.4游戏失败
- 三、DEMO代码实现
- 四、完整源代码
- 总结
🐱🚀个人博客https://blog.csdn.net/qq_51000584?type=blog
🐱👤收录专栏:C++游戏开发探索
🐱👓专栏目标:通过所学知识和自己对游戏的理解去开发一系列游戏提高自己的编码和逻辑能力
🐱💻作者:敲代码的猫(Codemon)
前言
上次我们通过qt工具实现了推箱子的图形化编程http://t.csdnimg.cn/08AY9,本节我们将使用Visual Studio2022和easyX图形库来实现贪吃蛇的demo。成品如下:
一、工具准备
1.1游戏开发框架
代码链接:http://t.csdnimg.cn/iAvkm
经过对推箱子的开发,相信大家对游戏开发的流程已经有了一定的理解,那么为了方便我将游戏demo开发做成了框架,我们直接在这基础上实现即可。
1.2visual studio2022下载
转载博客:VS2022的下载和使用(作者:羽舟_)
1.3easyX下载
easyX官网
下载后直接运行exe文件,单击下一步,然后选择vs2022安装即可
1.4图片素材
链接:https://pan.baidu.com/s/1pOJxEfaePlcN-lVAJ0mFRA
提取码:1025
下载并解压到本地即可
二、逻辑分析
在逻辑分析前,同样我们先来了解一下贪吃蛇的游戏规则,
游戏规则:
贪吃蛇游戏的基本规则如下:
玩家通过控制蛇的移动来寻找并吃掉地图上的食物,每次吃掉食物后,蛇的身体会相应加长。
蛇的移动方向由玩家通过键盘上的上下左右键来控制。
在游戏过程中,蛇不能碰到墙壁或者自己的身体
如果蛇在移动过程中撞到墙壁或者咬到自己的身体,游戏会立即结束。
2.1数据结构
根据游戏规则我们会发现,蛇的身体分为一节一节的,并且连续,这会让我们联想到相关的数据结构——链表。如果我们能维护一个链表,那么它的头节点就是蛇的头,每当头节点移动,全身的节点都移动到上一个节点的位置,这样就实现了蛇的移动。
为了方便蛇身之间可以找到相邻的节点,蛇身长度增加,我们实现一个双向链表的创建、尾添加、删除功能。
//双向链表节点
struct Node {
struct Node* pNext;
struct Node* pPre;
Point pos;
Node() {//节点初始化
pNext = nullptr;//下一个节点
pPre = nullptr;//上一个节点
}
};
//双向链表
struct List {
struct Node* pHead;
struct Node* pTail;
int length;
List() {
pHead = nullptr;//头节点
pTail = nullptr;//尾节点
length = 0;//链表长度
}
};
//创建双向链表
void CreateList(List** L) {
(*L) = new List;
}
//双向链表尾添加节点
void AddNode(List* L, Point p) {
if (L->pHead == nullptr) {//空链表
L->pHead = new Node;
L->pHead->pos = p;
L->pHead->pNext = L->pHead;
L->pTail = L->pHead;
L->length++;
}
else {//尾添加
Node* temp = new Node;
temp->pos = p;
L->pTail->pNext = temp;
temp->pPre = L->pTail;
L->pTail = temp;
L->length++;
}
}
//链表资源释放
void DeleteList(List* L) {
Node* pNode = L->pHead;
while (pNode != nullptr) {
Node* temp = pNode;
pNode = pNode->pNext;
delete temp;
temp = nullptr;
}
delete L;
L = nullptr;
}
2.2蛇的移动
蛇的移动和推箱子比起来就容易了很多,我们不需要再去判断复杂的地形,只需要保证蛇头没有碰到边界、没有咬到身体、对吃到食物进行处理即可。
蛇的移动是由程序自动控制的,玩家通过键盘的输入控制的只是改变了蛇的方向,因此我们定义一个全局变量和相应的宏
#define UP 0
#define LEFT 1
#define DOWN 2
#define RIGHT 3
int direct;
当我们按下键盘后只改变direct的值,然后每次刷新根据direct决定蛇向哪个方向移动。
2.3吃食物
我们假设蛇吃的是苹果,那么苹果每次出现在地图上的位置则是随机选定的,我们需要加入随机数种子的初始化。当蛇移动后的位置和苹果重合时,我们要更新苹果的位置,并且为蛇进行尾部添加节点的操作。
2.4游戏失败
当蛇移动后的位置超过了我们的地图边界即碰到了墙壁,此时失败。或者蛇移动后的位置是自己的身体时代表蛇咬到了自己,此时也会失败。我们可以在全局加入一个布尔值:
bool m_isQuit;
由m_isQuit的值来决定游戏的运行,当游戏 初始化时m_isQuit为true,当游戏失败条件触发时,将m_isQuit更改为false,这样就实现了游戏的失败退出功能。
综上来看贪吃蛇的逻辑要比推箱子容易很多。
三、DEMO代码实现
在游戏框架的代码基础上,首先我们要在GreedySnake类中添加我们需要使用的游戏相关全局变量:
bool m_isQuit;//游戏退出标识
List* snake;//存储蛇的双向链表
int direct;//记录当前方向
//屏幕大小
int screenWidth;
int screenHeight;
//苹果坐标
int appleX;
int appleY;
//格子大小
int base;
//蛇头坐标
Point m_point;
定义IMAGE对象来存储我们需要绘制的图,该类是easyX图形库中的成员,可以帮我们很方便的绘制图片。因此要加入头文件
#include <easyx.h>
IMAGE map;
IMAGE head_up;
IMAGE head_down;
IMAGE head_left;
IMAGE head_right;
IMAGE apple;
IMAGE body;
右键我们的vs项目,选择在文件资源管理器中打开文件夹
将我们的图片素材文件夹拖入到此处
进入该文件夹,点击map.bmp图片的属性
点击详细信息
可以看到该图片的大小为600*600像素,也就是说一格的大小为30*30,而蛇可移动的范围只有18*18格。我们设置窗口大小就为600*600
在构造函数中为成员变量初始化:
public:
GreedySnake() {
m_isQuit = true;//游戏运行
//窗口大小
screenWidth = 600;
screenHeight = 600;
srand((unsigned)time(NULL));//初始化随机数种子
OnInit();//游戏初始化
}
由于调用了time函数作为随机数种子,所以我们要加入头文件
#include <ctime>
对游戏进行初始化
void OnInit() {
direct = UP;//初始化蛇的方向为向上
base = 30;//格子大小30*30
//随机初始化苹果的位置
appleX = rand() % 18 + 1;
appleY = rand() % 18 + 1;
//初始化蛇
m_point.setPoint(9, 9);//蛇头坐标初始化
CreateList(&snake);//蛇双向链表初始化
//-------------------------------------------
Point temp;//定义坐标变量
temp.setPoint(9, 9);
AddNode(snake, temp);
temp.setPoint(9, 10);
AddNode(snake, temp);
temp.setPoint(9, 11);
AddNode(snake, temp);
//-------------------------初始化三个节点长的蛇
//加载图片(注意图片的路径)
loadimage(&map, L".\\image\\map.bmp");
loadimage(&head_right, L".\\image\\head0.bmp");//R
loadimage(&head_up, L".\\image\\head1.bmp");//U
loadimage(&head_left, L".\\image\\head2.bmp");//L
loadimage(&head_down, L".\\image\\head3.bmp");//D
loadimage(&apple, L".\\image\\apple.bmp");
loadimage(&body, L".\\image\\body.bmp");
initgraph(screenWidth, screenHeight);//初始化窗口大小
}
下面是游戏运行函数
void OnRun() {
while (m_isQuit) {//m_isQuit控制游戏的运行
if (_kbhit())//处理键盘输入-----------需要头文件conio.h
{
int k = _getch();//获取键盘输入
//需要头文件conio.h
switch (k)
{
case 'A':
case 'a':
case 75://左
direct = LEFT;
break;
case 'S':
case 's':
case 80://下
direct = DOWN;
break;
case 'D':
case 'd':
case 77://右
direct = RIGHT;
break;
case 'W':
case 'w':
case 72://上
direct = UP;
break;
}
}//------------------------------------
SnakeMove();//蛇的移动
if (!m_isQuit) {
break;
}
OnDraw();//绘制图片
Sleep(200);//控制刷新率,不易太快或太慢,以毫秒为单位
}
}
蛇的移动
void SnakeMove() {
switch (direct)//根据当前方向决定蛇头的移动
{
case UP:
m_point.y--;
break;
case DOWN:
m_point.y++;
break;
case LEFT:
m_point.x--;
break;
case RIGHT:
m_point.x++;
break;
}
if (m_point.x < 1 || m_point.x>18 || m_point.y < 1 || m_point.y>18) {
//移动后出界(碰墙)
m_isQuit = false;
return;
}
if (appleX == m_point.x && appleY == m_point.y) {
//和苹果坐标重叠(吃到苹果)
AddNode(snake, snake->pTail->pos);//给蛇添加节点
//更新苹果坐标
appleX = rand() % 18 + 1;
appleY = rand() % 18 + 1;
}
//从蛇尾向蛇头遍历,每一个节点移动到前一个节点的位置(思靠一下为什么从蛇尾向蛇头遍历)
Node* pNode = snake->pTail;
while (pNode->pPre != NULL) {
pNode->pos.x = pNode->pPre->pos.x;
pNode->pos.y = pNode->pPre->pos.y;
pNode = pNode->pPre;
}
snake->pHead->pos = m_point;
//从蛇头向蛇尾遍历检测是否咬到了蛇身
pNode = snake->pHead->pNext;
while (pNode != NULL) {
if (pNode->pos.x == m_point.x && pNode->pos.y == m_point.y) {
m_isQuit = false;
return;
}
pNode = pNode->pNext;
}
}
图片绘制
BeginBatchDraw()函数和EndBatchDraw()函数用于打开和关闭缓冲区,也是easyX中的函数,要配对使用,我们将绘制图片的函数放在他们中间即可。
void OnDraw() {
::BeginBatchDraw();//打开缓冲区开始绘制
putimage(0, 0, &map);//绘制背景从(0,0)开始
putimage(appleX * base, appleY * base, &apple);//绘制苹果
Node* pNode = snake->pHead->pNext;
//绘制蛇头
switch (direct)
{
case UP:
putimage(m_point.x * base, m_point.y * base, &head_up);
break;
case DOWN:
putimage(m_point.x * base, m_point.y * base, &head_down);
break;
case LEFT:
putimage(m_point.x * base, m_point.y * base, &head_left);
break;
case RIGHT:
putimage(m_point.x * base, m_point.y * base, &head_right);
break;
}
//绘制蛇身
while (pNode != NULL) {
putimage(pNode->pos.x * base, pNode->pos.y * base, &body);
pNode = pNode->pNext;
}
::EndBatchDraw();//关闭缓冲区结束绘制
}
游戏关闭:
由于我们使用到了链表,并且申请了堆的空间,因此在程序结束时需要对这一部分的资源进行释放。
先前在链表实现的地方已经写好了相应的函数,这里直接调用即可。
void OnClose() {
DeleteList(snake);
}
运行后就是下面的样子啦
四、完整源代码
#include <iostream>
#include <easyx.h>
#include <conio.h>
#include <ctime>
using namespace std;
//坐标结构体
struct Point {
int x;
int y;
void setPoint(int px, int py) {
this->x = px;
this->y = py;
}
};
//游戏框架类
class CGameFrame {
public:
CGameFrame() {
}
virtual ~CGameFrame() {
}
virtual void OnInit() = 0;//初始化
virtual void OnRun() = 0;//游戏运行
virtual void OnDraw() = 0;//绘制
virtual void OnClose() = 0;//游戏关闭
};
//双向链表节点
struct Node {
struct Node* pNext;
struct Node* pPre;
Point pos;
Node() {
pNext = nullptr;
pPre = nullptr;
}
};
//双向链表
struct List {
struct Node* pHead;
struct Node* pTail;
int length;
List() {
pHead = nullptr;
pTail = nullptr;
length = 0;
}
};
//创建双向链表
void CreateList(List** L) {
*L = new List;
(*L)->length = 0;
}
//双向链表尾添加节点
void AddNode(List* L, Point p) {
if (L->pHead == nullptr) {
L->pHead = new Node;
L->pHead->pos = p;
L->pHead->pNext = L->pHead;
L->pTail = L->pHead;
L->length++;
}
else {
Node* temp = new Node;
temp->pos = p;
L->pTail->pNext = temp;
temp->pPre = L->pTail;
L->pTail = temp;
L->length++;
}
}
//链表资源释放
void DeleteList(List* L) {
Node* pNode = L->pHead;
while (pNode != nullptr) {
Node* temp = pNode;
pNode = pNode->pNext;
delete temp;
temp = nullptr;
}
delete L;
L = nullptr;
}
#define UP 0
#define LEFT 1
#define DOWN 2
#define RIGHT 3
//贪吃蛇类
class GreedySnake :public CGameFrame
{
private:
bool m_isQuit;//游戏退出标识
List* snake;//存储蛇
int direct;//记录当前方向
IMAGE map;
IMAGE head_up;
IMAGE head_down;
IMAGE head_left;
IMAGE head_right;
IMAGE apple;
IMAGE body;
//屏幕大小
int screenWidth;
int screenHeight;
//苹果坐标
int appleX;
int appleY;
//格子大小
int base;
//蛇头坐标
Point m_point;
public:
GreedySnake() {
m_isQuit = true;
screenWidth = 600;
screenHeight = 600;
srand((unsigned)time(NULL));
OnInit();
}
~GreedySnake() {
}
void OnInit() {
direct = UP;
base = 30;
//初始化苹果
appleX = rand() % 18 + 1;
appleY = rand() % 18 + 1;
//初始化蛇
m_point.setPoint(9, 9);
CreateList(&snake);//蛇初始化
Point temp;
temp.setPoint(9, 9);
AddNode(snake, temp);
temp.setPoint(9, 10);
AddNode(snake, temp);
temp.setPoint(9, 11);
AddNode(snake, temp);
//加载图片
loadimage(&map, L".\\image\\map.bmp");
loadimage(&head_right, L".\\image\\head0.bmp");//R
loadimage(&head_up, L".\\image\\head1.bmp");//U
loadimage(&head_left, L".\\image\\head2.bmp");//L
loadimage(&head_down, L".\\image\\head3.bmp");//D
loadimage(&apple, L".\\image\\apple.bmp");
loadimage(&body, L".\\image\\body.bmp");
initgraph(screenWidth, screenHeight);
}
void OnRun() {
while (m_isQuit) {
if (_kbhit())//处理键盘输入-----------
{
int k = _getch();
switch (k)
{
case 'A':
case 'a':
case 75://左
direct = LEFT;
break;
case 'S':
case 's':
case 80://下
direct = DOWN;
break;
case 'D':
case 'd':
case 77://右
direct = RIGHT;
break;
case 'W':
case 'w':
case 72://上
direct = UP;
break;
}
}//------------------------------------
SnakeMove();
if (!m_isQuit) {
break;
}
OnDraw();
Sleep(200);
}
}
void OnDraw() {
::BeginBatchDraw();
putimage(0, 0, &map);
putimage(appleX * base, appleY * base, &apple);
Node* pNode = snake->pHead->pNext;
//绘制蛇头
switch (direct)
{
case UP:
putimage(m_point.x * base, m_point.y * base, &head_up);
break;
case DOWN:
putimage(m_point.x * base, m_point.y * base, &head_down);
break;
case LEFT:
putimage(m_point.x * base, m_point.y * base, &head_left);
break;
case RIGHT:
putimage(m_point.x * base, m_point.y * base, &head_right);
break;
}
//绘制蛇身
while (pNode != NULL) {
putimage(pNode->pos.x * base, pNode->pos.y * base, &body);
pNode = pNode->pNext;
}
::EndBatchDraw();
}
void OnClose() {
DeleteList(snake);
}
void SnakeMove() {
switch (direct)
{
case UP:
m_point.y--;
break;
case DOWN:
m_point.y++;
break;
case LEFT:
m_point.x--;
break;
case RIGHT:
m_point.x++;
break;
}
if (m_point.x < 1 || m_point.x>18 || m_point.y < 1 || m_point.y>18) {
m_isQuit = false;
return;
}
if (appleX == m_point.x && appleY == m_point.y) {
AddNode(snake, snake->pTail->pos);
appleX = rand() % 18 + 1;
appleY = rand() % 18 + 1;
}
Node* pNode = snake->pTail;
while (pNode->pPre != NULL) {
pNode->pos.x = pNode->pPre->pos.x;
pNode->pos.y = pNode->pPre->pos.y;
pNode = pNode->pPre;
}
snake->pHead->pos = m_point;
pNode = snake->pHead->pNext;
while (pNode != NULL) {
if (pNode->pos.x == m_point.x && pNode->pos.y == m_point.y) {
m_isQuit = false;
return;
}
pNode = pNode->pNext;
}
}
};
int main() {
GreedySnake* pGame = new GreedySnake;
pGame->OnRun();
pGame->OnClose();
delete pGame;
pGame = nullptr;
return 0;
}
总结
在前两节推箱子中已经实现过很多细节的部分,因此本篇贪吃蛇更多的是干货。下期我将继续使用qt工具实现贪吃蛇的完整版开发,敬请期待…
Codemon2024.02.21