到现在为止,我们的游戏窗口还只是一个黑乎乎的东西,最多加上一个渲染的精灵。我们需要的可不是这样,现在我们来绘制我们的游戏内容——游戏关卡(Game Level)。首先看看 Breakout 的游戏截图(来自维基百科 Break_out):
我们的游戏自然不会是这个画质(=。=),但是我们可以从这张图片中找到我们需要的东西。首先我们的游戏画面包含以下元素:
- 可 / 不可被破坏的方块
- 墙壁边界
- 一个可以活动的挡板
- 游戏角色
在游戏正式界面中,我们需要绘制游戏可能出现的场景,其中包括了游戏场景上半部分的可被破坏或者不可被破坏的砖块,一个可以碰撞弹动的游戏对象,一个在我们控制下移动的挡板(游戏对象碰撞到上,左,右边界和不可破坏砖块,挡板反弹,碰到下边界死亡,碰到可被破坏砖块,则砖块被破坏)。
游戏的砖块位置情况,我们用一个矩阵来表示,矩阵数字为0代表空,为1代表不可破坏砖块,为2代表可破坏砖块。例如:
1 | 1 1 1 1 1 1 |
这些砖块将被平铺在游戏场景的上半部分,下半部分底部为一个挡板和我们的游戏对象。现在我们来绘制上半部分的场景。
关于砖块位置的信息我们将其放在文件里,不同的砖块信息我们将之认为不同的关卡(Game Level)。在此,我们创建一个被称为游戏对象的组件作为一个游戏内物体的基本表示。这样的游戏对象持有一些状态数据,如其位置、大小与速率。它还持有颜色、旋转、是否坚硬(不可被摧毁)、是否被摧毁的属性,除此之外,它还存储了一个Texture2D变量作为其精灵(Sprite)。
首先看看 GameObject 类:
1 | // Container object for holding all state relevant for a single |
我们在 GameObject 里定义了一系列的属性,并且有一个 Draw 的方法,这个 Draw 则是简单的判断对象是否存在而是否进行渲染。 GameObject 的实现代码可以看这里:GameObject.cpp。
除此之外,我们需要一个关卡类,用于关卡的操作(初始化,结束):
1 | class GameLevel { |
GameLevel 类的实现看这里:GameLevel.cpp。
现在我们有了关卡类,可以正式的绘制关卡场景了。我们现在 Game 类里边加上一个存储关卡的数组以及一个游戏当前关卡的变量:
1 | std::vector<GameLevel> levels; |
在 Init 方法里,我们初始化这两个变量:
1 | // 加载关卡 |
有了关卡信息后,我们使用 GameLevel 类的 Draw 进行绘制:
1 | // 加载关卡 |
有了关卡信息后,我们使用 GameLevel 类的 Draw 进行绘制:
1 | if (this->state == GAME_ACTIVE) |
单一的颜色可能比较丑:
我们为其加上背景和砖块纹理,首先在 Game::Init 方法里使用 ResourceManager 类读取纹理:
1 | ResourceManager::LoadTexture2D("resources/textures/background.jpg", GL_FALSE, "background"); |
在关卡初始化的时候为可破坏和不可破坏的纹理的加入不同的纹理:
1 | void GameLevel::Init(std::vector<std::vector<GLuint>> tileData, const GLuint levelWidth, const GLuint levelHeight) { |
(注意注释部分,我们用实际加载的纹理代替了一个空的纹理对象)。最后我们在绘制关卡前先绘制背景纹理:
1 | void Game::Render() { |
最终效果如下:
貌似不错了,现在我们来绘制挡板,首先在 Game 类中添加 paddle 变量(同样也是 GameObject 对象):
1 | GameObject player; |
在 Game::Init 方法里,我们读取纹理并且初始化这个对象:
1 | ResourceManager::LoadTexture2D("resources/textures/paddle.png", GL_TRUE, "paddle"); |
最后,在 Game::Render 方法里绘制:
1 | // 绘制挡板 |
效果如下: