英文原文
Breakout已经接近尾声了,但是它仍然可以通过添加一个额外的游戏内容来变得更酷,这样它就不是普通的Breakout的翻版;能量块怎么样?
这个主意就是无论何时砖块被销毁,这个砖块会有一个小概率产生一个能量块。这样一个块将会缓慢落到底部并且如果它碰撞玩家挡板,一个基于能量块类型的有趣的效果将会产生。例如,一个能量块让挡板更大,另一个能量块允许小球穿透物体。我们还有几个负面能量块来对玩家产生负面影响。
我们能够像一个有一些额外特性的基础GameObject一样模拟一个能量块。这就是为什么我们定义一个继承自GameObject类的PowerUp并且添加这些额外的特性到对象上:
const glm::vec2 SIZE(60, 20);
const glm::vec2 VELOCITY(0.0f, 150.0f);
class PowerUp : public GameObject
{
public:
// PowerUp State
std::string Type;
GLfloat Duration;
GLboolean Activated;
// Constructor
PowerUp(std::string type, glm::vec3 color, GLfloat duration,
glm::vec2 position, Texture2D texture)
: GameObject(position, SIZE, texture, color, VELOCITY),
Type(type), Duration(duration), Activated()
{ }
};
PowerUp一个是有额外状态的GameObject,因此我们能够简单地定义一个头文件头文件
每一个能量块用一个字符串来定义它的类型,包含激活的持续时间以及当前激活状态是何时被激活的。在Breakout中我们会有4种正面能量块以及2种负面能量块:
- Speed:提升小球20%的速度。
- Sticky:当小球碰撞到档板,小球被卡在挡板上除非再次按下空格键。这允许玩家在释放小球前找一个合适的位置。
- Pass-Through:非固体砖块的碰撞反馈不会发生,小球将会穿过大多数砖块。
- Pad-Size-Increase:提升档板的宽度50像素。
- Confuse:激活颠倒后期处理特效一小段时间,让玩家颠倒。
- Chaos:激活混淆后期处理特效一小段时间,深度迷惑玩家。
下面是高清贴图:
贴图: Speed, Sticky, PassThrough, Pad-Size-Increase, Confuse, Chaos.
和关卡砖块贴图类似,任何一个能量块贴图也完全是灰度图。保证当我们在容器中制造大量能量块时它的颜色和谐。
由于能量块有状态,一个持续且确定的效果和它关联,我们会在游戏中记录所有当前激活的呢凉快;我们在vector中存储它们:
class Game {
public:
[...]
std::vector<PowerUp> PowerUps;
[...]
void SpawnPowerUps(GameObject &block);
void UpdatePowerUps(GLfloat dt);
};
我们还定义了两个方法来管理能量块。SpawnPowerUps在给定的砖块的位置制造一个能量块并且UpdatePowerUps在游戏中管理所有当前激活的能量块。
制造能量块
任何时候一个砖块被销毁我们将会给一个小概率来制造一个能量块。这个功能在game类的SpawnPowerUps类中被建立:
GLboolean ShouldSpawn(GLuint chance)
{
GLuint random = rand() % chance;
return random == 0;
}
void Game::SpawnPowerUps(GameObject &block)
{
if (ShouldSpawn(75)) // 1 in 75 chance
this->PowerUps.push_back(PowerUp("speed", glm::vec3(0.5f, 0.5f, 1.0f), 0.0f, block.Position, tex_speed));
if (ShouldSpawn(75))
this->PowerUps.push_back(PowerUp("sticky", glm::vec3(1.0f, 0.5f, 1.0f), 20.0f, block.Position, tex_sticky);
if (ShouldSpawn(75))
this->PowerUps.push_back(PowerUp("pass-through", glm::vec3(0.5f, 1.0f, 0.5f), 10.0f, block.Position, tex_pass));
if (ShouldSpawn(75))
this->PowerUps.push_back(PowerUp("pad-size-increase", glm::vec3(1.0f, 0.6f, 0.4), 0.0f, block.Position, tex_size));
if (ShouldSpawn(15)) // Negative powerups should spawn more often
this->PowerUps.push_back(PowerUp("confuse", glm::vec3(1.0f, 0.3f, 0.3f), 15.0f, block.Position, tex_confuse));
if (ShouldSpawn(15))
this->PowerUps.push_back(PowerUp("chaos", glm::vec3(0.9f, 0.25f, 0.25f), 15.0f, block.Position, tex_chaos));
}
SpawnPowerUps方法基于一个给定的概率(1~75是普通能量块,1~15是负面能量块)创建一个能量块对象并且设置它们的特性。任何能量块给定了一个特殊颜色来让它们对玩家来说更加容易辨认并且基于它的类型在短时间内持续;持续时间0.0f一位置持续时间无限。此外,任何能量块在被销毁的砖块出现并且使用之前给定的贴图中的一个。
激活能量块
我们更新game的DoCollisions方法来让它不仅仅检查砖块和挡板碰撞,还包括所有在挡板和不会被销毁的能量块之间的碰撞。一旦砖块被调回我们就调用SpawnPowerUps方法。
void Game::DoCollisions()
{
for (GameObject &box : this->Levels[this->Level].Bricks)
{
if (!box.Destroyed)
{
Collision collision = CheckCollision(*Ball, box);
if (std::get<0>(collision)) // If collision is true
{
// Destroy block if not solid
if (!box.IsSolid)
{
box.Destroyed = GL_TRUE;
this->SpawnPowerUps(box);
}
[...]
}
}
}
[...]
for (PowerUp &powerUp : this->PowerUps)
{
if (!powerUp.Destroyed)
{
if (powerUp.Position.y >= this->Height)
powerUp.Destroyed = GL_TRUE;
if (CheckCollision(*Player, powerUp))
{ // Collided with player, now activate powerup
ActivatePowerUp(powerUp);
powerUp.Destroyed = GL_TRUE;
powerUp.Activated = GL_TRUE;
}
}
}
}
所有能量块都没被销毁,我们检查能量块是到达了屏幕底边还是碰到了挡板。无论哪种情况,能量块都会被销毁,然后碰到挡板时会被激活。
激活一个挡板是通过设置它的激活特性为true来完成的并且通过赋值给ActivatePowerUp方法赋值来启用挡板:
ActivatePowerUp的作用和它听起来一样:按照我们在教程开始所描述的来激活能量块的特性。我们检查能量块的类型并且据此改变游戏状态。在“sticky”和“pass-through”特性中我们还改变的挡板的颜色并且小球分别给予玩家当前所激活的特性的反馈。
由于黏着和穿透特性轻微的改变了游戏逻辑,我们会将他们的特性存储在小球对象;这样我们能够基于小球当前激活的任何特性来改变游戏逻辑。我们在BallObject头文件中唯一改变的事情是添加这两个属性,完全更新后的代码:
BallObject: header, code.
我们能够通过在求和挡板碰撞之间稍微更新DoCollisions方法来简单的完成黏着特效:
if (!Ball->Stuck && std::get<0>(result))
{
[...]
Ball->Stuck = Ball->Sticky;
}
我们在这里设置小球被卡主的特性来和小球的黏着特性匹配。如果黏着特性被激活,小球将会在碰到玩家挡板时结束并卡在玩家挡板上;玩家能够通过按下空格键再次释放小球。
DoCollisions方法同样要为穿透特效做一些改变。当小球的穿透特效被设置为true,我们不会在非固体砖块上完成任何碰撞反馈。
Direction dir = std::get<1>(collision);
glm::vec2 diff_vector = std::get<2>(collision);
if (!(Ball->PassThrough && !box.IsSolid))
{
if (dir == LEFT || dir == RIGHT) // Horizontal collision
{
[...]
}
else
{
[...]
}
}
其他特性通过简单的修改一部分游戏的状态来激活,比如小球是速度,挡板的尺寸或者PostProcesser对象的一个特性。
更新能量块
现在所有剩下要做的就是确认能量块在他们被制造出来之后可以移动并且当它们被耗尽时它们是无效的;否则能量块会一直被激活。
在game类的UpdatePowerUps方法中我们基于他们的速度移动能量块并持续减少激活的能量块的持续时间。无论何时能量块的持续时间被减为0.0f,他的相关特性将会被设置为它的原始状态。
void Game::UpdatePowerUps(GLfloat dt)
{
for (PowerUp &powerUp : this->PowerUps)
{
powerUp.Position += powerUp.Velocity * dt;
if (powerUp.Activated)
{
powerUp.Duration -= dt;
if (powerUp.Duration <= 0.0f)
{
// Remove powerup from list (will later be removed)
powerUp.Activated = GL_FALSE;
// Deactivate effects
if (powerUp.Type == "sticky")
{
if (!isOtherPowerUpActive(this->PowerUps, "sticky"))
{ // Only reset if no other PowerUp of type sticky is active
Ball->Sticky = GL_FALSE;
Player->Color = glm::vec3(1.0f);
}
}
else if (powerUp.Type == "pass-through")
{
if (!isOtherPowerUpActive(this->PowerUps, "pass-through"))
{ // Only reset if no other PowerUp of type pass-through is active
Ball->PassThrough = GL_FALSE;
Ball->Color = glm::vec3(1.0f);
}
}
else if (powerUp.Type == "confuse")
{
if (!isOtherPowerUpActive(this->PowerUps, "confuse"))
{ // Only reset if no other PowerUp of type confuse is active
Effects->Confuse = GL_FALSE;
}
}
else if (powerUp.Type == "chaos")
{
if (!isOtherPowerUpActive(this->PowerUps, "chaos"))
{ // Only reset if no other PowerUp of type chaos is active
Effects->Chaos = GL_FALSE;
}
}
}
}
}
this->PowerUps.erase(std::remove_if(this->PowerUps.begin(), this->PowerUps.end(),
[](const PowerUp &powerUp) { return powerUp.Destroyed && !powerUp.Activated; }
), this->PowerUps.end());
}
你能看见对于每个特性我们通过重置相关特性到原始状态来禁用它。我们还设置能量块的激活特性为false。在UpdatePowerUps的结尾我们通过遍历能量块容器来去掉每个被销毁或者无效的能量块。我们使用remove_if方法从algorithm头文件来给定一个lambda谓语去掉他们。
Remove_if方法移除所有的元素,lambda谓语为真的时候直到容器对象底部并且返回一个移除元素范围开始迭代器。容器的移除方法取走迭代器以及容器的尾部迭代器并且移除两个迭代器之间的所有元素。
有可能出现当其中一个能量块特性被激活时另一个同类的能量块碰到挡板。在那种情况下我们在game类的PowerUps容器中有有多于一个当前被激活的能量块。然后,无论何时这些能量块中的一个失去效果,我们不会想要禁用它的特性直到另一个同类的能量块被激活。由于这个原因我们使用IsOtherPowerUpActive方法来检查这里是否仍然有另一个相同的被激活的能量块。仅仅当这个方法返回false时我们停用这个能量块。这样,这个给定类型能量块的持续时间被眼神到了最后一个被激活的能量块。
GLboolean IsOtherPowerUpActive(std::vector<PowerUp> &powerUps, std::string type)
{
for (const PowerUp &powerUp : powerUps)
{
if (powerUp.Activated)
if (powerUp.Type == type)
return GL_TRUE;
}
return GL_FALSE;
}
这个方法简单的检查所有被激活的能量块,如果这里仍然有任何一个同类被激活的能量块并且如果这样的话返回GL_TRUE。
最后要做的事情是激活给定的能量块:
void Game::Render()
{
if (this->State == GAME_ACTIVE)
{
[...]
for (PowerUp &powerUp : this->PowerUps)
if (!powerUp.Destroyed)
powerUp.Draw(*Renderer);
[...]
}
}
结合所有这些功能我们有一个运行的能量块系统来让游戏更加有趣,但是也有更多挑战,它看起来像这样:
http://learnopengl.com/video/in-practice/breakout/powerups.mp4
这里是更新后的game代码(游戏开始的时候也要重置所有的能量块):
Game: header, code.
转载请注明出处:http://blog.csdn.net/ylbs110/article/details/53056259