PyOgre Beginner Tutorial 5        

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.

Prerequisites

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.

Introduction

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.

Getting Started

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.

Buffered Input in Ogre

Introduction

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.

Shortcomings

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.

Combining FrameListeners with Key/Mouse Listeners

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.

Buffered Input Interfaces

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.

KeyListener

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).

MouseListener

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.

MouseMotionListener

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).

Multiple Inheritance with C++ Callbacks

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.

TutorialFrameListener

Variables

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.

The EventProcessor

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)

Key Bindings



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!

Mouse Bindings

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.

Other Input Systems

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.