2D Game Framework

As part of my MComp Games Computing self-directed project, I developed a 2D games framework in C++, using OpenGL with a custom maths library to recreate a level of Mega Man 2. It served as my first exploration into manual implementation of engine-level features, including:

  • Graphics with modern OpenGL
  • Image and audio loading
  • Supporting multiple operating systems (Windows and Linux)

Update: The framework followed many of the object-oriented practices taught in academic environments, some of which I have since found to be flawed or able to be improved upon. See my newer engine project Insolence for a look at how a project may be better structured.

Game loop

The game loop is extended from a base class that deals with setting up the rendering environment. It provides four methods that can be overridden:

class Game : public jframe::JGame // Base class is within 
                //the jframe namespace
{
    void Initialise()
    {
    // Program runs this function once
    // Used for loading assets and setting up
    }

    void Update(const jutil::GameTime& gameTime)
    {
    // Runs at a fixed timestep
    // Handle input, collisions etc. here
    }

    void Draw()
    {
    // Used for making draw calls to sprites
    }

    void Unload()
    {
    // This will be called once after the final update call
    // (i.e. when the program is terminated)
    // Use this for cleaning up memory
    }

The game loop begins with an initialisation, and then follows a cycle of updating and drawing until the application terminates, at which point it unloads. Such a system can be used to load game assets, render them to screen, update them with game logic, and clean up memory when the application finishes. This class is provided as a sample project to be used as a template to get a window set up without hassle.

Drawing sprites

The JFrame renderer is based around Sprites, which are individually managed sprites, and SpriteSets, which are batched rendered (and useful for things like level tiles). A sprite could be loaded, given a texture, and moved across the screen by a little bit each frame like so:

jvis::Sprite testSprite;
jvis::Texture* testTexture;

void Initialise()
{
    testTexture = jvis::Texture::Load("Content/testImage.bmp");
    testSprite.SetTexture(testTexture);
}

void Update(const jutil::GameTime& gameTime)
{
    testSprite.MoveX(0.1f);
}

void Draw()
{
    testSprite.Draw();
}

void Unload()
{
    delete testTexture;
}

Playing audio

Audio works in a similar way to sprites. We have an AudioManager, which can hold a number of AudioTracks. AudioTracks can be played, stopped, have their volume changed, and other expected audio-related things.


JSnd::AudioManager* audioManager;
JSnd::AudioTrack* testTrack;

void Initialise()
{
    testTrack = audioManager->CreateTrack("Content/testSound.wav");
}

void Update()
{
    if (GetWindow()->GetKey(JKEY_KEY_SPACEBAR) == JKEY_PRESS)
        testTrack->PlayInstance();

    audioManager->Manage();
}

void Draw()
{

}

void Unload()
{
    // Can release individual tracks
    // Not necessary here since releasing audio manager
    // will clear all linked tracks
    // audioManager->ReleaseTrack(testTrack); 
   
    audioManager->Release();
}

The above sample will play an instance of a track whenever the space bar is tapped - PlayInstance() allows the playback of duplicate buffers (suitable for things like bullet sounds that are used by more than one entity, and can overlap). If only one instance of a sound is required (such as a background track), this can be done with the Play() function. The Audio Manager will keep track of when duplicate audio buffers have finished playing, and clean them up appropriately. Individual tracks can be unloaded from the Audio Manager, or the Audio Manager and all linked tracks can be cleaned up at once.

Input

Input management is crucial to any game logic, and should be easily managed. In JFrame, keyboard and mouse input is related to the window. An example of keyboard input is available in the above JSnd sample. JFrame also accepts mouse input. The following sample will create a sprite that follows the cursor on screen.

void Update()
{
    testSprite.SetPosition(GetWindow()->GetMousePos());

    if (GetWindow()->GetMouseState(JMOUSE_LEFT) == JMOUSE_CLICK)
        testSprite.SetColour(jmath::vec4(1.f, 0.f, 0.f, 1.f));
    else if (GetWindow()->GetMouseState(JMOUSE_LEFT) == JMOUSE_RELEASE)
        testSprite.SetColour(jmath::vec4(0.f, 1.f, 0.f, 1.f));
}

The cursor position is easily retrieved from the window and the sprite position is set to it. The state of mouse buttons and keys (Press, Hold, Release) can also be obtained. In this sample, the colour of the sprite is turned red whilst the left mouse button is held, and turned blue when it is released.