贪吃蛇游戏设计中主要需要注意的几点:
1:坐标的定义:定义为左上角为(0,0),向右为x正方向,向下为y正方向
2:蛇的设计,
蛇身:m_body,这里用的是链表(是之前写好的双链表),一个节点就是蛇身的一节
每节蛇身的属性包括x,y坐标:column_x,row_y,x表示在地图上的第几列,y表示在地图上的第几行
蛇有一个属性叫朝向,也就是当前在往上、下、左、右的哪个方向移动:m_forward
蛇的动作有:Turn,转向。转向有个判断,就是不能向相反的方向转,比如本来向上运动,按向下键,是无效的。
MoveTo,移动到某个位置(通常就是m_forward方向上,蛇头的下一个位置)。MoveTo从逻辑上来说应该是每节蛇身的坐标依次前进一位,这里可以用取巧的做法,就是将最后一节蛇尾卸下来,接到蛇头之前,变成新的蛇头。使得看起来就像是整只蛇前进了一步。这样只需要改变这一节节点的坐标就可以了,效率比较高。
AddBody,吃到一个食物,蛇身要增加一节。
IsBody,判断某个位置是否已经被蛇身占了。蛇身不能交叉前进,因此需要判断。GetDstXY,获得蛇在m_forward方向上前进,下一步所要到达的位置。
GetHeadXY,获得当前蛇头的位置
GetTailXY,获得当前蛇尾的位置
3:食物的设计:
食物的属性包括column_x,row_y,x表示在地图上的第几列,y表示在地图上的第几行,以及一个valid,表示这个食物是否已经被蛇吃掉了
食物通常是一次出来3个,且不能几个食物出现在同一个位置。因此需要注意生成的随机数不能重复。
这里用到了之前写的随机数池,保证生成n个不重复的随机数。
此外食物出现的位置不能出现在地图边框上,不能已经被蛇身占住。
4:地图场景的设计,地图一般是个二维数组(你用一维表示也可以),在控制台下,每个元素可以用一个字符表示:' '表示空地,'□'表示蛇身,'■';表示食物,'◆'表示边框
这个二维数组用到了之前的数组模板
这里采用两个二维数组m_map1,和m_map2作为双缓冲,可以比对游戏前后的变化,每次游戏画面的更新,只重绘改变的部分,这样可以防止全屏重绘导致的闪屏。
此外,地图上还包括两个成员:就是之前提到的食物m_food(食物有多个,所以是个数组),一个用于随机食物坐标的随机数池m_food_xy_pool
还有就是蛇m_snake
场景提供的功能主要有:RandFood,随机生成位置不重复,且不被蛇身占住的食物
ProcessLogic,处理用户按下上、下、左、右时,蛇朝向某个方向移动一步的逻辑,包括:是否能移动(会不会移出地图边界的判断,会不会蛇头撞到蛇身的判断),会不会吃到食物的判断(吃到食物蛇身长度增加的逻辑处理),食物是否被吃完的判断(吃完则需要继续生成食物的逻辑处理)
IsSnakeDeath,判断蛇是否死了,即蛇移出地图边界了,或者蛇头碰到蛇身了
5:由于是控制台的程序,地图,蛇,食物都是字符表示的,就涉及到一个问题:如何将字符绘制到指定的位置上去
这里用到了FillConsoleOutputCharacter
如何检测上、下、左、右的按键,以及推出(ESC),这里用到了两个函数
_kbhit,和_getch
具体函数功能就不介绍了,可以google
6:蛇自身的定时移动,就是人不控制的时候,依然朝着原方向移动,这里用到了计时,比如,300毫秒移动一步。这个是之前写的计时器
7:游戏主循环逻辑大致如下:
//初始化
while(true)
{
if (玩家有键盘操作)
{
if (ESC键)
{
//跳出循环
}
else if (方向键)
{
if (游戏正在进行中没有结束)
{
//蛇按玩家操作的方向移动
}
}
else //其他按键
{
//不处理
}
}
//假定300毫秒处理一次游戏逻辑
//就是不管人是否控制的情况下,每300毫秒蛇身固定移动一步
if (300毫秒间隔时间到)
{
if (游戏正在进行中没有结束)
{
//蛇按默认方向移动
}
}
if (游戏结束)
{
//显示游戏结束
}
sleep
}
7.15修改:增加了游戏边框的显示,重新定义了SnakeScene中的一些常量,修改了主逻辑流程
游戏截图:
下面先给出代码
Snake.h:
#ifndef _Snake_
#define _Snake_
#include <windows.h>
#include "TBDLinkList.h"
const int SNAKE_UP = 1;
const int SNAKE_DOWN = -1;
const int SNAKE_LEFT = 2;
const int SNAKE_RIGHT = -2;
typedef struct SnakeBody
{
int column_x;
int row_y;
}SnakeBody;
class Snake
{
public:
Snake();
~Snake();
bool Turn(int forward);
int GetForward();
void GetTailXY(int& column_x, int& row_y);
void GetHeadXY(int& column_x, int& row_y);
void GetDstXY(int& column_x, int& row_y);
void MoveTo(int column_x, int row_y);
void AddBody(int column_x, int row_y);
bool IsBody(int column_x, int row_y);
private:
int m_forward;
TBDLinkList<SnakeBody> m_body;
};
#endif
Snake.cpp:
#include "Snake.h"
Snake::Snake()
{
m_forward = SNAKE_RIGHT;
}
Snake::~Snake()
{
TBDLinker<SnakeBody> *pLinker = m_body.PopHead();
while(NULL != pLinker)
{
delete pLinker;
pLinker = m_body.PopHead();
}
}
bool Snake::Turn(int forward)
{
if (m_forward == forward)
{
return true;
}
else
{
if (abs(m_forward) == abs(forward))
{
return false;
}
else
{
m_forward = forward;
return true;
}
}
}
int Snake::GetForward()
{
return m_forward;
}
void Snake::GetTailXY(int& column_x, int& row_y)
{
SnakeBody *pTail = m_body.GetTail();
if (NULL == pTail)
{
return ;
}
column_x = pTail->column_x;
row_y = pTail->row_y;
}
void Snake::GetHeadXY(int& column_x, int& row_y)
{
SnakeBody *pHead = m_body.GetHead();
if (NULL == pHead)
{
return ;
}
column_x = pHead->column_x;
row_y = pHead->row_y;
}
void Snake::GetDstXY(int& column_x, int& row_y)
{
SnakeBody *pHead = m_body.GetHead();
if (NULL == pHead)
{
return ;
}
int oldX = pHead->column_x;
int oldY = pHead->row_y;
switch(m_forward)
{
case SNAKE_UP:
oldY--;
break;
case SNAKE_DOWN:
oldY++;
break;
case SNAKE_LEFT:
oldX--;
break;
case SNAKE_RIGHT:
oldX++;
break;
default:
break;
}
column_x = oldX;
row_y = oldY;
}
void Snake::MoveTo(int column_x, int row_y)
{
TBDLinker<SnakeBody> *pTail = m_body.PopTail();
if (NULL != pTail)
{
pTail->m_Value.column_x = column_x;
pTail->m_Value.row_y = row_y;
pTail->m_pLinkList = NULL;
m_body.PushHead(pTail);
}
}
void Snake::AddBody(int column_x, int row_y)
{
TBDLinker<SnakeBody> *pLinker = new TBDLinker<SnakeBody>;
if (NULL != pLinker)
{
pLinker->m_Value.column_x = column_x;
pLinker->m_Value.row_y = row_y;
pLinker->m_pLinkList = NULL;
m_body.PushHead(pLinker);
}
}
bool Snake::IsBody(int column_x, int row_y)
{
TIterator<SnakeBody> iter;
iter.Register(m_body);
iter.BeginList();
SnakeBody *pBody = iter.GetNext();
while (NULL != pBody)
{
if (pBody->column_x == column_x && pBody->row_y == row_y)
{
return true;
}
pBody = iter.GetNext();
}
return false;
}
SnakeScene.h:
#ifndef _Snake_Scene_
#define _Snake_Scene_
#include "TArray.h"
#include "TRandPool.h"
#include "Snake.h"
const int SNAKE_MAX_FOOD = 3;
typedef struct Food
{
int column_x;
int row_y;
bool valid;
}Food;
//游戏区域在Map中的位置,行数,列数
const int SNAKE_GAME_ROW_Y = 1;
const int SNAKE_GAME_COLUMN_X = 1;
const int SNAKE_GAME_MAX_ROW = 20;
const int SNAKE_GAME_MAX_COLUMN = 30;
//整个Map的行数,列数
//多2行是上下边框
//多2列是左右边框
const int SNAKE_SCENE_MAX_ROW = 1 + SNAKE_GAME_MAX_ROW + 1;
const int SNAKE_SCENE_MAX_COLUMN = 1 + SNAKE_GAME_MAX_COLUMN + 1;
//空白场景格
const WCHAR WS_SNAKE_SCENE_GRID = L' ';
//蛇身
const WCHAR WS_SNAKE_BODY = L'□';
//食物
const WCHAR WS_SNAKE_FOOD = L'■';
//场景边框格
const WCHAR WS_SNAKE_SCENE_FRAME = L'◆';
class SnakeScene
{
public:
SnakeScene();
void InitMap();
void InitSnake();
void RandFood();
bool IsSnakeDeath();
TArray1<TArray1<WCHAR, SNAKE_SCENE_MAX_COLUMN>, SNAKE_SCENE_MAX_ROW>& GetMap1();
TArray1<TArray1<WCHAR, SNAKE_SCENE_MAX_COLUMN>, SNAKE_SCENE_MAX_ROW>& GetMap2();
bool ProcessLogic();
bool ProcessLogic(int forward);
private:
TArray1<TArray1<WCHAR, SNAKE_SCENE_MAX_COLUMN>, SNAKE_SCENE_MAX_ROW> m_map1;
TArray1<TArray1<WCHAR, SNAKE_SCENE_MAX_COLUMN>, SNAKE_SCENE_MAX_ROW> m_map2;//双缓冲,用于对比改变了的位置,防止闪屏
TArray1<Food, SNAKE_MAX_FOOD> m_food;
TRandPool<SNAKE_SCENE_MAX_COLUMN * SNAKE_SCENE_MAX_ROW> m_food_xy_pool;
Snake m_snake;
};
#endif
SnakeScene.cpp:
#include "SnakeScene.h"
SnakeScene::SnakeScene()
{
InitMap();
InitSnake();//InitSnake在RandFood之前,否则食物的位置可能被蛇身占住
RandFood();
}
void SnakeScene::InitMap()
{
for (int y = 0; y < SNAKE_SCENE_MAX_ROW; y++)
{
for (int x = 0; x< SNAKE_SCENE_MAX_COLUMN; x++)
{
m_map1[y][x] = WS_SNAKE_SCENE_FRAME;
}
}
for (int y = SNAKE_GAME_ROW_Y; y < SNAKE_GAME_ROW_Y + SNAKE_GAME_MAX_ROW; y++)
{
for (int x = SNAKE_GAME_COLUMN_X; x< SNAKE_GAME_COLUMN_X + SNAKE_GAME_MAX_COLUMN; x++)
{
m_map1[y][x] = WS_SNAKE_SCENE_GRID;
}
}
}
void SnakeScene::InitSnake()
{
m_snake.AddBody(1, 7);
m_snake.AddBody(2, 7);
m_snake.AddBody(3, 7);//AddBody是头插入,最后一个是蛇头
m_map1[7][3] = WS_SNAKE_BODY;
m_map1[7][2] = WS_SNAKE_BODY;
m_map1[7][1] = WS_SNAKE_BODY;
}
void SnakeScene::RandFood()
{
m_food_xy_pool.Rand();
int foodcount = 0;
for(unsigned int i = 0; i < m_food_xy_pool.Capacity(); i++)
{
int num = m_food_xy_pool[i];
int column_x = num % SNAKE_SCENE_MAX_COLUMN;
int row_y = num / SNAKE_SCENE_MAX_COLUMN;
if (m_map1[row_y][column_x] != WS_SNAKE_SCENE_FRAME &&
!m_snake.IsBody(column_x, row_y))
{
m_food[foodcount].column_x = column_x;
m_food[foodcount].row_y = row_y;
m_food[foodcount].valid = true;
m_map1[row_y][column_x] = WS_SNAKE_FOOD;
foodcount++;
if (SNAKE_MAX_FOOD == foodcount)
{
break;
}
}
}
}
TArray1<TArray1<WCHAR, SNAKE_SCENE_MAX_COLUMN>, SNAKE_SCENE_MAX_ROW>& SnakeScene::GetMap1()
{
return m_map1;
}
TArray1<TArray1<WCHAR, SNAKE_SCENE_MAX_COLUMN>, SNAKE_SCENE_MAX_ROW>& SnakeScene::GetMap2()
{
return m_map2;
}
bool SnakeScene::IsSnakeDeath()
{
int newX = 0;
int newY = 0;
m_snake.GetDstXY(newX, newY);
if (newX < SNAKE_GAME_COLUMN_X ||
newX >= SNAKE_GAME_COLUMN_X + SNAKE_GAME_MAX_COLUMN ||
newY < SNAKE_GAME_ROW_Y ||
newY >= SNAKE_GAME_ROW_Y + SNAKE_GAME_MAX_ROW||
m_snake.IsBody(newX, newY))
{
return true;
}
return false;
}
bool SnakeScene::ProcessLogic()
{
return ProcessLogic(m_snake.GetForward());
};
bool SnakeScene::ProcessLogic(int forward)
{
if (!m_snake.Turn(forward))
{
return true;
}
if (IsSnakeDeath())
{
return false;
}
int oldX = 0;
int oldY = 0;
m_snake.GetTailXY(oldX, oldY);
int newX = 0;
int newY = 0;
m_snake.GetDstXY(newX, newY);
bool isEat = false;
for (int i = 0; i < SNAKE_MAX_FOOD; i++)
{
if (m_food[i].valid)
{
if (newX == m_food[i].column_x && newY == m_food[i].row_y)
{
m_snake.AddBody(newX, newY);
m_map1[newY][newX] = WS_SNAKE_BODY;
m_food[i].valid = false;
isEat = true;
break;
}
}
}
if (!isEat)
{
m_snake.MoveTo(newX, newY);
m_map1[oldY][oldX] = WS_SNAKE_SCENE_GRID;
m_map1[newY][newX] = WS_SNAKE_BODY;
}
bool needRandFood = true;
for (int i = 0; i < SNAKE_MAX_FOOD; i++)
{
if (m_food[i].valid)
{
needRandFood = false;
break;
}
}
if (needRandFood)
{
RandFood();
}
return true;
}
main.cpp:
#include <stdio.h>
#include <windows.h>
#include <conio.h>
#include "TArray.h"
#include "TRandPool.h"
#include "Snake.h"
#include "SnakeScene.h"
#include "CPerformance.h"
const unsigned char KEY_FORWARD = 224;
const unsigned char KEY_UP = 72;
const unsigned char KEY_DOWN = 80;
const unsigned char KEY_LEFT = 75;
const unsigned char KEY_RIGHT = 77;
const unsigned char KEY_ESC = 27;
SnakeScene g_SnakeScene;
const int SCENE_ROW_Y = 0;
const int SCENE_COLUMN_X = 5;
const int SCENE_MAX_ROW = SNAKE_SCENE_MAX_ROW;
const int SCENE_MAX_COLUMN = SNAKE_SCENE_MAX_COLUMN;
const int LOGIC_UP = SNAKE_UP;
const int LOGIC_DOWN = SNAKE_DOWN;
const int LOGIC_LEFT = SNAKE_LEFT;
const int LOGIC_RIGHT = SNAKE_RIGHT;
void DrawMap(HANDLE hOut)
{
COORD pos = {0, 0};
for (int y = 0; y < SCENE_MAX_ROW; y++)
{
for (int x = 0; x< SCENE_MAX_COLUMN; x++)
{
if (g_SnakeScene.GetMap1()[y][x] != g_SnakeScene.GetMap2()[y][x])
{
pos.X = x * 2 + SCENE_COLUMN_X;
pos.Y= y + SCENE_ROW_Y;
FillConsoleOutputCharacter(hOut, g_SnakeScene.GetMap1()[y][x], 2, pos, NULL);
g_SnakeScene.GetMap2()[y][x] = g_SnakeScene.GetMap1()[y][x];
}
}
}
}
int main()
{
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFO bInfo;
GetConsoleScreenBufferInfo(hOut, &bInfo );
DrawMap(hOut);
CPerformance perf;//计时器
perf.Start();
float times = 0.0f;
bool bRes = true;//游戏是否结束的标志,true为进行中,false为结束
while (true)
{
if(_kbhit())
{
int ch = _getch();
if (KEY_ESC == ch)
{
break;
}
else if (KEY_FORWARD == ch)
{
if (bRes)
{
ch = _getch();
switch (ch)
{
case KEY_UP:
bRes = g_SnakeScene.ProcessLogic(LOGIC_UP);
break;
case KEY_DOWN:
bRes = g_SnakeScene.ProcessLogic(LOGIC_DOWN);
break;
case KEY_LEFT:
bRes = g_SnakeScene.ProcessLogic(LOGIC_LEFT);
break;
case KEY_RIGHT:
bRes = g_SnakeScene.ProcessLogic(LOGIC_RIGHT);
break;
}
DrawMap(hOut);
}
}
}
times = perf.End();
if (times > 300)
{
perf.Start();
if (bRes)
{
bRes = g_SnakeScene.ProcessLogic();
DrawMap(hOut);
}
}
if (!bRes)
{
WCHAR info[100] = {0};
wcscpy_s(info, 100, L"game over, press ESC key to exit.");
for (size_t i = 0; i < wcslen(info); i++)
{
COORD pos = {i, SCENE_MAX_ROW + SCENE_ROW_Y + 2};
FillConsoleOutputCharacter(hOut, info[i], 1, pos, NULL);
}
}
Sleep(1);
}
CloseHandle(hOut);
return 0;
}