Open the Room Editor and click on File -> New room, then write "2" next to Room width and Room height.
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.
That's it. Save the room as tut03.qr and close the Room Editor.
This is the image I'll be using (a spaceship and a missile), called 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.
#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.
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 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 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.
};
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