Newsletter #6 - Improving the Gameplay
Creating unique games for the PiTrex #pitrex #vectrex #retrogaming
In this newsletter I want to work on some game improvements, or better said, start making Vexxon a real game. So far you can just fly through the level but there is no interaction and also no game logic at all.
That’s what I want to do today.
Gameplay in Vexxon
As Vexxon will be a Zaxxon alike game the rules we want to include in the game are:
The Player
Can fire against enemy objects (tanks, jets, rockets).
For every enemy that will be destroyed, he gets points
The number of bullets is limited, but for each destroyed enemy, some bullets are refilled
Has some amount of fuel at the beginning, but must fill up the fuel by destroying fuel silos
Wins the level (game) if he reaches the end :)
Lose the game when he gets out of fuel before the level (game) ends
The Enemies
Fire against the player, if they hit the player, he must begin the level again
As you see, a lot to do, to make Vexxon a real game, and not just a fly-trough experience …
Adding Bullets
Let’s start by adding bullets to Vexxon.
#define SCORE_BULLETS 50 // Initial numbers of bullets
#define SCORE_BULLETS_ALARM 5 // Alarm when bullets gets below this value
At first, I add two definitions for the number of bullets to start with and when they reach a critical value. They are just used as default values since (as you remember) we load those values from the level file.
GameObject* _bullet;
Also, I have to add a game object that holds the bullet (more exactly a template of it) and a variable to store the number of bullets available.
int _bullets_available = SCORE_BULLETS;
In OnCreate()
I use both of them by assigning the values from the level file to the variable and also creating the game object here.
_bullets_available = _level.bullets;
_bullet = new GameObject("bullet", LEVEL_OBJECT_BULLET);
_bullet->SetColor(colorYellow);
In OnUpdate()
is the code that checks the buttons on the joystick to fire the bullets. In fact, I use two buttons for this. Button 4
to fire one bullet and Button 3
to fire two bullets at the same time
if (IsControlHold(CONTROL1_BTN3)) {
FireBullets(2);
}
else if (IsControlHold(CONTROL1_BTN4)) {
FireBullets(1);
}
Now let’s implement the function FireButtons()
and FireButton()
that does the real work:
void FireBullet(int offset) {
Vec3D pos = _player->GetPosition();
// Center
GameObject* cube = new GameObject(_bullet->GetMesh(), LEVEL_OBJECT_BULLET);
cube->SetLifeTime(1.0);
cube->SetPosition(pos.x + offset, pos.y, pos.z);
cube->SetSpeed(0, 0, 60);
cube->SetColor(colorYellowLight);
AddGameObject(cube);
}
void FireBullets(int bullets) {
if (_bullets_available <= 0) {
RBLOG("Fire: NO BULLETS LEFT");
return;
}
switch (bullets) {
case 1:
FireBullet(0);
break;
case 2:
FireBullet(-1);
FireBullet(1);
break;
case 3:
FireBullet(-1);
FireBullet(0);
FireBullet(1);
break;
default:
return;
}
_bullets_available -= bullets;
}
By using the engine, this task is now quite easy to implement. Whenever the player fires a bullet, I must create a new game object, but take the mesh data (that’s the visual of a bullet) from the _bullet game object we instantiated in OnCreate(). So we reuse it again and again.
The next lines set a lifetime for the bullet. That means, if no enemy gets hit before its lifetime ends, it’s set to “dead” (and gets destroyed later). Last but not least, I define position and speed and of course the color for the bullets and add it to the game engine.
In the next Update()
the engine will do the rest of the work and moves the bullet(s) at the speed I have defined. That’s it :)
FireBullets() is just a helper function that switches between the number of bullets fired per shoot.
Adding Fuel
Next, we need to implement fuel usage for our jet. This is done by the following lines of code:
#define SCORE_FUEL 200 // Initial fuel level
#define SCORE_FUEL_ALARM 50 // Alarm when fuel gets below this value
#define SCORE_FUEL_PS 1 // Fuel litres per second reduced
float _fuel_available = SCORE_FUEL;
int _fuel_per_second = SCORE_FUEL_PS;
void OnCreate() {
...
_fuel_available = _level.fuel;
_fuel_per_second = _level.fuel_per_second;
...
}
void OnUpdate() {
...
if (_fuel_available <= 0) {
_state = GAME_LOST;
return true;
}
_fuel_available -= _fuel_per_second * deltaTime;
_score += _score_per_second * deltaTime;
...
}
This should be quite self-explanatory. The main thing that is done in OnUpdate()
where the fuel gets reduces over time and also verified, if we still have fuel left, otherwise the level is lost.
Adding Score
Every game needs a scoring system. They are a key component of game mechanics and provide a mechanism whereby players are rewarded with point value whenever they accomplish a task in the game.
This is done in Vexxon simply by controlling time, fuel, bullets, and points. You get points for every second you survive in the level, which means the longer you fly, the more points you get. Some more rules are added later.
Adding more GameStates
As you have seen, I also introduced a new game state: GAME_LOST. This state happens when you have no more fuel. Later I add also more rules on how to lose in a level, like being hit by an enemy’s bullet.
Improve User Feedback
Adding a HUD
So far I have just added logic to handle bullets, fuel, and score but this is so far not visible to the user. You would fly “blind” through the level…
Therefore I added a new visual element that’s often called HUD. In video gaming, the HUD (heads-up display) or status bar is the method by which information is visually relayed to the player as part of a game's user interface.
I want to show the number of bullets, the fuel left, and the points that you get so far. I do that all in a function DrawHUD() that look like this:
void DrawHUD() {
// 1a. Print bullets left
char temp[256];
sprintf(temp, "B %03d", _bullets_available);
DrawBitmapString(temp, 20, 20, 40,
_bullets_available >= SCORE_BULLETS_ALARM ? colorWhite : colorRed);
// 1b. Print fuel left
sprintf(temp, "F %03d", (int)_fuel_available);
DrawBitmapString(temp, 100, 20, 40,
_fuel_available >= SCORE_FUEL_ALARM ? colorWhite : colorRed);
// 1c. Print score
sprintf(temp, "S %04d", (int)_score);
DrawBitmapString(temp, 280, 20, 40, 100);
Vec3D pos = _player->GetPosition();
float y = -pos.y - MIN_FLY_HEIGHT;
float height = 100 * y / (MAX_FLY_HEIGHT - MIN_FLY_HEIGHT);
// 2. Draw height status
DrawLine(10, HORIZONT, 10, HORIZONT + 100, colorGray);
DrawLine(10, HORIZONT, 10, HORIZONT + height, colorGreen);
DrawLine(11, HORIZONT, 11, HORIZONT + height, colorGreen);
DrawBitmapString(STR_HEIGHT_ABBRV, 6, 130, 40, 100);
// 3. Print game state
if (_state == GAME_END) {
DrawBitmapString(STR_LEVEL_END, 60, 60, 40, colorYellow);
}
else if (_state == GAME_LOST) {
DrawBitmapString(STR_LEVEL_LOST, 60, 60, 40, colorRed);
}
}
The Height Status
Also, I have added a visual element that shows the player at which height the jet flies (2). This help to navigate over or through the walls.
Add a Player Shadow
Another useful element is the shadow of the jet, so the player can more easily recognize which position the jet currently is in. The shadow of the jet is just a rectangle I draw in another color and is always visible on the ground. This is also simple to implement:
GameObject* _shadow;
_shadow = new GameObject(GAME_OBJECT_TYPE_RECTANGLE);
_shadow->SetPosition(0, 0, 0);
_shadow->SetColor(colorWhite);
_shadow->SetSpeed(0, 0, 0);
_shadow->SetHidden(false);
AddGameObject(_shadow);
void DrawShadow() {
Vec3D pos = _player->GetPosition();
pos.z = -pos.y / 2;
pos.y = 0;
_shadow->SetPosition(pos);
}
At first, a game object is created and added to the engine. The function DrawShadow()
is then responsible to update the shadow position related to the player position.
With these enhancements, I have already basic gameplay implemented which I update next time by adding collision detection. Bullets that never collide with an enemy don’t make much sense ;)
Enemy Alarms
Something I also wanted to add is enemy alarms. That’s just a warning text that pops up when a tank, jet, or rocket is at a given distance from the player. For this, I extend the function VerifyGameObjects()
:
// Enemy alarms
if (!gameObject->IsDead() && gameObject->GetPosition().z < DISTANCE_ALARM) {
int tag = gameObject->GetTag();
switch (tag) {
case LEVEL_OBJECT_JET_FLYING:
DrawBitmapString(STR_FLIGHT_ALARM, 20, 60, 20, colorRed);
return;
case LEVEL_OBJECT_JET_STANDING:
DrawBitmapString(STR_PLANE_ALARM, 20, 60, 20, colorYellow);
return;
case LEVEL_OBJECT_ROCKET:
DrawBitmapString(STR_ROCKET_ALARM, 20, 60, 20, colorRed);
return;
case LEVEL_OBJECT_TANK:
DrawBitmapString(STR_TANK_ALARM, 20, 60, 20, colorViolett);
return;
}
}
This code is also inside the loop that iterates over the number of game objects and checks if the object is not already dead and the z-distance is below DISTANCE_ALARM
. If so, a text is displayed in different colors, depending on which enemy type it is.
Some first Optimisation
Removing “Dead” Objects
With the introduction of bullets, another issue becomes visible: The number of game objects gets bigger and bigger. For this, the engine has already a function that removes “dead” game objects (variable m_dead
in GameObject
is true). As you have seen above, bullets are handled over their lifetimes, so nothing more to do. Bullets get automatically destroyed by the engine after their lifetime.
But other objects must also be removed. Level objects that are already behind and no longer visible to the player. This is simply done by checking the z-coordinate of a game object and they are set to “dead” if they are behind.
void RemoveDeadObjects() {
for (auto gameObject : m_gameObjects) {
if (gameObject->GetPosition().z < -40) {
gameObject->SetDead();
}
}
}
That’s it, I only have to call this function in OnUpdate()
and e voila!
Try it out and fire some bullets :)
This looks already more like a game but it needs another important aspect to make it complete. Collisions! Bullets can collide with game objects and game objects with another game object.
That’s what I will add in the next newsletter. Cy there!
And as always: Find the source code here on GitHub.