Version 6 (modified by robert, 7 years ago)

Converted to osgViewer

Handling Keyboard Input to Update a Callback

(Wiki editing note: Add link to complete source code at bottom)

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


       // construct the viewer
       osgViewer::Viewer viewer;

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

       // Set up and enter a simulation loop.
       viewer.setSceneData( root );

       return viewer.run();

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.