Areas
  • J3D FAQ
  • Tutorials
  • Utilities
  • Books
  • News
  • Other Sites
  •  Chapters
  • TOC
  • Getting Started
  • Lighting
  • Textures
  • Behaviours
  • User Feedback
  • Navigation
  • Audio
  • Input Devices
  • No Monitors
  •  Sections
  • Chapter TOC
  • Picking
  • Collisions
  • Summary
  •   

    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(s) that needs to be monitored. These IDs are sourced from the java.awt.event.MouseEvent class (or any other java.awt.event class for user input, such as keyboards or window resizing). Registering criteria really should be done in the initalize() 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.

    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. Using this as a guide, we make the constructor take a list of the event IDs that need to be registered and make it protected as well (prevents anyone directly instantiating this class).

    Registering our criteria in the initialize method becomes a simple case of looping through each of the given event masks and adding that to the list. Because we are interested in any of the event types, we combine them together with the boolean OR.

    int i;
    int size = event_masks.length;
    WakeupCriterion[] events = new WakeupCriterion[size];
    
    for(i = 0; i < size; i++)
      events[i] = new WakeupOnAWTEvent(event_masks[i]);
    
    WakeupCondition critter = new WakeupOr(events);
    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. Since we are interested in maintaining minimal information in the base class, we just pass the raw event onto this somewhere thing.

    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
          {
            processAWTEvent(events[i]);
          }
          catch(Exception e)
          {
            System.out.println("Error processing AWT event input");
            e.printStackTrace();
          }
        }
      }
    }
    

    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
    {
       private static final int[] AWT_EVENTS =
       {
         MouseEvent.MOUSE_DRAGGED
       };
    
       public MouseDragBehaviour()
       {
         super(AWT_EVENTS);
       }
    
       protected void processAWTEvent(AWTEvent evt)
       {
         System.out.println("got mouse 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
    {
      ...
      private WakeupCondition critters = null;
      ...
    
      public void initialize()
      {
        int i;
        int size = event_masks.length;
        WakeupCriterion[] events = new WakeupCriterion[size];
    
        for(i = 0; i < size; i++)
          events[i] = new WakeupOnAWTEvent(event_masks[i]);
    
        WakeupCondition critter = new WakeupOr(events);
        resetEvents();
      }
    
      protected final void resetEvents()
      {
        wakeupOn(critters);
      }
      ...
    }
    
    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!

      

    [ TOC ] [ Home ] [ FAQ ] [ Books ] [ Tutorials ] [ Utilities ] [ Contact Us ]