Version 1 (modified by osg, 7 years ago)

Initial copy of NPS tutorial

Handling Keyboard Input

Goals

Add the ability to associate keyboard events with specific functions. From a previous tutorial we added the ability to update a tank's turret rotation using an update callback. This tutorial will develop a keyboard interface class that allows us to update the turret's rotation based on user keyboard input.

GUI Event Handlers: GUI Event Adapters and GUI Action Adapters:

Note: This description is derived in large part from the online documentation for osg available here.

The GUIEventHandler class provideds developers with an interface to the windowing sytem's GUI events. The event handler recieves updates in the form of GUIEventAdapter instances. The event handler can also send requests for the GUI system to perform some operation using GUIActionAdapter instances.

Information about GUIEventAdapters instances include the type of event (PUSH, RELEASE, DOUBLECLICK, DRAG, MOVE, KEYDOWN, KEYUP, FRAME, RESIZE, SCROLLUP, SCROLLDOWN, SCROLLLEFT). Depending on the type of GUIEventAdapter, the instance may have additional information associated with it. Mouse events will have x and y positions associated with them. KEYUP and KEYDOWN events will have a key name (ex: 'a', 'F1') associated with them.

The GUIEventHandler uses GUIActionAdapters to request actions of the GUI system. These action include request to redraw - requestRedraw(), request to continually redraw - requestContinuousRedraw(), and request to reposition the cursor - requestWarpPointer(x,y).

The GUIEventHandler class interacts with the GUI primarily with the 'handle' method. The handle method has two arguments: an instance of GUIEventAdapter for receiving updates from the GUI, and a GUIActionAdapter for requesting actions of the GUI. The handle method can examine the type and values associated with the GUIEventAdapter, perform required operations, and make a request of the GUI system using the GUIActionAdapter. The handle method returns boolean variable set to true if the event has been 'handled', false otherwise.
Since there may be more than one GUIEventAdapter associated with a GUI, the return value of this method (and the order of GUIEventAdapters on a veiwer's eventHandlerList) can be used to control handling of single keyboard events multiple times. If a GUIEventHandler returns false, the next GUIEventHandler will also respond to the same keyboard event.

The following example demonstrates how a GUIEventHandler interacts with a GUI system: The TrackballManipulator class (derived from GUIEventHandler) receives updates of mouse events in the form of GUIEventAdapter instances. One of these mouse events is interpreted by the TrackballManipulator class as a 'throw' request. (The user want to throw the model so it will continually spin or move.) On interpreting this event, the TrackBallManipulator sends a request to the GUI system (using a GUIActionAdapter) to start a timer and request to be repeatedly called so it can calculate new model orientation or position data.

Sample keyboard interface class

The following briefly describes a keyboard class that allows users to associate keyboard input with specific functions. When users register keys and associated C++ functions to invoke, appropriate entries are made in a table. This table contains key values ('a','F1',etc.), key state (up or down) and the C++ function to invoke. This essentially allows users to create interaction in terms of "on sensing 'f' key down, perform action 'functionOne'." Because the keyBoardInterface class is derived from the GUIEventHandler class, each time the GUI system senses a GUI event, this classes, 'handle' method will be invoked. When the handle method is invoked, the GUI event's key value and state ('a' key 'UP') is compared to the table entries. If a match is found, the function associated with the key value and state is invoked.
The user registers keys using the addFunction method. There are two versions of this function. The first takes a key and function as arguments. This method is designed for situations where the user is only concerned with the KEY_DOWN events. For example if the user wants to associate the 'a' key down events with a method to toggle the anti-aliasing. The user would not be concerned with an action to take on key up.
In other cases, the user would want distinct actions to be associated with a single key's 'down' and 'up' events. Examples would include controling a first person shooter's motion. On sensing the 'w' key down, the model should acelerate forward. When the 'w' key is released, the motion model should coast to a stop. From a design approach, separate function calls for key up and key down were viewed as preferable. The alternative was to continually trigger the key-down method.

    #ifndef KEYBOARD_HANDLER_H
    #define KEYBOARD_HANDLER_H
    #include <osgGA/GUIEventHandler>

    class keyboardEventHandler : public osgGA::GUIEventHandler
    {
    public:

       typedef void (*functionType) ();
       enum keyStatusType
       {
          KEY_UP, KEY_DOWN 
       };

    // A struct for storing current status of each key and 
    // function to execute. Keep track of key's state to avoid
    // redundant calls. (If the key is already down, don't call the
    // key down method again.)
       struct functionStatusType
       {
          functionStatusType() {keyState = KEY_UP; keyFunction = NULL;}
          functionType keyFunction;
          keyStatusType keyState;
       };

    // Storage for list of registered key, function to execute and 
    // current state of key.
       typedef std::map<int, functionStatusType > keyFunctionMap;

    // Function to associate a key with a function. If the key has not
    // been previously registered, key and function are added to the
    // map of 'key down' events and 'true' is returned. Otherwise, no
    // entry made and false is returned.
       bool addFunction(int whatKey, functionType newFunction);

    // Overloded version allows users to specify if the function should 
    // be associated with KEY_UP or KEY_DOWN event.
       bool addFunction(int whatKey, keyStatusType keyPressStatus, 
          functionType newFunction);

    // The handle method checks the current key down event against 
    // list of registered key/key status entries. If a match is found 
    // and it's a new event (key was not already down) corresponding 
    // function is invoked.
       virtual bool handle(const osgGA::GUIEventAdapter& ea,
          osgGA::GUIActionAdapter&);

    // Overloaded accept method for dealing with event handler visitors
       virtual void accept(osgGA::GUIEventHandlerVisitor& v)
          { v.visit(*this); };

    protected:

    // Storage for registered 'key down' methods and key status
       keyFunctionMap keyFuncMap;

    // Storage for registered 'key up' methods and key status
       keyFunctionMap keyUPFuncMap;
    };
    #endif

Using the keyboard interface class:

    The following provides an example of how to use this class:

    // Create scene and viewer
    // ...

    // Declare and define some functions: 
    // startAction(), stopAction(), toggleSomething()
    // ... 

    // Declare and initialize a instance of the keyboard event 
    // handler.
       keyboardEventHandler* keh = new keyboardEventHandler();

    // Add this event handler to the viewer's event handler list.
    // If we use push_front and our handle method returns 'true' other
    // event handlers will not have the opportunity to resond to this
    // GUI event. We can use push_back if we want to give other handlers
    // first shot at the events, or have the handle method return false.
       viewer.getEventHandlerList().push_front(keh); 

    // Register some keys, associate them with functions.
    // Each time 'a' key is pressed, toggelSomething() is invoked
    // (releasing the 'a' key has no effect.)

       keh->addFunction('a',toggleSomething);

    // Each time the 'j' key is pressed, the startAction method is
    // invoked (for example: acclerate motion model). 
    // Note the second argument is not required.
       keh->addFunction('j',keyboardEventHandler::KEY_DOWN,startAction);

    // Each time the 'j' key is released, the stopAction method is
    // invoked
       keh->addFunction('j',keyboardEventHandler::KEY_UP,stopAction); 

    // Enter a simulation loop
    // ...

Handling Keyboard Input to Update a Callback

Goals

Tutorial 9 explains a class that lets us register functions to be used by an event handler. This tutorial provides a more basic approach to keyboard input. Rather than create and register functions, we'll override a GUIEventHandler. In this class we'll add code to perform specific actions in response to keyboard and mouse events. We'll also set up a way for the keyboard event handler to communicate with an update callback.

Sample problem description

Tutorial 8 demonstrates how to continually update the position of a DOF node in a scene graph by associating a callback with the DOF node. What if we want to control a node in the scene graph based on user keyboard input? For example if we have a tank model under a position attitude transform and want to move the tank forward when the user presses the 'w' key we'll need a few things:

  1. Read keyboard events
  2. Store the results of keyboard events
  3. Respond to keyboard events from within an update callback.

Solution

Step One: The base class osgGA::GUIEventHandler is designed to provide an opportunity to define custom actions for GUI keyboard and mouse events. We create custom actions by deriving from the base class and overriding the 'handle' method. We also need to provide and 'accept' method to enable GUIEventHandlerVisitors. The basic framework looks like this:

    class myKeyboardEventHandler : public osgGA::GUIEventHandler
    {
    public:
       virtual bool handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter&);
       virtual void accept(osgGA::GUIEventHandlerVisitor& v)   { v.visit(*this); };
    };

    bool myKeyboardEventHandler::handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter& aa)
     {
       switch(ea.getEventType())
       {
       case(osgGA::GUIEventAdapter::KEYDOWN):
          {
             switch(ea.getKey())
             {
             case 'w':
                std::cout << " w key pressed" << std::endl;
                return false;
                break;
             default:
                return false;
             } 
          }
       default:
          return false;
       }
    }

The bulk of this class is our 'handle' method that we override from the base class version. The method takes two arguments. An instance of the EventAdapter class allows us to receive GUI events. And an instance of the ActionAdapter class that lets us make requests of the GUI system such as request redraw or request continual redraw.[[BR]]
To handle additional events, we would extend the first switch statement to include other events such as KEYUP, DOUBLECLICK, DRAG. To handle additional key down events we would extend the switch statement within the KEYDOWN case.[[BR]]
The return type of the event controls whether event handlers after our event handler in the event handler list get a shot at handling keyboard mouse events. If we return 'true' the event is considered handled and will not be passed on to other handlers. If we return 'false' other handlers will have the opportunity to respond to that event.[[BR]]
To 'install' our event handler we need to create an instance of it and add it to the osgProducer::Viewer's EventHandler list. Code as follows:

{{{
       myKeyboardEventHandler* myFirstEventHandler = new myKeyboardEventHandler();

       viewer.getEventHandlerList().push_front(myFirstEventHandler); 
}}}

Step Two. So far our keyboard handler is not very interesting. All it does is output to the console window each time we hit the 'w' key. If we want to use key press information to control elements within the scene graph we need a communication mechanism between the keyboard handler and the update callback.[[BR]]
To make this happen we'll create a class to store keyboard state information. The event handler class will be responsible for keeping this state current based on most-recent keyboard mouse events. The update callback class will also need access to the keyboard state class so it can update the scene graph correctly. We'll set up the basic framework here. Extending this to something more useful will be left up to the user. Here's a class definition to enable communication between the keyboard event handler and the update callback:

{{{
    class tankInputDeviceStateType
    {
    public:
       tankInputDeviceStateType::tankInputDeviceStateType() : 
          moveFwdRequest(false) {}
       bool moveFwdRequest;
    };
}}}

The next trick is to set things up so the keyboard event handler and the update callback both have access to the right data. This data will be encapsulated in an instance of tankInputdeviceStateType. Since our event handler is unique to driving the tank, it seems reasonable to require the event handler to have a pointer to a valid instance of our tankInputDeviceStateType. So we'll add a data member (pointer to an instance of tankInputDeviceStateType) to our event handler. Since we don't want to have event handlers that don't have a pointer we'll make this a required argument for the constructor. The first changes to our class - adding a pointer to a tankIDevState instance and the new constructor are as follows:

{{{
    class myKeyboardEventHandler : public osgGA::GUIEventHandler {
    public:
       myKeyboardEventHandler(tankInputDeviceStateType* tids)
       {
          tankInputDeviceState = tids;
       }
    // ...
    protected:
       tankInputDeviceStateType* tankInputDeviceState;
    };
}}}

We'll also need to modify the 'handle' method so it does something more interesting than output to the console. We'll change the method so it updates the tank IDev state to indicate a request to move the tank forward.

{{{
    bool myKeyboardEventHandler::handle(const osgGA::GUIEventAdapter& ea,osgGA::GUIActionAdapter& aa)
    {
       switch(ea.getEventType())
       {
       case(osgGA::GUIEventAdapter::KEYDOWN):
          {
             switch(ea.getKey())
             {
             case 'w':
                tankInputDeviceState->moveFwdRequest = true;
                return false;
                break;
             default:
                return false;
             } 
          }
       default:
          return false;
       }
    }
}}}

Step Three: The update position callback will also need access to this keyboard state data. We'll impose the same requirements on the update callback. It will have a data member that points to the same instance of the tank iDev state. And it's constructor will require that users pass this pointer to the classes constructor. Once we have this pointer, we can use it within our callback. The callback will only move the tank forward it our keyboard state indicates a user request to move the tank forward. The callback looks like this:

{{{
    class updateTankPosCallback : public osg::NodeCallback {
    public:
       updateTankPosCallback::updateTankPosCallback(tankInputDeviceStateType* tankIDevState)
          : rotation(0.0) , tankPos(-15.,0.,0.)
       {
          tankInputDeviceState = tankIDevState;
       }
       virtual void operator()(osg::Node* node, osg::NodeVisitor* nv)
       {
          osg::PositionAttitudeTransform* pat =
             dynamic_cast<osg::PositionAttitudeTransform*> (node);
          if(pat)
          {
             if (tankInputDeviceState->moveFwdRequest)
             {
                tankPos.set(tankPos.x()+.01,0,0);
                pat->setPosition(tankPos);
             }
          }
          traverse(node, nv); 
       }
    protected:
       osg::Vec3d tankPos;
       tankInputDeviceStateType* tankInputDeviceState; };
}}}

Now the framework for communicating between keyboard and update callback is complete. Next we need to create an instance of our tankInputDeviceStateType. This instance will be an argument for the constructor of our event handler. It will also be an argument to the constructor of our tank position update callback. Once we add our event handler to the viewer's event handler list we can set up and enter the simulation loop.

{{{
       // Declare instance of class to record state of keyboard
       tankInputDeviceStateType* tIDevState = new tankInputDeviceStateType;

       // Set up the tank update callback
       //  pass the constructor a pointer to our tank input device state
       //  that we declared above.
       tankPAT->setUpdateCallback(new updateTankPosCallback(tIDevState));

       // The constructor for our event handler also gets a pointer to
       //   our tank input device state instance
       myKeyboardEventHandler* tankEventHandler = new myKeyboardEventHandler(tIDevState);

       // Add our event handler to the list
       viewer.getEventHandlerList().push_front(tankEventHandler); 

       // Set up and enter a simulation loop.
       viewer.setUpViewer(osgProducer::Viewer::STANDARD_SETTINGS);
       viewer.setSceneData( root );
       viewer.realize();

       while( !viewer.done() )
       {
          viewer.sync();
          viewer.update();
          viewer.frame();
       } 
}}}

That's it! The interesting parts are left to the user: extended this framework so the tank can stop moving forward on KEYUP, turn, accelerate, etc.