flasim - A Game Coding Example
posted by polylux on Feb. 5, 2019, 12:54 a.m.As a surprise party gag I recently coded a simple game that puts you in the seat of an anti-aircraft gunner. Its design is quite compact which is why I think it serves as a good example to dive into C++ graphics and game programming.
UPDATE: Precompiled for Ubuntu 18.04 in the Links section
UPDATE 2: Precompiled for Windows in the Links section
Introduction
In order to acquire the big present, the 'birthday boy' had to get through a series of tasks prepared by friends and relatives. In recognition of the time he served in an anti-aircraft unit in the military, I wanted to test his practical abilities and coded this mini game in which the player can control an AAA cannon with the joystick. Five planes are then spawned randomly, a successful shoot-down rewards the player with points and a bonus depending on the distance of the engagement.

flasim - successful engagement of an enemy plane
The game was surprisingly well received by the party guests and intense battles for the highest score ensued afterwards.
Scanning for incoming aircrafts - party crowd battling for the highscore.
My choice for the visual part was the irrlicht graphics engine. I use it a lot for the purpose of scientific and interactive visualization professionally as well as hobby projects. I like it for its clean API, its permissive zlib licence (which basically means, do whatever you want with it) and cross-platform abilities. Bear in mind, it's a graphics engine, it only does the drawing side of things. No physics (not needed here), no sound, no networking whatsoever.
The following section describes the main functional units, source code can be found in the links section below. I recommend cloning the repo and reading the source side-by-side with what follows.
Game Components
The game consists of only three main functional components I'll describe in more detail in this section. The source code is available on our Git server, see the links section below.
Aircraft
The Aircraft class wraps that actual plane model in the sky, and the functionality connected to it. It is instantiated by the Dispatcher which tells it to go from where to where in a given time. Aircraft consequently loads the plane model, rotates it along the flight path and attaches a FlyStraightAnimator to let it move.
Dispatcher
The Dispatcher is the central unit doing actual game-related work. Its main purpose is to launch aircrafts during an active game, keep a list of active planes, hit-test shots and reward points for a successful engagement. It is called once every engine's loop to do its job.
Firing the cannon calls the Dispatcher::evalShot(const line&)
function which hit-tests the passed-in line of fire with all active and 'healthy' airplanes. Upon a hit, 1000 points are rewarded plus bonus points for the distance at which the hit was scored.
The dispatcher also launches new aircrafts every 45 seconds. Where and how is (hopefully) nicely balanced between randomness and preserving equality in each engagement run. One of the key aspects for an early engagement - and thus higher points - is to observe and scan the horizon. The Dispatcher::dispatchAircraft()
function hence does the following:
- Create a unit vector
- Randomly rotate it by [0.0;360.0]
- Use the resulting vector
dVec
as the slope for our line (-> flight path) - Randomize the displacement
d
[0.0;500.0] of the line from our gun's position so it does not always fly above our head - Randomize the plane's altitude [50.0;500.0]
- Calculate start and end position at +/-2000 units out along that line and create aircraft
void Dispatcher::dispatchAircraft()
{
IRandomizer *rand = Globals::getDevice()->getRandomizer();
// rand dir vector around 0/0
vector2df dVec(1.f, 0.f);
dVec.rotateBy(rand->frand() * 360.f, vector2df(0.f, 0.f));
dVec.normalize();
vector2df nVec(dVec.Y, -dVec.X);
f32 d = rand->frand() * 500.f;
f32 alt = 50.f + rand->frand() * 500.f;
// start/end 2km off our pos, displaced by d * nVec to the side
vector2df start = dVec * -2000.f + nVec * d;
vector2df end = dVec * 2000.f + nVec * d;
line3df l;
l.start = vector3df(start.X, alt, start.Y);
l.end = vector3df(end.X, alt, end.Y);
aircrafts.push_back(std::make_shared<Aircraft>(l, 30000 + static_cast<u32>(rand->frand() * 15000.f)));
}
FireUnit
The central unit handling user input and gun properties and our view of the world in general. It displays the front section of our cannon which is rotated and elevated through joystick input. A camera attached to it enables us to see the world through the eyes of the gunner. In order to always be aligned along the barrels, the camera scene node uses a custom scene node animator TurretCamAnimator
which is a nice and simple example for deriving and implementing such an animator, something that frequently comes in handy for cases where you want to attach custom behavior to a scene node.
The cannon has an ammo capacity of 40 rounds per barrel firing in 8 round bursts (so you can pull the trigger up to five times until you run out of ammo). Reloading the gun is activated using joystick button #3 and adds 15 seconds of penalty. It's therefor a good idea to choose wisely when to reload, you wouldn't want to engage with only a fraction of your total ammo capacity. This - along with firing and moving the cannon - is all handled in FireUnit::OnEvent()
called by FlaSimApp
.
Firing the cannon sets the fireCount (uint)
property to > 0. This activates the computation of the line of fire in the units loop function FireUnit::draw()
. Unless this count is zero, the loop computes one line and hands it over to the Dispatcher for the hit testing. This way cannon corrections can be accounted for during the firing of the burst while fireCount
gradually reaches zero.
Sound
As irrlicht is a graphics engine only, we have to rely on something else for sonification. For such projects I like to use cAudio, a wrapper for OpenAL which provides a clean, modern interface to it and allows you to set up audio scenes with just some lines of code. If you use ArchLinux, I am providing an AUR package for it (see Links).
The AudioManager
is created in our Globals
singleton. Soundwise the requirements are simple: we want to hear the cannon firing and the airplanes whiz by over our heads. As for the former, FireUnit
loads a burst sound in the constructor played every time we pull the trigger. Generally, audio libs allow to play sounds with (3D) our without (2D) spacial representation. 3D sounds are perceived with respect to the position of the listener (usually the player position) and are therefor audible from the correct angle and attenuation. The 'flat' sounds are just replayed, with no spatial information. Contrary to all other audible stuff, sounds that we make should always be played in 2D mode - regardless of the actual mode - or else it always sounds awkward, especially if that 'we' can move, like a person, car, plane. So just bear in mind, your gun, your footsteps, your jumps, your what-else-your-game-may-have should be played 2D, everything else 3D.
The second source of audio is the airplane. I found some 'jet pass-by' wav on some free source site that the Airplane
unit starts playing when the plane closes in on our position. Once playing, the update loop accounts for updating the sound position to the aircraft position so that we hear the jet where we can see it:
void Aircraft::update(u32 curMS)
{
if (!flybySound->isPlaying())
{
u32 curTime = curMS;
curTime -= flightStarted;
if ( /* some math to start the sound well timed */ )
{
auto p = model->getAbsolutePosition();
flybySound->play3d(cAudio::cVector3(p.X, p.Y, p.Z));
}
}
else
{
auto p = model->getAbsolutePosition();
flybySound->move(cAudio::cVector3(p.X, p.Y, p.Z));
}
}
Visual Assets
Obviously flasim's scenic complexity is rather manageable. On the other hand it makes do with only a couple of assets. The sky is a classic skybox scene node, a special, virtual cube-shaped container surrounding the scene. To every side of this cube a texture is assigned representing a portion of our slightly cloudy sky. Put simply, a skybox always translates with the camera perspective-wise, so you get the impression of an immobile, distant sky.
The terrain is a height map I did in blender ages ago. A height map is a greyscale texture where height information is encoded in the color of the pixel. The brighter the more elevated a specific spot in the terrain will be. Irrlicht provides a terrain scene node to handle such geometry efficiently that can be fed with such a map. There are lots of tutorials available of how to craft terrain or you download some height map from one of the countless game resource sites. I added a simple grassy texture to give it an impression of a hilly countryside. Adding fog in the bluish base color of the sky makes it nicely blend into the scene.
For the cannon I just modeled two barrels with fancy muzzles. You can find the blender scenes in the /res folder. Crosshairs quickly drawn in Inkscape are added to the screen center as a transparent texture to aid target acquisition. Muzzle flashes and smoke effects are explosion textures I found on the fine OpenGameArt site.
Our opponent, the sleek Su-35 is from a 3D art resource site I just cannot find anymore and the zip file does not contain any additional info, sorry.
So, as with all aspects of life, it's always good to know a bit of craftsmanship yourself and at least know where to look for stuff you are unable do yourself. :)
Conclusion
flasim shows that it's quite simple to put together a mini game using a graphics engine and some knowledge of C++. If you want to take it from there, maybe try to extend the game by some realistic shot tracing and account for correct aim deflection. Or more precise hit-testing using collision meshes instead of bounding-box hit tests. Or get a physics engine involved. And and and....
In case you have additional questions, leave a comment or contact me.
Links
- Irrlicht Graphics Engine
- cAudio OpenAL wrapper, forked and kept updated, AUR package
- flasim repo
- flasim precompiled for ubuntu (assumes irrlicht and openal apt'ed)
- flasim precompiled for windows
Comments total: 0