首页 > 代码库 > 游戏开发(一)——控制台 贪吃蛇
游戏开发(一)——控制台 贪吃蛇
贪吃蛇游戏设计中主要需要注意的几点:
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:蛇自身的定时移动,就是人不控制的时候,依然朝着原方向移动,这里用到了计时,比如没有按键的情况下,100毫秒移动一步。这个是之前写的计时器
7:游戏主循环逻辑大致如下:
//初始化 while(true) { //假定100毫秒处理一次游戏逻辑 if(100毫秒间隔时间到) { if(玩家有键盘操作) { if(方向键 且 游戏正在进行中没有结束) //蛇按玩家操作的方向移动 elseif(ESC键) //跳出循环 else 玩家按的是其他无效按键 且 游戏正在进行中没有结束 //蛇按当前方向自己移动 } else { //游戏正在进行中没有结束,蛇按当前方向自己移动 } } else 100毫秒间隔没到 { sleep } }
游戏截图:
下面先给出代码
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(); void 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(); } } void Snake::Turn(int forward) { if (abs(m_forward) != abs(forward)) { m_forward = forward; } } 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; }
Scene.h:
#ifndef _Scene_ #define _Scene_ #include "TArray.h" #include "TRandPool.h" #include "Snake.h" const int MAP_MAX_ROW = 20; const int MAP_MAX_COLUMN = 40; const int MAX_FOOD = 3; // 且小于MAP_MAX_ROW, 且小于MAP_MAX_COLUMN typedef struct Food { int column_x; int row_y; bool valid; }Food; const WCHAR WS_GRID = L'□'; const WCHAR WS_SNAKE_BODY = L'■'; const WCHAR WS_FOOD = L'◆'; class Scene { public: void InitMap(); void InitSnake(); void RandFood(); bool IsSnakeDeath(); TArray1<TArray1<WCHAR, MAP_MAX_COLUMN>, MAP_MAX_ROW>& GetMap1(); TArray1<TArray1<WCHAR, MAP_MAX_COLUMN>, MAP_MAX_ROW>& GetMap2(); bool ProcessLogic(); bool ProcessLogic(int forward); private: TArray1<TArray1<WCHAR, MAP_MAX_COLUMN>, MAP_MAX_ROW> m_map1; TArray1<TArray1<WCHAR, MAP_MAX_COLUMN>, MAP_MAX_ROW> m_map2;//双缓冲,用于对比改变了的位置,防止闪屏 TArray1<Food, MAX_FOOD> m_food; TRandPool<MAP_MAX_COLUMN * MAP_MAX_ROW> m_food_xy_pool; Snake m_snake; }; #endif
Scene.cpp:
#include "Scene.h" void Scene::InitMap() { for (int y = 0; y < MAP_MAX_ROW; y++) { for (int x = 0; x< MAP_MAX_COLUMN; x++) { m_map1[y][x] = WS_GRID; } } } void Scene::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 % MAP_MAX_COLUMN; int row_y = num / MAP_MAX_COLUMN; if (!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_FOOD; foodcount++; if (MAX_FOOD == foodcount) { break; } } } } void Scene::InitSnake() { m_snake.AddBody(0, 7); m_snake.AddBody(1, 7); m_snake.AddBody(2, 7);//AddBody是头插入,最后一个是蛇头 m_map1[7][2] = WS_SNAKE_BODY; m_map1[7][1] = WS_SNAKE_BODY; m_map1[7][0] = WS_SNAKE_BODY; } TArray1<TArray1<WCHAR, MAP_MAX_COLUMN>, MAP_MAX_ROW>& Scene::GetMap1() { return m_map1; } TArray1<TArray1<WCHAR, MAP_MAX_COLUMN>, MAP_MAX_ROW>& Scene::GetMap2() { return m_map2; } bool Scene::IsSnakeDeath() { int newX = 0; int newY = 0; m_snake.GetDstXY(newX, newY); if (newX < 0 || newX >= MAP_MAX_COLUMN || newY < 0 || newY >= MAP_MAX_ROW || m_snake.IsBody(newX, newY)) { return true; } return false; } bool Scene::ProcessLogic() { return ProcessLogic(m_snake.GetForward()); }; bool Scene::ProcessLogic(int forward) { m_snake.Turn(forward); 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 < 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_GRID; m_map1[newY][newX] = WS_SNAKE_BODY; } bool needRandFood = true; for (int i = 0; i < 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 "Scene.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; Scene g_Scene; void DrawMap(HANDLE hOut) { COORD pos = {0, 0}; for (int y = 0; y < MAP_MAX_ROW; y++) { for (int x = 0; x< MAP_MAX_COLUMN; x++) { if (g_Scene.GetMap1()[y][x] != g_Scene.GetMap2()[y][x]) { pos.X = x * 2; pos.Y= y; FillConsoleOutputCharacter(hOut, g_Scene.GetMap1()[y][x], 2, pos, NULL); g_Scene.GetMap2()[y][x] = g_Scene.GetMap1()[y][x]; } } } } int main() { HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_SCREEN_BUFFER_INFO bInfo; GetConsoleScreenBufferInfo(hOut, &bInfo ); g_Scene.InitMap(); g_Scene.InitSnake(); g_Scene.RandFood(); DrawMap(hOut); CPerformance perf;//计时器 perf.Start(); float times = 0.0f; bool bRes = true;//游戏是否结束的标志,true为进行中,false为结束 while (true) { times = perf.End(); if (times > 100) { perf.Start(); if(_kbhit()) { int ch = _getch(); if (bRes && KEY_FORWARD == ch) { ch = _getch(); switch (ch) { case KEY_UP: bRes = g_Scene.ProcessLogic(SNAKE_UP); break; case KEY_DOWN: bRes = g_Scene.ProcessLogic(SNAKE_DOWN); break; case KEY_LEFT: bRes = g_Scene.ProcessLogic(SNAKE_LEFT); break; case KEY_RIGHT: bRes = g_Scene.ProcessLogic(SNAKE_RIGHT); break; } } else if (KEY_ESC == ch) { break; } else { if (bRes) { bRes = g_Scene.ProcessLogic(); } fflush(stdin); } } else { if (bRes) { bRes = g_Scene.ProcessLogic(); } } DrawMap(hOut); if (!bRes) { WCHAR info[100] = {0}; wcscpy_s(info, 100, L"game over, press any key to exit."); for (size_t i = 0; i < wcslen(info); i++) { COORD pos = {i, MAP_MAX_ROW + 2}; FillConsoleOutputCharacter(hOut, info[i], 1, pos, NULL); } } } else { Sleep(1); } } CloseHandle(hOut); return 0; }