简介
这次就来一个比较简单的小游戏贪吃蛇
贪吃蛇
游戏规则就是一串珠子不断的移动,碰到场景里面的食物变长一点,碰到墙壁游戏结束。
开始动手
设计绘制设备
首先我计划从一个控制台游戏开始,需要一个控制台下的绘图机制,希望是在一个网格上面可以根据输入坐标来显示各种元素。就像下图这样,每个方块或者圆圈就好比是一个像素,我们可以通过坐标位置来控制像素绘制的位置,通过输入每个像素的属性来控制画出来的是方块还是圆圈还是空白。
首先定义好每个像素快的属性,属性中有两个描述,一个是颜色,一个是图案类别,目前我们有三种,分别是方块,圆圈和空白。定义如下。
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,但是这里已经做好准备未来会用到。