Basic Tutorial 4
From Ogre Wiki
Beginner Tutorial 4: Frame Listeners and Unbuffered Input
Any problems you encounter while working with this tutorial should be posted to the Help Forum.
Contents |
Prerequisites
This tutorial assumes you have knowledge of C++ programming and are able to set up and compile an Ogre application (if you have trouble setting up your application, see this guide for specific compiler setups). This tutorial builds on the previous beginner tutorials, and it assumes you have already worked through them.
Introduction
In this tutorial we will be introducing one of the most useful Ogre constructs: the FrameListener. By the end of this tutorial you will understand FrameListeners, how to use FrameListeners to do things that require updates every frame, and how to use Ogre's unbuffered input system.
You can find the code for this tutorial here. As you go through the tutorial you should be slowly adding code to your own project and watching the results as we build it.
Getting Started
As with the previous tutorials, we will be using a pre-constructed code base as our starting point. Create a project in the compiler of your choice for this project, and add a source file which contains this code:
#include "ExampleApplication.h"
class TutorialFrameListener : public ExampleFrameListener
{
public:
TutorialFrameListener(RenderWindow* win, Camera* cam, SceneManager *sceneMgr)
: ExampleFrameListener(win, cam, false, false)
{
}
// Overriding the default processUnbufferedKeyInput so the key updates we define
// later on work as intended.
bool processUnbufferedKeyInput(const FrameEvent& evt)
{
return true;
}
// Overriding the default processUnbufferedMouseInput so the Mouse updates we define
// later on work as intended.
bool processUnbufferedMouseInput(const FrameEvent& evt)
{
return true;
}
bool frameStarted(const FrameEvent &evt)
{
return ExampleFrameListener::frameStarted(evt);
}
protected:
bool mMouseDown; // Whether or not the left mouse button was down last frame
Real mToggle; // The time left until next toggle
Real mRotate; // The rotate constant
Real mMove; // The movement constant
SceneManager *mSceneMgr; // The current SceneManager
SceneNode *mCamNode; // The SceneNode the camera is currently attached to
};
class TutorialApplication : public ExampleApplication
{
public:
TutorialApplication()
{
}
~TutorialApplication()
{
}
protected:
void createCamera(void)
{
}
void createScene(void)
{
}
void createFrameListener(void)
{
}
};
#if OGRE_PLATFORM == PLATFORM_WIN32 || OGRE_PLATFORM == OGRE_PLATFORM_WIN32
#define WIN32_LEAN_AND_MEAN
#include "windows.h"
INT WINAPI WinMain(HINSTANCE hInst, HINSTANCE, LPSTR strCmdLine, INT)
#else
int main(int argc, char **argv)
#endif
{
// Create application object
TutorialApplication app;
try {
app.go();
} catch(Exception& e) {
#if OGRE_PLATFORM == PLATFORM_WIN32 || OGRE_PLATFORM == OGRE_PLATFORM_WIN32
MessageBox(NULL, e.getFullDescription().c_str(), "An exception has occurred!", MB_OK | MB_ICONERROR | MB_TASKMODAL);
#else
fprintf(stderr, "An exception has occurred: %s\n",
e.getFullDescription().c_str());
#endif
}
return 0;
}
If you are using the OgreSDK under Windows, be sure to add the "[OgreSDK_DIRECTORY]\samples\include" directory to this project (the ExampleApplication.h file is located there) in addition to the standard include. If using the Ogre source distribution, this should be located in the "[OgreSource_DIRECTORY]\Samples\Common\include" directory. Do NOT try to run this program yet, since we have not defined keyboard behaviour yet. If you have problems, check this Wiki page for information about setting up your compiler, and if you still have problems try the Help Forum.
We will be defining the program controls during this tutorial.
FrameListeners
Introduction
In the previous tutorials we only looked at what we could do when we add code to the createScene method. In Ogre, we can register a class to receive notification before and after a frame is rendered to the screen. This FrameListener interface defines two functions:
bool frameStarted(const FrameEvent& evt) bool frameEnded(const FrameEvent& evt)
Ogre's main loop (Root::startRendering) looks like this:
- The Root object calls the frameStarted method on all registered FrameListeners.
- The Root object renders one frame.
- The Root object calls the frameEnded method on all registered FrameListeners.
This loops until any of the FrameListeners return false from frameStarted or frameEnded. The return values for these functions basically mean "keep rendering". If you return false from either, the program will exit. The FrameEvent object contains two variables, but only the timeSinceLastFrame is useful in a FrameListener. This variable keeps track of how long it's been since the frameStarted or frameEnded last fired. Note that in the frameStarted method, FrameEvent::timeSinceLastFrame will contain how long it has been since the last frameStarted event was last fired (not the last time a frameEnded method was fired).
One important concept to realize about Ogre's FrameListeners is that the order in which they are called is entirely up to Ogre. You cannot determine which FrameListener is called first, second, third...and so on. If you need to ensure that FrameListeners are called in a certain order, then you should register only one FrameListener and have it call all of the objects in the proper order.
You might also notice that the main loop really only does three things, and since nothing happens in between the frameEnded and frameStarted methods being called, you can use them almost interchangably. Where you decide to put all of your code is entirely up to you. You can put it all in one big frameStarted or frameEnded method, or you could divide it up between the two.
Registering a FrameListener
Currently the above code will compile, but since we have overridden the createFrameListener method of ExampleApplication and createCamera, if you run the application you will not be able to kill it. We'll first fix this problem before continuing on to the rest of the tutorial.
Find the TutorialApplication::createCamera method and add the following code to it:
// create camera, but leave at default position
mCamera = mSceneMgr->createCamera("PlayerCam");
mCamera->setNearClipDistance(5);
We have not done anything out of the ordinary with this. The only reason we need to override ExampleApplication's createCamera method is because the createCamera method moves the camera and changes its orientation, which we do not want for this tutorial.
Since the Root class is what renders frames, it is also in charge of keeping track of FrameListeners. The first thing we need to do is create an instance of our TutorialFrameListener and register it with the Root object. Find the TutorialApplication::createFrameListener method, and add this code to it:
// Create the FrameListener
mFrameListener = new TutorialFrameListener(mWindow, mCamera, mSceneMgr);
mRoot->addFrameListener(mFrameListener);
The mRoot and mFrameListener variables are defined in the ExampleApplication class. The addFrameListener method adds a FrameListener, and the removeFrameListener method removes a FrameListener (that is, the FrameListener will no longer receive updates). Note that the add|removeFrameListener methods only take in a pointer to a FrameListener (that is, FrameListeners do not have names you can use to remove them). This means that you will need to hold a pointer to each FrameListener you create so that you can later remove them.
The ExampleFrameListener (which our TutorialFrameListener is derived from), also provides a showDebugOverlay(bool) function, which tells the ExampleApplication whether or not to show the framerate box in the bottom left corner. We'll turn that on as well:
// Show the frame stats overlay
mFrameListener->showDebugOverlay(true);
Be sure you can compile the application before continuing.
Setting up the Scene
Introduction
Before we dive directly into the code, I would like to briefly outline what we will be doing so that you understand where I am going when we create and add things to the scene.
We will be placing one object (a ninja) in the scene, and one point light in the scene. If you left click the mouse, the light will toggle on and off. Holding down the right mouse button turns on "mouse look" mode (that is, you look around with the Camera). We will also be placing SceneNodes around the scene which we will be attaching the Camera to for different viewpoints. Pressing the 1 and 2 buttons chooses which Camera viewpoint to view the scene from.
The Code
Find the TutorialApplication::createScene method. The first thing we will be doing is setting the ambient light of the scene very low. We want scene objects to still be visible when the light is off, but we also want the light going on/off to be noticable:
mSceneMgr->setAmbientLight(ColourValue(0.25, 0.25, 0.25));
Now, add a Ninja entity to the scene at the origin:
Entity *ent = mSceneMgr->createEntity("Ninja", "ninja.mesh");
SceneNode *node = mSceneMgr->getRootSceneNode()->createChildSceneNode("NinjaNode");
node->attachObject(ent);
Now we will create a white point light and place it in the Scene, a small distance (relatively) away from the Ninja:
Light *light = mSceneMgr->createLight("Light1");
light->setType(Light::LT_POINT);
light->setPosition(Vector3(250, 150, 250));
light->setDiffuseColour(ColourValue::White);
light->setSpecularColour(ColourValue::White);
Now we need to create the SceneNodes which the Camera will be attached to:
// Create the scene node
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode1", Vector3(-400, 200, 400));
node->yaw(Degree(-45));
node->attachObject(mCamera);
// create the second camera node
node = mSceneMgr->getRootSceneNode()->createChildSceneNode("CamNode2", Vector3(0, 200, 400));
Now we are done with the TutorialApplication class. Onto the TutorialFrameListener...
TutorialFrameListener
Variables
We have defined a few variables in the TutorialFrameListener class which I'd like to go over before we get any further:
bool mMouseDown; // Whether or not the left mouse button was down last frame
Real mToggle; // The time left until next toggle
Real mRotate; // The rotate constant
Real mMove; // The movement constant
SceneManager *mSceneMgr; // The current SceneManager
SceneNode *mCamNode; // The SceneNode the camera is currently attached to
The mSceneMgr holds a pointer to the current SceneManager and the mCamNode holds the current SceneNode that the Camera is attached to. The mRotate and mMove are our constants of rotation and movement. If you want the movement or rotation to be faster or slower, tweak these variables to be higher or lower.
The other two variables (mToggle and mMouseDown) control our input. We will be using "unbuffered" mouse and key input in this tutorial (buffered input will be the subject of our next tutorial). This means that we will be calling methods during our frame listener to query the state of the keyboard and mouse. We run into an interesting problem when we try to use the keyboard to change the state of some object on the screen. If we see that a key is down, we can act on this information, but what happens the next frame? Do we see that the same key is down and do the same thing again? In some cases (like movement with the arrow keys) this is what we want to do. However, let's say we want the "T" key to toggle between a light being on or off. The first frame the T key is down, the light gets toggled, the next frame the T key is still down, so it's toggled again... and again and again until the key is released. We have to keep track of the key's state between frames to avoid this problem. I will present two separate methods for solving this.
The mMouseDown keeps track of whether or not the mouse was also down the previous frame (so if mMouse down is true, we do not perform the same action again until the mouse is released). The mToggle button specifies the time until we are allowed to perform an action again. That is, when a button is pressed, mToggle is set to some length of time where no other actions can occur.
Constructor
The first thing to notice about the constructor is that we make a call to the ExampleFrameListener's constructor:
: ExampleFrameListener(win, cam, false, false)
The important thing to note is that the third and fourth variables are set to false. The third variable specifies if we want to use buffered key input, the fourth is if we want to use buffered mouse input (which we don't in this tutorial).
In the TutorialFrameListener constructor, we will set default values for all variables:
// key and mouse state tracking
mMouseDown = false;
mToggle = 0.0;
// Populate the camera and scene manager containers
mCamNode = cam->getParentSceneNode();
mSceneMgr = sceneMgr;
// set the rotation and move speed
mRotate = 0.13;
mMove = 250;
That's it. The mCamNode variable is initialized to be whatever the current parent of the camera is.
The frameStarted Method
Now we are going to get into the real meat of the tutorial: performing actions every frame. Currently our frameStarted method has the following code in it:
return ExampleFrameListener::frameStarted(evt);
This chunk of code is what has allowed the tutorial application to run until we could get to this point. The ExampleFrameListener::frameStarted method defines a lot of behavior (such as all of the key bindings, all of the camera movement, etc). Clear out the contents of the TutorialFrameListener::frameStarted method.
The Open Input System (OIS) provides three primary classes to retrieve input: Keyboard, Mouse, and Joystick. In these tutorials we will really only be covering how to use the Keyboard and Mouse objects. If you are interested in using a joystick (or gamepad) with Ogre, you should look into the Joystick class.
The first thing we will need to do when using unbuffered input is to capture the current state of the keyboard and mouse. We do this by calling the capture method of the Mouse and Keyboard objects. The example framework already creates these objects for us in the mMouse and mKeyboard variables. Add the following code to the now empty TutorialFrameListener::frameStarted member function:
mMouse->capture();
mKeyboard->capture();
Next, we want to be sure that the program exits if the Escape key is pressed. We check to see if a button is pressed by calling the isKeyDown method of InputReader and specifying a KeyCode. If the Escape key is pressed, we'll just return false to end the program:
if(mKeyboard->isKeyDown(OIS::KC_ESCAPE))
return false;
In order to continue rendering the frameStarted method must return a positive boolean value. To do we will add the following line to the end of the method.
return true;
All of the following code that we will be discussing goes above that final "return true" line.
The first thing we are going to do with our FrameListener is make the left mouse button toggle the light on and off. We can find out if a mouse button is down by calling the getMouseButton method of InputReader with the button we want to query for. Usually 0 is the left mouse button, 1 is the right mouse button, and 2 is the center mouse button. On some systems button 1 is the middle and 2 is the right mouse button. Try this configuration if the mouse buttons don't work as expected.
bool currMouse = mMouse->getMouseState().buttonDown(OIS::MB_Left);
The currMouse variable will be true if the mouse button is down. Now we will toggle the light depending on whether or not currMouse is true, and if the mouse was not held down the previous frame (because we only want to toggle the light once every time the mouse is pressed). Also note that the setVisible method of the Light class determines if the object actually emits light or not:
if (currMouse && ! mMouseDown)
{
Light *light = mSceneMgr->getLight("Light1");
light->setVisible(! light->isVisible());
} // if
Now we need to set the mMouseDown variable to equal whatever the currMouse variable contains. Next frame this will tell us if the mouse button was up or down previously.
mMouseDown = currMouse;
Compile and run the application. Now left clicking toggles the light on and off! Note that since we no longer call the ExampleFrameListener's frameStarted method, we cannot move the camera around (yet).
This method of storing the previous state of the mouse button works well, since we know we already have acted on the mouse state. The drawback is to use this for every key we bind to an action, we'd need a boolean variable for it. One way we can get around this is to keep track of the last time any button was pressed, and only allow actions to happen after a certain amount of time has elapsed. We keep track of this state in the mToggle variable. If mToggle is greater than 0, then we do not perform any actions, if mToggle is less than 0, then we do perform actions. We'll use this method for the following two key bindings.
The first thing we want to do is decrement the mToggle variable by the time that has elapsed since the last frame:
mToggle -= evt.timeSinceLastFrame;
Now that we have updated mToggle, we can act on it. Our next key binding is making the 1 key attach the Camera to the first SceneNode. Before this, we check to make sure the mToggle variable is less than 0:
if ((mToggle < 0.0f ) && mKeyboard->isKeyDown(OIS::KC_1))
{
Now we need to set the mToggle variable so that it will be 1 second until the next action can be performed:
mToggle = 0.5f;
Next, we need to remove the camera from whatever it is currently attached to, set the mCamNode to contain "CamNode1", and attach the camera to it.
mCamera->getParentSceneNode()->detachObject(mCamera);
mCamNode = mSceneMgr->getSceneNode("CamNode1");
mCamNode->attachObject(mCamera);
}
We will also do this for CamNode2 when the 2 button is pressed. The code is identical except for changing 1 to 2, and using an else if instead of if (because we wouldn't be doing both at the same time):
else if ((mToggle < 0.0f) && mKeyboard->isKeyDown(OIS::KC_2))
{
mToggle = 0.5f;
mCamera->getParentSceneNode()->detachObject(mCamera);
mCamNode = mSceneMgr->getSceneNode("CamNode2");
mCamNode->attachObject(mCamera);
}
Compile and run the tutorial. We can now swap the Camera's viewpoint by pressing 1 and 2.
The next thing we need to do is translate mCamNode whenever the user holds down one of the arrow keys or WASD. Unlike the code above, we do not need to keep track of the last time we moved the camera, since for every frame the key is held down we want to translate it again. This makes our code relatively simple. First we will create a Vector3 to hold where we want to translate to:
Vector3 transVector = Vector3::ZERO;
Now, when the W key or the up arrow is pressed, we want to move straight forward (which is the negative z axis, remember negative z is straight into the computer screen):
if (mKeyboard->isKeyDown(OIS::KC_UP) || mKeyboard->isKeyDown(OIS::KC_W))
transVector.z -= mMove;
We do almost the same thing for the S and Down arrow keys, but we move in the positive z axis instead:
if (mKeyboard->isKeyDown(OIS::KC_DOWN) || mKeyboard->isKeyDown(OIS::KC_S))
transVector.z += mMove;
For left and right movement, we go in the positive or negative x direction:
if (mKeyboard->isKeyDown(OIS::KC_LEFT) || mKeyboard->isKeyDown(OIS::KC_A))
transVector.x -= mMove;
if (mKeyboard->isKeyDown(OIS::KC_RIGHT) || mKeyboard->isKeyDown(OIS::KC_D))
transVector.x += mMove;
Finally, we also want to give a way to move up and down along the y axis. I personally use E/PageDown for downwards motion and Q/PageUp for upwards motion:
if (mKeyboard->isKeyDown(OIS::KC_PGUP) || mKeyboard->isKeyDown(OIS::KC_Q))
transVector.y += mMove;
if (mKeyboard->isKeyDown(OIS::KC_PGDOWN) || mKeyboard->isKeyDown(OIS::KC_E))
transVector.y -= mMove;
Now, our transVector variable has the translation we wish to apply to the camera's SceneNode. The first pitfall we can encounter when doing this is that if you rotate the SceneNode, then our x, y, and z coordinates will be wrong when translating. To fix this, we need to apply all of the rotations we have done to the SceneNode to our translation node. This is actually simpler than it sounds.
To represent rotations, Ogre does not use transformation matrices like some graphics engines. Instead it uses Quaternions for all rotation operations. The math behind Quaternions involves four dimensional linear algebra, which is very difficult to understand. Thankfully, you do not have to understand the math behind them to understand how to use them. Quite simply, to use a Quaternion to rotate a vector, all you have to do is multiply the two together. In this case, we want to apply all of the rotations done to the SceneNode to the translation vector. We can get a Quaternion representing these rotations by calling SceneNode::getOrientation(), then we can apply them to the translation node using multiplication.
The second pitfall we have to watch out for is we have to scale the amount we translate by the amount of time since the last frame. Otherwise, how fast you move would be dependent on the framerate of the application. Definitely not what we want. This is the function call we need to make to translate our camera node without encountering these problems:
mCamNode->translate(transVector * evt.timeSinceLastFrame, Node::TS_LOCAL);
Now we have introduced something new. Whenever you translate a node, or rotate it about any axis, you can specify which Transformation Space you want to use to move the object. Normally when you translate an object, you do not have to set this parameter. It defaults to TS_PARENT, meaning that the object is moved in whatever transformation space the parent node is in. In this case, the parent node is the root scene node. When we press the W button (to move forward), we subtracted from the Z direction, meaning we move towards the negative Z axis. If we did not specify TS_LOCAL in this previous line of code, we would move the camera along the global -Z axis. However, since we are trying to make a camera which goes forward when we press W, we need it to go in the direction that the node is actually facing. Hence, we use the "local" transformation space.
There is another way we can do this (though it is less direct). We could have gotten the orientation of the node, a quaternion, and multiplied this by the direction vector to get the same result. This would be perfectly valid:
// Do not add this to the program
mCamNode->translate(mCamNode->getOrientation() * transVector * evt.timeSinceLastFrame, Node::TS_WORLD);
This also translates the camera node in the local space. In this case, there is no real reason to do this. Ogre defines three transform spaces: TS_LOCAL, TS_PARENT, and TS_WORLD. There may be a case where you need to make a translation or a rotation in another vector space than these three. If that is the case, you would do it similar to the previous line of code. Take a quaternion representing the vector space (or the orientation of whatever object you are trying to match), multiply it by the translation vector to get the corrected translation vector, and then move it in the TS_WORLD space. This will probably not come up for quite a while though, and we will not refer to it in any of the future tutorials.
Now that we have key movement down, we want to have the mouse affect which direction we are looking in, but only if the user is holding down the right mouse button. To do this we first check to see if the right mouse button is down:
if (mMouse->getMouseState().buttonDown(OIS::MB_Right))
{
If so, we yaw and pitch the camera based on the amount the mouse has moved since the last frame. To do this, we will take the X and Y relative changes and turn these into pitch and yaw function calls:
mCamNode->yaw(Degree(-mRotate * mMouse->getMouseState().X.rel), Node::TS_WORLD);
mCamNode->pitch(Degree(-mRotate * mMouse->getMouseState().Y.rel), Node::TS_LOCAL);
}
Note that we have used the TS_WORLD vector space for the yaw (rotation functions always use TS_LOCAL as a default, if not specified). We are trying to ensure that the pitch of the object does not affect the yaw in any way. We always want the yaw to rotate us around the same axis. This is the third pitfall: forgetting the interactions between rotations. If we set the yaw to take place in TS_LOCAL, we would get something like this happening: [Dead Link]http://www.idleengineer.net/images/beginner04_rot.png Alternate: http://tatis3.springnote.com/pages/1024348/attachments/434180
Compile the program and try it out.
This tutorial is not meant to be a full walkthrough on rotations and Quaternions (that is enough material to fill an entire tutorial by itself). In the next tutorial, we will use buffered mouse input instead of checking for keys being down every frame.
- Proceed to Basic Tutorial 5 Buffered Input
| Ogre Tutorials |
|---|
|
Ogre Beginner Tutorials: 1. Basic Introduction - 2. Cameras, Lights and Shadows - 3. Terrain, Sky and Fog - 4. Frame Listeners and Unbuffered Input - 5. Buffered Input - 6. The Ogre Startup Sequence - 7. CEGUI and OGRE - 8. Multiple and Dual SceneManagers Intermediate Tutorials: 1. Animation, Interpolation and Quaternions - 2. RaySceneQueries and Basic Mouse Usage (1/2) - 3. Mouse Picking and SceneQuery Masks (2/2) - 4. Volume Selection and Manual Objects - 5. Static Geometry - 6. Projective Decals - 7. Render to Texture Advanced Tutorials: 1. Resources and ResourceManagers See also: Artist Tutorials - Ogre Articles - Cookbook |

