Contents

Chapters

Sections

j3d.org

[ Previous ] [ Up ] [ Next ]

Mouse Handling

© Justin Couch 2000

The first thing that you must do when dealing with the mouse, is to build some way of getting that input into your application. With the standard 2D AWT that is pretty easy - for any component, just add a MouseListener or MouseMotionListener and deal with the input. Since Canvas3D is just a component one could assume that we could do the same thing for the 3D world. Unfortunately (as discovered through trial and error by the author) this is not the best way to go about it and is the reason for the existence of the WakeupOnAWTEvent criteria.

When creating a handler for mouse input, there are two things that typically a mouse is used for: moving the user or object and picking an object. While related, there are certain extra aspects to picking that do not need to be considered with standard mouse input. Picking is covered in detail in the next section so for the moment we will concentrate on dealing with the low-level mouse handling.

 

General Mouse Behaviour

Writing a generalised mouse behaviour is a task requiring some delicate balance. It will also probably be your first experience in building a custom behaviour. The general behaviour should be to take the raw mouse input and use it to manipulate something. That something should be arbitrary because we could use it to move the view around the scene or for sliding an object around in the scene. Ideally what we create here should also be general purpose enough to form the basis for the mouse picking behaviour too.

To start with, the first thing that we need to do is create a Behavior implementation so that we may interface the mouse input with the scene graph. We’ll call this class MouseBehaviour and start with the basic outline of:

public class MouseBehaviour
{
  public MouseBehaviour()
  {
  }

  public void initialize()
  {
  }

  public void processStimulus(Enumeration criteria)
  {
  }
}
In order to create a behaviour we need to register some criteria. For mouse behaviour, the only way to do this is to use the WakeupOnAWTEvent criteria. The constructor takes the ID of an AWT event mask for the types of events that need to be monitored. These IDs are sourced from the java.awt.AWTEvent class - they are all the constants that end with EVENT_MASK. Registering criteria can only be done in the initalize() or processStimulus() method, but the only place that we can notify the class of the events we are interested in is through the constructor or a separate set method. For the purposes of this discussion, let's add a class variable that gets set through the constructor:
public class MouseBehaviour
{
  private long eventMask;

  public MouseBehaviour(long mask)
  {
    eventMask = mask;
  }

     Note
    There are only two types of event masks that can be registered for mouse events - MOUSE_EVENT_MASK and MOUSE_MOTION_EVENT_MASK. These correspond to the two types of listeners you could normally attach to AWT/Swing components. In some projects, I prefer to not let the user provide the mask in the constructor, but only allow them to use two boolean flags to control what events need to be registered.

Deciding on how to structure the behaviour classes depends on how you want to use them. In this case, we want to create a base class and then derive it for individual application specific behaviours. To do this means that we need to pass the types of events that we want through into the constructor. For example, a pick based behaviour really only wants to be woken when a mouse is pressed or released on the object, but for navigation we may only want to know about drag events. There are a few ways of implementing this system, but my prefered approach is to have the base mouse class provide all the event distribution services and then let the derived classes override the methods that they are interested in. To process and send mouse events, then becomes the job of the processStimulus() method, which will be covered shortly. Right now, you need to know how to register the event. Registering for criteria, as you have previously seen, can only be done in initalize() or processStimulus(). To get your behaviour running the first time, you will need to register the initial condition in initialize()

  public void initialize()
  {
    WakeupOnAWTEvent critter = new WakeupOnAWTEvent(eventMask);

    wakeupOn(critter);
  }

After we have registered the conditions we also need to deal with the resulting stimulations by providing an implementation of the processStimulus() method. The parameter contains a list of the criteria that caused the behaviour to be executed. Although we should only ever get mouse events, we still take the precaution of checking all the values in the provided list. From this, when we come across an AWT condition we fetch the events and then need to do something with them. At this point we need to decide what to do with the mouse event. We could either process them and pass the result to somewhere, or just pass the raw event on. For this example, we are interested in maintaining the minimal code in the base class, so we just pass the raw event on to somewhere.

Somewhere is defined to be the derived class, as it should know what to do with the event. The easiest way to do this is provide an abstract method definition and call that with the AWT event to be processed. The resulting code looks like this:

protected abstract void processAWTEvent(AWTEvent evt);

public void processStimulus(Enumeration criteria)
{
  WakeupCondition cond;
  AWTEvent[] events;

  while(criteria.hasMoreElements())
  {
    cond = (WakeupCondition)criteria.nextElement();
    if(!(cond instanceof WakeupOnAWTEvent))
      continue;

    events = ((WakeupOnAWTEvent)cond).getAWTEvent();

    for(int i = 0; i < events.length; i++)
    {
      try
      {
        if(events[i] instanceof MouseEvent)
            processAWTEvent(events[i]);
      }
      catch(Exception e)
      {
        System.out.println("Error processing AWT event input");
        e.printStackTrace();
      }
    }
  }
}
One thing to note in this behaviour, is that there is an array of events that we must deal with. The way the AWT behaviour works in Java3D is to store up all the events that occur between calls to your behaviour and pass them in as a large pile. If you don't care about all of the values, just use the last item in the array as that is the most recent event that was generated.

Drag Mouse Behaviour

Once we have the base class established we need to look at what sort of behaviour we want to create. A typical user interface action is to drag the mouse across the window to do something - usually navigate around (Doom style game navigation) or rotate an object (usually called examine mode in most CAD programs).

As usual with OO designs, there are all sorts of ways that we can skin the cat. In the end we elected to follow a set that traces the basic mouse event types. The drag class starts by deriving from the basic mouse behaviour and in the constructor all we pass is the mouse drag event (MouseEvent.MOUSE_DRAGGED). Following this, we just provide an implementation of the processAWTEvent method. For illustration, this will just issue a println telling you of the fact that an event was received.

public class MouseDragBehaviour extends MouseBehaviour
{
   public MouseDragBehaviour()
   {
     super(AWTEvent.MOUSE_MOTION_EVENT_MASK);
   }

   protected void processAWTEvent(AWTEvent evt)
   {
     System.out.println("got mouse drag event");
   }
}
To add a mouse behaviour to the system, the process is no different from any other. Pick a group that you want the behaviour to apply to and then add it as a child. For mouse behaviours, where you add it really doesn’t matter. We could add it as another child of the TransformGroup that we added our rotator behaviour in the previous chapter or just as another world object. It will always be called. Even when applying it to a particular transform, the placement does not really matter. The reason for this is that the behaviour is triggered by an external resource, not by something in the scene graph. Since the behaviour is used to effect the entire world we will place it as a root object in the world branch group.
Point3d origin = new Point3d(0, 0, 0);

Behavior bh = new MouseDragBehaviour();
BoundingSphere bounds =
        new BoundingSphere(origin, Double.POSITIVE_INFINITY);
bh.setSchedulingBounds(bounds);

universe.addWorldObject(bh);
Note here that we have had to add scheduling bounds like we have for every other behaviour. In this case, because we want the mouse to always be useable, the bounds have been set to infinity. This ensures that the mouse is always useable. Having the mouse always useable may not always be the case though. For example, when you wish to pick an object, you may want the mouse to be effective only when you are within a certain distance of the object, which prevents unwanted objects being picked that may be a long distance from the camera. Remember that a good choice of scheduling bounds can be just as important to the useability of your application as any other technique - both in terms of efficiency and user friendliness.

Now if you added these changes and then ran the program you might have noticed some odd behaviour. The first time that you drag, you get a single printout of the message. Next time you drag, nothing happens. Behaviours are registered to act at the point that they are called. Effectively they are a one-shot application. If you want the behaviour to continually evaluate, you need to keep registering it for the next invocation of that criterion. To enable this to happen, a new method is added to the base class that re-registers the criteria that we created for the first run of the class. Then, every time that the processAwtEvent() method is called in the derived class, we call this method in the base class. Our modified base class now looks like this:

public abstract class MouseBehaviour extends Behavior
{
  ...

  public void initialize()
  {
    WakeupCondition critter = new WakeupOr(eventMask);
    resetEvents();
  }

  protected final void resetEvents()
  {
    wakeupOn(critters);
  }
  ...
}

     Note
    The AWT criterion seems to be quite different to the others. The undocumented behaviour is that it will continue to send you events without needing to re-register it every frame like you need to do with other wakeup conditions. This is undocumented, and based on the word of the Java3D developers at Sun, so take it with a grain of salt as it may not work in future versions. It has changed between previous versions too.

To complement this, the mouse drag behaviour modifies its processAwtEvent() method to read:

protected void processAWTEvent(AWTEvent evt)
{
   System.out.println("got mouse event");
   resetEvents();
}
With these changes, your behaviour now will generate an huge stream of printlns every time you drag the mouse around the window.

Other Mouse Behaviours

Once you have established one sort of mouse behaviour, the rest follow pretty quickly. Just by changing the list of AWT events that you are interested in, you may create any form of behaviour. Actually, because the base class only cares about AWT events, you could easily convert the class into a general-purpose handler for all window events. For example, you might want one that listens for focus appearing on the canvas.

For each of these custom behaviours you need to provide a class with the method implementations like we outlined above. Then from there you would create another set of derived classes to do particular things. Say you wanted a mouse behaviour that, when dragging, rotated an object about. For this you would need the new derived class to take a reference to a TransformGroup (just like the RotationInterpolator example of the previous chapter). From there, the sky’s the limit!