Behaviour Fundamentals
© Justin Couch 2000
Unlike many other 3D graphics APIs and even most 2D APIs, behaviours form a
fundamental part of the Java3D system. As a rough estimate, 80% of all
non-static scene graph interactions at the code level will have originated
with a behaviour. Typically a behaviour is what triggers your code to start
running, which in turn makes modifications to the scene graph. A cascade
effect may then start where one behaviour triggers a series of follow-on
actions. For example, the behaviour of pressing the up arrow key would move
the user forward , this then makes an object become visible causing some
animation to start. At the same time, this action could also cause the user
to collide with another object forcing the user to move in reaction; and so
the chain reaction goes on.
Scene Graph Relationships
Behaviours form part of the standard scene graph, just like pieces of
geometry do. In the same way that a piece of geometry not existing in the
renderable scene graph is not drawn, a behaviour not existing in the
renderable scene graph will not be executed.
The Behavior class extends the Leaf class
suggesting that it too forms part of a normal scene graph. As you will see
shortly, the execution of the behaviour depends on that instance being
placed along side the nodes.
Being a leaf node in the scene graph structure allows you to place behaviours
almost anywhere that you can place geometry. At the same time this implies
that as you move an object around, its behaviour moves with it. In a sort
of OO way, the behaviours and geometry move together. As you remove a part
of the scene graph and place it under a different parent, any associated
behaviours also move with it.
Although the behaviour is added as part of the scene graph, it does not form
a part of it. Figure 1 illustrates the common representation used of
behaviours. As you can see, the behaviour is connected to the scene graph
but is kept in another area of the Java3D runtime environment. There is a
whole portion devoted purely to behaviour execution: deciding when to run
each behaviour, storage and other management issues.

Figure 1: The structure relationship between geometry and behaviours
in a scene graph
Note
|
The objects in this picture seem to be the common expressions that have
evolved around J3D tutorials that have been presented over the past few
years. I don’t know who originated them, but this is the general style in
use rather than the more formal and trendy UML syntax.
|
Behavior Base Class
All behaviours within Java3D must extend the Behavior base
class. This class is used as a signature by the runtime environment to make
sure that it gets treated differently. To create a behaviour you must extend
this base class and then add it to the scene graph at the appropriate place
that you want it to effect. For example, if you want to have a rotating cube
then you place the rotation behaviour under the same transform as the cube.
With the collection of pre-built behaviours
available there is little more that you need to do. The environment calls
methods in the behaviour to make sure the appropriate actions take place.
So what methods are called?
Most of the methods provided by the base class provide full implementations
so you rarely need to add any more. When a behaviour is first added to the
scene graph it needs to be put into a known condition. Java3D calls the
initialize() method to make sure that your behaviour is in that
known state. Then, each time something happens that causes the behaviour to
be executed, the processStimulus() method is called. A stimulus
is the Java3D term for the notification that is sent to behaviour to tell it
that it is time to do something.
The initialize() method is typically used by the behaviour to
register the types of criteria that the behaviour should be executed on. For
example, you might only want to execute when the object becomes visible or
it may be some complex combination of criteria. Another use for the method
is to set the related object into a known condition. Our earlier rotation
behaviour might set the cube to the starting position so that you don’t
get any strange jumping effects when the real rotation starts. Alternatively
the behaviour might read the cube’s starting rotation and progresses from there.
processStimulus() is called every time that the runtime decides
that your behaviour should be run. Apart from the conditions that you set
during the initialize method, you have no control over when this method is
called. However, once it has been called, you need to take the appropriate
action. For the rotating cube it should change the orientation of the cube
to the next value. Because this method is called between renders of
individual frames, you need to be careful with the amount of work that is
done here. The more work, the longer it takes to get back to rendering and
results in lowering the frame rate.
Most of the important methods are covered in more detail in the next couple
of sections, but the following are also useful:
setEnable() Used to enable and disable the behaviour
execution on demand. You may have needs to disable the behaviour because it
is inappropriate to perform it at the current time. This could be used to
switch in and out various behaviours without needing to make changes to the
scene graph during runtime.
getView() Used to fetch the current view object for
calculation of various items. You might want to know where the viewer
currently is relative to your object that the behaviour is effecting.
Criteria for Running
Definition of when a behaviour is to execute is fulfilled through the use
of criteria classes. These classes define how to control when a behaviour
is to be run. There are two parts to deciding how to run a behaviour:
- The statement of what sort of condition that a behaviour should be run.
This might be because another object has collided with this object or that
the user has clicked on the screen. These classes are derived from
WakeupCriterion and define a single type of condition.
- The combination of individual conditions that will result in the
behaviour executing. These classes based on
WakeupCondition
provide boolean logic combinations of individual criteria. For example
you can make sure that a behaviour is only executed after every third frame
has been rendered or after an elapsed time of 300 milliseconds.
Once you have decided on the combination of these two sets of classes you
then need to set it all up. The example in the second point above would be
coded as:
public void initialize() {
WakeupCriterion[] criteria = new WakeupCriterion[2];
criteria[0] = new WakeupOnElapsedFrames(3);
criteria[1] = new WakeupOnElapsedTime(300);
WakeupCondition condition = new WakeupOr(criteria);
wakeupOn(condition);
}
The last line is a call to the protected method wakeupOn() .
This method is used to inform the runtime system that a new set of
conditions are now to be used. The default behaviour is to not run the
behaviour at all. If you call the method anywhere else except in
initialize or processStimulus() an
IllegalStateException is generated.
There is quite a list of both criteria for operation and combining them
together. Although you usually won’t need much more than a simple AND or OR,
a good grasp of boolean logic (and De Morgan's Theorem) will help you
optimise the combinations to provide minimal overheads. When the
processStimulus() method is called, you are given an
Enumeration of the conditions that were fulfilled. This
returns only the criteria, not the combination of conditions required. In
the earlier example we could use this to determine how much time has elapsed,
and therefore how much to adjust our output by. If we got frames elapsed
conditions then it means the system is lightly loaded, but if the elapsed
time criteria has passed, maybe we should look at lightening the calculation
load a bit to keep the frame rates up.
Bounding & Scheduling
So far the assumption has been that behaviours are always executed assuming
that the appropriate criteria are met. In reality, this is far from the case.
Because a really complex world may have hundreds of potential behaviours,
trying to decide on every frame whether any given behaviour should be run
can be very taxing on the system. To relieve this problem Java3D uses two
mechanisms: bounds on the behaviour and selective scheduling.
Scheduling is actually dependent on the bounds setup so we shall look at
that first. Bounds are used to define when an object should be added to the
list of scheduled objects. That is, bounds are used to control the distance
from the target object a behaviour is to be executed. As you saw in the
previous chapter with the headlight example, we can create any arbitrary
shape and bounding region. A simple spherical radius may be all that is
wanted, while others, such as a collidable object may want a bounding box.
Once you have created the desired region you may then inform the system using
the setSchedulingBounds() method. The bounds are centered
relative to the parent group that you have placed it in. For our rotating
cube example, that means the parent transform that is used to rotate
the cube forms the local origin of the bounds.
Setting a scheduling bounds on the behaviour is only half the solution.
How do you know when that bounds has been entered? The answer is that it
lies in the combination of two sets of bounds: the bounds of the behaviour
and the bounds of the current View . The view part is important
because it too has a set of bounds that decides what can be seen close to
the user and a maximum distance. The Java3D specification defines that
processStimulus() method is called when the two bounds intersect.
Figures 2 to 4 illustrate the three conditions that could occur. The camera
on the left of the picture is moving right towards the object (plus behaviour)
on the right. For illustrative purposes, the object is a clown on a
unicycle going around in circles.

Figure 2: The object scheduling bounds completely outside the view bounds.
At the start, the view is completely separated from the bounds of the behaviour. (Figure 11-2)
Because the two bounds don’t intersect, the behaviour is not permitted to operate regardless of the
criteria that has been set. At no time will we get to move the clown about the world.

Figure 3: The object’s scheduling bounds and view bound intersect.
As the view moves to the right, the bounds start to intersect (Figure 3).
Assuming that the appropriate criteria are satisfied, the behaviour is run.
Why might this be the case? Well the object, as a result of the behaviour may
start to travel towards the camera. Because the clown’s starting position
(relative to the camera) is outside the field of view but his path crosses
into the field of view, we really want to see the clown move. The clown will
slowly circle now and come into view for part of the time that the behaviour
is executed. This is the natural behaviour that we expect.

Figure 4: The object scheduling bounds is completely inside the view
bounds.
Finally, as we draw closer, the complete scheduling bounds falls inside the
view bounds. There is no change from the partial intersection case except for
the fact that we now see the clown for the full circle. The behaviour still
runs all the time.
|