Lesson 04 - Creating A Game World With Static Objects

 *//* 
This lesson will show you how to create and populate the world in which games are set. This lesson will only introduce static (as in unmoving) game objects and the structure thereof, as well as the structure of the classes which make up the game world.

This game engine provides a set of classes to facilitate creation of interactive 2D game worlds. The intention is to provide only common functionality in these base classes (which will grow to include semi-game-specific functionality if/when it proves to be common to most game implementations), and leave game-specific details up to each game's implementation. The important classes are:

There are two additional classes which facilitate rendering and integration into the GUI widget hierarchy.

In this lesson we will create a simple game world consisting of a random mess of sprites, and a customized WorldView subclass in which to implement view zooming, rotation and movement.

Procedural Overview -- Items in bold are additions/changes to the previous lesson.

Comments explaining previously covered material will be made more terse or deleted entirely in each successive lesson. If something is not explained well enough, it was probably already explained in previous lessons.

Code Diving!

 */
#include "xrb.hpp"                         // Must be included in every source/header file.

#include "xrb_engine2_objectlayer.hpp"     // For use of the Engine2::ObjectLayer class.
#include "xrb_engine2_sprite.hpp"          // For use of the Engine2::Sprite class.
#include "xrb_engine2_world.hpp"           // For use of the Engine2::World class.
#include "xrb_engine2_worldview.hpp"       // For use of the Engine2::WorldView class.
#include "xrb_engine2_worldviewwidget.hpp" // For use of the Engine2::WorldViewWidget class.
#include "xrb_event.hpp"                   // For use of the Event classes.
#include "xrb_eventqueue.hpp"              // For use of the EventQueue class.
#include "xrb_inputstate.hpp"              // For use of the InputState class (via Singleton::).
#include "xrb_input_events.hpp"            // For use of the EventMouseWheel class.
#include "xrb_math.hpp"                    // For use of the functions in the Math namespace.
#include "xrb_screen.hpp"                  // For use of the necessary Screen widget class.
#include "xrb_sdlpal.hpp"                  // For use of the SDLPal platform abstraction layer.

using namespace Xrb;                     // To avoid having to use Xrb:: everywhere.

/* 
Our customized WorldView class will implement view zooming, rotation and movement. The view will zoom in and out by mouse-wheeling-up and mouse-wheeling-down respectively. The view will rotate clockwise and counterclockwise by holding an ALT key and mouse-wheeling-up and mouse-wheeling-down respectively. View movement will be done by holding the left mouse button and dragging.

In order to process these mouse events, we will need to override a couple of methods in WorldView -- Xrb::Engine2::WorldView::ProcessMouseWheelEvent and Xrb::Engine2::WorldView::ProcessMouseMotionEvent. These methods do exactly what you think they do.

 */
class AwesomeWorldView : public Engine2::WorldView
{
public:

    // Trivial constructor which is just a frontend for WorldView's constructor.
    AwesomeWorldView (Engine2::WorldViewWidget *parent_world_view_widget)
        :
        Engine2::WorldView(parent_world_view_widget)
    { }

    // This method is called with all mouse wheel events received by this
    // WorldView object.  These aren't inherited from Widget (as WorldView
    // does not inherit Widget), but are called by their counterparts in
    // WorldViewWidget.
    virtual bool ProcessMouseWheelEvent (EventMouseWheel const *e)
    {
        /* 
Note that the accessor for ALT key state is on the event, and not on the Xrb::InputState singleton. This is because since events can be handled asynchronously, they must retain the key modifier states (e.g. ALT, CTRL) themselves.
 */
        // If either ALT key is pressed, we will rotate the view depending
        // on which of mouse-wheel-up or mouse-wheel-down this event indicates.
        if (e->IsEitherAltKeyPressed())
        {
            if (e->ButtonCode() == Key::MOUSEWHEELUP)
                RotateView(-15.0f); // Rotate 15 degrees clockwise.
            else
            {
                ASSERT1(e->ButtonCode() == Key::MOUSEWHEELDOWN);
                RotateView(15.0f); // Rotate 15 degrees counterclockwise.
            }
        }
        // Otherwise, we will zoom the view depending on which of
        // mouse-wheel-up or mouse-wheel-down this event indicates.
        else
        {
            if (e->ButtonCode() == Key::MOUSEWHEELUP)
                ZoomView(1.2f); // Zoom in by a factor of 1.2f
            else
            {
                ASSERT1(e->ButtonCode() == Key::MOUSEWHEELDOWN);
                ZoomView(1.0f / 1.2f); // Zoom out by a factor of 1.2f
            }
        }
        // Indicates that the event was used by this method.
        return true;
    }
    // This method is the mouse motion analog of ProcessMouseWheelEvent.
    virtual bool ProcessMouseMotionEvent (EventMouseMotion const *e)
    {
        // Only do stuff if the left mouse button was pressed for this event.
        if (e->IsLeftMouseButtonPressed())
        {
            // This transforms the screen-coordinate movement delta of the
            // mouse motion event into world-coordinates.
            FloatVector2 position_delta(
                ParallaxedScreenToWorld() * e->Delta().StaticCast<Float>() -
                ParallaxedScreenToWorld() * FloatVector2::ms_zero);
            // Move the view using the calculated world-coordinate delta.  We
            // negate the delta because by dragging the view down, the view
            // should move up; while dragging, the mouse cursor should always
            // stay on the same spot relative to the game world.
            MoveView(-position_delta);
            // Indicates that the event was used by this method.
            return true;
        }
        else
            // Event not used.
            return false;
    }
}; // end of class AwesomeWorldView

/* 
This is a helper function which will create our World instance and populate it with Sprites. The return value is the created World object.
 */
Engine2::World *CreateAndPopulateWorld ()
{
    // Create the world via the static method Engine2::World::Create.
    // The first parameter is a pointer to a PhysicsHandler object, however
    // we will not implement a PhysicsHandler in this lesson.
    Engine2::World *world = Engine2::World::CreateEmpty(NULL);
    // Decide some size for the ObjectLayer (arbitrary).
    static Float const s_object_layer_side_length = 1000.0f;
    /* 
Using the static method Xrb::Engine2::ObjectLayer::Create we will create the ObjectLayer in which we'll add all the Sprites. The parameters are: We will then add the created ObjectLayer to the World object and set its main ObjectLayer (the meaning of this will be explained in later lessons).
 */
    Engine2::ObjectLayer *object_layer =
        Engine2::ObjectLayer::Create(
            world,                      // owner world
            false,                      // not wrapped
            s_object_layer_side_length, // side length
            6,                          // visibility quad tree depth
            0.0f);                      // z depth
    world->AddObjectLayer(object_layer);
    world->SetMainObjectLayer(object_layer);

    static Uint32 const s_object_count = 100;
    // Create a random mess of objects
    for (Uint32 i = 0; i < s_object_count; ++i)
    {
        // Create the sprite using the texture with given path
        Engine2::Sprite *sprite = Engine2::Sprite::Create("resources/interloper2_small.png");
        // Place the sprite randomly on the 1000x1000 ObjectLayer.  The
        // ObjectLayer is centered on the origin, so the valid range of
        // coordinates are [-500,500] for both X and Y.
        sprite->SetTranslation(
            FloatVector2(Math::RandomFloat(-500.0f, 500.0f), Math::RandomFloat(-500.0f, 500.0f)));
        // Size the sprite between 1% and 10% (arbitrary) of the ObjectLayer
        sprite->SetScaleFactor(s_object_layer_side_length * Math::RandomFloat(0.01f, 0.1f));
        // Set the angle of the sprite randomly
        sprite->SetAngle(Math::RandomFloat(0.0f, 360.0f));
        // Add the sprite to the object layer
        world->AddStaticObject(sprite, object_layer);
    }
    // Return the created World object.
    return world;
}

void CleanUp ()
{
    fprintf(stderr, "CleanUp();\n");
    // Shutdown the Pal and singletons.
    Singleton::Pal().Shutdown();
    Singleton::Shutdown();
}

int main (int argc, char **argv)
{
    fprintf(stderr, "main();\n");

    // Initialize engine singletons.
    Singleton::Initialize(SDLPal::Create, "none");
    // Initialize the Pal.
    if (Singleton::Pal().Initialize() != Pal::SUCCESS)
        return 1;
    // Set the window caption.
    Singleton::Pal().SetWindowCaption("XuqRijBuh Lesson 04");
    // Create Screen object and initialize given video mode.
    Screen *screen = Screen::Create(800, 600, 32, false);
    // If the Screen failed to initialize, print an error message and quit.
    if (screen == NULL)
    {
        CleanUp();
        return 2;
    }

    // Here is where the application-specific code begins.
    {
        /* 
Our application specific code consists of creating the game world and the two classes necessary to draw the game world via the GUI hierarchy.
 */
        // Create our sweet game world via a call to CreateAndPopulateWorld.
        Engine2::World *world = CreateAndPopulateWorld();
        // Create the WorldViewWidget as a child of screen.  This is what will
        // contain an instance of WorldView and will cause it to be rendered.
        Engine2::WorldViewWidget *world_view_widget = new Engine2::WorldViewWidget(screen);
        screen->SetMainWidget(world_view_widget);
        // Create an instance of our AwesomeWorldView, using the newly created
        // WorldViewWidget as its "parent".  Set the zoom factor so something
        // reasonable (though arbitrary).
        AwesomeWorldView *world_view = new AwesomeWorldView(world_view_widget);
        world_view->SetZoomFactor(0.004f);
        // Attach the newly created WorldView to the World object.
        world->AttachWorldView(world_view);
        /* 
Below, the game loop remains unchanged relative to the previous lesson except for a call to world->ProcessFrame(current_real_time) which handles all off-screen game computation.
 */
        // These values will be used below in the framerate control code.
        Float current_real_time = 0.0f;
        Float next_real_time = 0.0f;
        Float desired_framerate = 30.0f;
        // Run the game loop until the Screen no longer has the will to live.
        while (!screen->IsQuitRequested())
        {
            // Get the current real time and figure out how long to sleep, then sleep.
            current_real_time = 0.001f * Singleton::Pal().CurrentTime();
            Sint32 milliseconds_to_sleep = Max(0, static_cast<Sint32>(1000.0f * (next_real_time - current_real_time)));
            Singleton::Pal().Sleep(milliseconds_to_sleep);
            // Calculate the desired next game loop time
            next_real_time = Max(current_real_time, next_real_time + 1.0f / desired_framerate);

            // Process events until there are no more.
            Event *event = NULL;
            while ((event = Singleton::Pal().PollEvent(screen, current_real_time)) != NULL)
            {
                // Let the InputState singleton "have a go" at keyboard/mouse events.
                if (event->IsKeyEvent() || event->IsMouseButtonEvent())
                    Singleton::InputState().ProcessEvent(event);
                // Give the GUI hierarchy a chance at the event and then delete it.
                screen->ProcessEvent(event);
                Delete(event);
            }

            /* 
This call is what performs all off-screen game processing, mainly by World, and then in later lessons by PhysicsHandler and Entity subclasses. This is where all game logic and physics processing will happen.
 */
            world->ProcessFrame(current_real_time);

            // Turn the EventQueue crank, Perform off-screen GUI processing,
            // turn the EventQueue crank again, and then draw everything.
            screen->OwnerEventQueue()->ProcessFrame(current_real_time);
            screen->ProcessFrame(current_real_time);
            screen->OwnerEventQueue()->ProcessFrame(current_real_time);
            screen->Draw();
        }

        /* 
Although screen would automatically delete world_view_widget if left around, we must delete it ourselves in order to ensure proper deletion order relative to World. Deletion of WorldViewWidget will cause the deletion of the attached WorldView which will in turn automatically detach itself from World. Having no attached WorldViews is a necessary precondition for the destruction of World.
 */
        Delete(world_view_widget);
        Delete(world);
    }

    // Delete the Screen (and GUI hierarchy), "SHUT IT DOWN", and return success.
    Delete(screen);
    CleanUp();
    return 0;
}
/* 

Exercises

Thus concludes lesson04, you crazy almost-game-programming bastard, you.


Hosted by SourceForge.net Logo -- Generated on Fri Aug 21 21:46:38 2009 for XuqRijBuh by doxygen 1.5.8