✅作者简介:大家好,我是橘橙黄又青,一个想要与大家共同进步的男人😉😉
🍎个人主页:再无B~U~G-CSDN博客
1. 游戏背景
贪吃蛇是久负盛名的游戏,它也和俄罗斯⽅块,扫雷等游戏位列经典游戏的⾏列。
在编程语⾔的教学中,我们以贪吃蛇为例,从设计到代码实现来提升学⽣的编程能⼒和逻辑能⼒。
2. 游戏效果演⽰
屏幕录制
3. 课程⽬标
使⽤C语⾔在Windows环境的控制台中模拟实现经典⼩游戏贪吃蛇
实现基本的功能:
•
贪吃蛇地图绘制
•
蛇吃⻝物的功能 (上、下、左、右⽅向键控制蛇的动作)
•
蛇撞墙死亡
•
蛇撞⾃⾝死亡
•
计算得分
•
蛇⾝加速、减速
•
暂停游戏
4. 课程定位
•
提⾼⼩⽐特对编程的兴趣
•
对C语⾔语法做⼀个基本的巩固。
•
对游戏开发有兴趣的同学做⼀个启发。
•
项⽬适合:C语⾔学完的同学,有⼀定的代码能⼒,初步接触数据结构中的链表。
5. 技术要点
C语⾔函数、枚举、结构体、动态
内存管理、预处理指令、链表、Win32 API等。
整体的框架:
课前准备:
调好项目适应本地化
了解EasyX的坐标体系
6. 控制台程序
6.1 控制台程序
设置如下:
6.1 相关函数的使用
地区设置为"C"时,库函数按正常⽅式执⾏,⼩数点是⼀个点。
当程序运⾏起来后想改变地区,就只能显⽰调⽤setlocale函数。⽤" "作为第2个参数,调⽤setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。⽐如:切换到我们的本地模式后就⽀持宽字符(汉字)的输出等
<locale.h>本地化
全部来自于EasyX简单图形库
6.2pause
6.3 outtextxy
两个一个输出字符,一个输出字符串
案例:
6.4cleardevice
6.5图片设置
相关用法:
6.6rectangle
6.7setlinecolor设置线条颜色
7. 游戏实现
7.1地图坐标
我们假设实现⼀个棋盘27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙,
如下:
7.2 蛇⾝和⻝物
初始化状态,假设蛇的⻓度是5,
⽐如(24, 5)处开始出现蛇,连续5个节点。注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是1的15倍数),坐标不能和蛇的⾝体重合
7.3 数据结构设计
游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信
息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,所以蛇节点结构如下:
要管理整条贪吃蛇,我们再封装⼀个Snake的结构来维护整条贪吃蛇:
蛇的⽅向,可以⼀⼀列举,使⽤枚举
游戏状态,可以⼀⼀列举,使⽤枚举
7.4 游戏流程设计
8. 核⼼逻辑实现分析
8.1 游戏主逻辑
8.2 游戏开始前面的准备
8.2.1 打印欢迎界⾯
看看效果:
8.2.2 创建地图
打印地图的关键是要算好坐标,才能在想要的位置打印墙体。
看看效果:
8.2.3 初始化蛇⾝
蛇最开始⻓度为5节,每节对应链表的⼀个节点,蛇⾝的每⼀个节点都有⾃⼰的坐标。
创建5个节点,然后将每个节点存放在链表中进⾏管理。创建完蛇⾝后,将蛇的每⼀节打印在屏幕上。
创建完蛇⾝后随便,初始化蛇的其他属性
蛇的图片是:
8.2.4 创建第⼀个⻝物
先随机⽣成⻝物的坐标
- x坐标必须是15的倍数
- ⻝物的坐标不能和蛇⾝每个节点的坐标重复
- 创建⻝物节点,打印⻝物
创建⻝物的函数:CreateFood
8.3 游戏运⾏过程
游戏运⾏期间,右侧打印帮助信息,提⽰玩家
根据游戏状态检查游戏是否继续,如果是状态是OK,游戏继续,否则游戏结束。
如果游戏继续,就是检测按键情况,确定蛇下⼀步的⽅向,或者是否加速减速,是否暂停或者退出游戏。
确定了蛇的⽅向和速度,蛇就可以移动了。
8.3.1 KEY_PRESS
检测按键状态,我们封装了⼀个宏
# define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
8.3.2 PrintHelpInfo 打印规则信息
8.3.3 蛇⾝移动
先创建下⼀个节点,根据移动⽅向和蛇头的坐标,蛇移动到下⼀个位置的坐标。
确定了下⼀个位置后,看下⼀个位置是否是⻝物(NextIsFood),是⻝物就做吃⻝物处理
(EatFood),如果不是⻝物则做前进⼀步的处理(NoFood)。
蛇⾝移动后,判断此次移动是否会造成撞墙(KillByWall)或者撞上⾃⼰蛇⾝(KillBySelf),从⽽影响游戏的状态。
8.3.3.1 NextIsFood
8.3.3.2 EatFood
8.3.3.3 NoFood
将下⼀个节点头插⼊蛇的⾝体,并将之前蛇⾝最后⼀个节点打印为空格,放弃掉蛇⾝的最后⼀个节点
8.3.3.4 KillByWall
8.3.3.5 KillBySelf
8.4 游戏结束
戏状态不再是OK(游戏继续)的时候,要告知游戏结束的原因,并且释放蛇⾝节点。
9. 参考代码
完整代码实现,分3个⽂件实现
test.cpp
#include"snake.h"
void test()
{
//修改适配本地中文环境
setlocale(LC_ALL, "");
bool c = false;
char str[4] = " ";
do
{
Snake snake = { 0 };
GameStart(&snake);//游戏开始前的初始化
GameRun(&snake);//玩游戏的过程
GameEnd(&snake);//善后的工作
outtextxy(350, 500, "再来一局吗?(Yes/No):");
scanf("%s", str);
c = Determine(str);
} while (c);
}
int main()
{
//修改适配本地中文环境
setlocale(LC_ALL, "");
test();
return 0;
}
snake.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<locale.h>
#include<windows.h>
#include<stdbool.h>
#include <graphics.h>
#include <conio.h>
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//蛇默认的起始坐标
#define POS_X 300
#define POS_Y 200
//背景单位
#define SIZE_X 15
#define SIZE_Y 20
//游戏的状态
enum GAME_STATUS
{
OK = 1,//正常运行
ESC, //按了ESC键退出,正常退出
KILL_BY_WALL,//撞墙
KILL_BY_SELF //撞到自身
};
typedef struct snake {
int x;
int y;
struct snake* next;
}SnakeNode, * pSnakeNode;//结构体指针重命名
//蛇行走的方向
enum DIRECTION
{
UP = 1,
DOWN,
LEFT,
RIGHT//向右
};
//贪吃蛇应该有的属性
typedef struct Snake
{
pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头
pSnakeNode pFood;//指向食物的指针
int Score;//当前累积的分数
int FoodWeight;//一个食物的分数
int SleepTime;//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢
enum GAME_STATUS status;//游戏当前的状态
enum DIRECTION dir;//蛇当前走的方向
//...
}Snake, * pSnake;
//游戏前的初始化
void GameStart(pSnake p);
//打印欢迎信息
void WelcomeToGame();
//绘制地图
void CreateMap();
//初始化蛇
void InitSnake(pSnake ps);
//打印图片
void picture(int x, int y);
//创建食物
void CreateFood(pSnake ps);
//玩游戏的过程
void GameRun(pSnake ps);
//打印帮助(规则)信息
void PrintHelpInfo();
//当前分数情况
void Score(pSnake ps);
//游戏要暂停
void pause();
//蛇走一步
void SnakeMove(pSnake ps);
//下一个坐标是不是食物
bool NextIsFood(pSnake ps, pSnakeNode pNext);
//是食物就吃掉
void EatFood(pSnake ps, pSnakeNode pNext);
//不是食物就正常一步
void NotEatFood(pSnake ps, pSnakeNode pNext);
//检测撞墙
void KillByWall(pSnake ps);
//检测撞到自己
void KillBySelf(pSnake ps);
//结束游戏后的工作
void GameEnd(pSnake ps);
//判断是否再来一局
bool Determine(char str[]);
snake.cpp
#include"snake.h"
void GameStart(pSnake ps) {
//设置控制台信息,窗口大小,窗口名
initgraph(1300, 720, EX_SHOWCONSOLE);
//打印欢迎信息
WelcomeToGame();
//绘制地图
CreateMap();
//初始化蛇
InitSnake(ps);
//创建食物
CreateFood(ps);
}
//打印欢迎信息
void WelcomeToGame() {
//欢迎信息
system("title 贪吃蛇");
outtextxy(500, 350, "欢迎来到贪吃蛇小游戏");
outtextxy(600, 400, "随意点击屏幕继续. . .");
//随意点击屏幕
system("pause");
//清空屏幕,换画面
cleardevice();
//功能介绍信息
outtextxy(400, 350, "1.用 ↑ ↓ ← → 来控制蛇的移动,F3是加速,F4是减速");
outtextxy(400, 370, "2.加速能得到更高的分数");
outtextxy(600, 400, "随意点击屏幕继续. . .");
//打印尾巴信息
system("pause");
//清空桌面
cleardevice();
}
//绘制地图
void CreateMap() {
IMAGE img1;
//玩游戏背景图
loadimage(&img1, "picture.jpg\\picture.jpg", 900, 700, true);
//从0,开始画图
putimage(0, 0, &img1);
//画线矩形
setlinecolor(GREEN);
//rectangle(0, 0, 900, 699);
//rectangle(3, 3, 897, 697);
//上
int i = 0;
for (i = 0; i <= 900; i += 15)
{
rectangle(i, 0, i + 13, 13);
}
//下
for (i = 0; i <= 900; i += 15)
{
rectangle(i, 700, i + 13, 700 + 13);
}
//左
for (i = 20; i < 700; i += 20)
{
rectangle(0, i , 0 + 13, i + 17);
}
//右
for (i = 1; i < 700; i += 20)
{
rectangle(900, i, 900 + 13, i + 17);
}
}
//初始化蛇
void InitSnake(pSnake ps) {
//创建5个节点
pSnakeNode cur = NULL;
for (int i = 0; i < 5; i++) {
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL) {
perror("malloc fail!");
return;
}
cur->x = POS_X + i * 15;
cur->y = POS_Y;
cur->next = NULL;
if (ps->pSnake == NULL) {
ps->pSnake = cur;
}
else {
cur->next = ps->pSnake;
ps->pSnake = cur;
}
}
//打印蛇的位置
cur = ps->pSnake;
while (cur) {
picture(cur->x, cur->y);
cur = cur->next;
}
//初始化贪吃蛇其他属性
ps->dir = RIGHT;//向右
ps->pFood = NULL;
ps->Score = 0;
ps->FoodWeight = 10;
ps->SleepTime = 200;
ps->status = OK;
}
//打印蛇身
void picture(int x, int y) {
IMAGE img1;
loadimage(&img1, "picture.jpg\\picture2.jpg", SIZE_X, SIZE_Y, true);
putimage(x, y, &img1);
}
//创建食物
void CreateFood(pSnake ps) {
pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNext == NULL) {
perror("pNext malloc fail!");
return;
}
int x = 0;
int y = 0;
circulate:
do {
//避免出现在(0,0)
x = rand() % 871 + 15 ;
y = rand() % 681 + 20;
} while (x % 15 != 0 || y % 20 != 0);
pSnakeNode cur = ps->pSnake;
while (cur) {
if (cur->x == x && cur->y == y) {
goto circulate;
}
cur = cur->next;
}
pNext->x = x;
pNext->y = y;
pNext->next = NULL;
//维护食物
ps->pFood = pNext;
//打印食物
IMAGE img3;
loadimage(&img3, "picture.jpg\\R-C (1).jpg", 15, 20, true);
putimage(x, y, &img3);
}
//玩游戏的过程
void GameRun(pSnake ps) {
//打印帮助(规则)信息
PrintHelpInfo();
do {
//当前分数情况
Score(ps);
//向上
if (KEY_PRESS(VK_UP) && ps->dir != DOWN) {
ps->dir = UP;
}
//向下
else if (KEY_PRESS(VK_DOWN) && ps->dir != UP) {
ps->dir = DOWN;
}
//向左
else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {
ps->dir = LEFT;
}
//向右
else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) {
ps->dir = RIGHT;
}
//暂停
else if (KEY_PRESS(VK_SPACE)) {
//游戏要暂停
pause();
}
//加速
else if (KEY_PRESS(VK_F3)) {
if (ps->FoodWeight < 16) {
ps->FoodWeight += 2;
ps->SleepTime -= 50;
}
}
//减速
else if (KEY_PRESS(VK_F4)) {
if (ps->FoodWeight > 4) {
ps->FoodWeight -= 2;
ps->SleepTime += 50;
}
}
//退出游戏
else if (KEY_PRESS(VK_ESCAPE)) {
ps->status = ESC;
}
//走一步
SnakeMove(ps);
//休眠
Sleep(ps->SleepTime);
} while (ps->status == OK);
}
//打印帮助(规则)信息
void PrintHelpInfo() {
//规则
setlinecolor(MAGENTA);
rectangle(918, 50, 1250, 180);
outtextxy(930, 60, "1.不能穿墙,不能咬到自己");
outtextxy(930, 80, "2.用 ↑.↓.←.→ 来控制蛇的移动");
outtextxy(930, 100, "3.F3是加速,F4是减速");
outtextxy(930, 120, "4.一个食物基础分10分");
outtextxy(930, 140, "5.加速一次加2分,最大加6分");
outtextxy(930, 160, "6.减速一次减2分,最大减6分");
}
//当前分数情况
void Score(pSnake ps) {
char num1[10];
char num2[10];
sprintf(num1, "%d", ps->Score);
sprintf(num2, "%d", ps->FoodWeight);
outtextxy(930, 300, "当前总积分:");
outtextxy(1020, 300, num1);
outtextxy(930, 340, "当前一个食物积分:");
outtextxy(1065, 340, num2);
}
//游戏要暂停
void pause() {
while (true) {
if (KEY_PRESS(VK_SPACE)) {
break;
}
Sleep(200);
}
}
//蛇走一步
void SnakeMove(pSnake ps) {
//生成下一个走过的节点
pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pNext == NULL) {
perror("malloc tail");
return;
}
pNext->next = NULL;
//向上
if (ps->dir == UP) {
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y - 20;
}
//向下
else if (ps->dir == DOWN) {
pNext->x = ps->pSnake->x;
pNext->y = ps->pSnake->y + 20;
}
//向右
else if (ps->dir == LEFT) {
pNext->x = ps->pSnake->x - 15;
pNext->y = ps->pSnake->y;
}
//向左
else if (ps->dir == RIGHT) {
pNext->x = ps->pSnake->x + 15;
pNext->y = ps->pSnake->y;
}
//下一个坐标处是否是食物
if (NextIsFood(ps, pNext))
{
//是食物就吃掉
EatFood(ps, pNext);
}
else
{
//不是食物就正常一步
NotEatFood(ps, pNext);
}
//检测撞墙
KillByWall(ps);
//检测撞到自己
KillBySelf(ps);
}
//判断下一个坐标是不是食物
bool NextIsFood(pSnake ps, pSnakeNode pNext) {
if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y) {
return true;
}
else {
return false;
}
}
//是食物就吃掉
void EatFood(pSnake ps, pSnakeNode pNext) {
//头插
pNext->next = ps->pSnake;
ps->pSnake = pNext;
//打印蛇
pSnakeNode cur = ps->pSnake;
while (cur) {
picture(cur->x, cur->y);S
cur = cur->next;
}
//改变分数
ps->Score += ps->FoodWeight;
//释放旧的食物
free(ps->pFood);
//重新生成食物
CreateFood(ps);
}
//不是食物就正常一步
void NotEatFood(pSnake ps, pSnakeNode pNext) {
//头插
pNext->next = ps->pSnake;
ps->pSnake = pNext;
pSnakeNode cur = ps->pSnake;
while (cur->next->next) {
picture(cur->x, cur->y);
cur = cur->next;
}
//把尾巴打印成背景图片
IMAGE img4;
loadimage(&img4, "picture.jpg\\picture4.jpg", SIZE_X, SIZE_Y, true);
putimage(cur->next->x, cur->next->y, &img4);
//释放尾部
free(cur->next);
cur->next = NULL;
}
//检测撞墙
void KillByWall(pSnake ps) {
if (ps->pSnake->x == 885 ||
ps->pSnake->y == 680 ||
ps->pSnake->x == 15 ||
ps->pSnake->y == 20)
{
ps->status = KILL_BY_WALL;
return;
}
}
//检测撞到自己
void KillBySelf(pSnake ps) {
pSnakeNode cur = ps->pSnake->next;
while (cur) {
if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y)
{
ps->status = KILL_BY_SELF;
return;
}
cur = cur->next;
}
}
//结束游戏后的工作
void GameEnd(pSnake ps)
{
switch (ps->status)
{
case ESC:
outtextxy(330, 300, "主动退出游戏,正常退出\n");
break;
case KILL_BY_WALL:
outtextxy(330, 300, "很遗憾,撞墙了,游戏结束\n");
break;
case KILL_BY_SELF:
outtextxy(330, 300, "很遗憾,咬到自己了,游戏结束\n");
break;
}
//释放贪吃蛇的链表资源
pSnakeNode cur = ps->pSnake;
pSnakeNode del = NULL;
while (cur)
{
del = cur;
cur = cur->next;
free(del);
}
//把最后打印的食物打印成背景图片
IMAGE img4;
loadimage(&img4, "picture.jpg\\picture4.jpg", SIZE_X, SIZE_Y, true);
putimage(ps->pFood->x, ps->pFood->y, &img4);
free(ps->pFood);
ps = NULL;
}
//判断是否再来一局
bool Determine(char str[]) {
char arr[4] = "Yes";
int ret = strcmp(str, arr);
if (ret == 0) {
return true;
}
else {
return false;
}
}
好啦今天的分享就到这里了,感谢观看。