This is the Polish translation of In-Practice/2D-Game/Powerups article of learnopengl.com tutorial series.
Breakout jest bliski ukończenia, ale byłoby fajnie przynajmniej dodać jeszcze jedną mechanikę rozgrywki, aby nie był to przeciętny standardowy klon Breakout; a co z powerupami?
Chodzi o to, że ilekroć cegła jest zniszczona, cegła ma małą szansę na stworzenie bloku powerup. Taki blok będzie powoli opadał w dół i jeśli zderzy się z wiosłem gracza, pojawia się ciekawy efekt w zależności od rodzaju wzmocnienia. Na przykład, jeden powerup sprawia, że wiosło jest większe, a inny powerup pozwala kulce przejść przez cegły. Mamy również kilka negatywnych bonusów, które wpływają na gracza w negatywny sposób.
Możemy zamodelować powerup jako w zasadzie
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()
{ }
};
Klasa
Każdy powerup definiuje swój typ jako ciąg znaków, posiada czas jego trwania i czy jest on obecnie aktywowany. W ramach Breakout będziemy mieli w sumie 4 pozytywne bonusy i 2 negatywne bonusy:
- Speed: zwiększa prędkość piłki o 20%.
- Sticky: gdy piłka zderza się z wiosłem, piłka zostaje przyklejona do niego, chyba że spacja zostanie ponownie naciśnięta. Pozwala to graczowi lepiej pozycjonować piłkę przed jej zwolnieniem.
- Pass-Through: reakcje kolizji zostają wyłączone dla zniszczalnych bloków, dzięki czemu kula przechodzi przez wiele bloków.
- Pad-Size-Increase: zwiększa szerokość wiosła o 50 pikseli.
- Confuse: aktywuje efekt postprocessingu Confuse na krótki czas, dezorientując użytkownika.
- Chaos: na krótki czas aktywuje efekt postprocessingu Chaos, co mocno dezorientuje użytkownika.
Poniżej znajdziesz tekstury o wysokiej jakości:
- Textures: Speed, Sticky, Pass-Through, Pad-Size-Increase, Confuse, Chaos.
Podobnie jak w przypadku tekstur bloków poziomu, każda z dodanych tekstur jest w skali szarości. Dzięki temu kolory ulepszeń pozostają zrównoważone, gdy pomnożymy je przez wektor koloru.
Ponieważ bonusy mają stan, czas trwania i pewne efekty z nimi związane, chcielibyśmy śledzić wszystkie bonusy aktualnie aktywne w grze; przechowujemy je w wektorze:
class Game {
public:
[...]
std::vector<PowerUp> PowerUps;
[...]
void SpawnPowerUps(GameObject &block);
void UpdatePowerUps(GLfloat dt);
};
Zdefiniowaliśmy również dwie funkcje zarządzania powerupami.
Tworzenie PowerUp’ów
Za każdym razem, gdy bloczek jest niszczony, chcemy stworzyć powerup biorąc pod uwagę małą szansę na jego stworzenie. Ta funkcja to
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
));
}
Funkcja 0.0f
oznacza, że czas jego trwania jest nieskończony. Dodatkowo każde ulepszenie otrzymuje pozycję zniszczonego bloku i jedną z tekstur z poprzedniej sekcji.
Aktywacja PowerUp’ów
Następnie aktualizujemy funkcję
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;
}
}
}
}
W przypadku wszystkich bonusów, które nie zostały jeszcze zniszczone, sprawdzamy, czy powerup osiągnął dolną krawędź ekranu, czy też zderzył się z wiosłem. W obu przypadkach powerup jest niszczony, ale po zderzeniu z wiosłem jest również aktywowany.
Aktywacja bonusu jest dokonywana poprzez zmianę jego właściwości Activated na true
i włączenie efektu powerup poprzez podaniu tej właściwości do funkcji
void ActivatePowerUp(PowerUp &powerUp)
{
// Initiate a powerup based type of powerup
if (powerUp.Type == "speed")
{
Ball->Velocity *= 1.2;
}
else if (powerUp.Type == "sticky")
{
Ball->Sticky = GL_TRUE;
Player->Color = glm::vec3(1.0f, 0.5f, 1.0f);
}
else if (powerUp.Type == "pass-through")
{
Ball->PassThrough = GL_TRUE;
Ball->Color = glm::vec3(1.0f, 0.5f, 0.5f);
}
else if (powerUp.Type == "pad-size-increase")
{
Player->Size.x += 50;
}
else if (powerUp.Type == "confuse")
{
if (!Effects->Chaos)
Effects->Confuse = GL_TRUE; // Only activate if chaos wasn't already active
}
else if (powerUp.Type == "chaos")
{
if (!Effects->Confuse)
Effects->Chaos = GL_TRUE;
}
}
Celem
Ponieważ efekty sticky i pass-through nieznacznie zmieniają logikę gry, przechowujemy ich efekt jako właściwość obiektu piłki; w ten sposób możemy zmienić logikę gry na podstawie tego, jaki efekt na piłkę jest obecnie aktywny. Jedyną zmianą jakiej dokonujemy w nagłówku
Następnie możemy łatwo zaimplementować efekt sticky, nieznacznie aktualizując funkcję
if (!Ball->Stuck && std::get<0>(result))
{
[...]
Ball->Stuck = Ball->Sticky;
}
Tutaj ustawiamy właściwość Stuck piłki, która jest podobna do właściwości Sticky piłki. Jeśli efekt sticky zostanie aktywowany, piłka będzie w końcu przyklejała się do wiosła gracza, gdy się z nim zderzy; użytkownik musi ponownie nacisnąć spację, aby zwolnić piłkę.
Podobna niewielka zmiana dotyczy efektu pass-through w ramach tej samej funkcji true
, nie wykonujemy żadnej kolizji na zniszczalnych klockach.
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
{
[...]
}
}
Pozostałe efekty aktywuje się po prostu modyfikując część stanu gry, np. prędkość piłki, rozmiar wiosła lub efekt obiektu
Aktualizacja PowerUp’ów
Teraz pozostaje tylko upewnić się, że bonusy są w stanie poruszać się po ich stworzeniu i że są dezaktywowane, gdy tylko skończy się ich czas trwania; inaczej powerupy pozostaną aktywne na zawsze.
W ramach funkcji 0.0f
, jego efekt zostaje dezaktywowany, a odpowiednie zmienne zostają ustawione na swój pierwotny stan.
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());
}
Możesz zobaczyć, że dla każdego efektu wyłączamy go, resetując odpowiednie elementy do ich oryginalnego stanu. Ustawiliśmy także właściwość Activated na false
. Pod koniec
Funkcja true
, na koniec obiektu kontenera i zwraca iterator na początek tego usuniętego zakresu elementów. Funkcja
Może się zdarzyć, że gdy jeden z efektów jest aktywny, inne bonusy tego samego typu kolidują z wiosłem gracza. W takim przypadku mamy więcej niż 1 powerup danego typu, aktualnie aktywnego w wektorze PowerUps. Następnie, gdy jeden z tych bonusów zostanie dezaktywowany, nie chcemy wyłączać jego efektów, ponieważ inne bonusy tego samego typu mogą być nadal aktywne. Z tego powodu używamy funkcji false
, dezaktywujemy powerup. W ten sposób czas trwania danego typu bonusu zostaje przedłużony do czasu jego ostatniego aktywowanego powerupu.
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;
}
Funkcja po prostu sprawdza dla wszystkich aktywowanych powerup’ów, czy nadal są aktywne jakiekolwiek powerupy tego samego typu, a jeśli tak, zwraca GL_TRUE
.
Ostatnią rzeczą do zrobienia jest renderowanie powerup’ów:
void Game::Render()
{
if (this->State == GAME_ACTIVE)
{
[...]
for (PowerUp &powerUp : this->PowerUps)
if (!powerUp.Destroyed)
powerUp.Draw(*Renderer);
[...]
}
}
Połącz wszystkie te funkcje, a uzyskasz działający system powerup, który nie tylko sprawia, że gra jest przyjemniejsza, ale także o wiele trudniejsza. Wygląda to mniej więcej tak:
Poniżej znajdziesz zaktualizowany kod klasy Game (tam też resetujemy wszystkie efekty powerup’ów po każdym zresetowaniu poziomu):