💡目录
效果图
界面设计思路
1. 基本布局
2. 视觉元素
游戏机制设计
基本规则
游戏代码
前期准备
游戏代码详解
数据结构设计
宏定义
数据结构定义
函数原型(详见后文)
主函数代码
核心代码
Review
效果图
界面设计思路
1. 基本布局
- 游戏区域:屏幕中央占据大部分面积,用于显示游戏地图,地图通常被设计成一个矩形网格,每个单元格代表蛇可以移动的一个位置
- 得分显示:位于屏幕的顶部或角落,实时更新并显示玩家当前的得分,鼓励玩家追求更高的分数
- 控制提示:初学者友好设计,简短说明如何控制蛇的移动(如使用箭头键),通常在游戏开始前或暂停时显示
2. 视觉元素
- 蛇:使用不同的字符或颜色块来表示蛇的身体和头部,头部可能有特殊标记以区分,蛇移动时,通过改变字符或颜色的位置来模拟动画效果
- 食物:采用醒目的颜色或符号,使其在地图上易于识别,吸引玩家去追逐
- 边界:地图边缘用特殊的符号或颜色加重,提醒玩家避免碰撞
游戏机制设计
基本规则
- 移动:玩家通过键盘控制蛇的前进方向,蛇会在每一帧自动向前移动一格,无法倒退或立即转向180度
- 生长:当蛇头触碰到食物时,蛇的长度增加,同时分数增加,并在地图上的随机空白处生成新的食物
- 死亡条件:蛇碰到自己的身体或地图边界时,游戏结束。此外,可以设定时间限制或生命值系统增加挑战性
- 速度控制:玩家可通过按键调整蛇的移动速度
游戏代码
snake.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#define _KEY_STATE(x) (GetAsyncKeyState(x)&1?1:0)
#define _SNAKE_LENTH_ 5
#define GAME_WIDTH 26
#define GAME_HEIGHT 26
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#include<io.h>
#include<fcntl.h>
#include<time.h>
typedef enum DIRECTION//蛇的方向
{
UP,DOWN,LEFT,RIGHT
}DIRECTION;
typedef enum STATE//蛇的状态
{
RUN,PAUSE,KILL_BY_SELF,KILL_BY_WALL
}STATE;
typedef struct Psnake //用于记录蛇的各个节点
{
int x;
int y;
struct Psnake* next;
}Psnake;
Psnake* PsnakeNode;
typedef struct Food
{
int x;
int y;
int score;
}Food;
typedef struct object
{
Psnake* psnakenode;
Food food;
int score;
int speed;
DIRECTION dir;
STATE state;
}object;
object SNAKE;
void set_pos(short x, short y);//设置光标坐标
void cursor(bool state, int x);//设置光标可见性和大小
void map();//打印地图
void cover();//打印封面
void GameStart();//游戏初始化
void printInstruction(int y, const wchar_t* message);//打印提示信息
void PrintFood(Food* pos);//打印食物
void CreateFood(Psnake* snake);//创建食物
bool FoodJudge(Psnake* next);//判断下一节点是否为食物
Psnake* buynode(int x, int y);//申请节点
Psnake* InitSnake(Psnake** head);//初始化蛇
void PrintSnake(Psnake* head);//打印蛇
void IsDie();//判断满足结束条件
void SnakeMove(Psnake** head);//移动蛇
void ClearScreen();//清屏
void GameOver(int score);//游戏结束
void GameRun(Psnake** head);//运行游戏
void GameStart();//游戏前准备
snake.c
#include "snake.h"
//设置坐标
void set_pos(short x, short y)
{
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(hConsole, coord);
}//设置坐标
//设置光标信息
void cursor(bool state, int x)
{
HANDLE op = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO con;
con.dwSize = x;
con.bVisible = state;
SetConsoleCursorInfo(op, &con);
}
//打印提示信息
void printInstruction(int y, const wchar_t* message) {
set_pos(60, y);
wprintf(message);
}
//打印地图
void map() {
// Draw top border
set_pos(0, 0);
for (int i = 0; i < 29; i++) {
wprintf(L"□");
}
// Draw left and right borders
for (int i = 1; i < 29; i++) {
set_pos(0, i);
wprintf(L"□");
set_pos(28 * 2, i); //右边的坐标(28*2 ,i)
wprintf(L"□");
}
// Draw bottom border
set_pos(0, 29);
for (int i = 0; i < 29; i++) {
wprintf(L"□");
}
const wchar_t* instructions[] = {
L"操作指南: 使用方向键←↑→↓",
L"速度控制: F2 加速, F3 减速",
L"暂停游戏: 按 P",
L"退出游戏: 按 ESC"
};
for (int i = 0; i < sizeof(instructions) / sizeof(instructions[0]); ++i) {
printInstruction(10 + i, instructions[i]);
}
}
//打印封面
void cover()
{
system("mode con cols=120 lines=50");
system("title 贪吃蛇");
set_pos(40, 10);
wprintf(L" \n");
wprintf(L" .-----------------. .-----------------. .----------------. .----------------. .---------------- .\n");
wprintf(L" | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |\n");
wprintf(L" | | _______ | || | ____ _____ | || | __ | || | ______ | || | ___ ____ | |\n");
wprintf(L" | | / ___ | | || ||_ \\ |_ _| | || | / \\ | || | .' ___ | | || | |_ ||_ _| | |\n");
wprintf(L" | | | (__ \\_| | || | | \\ | | | || | / /\\ \\ | || | / .' \\_| | || | | |_/ / | |\n");
wprintf(L" | | '.___`-. | || | | |\\ \\| | | || | / ____ \\ | || | | | | || | | __'. | |\n");
wprintf(L" | | |`\\____) | | || | _| |_\\ | |_ | || | / / \\ \\_ | || | \\ `.___.'\\ | || | _| | \\ \\_ | |\n");
wprintf(L" | | |_______.' | || ||_____|\\____| | || ||____| |____|| || | `._____.' | || | |____ || __|_| |\n");
wprintf(L" | | | || | | || | | || | | || | | |\n");
wprintf(L" | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' |\n");
wprintf(L" '----------------' '------'---------' '----------------' '------'---------' '--'-------------'\n");
set_pos(45, 29);
system("pause");
system("cls");
map();
}
//打印食物坐标
void PrintFood(Food* pos)
{
set_pos((*pos).x, (*pos).y);
wprintf(L"◆");
}
//创造食物
void CreateFood(Psnake* snake)
{
Food* pos = (Food*)malloc(sizeof(Food));
if (!pos)
{
perror("MALLOC FAILED:");
return;
}
Psnake* cur = snake;
//生成随机数
srand((unsigned int)time(NULL));
again:
(*pos).x = (rand() % GAME_WIDTH + 2) * 2; // rand() % 27 会得到0到26之间的数,加2后变为2到28
(*pos).y = rand() % GAME_HEIGHT + 2; // rand() % 27 会得到0到26之间的数,加2后变为2到28
(*pos).score = 1;
while (cur)
{
if ((*pos).x == cur->x && (*pos).y == cur->y)
goto again;
cur = cur->next;
}
SNAKE.food = *pos;
PrintFood(pos);
}
//判断下一个节点是否为食物
bool FoodJudge(Psnake* next)
{
return ((next->x == SNAKE.food.x) && (next->y == SNAKE.food.y)) ? true : false;
}
//申请一个节点
Psnake* buynode(int x,int y)
{
Psnake* newnode = (Psnake*)calloc(1, sizeof(Psnake));
if (newnode == NULL)
{
perror("calloc:");
return NULL;
}
newnode->next = NULL;
newnode->x = x;
newnode->y = y;
return newnode;
}
//初始化蛇的信息
Psnake* InitSnake(Psnake**head)
{
if (!(head&&*head)) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
(*head) = (Psnake*)calloc(1, sizeof(Psnake));
// 初始化成员
(*head)->x = 6;
(*head)->y = 10;
(*head)->next = NULL;
Psnake* cur = *head;
if (!cur)
{
perror("head is nullptr");
return NULL;
}
int x = _SNAKE_LENTH_;
while (x--)
{
Psnake* newnode= buynode((cur->x)+2, cur->y);
newnode->next = cur;
cur = newnode;
}
SNAKE.dir = RIGHT;
SNAKE.psnakenode = cur;
SNAKE.state = RUN;
SNAKE.speed = 150;
return cur;
}
//打印蛇
void PrintSnake(Psnake* head)
{
Psnake* cur = SNAKE.psnakenode;
while (cur)
{
set_pos(cur->x, cur->y);
wprintf(L"●");
cur = cur->next;
}
}
void IsDie() {
Psnake* cur = SNAKE.psnakenode->next; // 从第二个节点开始
Psnake* prev = SNAKE.psnakenode; // 保存蛇头节点用于比较
while (cur->next) {
cur = cur->next;
if (cur->x == prev->x && cur->y == prev->y) { // 检查当前节点是否与之前的节点重合
SNAKE.state = KILL_BY_SELF;
return;
}
}
if (SNAKE.psnakenode->x == 56||SNAKE.psnakenode->x==0 ||SNAKE.psnakenode->y==0||SNAKE.psnakenode->y == 28) {
SNAKE.state = KILL_BY_WALL;
return;
}
}
//蛇的移动
void SnakeMove(Psnake** head)
{
// 首先检查蛇头的下一个位置
Psnake* next = (Psnake*)calloc(1, sizeof(Psnake));
if (!next) {
perror("Failed to allocate memory for next snake node");
exit(EXIT_FAILURE);
}
// 根据当前方向设置蛇头的下一个位置
switch (SNAKE.dir)
{
case UP:
next->x = SNAKE.psnakenode->x;
next->y = SNAKE.psnakenode->y - 1;
break;
case DOWN:
next->x = SNAKE.psnakenode->x;
next->y = SNAKE.psnakenode->y + 1;
break;
case LEFT:
next->x = SNAKE.psnakenode->x - 2;
next->y = SNAKE.psnakenode->y;
break;
case RIGHT:
next->x = SNAKE.psnakenode->x + 2;
next->y = SNAKE.psnakenode->y;
break;
default:
free(next);
return; // 如果方向无效,不移动蛇
}
// 检查下一个位置是否有食物
if (FoodJudge(next))
{
SNAKE.score += SNAKE.food.score; // 增加得分
next->next = (*head); // 将新节点放在头部
*head = next; // 更新头指针
CreateFood(*head); // 生成新的食物
}
else
{
// 没有食物,移动蛇的尾部到头部
Psnake* tail = SNAKE.psnakenode;
Psnake* prev = NULL;
if (!(tail || prev))
{
perror("POINTER IS NULL:");
return;
}
// 找到尾节点和其前一个节点
while (tail->next) {
prev = tail;
tail = tail->next;
}
// 将尾部节点移动到头部
prev->next = NULL; // 将前一个节点的next设置为NULL,它现在是尾部
set_pos(tail->x, tail->y);
wprintf(L" ");
tail->next = SNAKE.psnakenode; // 将原尾部节点放在头部
SNAKE.psnakenode = tail; // 更新头指针
// 更新头部位置
SNAKE.psnakenode->x = next->x;
SNAKE.psnakenode->y = next->y;
// 释放之前分配的next节点,因为我们只是移动了尾部节点
free(next);
}
IsDie();
PrintSnake(SNAKE.psnakenode);
}
//=========================================
//运行游戏
void ClearScreen() { // 定义清屏函数,适用于Windows和Linux/macOS
#ifdef _WIN32
system("cls");
#else
system("clear");
#endif
}
void GameOver(int score) {
// 游戏结束后的善后处理
ClearScreen(); // 清除屏幕
wprintf(L"Game Over!\n"); // 打印游戏结束信息
wprintf(L"总分: %d\n", score); // 显示玩家得分
// 询问玩家是否想再玩一次
wprintf(L"再来一次?(y/n): ");
system("pause >> nul");
if (_KEY_STATE(0x59)){
GameStart();
fflush(stdin);
}
else {
wprintf(L"感谢游玩\n");
exit(0); // 结束程序
}
}
void GameRun(Psnake**head)
{
do
{
set_pos(60, 6);
wprintf(L"当前分数: %d", SNAKE.score);
if ((_KEY_STATE(VK_UP) || _KEY_STATE(0x57)) && SNAKE.dir != DOWN)
{
SNAKE.dir = UP;
}
else if ((_KEY_STATE(VK_DOWN) || _KEY_STATE(0x53)) && SNAKE.dir != UP)
{
SNAKE.dir = DOWN;
}
else if ((_KEY_STATE(VK_LEFT) || _KEY_STATE(0x41)) && SNAKE.dir != RIGHT)
{
SNAKE.dir = LEFT;
}
else if ((_KEY_STATE(VK_RIGHT) || _KEY_STATE(0x44)) && SNAKE.dir != LEFT)
{
SNAKE.dir = RIGHT;
}
else if (_KEY_STATE(0x71)&&SNAKE.speed>=0&&SNAKE.speed<=500)
{
(SNAKE.speed) -= 30;
}
else if (_KEY_STATE(0x72)&&SNAKE.speed>=0&&SNAKE.speed<=500)
{
(SNAKE.speed) += 30;
}
else if (_KEY_STATE(0x50))
{
SNAKE.speed = 2147483647;
}
SnakeMove(head);
Sleep(SNAKE.speed);
} while (SNAKE.state == RUN);
GameOver(SNAKE.score);
}
//开始游戏及游戏前准备
void GameStart()
{
cover();
SNAKE.psnakenode = (Psnake*)malloc(sizeof(Psnake));
InitSnake(&SNAKE.psnakenode);
PrintSnake(SNAKE.psnakenode);
CreateFood(SNAKE.psnakenode);
set_pos(40, 36);
GameRun(&SNAKE.psnakenode);
}
main.c
#include"snake.h"
int main()
{
// 环境配置
//=================================================
// 可打印宽字符
_setmode(_fileno(stdout), _O_U16TEXT);
//切换到本地
setlocale(LC_ALL, "");
system("mode con cols=10 lines=20");
set_pos(0, 20);
cursor(false, 10);
//=================================================
// 正式开始
GameStart();
set_pos(0, 40);
return 0;
}
前期准备
在 visual studio 2022中,默认窗口为cmd终端,无法实现预期效果,我们需要以下设置。若为其他开发环境请自查资料。
游戏代码详解
数据结构设计
snake.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1
#define _KEY_STATE(x) (GetAsyncKeyState(x)&1?1:0)
#define _SNAKE_LENTH_ 5
#define GAME_WIDTH 26
#define GAME_HEIGHT 26
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<stdbool.h>
#include<locale.h>
#include<io.h>
#include<fcntl.h>
#include<time.h>
typedef enum DIRECTION//蛇的方向
{
UP,DOWN,LEFT,RIGHT
}DIRECTION;
typedef enum STATE//蛇的状态
{
RUN,PAUSE,KILL_BY_SELF,KILL_BY_WALL
}STATE;
typedef struct Psnake //用于记录蛇的各个节点
{
int x;
int y;
struct Psnake* next;
}Psnake;
Psnake* PsnakeNode;
typedef struct Food
{
int x;
int y;
int score;
}Food;
typedef struct object
{
Psnake* psnakenode;
Food food;
int score;
int speed;
DIRECTION dir;
STATE state;
}object;
object SNAKE;
void set_pos(short x, short y);//设置光标坐标
void cursor(bool state, int x);//设置光标可见性和大小
void map();//打印地图
void cover();//打印封面
void GameStart();//游戏初始化
void printInstruction(int y, const wchar_t* message);//打印提示信息
void PrintFood(Food* pos);//打印食物
void CreateFood(Psnake* snake);//创建食物
bool FoodJudge(Psnake* next);//判断下一节点是否为食物
Psnake* buynode(int x, int y);//申请节点
Psnake* InitSnake(Psnake** head);//初始化蛇
void PrintSnake(Psnake* head);//打印蛇
void IsDie();//判断满足结束条件
void SnakeMove(Psnake** head);//移动蛇
void ClearScreen();//清屏
void GameOver(int score);//游戏结束
void GameRun(Psnake** head);//运行游戏
void GameStart();//游戏前准备
宏定义
#pragma once
: 用于防止头文件被重复包含。_CRT_SECURE_NO_WARNINGS 1
: 禁用Visual Studio中的某些不安全函数警告,比如使用scanf
等。_KEY_STATE(x) (GetAsyncKeyState(x)&1?1:0)
: 自定义宏,用来检查给定虚拟键是否被按下。GetAsyncKeyState
是Windows API函数( 配合虚拟键码使用 ),用于检查按键状态。_SNAKE_LENTH_ 5
: 定义初始蛇的长度为5个单位。GAME_WIDTH 26
和GAME_HEIGHT 26
: 定义游戏地图的宽度和高度,均为26个单位。
数据结构定义
enum DIRECTION
: 定义蛇的移动方向,包括上、下、左、右。enum STATE
: 定义蛇的状态,包括运行中、暂停、自杀死亡、撞墙死亡。struct Psnake
: 蛇的节点结构体,包含位置坐标(x, y)
和指向下一个节点的指针。struct Food
: 食物结构体,包含位置坐标(x, y)
和分数。struct object
: 游戏对象结构体,整合蛇、食物、得分、速度、方向和状态等游戏所需的所有信息。
将蛇、食物抽象成一个结构体,用宽字符来打印,enum STATE、enum DIRECTION 来表示蛇的状态。将以上数据放在object结构体中进行管理。
函数原型(详见后文)
set_pos(short x, short y)
: 设置控制台光标位置。cursor(bool state, int x)
: 设置控制台光标的可见性和大小。map()
: 打印游戏地图。cover()
: 打印游戏封面。GameStart()
: 游戏初始化入口,可能包含了封面展示、地图绘制、蛇的初始化等。printInstruction(int y, const wchar_t* message)
: 在指定行打印宽字符格式的提示信息。PrintFood(Food* pos)
: 在指定位置打印食物。CreateFood(Psnake* snake)
: 随机生成食物,并确保不与蛇身重叠。FoodJudge(Psnake* next)
: 判断蛇的下一个位置是否为食物。buynode(int x, int y)
: 申请一个新的蛇节点内存。InitSnake(Psnake** head)
: 初始化蛇的链表结构。PrintSnake(Psnake* head)
: 打印蛇的当前位置。IsDie()
: 判断游戏是否结束,即蛇是否撞墙或自相残杀。SnakeMove(Psnake** head)
: 处理蛇的移动逻辑,包括方向改变和吃到食物的处理。ClearScreen()
: 清除屏幕内容。GameOver(int score)
: 游戏结束时的操作,显示分数并可能询问是否重新开始。GameRun(Psnake** head)
: 游戏主循环,处理用户输入和游戏逻辑。
整个框架围绕着贪吃蛇的基本逻辑展开,包括了游戏的初始化、地图绘制、蛇的移动与增长、食物的生成与检测、游戏状态管理以及用户交互。
主函数代码
main.c
#include"snake.h"
int main()
{
// 环境配置
//=================================================
// 可打印宽字符
_setmode(_fileno(stdout), _O_U16TEXT);
//切换到本地
setlocale(LC_ALL, "");
system("mode con cols=10 lines=20");
set_pos(0, 20);
cursor(false, 10);
//=================================================
// 正式开始
GameStart();
set_pos(0, 40);
return 0;
}
_setmode(_fileno(stdout) , _O_U16TEXT); 将输出模式设置为 _O_U16TEXT
模式,以便后续打印宽字符。
_setmode
函数来自<io.h>
头文件。_fileno
函数来自<stdio.h>
头文件。_O_U16TEXT
宏定义通常在<fcntl.h>
或<io.h>
中找到
setlocale(LC_ARR,""); 切换到本地模式
LC_ALL
是一个宏,代表所有类别,包括日期和时间格式、数字表示、货币符号、语言等。使用LC_ALL
意味着你想改变所有方面的区域设置。""
作为第二个参数,是一个特殊的值,它告诉函数使用用户默认的或者环境变量(如LANG
或LC_ALL
)指定的区域设置。
system("mode con cols=10 lines=20"); 将窗口设置为20行高,10列宽,在vs控制台中,字符高度一致,但宽度不一致,一般来说宽字符是窄字符的两倍,为了达到预期效果,需按照以上比例设置。
set_pos(0,20); 设置打印字符的位置,这是我自己封装的函数,后续会讲到。
cursor(false,10); 设置光标状态,自己封装的函数,后续会讲到。
GameStart(); 正式进入游戏。跳转到snake.c文件中。
核心代码
snake.c
#include "snake.h"
//设置坐标
void set_pos(short x, short y)
{
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(hConsole, coord);
}//设置坐标
//设置光标信息
void cursor(bool state, int x)
{
HANDLE op = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO con;
con.dwSize = x;
con.bVisible = state;
SetConsoleCursorInfo(op, &con);
}
//打印提示信息
void printInstruction(int y, const wchar_t* message) {
set_pos(60, y);
wprintf(message);
}
//打印地图
void map() {
// Draw top border
set_pos(0, 0);
for (int i = 0; i < 29; i++) {
wprintf(L"□");
}
// Draw left and right borders
for (int i = 1; i < 29; i++) {
set_pos(0, i);
wprintf(L"□");
set_pos(28 * 2, i); //右边的坐标(28*2 ,i)
wprintf(L"□");
}
// Draw bottom border
set_pos(0, 29);
for (int i = 0; i < 29; i++) {
wprintf(L"□");
}
const wchar_t* instructions[] = {
L"操作指南: 使用方向键←↑→↓",
L"速度控制: F2 加速, F3 减速",
L"暂停游戏: 按 P",
L"退出游戏: 按 ESC"
};
for (int i = 0; i < sizeof(instructions) / sizeof(instructions[0]); ++i) {
printInstruction(10 + i, instructions[i]);
}
}
//打印封面
void cover()
{
system("mode con cols=120 lines=50");
system("title 贪吃蛇");
set_pos(40, 10);
wprintf(L" \n");
wprintf(L" .-----------------. .-----------------. .----------------. .----------------. .---------------- .\n");
wprintf(L" | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |\n");
wprintf(L" | | _______ | || | ____ _____ | || | __ | || | ______ | || | ___ ____ | |\n");
wprintf(L" | | / ___ | | || ||_ \\ |_ _| | || | / \\ | || | .' ___ | | || | |_ ||_ _| | |\n");
wprintf(L" | | | (__ \\_| | || | | \\ | | | || | / /\\ \\ | || | / .' \\_| | || | | |_/ / | |\n");
wprintf(L" | | '.___`-. | || | | |\\ \\| | | || | / ____ \\ | || | | | | || | | __'. | |\n");
wprintf(L" | | |`\\____) | | || | _| |_\\ | |_ | || | / / \\ \\_ | || | \\ `.___.'\\ | || | _| | \\ \\_ | |\n");
wprintf(L" | | |_______.' | || ||_____|\\____| | || ||____| |____|| || | `._____.' | || | |____ || __|_| |\n");
wprintf(L" | | | || | | || | | || | | || | | |\n");
wprintf(L" | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' |\n");
wprintf(L" '----------------' '------'---------' '----------------' '------'---------' '--'-------------'\n");
set_pos(45, 29);
system("pause");
system("cls");
map();
}
//打印食物坐标
void PrintFood(Food* pos)
{
set_pos((*pos).x, (*pos).y);
wprintf(L"◆");
}
//创造食物
void CreateFood(Psnake* snake)
{
Food* pos = (Food*)malloc(sizeof(Food));
if (!pos)
{
perror("MALLOC FAILED:");
return;
}
Psnake* cur = snake;
//生成随机数
srand((unsigned int)time(NULL));
again:
(*pos).x = (rand() % GAME_WIDTH + 2) * 2; // rand() % 27 会得到0到26之间的数,加2后变为2到28
(*pos).y = rand() % GAME_HEIGHT + 2; // rand() % 27 会得到0到26之间的数,加2后变为2到28
(*pos).score = 1;
while (cur)
{
if ((*pos).x == cur->x && (*pos).y == cur->y)
goto again;
cur = cur->next;
}
SNAKE.food = *pos;
PrintFood(pos);
}
//判断下一个节点是否为食物
bool FoodJudge(Psnake* next)
{
return ((next->x == SNAKE.food.x) && (next->y == SNAKE.food.y)) ? true : false;
}
//申请一个节点
Psnake* buynode(int x,int y)
{
Psnake* newnode = (Psnake*)calloc(1, sizeof(Psnake));
if (newnode == NULL)
{
perror("calloc:");
return NULL;
}
newnode->next = NULL;
newnode->x = x;
newnode->y = y;
return newnode;
}
//初始化蛇的信息
Psnake* InitSnake(Psnake**head)
{
if (!(head&&*head)) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
(*head) = (Psnake*)calloc(1, sizeof(Psnake));
// 初始化成员
(*head)->x = 6;
(*head)->y = 10;
(*head)->next = NULL;
Psnake* cur = *head;
if (!cur)
{
perror("head is nullptr");
return NULL;
}
int x = _SNAKE_LENTH_;
while (x--)
{
Psnake* newnode= buynode((cur->x)+2, cur->y);
newnode->next = cur;
cur = newnode;
}
SNAKE.dir = RIGHT;
SNAKE.psnakenode = cur;
SNAKE.state = RUN;
SNAKE.speed = 150;
return cur;
}
//打印蛇
void PrintSnake(Psnake* head)
{
Psnake* cur = SNAKE.psnakenode;
while (cur)
{
set_pos(cur->x, cur->y);
wprintf(L"●");
cur = cur->next;
}
}
void IsDie() {
Psnake* cur = SNAKE.psnakenode->next; // 从第二个节点开始
Psnake* prev = SNAKE.psnakenode; // 保存蛇头节点用于比较
while (cur->next) {
cur = cur->next;
if (cur->x == prev->x && cur->y == prev->y) { // 检查当前节点是否与之前的节点重合
SNAKE.state = KILL_BY_SELF;
return;
}
}
if (SNAKE.psnakenode->x == 56||SNAKE.psnakenode->x==0 ||SNAKE.psnakenode->y==0||SNAKE.psnakenode->y == 28) {
SNAKE.state = KILL_BY_WALL;
return;
}
}
//蛇的移动
void SnakeMove(Psnake** head)
{
// 首先检查蛇头的下一个位置
Psnake* next = (Psnake*)calloc(1, sizeof(Psnake));
if (!next) {
perror("Failed to allocate memory for next snake node");
exit(EXIT_FAILURE);
}
// 根据当前方向设置蛇头的下一个位置
switch (SNAKE.dir)
{
case UP:
next->x = SNAKE.psnakenode->x;
next->y = SNAKE.psnakenode->y - 1;
break;
case DOWN:
next->x = SNAKE.psnakenode->x;
next->y = SNAKE.psnakenode->y + 1;
break;
case LEFT:
next->x = SNAKE.psnakenode->x - 2;
next->y = SNAKE.psnakenode->y;
break;
case RIGHT:
next->x = SNAKE.psnakenode->x + 2;
next->y = SNAKE.psnakenode->y;
break;
default:
free(next);
return; // 如果方向无效,不移动蛇
}
// 检查下一个位置是否有食物
if (FoodJudge(next))
{
SNAKE.score += SNAKE.food.score; // 增加得分
next->next = (*head); // 将新节点放在头部
*head = next; // 更新头指针
CreateFood(*head); // 生成新的食物
}
else
{
// 没有食物,移动蛇的尾部到头部
Psnake* tail = SNAKE.psnakenode;
Psnake* prev = NULL;
if (!(tail || prev))
{
perror("POINTER IS NULL:");
return;
}
// 找到尾节点和其前一个节点
while (tail->next) {
prev = tail;
tail = tail->next;
}
// 将尾部节点移动到头部
prev->next = NULL; // 将前一个节点的next设置为NULL,它现在是尾部
set_pos(tail->x, tail->y);
wprintf(L" ");
tail->next = SNAKE.psnakenode; // 将原尾部节点放在头部
SNAKE.psnakenode = tail; // 更新头指针
// 更新头部位置
SNAKE.psnakenode->x = next->x;
SNAKE.psnakenode->y = next->y;
// 释放之前分配的next节点,因为我们只是移动了尾部节点
free(next);
}
IsDie();
PrintSnake(SNAKE.psnakenode);
}
//=========================================
//运行游戏
void ClearScreen() { // 定义清屏函数,适用于Windows和Linux/macOS
#ifdef _WIN32
system("cls");
#else
system("clear");
#endif
}
void GameOver(int score) {
// 游戏结束后的善后处理
ClearScreen(); // 清除屏幕
wprintf(L"Game Over!\n"); // 打印游戏结束信息
wprintf(L"总分: %d\n", score); // 显示玩家得分
// 询问玩家是否想再玩一次
wprintf(L"再来一次?(y/n): ");
system("pause >> nul");
if (_KEY_STATE(0x59)){
GameStart();
fflush(stdin);
}
else {
wprintf(L"感谢游玩\n");
exit(0); // 结束程序
}
}
void GameRun(Psnake**head)
{
do
{
set_pos(60, 6);
wprintf(L"当前分数: %d", SNAKE.score);
if ((_KEY_STATE(VK_UP) || _KEY_STATE(0x57)) && SNAKE.dir != DOWN)
{
SNAKE.dir = UP;
}
else if ((_KEY_STATE(VK_DOWN) || _KEY_STATE(0x53)) && SNAKE.dir != UP)
{
SNAKE.dir = DOWN;
}
else if ((_KEY_STATE(VK_LEFT) || _KEY_STATE(0x41)) && SNAKE.dir != RIGHT)
{
SNAKE.dir = LEFT;
}
else if ((_KEY_STATE(VK_RIGHT) || _KEY_STATE(0x44)) && SNAKE.dir != LEFT)
{
SNAKE.dir = RIGHT;
}
else if (_KEY_STATE(0x71)&&SNAKE.speed>=0&&SNAKE.speed<=500)
{
(SNAKE.speed) -= 30;
}
else if (_KEY_STATE(0x72)&&SNAKE.speed>=0&&SNAKE.speed<=500)
{
(SNAKE.speed) += 30;
}
else if (_KEY_STATE(0x50))
{
SNAKE.speed = 2147483647;
}
SnakeMove(head);
Sleep(SNAKE.speed);
} while (SNAKE.state == RUN);
GameOver(SNAKE.score);
}
//开始游戏及游戏前准备
void GameStart()
{
cover();
SNAKE.psnakenode = (Psnake*)malloc(sizeof(Psnake));
InitSnake(&SNAKE.psnakenode);
PrintSnake(SNAKE.psnakenode);
CreateFood(SNAKE.psnakenode);
set_pos(40, 36);
GameRun(&SNAKE.psnakenode);
}
1. set_pos( short x , short y )
void set_pos(short x, short y)
{
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
COORD coord;
coord.X = x;
coord.Y = y;
SetConsoleCursorPosition(hConsole, coord);
}//设置坐标
- 作用: 设置光标在控制台上的位置,以便在指定坐标输出字符。
- 思路: 使用GetStdHandle获得控制台光标句柄 ---> 修改值 ---> SetConsoleCursorPosition实现对光标的控制。
注:SetConsoleCursorPosition的使用需一个COORD结构体,将x和y改成目标值。
- 知识点: Windows API使用、句柄概念、坐标系统。
2. cursor ( bool state , short x )
//设置光标信息
void cursor(bool state, int x)
{
HANDLE op = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO con;
con.dwSize = x;
con.bVisible = state;
SetConsoleCursorInfo(op, &con);
}
- 作用: 控制光标的显示状态(可见或隐藏)及大小。
- 思路: 使用 GetStdHandle 获得光标句柄 ---> 进行值的修改 ---> 使用SetConsoleCursorInfo进行设置 。
注:SetConsoleCursorInfo需要定义 CONSOLE_CURSOR_INFO类型的结构体
- 知识点: 控制台编程、光标属性设置。
3. printInstruction( int y , const wchar_t* message )
//打印提示信息
void printInstruction(int y, const wchar_t* message) {
set_pos(60, y);
wprintf(message);
}
- 作用: 在指定的行上打印游戏的指令或提示信息。
- 思路: 调用
set_pos
定位光标,然后使用宽字符输出信息。 - 知识点: 宽字符输出、动态打印信息布局。
4. map( )
//打印地图
void map() {
// Draw top border
set_pos(0, 0);
for (int i = 0; i < 29; i++) {
wprintf(L"□");
}
// Draw left and right borders
for (int i = 1; i < 29; i++) {
set_pos(0, i);
wprintf(L"□");
set_pos(28 * 2, i); //右边的坐标(28*2 ,i)
wprintf(L"□");
}
// Draw bottom border
set_pos(0, 29);
for (int i = 0; i < 29; i++) {
wprintf(L"□");
}
const wchar_t* instructions[] = {
L"操作指南: 使用方向键←↑→↓",
L"速度控制: F2 加速, F3 减速",
L"暂停游戏: 按 P",
L"退出游戏: 按 ESC"
};
for (int i = 0; i < sizeof(instructions) / sizeof(instructions[0]); ++i) {
printInstruction(10 + i, instructions[i]);
}
}
- 作用: 绘制游戏的地图边界和游戏说明。
- 思路: 分别绘制上、下、左、右边界,并在固定位置打印游戏规则。
注:宽字符打印需要wprintf,wprintf( L " " ) ;
- 知识点: 循环结构、字符串输出、坐标计算。
5. cover ( )
//打印封面
void cover()
{
system("mode con cols=120 lines=50");
system("title 贪吃蛇");
set_pos(40, 10);
wprintf(L" \n");
wprintf(L" .-----------------. .-----------------. .----------------. .----------------. .---------------- .\n");
wprintf(L" | .--------------. || .--------------. || .--------------. || .--------------. || .--------------. |\n");
wprintf(L" | | _______ | || | ____ _____ | || | __ | || | ______ | || | ___ ____ | |\n");
wprintf(L" | | / ___ | | || ||_ \\ |_ _| | || | / \\ | || | .' ___ | | || | |_ ||_ _| | |\n");
wprintf(L" | | | (__ \\_| | || | | \\ | | | || | / /\\ \\ | || | / .' \\_| | || | | |_/ / | |\n");
wprintf(L" | | '.___`-. | || | | |\\ \\| | | || | / ____ \\ | || | | | | || | | __'. | |\n");
wprintf(L" | | |`\\____) | | || | _| |_\\ | |_ | || | / / \\ \\_ | || | \\ `.___.'\\ | || | _| | \\ \\_ | |\n");
wprintf(L" | | |_______.' | || ||_____|\\____| | || ||____| |____|| || | `._____.' | || | |____ || __|_| |\n");
wprintf(L" | | | || | | || | | || | | || | | |\n");
wprintf(L" | '--------------' || '--------------' || '--------------' || '--------------' || '--------------' |\n");
wprintf(L" '----------------' '------'---------' '----------------' '------'---------' '--'-------------'\n");
set_pos(45, 29);
system("pause");
system("cls");
map();
}
- 作用: 打印游戏封面。
- 思路: 使用特殊字符和格式化输出来设计一个吸引人的启动画面,随后清除屏幕并进入游戏。
- 知识点: 字符串操作、系统调用(改变窗口尺寸、标题)、多行输出。
6. void PrintFood(Food* pos)
//打印食物坐标
void PrintFood(Food* pos)
{
set_pos((*pos).x, (*pos).y);
wprintf(L"◆");
}
- 作用: 在指定位置打印食物图标。
- 思路: 移动光标至食物坐标,输出食物的符号。(Food 在 snake.h 会详解)
- 知识点: 结构体使用、函数传参。
7. CreateFood ( Psnake* snake )
//创造食物
void CreateFood(Psnake* snake)
{
Food* pos = (Food*)malloc(sizeof(Food));
if (!pos)
{
perror("MALLOC FAILED:");
return;
}
Psnake* cur = snake;
//生成随机数
srand((unsigned int)time(NULL));
again:
(*pos).x = (rand() % GAME_WIDTH + 2) * 2; // rand() % 27 会得到0到26之间的数,加2后变为2到28
(*pos).y = rand() % GAME_HEIGHT + 2; // rand() % 27 会得到0到26之间的数,加2后变为2到28
(*pos).score = 1;
while (cur)
{
if ((*pos).x == cur->x && (*pos).y == cur->y)
goto again;
cur = cur->next;
}
SNAKE.food = *pos;
PrintFood(pos);
}
- 作用: 随机生成食物并确保不与蛇身重叠。
- 思路: 使用 rand 随机生成食物 x、y 坐标,食物遍历贪吃蛇每个节点的坐标,然后更新食物位置信息。
- 知识点: 随机数生成、链表遍历、内存管理(malloc)。
8. FoodJudge(Psnake* next)
//判断下一个节点是否为食物
bool FoodJudge(Psnake* next)
{
return ((next->x == SNAKE.food.x) && (next->y == SNAKE.food.y)) ? true : false;
}
- 作用: 判断下一个节点位置是否为食物所在。
- 思路: 比较节点坐标与食物坐标。
- 知识点: 简单逻辑判断、结构体成员访问。
9.buynode(int x, int y)
//申请一个节点
Psnake* buynode(int x,int y)
{
Psnake* newnode = (Psnake*)calloc(1, sizeof(Psnake));
if (newnode == NULL)
{
perror("calloc:");
return NULL;
}
newnode->next = NULL;
newnode->x = x;
newnode->y = y;
return newnode;
}
- 作用: 创建一个新的蛇节点。
- 思路: 申请内存,初始化节点数据,链接到链表。
蛇按照方向进行移动,需要对蛇头的前,左右进行申请节点
- 知识点: 动态内存分配、链表操作。
10.InitSnake(Psnake**head)
//初始化蛇的信息
Psnake* InitSnake(Psnake**head)
{
if (!(head&&*head)) {
fprintf(stderr, "Memory allocation failed\n");
return NULL;
}
(*head) = (Psnake*)calloc(1, sizeof(Psnake));
// 初始化成员
(*head)->x = 6;
(*head)->y = 10;
(*head)->next = NULL;
Psnake* cur = *head;
if (!cur)
{
perror("head is nullptr");
return NULL;
}
int x = _SNAKE_LENTH_;
while (x--)
{
Psnake* newnode= buynode((cur->x)+2, cur->y);
newnode->next = cur;
cur = newnode;
}
SNAKE.dir = RIGHT;
SNAKE.psnakenode = cur;
SNAKE.state = RUN;
SNAKE.speed = 150;
return cur;
}
- 作用: 初始化蛇的结构,创建初始蛇身。
- 思路: 设置蛇头位置、按照长度申请节点,并设置蛇其他信息
- 知识点: 指针引用、循环构造链表、内存管理。
11.PrintSnake(Psnake* head)
//打印蛇
void PrintSnake(Psnake* head)
{
Psnake* cur = SNAKE.psnakenode;
while (cur)
{
set_pos(cur->x, cur->y);
wprintf(L"●");
cur = cur->next;
}
}
- 作用: 打印蛇身在屏幕上。
- 思路: 遍历链表,逐个输出蛇身位置的字符。
- 知识点: 链表遍历、光标移动打印。
12. IsDie()
void IsDie() {
Psnake* cur = SNAKE.psnakenode->next; // 从第二个节点开始
Psnake* prev = SNAKE.psnakenode; // 保存蛇头节点用于比较
while (cur->next) {
cur = cur->next;
if (cur->x == prev->x && cur->y == prev->y) { // 检查当前节点是否与之前的节点重合
SNAKE.state = KILL_BY_SELF;
return;
}
}
if (SNAKE.psnakenode->x == 56||SNAKE.psnakenode->x==0 ||SNAKE.psnakenode->y==0||SNAKE.psnakenode->y == 28) {
SNAKE.state = KILL_BY_WALL;
return;
}
}
- 作用: 检查游戏是否结束(撞墙或自食)。
- 思路: 检测是否撞到自身:将蛇头坐标与每个蛇身节点坐标相比较;撞墙:蛇头与游戏边界重叠。之后将蛇的状态修改。
- 知识点: 边界条件检测、循环逻辑。
13. SnakeMove(Psnake** head)
//蛇的移动
void SnakeMove(Psnake** head)
{
// 首先检查蛇头的下一个位置
Psnake* next = (Psnake*)calloc(1, sizeof(Psnake));
if (!next) {
perror("Failed to allocate memory for next snake node");
exit(EXIT_FAILURE);
}
// 根据当前方向设置蛇头的下一个位置
switch (SNAKE.dir)
{
case UP:
next->x = SNAKE.psnakenode->x;
next->y = SNAKE.psnakenode->y - 1;
break;
case DOWN:
next->x = SNAKE.psnakenode->x;
next->y = SNAKE.psnakenode->y + 1;
break;
case LEFT:
next->x = SNAKE.psnakenode->x - 2;
next->y = SNAKE.psnakenode->y;
break;
case RIGHT:
next->x = SNAKE.psnakenode->x + 2;
next->y = SNAKE.psnakenode->y;
break;
default:
free(next);
return; // 如果方向无效,不移动蛇
}
// 检查下一个位置是否有食物
if (FoodJudge(next))
{
SNAKE.score += SNAKE.food.score; // 增加得分
next->next = (*head); // 将新节点放在头部
*head = next; // 更新头指针
CreateFood(*head); // 生成新的食物
}
else
{
// 没有食物,移动蛇的尾部到头部
Psnake* tail = SNAKE.psnakenode;
Psnake* prev = NULL;
if (!(tail || prev))
{
perror("POINTER IS NULL:");
return;
}
// 找到尾节点和其前一个节点
while (tail->next) {
prev = tail;
tail = tail->next;
}
// 将尾部节点移动到头部
prev->next = NULL; // 将前一个节点的next设置为NULL,它现在是尾部
set_pos(tail->x, tail->y);
wprintf(L" ");
tail->next = SNAKE.psnakenode; // 将原尾部节点放在头部
SNAKE.psnakenode = tail; // 更新头指针
// 更新头部位置
SNAKE.psnakenode->x = next->x;
SNAKE.psnakenode->y = next->y;
// 释放之前分配的next节点,因为我们只是移动了尾部节点
free(next);
}
IsDie();
PrintSnake(SNAKE.psnakenode);
}
- 作用: 处理蛇的移动逻辑,包括食物检测和身体跟随。
- 思路:申请三个节点(蛇头前左右) 用switch对蛇的方向进行判断(先让蛇头走一步),判断该方向下一节点是否为食物,将蛇身剩下的节点逐个移动一个节点。
移动分两部分,蛇头移动和蛇身移动,先让蛇头走一步,蛇身每个节点进行移动
- 知识点: 方向判断、链表头节点更新、内存释放。
14.ClearScreen( )
void ClearScreen() { // 定义清屏函数,适用于Windows和Linux/macOS
#ifdef _WIN32
system("cls");
#else
system("clear");
#endif
}
- 作用: 清除屏幕内容。
- 思路: 根据操作系统调用不同的清屏命令。
#ifdef 与 #endif 要成对存在
- 知识点: 条件编译、系统调用。
15.GameOver( int score )
void GameOver(int score) {
// 游戏结束后的善后处理
ClearScreen(); // 清除屏幕
wprintf(L"Game Over!\n"); // 打印游戏结束信息
wprintf(L"总分: %d\n", score); // 显示玩家得分
// 询问玩家是否想再玩一次
wprintf(L"再来一次?(y/n): ");
system("pause >> nul");
if (_KEY_STATE(0x59)){
GameStart();
fflush(stdin);
}
else {
wprintf(L"感谢游玩\n");
exit(0); // 结束程序
}
}
- 作用: 游戏结束时显示分数,并询问是否重新开始。
- 思路: 显示最终得分,根据用户输入决定是否重启游戏。
system("pause>>nul"); 与 system("pause"); 效果类似,后者会打印按任意键继续字样,前者不会
- 知识点: 用户输入读取、流程控制。
16. GameRun(Psnake**head)
void GameRun(Psnake**head)
{
do
{
set_pos(60, 6);
wprintf(L"当前分数: %d", SNAKE.score);
if ((_KEY_STATE(VK_UP) || _KEY_STATE(0x57)) && SNAKE.dir != DOWN)
{
SNAKE.dir = UP;
}
else if ((_KEY_STATE(VK_DOWN) || _KEY_STATE(0x53)) && SNAKE.dir != UP)
{
SNAKE.dir = DOWN;
}
else if ((_KEY_STATE(VK_LEFT) || _KEY_STATE(0x41)) && SNAKE.dir != RIGHT)
{
SNAKE.dir = LEFT;
}
else if ((_KEY_STATE(VK_RIGHT) || _KEY_STATE(0x44)) && SNAKE.dir != LEFT)
{
SNAKE.dir = RIGHT;
}
else if (_KEY_STATE(0x71)&&SNAKE.speed>=0&&SNAKE.speed<=500)
{
(SNAKE.speed) -= 30;
}
else if (_KEY_STATE(0x72)&&SNAKE.speed>=0&&SNAKE.speed<=500)
{
(SNAKE.speed) += 30;
}
else if (_KEY_STATE(0x50))
{
SNAKE.speed = 2147483647;
}
SnakeMove(head);
Sleep(SNAKE.speed);
} while (SNAKE.state == RUN);
GameOver(SNAKE.score);
}
- 作用: 游戏主循环,处理用户输入和游戏逻辑。
- 思路: 此处do while效果要比while效果要好,通过检测检测按键状态来确定蛇的移动方向。
虚拟键码(Virtual Key Codes)是在计算机键盘输入中使用的一套编码标准,用于标识键盘上的每一个按键,无论键盘的实际物理布局如何。
- 知识点: 主循环控制、事件响应、时间延迟。
17. GameStart ()
//开始游戏及游戏前准备
void GameStart()
{
cover();
SNAKE.psnakenode = (Psnake*)malloc(sizeof(Psnake));
InitSnake(&SNAKE.psnakenode);
PrintSnake(SNAKE.psnakenode);
CreateFood(SNAKE.psnakenode);
set_pos(40, 36);
GameRun(&SNAKE.psnakenode);
}
- 作用: 游戏启动入口,包含初始化、封面展示和游戏开始。
- 思路: 先展示封面,初始化游戏环境,然后进入游戏主循环。
将蛇“创建”出来,作为游戏运行的主体
- 知识点: 函数调用顺序、程序入口点。
Review
整理一下思路:
-
需求分析
- 明确游戏目标:控制蛇吃食物变长,避免碰撞自身或边界。
- 确定基本功能:初始化游戏、绘制游戏界面、处理用户输入、蛇的移动与增长、食物的随机生成、碰撞检测、得分计算、游戏结束与重开机制。
-
技术选型与环境搭建
- 准备vs2022
-
核心模块设计
- 游戏界面:设计简洁直观的游戏界面,包括游戏区域、分数显示、游戏说明等。
- 蛇的实现:
- 数据结构:链表或数组来存储蛇身的各个部分。
- 移动逻辑:根据用户输入改变蛇头方向,自动延伸蛇身。
- 食物管理:随机位置生成食物,检测蛇头碰撞并重新生成。
- 碰撞检测:检查蛇头是否触碰边界或自身身体。
- 得分系统:每次吃到食物增加分数,记录最高分。
-
用户交互
- 接收用户输入(键盘/触摸),转换为游戏内操作指令。
- 提供开始新游戏、退出游戏等选项。
-
性能与优化
- 优化游戏循环,确保流畅运行。
- 考虑内存管理,避免内存泄漏。
在这个数字编织的宇宙🌌中,你已悄然绘制了一条属于自己的璀璨轨迹。
每一次思考与实践,都在为你的技能之树浇灌成长的甘露🌱。愿你带着这份宝贵的经历,继续遨游于代码的星辰大海,发现更多的奇遇与美好。
加油,程序员之旅的探险者们,前路广阔,星辰大海,我们共勉!🚀✨