History: Basic Tutorial 4
Preview of version: 113
- «
- »
|
|
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 OIS's unbuffered input system. 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->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 the FrameListener and unbuffered input.
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.
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.
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.
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 toggle = 0.0; static Ogre::Real rotate = .13; static Ogre::Real move = 250;
The last two variables will be used for movement later. toggle 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 For Avoiding Rapid Toggling
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.
mToggle acts as a 0.5 second delay before any additional changes can take place.
In practice, this delay is longer than necessary, but it illustrates the point.
Let's add an additional way to toggle light on and off:
if ((mToggle < 0.0f ) && mKeyboard->isKeyDown(OIS::KC_1)) { mToggle = 0.5; Ogre::Light* light = mSceneMgr->getLight("pointLight"); light->setVisible(! light->isVisible()); }
Compile and run the tutorial. We can now turn the light on and off by pressing '1'.
Moving the Ninja
The next thing we need to do is translate the node holding the ninja whenever the user holds down one of the IJKL keys. 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 the position we want to translate to:
Ogre::Vector3 transVector = Ogre::Vector3::ZERO;
Now, when the I key 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_I)) // Forward { transVector.z -= mMove; }
We do almost the same thing for the K key, but we move in the positive z axis instead:
if (mKeyboard->isKeyDown(OIS::KC_K)) // Backward { transVector.z += mMove; }
For left and right movement, we go in the positive or negative x direction, or yaw to the left or right when left-shift is held:
if (mKeyboard->isKeyDown(OIS::KC_J)) // Left - yaw or strafe { if(mKeyboard->isKeyDown( OIS::KC_LSHIFT )) { // Yaw left mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(mRotate * 5)); } else { transVector.x -= mMove; // Strafe left } } if (mKeyboard->isKeyDown(OIS::KC_L)) // Right - yaw or strafe { if(mKeyboard->isKeyDown( OIS::KC_LSHIFT )) { // Yaw right mSceneMgr->getSceneNode("NinjaNode")->yaw(Ogre::Degree(-mRotate * 5)); } else { transVector.x += mMove; // Strafe right } }
Finally, we also want to give a way to move up and down along the y axis, using keys U and O:
if (mKeyboard->isKeyDown(OIS::KC_U)) // Up { transVector.y += mMove; } if (mKeyboard->isKeyDown(OIS::KC_O)) // Down { transVector.y -= mMove; }
Now, our transVector variable has the translation we wish to apply to the Ninja'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 vector. This is actually simpler than it sounds. 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 I 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 ninja along the global -Z axis. However, since we are trying to make the ninja go forward when we press I, we need it to go in the direction that the node is actually facing, so we use the local transformation space.
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:
mSceneMgr->getSceneNode("NinjaNode")->translate(transVector * evt.timeSinceLastFrame, Ogre::Node::TS_LOCAL);
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. 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. This would be perfectly valid:
// Do not add this to the program mSceneMgr->getSceneNode("NinjaNode")->translate(mSceneMgr->getSceneNode("NinjaNode")->getOrientation() * transVector * evt.timeSinceLastFrame, Ogre::Node::TS_WORLD);
This also translates the ninja node in the local space. In this case, there is no real reason to do this.
Ogre defines three transformation 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 similarly 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.
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.
Conclusion
Now you should have a base understanding of frame listeners and unbuffered input using OIS.
Full Source
The full source for this tutorial is here.
Next
Alias: Basic_Tutorial_4