*//*
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. /*
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) { /*
*/ // 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 /*
*/ 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; /*
*/ 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. { /*
*/ // 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); /*
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); } /*
*/ 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(); } /*
*/ Delete(world_view_widget); Delete(world); } // Delete the Screen (and GUI hierarchy), "SHUT IT DOWN", and return success. Delete(screen); CleanUp(); return 0; } /*
Exercises
virtual void AwesomeWorldView::HandleFrame()
for this. You can use the Xrb::FrameHandler::FrameTime method to retrieve the current time from inside HandleFrame. virtual void AwesomeWorldView::HandleFrame()
for this. You can use the Xrb::FrameHandler::FrameTime method to retrieve the current time from inside HandleFrame. sprite->SetTranslation
call in CreateAndPopulateWorld and set the sprite's translation to somewhere outside the 1000x1000 grid of the ObjectLayer and see what happens. sprite->SetTranslation
call again and see what happens when you place a Sprite outside the domain of the ObjectLayer this time. virtual void AwesomeWorldView::HandleFrame()
to: zoom factor = 0.008 * sin(k*time) + 0.01
where k is an arbitrary constant you can pick (I suggest k = 90). You'll need to use the Xrb::Engine2::WorldView::SetZoomFactor method. angle = 400 * cos(k*time)
where k is an arbitrary constant you can pick (I suggest k = 90). You'll need to use the Xrb::Engine2::WorldView::SetAngle method. position = 500 * (cos(k*time), sin(k*time))
where k is an arbitrary constant you can pick (I suggest k = 90). You'll need to use the Xrb::Engine2::WorldView::SetCenter method. Thus concludes lesson04, you crazy almost-game-programming bastard, you.