00001 // /////////////////////////////////////////////////////////////////////////// 00002 // lesson04_main.cpp by Victor Dods, created 2006/08/06 00003 // /////////////////////////////////////////////////////////////////////////// 00004 // Unless a different license was explicitly granted in writing by the 00005 // copyright holder (Victor Dods), this software is freely distributable under 00006 // the terms of the GNU General Public License, version 2. Any works deriving 00007 // from this work must also be released under the GNU GPL. See the included 00008 // file LICENSE for details. 00009 // /////////////////////////////////////////////////////////////////////////// 00010 00011 00012 // /////////////////////////////////////////////////////////////////////////// 00013 // Lesson 04 - Creating A Game World With Static Objects 00014 // /////////////////////////////////////////////////////////////////////////// 00015 00016 /* @endcode 00019 This lesson will show you how to create and populate the world in which games 00020 are set. This lesson will only introduce static (as in unmoving) game objects 00021 and the structure thereof, as well as the structure of the classes which make 00022 up the game world. 00023 00024 <ul> 00025 <li>@ref lesson04_main.cpp "This lesson's source code"</li> 00026 <li>@ref lessons "Main lesson index"</li> 00027 </ul> 00028 00029 This game engine provides a set of classes to facilitate creation of 00030 interactive 2D game worlds. The intention is to provide only common 00031 functionality in these base classes (which will grow to include 00032 semi-game-specific functionality if/when it proves to be common to most game 00033 implementations), and leave game-specific details up to each game's 00034 implementation. The important classes are: 00035 00036 <ul> 00037 <li>@ref Xrb::Engine2::World is what contains everything and is the main 00038 point of control for non-rendering game logic. Games may subclass 00039 it to provide game flow logic such as enemy spawning or victory 00040 condition detection. World contains multiple instances of 00041 ObjectLayer, all instances of Entity, and optionally an instance of 00042 PhysicsHandler.</li> 00043 <li>@ref Xrb::Engine2::ObjectLayer contains all instances of Object. 00044 Depending on how the game world is rendered, ObjectLayer can be used 00045 to represent different depths in a view-parallaxed scene (i.e. foreground, 00046 midground and background layers).</li> 00047 <li>@ref Xrb::Engine2::Object is the baseclass for the physical, tangible 00048 game objects -- Sprite and Compound. Itself, an Object can't move; 00049 see Entity. An Object not paired with an Entity is referred to as a 00050 "static object" because of its lack of movement/interaction. An Object 00051 has physical properties such as position, scale, and angle.</li> 00052 <li>@ref Xrb::Engine2::Sprite is a subclass of Object and implements 00053 rectangular, single-texture game objects.</li> 00054 <li>@ref Xrb::Engine2::Compound is a subclass of Object and implements 00055 conglomerates of arbitrary textured polygons. Compound will be covered 00056 in later lessons.</li> 00057 <li>@ref Xrb::Engine2::Entity is the baseclass for game-specific interactive 00058 objects. An Entity is intangible by itself, and must be attached to 00059 an Object. An Object with an attached Entity is referred to as a 00060 "dynamic object". Dynamic objects are specially treated by the World 00061 to allow them to move within the game world and interact with other 00062 dynamic objects. Entity will be subclassed in each game's implementation 00063 to provide game-specific behavior (example subclasses could be 00064 SpaceShip, Missile, or even invisible things such as DamageArea). 00065 Entity will be covered in depth in later lessons.</li> 00066 <li>@ref Xrb::Engine2::PhysicsHandler is the baseclass for game-specific 00067 collision detection and resolution. When a dynamic object is added to 00068 World, its Entity is passed to the PhysicsHandler. The game-specific 00069 implementation of PhysicsHandler may then track each Entity however it 00070 chooses. PhysicsHandler will be covered in detail in later lessons.</li> 00071 </ul> 00072 00073 There are two additional classes which facilitate rendering and integration 00074 into the GUI widget hierarchy. 00075 00076 <ul> 00077 <li>@ref Xrb::Engine2::WorldView is what actually renders the game world. 00078 The reason rendering is kept separate from World itself is for two 00079 reasons. 00080 <ul> 00081 <li>This design follows the document/view paradigm. Therefore, a 00082 single document (World) may have multiple views (WorldView).</li> 00083 <li>This allows a separation of off-screen World processing and 00084 WorldView rendering which is necessary for real-time network games; 00085 the server performs off-screen World processing at some relatively low, 00086 fixed framerate, while the clients render WorldView at a relatively high 00087 framerate (interpolating the position/scale/angle of the game objects 00088 to create the illusion of arbitrarily smooth movement despite the low 00089 "real" framerate dictated by the server).</li> 00090 </ul> 00091 As of September 2006, independent WorldView rendering is not implemented; 00092 rendering must run synchronously with off-screen World processing. Also 00093 as of September 2006, only top-down, parallaxed world rendering is 00094 implemented, though there's nothing stopping you from writing your own 00095 implementation. Other implementations could include isometric (e.g. 00096 Starcraft). In the future, WorldView will be made into an interface 00097 class, and specific implementations of it will be branched off (e.g. 00098 ParallaxedWorldView, IsometricWorldView, RadarView etc).</li> 00099 <li>@ref Xrb::Engine2::WorldViewWidget is a subclass of Widget which contains 00100 a single instance of WorldView. In @ref lesson01 "an earlier lesson" 00101 I mentioned that everything visible in the game engine happens via use 00102 of a Widget or subclass thereof. This particular Widget is what ties 00103 the world rendering into the GUI hierarchy. It's very simple and 00104 straightforward, so you should probably never need to subclass or change 00105 it.</li> 00106 </ul> 00107 00108 <!-- TODO: graphical representation of Engine2 design --> 00109 00110 In this lesson we will create a simple game world consisting of a random mess of 00111 sprites, and a customized WorldView subclass in which to implement view zooming, 00112 rotation and movement. 00113 00114 <strong>Procedural Overview</strong> -- Items in bold are additions/changes to the previous lesson. 00115 00116 <ul> 00117 <li>Global declarations</li> 00118 <ul> 00119 <li><strong>Declare subclass of Engine2::WorldView specific to this app.</strong></li> 00120 <li><strong>CreateAndPopulateWorld function which will dynamically 00121 allocate the World and Object instances which inhabit said 00122 World.</strong></li> 00123 </ul> 00124 <li>Main function</li> 00125 <ul> 00126 <li>Initialize the Pal and game engine singletons. Create the Screen object.</li> 00127 <li>Execute game-specific code.</li> 00128 <ul> 00129 <li>Create application-specific objects and GUI elements, and make necessary signals.</li> 00130 <ul> 00131 <li><strong>Create the game world via CreateAndPopulateWorld.</strong></li> 00132 <li><strong>Create the WorldViewWidget and set it as screen's main widget.</strong></li> 00133 <li><strong>Create the game-specific WorldView.</strong></li> 00134 <li><strong>Attach the WorldView to the WorldViewWidget.</strong></li> 00135 <li><strong>Attach the WorldView to the World.</strong></li> 00136 </ul> 00137 <li>Run the game loop</li> 00138 <ul> 00139 <li>Calculate the Singleton::Pal().Sleep duration necessary to achieve the desired framerate.</li> 00140 <li>Handle events (user and system-generated).</li> 00141 <li>Perform off-screen processing, <strong>including game world processing.</strong></li> 00142 <li>Draw the Screen object's entire widget hierarchy.</li> 00143 </ul> 00144 <li><strong>Destroy application-specific objects.</strong></li> 00145 <ul> 00146 <li><strong>Destroy WorldViewWidget object, which will destroy WorldView object.</strong></li> 00147 <li><strong>Destroy World object, which will destroy all its ObjectLayers and Objects.</strong></li> 00148 </ul> 00149 </ul> 00150 <li>Delete the Screen object. Shutdown the Pal and game engine singletons.</li> 00151 </ul> 00152 </ul> 00153 00154 Comments explaining previously covered material will be made more terse or 00155 deleted entirely in each successive lesson. If something is not explained 00156 well enough, it was probably already explained in 00157 @ref lessons "previous lessons". 00158 00159 <strong>Code Diving!</strong> 00160 00161 @code */ 00162 #include "xrb.hpp" // Must be included in every source/header file. 00163 00164 #include "xrb_engine2_objectlayer.hpp" // For use of the Engine2::ObjectLayer class. 00165 #include "xrb_engine2_sprite.hpp" // For use of the Engine2::Sprite class. 00166 #include "xrb_engine2_world.hpp" // For use of the Engine2::World class. 00167 #include "xrb_engine2_worldview.hpp" // For use of the Engine2::WorldView class. 00168 #include "xrb_engine2_worldviewwidget.hpp" // For use of the Engine2::WorldViewWidget class. 00169 #include "xrb_event.hpp" // For use of the Event classes. 00170 #include "xrb_eventqueue.hpp" // For use of the EventQueue class. 00171 #include "xrb_inputstate.hpp" // For use of the InputState class (via Singleton::). 00172 #include "xrb_input_events.hpp" // For use of the EventMouseWheel class. 00173 #include "xrb_math.hpp" // For use of the functions in the Math namespace. 00174 #include "xrb_screen.hpp" // For use of the necessary Screen widget class. 00175 #include "xrb_sdlpal.hpp" // For use of the SDLPal platform abstraction layer. 00176 00177 using namespace Xrb; // To avoid having to use Xrb:: everywhere. 00178 00179 /* @endcode 00180 Our customized WorldView class will implement view zooming, rotation and 00181 movement. The view will zoom in and out by mouse-wheeling-up and 00182 mouse-wheeling-down respectively. The view will rotate clockwise and 00183 counterclockwise by holding an ALT key and mouse-wheeling-up and 00184 mouse-wheeling-down respectively. View movement will be done by holding 00185 the left mouse button and dragging. 00186 00187 In order to process these mouse events, we will need to override a couple 00188 of methods in WorldView -- Xrb::Engine2::WorldView::ProcessMouseWheelEvent 00189 and Xrb::Engine2::WorldView::ProcessMouseMotionEvent. These methods do 00190 exactly what you think they do. 00191 @code */ 00192 class AwesomeWorldView : public Engine2::WorldView 00193 { 00194 public: 00195 00196 // Trivial constructor which is just a frontend for WorldView's constructor. 00197 AwesomeWorldView (Engine2::WorldViewWidget *parent_world_view_widget) 00198 : 00199 Engine2::WorldView(parent_world_view_widget) 00200 { } 00201 00202 // This method is called with all mouse wheel events received by this 00203 // WorldView object. These aren't inherited from Widget (as WorldView 00204 // does not inherit Widget), but are called by their counterparts in 00205 // WorldViewWidget. 00206 virtual bool ProcessMouseWheelEvent (EventMouseWheel const *e) 00207 { 00208 /* @endcode 00209 Note that the accessor for ALT key state is on the event, and not 00210 on the @ref Xrb::Singleton::InputState "Xrb::InputState singleton". This is 00211 because since events can be handled asynchronously, they must retain 00212 the key modifier states (e.g. ALT, CTRL) themselves. 00213 @code */ 00214 // If either ALT key is pressed, we will rotate the view depending 00215 // on which of mouse-wheel-up or mouse-wheel-down this event indicates. 00216 if (e->IsEitherAltKeyPressed()) 00217 { 00218 if (e->ButtonCode() == Key::MOUSEWHEELUP) 00219 RotateView(-15.0f); // Rotate 15 degrees clockwise. 00220 else 00221 { 00222 ASSERT1(e->ButtonCode() == Key::MOUSEWHEELDOWN); 00223 RotateView(15.0f); // Rotate 15 degrees counterclockwise. 00224 } 00225 } 00226 // Otherwise, we will zoom the view depending on which of 00227 // mouse-wheel-up or mouse-wheel-down this event indicates. 00228 else 00229 { 00230 if (e->ButtonCode() == Key::MOUSEWHEELUP) 00231 ZoomView(1.2f); // Zoom in by a factor of 1.2f 00232 else 00233 { 00234 ASSERT1(e->ButtonCode() == Key::MOUSEWHEELDOWN); 00235 ZoomView(1.0f / 1.2f); // Zoom out by a factor of 1.2f 00236 } 00237 } 00238 // Indicates that the event was used by this method. 00239 return true; 00240 } 00241 // This method is the mouse motion analog of ProcessMouseWheelEvent. 00242 virtual bool ProcessMouseMotionEvent (EventMouseMotion const *e) 00243 { 00244 // Only do stuff if the left mouse button was pressed for this event. 00245 if (e->IsLeftMouseButtonPressed()) 00246 { 00247 // This transforms the screen-coordinate movement delta of the 00248 // mouse motion event into world-coordinates. 00249 FloatVector2 position_delta( 00250 ParallaxedScreenToWorld() * e->Delta().StaticCast<Float>() - 00251 ParallaxedScreenToWorld() * FloatVector2::ms_zero); 00252 // Move the view using the calculated world-coordinate delta. We 00253 // negate the delta because by dragging the view down, the view 00254 // should move up; while dragging, the mouse cursor should always 00255 // stay on the same spot relative to the game world. 00256 MoveView(-position_delta); 00257 // Indicates that the event was used by this method. 00258 return true; 00259 } 00260 else 00261 // Event not used. 00262 return false; 00263 } 00264 }; // end of class AwesomeWorldView 00265 00266 /* @endcode 00267 This is a helper function which will create our World instance and populate it 00268 with Sprites. The return value is the created World object. 00269 @code */ 00270 Engine2::World *CreateAndPopulateWorld () 00271 { 00272 // Create the world via the static method Engine2::World::Create. 00273 // The first parameter is a pointer to a PhysicsHandler object, however 00274 // we will not implement a PhysicsHandler in this lesson. 00275 Engine2::World *world = Engine2::World::CreateEmpty(NULL); 00276 // Decide some size for the ObjectLayer (arbitrary). 00277 static Float const s_object_layer_side_length = 1000.0f; 00278 /* @endcode 00279 Using the static method @ref Xrb::Engine2::ObjectLayer::Create we will 00280 create the ObjectLayer in which we'll add all the Sprites. The parameters 00281 are: 00282 <ul> 00283 <li>The World to which it belongs.<li> 00284 <li>A boolean value indicating wether or not positional wrapping is 00285 enabled. A wrapped ObjectLayer will repeat itself as if its 00286 opposing edges wrapped around and attached to themselves. 00287 Topologically speaking, a wrapped ObjectLayer maps to the 00288 surface of a 3 dimensional torus.</li> 00289 <li>The side length of the ObjectLayer's square domain.</li> 00290 <li>Depth of the QuadTree used to store the subordinate Objects. 00291 Don't worry about this one yet, it will be explained further in 00292 later lessons.</li> 00293 <li>The Z depth of the ObjectLayer. The meaning of this value is 00294 specific to the WorldView which is doing the rendering. In the 00295 case of parallaxed WorldViews, the ObjectLayers are stacked on 00296 top of one another, with their depths given by this value. The 00297 WorldView will use this value to calculate how large on-screen to 00298 render the Objects in each ObjectLayer.</li> 00299 </ul> 00300 We will then add the created ObjectLayer to the World object and set its 00301 main ObjectLayer (the meaning of this will be explained in later lessons). 00302 @code */ 00303 Engine2::ObjectLayer *object_layer = 00304 Engine2::ObjectLayer::Create( 00305 world, // owner world 00306 false, // not wrapped 00307 s_object_layer_side_length, // side length 00308 6, // visibility quad tree depth 00309 0.0f); // z depth 00310 world->AddObjectLayer(object_layer); 00311 world->SetMainObjectLayer(object_layer); 00312 00313 static Uint32 const s_object_count = 100; 00314 // Create a random mess of objects 00315 for (Uint32 i = 0; i < s_object_count; ++i) 00316 { 00317 // Create the sprite using the texture with given path 00318 Engine2::Sprite *sprite = Engine2::Sprite::Create("resources/interloper2_small.png"); 00319 // Place the sprite randomly on the 1000x1000 ObjectLayer. The 00320 // ObjectLayer is centered on the origin, so the valid range of 00321 // coordinates are [-500,500] for both X and Y. 00322 sprite->SetTranslation( 00323 FloatVector2(Math::RandomFloat(-500.0f, 500.0f), Math::RandomFloat(-500.0f, 500.0f))); 00324 // Size the sprite between 1% and 10% (arbitrary) of the ObjectLayer 00325 sprite->SetScaleFactor(s_object_layer_side_length * Math::RandomFloat(0.01f, 0.1f)); 00326 // Set the angle of the sprite randomly 00327 sprite->SetAngle(Math::RandomFloat(0.0f, 360.0f)); 00328 // Add the sprite to the object layer 00329 world->AddStaticObject(sprite, object_layer); 00330 } 00331 // Return the created World object. 00332 return world; 00333 } 00334 00335 void CleanUp () 00336 { 00337 fprintf(stderr, "CleanUp();\n"); 00338 // Shutdown the Pal and singletons. 00339 Singleton::Pal().Shutdown(); 00340 Singleton::Shutdown(); 00341 } 00342 00343 int main (int argc, char **argv) 00344 { 00345 fprintf(stderr, "main();\n"); 00346 00347 // Initialize engine singletons. 00348 Singleton::Initialize(SDLPal::Create, "none"); 00349 // Initialize the Pal. 00350 if (Singleton::Pal().Initialize() != Pal::SUCCESS) 00351 return 1; 00352 // Set the window caption. 00353 Singleton::Pal().SetWindowCaption("XuqRijBuh Lesson 04"); 00354 // Create Screen object and initialize given video mode. 00355 Screen *screen = Screen::Create(800, 600, 32, false); 00356 // If the Screen failed to initialize, print an error message and quit. 00357 if (screen == NULL) 00358 { 00359 CleanUp(); 00360 return 2; 00361 } 00362 00363 // Here is where the application-specific code begins. 00364 { 00365 /* @endcode 00366 Our application specific code consists of creating the game world and 00367 the two classes necessary to draw the game world via the GUI hierarchy. 00368 @code */ 00369 // Create our sweet game world via a call to CreateAndPopulateWorld. 00370 Engine2::World *world = CreateAndPopulateWorld(); 00371 // Create the WorldViewWidget as a child of screen. This is what will 00372 // contain an instance of WorldView and will cause it to be rendered. 00373 Engine2::WorldViewWidget *world_view_widget = new Engine2::WorldViewWidget(screen); 00374 screen->SetMainWidget(world_view_widget); 00375 // Create an instance of our AwesomeWorldView, using the newly created 00376 // WorldViewWidget as its "parent". Set the zoom factor so something 00377 // reasonable (though arbitrary). 00378 AwesomeWorldView *world_view = new AwesomeWorldView(world_view_widget); 00379 world_view->SetZoomFactor(0.004f); 00380 // Attach the newly created WorldView to the World object. 00381 world->AttachWorldView(world_view); 00382 /* @endcode 00383 Below, the game loop remains unchanged relative to the 00384 @ref lesson03 "previous lesson" except for a call to 00385 <tt>world->ProcessFrame(current_real_time)</tt> which handles all off-screen 00386 game computation. 00387 @code */ 00388 // These values will be used below in the framerate control code. 00389 Float current_real_time = 0.0f; 00390 Float next_real_time = 0.0f; 00391 Float desired_framerate = 30.0f; 00392 // Run the game loop until the Screen no longer has the will to live. 00393 while (!screen->IsQuitRequested()) 00394 { 00395 // Get the current real time and figure out how long to sleep, then sleep. 00396 current_real_time = 0.001f * Singleton::Pal().CurrentTime(); 00397 Sint32 milliseconds_to_sleep = Max(0, static_cast<Sint32>(1000.0f * (next_real_time - current_real_time))); 00398 Singleton::Pal().Sleep(milliseconds_to_sleep); 00399 // Calculate the desired next game loop time 00400 next_real_time = Max(current_real_time, next_real_time + 1.0f / desired_framerate); 00401 00402 // Process events until there are no more. 00403 Event *event = NULL; 00404 while ((event = Singleton::Pal().PollEvent(screen, current_real_time)) != NULL) 00405 { 00406 // Let the InputState singleton "have a go" at keyboard/mouse events. 00407 if (event->IsKeyEvent() || event->IsMouseButtonEvent()) 00408 Singleton::InputState().ProcessEvent(event); 00409 // Give the GUI hierarchy a chance at the event and then delete it. 00410 screen->ProcessEvent(event); 00411 Delete(event); 00412 } 00413 00414 /* @endcode 00415 This call is what performs all off-screen game processing, mainly by 00416 World, and then in later lessons by PhysicsHandler and Entity subclasses. 00417 This is where all game logic and physics processing will happen. 00418 @code */ 00419 world->ProcessFrame(current_real_time); 00420 00421 // Turn the EventQueue crank, Perform off-screen GUI processing, 00422 // turn the EventQueue crank again, and then draw everything. 00423 screen->OwnerEventQueue()->ProcessFrame(current_real_time); 00424 screen->ProcessFrame(current_real_time); 00425 screen->OwnerEventQueue()->ProcessFrame(current_real_time); 00426 screen->Draw(); 00427 } 00428 00429 /* @endcode 00430 Although screen would automatically delete world_view_widget if left 00431 around, we must delete it ourselves in order to ensure proper deletion 00432 order relative to World. Deletion of WorldViewWidget will cause the 00433 deletion of the attached WorldView which will in turn automatically 00434 detach itself from World. Having no attached WorldViews is a necessary 00435 precondition for the destruction of World. 00436 @code */ 00437 Delete(world_view_widget); 00438 Delete(world); 00439 } 00440 00441 // Delete the Screen (and GUI hierarchy), "SHUT IT DOWN", and return success. 00442 Delete(screen); 00443 CleanUp(); 00444 return 0; 00445 } 00446 /* @endcode 00447 00448 <strong>Exercises</strong> 00449 00450 <ul> 00451 <li>Add code to AwesomeWorldView to smooth the zooming. You'll need 00452 to implement <tt>virtual void AwesomeWorldView::HandleFrame()</tt> 00453 for this. You can use the @ref Xrb::FrameHandler::FrameTime 00454 method to retrieve the current time from inside HandleFrame.</li> 00455 <li>Add code to AwesomeWorldView to smooth the rotation. You'll need 00456 to implement <tt>virtual void AwesomeWorldView::HandleFrame()</tt> 00457 for this. You can use the @ref Xrb::FrameHandler::FrameTime 00458 method to retrieve the current time from inside HandleFrame.</li> 00459 <li>Change the zooming code so that the zooming in 4 times in a row 00460 will result in an overall zoom-in of a factor of 2. Hint: the 00461 factor to zoom by should equal 2 when raised to the 4th power. You 00462 will need to use the @ref Xrb::Math::Pow function.</li> 00463 <li>Screw with the <tt>sprite->SetTranslation</tt> call in 00464 CreateAndPopulateWorld and set the sprite's translation to somewhere 00465 outside the 1000x1000 grid of the ObjectLayer and see what happens.</li> 00466 <li>In CreateAndPopulateWorld, change the second parameter of 00467 Engine2::ObjectLayer::Create to true and see what happens.</li> 00468 <li>Screw with the <tt>sprite->SetTranslation</tt> call again and see what 00469 happens when you place a Sprite outside the domain of the ObjectLayer 00470 this time.</li> 00471 <li>In AwesomeWorldView::ProcessMouseMotionEvent, remove the negative 00472 sign from in front of position_delta in the call to MoveView, and 00473 see what the effects are. This new behavior might be preferable 00474 to some.</li> 00475 <li>Change CreateAndPopulateWorld so the sprites are not created with 00476 random positions and sizes, but form a spiral instead. Make each 00477 sprite's scale factor proportional to its distance from the origin, 00478 and set its angle such that it's facing away from the origin.</li> 00479 <li>Instead of creating a WorldViewWidget as the child of screen, make 00480 a grid Layout (e.g. row major with major count 2) and create several 00481 pairs of WorldViewWidget and WorldView, so that you have a 2x2 grid 00482 showing 4 total independent WorldViews. Remember to delete them 00483 at the end of the app-specific code. You can do this conveniently 00484 by deleting the Layout containing them.</li> 00485 <li>Disable the mouse-based view zooming/rotating/moving and implement 00486 <tt>virtual void AwesomeWorldView::HandleFrame()</tt> to: 00487 <ul> 00488 <li>Zoom in and out smoothly using the equation 00489 <tt>zoom factor = 0.008 * sin(k*time) + 0.01</tt> where k is 00490 an arbitrary constant you can pick (I suggest k = 90). You'll need 00491 to use the @ref Xrb::Engine2::WorldView::SetZoomFactor 00492 method.</li> 00493 <li>Rotate counter/clockwise smoothly using the equation 00494 <tt>angle = 400 * cos(k*time)</tt> where k is an arbitrary 00495 constant you can pick (I suggest k = 90). You'll need to use 00496 the @ref Xrb::Engine2::WorldView::SetAngle method.</li> 00497 <li>Move the view in a circle using the vector equation 00498 <tt>position = 500 * (cos(k*time), sin(k*time))</tt> where k 00499 is an arbitrary constant you can pick (I suggest k = 90). 00500 You'll need to use the @ref Xrb::Engine2::WorldView::SetCenter 00501 method.</li> 00502 </ul> 00503 You can use the @ref Xrb::FrameHandler::FrameTime method to retrieve 00504 the current time from inside HandleFrame. Mess around with the equations 00505 and then show your kid brother how much cooler you are than him.</li> 00506 </ul> 00507 00508 Thus concludes lesson04, you crazy almost-game-programming bastard, you. 00509 */