Ogre基础教程4:监听器类与无缓冲输入处理

来源:互联网 发布:家装平面模板软件 编辑:程序博客网 时间:2024/06/04 07:17

原文地址:http://www.ogre3d.org/tikiwiki/tiki-index.php?page=Basic+Tutorial+4&structure=Tutorials

Ogre基础教程4:监听器类与无缓冲输入处理

教程介绍

这篇教程将介绍监听器类的概念。我们将使用Ogre的FrameListener类,在每一帧中处理无缓冲的输入。这个输入系统依赖于使用像isKeyDown这样的方法,向键盘这样的输入设备请求设备当前的状态。无论一个输入事件发生在何时,回调方法都会被调用;这与有缓冲输入形成了对比。有缓冲输入将在下一篇教程中探讨。

预备知识

本教程假设,你已经知道如何建立一个Ogre项目,并且成功地编译。如果你需要对这些的帮助,那么请阅读《Setting Up An Application》章节。本篇教程亦是基础教程系列的一部分,并且默认你已掌握了前面教程中的知识。
这里写图片描述

目录

预备知识
设置场景
监听器类
监听器如何工作
注册一个监听器
处理输入
调用输入函数
避免切换过快的另一种方法
让忍者移动
总结

设置场景

开始,请如下设置你的TutorialApplication 类:

TutorialApplication.h

<pre><code> #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);

};

TutorialApplication.cpp

#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

在createScene中,我们已经构建了一个忍者实体,并且放置了一个有向光照。到这里为止,都与之前类似。我们将使用这个简单的场景来显示无缓冲输入以及监听器。
目标计划是当用户单机鼠标左键时,切换灯光的开关;我们也允许用户用IJKL按键来旋转、移动忍者对象;这将包含无缓冲输入的使用。这意味着,我们将 不 收集所有的输入事件并且处理每一个,而是我们询问Ogre,一个按键是否在我们请求的时间点被按下。有缓冲输入将在教程系列的下一篇涉及。

FrameListener类

监听器类的概念,在许多不同的编程情形下都被用到。这个类将被建立来接收通知,无论特定的事件发生在任何时间。通过使用“回调方法”,此类被通知。当一个监听器被注册关注的事件发生时,应用将会“回调”监听器类,通过调用一个被设计来处理事件的预定义方法。
在Ogre中,我们可以注册一个监听器类,在帧渲染处理的不同步骤中接收通知。这个类被称为 帧监听器 。帧监听器声明了三个回调方法:

virtual bool frameStarted(const FrameEvent&)

在每一帧被渲染之前被调用。

virtual bool frameRenderingQueued(const FrameEvent&)    

在渲染缓冲被翻转之前被调用。

virtual bool frameEnded(const FrameEvent&)

在每一帧被渲染之后即被调用。

如果这些方法中的任意一个返回false,那么你的应用将会退出它的渲染循环。当你希望你的应用继续渲染时,确定你在使用任何这些方法时返回的是true。
帧事件结构包含两个变量,但仅有来自帧监听器的timeSinceLastFrame是有用的。这个变量持续追踪,从最后一次分别对frameStarted或frameEnded调用后,过了几秒。所以,如果你在frameStarted中检查这个变量,将得到从最后一次调用frameStarted到当前经过的时间;而在frameEnded中检查这个变量,将得到从上一次调用frameEnded到当前经过的时间。
如果在你的场景中,有多个帧监听器是活动的;那么,你无法保证它们将按任何特定顺序被调用,这很重要。如果你需要保证事情以特定顺序发生,你应当使用一个帧监听器,并且以正确顺序进行所有调用。
你应使用这些方法中的哪个,取决于你特定的需求。如果你进行普通的每帧更新,那么你应将这些工作放在frameRenderingQueued中。这个方法会在GPU即将开始翻转你的渲染缓冲之前被调用。出于性能考虑,你会希望在GPU进行工作时,保持你的CPU也不闲置。当你必须在渲染过程中一个特定的时间点构建东西时,其它的方法可以发挥作用;当你添加了像物理库这样的内容到应用中时,这些事情变得更常见。

帧监听器如何工作

为了更好地理解监听器过程如何工作,我们来看Ogre::Root::renderOneFrame这个方法:

bool Root::renderOneFrame(void){  if(!_fireFrameStarted())    return false;  if (!_updateAllRenderTargets())    return false;  return _fireFrameEnded();}

你可以看到,这个方法在更新所有渲染目标之前,调用了FrameStarted事件,并且随后在最后调用FrameEnded事件。为了明白FrameRenderingQueued事件何时调用,我们阅读来自在前例中被调用的 Ogre::Root::_updateAllRenderTargets 方法的摘录:

bool Root::_updateAllRenderTargets(void){  mActiveRenderer->_updateAllRenderTargets(false);  bool ret = _fireFrameRenderingQueued();  // thread is blocked for final swap  mActiveRenderer->_swapAllRenderTargetBuffers(    mActiveRenderer->getWaitForVerticalBlank());  ...

你可以看到,它在更新了所有渲染目标之后、线程被阻塞之前,立刻调用了FrameRenderingQueued事件;所以GPU可以交换所有的渲染缓冲。

Registering a FrameListener

好消息是:我们的TutorialApplication类已经是一个帧监听器了。它继承自BaseApplication,这继承自FrameListener。你可以在BaseApplication的头部看到这些内容:

class BaseApplication  : public Ogre::FrameListener,    public Ogre::WindowEventListener,    public OIS::KeyListener,    public OIS::MouseListener,    OgreBites::SdkTrayListener{  ...

如你所见,BaseApplication实际上担任了多个不同的监听器。BaseApplication实现了我们在基础教程3中已经改写过的createFrameListener和frameRenderingQueued。

对于一个接收通知的监听器,它必须在Ogre::Root的实例中注册自己。这允许Ogre::Root能够调用帧监听器的回调方法,无论一个帧事件发生在何时。我们使用Ogre::Root::addFrameListener方法来注册我们的帧监听器。通过使用removeFrameListener方法,一个帧监听器可以要求不再被帧事件通知。
如果你查阅BaseApplication,你可以看到,它在createFrameListener中使用了这个方法将自己注册到Ogre::Root中:

mRoot->addFrameListener(this);

这保证了我们的frameRenderingQueued方法在合适的时机会被调用。

处理输入

注:这一部分使用OIS系统处理输入;当Ogre使用外部构建提供窗体(如Qt提供的)进行渲染时,如果渲染目标窗体不是顶级窗体(top level window),OIS系统将无法注册输入设备,产生错误。所以使用OIS的部分暂不翻译。

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(external link) 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 frameRenderingQueued right after our previous code:

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(external link) 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.

0 0
原创粉丝点击