Implementing an abstraction layer for supporting multiple input methods

In this recipe we are going to rework the code produced in Handling keyboard and mouse input. We are going to add an abstraction layer to the input handling sections of our code to hide away the specifics of the input device being used. This means that the gameplay code that controls animation and movement of the panda will not be handling any specific keys being pressed. Instead, there will be just a set of unified events for the actions the panda should be able to perform.

The reason for adding such a layer can already be found in the title of this recipe. We want to generalize our gameplay code to be able to support more input devices than just the classic keyboard and mouse combination. Further down in this chapter we will use this approach to add gamepad support to our demo, for example. We could also use the approach of this recipe as a starting point for implementing artificial intelligence for a game—why not let the AI controlled bots use a virtual gamepad, keyboard, or mouse to send the same commands as a human player would do?

Not only will this approach open new possibilities like the ones just stated, it will also make the character and gameplay handling code a bit shorter and easier to comprehend.

Getting ready

Although this recipe can be finished on its own, it is recommended to follow and understand the first recipe in this chapter before proceeding. This will help you to better understand the benefits of adding an abstraction layer to your input handling code. Also note that the following code will be discussed further based on the recipe Handling keyboard and mouse input dealing with keyboard and mouse input.

How to do it...

To finish this recipe, complete the tasks below:

  1. Add a new file called InputHandler.py and insert the following code:
    from direct.showbase.DirectObject import DirectObject
    from panda3d.core import *
    class InputHandler(DirectObject):
    def __init__(self):
    DirectObject.__init__(self)
    self.walk = False
    self.reverse = False
    self.left = False
    self.right = False
    taskMgr.add(self.updateInput, "update input")
    def beginWalk(self):
    messenger.send("walk-start")
    self.walk = True
    def endWalk(self):
    messenger.send("walk-stop")
    self.walk = False
    def beginReverse(self):
    messenger.send("reverse-start")
    self.reverse = True
    def endReverse(self):
    messenger.send("reverse-stop")
    self.reverse = False
    def beginTurnLeft(self):
    self.left = True
    def endTurnLeft(self):
    self.left = False
    def beginTurnRight(self):
    self.right = True
    def endTurnRight(self):
    self.right = False
    def dispatchMessages(self):
    if self.walk:
    messenger.send("walk", [-0.1])
    elif self.reverse:
    messenger.send("reverse", [0.1])
    if self.left:
    messenger.send("turn", [0.8])
    elif self.right:
    messenger.send("turn", [-0.8])
    def updateInput(self, task):
    return task.cont
    
  2. Add another new file and name it KeyboardMouseHandler.py. Open it and implement the following class:
    from InputHandler import InputHandler
    from panda3d.core import *
    class KeyboardMouseHandler(InputHandler):
    def __init__(self):
    InputHandler.__init__(self)
    base.disableMouse()
    props = WindowProperties()
    props.setCursorHidden(True)
    base.win.requestProperties(props)
    self.accept("escape", exit)
    self.accept("w", self.beginWalk)
    self.accept("w-up", self.endWalk)
    self.accept("s", self.beginReverse)
    self.accept("s-up", self.endReverse)
    self.accept("a", self.beginTurnLeft)
    self.accept("a-up", self.endTurnLeft)
    self.accept("d", self.beginTurnRight)
    self.accept("d-up", self.endTurnRight)
    taskMgr.add(self.updateInput, "update input")
    def resetMouse(self):
    cx = base.win.getProperties().getXSize() / 2
    cy = base.win.getProperties().getYSize() / 2
    base.win.movePointer(0, cx, cy)
    def updateInput(self, task):
    if base.mouseWatcherNode.hasMouse():
    messenger.send("turn", [-base.mouseWatcherNode.getMouseX() * 10])
    self.resetMouse()
    self.dispatchMessages()
    return task.cont
    
  3. Open Application.py and replace its contents with the following source code:
    from direct.showbase.ShowBase import ShowBase
    from direct.actor.Actor import Actor
    from panda3d.core import *
    from FollowCam import FollowCam
    from KeyboardMouseHandler import KeyboardMouseHandler
    class Application(ShowBase):
    def __init__(self):
    ShowBase.__init__(self)
    self.world = loader.loadModel("environment")
    self.world.reparentTo(render)
    self.world.setScale(0.5)
    self.world.setPos(-8, 80, 0)
    self.panda = Actor("panda", {"walk": "panda-walk"})
    self.panda.reparentTo(render)
    self.followCam = FollowCam(self.cam, self.panda)
    self.keyInput = KeyboardMouseHandler()
    self.accept("walk-start", self.beginWalk)
    self.accept("walk-stop", self.endWalk)
    self.accept("reverse-start", self.beginReverse)
    self.accept("reverse-stop", self.endReverse)
    self.accept("walk", self.walk)
    self.accept("reverse", self.reverse)
    self.accept("turn", self.turn)
    def beginWalk(self):
    self.panda.setPlayRate(1.0, "walk")
    self.panda.loop("walk")
    def endWalk(self):
    self.panda.stop()
    def beginReverse(self):
    self.panda.setPlayRate(-1.0, "walk")
    self.panda.loop("walk")
    def endReverse(self):
    self.panda.stop()
    def walk(self, rate):
    self.panda.setY(self.panda, rate)
    def reverse(self, rate):
    self.panda.setY(self.panda, rate)
    
  4. Launch the program. If nothing about the input response has changed, you did just fine!

How it works...

In this recipe we moved the event handling methods, the movement flags, and the updating task to the InputHandler class. In this class, we implement the general parts of the input system. It acts as an abstraction layer between the game code and the input handling code, translating input device events to generalized, device-agnostic events.

The KeyboardMouseHandler class shows us one of the benefits of this architecture, as it only contains device specific code. Instead of having to deal with one big piece of code as before, we have now moved this part of the implementation in a separate, concise, and easy to understand class. This way writing implementations for new devices only requires wiring events to the appropriate handling functions and providing an implementation of updateInput() that at least calls dispatchMessages().

With this change, we now are able to add support for new input devices without having to touch gameplay code. In fact, the gameplay logic has become completely independent from how input is generated. Here we added a handler for keyboard and mouse, but we might as well add support for joysticks and gamepads, as can be seen in the recipe Handling input from an Xbox 360 controller found in this chapter.

Finally, we can take a look at our cleaned up implementation of the Application class. First we need to create an instance of our KeyboardMouseHandler class (or any other device-specific derived implementation of the InputHandler class). We then just need to register and implement a few event handlers for the device-independent messages and that's it!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset