首页 > 代码库 > 游戏开发(一)——控制台 贪吃蛇

游戏开发(一)——控制台 贪吃蛇

贪吃蛇游戏设计中主要需要注意的几点:

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;
}