*//*
In this lesson we will be creating an animated gravitational planet/moon system. This will require us to subclass the pure virtual Engine2::Entity class with our own application-specific implementation, and to write code to update the positions of the game objects to simulate gravitation. The latter will be in a custom subclass of Engine2::World.
Below, in the documentation for AwesomeWorld::HandleFrame, there is an explanation of Euler Integration, which is absolutely central to game programming. Make sure not to skip it.
First, more detail on Object and Entity and their relationship.
As shown in the previous lesson, Object is the physical, visible game object which has as its properties: position, scale and angle. By itself, it can't move or be interacted with. Its subclasses implement its Draw method -- as of Sept 2006, Sprite and Compound. Alone, this is referred to as a "static object" (as opposed to a "dynamic object"; see Xrb::Engine2::Object::IsDynamicObject).
Entity (Xrb::Engine2::Entity) can be thought of as "imbuing a soul" upon Object. Entity is a pure virtual class intended to be subclassed to add all the application-specific properties and code necessary for an interactive game object. An Object and Entity are both instantiated, and then the Entity instance is attached to the Object instance, and which point, the Object instance is a "dynamic object". The Entity baseclass doesn't provide any substance besides the bare minimum framework -- the actual form of the "soul" is completely up to the application-specific implementation.
In this particular lesson, the application-specific properties we will add to our custom subclass of Entity are mass, velocity and accumulated force. These values will be used by the gravitation simulation code to update the position of each respective dynamic object.
Our custom subclass of World will perform two functions: a one-time generation of the game world (a large planet and many orbiting moons), and once-per-frame calculations for the simulation of the gravitational system using the properties of each dynamic object in the World.
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. /*
*/ class AwesomeEntity : public Engine2::Entity { public: // The constructor simply initializes the properties to sane values. Mass // must be greater than zero to avoid division by zero in some calculations. AwesomeEntity () : Engine2::Entity(), m_mass(1.0f), m_velocity(FloatVector2::ms_zero), m_force(FloatVector2::ms_zero) { } // Trivial accessors for the properties of AwesomeEntity. inline Float Mass () const { return m_mass; } inline FloatVector2 const &Velocity () const { return m_velocity; } inline FloatVector2 const &Force () const { return m_force; } // Modifiers for the properties of AwesomeEntity. The ASSERT_NAN_SANITY_CHECK // macro is used in various places in Engine2 code to quickly catch common // bugs in game code which result in NaN values being fed to the snake. inline void SetMass (Float mass) { ASSERT_NAN_SANITY_CHECK(Math::IsFinite(mass)); ASSERT1(mass > 0.0f); m_mass = mass; } inline void SetVelocity (FloatVector2 const &velocity) { ASSERT_NAN_SANITY_CHECK(Math::IsFinite(velocity[Dim::X])); ASSERT_NAN_SANITY_CHECK(Math::IsFinite(velocity[Dim::Y])); m_velocity = velocity; } // Procedures which will be used by the gravity calculations in AwesomeWorld. void IncrementVelocity (FloatVector2 const &velocity_delta) { ASSERT_NAN_SANITY_CHECK(Math::IsFinite(velocity_delta[Dim::X])); ASSERT_NAN_SANITY_CHECK(Math::IsFinite(velocity_delta[Dim::Y])); m_velocity += velocity_delta; } void IncrementForce (FloatVector2 const &force_delta) { ASSERT_NAN_SANITY_CHECK(Math::IsFinite(force_delta[Dim::X])); ASSERT_NAN_SANITY_CHECK(Math::IsFinite(force_delta[Dim::Y])); m_force += force_delta; } void ResetForce () { m_force = FloatVector2::ms_zero; } // These are pure virtual methods which have to be implemented in Entity // subclasses. Write can be left blank for our purposes. virtual void Write (Serializer &serializer) const { } // This method is called on entities which have hit the side of the // ObjectLayer. For now we'll just stop the entity's motion along the // indicated dimension(s). This method will only be called on entities // in a non-wrapped ObjectLayer. virtual void HandleObjectLayerContainment (bool component_x, bool component_y) { if (component_x) m_velocity[Dim::X] = 0.0f; if (component_y) m_velocity[Dim::Y] = 0.0f; } protected: // These are pure virtual methods which have to be implemented in Entity // subclasses. For our purposes, they can be left empty. virtual void HandleNewOwnerObject () { } virtual void CloneProperties (Entity const *entity) { } private: Float m_mass; FloatVector2 m_velocity; FloatVector2 m_force; }; // end of class AwesomeEntity /*
*/ class AwesomeWorld : public Engine2::World { public: /*
*/ AwesomeWorld () : Engine2::World(NULL), m_gravitational_constant(60.0f) { // At this point, the world is empty. // Decide some size for the ObjectLayer (the hardcoded scale factors // and translations below are loosely dependent on this value). static Float const s_object_layer_side_length = 2000.0f; // Create the ObjectLayer which will hold our game objects. Engine2::ObjectLayer *object_layer = Engine2::ObjectLayer::Create( this, // owner world false, // not wrapped s_object_layer_side_length, // side length 6, // visibility quad tree depth 0.0f); // z depth AddObjectLayer(object_layer); SetMainObjectLayer(object_layer); Engine2::Sprite *sprite; AwesomeEntity *planet; // Create a large, heavy planet. sprite = Engine2::Sprite::Create("resources/demi3_small.png"); planet = new AwesomeEntity(); sprite->SetEntity(planet); planet->SetTranslation(FloatVector2::ms_zero); planet->SetScaleFactor(250.0f); planet->SetMass(100.0f * planet->ScaleFactor() * planet->ScaleFactor()); AddDynamicObject(sprite, object_layer); // Create a bunch of small, light moons. static Uint32 const s_moon_count = 50; for (Uint32 i = 0; i < s_moon_count; ++i) { sprite = Engine2::Sprite::Create("resources/shade3_small.png"); AwesomeEntity *moon = new AwesomeEntity(); sprite->SetEntity(moon); sprite->SetZDepth(-0.1f); moon->SetScaleFactor(Math::RandomFloat(10.0f, 20.0f)); moon->SetMass(0.01f * moon->ScaleFactor() * moon->ScaleFactor()); // Pick a distance to orbit the moon at. Float minimum_orbital_radius = planet->ScaleFactor() + moon->ScaleFactor() + 100.0f; Float orbital_radius = Math::RandomFloat(minimum_orbital_radius, minimum_orbital_radius + 400.0f); ASSERT1(orbital_radius > 0.0f); // The moon will be placed randomly using polar coordinates. // We've calculated the R value, now we need theta. Float angle = Math::RandomFloat(0.0f, 360.0f); // Initialize the moon's position moon->SetTranslation(orbital_radius * Math::UnitVector(angle)); // In order to figure out what speed to use to set the moon into // circular orbit, we need to know the magnitude of the gravitational // force between it and the large planet. Float gravitational_force = CalculateGravitationalForce(planet, moon); /*
We must also replace by known quantities. This can be done using Newton's Second Law; let be scalar acceleration, be scalar force (magnitude of the force vector), and be mass (of the orbiting body, so in this case, the small moon).
Composing the two, we get
Solve for :
*/ Float orbital_speed = Math::Sqrt(gravitational_force * orbital_radius / moon->Mass()); // The velocity must be perpendicular to the vector joining the // centers of the planet and the moon. moon->SetVelocity(orbital_speed * Math::UnitVector(angle+90.0f)); // Finally add it to the world. AddDynamicObject(sprite, object_layer); } } protected: /*
*/ virtual void HandleFrame () { Uint32 entity_capacity = EntityCapacity(); // Apply gravitational forces between each distinct pair of entities. for (Uint32 i = 0; i < entity_capacity; ++i) { AwesomeEntity *entity0 = dynamic_cast<AwesomeEntity *>(GetEntity(i)); if (entity0 == NULL) continue; for (Uint32 j = i+1; j < entity_capacity; ++j) { AwesomeEntity *entity1 = dynamic_cast<AwesomeEntity *>(GetEntity(j)); if (entity1 == NULL) continue; ASSERT1(entity0 != entity1); // Use the helper function to calculate the gravitational force // between the two entities. Float gravitational_force = CalculateGravitationalForce(entity0, entity1); ASSERT1(gravitational_force >= 0.0f); // If the force is zero (which can happen when the entities' // centers coincide and the gravitation equation would divide // by zero), skip this entity pair. if (gravitational_force == 0.0f) continue; // The gravitational force is from entity0 to entity1 FloatVector2 force_direction = (entity1->Translation() - entity0->Translation()).Normalization(); // Apply equal and opposite gravitational force to both entities. entity0->IncrementForce( gravitational_force * force_direction); entity1->IncrementForce(-gravitational_force * force_direction); } } /*
FrameDT()
). In the following, the time-step is given by dt
.
principal += derivative * dt
In this lesson, there are two integrations to perform: updating velocity (the principal) using acceleration (the derivative), and updating position (the principal) using velocity (the derivative). It should be noted that both of these values are vector-valued, and that the above and above-referenced descriptions of Euler Integration appear to be scalar-valued. The derivative of a vector value is defined as a component-wise derivative (the vector containing derivative of the X-component and the derivative of the Y-component). The time delta is always scalar.
Believe it or not, by doing this, you're actually computing numeric solutions for differential equations. Euler Integration is a very simple method for numeric integration, but is relatively inaccurate. Fortunately for the purposes of games, it works just fine. For a more accurate method, see http://en.wikipedia.org/wiki/Runge-Kutta_methods -- it is much more complicated and difficult to implement, but if accuracy is a consideration (e.g. in scientific computation) then it's worth it.
*/ // Update the velocity vector of each entity with the accumulated force // and update the position vector using the newly calculated velocity. for (Uint32 i = 0; i < entity_capacity; ++i) { AwesomeEntity *entity = dynamic_cast<AwesomeEntity *>(GetEntity(i)); if (entity == NULL) continue; ASSERT1(entity->Mass() > 0.0f); // Use Euler Integration to calculate the new velocity, based on // the accumulated force during this frame. entity->IncrementVelocity(entity->Force() / entity->Mass() * FrameDT()); // Reset the accumulated force for next frame. entity->ResetForce(); // Use Euler Integration again to calculate the new position, // based on the entity's velocity. entity->Translate(entity->Velocity() * FrameDT()); } /*
*/ Engine2::World::HandleFrame(); } private: /*
See http://en.wikipedia.org/wiki/Gravitation for more info. In order to prevent a divide by zero, if the entities are too close (overlapping), the return value is zero.
*/ Float CalculateGravitationalForce (AwesomeEntity *entity0, AwesomeEntity *entity1) const { ASSERT1(entity0 != NULL && entity1 != NULL); FloatVector2 entity_offset(entity1->Translation() - entity0->Translation()); Float distance = entity_offset.Length(); // If they're touching, don't apply gravitational force (this // is to avoid a divide by zero if their positions coincide). if (distance < entity0->ScaleFactor() + entity1->ScaleFactor()) return 0.0f; else return m_gravitational_constant * entity0->Mass() * entity1->Mass() / (distance * distance); } Float m_gravitational_constant; }; // end of class AwesomeWorld /*
*/ 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) { } // Called by WorldViewWidget with all mouse wheel events for this WorldView virtual bool ProcessMouseWheelEvent (EventMouseWheel const *e) { // Rotate the view on ALT+mouse-wheel-up/down. if (e->IsEitherAltKeyPressed()) RotateView((e->ButtonCode() == Key::MOUSEWHEELUP) ? -15.0f : 15.0f); // Otherwise, we will zoom the view on mouse-wheel-up/down. else ZoomView((e->ButtonCode() == Key::MOUSEWHEELUP) ? 1.2f : 1.0f / 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()) { // Move the view by a delta which is calculated by transforming // the screen coordinates of the event to world coordinates as used // by WorldView. MoveView( ParallaxedScreenToWorld() * FloatVector2::ms_zero - ParallaxedScreenToWorld() * e->Delta().StaticCast<Float>()); // Indicates that the event was used by this method. return true; } else // Event not used. return false; } }; // end of class AwesomeWorldView 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 05"); // 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. AwesomeWorld *world = new AwesomeWorld(); // 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.002f); // Attach the newly created WorldView to the World object. world->AttachWorldView(world_view); /*
*/ // 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 = 60.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); } // Perform all off-screen game processing. 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 and world, in that order. This will // automatically delete our AwesomeWorldView instance. 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 bool ProcessKeyEvent (EventKey const *e)
in AwesomeWorldView). See how the different integration methods perform compared to each other at different framerates, especially low ones. Thus concludes lesson05, you crazy almost-game-programming bastard, you.