delta3d

GM Tutorial Parts 3 and 4

We now continue on to the second half of our Game Manager Tutorial. Click here to go to the previous part.

Part Three – Creating a Game with GameStart

Game Start and the GameEntryPoint Library

We now have an Actor, but how do we go from that to a working game? The Game Manager architecture provides a new entry point for your applications called GameStart. GameStart is a free standing executable that you can use to start up your applications without having to build your own executable. GameStart works by looking for a GameEntryPoint class in your library. Once it finds your entry point, it calls 3 methods and then kicks off its own GameApplication and runs the game loop. The GameEntryPoint is to GameStart as the ActorPluginRegistry is to an ActorLibrary. This is a simple, but required step. GameEntryPoint has the following 3 methods:

  1. Initialize() – Passes in any command line params. Typically, you just want to grab any params you need. However, Initialize() is also your opportunity to do any very early configuration work – stuff that needs to be done before anything else. Do not do any GM work in here. Overriding this method is optional.
  2. CreateGameManager() – A short and sweet method where the actual GameManager instance is created. Override this in the unusual case that have a subclass of GameManager. The base implementation creates a dtGame::GameManager for you.
  3. OnStartup() – Where the fun begins! Create all your components, load your map, and do any other work you need to do to startup your game. This is the last thing that is called before your infinite game loop starts. This method is required!

So, if you’ve already written a Delta3D app, you’ve almost certainly used dtABC::Application. So, why in the world should you use a GameEntryPoint over a dtABC::Application? Well, the primary reason is that it makes your system configurable. GameStart mirrors the type of setup that most game engines have. Consider Unreal and other modern engines. They use the same executable to start – an executable that usually comes ready to run, right out of the box. You simply build your custom library or map and tell the executable to go!

This is really the only way you can ever run an app directly from an editor. Yep, you heard that right. Using GameEntryPoint, future versions of STAGE could be able to launch your app right from the editor. Heck, you could even have separate test versions of your GameEntryPoint that test single levels or stub out network behavior!

Helpful Hint – The Initialize() method lets you customize how your game runs by providing a way to get command line parameters. Since Initialize() is the very first thing called, you can use this method to support various test modes or alternate configurations of your application.

Game Start and the GameEntryPoint Library

Once you have a GameEntryPoint in your library, then running GameStart would look like this:


“GameStart.exe MyCoolLibrary”

So the question is: how do you create a GameEntryPoint? Well, if GameStart is looking for a library, then obviously, we’re going to need a library to put it in. Fortunately we already have one – the HoverTank actor library! We can just put our entry point class there. All we need to do now is create our GameEntryPoint class and implement our methods. In our case, we will take the default implementation for Initialize() and CreateGameManager(), so all we need to implement is OnStartup(). Here’s what it looks like.

In “TutorialGameEntryPoint.h”:


#include "export.h"
#include <dtCore/refptr.h>
#include <dtGame/gameentrypoint.h>

namespace dtGame
{
   class GameApplication;
   class GameManager;
   class GameActorProxy;
}

/**
 * Our entry point into the game.  The GameStart.exe application can load this like this:
 *     "GameStartd.exe TutorialGameActors"
 * We create our Game Manager and components in this class.
 */ 
class TUTORIAL_HOVER_EXPORT TutorialGameEntryPoint: public dtGame::GameEntryPoint
{
   public:
      TutorialGameEntryPoint() { };
      virtual ~TutorialGameEntryPoint() { };

      // Called to do early initializtion.  Grab your command line params here.
      // We just use the base implementation
      //virtual void Initialize(dtGame::GameApplication& app, int argc, char **argv) 
      //   throw (dtUtil::Exception);

      // Create your game manager.
      // We just use the base implementation
      // virtual dtCore::RefPtr<dtGame::GameManager> CreateGameManager(dtCore::Scene& scene);

      // Called just before your application's game loop starts.  This is your main 
      // opportunity to create components, load maps, create unique actors, etc...
      virtual void OnStartup(dtGame::GameManager &gameManager);
};

With just one method to implement, this class is about as simple as it gets.

In “TutorialGameEntryPoint.cpp”:


#include "TutorialGameEntryPoint.h"
#include "TutorialInputComponent.h"

#include <dtGame/gamemanager.h>
#include <dtGame/defaultmessageprocessor.h>
#include <dtGame/gameapplication.h>

#include <dtCore/camera.h>
#include <dtCore/flymotionmodel.h>
#include <dtCore/camera.h>
#include <dtCore/keyboard.h>

#include <dtDAL/project.h>
#include <dtABC/application.h>


//////////////////////////////////////////////////////////////////////////
extern "C" TUTORIAL_HOVER_EXPORT dtGame::GameEntryPoint* CreateGameEntryPoint()
{
   return new TutorialGameEntryPoint;
}

//////////////////////////////////////////////////////////////////////////
extern "C" TUTORIAL_HOVER_EXPORT void DestroyGameEntryPoint(dtGame::GameEntryPoint* entryPoint)
{
   delete entryPoint;
}

//////////////////////////////////////////////////////////////////////////
void TutorialGameEntryPoint::OnStartup(dtGame::GameManager &gameManager)
{
   // Add Component - DefaultMessageProcessor 
   dtGame::DefaultMessageProcessor *dmp = new dtGame::DefaultMessageProcessor("DefaultMessageProcessor");
   gameManager.AddComponent(*dmp,dtGame::GameManager::ComponentPriority::HIGHEST);

   // Add Component - Input Component
   dtCore::RefPtr<TutorialInputComponent> inputComp = new TutorialInputComponent("TutorialInputComponent");
   gameManager.AddComponent(*inputComp, dtGame::GameManager::ComponentPriority::NORMAL);

   // Load the map we created in STAGE.
   dtDAL::Project::GetInstance().SetContext("StageProject");
   gameManager.ChangeMap("HoverMap");
   gameManager.GetScene().UseSceneLight(true);

   // Attach our camera to the fly motion model..
   dtABC::Application& app = gameManager.GetApplication();
   dtCore::FlyMotionModel *fmm = new dtCore::FlyMotionModel
         (app.GetKeyboard(), app.GetMouse(), false);
   fmm->SetTarget(app.GetCamera());
   
   // Set initial camera position so we can see our tank
   dtCore::Transform tx(0.0f,-40.0f,15.0f,-50.0f,-15.0f,0.0f);
   app.GetCamera()->SetTransform(&tx); 
}

That’s it! We have the boiler plate entry point code: CreateGameEntryPoint() and DestroyGameEntryPoint(). Those create and destroy your instance of the TutorialGameEntryPoint class. Then, we implement the one simple method: OnStartup(). There, we created a couple components for default message processing and keyboard input, load our map, attach a motion model, and position our camera. That’s less than 15 lines of code total.

DefaultMessageProcessor and Component Priority

Create our DefaultMessageProcessor:


   dtGame::DefaultMessageProcessor *dmp = new dtGame::DefaultMessageProcessor("DefaultMessageProcessor");
   gameManager.AddComponent(*dmp,dtGame::GameManager::ComponentPriority::HIGHEST);

You must ALWAYS do this when you create the Game Manager or else your actors will not get spawned properly, and basically none of the “default” message handling will occur. You may be asking yourself, “Well why doesn’t the Game Manager do this for you?” The answer is simple, the DefaultMessageProcessor is meant to be extended to suit the needs of your game or simulation. If your application needs to intercept actor create messages, update messages, or actor destroy messages, then you can create your own DefaultMessageProcessor. You might want to have one default processor on a client and a different one on the server. Or you might have different processors for a variety of apps using the same base code.

Overriding the DefaultMessageProcessor actually gives you the ability to CHANGE the way actors are created, destroyed, and updated. Before we wrap up the default message processor discussion, please note one other VERY IMPORTANT thing about the code. When you add an instance of the DefaultMessageProcessor be sure to add it with the HIGHEST priority level. All components have an associated priority level: HIGHEST, HIGHER, NORMAL, LOWER, LOWEST. These priorities determine the order in which components process messages going through the Game Manager. Since we almost always want the DefaultMessageProcessor to get messages before other components we add it in the HIGHEST priority queue. Think about it, the DefaultMessageProcessor creates actor instances, destroys them, and updates their properties. We better do that stuff first, so that other components that may be listening for actor messages can be confident that the actor has already been created, destroyed, or updated! It would stink to get an ActorUpdate message only to find that the properties haven’t changed yet!

Finishing our GameEntryPoint

The next thing we do is add our input component. This is a new class that we haven’t talked about yet. We’ll get to that later. Here’s the code:


   dtCore::RefPtr<TutorialInputComponent> inputComp = new TutorialInputComponent("TutorialInputComponent");
   gameManager.AddComponent(*inputComp, dtGame::GameManager::ComponentPriority::NORMAL);

The next portion of code in the above snippet sets our project context and loads a map. The Game Manager makes loading maps even easier than you see in the Actor tutorial (LINK).


   dtDAL::Project::GetInstance().SetContext("StageProject");
   gameManager.ChangeMap("HoverMap");
   gameManager.GetScene().UseSceneLight(true);

Pretty simple isn’t it? Set out project context and tell the Game Manager to load our map. That’s it! Just in case you’re wondering about the last line of code. We tell the scene to use the default skylight after the map is loaded since loading a map effectively clears all drawables from the scene including the default sky light that may have been present. So we have to tell the scene to use the default light AFTER loading the map. If your map uses actors for lights and they are in your map, then you can skip this line. When ChangeMap() is finished, it will send a MAP_CHANGE message. Technically the GM hasn’t really started processing messages yet, so it just puts it in the queue for the next tick. We’ll talk more about that later.

The last thing we do is setup a motion model and position our camera. Very typical Delta3D code for this:


   // Attach our camera to the fly motion model..
   dtABC::Application& app = gameManager.GetApplication();
   dtCore::FlyMotionModel *fmm = new dtCore::FlyMotionModel(app.GetKeyboard(), app.GetMouse(), false);
   fmm->SetTarget(app.GetCamera());
   
   // Set initial camera position so we can see our tank
   dtCore::Transform tx(0.0f,-40.0f,15.0f,-50.0f,-15.0f,0.0f);
   app.GetCamera()->SetTransform(&tx); 

If you are running in Linux, you should be able to go to the directory where you built your library, and type, “GameStart.exe TutorialGameActors”. If you are running Visual Studio, you need to setup your project to look something like this:


Figure 7. GameStart Project Settings in Visual Studio

That’s it for this section. Except for the TutorialInputComponent, which we haven’t created yet, you should have enough code here to compile and run. Already, you have created a GM, handled basic actor messages, loaded a map, and setup a camera. If you run it, you should be able to see your tank, see the terrain, and fly the camera around the world! Now it’s time to do some fun stuff with our tank!

Helpful Hint – Although GameStart and the GameEntryPoint make it a lot easier to create a GameManager based application, they are not required. You can still choose to build an Application to run your game loop. In fact, GameStart uses its own Application class called GameApplication. The note here is that if you do use GameStart, you will not be able to use your own Application. So, if you already have an Application class for your project, pull the code from your Application subclass and put it in a component, actor, or the GameEntryPoint.

Part Four – Messages and Invokables

In this section we are going to talk about game events, invokables, and ticks. Say, what? Don’t worry, we’ll explain everything you need to know. Basically we are going to build some code to send messages when the user presses certain keys. Then, we’re going to add code to our hover tank to receive those messages and turn its engines on and off. Finally, we’re going to show how we can move our tank around and ground clamp it by listening for ticks.

Game Events

To get us started sending and receiving messages, let’s talk a little bit about GameEvents. GameEvents are simple string identifiers that are sent out in a special GameEventMessage when some significant event occurs. Game Events represent a single action such as ‘Apple Found’, or ‘Hostage Rescued’. Since sometimes, people are confused about the difference, let’s be really clear: a GameEvent is a simple data class and a GameEventMessage is an message that sends a GameEvent.

In order to keep track of all the GameEvents in the system, we created dtDAL::EventManager. The EventManager is a singleton class that provides a simple management layer to allow you to look up game events from anywhere. To use a GameEvent, you create it and then register it with the EventManager (in dtDAL). Future versions of STAGE will let you define your GameEvents directly in your map. The code looks like this:


   mToggleEngineEvent = new dtDAL::GameEvent("ToggleEngine");
   dtDAL::GameEventManager::GetInstance().AddEvent(*mToggleEngineEvent);

We’re going to see it in use below. At this point, we are able to create a game event, but how do we send them through the Game Manager to other actors and components? We use a GameEventMessage of course. A GameEventMessage is a very simple message with a single parameter for the GameEvent. Telling the Game Manager to send a game event message is analogous to “firing” the event. We’ll see it all in action when we create our TutorialInputComponent at the bottom of this section, but here’s the snippet to fire a game event message.


   dtCore::RefPtr<dtGame::GameEventMessage> eventMsg;
   GetGameManager()->GetMessageFactory().CreateMessage(dtGame::MessageType::INFO_GAME_EVENT, eventMsg);

   eventMsg->SetGameEvent(event);
   GetGameManager()->SendMessage(*eventMsg);   

We first create an event message using the GM’s message factory. Once we have our message created, we bind a game event to it and tell the Game Manager to process the message.

Note that GameEvents take a string identifier in their constructor. They can optionally take a description as well. In order to access GameEvents from the GameEventManager, you need to ask for them by those string identifiers. So in a real world application, you’ll probably want to put these in some sort of resources file, or some other common place. Future versions of STAGE, the Delta3D level editor, will have UI’s for editing these events in which case they will be loaded with your map automatically.

Invokables

We can now create and send GameEvents, but how do we receive them? To answer that, we need to talk a bit about invokables. Think about invokables as event callbacks, except that they are handled and managed much like properties. To quote the Game Manager Design Document, “Invokables are to method calls what properties are to data members. Just like a property, invokables are created when the proxy is created, have names, and can be requested by name. Like a method, they can be called, or invoked.” In this section, we’ll look at how to use invokables to respond to our new game events.

If an invokable is basically just a method, then the first thing we need to do is create an invokable method. Here’s what one looks like:

In “HoverTankActor.cpp”:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActor::ProcessMessage(const dtGame::Message &message)
{
   if (message.GetMessageType() == dtGame::MessageType::INFO_GAME_EVENT)
   {
      const dtGame::GameEventMessage &eventMsg = static_cast<const 
            dtGame::GameEventMessage&>(message);

      // Note, we are using strings which aren't constants.  In a real application, these 
      // event names should be stored in some sort of shared place and should be constants...
      if (eventMsg.GetGameEvent() != NULL)
      {
         // Handle "ToggleEngine" Game Event
         if (eventMsg.GetGameEvent()->GetName() == "ToggleEngine")
         {
            mIsEngineRunning = !mIsEngineRunning;
	    mDust->SetEnabled(mIsEngineRunning);
         } 
         // Handle "SpeedBoost" Game Event
         else if (eventMsg.GetGameEvent()->GetName() == "SpeedBoost")
         {
            SetVelocity(mVelocity + -5.0f);
         }
      }
   }
}

Hey, wait! There’s nothing special in that code! It’s just a simple method that takes a message! Well, you’re right of course because that’s basically the whole reason we have Invokables! It’s just a method on your class that takes a message as a parameter. We just check to see what type of message we received, and react appropriately.

In this case, we are using the default Invokable called ProcessMessage(). GameActorProxy provides two other default Invokables as well: TickLocal() and TickRemote(). However, there are times when you want to create your own Invokable. To do that, you need to create your handler method and then wrap it in an Invokable. You do this in the BuildInvokables() method located in your GameActorProxy. BuildInvokables is called when the game actor is created.


// Not part of the tutorial – provided as an example of creating an Invokable
///////////////////////////////////////////////////////////////////////////////
void XYZProxy::BuildInvokables()
{
   XYZActor &actor = static_cast<XYZActor&>(GetGameActor());
   dtActors::GameMeshActorProxy::BuildInvokables();

   // create our invokable for events
   AddInvokable(*new dtGame::Invokable(“My Call Back”, dtDAL::MakeFunctor(actor,&XYZ::MyMethod)));
}

Next, we need to register our invokable with the Game Manager. We’ll do this in the method OnEnteredWorld(). That’s called when the game actor is added to the Game Manager. Make sure to use the same string name for your invokable when you create it and when register it. Here is what it looks like.

In “HoverTankActor.cpp”:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActorProxy::OnEnteredWorld()
{
   //Register an invokable for Game Events...
   RegisterForMessages(dtGame::MessageType::INFO_GAME_EVENT);

   // Register an invokable for tick messages. Local or Remote only, not both!
   if (IsRemote())
      RegisterForMessages(dtGame::MessageType::TICK_REMOTE, dtGame::GameActorProxy::TICK_REMOTE_INVOKABLE);
   else
      RegisterForMessages(dtGame::MessageType::TICK_LOCAL, dtGame::GameActorProxy::TICK_LOCAL_INVOKABLE);

   dtActors::GameMeshActorProxy::OnEnteredWorld();
}

It’s just one line of code to register for a message type. Then, we take a little side trip to register handlers for TICK_REMOTE and TICK_LOCAL. We haven’t really talked about ticks yet, but just know that Ticks are messages too! So naturally, if our Actor has to register to receive message like GameEventMessage, then we have to register for ticks too. In this code, we register for either the local or remote ticks and use the default Invokables, TickLocal() and TickRemote(). We’ll learn more about this in just a moment. To recap, we wrote a method called ProcessMessage() and registered to receive tick and game event messages from the Game Manager.

Now, let’s go add some fun code to our tank to add a particle system! For this, we’re going to override the OnEnteredWorld() method on our actor. Didn’t we just do that? No, we overrode the method on our PROXY. Our Actor has one too, and we can override it. Although the proxy’s OnEnteredWorld is called first, both are called when the game actor is added to the GM. Here’s what it looks like:

In “HoverTankActor.cpp”:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActor::OnEnteredWorld()
{
   // add our dust particle
   mDust = new dtCore::ParticleSystem();
   mDust->LoadFile("Particles/dust.osg",true);
   mDust->SetEnabled(false);
   AddChild(mDust.get());

   dtActors::GameMeshActor::OnEnteredWorld();
}

Well, that’s pretty simple. Just create a particle system, load our particle, make sure it’s off when we start, and attach it to our actor.

Helpful Hint – There are two other ways to Register for messages: RegisterForMessagesAboutOtherActor() and RegisterForMessagesAboutSelf(). Unlike this simple tutorial, your actors will typically only be interested in messages about itself. For instance, in a more robust game, our HoverTank would only want it’s own “ToggleEngine” messages. After all, when other players turn off their engines, we don’t want ours to shut down too! For that, we would have used, RegisterForMessagesAboutSelf(dtGame::MessageType::INFO_GAME_EVENT) in the OnEnteredWorld() method. Then, we would revisit TutorialInputComponent::FireGameEvent() and set the about actor id like this: eventMsg->SetAboutActorId(…).

An Input Component

Now we have some nifty behavior to handle game event messages, but if we compile right now, it won’t do anything. Why? Because no one is sending those events! Let’s spice up our app by trapping keyboard events and sending messages! In other tutorials, you learned how to trap HandleKeyPressed() in your Application class. However, we just learned that we can’t put our code in Application anymore. So, where should it go? Well, obviously the GM must have some sort of way for handling key events. But, the GM works with Actors and Components. … What to do?

Well, hopefully, you already guessed that we’re going to need a component. After all, the GM works primarily with (1) Messages, (2) Actors, and (3) Components. Since our goal is to cause a (1) Message to go to an (2) Actor, that really only leaves a (3) Component. Components, have a very long life span, so they can work at a very high level. In fact, GMComponents were specifically intended to support high level behavior – such as keyboard input.

What we want to use is dtGame::BaseInputComponent. This class already knows how to trap keyboard and mouse events. All we need to do is override whichever mouse/keyboard behavior we want to support. For this tutorial, we’re looking for Keyboard Pressed behavior. First, we need to define our class:

In “TutorialInputComponent.h”:


#include "export.h"
#include <dtCore/refptr.h>
#include <dtGame/baseinputcomponent.h>
#include <dtDAL/gameevent.h>
#include <dtDAL/gameeventmanager.h>

/**
 * This is an example use of the base input component for our tutorial. We will put 
 * all of our keyboard and mouse event handling in here. As a GM component, it will
 * also receive all game messages in ProcessMessage() and DispatchNetworkMessage().
 */
class TUTORIAL_HOVER_EXPORT TutorialInputComponent : public dtGame::BaseInputComponent
{
   public:
      // Constructor
      TutorialInputComponent(const std::string &name);

      // We're going to handle key presses!
      bool HandleKeyPressed(const dtCore::Keyboard* keyboard,
         Producer::KeyboardKey key,Producer::KeyCharacter character);

      // Handle messages from the GM – none for now, so we ignore
      //void ProcessMessage(const dtGame::Message &message);

   protected:
      /// Destructor
      virtual ~TutorialInputComponent() { }

   private:
      // Simple helper method to fire a GameEventMessage.  This method creates the game 
      void FireGameEvent(dtDAL::GameEvent &event);

      // Our GameEvents that we want to fire when the user presses keys
      dtCore::RefPtr<dtDAL::GameEvent> mToggleEngineEvent;
      dtCore::RefPtr<dtDAL::GameEvent> mSpeedBoost;
   };

Note that our component doesn’t even override ProcessMessage(). Although that seems strange, it’s a great illustration that GMComponents can do all sorts of high level behavior. Most of your components will override ProcessMessage(), but it is not required. Now for the implementation!

First, let’s construct our class. In “TutorialInputComponent.cpp”:


////////////////////////////////////////////////////////////////////
TutorialInputComponent::TutorialInputComponent(const std::string &name) :
   dtGame::BaseInputComponent(name)
{
   // Create a few game events and register them with the event manager.
   mToggleEngineEvent = new dtDAL::GameEvent("ToggleEngine");
   dtDAL::GameEventManager::GetInstance().AddEvent(*mToggleEngineEvent);

   mSpeedBoost = new dtDAL::GameEvent("SpeedBoost");
   dtDAL::GameEventManager::GetInstance().AddEvent(*mSpeedBoost);
}

The only thing we’re doing here is constructing our GameEvents (“ToggleEngine” and “SpeedBoost”) and registering them for later use. Note that we pass a name for our component to the BaseInputComponent. All components are required to have a unique name. This let’s you get access to your components anywhere, anytime, by calling GetComponentByName(“MyName”) on the GM.

Now, let’s handle keyboard events. In “TutorialInputComponent.cpp”:


////////////////////////////////////////////////////////////////////
bool TutorialInputComponent::HandleKeyPressed(const dtCore::Keyboard* keyboard,
   Producer::KeyboardKey key, Producer::KeyCharacter character)
{
   bool handled = true;
   switch(key)
   {
      // 'Space' - Toggle engines on and off
      case Producer::Key_space:
         FireGameEvent(*mToggleEngineEvent);
         break;

      // 'Return' - Give the tank a speed boost
      case Producer::Key_Return:
         FireGameEvent(*mSpeedBoost);
         break;

      // '+' - Speed up simulation time factor, just for fun
      case Producer::Key_equal:
         GetGameManager()->ChangeTimeSettings(GetGameManager()->GetSimulationTime(),
            GetGameManager()->GetTimeScale() * 1.1, GetGameManager()->GetSimulationClockTime());
         break;

      // '-' - Slow down the simulation time factor
      case Producer::Key_minus:
         GetGameManager()->ChangeTimeSettings(GetGameManager()->GetSimulationTime(),
            GetGameManager()->GetTimeScale() * 0.9, GetGameManager()->GetSimulationClockTime());
         break;

      default:
         handled = false;
         break;
   }

   // the default case handles the escape key to quit.
   if (!handled)
      return BaseInputComponent::HandleKeyPressed(keyboard, key, character);

   return handled;
}

That’s pretty simple. For ‘space’, we fire our “ToggleEngine” event. For ‘Return’, we fire our “SpeedBoost” event. Then, just for fun, we trap the ‘+’ and ‘-‘ keys. When pressed, we speed up or slow down the simulation time factor by 10%. Although the code is trivial, it’s the first time we’ve really talked about time.

As you’ll see later when we handle the TickLocal() behavior, you should always plan to deal with Simulation Time, as opposed to Real Time. Real Time is the actual clock change, where 1 real second from the CPU = 1 second. You might use Real Time for dealing with frame rate issues (such as slow downs or processing bottle necks). However, for anything else, you should use Simulation (Sim) Time. Sim time is the virtual time that the simulation thinks it has been running. As we see above, you can slow down and speed up sim time. If you are using the delta (i.e. change) in simulation time, your application should NEVER know the difference. Using this, you could implement rudimentary ‘bullet time’ with one line of code!

Ok, here’s our last bit of code. In “TutorialInputComponent.cpp”:


//////////////////////////////////////////////////////////////////////////
void TutorialInputComponent::FireGameEvent(dtDAL::GameEvent &event)
{
   dtCore::RefPtr<dtGame::GameEventMessage> eventMsg;
   GetGameManager()->GetMessageFactory().CreateMessage(dtGame::MessageType::INFO_GAME_EVENT, eventMsg);

   eventMsg->SetGameEvent(event);
   GetGameManager()->SendMessage(*eventMsg);   
}

A simple utility method. It gets the GameManager’s MessageFactory and asks it to create a GameEventMessage. Then, it sets the event parameter of the message and tells the GM to SendMessage(). This is exactly the process you will use to send messages anywhere in the game.

Helpful Hint – Calling ChangeTimeSettings() on the GM will cause a message to be sent out. It sends a INFO_TIME_CHANGED message of class dtGame::TimeChangeMessage. You can catch this if need be in ProcessMessage().

Tick, Tock: Ticking our Tank

Well by now you should have a “cool” hover tank actor that we have added to a STAGE map. We have learned how to create our GameEntryPoint, create a GameManager, create a new component, load our map, and add everything to our GM. We have created some game events and instructed the Game Manger to send GameEventMessages when the user presses a key on the keyboard. Lastly, we have added code to our hover tank actor to respond to those messages and turn its engines on and off. Not too bad, but we still have a little bit more work to do.

If you’ve been writing this code as you go, you’ve already noticed that our poor hover tank can’t move. What we want to do is allow the user to move our tank smoothly around the world. Plus, it would be cool if our hover tank actually ‘hovered’ over the ground, so we’ll need to do some ground clamping as its moves about.

So how are we going to do that? More importantly, where are we going to do that? Well, to make our movement smooth, we need to process the tank every single time we draw a frame. So, if our system can draw at 20 Frames per second (FPS), we want to reposition our tank 20 times a second. If it can draw at 300 FPS, then we’ll move it 300 times! That’s where the game loop comes in.

So what’s a game loop? In its simplest form, a game loop is an infinite loop that tells all the parts of the game to do work. Every time the game loop ‘loops’, it tells all parts of the system to do a little bit of work. Well, one ‘loop’ is called a ‘tick’. Although not technically the same, a ‘tick’ is often used interchangeably with ‘frame’. If you don’t take into consideration advanced tick optimization or multi-threaded game loops, then basically the game-loop is just a way of ‘ticking’ everything once per frame.

Ok, so we have an idea of what a game loop is and that ticking is related to updating your game simulation, so how does that translate into actor ticks? Well, pretty simple actually. As long as your game is running, the GameManager will send TICK_LOCAL and TICK_REMOTE messages. In fact, all ticking is done with messages. Why? Well, that’s a very good question. We could have just called the TickLocal() and TickRemote() methods on your GameActors every tick. But, wait a minute. What about actors that don’t need to be ticked? What about static meshes like trees and terrain and projective textures? What about actors that are simply waiting for specific game events? We don’t want them to be ticked do we? Since game worlds can have thousands of actors, we don’t really want to make 2000-3000 needless calls every tick, do we? No, we certainly don’t. So, Actors have to register for ticks if they want to receive them.

If you remember, we did that up above. Here’s what it looked like:


void HoverTankActorProxy::OnEnteredWorld()
{
   // …
   // Register an invokable for tick messages. Local or Remote only, not both!
   if (IsRemote())
      RegisterForMessages(dtGame::MessageType::TICK_REMOTE, dtGame::GameActorProxy::TICK_REMOTE_INVOKABLE);
   else
      RegisterForMessages(dtGame::MessageType::TICK_LOCAL, dtGame::GameActorProxy::TICK_LOCAL_INVOKABLE);
   // …
}

By default, GameActors already have Invokables for this called TickLocal() and TickRemote(). So, all we have to do is override them. We did that up above, but now we’re ready to implement the code.

In “HoverTankActor.cpp”:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActor::TickLocal(const dtGame::Message &tickMessage)
{
   const dtGame::TickMessage &tick = static_cast<const dtGame::TickMessage&>(tickMessage);
   float deltaSimTime = tick.GetDeltaSimTime();

   ComputeVelocityAndTurn(deltaSimTime);

   MoveTheTank(deltaSimTime);
}

Ok, that’s pretty simple. When we get a tick local message we compute our new velocity, turn our vehicle, and then move the tank. Please note again the use of the simulation time. Tick.GetDeltaSimTime() will return the change in simulation time since our last tick. So, if we were getting 10 frames a second and the speed factor was 1, deltaSimTime would be very close to 0.1. If we were getting 300 frames a second and the speed factor was 1.5x, deltaSimTime would be very close to 0.00499. Even though the delta’s are wildly different, our game shouldn’t care. As long as we calculate based on the deltaSimTime, we don’t need to know the difference in frame rate or in speed factor.

So, we just implemented our ‘local’ behavior. But what is this concept of ‘remote’? Well, if you’ve never written a game or simulation before, it will seem odd. Local behavior is what we do when we ‘own’ the object. In a single player game, you typically own all the objects on your machine, so everything is ‘local’. However, in a networked game, you only own your objects (like Player). You don’t own the other players. Since you don’t own them, you shouldn’t be simulating them, should you? No. So, the bottom line is that you need perform different behavior when the actor is owned by you (‘local’) than you would if the actor is not-owned by you (‘remote’). Let’s consider our tank. When we ‘own’ the tank, we’re going to recompute the velocity and rotation. After all, our users will be able to steer the tank. But, we don’t want other clients changing the direction and velocity, we just want them to move the tank! Let’s see the difference:

In “HoverTankActor.cpp”:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActor::TickRemote(const dtGame::Message &tickMessage)
{
   const dtGame::TickMessage &tick = static_cast<const dtGame::TickMessage&>(tickMessage);
   float deltaSimTime = tick.GetDeltaSimTime();

   // do NOT recompute velocity and turn rate since we don't own this tank!

   MoveTheTank(deltaSimTime);
}

Note that we don’t call ComputeVelocityAndTurn() this time. Cool! Now, let’s actually move our tank. This next bit of code is fairly complex and we’re not going to cover it in detail. Just know that it works.

In “HoverTankActor.cpp”:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActor::MoveTheTank(float deltaSimTime)
{
   dtCore::Transform tx;
   osg::Matrix mat;
   osg::Quat q;
   osg::Vec3 viewDir;

   GetTransform(&tx);
   tx.GetRotation(mat);
   mat.get(q);
   viewDir = q * osg::Vec3(0,-1,0);

   // translate the player along its current view direction based on current velocity
   osg::Vec3 pos;
   tx.GetTranslation(pos);
   pos = pos + (viewDir*(mVelocity*deltaSimTime));
   
   // attempt to ground clamp the actor so that he doesn't go through mountains.
   osg::Vec3 intersection;
   dtCore::RefPtr<dtCore::Isector> query = new 
         dtCore::Isector((&GetGameActorProxy().GetGameManager()->GetScene()));
   query->Reset(); 
   query->SetStartPosition(osg::Vec3(pos.x(),pos.y(),-10000));
   query->SetDirection(osg::Vec3(0,0,1));
   if (query.get()->Update())
   {
      osgUtil::IntersectVisitor &iv = query.get()->GetIntersectVisitor();
      osg::Vec3 p = iv.getHitList(query.get()->GetLineSegment())[0].getWorldIntersectPoint();
      // make it hover
      pos.z() = p.z() + 2.0f;
   }
   
   osg::Vec3 xyz = GetGameActorProxy().GetRotation();
   xyz[2] += 360.0f * mTurnRate * deltaSimTime;

   tx.SetTranslation(pos);
   SetTransform(&tx);
   GetGameActorProxy().SetRotation(xyz);
}

Woah. There’s some icky vector math going on in there, plus we have this intersection stuff going on. For now, you just have to trust that it works because the explanation is beyond the scope of this tutorial. One thing you should notice is that this method uses ‘mVelocity’ and ‘mTurnRate’ to move the tank. We better do something to change those! Let’s check out the next method.

In “HoverTankActor.cpp”:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActor::ComputeVelocityAndTurn(float deltaSimTime)
{
   // calculate current velocity
   float decelDirection = (mVelocity >= 0.0) ? -1.0f : 1.0f;
   float accelDirection = 0.0f;
   float acceleration = 0.0;

   dtCore::Keyboard *keyboard = GetGameActorProxy().GetGameManager()->GetApplication().GetKeyboard();

   // which way is the user trying to go? 
   if (keyboard->GetKeyState(Producer::Key_I))
      accelDirection = -1.0f;
   else if (keyboard->GetKeyState(Producer::Key_K))
      accelDirection = 1.0f;

   // speed up based on user and current speed (ie, too fast)
   if (mIsEngineRunning && accelDirection != 0.0f)
   {
       //  boosted too fast, slow down
      if ((accelDirection > 0 && mVelocity > MAXTANKVELOCITY) ||
            (accelDirection < 0 && mVelocity < -MAXTANKVELOCITY))
         acceleration = deltaSimTime*(MAXTANKVELOCITY/3.0f)*decelDirection;
      // hold speed
      else if (mVelocity == accelDirection * MAXTANKVELOCITY)
         acceleration = 0;
      // speed up normally - woot!
      else 
         acceleration = accelDirection*deltaSimTime*(MAXTANKVELOCITY/2.0f);
   }
   else if (mVelocity > -0.1 && mVelocity < 0.1)
      acceleration = -mVelocity; // close enough to 0, so just stop
   else // coast to stop
      acceleration = deltaSimTime*(MAXTANKVELOCITY/6.0f)*decelDirection;

   SetVelocity(mVelocity + acceleration);

   if (mIsEngineRunning && keyboard->GetKeyState(Producer::Key_L))
      SetTurnRate(-0.25f);
   else if (mIsEngineRunning && keyboard->GetKeyState(Producer::Key_J))
      SetTurnRate(0.25f);
   else 
      SetTurnRate(0.0f);
}

Again, the details are beyond the scope of this tutorial. It is important to notice that we are actually polling the keyboard directly, each tick. We call keyboard.GetKeyState() to determine if the various speed (‘I’ and ‘K’) and direction (‘J’ and ‘L’) keys are pressed. That’s what we use to call SetTurnRate() and SetVelocity(). Well that’s interesting. Didn’t we trap keyboard events in our TutorialInputComponent? Yes, we did. So, here we see another way to deal with the keyboard that is directly on an Actor, instead of on a component.

To finish up, let’s go back and revisit our Set methods from earlier.

In “HoverTankActor.cpp”:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActor::SetTurnRate(float rate)
{
   if (mTurnRate != rate)
   {
      mTurnRate = rate;
      // Notify the world that our turn rate changed. Only changes on keypress
      GetGameActorProxy().NotifyFullActorUpdate();
   }
}

Notice the call to NotifyFullActorUpdate() here. We add that so that we will send out a message when our actor changes. This is VERY important. If you are running a networked game, or want to add the ability to do playback, then you MUST send out updates when your actor significantly changes. Ok, let’s check out our velocity:


///////////////////////////////////////////////////////////////////////////////
void HoverTankActor::SetVelocity(float velocity)
{
   mVelocity = velocity;

   // Notify the world that our velocity changed, if there is enough difference
   // In a more sophisticated app, you would track acceleration, not just velocity
   // And then you wouldn't have to send velocity but every so often, since acceleration
   // would allow you to dead reckon the position without a network update.
   if ((fabs(fabs(mLastReportedVelocity) - fabs(mVelocity)) > 0.5) || 
      (mLastReportedVelocity != mVelocity && 
         (mVelocity == MAXTANKVELOCITY || mVelocity == 0.0f || mVelocity == -MAXTANKVELOCITY )))
   {
      mLastReportedVelocity = mVelocity;
      GetGameActorProxy().NotifyFullActorUpdate();
   }
}

Here we do very similar behavior. This time however, we have a bit of extra logic. In order to decay our velocity over time (speeding up and slowing down), we have to change our velocity almost every tick. However, we certainly don’t want to send a full actor update every tick. Instead, we send it out periodically. In a more sophisticated app, you would also use ‘acceleration’. Since our acceleration is pretty linear, we would rarely need to send update messages out. Then, remote clients would get periodic updates for the acceleration and could dead-reckon between them.

Alright, we have finished our hover tank actor! So, let’s recap. We have a hover tank that responds to game events and turns its engines on and off. This causes a particle system to be attached to blow dust. Then we also have the ability to set the hover tank’s velocity and turn rate and make sure it is ground clamped. And we make sure that when a property on the tank actor changes, the world is notified using an ActorUpdateMessage. We also trap the keyboard input so we can speed up and slow down our tank.

Helpful Hint – In SetVelocity(), we call NotifyFullActorUpdate() on the ActorProxy. This method takes all of the properties on the actor and puts them into one large ActorUpdateMessage. Alternately, we could have build our own ActorUpdateMessage with just the fields that changed. There are already methods, PopulateActorUpdate() and NotifyActorUpdate() on GameActorProxy to help you along the way. Most complex applications will eventually get to the point where they send frequent position and rotation updates, but infrequent updates for larger sets of data like a characters graphical appearance. For instance, a networked role-playing game would send lots of updates about player position and rotation, medium updates about health, and few updates about race, class, and level.

Helpful Hint – Although we didn’t do it in our tutorial, there is often a need to create Actors on the fly, directly in code. You’d do this for anything that isn’t statically placed in your map. Here’s an example of how you do that using the TaskActor:

 
      dtCore::RefPtr<dtActors::TaskActorProxy> rollupTaskProxy;
      mGameManager->CreateActor("dtcore.Tasks","Rollup Task Actor", rollupTaskProxy);

Conclusion

Here’s what our tank looks like running in circles:


Figure 8 Tutorial Screenshot

So that’s pretty much it! We now have a game that controls our hover tank actor and we have learned about invokables, game messages, ticks, and all that good stuff. Using the basic code presented here, you should have enough information to do just about anything you want using the Game Manager architecture! Hope you had fun!

To see the final code, check out the tutorial project.

Click here to return to the tutorials page.

Trackback

Trackback URL for this entry: http://delta3dengine.org/trackback.php?id=20060620134503728

No trackback comments for this entry.

About delta3d

delta3d is a game and simulation engine appropriate for a wide variety of simulation and entertainment applications. delta3d uses best-of-breed open source technologies to create a fully integrated game engine and with content creation tools.MORE

Twitter

User Functions





Don't have an account yet? Sign up as a New User!

Lost your password?