贪吃蛇是一款很老的游戏了,大家一定都玩过,我们一起写个简单的贪吃蛇游戏吧。
本款贪吃蛇的规则如下:
1.不能撞自己
2.不能撞墙
3.蛇吃食物的时候,自身要长一截
4.可以通过用户控制对蛇加速和减速
5.更重要的一点是游戏当然得有加分。
这里我们可以从MVC的角度考虑(Model View Controller)
Model :数据结构/结构体,就是整个游戏的结构
View : UI Console/Web/MFC,在这里指用户界面,就是我们我们表面看到的
Controller : 控制流程 算法/函数,这里指的是整个游戏的运行
Model层,我们需要对游戏初始化,这就涉及了蛇的初始化以及食物的初始化了,我们规定蛇的初识长度为3,是以链表的方式进行存储,初始化蛇的时候要注意以上规则的前两点,而初始化食物也需要注意一下三点:
食物初始化的原则:
1.不能和蛇重叠
2.不能出墙
3.要有随机性
Model.h
#pragma once
#include
// 坐标结构体
typedef struct {
int x;
int y;
} Position;
// 链表的结点结构体
typedef struct Node {
Position pos;
struct Node *next;
} Node;
// 蛇的前进方向
typedef enum {
UP, RIGHT, DOWN, LEFT
} Direction;
// 蛇
typedef struct {
Node *head; // 蛇头结点,链表的第一个结点
Node *tail;
Direction direction;
} Snake;
// 游戏
typedef struct {
Snake snake;
Position food;
int width;
int height;
int speed;
int score;
} Game;
void GameInit(Game *pGame);
void GameDestroy(Game *pGame);
void FoodInit(Position *pFood, int width, int height,
const Snake *pSnake);
void SnakeAddHead(Snake *pSnake, Position nextPos);
void SnakeRemoveTail(Snake *pSnake);
Model.c
这里我们需要注意:蛇是以链表的形式存储的,但是蛇头是链表的尾,而蛇尾是链表的头,这里听起来可能有点绕,但理解起来并不是很困难。
#include "Model.h"
#include "View.h"
#include
#include
/*
static:
1. 修饰全局变量
修改链接属性 从外部修改成内部
2. 修饰局部变量
修改生命周期
3. 修饰函数
修改链接属性 从外部修改成内部
*/
static void SnakeInit(Snake *pSnake)
{//蛇的初始化
pSnake->direction = RIGHT;
pSnake->head = NULL;
for (int i = 0; i < 3; i++) {
int x = 3 + i;
int y = 3;
Node *node = (Node *)malloc(sizeof(Node));
node->pos.x = x;
node->pos.y = y;
node->next = pSnake->head;
pSnake->head = node;
}
}
// 本质上是链表的查找问题
static bool IsOverlapSnake(int x, int y, const Snake *pSnake)
{//看蛇是否会撞到自己
for (Node *cur = pSnake->head; cur != NULL; cur = cur->next) {
if (x == cur->pos.x && y == cur->pos.y) {
return true;
}
}
return false;
}
// 生成食物
// 1. 随机
// 2. 不能出框
// 3. 不能和蛇重叠
void FoodInit(Position *pFood, int width, int height, const Snake *pSnake)
{
int x;
int y;
do {
x = rand() % width;
y = rand() % height;
} while (IsOverlapSnake(x, y, pSnake));
pFood->x = x;
pFood->y = y;
DisplayFood(x, y);
}
void GameInit(Game *pGame)
{
pGame->width = 28;
pGame->height = 27;
SnakeInit(&pGame->snake);
FoodInit(&pGame->food, pGame->width, pGame->height, &pGame->snake);
pGame->speed = 300; //每个周期的间隔,单位是ms
pGame->score = 0;
}
static void SnakeDestroy(Snake *pSnake)
{
Node *cur, *next;
for (cur = pSnake->head; cur != NULL; cur = next) {
next = cur->next;
free(cur);
}
}
void GameDestroy(Game *pGame)
{
SnakeDestroy(&pGame->snake);
}
void SnakeAddHead(Snake *pSnake, Position nextPos)
{
Node *node = (Node *)malloc(sizeof(Node));
node->pos.x = nextPos.x;
node->pos.y = nextPos.y;
node->next = pSnake->head;
pSnake->head = node;
DisplaySnakeBlock(nextPos.x, nextPos.y);
}
void SnakeRemoveTail(Snake *pSnake)
{
// 删除最后一个结点,需要找到倒数第二个
// 不需要去判断链表中有几个结点,因为一开始就有 3 个
Node *cur;
for (cur = pSnake->head; cur->next->next != NULL; cur = cur->next) {
}
CleanSnakeBlock(cur->next->pos.x, cur->next->pos.y);
free(cur->next);
cur->next = NULL;
}
在这一层我们开始考虑运行游戏,以RunGame()展开,先获取蛇的方向,然后判断蛇是否会迟到食物,或者撞到自己,撞到墙…一系列情况。主要代码如下:
Controller.c
#include "Model.h"
#include "View.h"
#include
#include
static Position GetNextPosition(const Snake *pSnake)
{
Position nextPos = pSnake->head->pos;
switch (pSnake->direction) {
case UP:
nextPos.y -= 1;
break;
case RIGHT:
nextPos.x += 1;
break;
case DOWN:
nextPos.y += 1;
break;
case LEFT:
nextPos.x -= 1;
break;
}
return nextPos;
}
/*
inline 内联函数
*/
static bool IsEaten(Position nextPos, Position food)
{
return nextPos.x == food.x && nextPos.y == food.y;
}
static bool IsHitWall(Position nextPos, int width, int height)
{
if (nextPos.x < 0) {
// 撞左边了
return true;
}
if (nextPos.x >= width) {
// 撞右边了
return true;
}
if (nextPos.y < 0) {
// 撞上边了
return true;
}
if (nextPos.y >= height) {
// 撞下边了
return true;
}
return false;
}
static bool IsHitSelf(Position nextPos, Snake *pSnake)
{
for (Node *cur = pSnake->head->next; cur != NULL; cur = cur->next) {
if (cur->pos.x == nextPos.x && cur->pos.y == nextPos.y) {
return true;
}
}
return false;
}
void RunGame()
{
Game game;
GameInit(&game);
ViewInit(game.width, game.height);
DisplayWall(game.width, game.height);
DisplaySnake(&game.snake);
int count = 0;
while (1) {
// 控制方向
if (GetAsyncKeyState(VK_UP) && game.snake.direction != DOWN) {
game.snake.direction = UP;
}
else if (GetAsyncKeyState(VK_DOWN) && game.snake.direction != UP) {
game.snake.direction = DOWN;
}
else if (GetAsyncKeyState(VK_LEFT) && game.snake.direction != RIGHT) {
game.snake.direction = LEFT;
}
else if (GetAsyncKeyState(VK_RIGHT) && game.snake.direction != LEFT) {
game.snake.direction = RIGHT;
}
else if (GetAsyncKeyState(VK_F1)) {
game.speed -= 100;
}
else if (GetAsyncKeyState(VK_F2)) {
game.speed += 100;
}
Position nextPos = GetNextPosition(&game.snake);
if (IsEaten(nextPos, game.food)) {
SnakeAddHead(&game.snake, nextPos);
game.score += 10;
if (game.speed >= 100) {
game.speed -= 10;
}
FoodInit(&game.food, game.width, game.height, &game.snake);
}
else {
SnakeAddHead(&game.snake, nextPos);
SnakeRemoveTail(&game.snake);
}
if (IsHitWall(nextPos, game.width, game.height)) {
break;
}
if (IsHitSelf(nextPos, &game.snake)) {
break;
}
Sleep(game.speed);
}
SetCurPos(78, 20);
printf("玩家得分:%d", game.score);
GameDestroy(&game);
}
int main()
{
RunGame();
return 0;
}
在这一层,我们将实现游戏的美观,就是简单的呈现出一个界面(虽然界面有点low),我们会看到墙,看到蛇,看到食物。
注意,我们坐标是x轴正方向向右,y轴正方向向下,如下图:
View.c
#include "Model.h"
#include
#include
void SetCurPos(int X, int Y)
{
HANDLE hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
// 标准输入/标准输出/标准错误输出
COORD coord = { X, Y };
SetConsoleCursorPosition(hStdOutput, coord);
}
void DisplayWall(int width, int height)
{
SetCurPos(0, 0);
for (int i = 0; i < width + 2; i++)
{
printf("█");
}
SetCurPos(0, height + 1);
for (int i = 0; i < width + 2; i++)
{
printf("█");
}
for (int i = 0; i < height + 2; i++)
{
SetCurPos(0, i);
printf("█");
}
for (int i = 0; i < height + 2; i++)
{
SetCurPos(2 * (width + 1), i);
printf("█");
}
SetCurPos(74, 8);
printf("※欢迎来到贪吃蛇小游戏......");
SetCurPos(78, 10);
printf("请按↑ ↓ ← →控制方向");
SetCurPos(78, 12);
printf(" F1加速 F2减速 ");
SetCurPos(78, 14);
printf(" ESC退出游戏! ");
}
void DisplayFood(int x, int y)
{
SetCurPos(2*(x + 1), y + 1);
printf("◎");
}
void DisplaySnake(const Snake *pSnake)
{
for (Node *cur = pSnake->head; cur != NULL; cur = cur->next) {
SetCurPos(2*cur->pos.x+1, cur->pos.y+1);
printf("▇");
}
}
void DisplaySnakeBlock(int x, int y)
{
SetCurPos(2*(x+1), y+1);
printf("▇");
}
void CleanSnakeBlock(int x, int y)
{
SetCurPos(2*(x+1), y+1); // 先统一两个坐标系
printf(" ");
}
void ViewInit(int width, int height)
{
HANDLE hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO info;
GetConsoleCursorInfo(hStdOutput, &info);
info.bVisible = 0;
SetConsoleCursorInfo(hStdOutput, &info);
}
在整个过程中,我们用到了一些新的函数,例如:控制方向的,这就涉及到了句柄的概念,还有隐藏光标的,放置光标的函数,这些不用深究,简单了解即可。