A long time ago, when newsletter #6 became available. Again, sorry for this. Life sometimes goes in a different direction than planned. In the future, a newsletter about Vexxon will be available at least once every month.
But now, with no further ado, let's start with newsletter #7
So far, we have a game skeleton that looks graphically ok. We can create levels, and we can even steer the vexxon fighter. But that’s still not a game. We missed a gameplay so far. Something like this:
The player must avoid all walls and enemies. Otherwise, it loses one life and must start from begin of the level
The player can shoot bullets, and when the enemy gets hit, the enemy gets destroyed, and the user receives points
The enemies can also fire shots, and when the player gets hit, he loses one life and must start from begin of level
To get fuel, the player must destroy tanks. To get bullets, the player must destroy enemies.
Gameplay
Collision detection
The most crucial functionality to implement the gameplay elements described above is the possibility to detect collisions. What do I mean by that?
For example, nothing happens when the user flies through a wall or gets hit by a bullet. That is because we don’t handle that in our code so far. We have to detect if two objects collide, in other words, intersect with each other.
Many ways exist, but the most straightforward and fastest approach is to use axis-aligned bounding boxes (AABB). This consists of wrapping game entities in a non-rotated (thus axis-aligned) box and checking the positions of these boxes in the 3D coordinate space to see if they are overlapping. The reason why I use axis-aligned boxes is simple. If we also consider rotation, we need trigonometric operations, while AABB can be purely done by logical comparison. The code would look something like this.
bool GameObject::IsColliding(GameObject& other) {
return (
GetMinX() <= other.GetMaxX() &&
GetMaxX() >= other.GetMinX() &&
GetMinY() <= other.GetMaxY() &&
GetMaxY() >= other.GetMinY() &&
GetMinZ() <= other.GetMaxZ() &&
GetMaxZ() >= other.GetMinZ()
);
}
Wall collisions
That’s the base code we need, but how do we use it? At first, we want to integrate collision detection between the player and the walls. So that’s relatively straightforward: We test the player against all walls in each frame. But we can simplify that even more because in our game (Vexxon), we have to test against the wall that comes next. Since we can not fly back, we don’t have to test against walls behind us, and since walls also come in sections, just one wall per time can be close enough to have a collision. If yes, the player gets destroyed and must start from the beginning of the level.
Check against the next wall.
Do we have a collision?
If not, then nothing happens.
If yes, then
The player gets destroyed
The player loses one life
The game starts from the beginning of the level
Let’s implement this now at first.
Player bullet collisions
Detecting collisions between the player bullets and an enemy works similarly, but we have to test it against all bullets still flying around this time. That means we loop through the list of bullets, check if they are still alive, and if yes, if they intersect with an enemy object.
For that, we need code that implements the following.
Loop through the list of player bullets
For each bullet, loop through the list of enemies in front of the player
Do we have a collision?
If not, test against the next enemy
If yes
Destroy the enemy
Add score
Next enemy
Next bullet
Enemy bullet collisions
Detecting collisions with bullets works similarly, but we have to test it against all bullets still flying around this time. That means we loop through the list of bullets, check if they are still alive, and if yes, if they intersect with the player.
For that, we need code that implements the following.
Loop through the list of enemy bullets.
Do we have a collision with the player?
If not, test against the next bullet
If yes
The player gets destroyed
The player loses one life
The game starts from the beginning of the level
Next bullet
Quite a few things to do, right? But that was not everything. We also have to implement the visualization of the gameplay.
Of course, there is more to change to make this work, but overall, collision detection makes the difference.
The Code Changes
Let’s finally examine how all these things get added to the game code.
game_vexxon.cpp
At first, we need an additional game state when a player gets hit by an enemy or touches a wall. We use PLAYER_HIT
for this.
typedef enum _GAME_STATE {
GAME_INITIALIZE, GAME_INTRO, GAME_PLAY, GAME_END, GAME_LOST, PLAYER_HIT
} GAME_STATE;
And, of course, we must handle this new game state in OnUpdate(). This is simple: we must draw lines indicating a crash and wait for the player to restart the level by pressing a button.
virtual bool OnUpdate(float deltaTime) {
...
else if (_state == PLAYER_HIT) {
DrawHUD();
PlayerHitScene();
if (IsControlPressed(CONTROL1_BTN4)) {
ClearLevel();
Restart();
}
}
...
}
But we also have to call collision detection in OnUpdate()
, which we do in line 249 by calling DetectCollisions()
. In this function, we implement the scenarios I outlined before using IsColliding().
void DetectCollisions() {
// Player collisions
for (auto gameObject : m_gameObjects) {
// Wall detection
if (gameObject->GetTag() == LEVEL_OBJECT_LEVEL
&& !gameObject->IsDead()) {
if (_player->IsColliding(*gameObject)) {
// We have a collision between player and wall
PlayerHitObject(gameObject);
}
}
// Enemy detection
else if (gameObject->GetTag() <= LEVEL_OBJECT_BULLET
&& !gameObject->IsDead()) {
if (_player->IsColliding(*gameObject)) {
// We have a collision between player and enemy
PlayerHitEnemy(gameObject);
}
}
}
// Player bullet collisions
for (auto bullet : m_bullets) {
for (auto gameObject : m_gameObjects) {
// Enemy detection
if (gameObject->GetTag() < LEVEL_OBJECT_BULLET
&& !gameObject->IsDead()) {
if (bullet->IsColliding(*gameObject)) {
// We have a collision between player bullet and enemy
PlayerBulletHitEnemy(bullet, gameObject);
}
}
}
}
}
We only miss the functions that implement the behavior when one of these situations happens. The first two handle the situation when an enemy or a wall hits the player. We handle both in the same way by changing the game state to PLAYER_HIT
and disabling the movement of all game objects.
void PlayerHitObject(GameObject* gameObject) {
printf("Player hits level <%d>\n", gameObject->GetID());
_state = PLAYER_HIT;
SetAutoUpdate(false);
}
void PlayerHitEnemy(GameObject* gameObject) {
printf("Player hits enemy <%d>\n", gameObject->GetID());
_state = PLAYER_HIT;
SetAutoUpdate(false);
}
The third one handles the situation when the player's bullet hits an enemy. We changed the score and fuel depending on the enemy object we destroyed and continued playing.
void PlayerBulletHitEnemy(GameObject* bullet, GameObject* gameObject) {
printf("Player bullet <%d> hits enemy <%d>\n", gameObject->GetID());
bullet->SetDead();
gameObject->SetDead();
switch (gameObject->GetTag()) {
case LEVEL_OBJECT_FUELSILO:
_fuel_available += _level.fuel_fillup_per_silo;
_score += _level.score_silo;
break;
case LEVEL_OBJECT_TANK:
_score += _level.score_tank;
_bullets_available += _level.bullets/4;
break;
case LEVEL_OBJECT_ROCKET:
_score += _level.score_rocket;
_bullets_available += _level.bullets/4;
break;
case LEVEL_OBJECT_JET_FLYING:
_score += _level.score_jet_flying;
_bullets_available += _level.bullets/4;
break;
case LEVEL_OBJECT_JET_STANDING:
_score += _level.score_jet;
_bullets_available += _level.bullets/4;
break;
}
}
That’s it for this time. It’s not yet perfect, and we must fix and fine-tune some more things, but overall, significant progress!
Thanks for this excellent bezel overlay created by Ralf from Vectrex Overlays!!
Stay tuned for the following Newsletter, which should also be available in 2023, so you will have a first working version of Vexxon by the end of this year. Then, we improve the enemies and the player bullets.
Cheers, and thanks for your patience!