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

本文详细介绍了贪吃蛇游戏的设计与实现过程,包括坐标系定义、蛇和食物的属性设计、地图场景构建等内容,并提供了游戏逻辑处理及控制台绘制的具体实现。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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

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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值