Tutorial 3 - Game Objects, Sprites and Scrolling

In this tutorial, we will create a spaceship armed with missiles and move it around a scrollable room.

The Scrollable Room

Last time, we created a room that was one screen wide and one screen tall. You can't do any scrolling in a room that fits completely inside the game window, so let's create a larger room.

Open the Room Editor and click on File -> New room, then write "2" next to Room width and Room height.

tut03newroom.png

You can now scroll around the room by using the Scroll tool (hand icon). Create a couple of background layers (use the same parameters from Tutorial 2 - Creating and Loading a Room File if you want), and fill the room with tiles.

tut03roomfinished.png

That's it. Save the room as tut03.qr and close the Room Editor.

The Sprites

To display our game objects, we're going to need a few sprites. An HGE resource manager is our best option here, so let's define two sprites in a resource script.

This is the image I'll be using (a spaceship and a missile), called spacesprites.png.

spacesprites.png

And this is the resource script to load them, tut03sprites.txt:

Texture texSprites {
    filename = spacesprites.png
}

Sprite sprShip {
    texture = texSprites
    rect = 0,0,44,64
    hotspot = 22,32
}

Sprite sprMissile {
    texture = texSprites
    rect = 44,43,10,2
    hotspot = 5,20
}

Save both of these files in the project's output directory.

Planning the Game's Classes

Back to C++! Start by declaring the global HGE pointer, a resource manager, and a few constants:

#include <hge.h>
#include <hgeresource.h>
#include <qafEnvironment.h>

using namespace qaf;

HGE * hge = NULL;
hgeresourceManager * pResManager = NULL;

#define SHIP_ACCEL     800.0f
#define SHIP_TOPSPEED  200.0f
#define SHIP_ANGSPEED    2.0f
#define MISSILE_SPEED  400.0f

Now the real fun starts. Our "game" will have two types of objects:

Qaf favors a software design philosophy where each type of object is represented by a class. Thus, we will create two classes -- one for the ship, and another for the missile.

The Missile Class

Every game object needs to have qaf::GameObj as its base class, so the missile class declaration should look like this:

class MissileObj : public GameObj {
    private:
        hgeSprite * m_sprite; // Missile sprite
        Vector2D    m_pos;    // Missile position
        Vector2D    m_vel;    // Missile velocity
    
    public:
        // Constructor:
        MissileObj ( Vector2D pos, Vector2D vel ) {
            m_pos = pos;
            m_vel = vel;
            m_sprite = pResManager->GetSprite( "sprMissile" );
        }

Finally, it's time to program the behavior of our missiles. The qaf::GameObj class doesn't do anything on its own, but it defines a set of methods you can override to implement objects' actions. In this tutorial, we will use two of them: update() and render().

The update() method is a callback used to notify the game object that it should update its internal state. Our missile needs to move (based on its velocity) and remove itself from the game when it leaves the room's area.

        void update ( int objLayer, float dt ) {
            // Move the missile:
            m_pos += m_vel * dt;
            
            // Check if the missile has left the room's area.
            Room * pRoom = Environment::getLoadedRoom();
            if ( m_pos.x < 0 || m_pos.x > pRoom->getPixelWidth() ||
                 m_pos.y < 0 || m_pos.y > pRoom->getPixelHeight() ) {
                // The missile has left the room!
                // We can remove it from the game and delete it permanently.
                Environment::removeGameObj( this, true );
            }
        }

The render method is a callback used to notify the game object that it should render itself with HGE functions. Here, we're going to rotate the missile sprite so it will always face towards its velocity, and render it. The scrollX and scrollY parameters are extremely important! You need to subtract these values from the coordinates of everything you render on the screen, or the objects will not appear to scroll along with the background.

        void render ( int objLayer, float scrollX, float scrollY ) {
            // Since the missile sprite is pointing upwards, let's get the
            // relative angle between the velocity and a vector pointing up.
            Vector2D vUp = Vector2D(0, -1);
            float rot = vUp.angle( m_vel );
            
            // Render it:
            m_sprite->RenderEx( m_pos.x - scrollX, m_pos.y - scrollY, rot );
        }

That's all we need for the missile class.

};

The Ship Class

The ship is a more complex object. It will be controlled by the player with directional arrows (left/right to turn, up/down to move) and the space bar (to shoot missiles). Furthermore, we can't allow it to leave the playfield.

The class declaration is similar to the missile, but we also need to store the ship's current direction:

class ShipObj : public GameObj {
    private:
        hgeSprite * m_sprite; // Ship sprite
        Vector2D    m_pos;    // Ship position
        Vector2D    m_vel;    // Ship velocity
        Vector2D    m_dir;    // Ship direction (unit vector)
    public:
        // Constructor:
        ShipObj ( Vector2D pos, Vector2D dir ) {
            m_pos = pos;
            m_vel = Vector2D(0, 0);
            m_dir = dir.unit(); // Make sure it's a unit vector!
            m_sprite = pResManager->GetSprite( "sprShip" );
        }   

When updating the ship, we need to do the following:

        void update ( int objLayer, float dt ) {
            // Calculate acceleration:
            Vector2D accel = Vector2D(0, 0);
            
            if ( hge->Input_GetKeyState( HGEK_UP ) )
                accel += m_dir * SHIP_ACCEL;
            
            if ( hge->Input_GetKeyState( HGEK_DOWN ) )
                accel -= m_dir * SHIP_ACCEL;
            
            // Move:
            m_pos += m_vel * dt + 0.5f * accel * dt * dt;
            
            // Update velocity:
            m_vel += accel * dt;
            
            if ( m_vel.length() > SHIP_TOPSPEED )
                m_vel = m_vel.unit() * SHIP_TOPSPEED;
            
            // Rotate clockwise?
            if ( hge->Input_GetKeyState( HGEK_RIGHT ) ) {
                m_dir = m_dir.rotate( SHIP_ANGSPEED * dt );
                m_dir.normalize(); // Prevent floating-point precision errors
            }
            
            // Rotate counter-clockwise?
            if ( hge->Input_GetKeyState( HGEK_LEFT ) ) {
                m_dir = m_dir.rotate( -SHIP_ANGSPEED * dt );
                m_dir.normalize(); // Prevent floating-point precision errors
            }
            
            // Check if the ship has left the playfield.
            Room * pRoom = Environment::getLoadedRoom();
            if ( m_pos.x < 50 )
                m_pos.x = 50.0f;
            
            if ( m_pos.x > pRoom->getPixelWidth() - 50 )
                m_pos.x = (float) (pRoom->getPixelWidth() - 50);
            
            if ( m_pos.y < 50 )
                m_pos.y = 50.0f;
            
            if ( m_pos.y > pRoom->getPixelHeight() - 50 )
                m_pos.y = (float) (pRoom->getPixelHeight() - 50);
            
            // Force scrolling to follow the ship:
            Environment::centerScrollingPoint( (int) m_pos.x, (int) m_pos.y );
            
            // Shoot a missile?
            if ( hge->Input_GetKey() == HGEK_SPACE ) {
                // Create a new missile object and put it in the Environment.
                Environment::addGameObj( new MissileObj( m_pos, m_dir * MISSILE_SPEED ) );
            }
        }

I can hear someone shouting: "You created a new MissileObj, but you didn't store a pointer to it! You won't be able to delete it later! You're going to have a memory leak!"

That's not completely true. While we're not explicitly keeping track of all created missile objects, the Environment is! If you look at the code in MissileObj::update(), you'll see we're calling Environment::removeGameObj() with its second parameter set to true. This instructs the Environment to automatically delete the object at the end of the frame function.

This "distributed memory management" will be a prevalent design pattern in these tutorials. We don't need to keep memory managers or global object lists, but we do need to be careful and make sure every object will eventually delete itself.

The rendering code is nearly identical to MissileObj's.

        void render ( int objLayer, float scrollX, float scrollY ) {
            // Since the ship sprite is pointing upwards, let's get the
            // relative angle between the direction and a vector pointing up.
            Vector2D vUp = Vector2D(0, -1);
            float rot = vUp.angle( m_dir );
            
            // Render it:
            m_sprite->RenderEx( m_pos.x - scrollX, m_pos.y - scrollY, rot );
        }

And we're done with our ship.

};

Bringing It All Together

The frame function and initialization code haven't changed at all. This time, however, we not only need to load a room, but also load the resources and place a ship object inside it. In WinMain:

    if ( !hge->System_Initiate() )
        MessageBox( NULL, hge->System_GetErrorMessage(), "Error", MB_OK | MB_ICONERROR | MB_SYSTEMMODAL );
    
    // Load resources:
    pResManager = new hgeResourceManager( "tut03sprites.txt" );
    
    // Set up the environment:
    Environment::initialize( false, true );
    
    // Load the room:
    Environment::loadRoom( "tut03.qr" );
    
    // The player's avatar:
    Environment::addGameObj( new ShipObj( Vector2D(320, 240), Vector2D(0, -1) ) );
    
    // Start the game loop:
    if ( !hge->System_Start() ) {
        MessageBox( NULL, hge->System_GetErrorMessage(), "Error", MB_OK | MB_ICONERROR | MB_SYSTEMMODAL );
    }
    
    // Shutdown:
    Environment::shutdown();
    hge->System_Shutdown();
    hge->Release();

Once again, we're allocating a new ShipObj and neglecting to store the resulting pointer. We're still safe, though: When we shutdown() Qaf, any objects that were left in the environment are automatically deleted.

See the full source code for this tutorial in the file: tutorials/tutorial03.cpp


Generated on Sun Mar 25 12:32:13 2007 for Qaf Framework by  doxygen 1.5.1-p1