This tutorial will introduce the concept of a listener class. We will use Ogre's FrameListener class to process unbuffered input every frame. This input system relies on asking for the current state of an input device like the keyboard with methods like isKeyDown. This is in contrast to buffered input where callback methods are called whenever an input event occurs. Buffered input will be covered in the next tutorial. The full source for this tutorial is here. |
Any problems you encounter during working with this tutorial should be posted in the Help Forum.
Prerequisites
This tutorial assumes that you already know how to set up an Ogre project and compile it successfully. If you need help with this, then read Setting Up An Application. This tutorial is also part of the Basic Tutorials series and knowledge from the previous tutorials will be assumed.
Table of contents
Setting Up the Scene
To begin, set up your TutorialApplication class like this:
#include "BaseApplication.h" class TutorialApplication : public BaseApplication { public: TutorialApplication(); virtual ~TutorialApplication(); protected: virtual void createScene(); virtual bool frameRenderingQueued(const Ogre::FrameEvent& fe); private: bool processUnbufferedInput(const Ogre::FrameEvent& fe); };
#include "TutorialApplication.h" TutorialApplication::TutorialApplication() { } TutorialApplication::~TutorialApplication() { } void TutorialApplication::createScene() { mSceneMgr->setAmbientLight(Ogre::ColourValue(.25, .25, .25)); Ogre::Light* pointLight = mSceneMgr->createLight("PointLight"); pointLight->setType(Ogre::Light::LT_POINT); pointLight->setPosition(250, 150, 250); pointLight->setDiffuseColour(Ogre::ColourValue::White); pointLight->setSpecularColour(Ogre::ColourValue::White); Ogre::Entity* ninjaEntity = mSceneMgr->createEntity("ninja.mesh"); Ogre::SceneNode* ninjaNode = mSceneMgr->getRootSceneNode()->createChildSceneNode("NinjaNode"); ninjaNode->attachObject(ninjaEntity); } bool TutorialApplication::frameRenderingQueued(const Ogre::FrameEvent& fe) { bool ret = BaseApplication::frameRenderingQueued(fe); return ret; } bool TutorialApplication::processUnbufferedInput(const Ogre::FrameEvent& fe) { return true; } // MAIN FUNCTION OMITTED FOR SPACE
In createScene, we've constructed a ninja Entity and placed a directional light. This should all be familiar by now. We will use this simple scene to demonstrate the use of unbuffered input and the FrameListener.
The plan is to toggle the light on and off when the user clicks the left mouse button. We will also allow the user to rotate and move the ninja Entity with the IJKL keys. This will involve the use of unbuffered input. This means that we are not gathering up all of the input events and dealing with each one. Instead, we simply ask Ogre if a key is currently being pressed down at the time of our request. Buffered input will be covered in the next tutorial series.
The FrameListener Class
The concept of a listener class is used in many different programming situations. This class will be set up to receive notifications whenever certain events occur. The class is notified by the use of "callback methods". When an event the listener is registered for occurs, the application will "call back" to the listener class by calling a pre-defined method that was designed to handle the event.
In Ogre, we can register a listener class to be notified during different stages in the frame rendering process. This class is called the FrameListener. The FrameListener declares three callback methods:
virtual bool frameStarted(const FrameEvent&) | called before each frame is rendered |
virtual bool frameRenderingQueued(const FrameEvent&) | called just before the rendering buffers are flipped |
virtual bool frameEnded(const FrameEvent&) | called right after each frame is rendered |
If any of these methods returns false, then your application will exit its rendering loop. Make sure to return true when using any of these methods while you want your application to continue rendering.
The FrameEvent struct contains two variables, but only timeSinceLastFrame is useful from the FrameListener. This variable keeps track of how many seconds have passed since the last call to frameStarted or frameEnded respectively. So if you check this variable in frameStarted, it will contain the time since the last call to frameStarted, and if you check it in frameEnded, it will contain the time since the last call to frameEnded.
If you have multiple FrameListeners active in your scene, it is important to know you are not guaranteed they will be called in any particular order. If you need to ensure that things occur in a specific order, then you should use a single FrameListener and make all of the calls in the correct order.
Which of these methods you should use depends on your particular needs. If you are doing normal per frame updates, then you should generally put those in frameRenderingQueued. This method is called right before the GPU begins to flip you rendering buffers. For performance reasons, you want to keep your CPU busy while the GPU does its work. The other methods are useful when you must set up things at a specific time during the rendering process. This kind of thing becomes more common when you add something like a physics library to your application.
How the FrameListener Works
To better understand how the listener process works, we will look at the Ogre::Root::renderOneFrame method:
bool Root::renderOneFrame(void) { if(!_fireFrameStarted()) return false; if (!_updateAllRenderTargets()) return false; return _fireFrameEnded(); }
You can see that this method fires the FrameStarted event before updating all of the render targets, and then fires the FrameEnded event afterwards. To understand when the FrameRenderingQueued event fires, we will look at an excerpt from the Ogre::Root::_updateAllRenderTargets method which is called in the previous example:
bool Root::_updateAllRenderTargets(void) { mActiveRenderer->_updateAllRenderTargets(false); bool ret = _fireFrameRenderingQueued(); // thread is blocked for final swap mActiveRenderer->_swapAllRenderTargetBuffers( mActiveRenderer->getWaitForVerticalBlank()); ...
You can see that it fires the FrameRenderingQueued event immediately after updating all of the render targets, but right before the thread is blocked so the GPU can swap all of the rendering buffers.
Registering a FrameListener
The good news is that our TutorialApplication class is already a FrameListener! It is derived from BaseApplication, which inherits from FrameListener. You can see this by looking at the header for BaseApplication:
class BaseApplication : public Ogre::FrameListener, public Ogre::WindowEventListener, public OIS::KeyListener, public OIS::MouseListener, OgreBites::SdkTrayListener { ...
As you can see, BaseApplication actually serves as a number of different listeners. BaseApplication implements createFrameListener and frameRenderingQueued, both of which we overrode in Basic Tutorial 3.
For a listener to receive notifications, it has to register itself with our instance of Ogre::Root. This allows Ogre::Root to call the FrameListener's callback methods whenever a FrameEvent occurs. To register our FrameListener, we use the Ogre::Root::addFrameListener method. A FrameListener can ask to no longer be notified of FrameEvents by using removeFrameListener.
If you look at BaseApplication, you can see that it uses this method in createFrameListener to register itself with Ogre::Root:
mRoot->addFrameListener(this);
This makes sure our frameRenderingQueued method will be called when appropriate.
Processing Input
We'll begin building our input processing method now. First, we are going to add some static variables we will use to control how the input works. Add the following to the beginning of processUnbufferedInput:
static bool mouseDownLastFrame = false; static Ogre::Real toggleTimer = 0.0; static Ogre::Real rotate = .13; static Ogre::Real move = 250;
The last two variables will be used for movement later. toggleTimer will be used to set how long the system waits before toggling the light again. mouseDownLastFrame will be used to keep track of whether the left mouse button was held down during the last frame. If we do not do this, then the unbuffered input would turn the light on and off many times whenever we clicked. This is because the user's click will almost assuredly last longer than one frame. So it would check the mouse every frame and quickly toggle the light. Sometimes this might be what you want. If you are using the input to move an Entity, then you would want to apply an acceleration to the Entity for every frame the input event is active. The variables are made static simply for convenience. They could have been class members as well, but they are only used in this method.
The Object Oriented Input System (OIS) provides three primary classes for dealing with input: Keyboard, Mouse, and Joystick. These tutorials will cover the use of the Keyboard and Mouse. You can read through the Joystick class to understand how to get input from things like game controllers.
In our case, the BaseApplication class is already capturing Keyboard and Mouse input in BaseApplication::frameRenderingQueued. It is accomplished with these two lines:
mMouse->capture(); mKeyboard->capture();
Since this information is already being captured for us, we don't need to do anything else to access it. Add the following to processUnbufferedInput right after the static variables we just defined:
bool leftMouseDown = mMouse->getMouseState().buttonDown(OIS::MB_Left);
leftMouseDown will be true whenever the left mouse was held down during the last frame. You can see we use constants defined by OIS to identify the different mouse buttons.
Next we are going to toggle the visibility of our light based on the two booleans we've just defined.
if (leftMouseDown && !mouseDownLastFrame) { Ogre::Light* light = mSceneMgr->getLight("PointLight"); light->setVisible(!light->isVisible()); }
First, we check to see if the left mouse button was held down and we make sure it was not held down last frame. This is going to help prevent the rapid toggling problem we mentioned earlier.
The last thing we do is set our mouseDownLastFrame by assigning the current value of leftMouseDown.
mouseDownLastFrame = leftMouseDown;
This ensures that it will hold the correct value next frame. This is why mouseDownLastFrame had to be a static variable. It needed to persist between calls like a class member would.
Calling the Input Function
To get everything working, we need to make sure to call our processUnbufferedInput method each frame. As was mentioned before, the best place to do this is in frameRenderingQueued, because it needs to be done every frame. Add the following to frameRenderingQueued right after the call to BaseApplication::frameRenderingQueued:
if(!processUnbufferedInput(fe)) return false;
This makes sure that we only continue running the application if the input is processed successfully.
Compile and run the application. You should now be able to turn the light on and off by clicking the left mouse button. The Camera controller should still work fine, because we are calling BaseApplication::frameRenderingQueued.
Another Method to Avoid Rapid Toggling
A drawback of our current method is that we would need to add a new boolean for every input event we wanted to keep track of. If we wanted to use the right mouse button, then we would need a rightMouseDownLastFrame variable as well. One way to get around this is by using a timer that resets after either mouse button has been pressed. We simply start the timer countdown after the first click and then only allow another click to affect the light if the timer has passed zero. This is what we'll use the toggleTimer variable for.
The first thing we do is decrease the toggleTimer value by the amount of time that has passed since the last frame. Add the following to frameRenderingQueued right after our previous code:
toggleTimer -= fe.timeSinceLastFrame;
This will make sure toggleTimer is decreased each frame. Once it reaches zero, then we can reset the timer and allow another click to be processed.
if ((toggleTimer < 0) && mMouse->getMouseState().buttonDown(OIS::MB_Right)) { toggleTimer = 0.5; Ogre::Light* light = mSceneMgr->getLight("PointLight"); light->setVisible(!light->isVisible()); }
Compile and run your application. You should now be able to toggle the light with either the left or right mouse button. A side effect of our new method is that the light will slowly turn off and on if you continually hold the right mouse button.
Moving the Ninja
Now we are going to allow the user to move and turn the ninja using the keyboard. We will use the IJKL keys like the WASD keys are used for the Camera, and we will use U and O to turn the ninja. This one of the cases where we do not need to keep track of the event from the last frame, because we want the event to be processed every frame the keys are held down.
The first thing we'll do is create a vector to hold the direction we want to move the ninja. Add the following to processUnbufferedInput right after the mouseDownLastFrame assignment:
Ogre::Vector3 dirVec = Ogre::Vector3::ZERO;
When the I key is pressed, we want the ninja to move straight forward. In the ninja's local coordinate frame, this would mean moving down the negative z-axis.
if (mKeyboard->isKeyDown(OIS::KC_I)) dirVec.z -= move;
For the K key, we do the same thing in the other direction to move the ninja backwards.
if (mKeyboard->isKeyDown(OIS::KC_K)) dirVec.z += move;
We will use the U and O keys for movement up and down. This will be along the ninja's y-axis.
if (mKeyboard->isKeyDown(OIS::KC_U)) dirVec.y += move; if (mKeyboard->isKeyDown(OIS::KC_O)) dirVec.y -= move;
For movement left and right, our ninja will move along its x-axis. We also want the user to be able to rotate the ninja if they are holding the left shift button.
if (mKeyboard->isKeyDown(OIS::KC_J)) { if(mKeyboard->isKeyDown(OIS::KC_LSHIFT)) mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(5 * rotate)); else dirVec.x -= move; } if (mKeyboard->isKeyDown(OIS::KC_L)) { if(mKeyboard->isKeyDown(OIS::KC_LSHIFT)) mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(-5 * rotate)); else dirVec.x += move; }
Keep in mind that these do need to be separate if statements for each key. This is because we want the user to be able to move forward and to the side at the same time.
The last thing we need to do is apply this direction vector to our ninja's SceneNode. It may seem like the vector we have created will not correctly move our ninja, because surely as soon as the ninja turns a little, then his forward direction will no longer be along the z-axis, right? This is true in the coordinate system of our world coordinates (the coordinates of our root SceneNode), but it is not true for the local coordinate system of our ninja. This coordinate system moves along with the ninja so that its z-axis is always pointing straight ahead.
By using this local coordinate system, we are able to translate the ninja correctly by using constant notions of direction. The SceneNode::translate method has a second parameter that allows us to choose which transformation space to use. We can choose from: TS_LOCAL, TS_PARENT, and TS_WORLD. If we use TS_LOCAL, then our direction vector will work almost perfectly.
The one other thing we need to worry about is consistent movement. Different computers will render frames at different speeds. This means that any changes in our application that need to happen at a steady pace will speed up or slow down based on how fast the computer updates. This is one of the most well-known problems in graphics programming and it is solved in a large number of ways. If you want to know more, this article has become a somewhat famous discussion of the topic.
To fix the problem for our application, we will simply multiply our direction vector by the amount of time that has passed since the last frame.
mSceneMgr->getSceneNode("NinjaNode")->translate( dirVec * fe.timeSinceLastFrame, Ogre::Node::TS_LOCAL);
This means if a relatively long period of time passes, then the ninja will be moved farther in that frame. This is what we want, because the ninja would need to make up for the longer frame time by moving a farther distance. This is one of the simplest ways of dealing with this problem, but if you read the Fix Your Timestep article, you'll learn it is not always the best idea. This becomes especially true when you are working with something like a physics engine, because when time speeds up or slows down it can really mess up your physics.
Compile and run the application. You should now be able to move the ninja around with all of the keys we've defined.
Conclusion
This tutorial has introduced the use of unbuffered input. This is a form of input handling where the state of the input device is queried when the information is needed through the use of methods like isKeyDown. This differs from buffered input where the input events are stored in a buffer and then dealt with as a group. This type of input will be the topic of the next tutorial.
The second important topic that was introduced in this tutorial was the notion of a listener class. In particular, we went into detail about Ogre's FrameListener class. After registering this class with our Ogre::Root object, its "callback methods" were automatically called every time a FrameEvent occurred. If performance is the main goal, then it is best to do updates in the frameRenderingQueued callback method, because we want our CPU to have things to do while our GPU blocks to swap all of the rendering buffers.
Full Source
The full source for this tutorial is here.
Next
Alias: Basic_Tutorial_4