从零到屎山系列-游戏开发(Day2)

简介

这次就来一个比较简单的小游戏贪吃蛇

贪吃蛇

游戏规则就是一串珠子不断的移动,碰到场景里面的食物变长一点,碰到墙壁游戏结束。

开始动手

设计绘制设备

首先我计划从一个控制台游戏开始,需要一个控制台下的绘图机制,希望是在一个网格上面可以根据输入坐标来显示各种元素。就像下图这样,每个方块或者圆圈就好比是一个像素,我们可以通过坐标位置来控制像素绘制的位置,通过输入每个像素的属性来控制画出来的是方块还是圆圈还是空白。

首先定义好每个像素快的属性,属性中有两个描述,一个是颜色,一个是图案类别,目前我们有三种,分别是方块,圆圈和空白。定义如下。

enum PixelColor
{
	White = 0
};

enum PixelType
{
	None = 0,
	Square,
	Circle,
};

struct ScreenAttribute
{
	PixelType mPixel = PixelType::None;
	PixelColor mColor = PixelColor::White;
};

然后定义一个虚拟设备(BlockDisplayDevice)负责在控制台绘制东西,如下代码片段,通过向mRenderQueue中输入绘制信息,绘制设备每帧都会通过读取mRenderQueue中的内容填充到mBlockBuffer中,通过mPixelMap来映射最终画在屏幕上的到底是什么字符。

struct RenderElement
{
	ivec2 mPosition;
	ScreenAttribute mAttribute;
};

class BlockDisplayDevice
{
	std::vector<std::vector<ScreenAttribute>> mBlockBuffer;
	std::vector<std::string> mPixelMap = { "  ","■", "●" };
	std::vector<RenderElement> mRenderQueue;
public:
    
    ...

    void Display()
	{
		for (auto& block : mRenderQueue)
		{
			int x = block.mPosition.mX;
			int y = block.mPosition.mY;
			if (x < mBlockBuffer.size() && y < mBlockBuffer[x].size())
			{
				mBlockBuffer[block.mPosition.mX][block.mPosition.mY] = block.mAttribute;
			}
		}

		for (int i = 0; i < mBlockBuffer.size(); i++)
		{
			for (int j = 0; j < mBlockBuffer[i].size(); j++)
			{
				std::cout << mPixelMap[mBlockBuffer[i][j].mPixel];
			}

			std::cout << std::endl;
		}

		mRenderQueue.clear();
	}
};

定义游戏元素

游戏元素有三种,蛇、障碍物、食物,这三种元素对于游戏来说都是普通的物件,可以被绘制,也可以有处理逻辑,所以需要一类概念来表示这些物件,这里称之为GameObject,如下代码片段。因为三个物件都是可以被绘制的,所以定义了RenderObject来表示绘制信息,这样就可以在绘制不同物件的时候只需要关注每个物件中的RenderObject中的内容,拿了数据就可以无脑绘制了。

struct RenderObject
{
	PixelType mPixel = PixelType::None;
	PixelColor mColor = PixelColor::White;
	std::vector<ivec2> mPositions;
};

class GameObject
{
protected:
	RenderObject mRenderObject;
public:
	GameObject() {}
	RenderObject& GetRenderObject()
	{
		return mRenderObject;
	}
};

然后就可以从这个类派生出我们需要类来表示我们的游戏元素,如下代码片段,Food需要一个position来表示位置,Snake中一个队列来表示蛇,贪吃蛇游戏就是典型的队列数据结构的应用,我们先把更新逻辑放在Update中。至于障碍物目前没有什么功能,所以就先用GameObject基类表示。

class Food : public GameObject
{
	ivec2 mPosition;
public:
    ...
};

class Snake : public GameObject
{
	std::queue<ivec2> mPositions;
	bool mNeedGrow = false;
public:
    ...

    void Update()
	{
		// Update logic
		auto head = mPositions.back();
		head += ivec2::Right();
		if (!mNeedGrow)
			mPositions.pop();
		mPositions.push(head);
		mNeedGrow = false;

		// Update render data
		mRenderObject.mPositions.clear();
		int queueSize = mPositions.size();
		for (int i = 0; i < queueSize; i++)
		{
			auto pos = mPositions.front();
			mRenderObject.mPositions.push_back(pos);
			mPositions.push(pos);
			mPositions.pop();
		}
	}
};

游戏主逻辑

Initialize中把游戏元素都设置好,然后UpdateFrame中每帧都更新,先更新渲染,再更新逻辑,如下代码片段。

class Game
{
	GameObject mWall;
	Food mFood;
	Snake mSnake;
public:
	Game() {}

	void Initialize()
	{
		auto& wall = mWall.GetRenderObject();
		wall.mPixel = PixelType::Square;
		for (int i = 0; i < 30; i++)
		{
			wall.mPositions.push_back(ivec2(i, 0));
			wall.mPositions.push_back(ivec2(0, i));
			wall.mPositions.push_back(ivec2(i, 29));
			wall.mPositions.push_back(ivec2(29, i));
		}

		mSnake.Start();
		mFood.Start();
		mFood.SetPosition(ivec2(10, 4));
	}

	void UpdateFrame()
	{
		BlockDisplayDevice::Instance().AppenndRenderQueue(mWall.GetRenderObject());
		BlockDisplayDevice::Instance().AppenndRenderQueue(mSnake.GetRenderObject());
		BlockDisplayDevice::Instance().AppenndRenderQueue(mFood.GetRenderObject());
		BlockDisplayDevice::Instance().Display();

		// TODO:Get input

		if (mSnake.Collide(mFood.GetPosition()))
		{	
			mSnake.Grow();
		}

		mSnake.Update();
		mFood.Update();

		BlockDisplayDevice::Instance().Clear();
	}
};

初步效果

这次就先实现这么多,还有输入没有实现,目前还没必要搞一个GameObjectManager出来去维护所有的GameObject,但是这里已经做好准备未来会用到。

https://github.com/ARTELE/GameDev.git

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值