Beginner Tutorial 5: Buffered Input
Original version by Clay Culver
Note: This was written for PyOgre 1.0.5, and is known to work with it. If this is not the current version of PyOgre and something is not working or you do not understand anything that is contained here, please post your questions to the PyOgre Forum.
Table of contents
- Getting Started
- Buffered Input in Ogre
- Buffered Input Interfaces
- Other Input Systems
This tutorial assumes you have knowledge of Python programming and you have already installed PyOgre. This tutorial builds on the material covered in previous tutorials, and it assumes you have read them.
In this short tutorial you will be learning to use Ogre's buffered input as opposed to the unbuffered input we used last tutorial. I will also discuss some of the shortcomings of Ogre's input system, and some alternatives that you can use instead of it.
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.
This tutorial will be building on the last tutorial, but we are changing the way that we do input. Since the functionality will be basically the same, we will use the same TutorialApplication class from last time, but we will be starting over on the TutorialFrameListener. Create a file named beginner_5.py:
# this code is in the public domain # http://www.ogre3d.org/wiki/index.php/PyOgre_Beginner_Tutorial_5 from pyogre import ogre import SampleFramework class TutorialFrameListener(SampleFramework.FrameListener): def __init__(self, renderWindow, camera, sceneManager): SampleFramework.FrameListener.__init__(self, renderWindow, camera) # populate the camera and scene manager containers self.camNode = camera.parentSceneNode.parentSceneNode self.sceneManager = sceneManager self.rotate = 72 # rotation speed self.move = 250 # movement speed self.keepRendering = True # whether to continue rendering or not # the direction we are currently moving in self.direction = ogre.Vector3(0, 0, 0) def frameStarted(self, evt): return self.keepRendering class TutorialApplication(SampleFramework.Application): def _createScene(self): sceneManager = self.sceneManager sceneManager.ambientLight = 0.25, 0.25, 0.25 ent = sceneManager.createEntity("Ninja", "ninja.mesh") node = sceneManager.rootSceneNode.createChildSceneNode("NinjaNode") node.attachObject(ent) light = sceneManager.createLight("Light1") light.type = ogre.Light.LT_POINT light.position = 250, 150, 250 light.diffuseColour = 1, 1, 1 light.specularColour = 1, 1, 1 # create the first camera node/pitch node node = sceneManager.rootSceneNode.createChildSceneNode("CamNode1", (-400, 200, 400)) node.yaw(ogre.Degree(-45)) # look at the ninja node = node.createChildSceneNode("PitchNode1") node.attachObject(self.camera) # create the second camera node/pitch node node = sceneManager.rootSceneNode.createChildSceneNode("CamNode2", (0, 200, 400)) node.createChildSceneNode("PitchNode2") def _createCamera(self): self.camera = self.sceneManager.createCamera("PlayerCam") self.camera.nearClipDistance = 5 def _createFrameListener(self): self.frameListener = TutorialFrameListener(self.renderWindow, self.camera, self.sceneManager) self.root.addFrameListener(self.frameListener) self.frameListener.showDebugOverlay(True) if __name__ == '__main__': try: ta = TutorialApplication() ta.go() except ogre.OgreException, e: print e
The program controls will be the same as in the last tutorial.
In the previous tutorial we used unbuffered input, that is, every frame we queried the state of an InputReader object to see what keys and mouse buttons were being held down. Buffered input uses mouse and key listener interfaces to update objects. For example, when a key is pressed, a KeyListener.keyPressed event is fired and when the button is released (no longer being pressed) a KeyListener.keyReleased event is fired to all registered KeyListener classes. This takes care of having to keep track of toggle times or whether the key was pressed or not during the previous frame.
The Ogre buffered input system is not entirely consistent, which I will discuss later. There are also parts of the system that are broken entirely, and cannot be used (which I will also discuss). Ogre also does not support joysticks or gamepads, which can be a serious problem in certain types of games.
Why is Ogre's input system partially broken and not internally consistent? Well to put it quite simply, Ogre is designed to only be a graphics engine. Sinbad has said time and again that the input system would not even exist if it wasn't needed to create cross-platform demos for the engine.
In short, the Ogre input system is "good enough" for most applications, and most of the shortcomings can be overcome. You may want to look into other input systems, which we will cover at the end of the tutorial.
Even though in this tutorial our FrameListener is also the key and mouse listener, please don't get stuck in the concept of having to combine the FrameListener and key/mouse listeners. It's often a very good idea to split the two up, especially in larger applications.
There are three interfaces we will be using. The KeyListener interface is used for all key input callbacks, the MouseListener interface is used for mouse clicking callbacks, and the MouseMotionListener interface is used for all mouse motion and dragging. You will notice a lot of functions that don't seem to be useful/relevant. Ogre used to define and implement its own GUI system, which has been largely replaced by CEGUI. I will mention the legacy code when we run across it, but I will not go into any detail about it.
The KeyListener interface defines several functions for getting keyboard input. The keyPressed method is called when a key is pressed (that is, goes down). The keyReleased function is called when the key comes back up. The keyClicked method is called when a key is pressed and then comes back up (which is almost always equivalent to keyReleased). Whenever a key is pressed, three events are generated: one when the key is pressed down (keyPressed), one when the key is let up (keyReleased), and one after the previous two (keyClicked).
The MouseListener interface defines functions for getting mouse click input. The mousePressed function is called when the user depresses a mouse button. The mouseReleased function is called when the user releases a mouse button. The mouseClicked function is broken and does not work, so do not try to use it. The mouseEntered and mouseExited functions are both legacy code which we will not use.
The MouseMotionListener interface defines functions for getting mouse movement events. The mouseMoved function is called whenever the mouse is moved, and the mouseDragged function is called whenever the mouse is moved when a button is held down. The mouseDragMoved function seems to be broken (at least I cannot find anything that makes it actually get called).
With all of this talk about adding interfaces for KeyListener, MouseListener, and MouseMotionListener on top of the FrameListener class might make you think that we should add these to the TutorialFrameListener's inheritance definition. Normally this would be the case, but the current version of SWIG's director classes (C++ callbacks in Python), does not understand how to handle multiple inheritance.
Obviously this poses several problems. First and foremost, we cannot directly inherit from both KeyListener and MouseListener at the same time. However, it is very common to have Key and Mouse listeners act on the same set of data (as we will do in this tutorial). As a fix for this problem, we have added a class called CombinedListener, which is a Mouse, MouseMotion, Key, and Frame Listener all in one class. In fact, if you pull up the SampleFramework.py and find the FrameListener class, it actually inherits from ogre.CombinedListener instead of directly from ogre.FrameListener. This is why we can call addMouseListener(self) (and similar functions), even though we have not directly inherited from MouseListener.
This bug exists in SWIG, and there is little we can do about it. It has been filed as a bug, and we will update this tutorial as soon as it is fixed.
A few variables have changed from the last tutorial. I have removed toggle and mouseDown (which are no longer needed). I have added a few as well:
self.camNode # the camera node self.sceneManager # the scene manager self.rotate # rotation speed self.move # movement speed self.keepRendering # whether to continue rendering or not self.direction # the direction we are currently moving in
The rotate, move, sceneManager, and camNode are the same as the last tutorial (though we will be changing the value of rotate since we are using it differently). The keepRendering variable is returned from the frameStarted method. When we set keepRendering to be False the program will exit. The direction variable contains information on how we are going to translate camera node every frame.
We first need to set the TutorialFrameListener object to receive key and mouse events. We do that by redefining the SampleFramework.EventListener._setupInput function. In the SampleFramework (and in previous tutorials), this function creates the inputDevice variable which allows us to read which keys are up and down by calling inputDevice.isKeyDown. For this tutorial we don't want inputDevice, we want callbacks for key and mouse events.
To accomplish this, we will reimplement the _setupEvent method to create what is called an EventProcessor. The event processor reads in key and mouse events and dispatches them to all registered listeners. In the TutorialFrameListener class, define a new function called _setupInput. In it we will create an EventProcessor object, tell it what window it is acting on, and then tell it to start processing events (and dispatching them to registered listeners):
def _setupInput(self): self.eventProcessor = ogre.EventProcessor() self.eventProcessor.initialise(self.renderWindow) self.eventProcessor.startProcessingEvents()
Now that we have started the EventProcessor, we need to actually give it event handlers to send events to. Since we want all of these events to go to the TutorialFrameListener object, we will register them on self:
# register as a listener for events self.eventProcessor.addKeyListener(self) self.eventProcessor.addMouseListener(self) self.eventProcessor.addMouseMotionListener(self)
Before we go any further, we should bind the Escape key to exiting the program so we can run it. Find the TutorialFrameListener.keyPressed method. This method is called with a KeyEvent object every time a button on the keyboard goes down. We can obtain the key code (KC_*) of the key that was pressed from the "key" attribute on the object. We will build an if statement for all of the key bindings we use in the application based on this value. Create a new method called keyPressed, and add the following code to it:
def keyPressed(self, evt): if evt.key == ogre.KC_ESCAPE: self.keepRendering = False
Run the application before continuing, though it will only be a blank screen. Now, we need to add bindings for other keys in that switch statement. The next thing we are going to do is allow the user to switch between the viewpoints by pressing 1 and 2. The code for this (modified to be in the switch statement) is the same as it was in the previous tutorial, except we no longer have to deal with the mToggle variable:
elif evt.key == ogre.KC_1: self.camera.parentSceneNode.detachObject(self.camera) self.camNode = self.sceneManager.getSceneNode("CamNode1") self.sceneManager.getSceneNode("PitchNode1").attachObject(self.camera) elif evt.key == ogre.KC_2: self.camera.parentSceneNode.detachObject(self.camera) self.camNode = self.sceneManager.getSceneNode("CamNode2") self.sceneManager.getSceneNode("PitchNode2").attachObject(self.camera)
As you can see, this is much cleaner than dealing with a temporary variable to keep track of toggle times.
The next thing we are going to add is keyboard movement. Every time the user presses a key that is bound for movement, we will add or subtract mMove (depending on direction) from the correct direction in the vector:
elif evt.key in (ogre.KC_UP, ogre.KC_W): self.direction.z -= self.move elif evt.key in (ogre.KC_DOWN, ogre.KC_S): self.direction.z += self.move elif evt.key in (ogre.KC_LEFT, ogre.KC_A): self.direction.x -= self.move elif evt.key in (ogre.KC_RIGHT, ogre.KC_D): self.direction.x += self.move elif evt.key in (ogre.KC_PGDOWN, ogre.KC_E): self.direction.y += self.move elif evt.key in (ogre.KC_PGUP, ogre.KC_Q): self.direction.y -= self.move
Now we need to "undo" the change to the mDirection vector whenever the key is released to stop the movement. Create a function called keyReleased and add this code to it:
def keyReleased(self, evt): if evt.key in (ogre.KC_UP, ogre.KC_W): self.direction.z += self.move elif evt.key in (ogre.KC_DOWN, ogre.KC_S): self.direction.z -= self.move elif evt.key in (ogre.KC_LEFT, ogre.KC_A): self.direction.x += self.move elif evt.key in (ogre.KC_RIGHT, ogre.KC_D): self.direction.x -= self.move elif evt.key in (ogre.KC_PGDOWN, ogre.KC_E): self.direction.y -= self.move elif evt.key in (ogre.KC_PGUP, ogre.KC_Q): self.direction.y += self.move
Now that we have direction updated based on key input, we need to actually make the translation in frameStarted. This code is the exact same as the last tutorial. Add this to frameStarted::
self.camNode.translate(self.camNode.orientation * self.direction * evt.timeSinceLastFrame)
Compile and run the application. We now have key-based movement using buffered input!
Now that we have key bindings completed, we need to work on getting the mouse working. We'll start with toggling the light on and off based on a left mouse click. The mousePressed function is called with a MouseEvent object. This is where we run into one of the major inconsistencies with Ogre input. Before we used the values 0, 1, 2, etc for mouse buttons. Now we will use a mask to determine if a button is pressed:
def mousePressed(self, evt): if evt.buttonID & ogre.MouseEvent.BUTTON0_MASK:
This isn't terrible, but you always have to remember that when you are using MouseListener callbacks you have to use the button masks, yet when you are using all other mouse related input you need to use the mouse button numbers (0, 1, 2, 3) to check for mouse down. Finish off this if statement in mousePressed with the same code we used in the previous tutorial:
light = self.sceneManager.getLight("Light1") light.visible = not light.visible
Run the application. Viola! Now that this is working, the only thing we have left is to bind the right mouse button to a mouse look mode. Unfortunately this is not entirely possible with the framework we have created. The main problem is that even though the MouseMotion callbacks receive a MouseEvent object, as of Ogre 1.0.0 the getButtonID method always returns 0. For now, we can just add the code to mouseDragged and make it work for either mouse button:
def mouseDragged(self, evt): self.camNode.yaw(ogre.Degree(-evt.relX * self.rotate)) self.camNode.getChild(0).pitch(ogre.Degree(-evt.relY * self.rotate))
Compile and run the application. As you can see, dragging the mouse with either button held down makes the camera move. You can remedy this problem by placing code in mousePressed and mouseReleased to set boolean values to be True/False depending on whether the mouse buttons are up or down. I go through a full example of how to get around this Ogre bug in PyOgre Intermediate Tutorial 3.
Ogre's input system, while somewhat buggy in places, seems to work for most situations. With that said, you can also look into integrating other input systems instead of using Ogre's. Some windowing systems may be what you are looking for, such as wxPython, which people have successfully integrated with Ogre. You could also look into SDL, which provides not only cross platform windowing/input systems, but also joystick/gamepad input which Ogre is lacking. While I cannot give you guidance on setting up wx/gtk/qt/etc with Ogre (as I've never done it before), I have had a good amount of success getting SDL's joystick/gamepad input to work well with C++ Ogre. I'm sure this will also work with PyOgre.