From delta3d Wiki
Jump to: navigation, search

Object Motion Model Part III

So far we have gone over the orbital, fly, ufo, and walk motion models. This tutorial will broaden our horizons to the collision, fps, and rts motion models. The reason that this tutorial is labeled as advanced, is because we will be using the models in a rather unconventional way. We will be applying them to some meshes and then changing the camera's transform to make it look as if we are in first person with respect to the model.

I will be referring to this project as MotionModelTester, and my .cpp and .h files have this name as well.

You are currently in tutorial 3/3 of the object motion model series.

** It is highly recommended that you load the map from the download into STAGE and get familiar with the actor names and properties. **

Additional Tutorials that will apply:
HelloWorld3

Final product: Running

The Header File

In your MotionModelTester header file we look like the following (Take the time to read over the comments, and don't worry about any details for now):

#include <dtABC/application.h>
#include <dtCore/refptr.h>
#include <dtCore/object.h>
#include <dtUtil/datapathutils.h>
#include <dtCore/fpsmotionmodel.h>
#include <dtCore/collisionmotionmodel.h>
#include <dtABC/beziercontroller.h>
#include <dtABC/labelactor.h>
#include <dtDAL/baseactorobject.h>

//(Is all of the following) Object->Physical->Transformable->Drawable->Base (Is not any of the previous)

class MotionModelTester : public dtABC::Application
{
   public:
      /**
       * Construct with a configuration file already made.
       */
      MotionModelTester(const std::string& configFilename);

      /** 
       * Empty constructor
       */
      MotionModelTester();

      /** 
       * Configure our map.
       */
      virtual void Config();

      /**
       * Called when a mouse button is pressed.  Overwrite for custom functionality.
       * @param mouse Handle to the Mouse that triggered this
       * @param button The button index
       */
      virtual bool MouseButtonPressed(const dtCore::Mouse* mouse, dtCore::Mouse::MouseButton button);

      /** 
       * Called when a key is pressed. Overwrite for custom functionality.
       * @param kb the source of the event
       * @param key the key pressed
       */
      virtual bool KeyPressed(const dtCore::Keyboard* kb, int key);

      /** 
       * Set the point of view to be from mPersonFPS, and change the motion model to
       * FPS.
       */
      void SetPOVFPS();

      /** 
       * Set the point of view to be from mPersonCollision, and change the motion model to
       * Collision.
       */
      void SetPOVCollision();

      /** 
       * Remove all children from character Objects.  Make mPerson Collision
       * have the default Scene as its parent.
       */
      void RemoveAllChildren();

      /** 
       * Get the transform of where the default camera should be.
       * An actor named "CameraData" has the transform we want.
       * @param outPosition the transform to set
       */
      void GetDefaultCameraTransform(dtCore::Transform &outPosition);

   protected:
      /** 
       * Standard deconstructor.
       */
      virtual ~MotionModelTester();

   private:
      dtCore::RefPtr<dtCore::Object> mPersonFPS;
      dtCore::RefPtr<dtCore::Object> mPersonCollision;
      dtCore::RefPtr<dtCore::MotionModel> mMotionModel;
      dtCore::RefPtr<dtDAL::Map> mMap;
      dtCore::Transform fpsPosition; 
      dtCore::Transform collisionPosition;
};

The Implementation File

We will implement the functions in the order in which they are declared in the header file.

First is the Config() method. The tasks that must be completed in this function are the following:

  1. Configure the system with by calling the Application's Config() function.
  2. Set the title of your window to what you see fit.
  3. Construct our mMotionModel to the RTS type and make the target the camera.
  4. Set the contents of our project (The directory which contains the "maps" folder, where the map you downloaded is located). Make sure that if you are manually entering a string into the SetContext function that you always use a single forward slash "/" or two backslashes "\\" between directories.
  5. Load the map.
  6. Retrieve the default camera transform from the GetDefaultCameraTransform function. Then set the camera to have the transform.
  7. Retrieve the references of the StaticMesh character assets from the map. Then set our Persons to point to the referenced. (HINT: You will need to use the Map::FindProxies function. This function is well documented in the map.h file).
  8. Finally we want to save the current transforms of our Persons so that we can later reset the application if desired.


After completing all the steps, your Config function should look something like this:

void MotionModelTester::Config()
{
    //Configure the system
    Application::Config();
	
    GetWindow()->SetWindowTitle("Motion Model Tester");
	
    //Set the motion model to RTS and set it as the camera's default motion
    mMotionModel = new dtCore::RTSMotionModel(GetKeyboard(), GetMouse());
    mMotionModel->SetTarget(GetCamera());

    //Get the path of where you have your "maps" directory
    std::string contextName = dtUtil::GetDeltaRootPath() + 
       "/examples/data/demoMap";
    dtDAL::Project::GetInstance().SetContext(contextName, true);

    //Load your map
    mMap = &dtDAL::Project::GetInstance().GetMap("MotionTester");
    dtABC::BaseABC::LoadMap(*mMap);

    //Retrieve the default camera transform and set it
    //Set camera transform
    dtCore::Transform camPosition;
    GetDefaultCameraTransform(camPosition);
    GetCamera()->SetTransform(camPosition);

    //Retrieve the references of your StaticMesh character assets from the map
    std::vector<dtCore::RefPtr<dtDAL::BaseActorObject> > containerSomali;
    std::vector<dtCore::RefPtr<dtDAL::BaseActorObject> > containerCivilian;
    mMap->FindProxies(containerSomali, "Somali", "", "", "");
    mMap->FindProxies(containerCivilian, "Civilian", "", "", "");

    //Make sure the actors exists with the correct names in the map
    if(!containerSomali.empty() && !containerCivilian.empty())
    {
    	containerSomali[0].get()->GetActor(mPersonFPS);		
	containerCivilian[0].get()->GetActor(mPersonCollision);
    }
    else
    {
	LOG_ERROR("Could not find one or more required actors. Application Exiting.");
	Quit();
    }

    //Retrieve the transforms of our characters so we can reset their 
    //transforms within the application if desired
    mPersonCollision->GetTransform(collisionPosition);
    mPersonFPS->GetTransform(fpsPosition);
}


The next function is MouseButtonPressed. We are overriding this function from the Application class which defines what the mouse buttons will do:

  1. Our left mouse button will set the camera perspective and set the mMotionModel to either the FPS or Collision persons depending on which one you physically click on. In order to implement this you must use a function in the dtABC::BaseABC class. Try and find the function on your own.
    1. Now that have found the function we want to see if the picked object is the same as one of our Persons. If it is FPS then call the SetPOVFPS function, if it is Collision then call the SetPOVCollision function. (We will implement them later).
  2. Our middle mouse button will reset the camera back to its default position.
    1. First we have to call the RemoveAllChildren function. (We will implement this later).
    2. Now we have to set our motion model back to RTS, and make the target the camera again.
    3. Finally we get the default camera transform and apply it to the camera.
  3. Our right mouse button will act as an application reset, making everything as it was upon starting.
    1. We remove all children first.
    2. Set the motion model to RTS and make the target the camera.
    3. Now we get the default camera transform and apply it to the camera.
    4. Finally we set the transforms of our Persons to their default transforms saved in the Config function.


After completing all the steps, your MouseButtonPressed function should look something like this:

bool MotionModelTester::MouseButtonPressed(const dtCore::Mouse* mouse, 
    dtCore::Mouse::MouseButton button)
{
    assert(mouse != NULL);

    bool ret = true;

    dtCore::Transform camPos;
    osg::Vec2 mousePos = mouse->GetPosition();

    //Set the character to be focused on
    if(button == dtCore::Mouse::LeftButton) //Left mouse button
    {
 	//Check if the object that has been clicked on is the FPS character
	if(mPersonFPS.get() == GetView()->GetPickedObject(mousePos))
	{	
    	    SetPOVFPS();
	}
		
	else if(mPersonCollision.get() == GetView()->GetPickedObject(mousePos))
	{
	    SetPOVCollision();
	}
    }
    //Set the camera back to where it was when the application started
    else if(button == dtCore::Mouse::MiddleButton) //Middle mouse button
    {
        RemoveAllChildren();

	//Turn the cursor on
	GetWindow()->ShowCursor(true);

	//Set the motion model to RTS and set the target the camera
	mMotionModel = new dtCore::RTSMotionModel(GetKeyboard(), GetMouse());
	mMotionModel->SetTarget(GetCamera());
	
	//Set the transform of the camera back to where it was when the application started
	GetDefaultCameraTransform(camPos);
	GetCamera()->SetTransform(camPos);
    }
    //Act as a reset button for the application
    else if(button == dtCore::Mouse::RightButton) //Right mouse button
    {
        RemoveAllChildren();

	//Turn the cursor on
	GetWindow()->ShowCursor(true);

	//Set the motion model to RTS, and set the target to the camera
	mMotionModel = new dtCore::RTSMotionModel(GetKeyboard(), GetMouse());
	mMotionModel->SetTarget(GetCamera());

	//Set the transform of the camera back to where it was when the application started
	GetDefaultCameraTransform(camPos);
	GetCamera()->SetTransform(camPos);

	//Set the characters transforms back to where they were at the start of the application
	mPersonCollision->SetTransform(collisionPosition);
	mPersonFPS->SetTransform(fpsPosition);
    }
    else
    {
        ret = false;
    }

    return ret;
}


The next function is KeyPressed. We will override this from the Application class, setting what certain keys will do in our application when pressed (This will be very similar skeleton as other KeyPressed function we have written in previous tutorials).

  1. If the key is F1 we will call SetPOVFPS.
  2. F2, call SetPOVCollision.
  3. Escape, quit the application.
  4. Tilde, show application statistics.

After completing all the steps, your KeyPressed function should look something like this:

bool MotionModelTester::KeyPressed(const dtCore::Keyboard* kb, int key)
{
    bool ret = true;
    dtCore::Transform camPos;

    //Check which key has been pressed
    switch(key)
    {
    //osgGA::GUIEventAdapter provides the input values
    //Set the focus to be the FPS character
    case osgGA::GUIEventAdapter::KEY_F1: //F1
	{
    	    SetPOVFPS();
	    break;
	} 
    //Set the focus to be the Collision character
    case osgGA::GUIEventAdapter::KEY_F2: //F2
 	{
   	    SetPOVCollision();
	    break;
	} 
    //Quit the application
    case osgGA::GUIEventAdapter::KEY_Escape:
 	{
   	    Quit();
	}
    //Show statistics
    case '~':
	{
	    SetNextStatisticsType();
	    break;
	}
    default:
 	{
   	    ret = false;
	    break;
	}
    }

    return ret;
}


The next function is SetPOVFPS. This will make will set our point of view to the mPersonFPS':

  1. We first remove all children.
  2. Set the motion model to FPS and set the target to our mPersonFPS.
  3. Now we are going to have to change the transform of the camera. We do this because after the transform is changed we are going to make the camera a child of our mPersonFPS. This transform change will act as an offset to the origin of the mPersonFPS. (When you set one object as a child of another, its transform will change from an absolute transform to an offset of the parents origin). For this tutorial set the offset to be (.0f, .2f, 2.0f, .0f, .0f, .0f).
  4. Finally make the camera a child of mPersonFPS.

After completing all the steps, your SetPOVFPS function should look something like this:

void MotionModelTester::SetPOVFPS()
{
    dtCore::Transform camPos;

    RemoveAllChildren();

    //Turn off the cursor
    GetWindow()->ShowCursor(false);

    //Set the motion model to FPS and set its target as your FPS character
    mMotionModel = new dtCore::FPSMotionModel(GetKeyboard(), GetMouse());
    mMotionModel->SetTarget(mPersonFPS);

    //Set the offset of the camera relative to the characters origin (his feet)
    camPos.Set(.0f, .2f, 2.0f, .0f, .0f, .0f);
    GetCamera()->SetTransform(camPos);

    //Make the camera a child of the FPS character
    //With the offset in effect it will make the camera eye level with the 
    //character
    mPersonFPS.get()->AddChild(GetCamera());
}


Next up - SetPOVCollision. This implementation will be very different from the previous. The problem here is that when using a collision motion model, the geometry used for colliding with objects is always set below the origin of the targeted object (this is because the CollisionMotionModel is meant to be applied to the camera, which has no geometry, therefore it needs collision volumes placed underneath it). Thus, we cannot directly set the motion model on mPersonCollision because its origin is at the meshes feet. If we did, then the geometry would be below the surface of our ground structure, thus we would fall through it. So we need to think of a work around. In this tutorial we are going to apply the motion model to the camera and then set mPersionCollision as a child of it. All we will need to do is set the transform of the camera to be at head height so that the geometry of the motion model will not conflict negatively with the ground mesh, and simply collide with it as desired:

  1. As usual, remove all children.
  2. Now we must make sure the mPersonCollision has no parents. This is because Delta3D does not allow objects to have multiple parents, and by default the scene will be the parent of our Persons. Orphaning an object can be done with the Emancipate function.
  3. It is time to set mMotionModel to be of the collision type. Get to know the CollisionMotionModel constructor. In this tutorial the height and radius should be 1.7f and 0.1f respectively. This is because character meshes by default have a height of 1.7 meters, and we set the radius to be very small so that we can get through any doorways, etc.
  4. We must also set the collision bits for the feet and torso of our motion model. This tells the model which type of things to collide with. For this tutorial we will use COLLISION_CATEGORY_MASK_OBJECT for both.
  5. Now we will calculate the transform for the head of our character mesh. First get the transform of mPersonCollide (this is from his feet), then we add 1.7f to the z-axis (the height of the mesh).
  6. Set the camera's transform to the one calculated.
  7. Set the target of our motion model to the camera.
  8. Set mPersonCollision's transform to be -1.7f in the z-axis from the camera's tranform.
  9. Make the object a child of our camera.

After completing all the steps, your SetPOVCollision function should look something like this:

void MotionModelTester::SetPOVCollision()
{
    dtCore::Transform camPos;

    RemoveAllChildren();

    //For this character we need to remove all of his parents
    //This is because we need to make the mesh a child of the camera
    //Delta3D only allows for each object to have 1 parent
    mPersonCollision->Emancipate();

    //Turn off the cursor
    GetWindow()->ShowCursor(false);

    //Set the motion model to Collision
    //The default height for a character is 1.7 meters, we make the collision 
    //radius small so we can walk through doors
    dtCore::CollisionMotionModel* mCollisionModel = 
	new dtCore::CollisionMotionModel(1.7f, .1f, 1.5f, 0.01f, GetScene(), 
	GetKeyboard(), GetMouse()); 
    mCollisionModel->GetFPSCollider().SetCollisionBitsForFeet(COLLISION_CATEGORY_MASK_OBJECT);
    mCollisionModel->GetFPSCollider().SetCollisionBitsForTorso(COLLISION_CATEGORY_MASK_OBJECT);
    mMotionModel = mCollisionModel;

    //Get the transform of the collision character's origin
    mPersonCollision->GetTransform(camPos);

    //Retrieve the translation of the transform
    //Add 1.7 to the z axis (at the head of the character)
    osg::Vec3 newCamPos = camPos.GetTranslation();
    newCamPos += osg::Vec3(.0, .0, 1.7f);
    camPos.SetTranslation(newCamPos); 

    //Set the camera to the new transform
    GetCamera()->SetTransform(camPos);

    //Set the target of the motion model to the camera
    mMotionModel->SetTarget(GetCamera());

    //Set the offset for the Collision character
    //Because the origin is relative to the origin if you did not set the 
    //offset to be -1.7 in the z axis, the character would be 1.7 meters in 
    //the air once he is set as the child of the camera
    camPos.Set(.0f, .0f, -1.7f, .0f, .0f, .0f);
    mPersonCollision->SetTransform(camPos);

    //Set the Collision character as the child of the camera
    GetCamera()->AddChild(mPersonCollision.get());
}


In RemoveAllChildren we will make sure that we can make the camera a child of mPersonFPS if desired, mPersonCollision a child of the camera, and make mPersonCollision render by making its parent the scene:

  1. Remove the camera from the list of mPersonFPS' children.
  2. Get the absolute transform of mPersonCollision (so that we can retain its position once removing it as a child of the camera).
  3. Remove mPersonCollision from the list of children of the camera.
  4. Set the transform of mPersonCollision.
  5. Remove all the parents of mPersonCollision, then set the scene as his parent.

After completing all the steps, your RemoveAllChildren function should look something like this:

void MotionModelTester::RemoveAllChildren()
{
    mPersonFPS->RemoveChild(GetCamera());

    //Get the current absolute transform of the Collision character.
    dtCore::Transform trans2;
    mPersonCollision->GetTransform(trans2);

    //Remove Collision character from the camera's children
    GetCamera()->RemoveChild(mPersonCollision.get());

    //Set the Collision character back to the transform before orphaned
    mPersonCollision->SetTransform(trans2);

    //Make sure Collision character has no parents
    mPersonCollision->Emancipate();

    //Set the parent to be the scene so the character is visible again
    GetScene()->AddChild(mPersonCollision.get());
}


Hang in there we are almost done!

Now comes the implementation of GetDefaultCameraTransform. In the map there is an actor named "CameraData" who holds the transform that we need to set our camera to by default. This function will take in a reference of a transform in which to set the coordinates in:

  1. Get the "CameraData" proxy.
  2. Retrieve the actors tranform

After completing all the steps, your GetDefaultTransform function should look something like this:

void MotionModelTester::GetDefaultCameraTransform(dtCore::Transform& outPosition)
{
    //Find the actor proxy named "CameraData"
    std::vector<dtCore::RefPtr<dtDAL::BaseActorObject> > containerCamData;
    mMap->FindProxies(containerCamData, "CameraData", "", "", "");

    //Make sure that the actor was found
    if(!containerCamData.empty())
    {
        //Get the transform
	dtCore::RefPtr<dtCore::Object> cameraData;
	containerCamData[0].get()->GetActor(cameraData);
	cameraData->GetTransform(outPosition);
    }
    else
    {
	LOG_ERROR("Could not find CameraData actor. Application Exiting.");
	Quit();
    }
}


We now have all the source code written!
If you are having trouble getting the code to compile, refer to the following list of header files your project might need:

#include <dtABC/application.h>
#include <dtCore/refptr.h>
#include <dtCore/object.h>
#include <dtUtil/datapathutils.h>
#include <dtCore/fpsmotionmodel.h>
#include <dtCore/collisionmotionmodel.h>
#include <dtABC/beziercontroller.h>
#include <dtABC/labelactor.h>
#include <dtDAL/baseactorobject.h>
#include "MotionModelTester.h"
#include <dtCore/deltawin.h>
#include <dtCore/transform.h>
#include <dtCore/infiniteterrain.h>
#include <osgGA/GUIEventAdapter>
#include <dtCore/rtsmotionmodel.h>
#include <dtDAL/project.h>
#include <dtDAL/map.h>
#include <dtCore/view.h>
#include <dtCore/deltawin.h>
#include <dtCore/collisioncategorydefaults.h>
#include <assert.h>
#include <iostream>

Running

Now let's compile and run the app! You should see a screen similar to this: MotionModelTesterApp.png

In-app controls:

  • Pressing F1 will set the target to mPersonFPS.
  • F2 will set the target to mPersonCollision.
  • ~ will show application statistics in the upper left-hand corner.
  • MIDDLE_MOUSE will reset the camera to the default position.
  • RIGHT_MOUSE will reset the entire scene as if the application was reloaded.
  • LEFT_MOUSE over one of the two persons will set the target to that person.
  • ESC will quit the application.


The controls for each model are:

  RTS Motion Model:
  W      -  Move forward
  S      -  Move backward
  A      -  Strafe left
  D      -  Strafe right
  SCROLL -  Zoom in / out
  Mouse  -  All movement types based on cursor position


  FPS Motion Model:
  Up     -  Move forward
  Down   -  Move backward
  Left   -  Strafe left
  Right  -  Strafe right
  W      -  Move forward
  S      -  Move backward
  A      -  Strafe left
  D      -  Strafe right
  Mouse  -  Point the direction of movement
 

  Collision Motion Model:
  Up     -  Move forward
  Down   -  Move backward
  Left   -  Strafe left
  Right  -  Strafe right
  W      -  Move forward
  S      -  Move backward
  A      -  Strafe left
  D      -  Strafe right
  SPACE  -  Jump
  Mouse  -  Look around / give forward movement path


That's the end of the Delta3D Object Motion Model Part III tutorial.

Complete files to download: MotionModelTester.zip