JavaOur First 3D Game Program: Math for Java Game Programmers

Our First 3D Game Program: Math for Java Game Programmers

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

Java Programming Notes # 1712


Preface

General

Next in a series

This tutorial is the next lesson in a series designed to teach you some of the mathematical skills that you will need (in addition to good programming skills) to become a successful game programmer. The first lesson was titled Math for Java Game Programmers, Getting Started. The previous lesson was titled Math for Java Game Programmers, Venturing into a 3D World (see Resources).

In addition to helping you with your math skills, I will also help you learn how to incorporate those skills into object-oriented programming using Java. If you are familiar with other object-oriented programming languages such as C#, you should have no difficulty porting this material from Java to those other programming languages.

Lots of graphics

Since most computer games make heavy use of either 2D or 3D graphics, you will need skills in the mathematical areas that are required for success in 2D and 3D graphics programming. As a minimum, this includes but is not limited to skills in:

  • Geometry
  • Trigonometry
  • Vectors
  • Matrices
  • 2D and 3D transforms
  • Transformations between coordinate systems
  • Projections

Of course, game programming requires mathematical skills beyond those required for graphics. However, for starters, this series will concentrate on the last five items in the above list.

What you have learned

In the previous lesson, you learned how to update the game-math library to support 3D math, how to program the equations for projecting a 3D world onto a 2D plane, how to add vectors in 3D, about scaling, translation, and rotation of a point in both 2D and 3D, about the rotation equations and how to implement them in both 2D and 3D, and much more.

What you will learn

In this lesson, you will learn how to write your first interactive 3D game using the game-math library named GM01. You will also learn how to write a Java program that simulates flocking behavior such as that exhibited by birds and fish and how to incorporate that behavior into a game. Finally, you will examine three other programs that illustrate various aspects of both 2D and 3D animation using the game-math library.

Viewing tip

I recommend that you open another copy of this document in a separate browser window and use the following links to easily find and view the figures and listings while you are reading about them.

Figures

  • Figure 1. A school of red prey fish being attacked by a blue predator.
  • Figure 2. Screen shot of the graphic output from GM01test04.
  • Figure 3. Screen shot of the graphic output from GM01test03.
  • Figure 4. Screen shot of the graphic output from StringArt04.
  • Figure 5. Prey fish in a large swirling cluster.
  • Figure 6. Formation starting to break up due to closeness of the predator.
  • Figure 7. 100 prey fish trying to occupy the same location in 3D space.
  • Figure 8. 100 prey fish maintaining a reasonable separation in 3D space.
  • Figure 9. Twelve predators swimming in a hexagon formation in GM01test04.
  • Figure 10. Six predators swimming in a diamond formation.

Listings

  • Listing 1. Abbreviated constructor for the GUI class in GM01test08
  • Listing 2. Beginning of the actionPerformed method in GM01test08.
  • Listing 3. Code that services the Attack button.
  • Listing 4. Code that services the Stop button.
  • Listing 5. Beginning of the Animate class and the run method.
  • Listing 6. Create the population of prey fish.
  • Listing 7. Create and position the predator object.
  • Listing 8. A few more housekeeping details.
  • Listing 9. Beginning of the animation loop.
  • Listing 10. Cause all of the prey fish to spiral toward the center.
  • Listing 11. Create a rotated vector.
  • Listing 12. Move the prey fish object based on the sum of the vectors.
  • Listing 13. Save a clone of the relocated prey fish object.
  • Listing 14. Save a normalized direction vector.
  • Listing 15. Code for when the population contains only one prey fish.
  • Listing 16. Separate the prey fish.
  • Listing 17. The remainder of the inner loop.
  • Listing 18. Save the primary prey fish object and do another iteration.
  • Listing 19. Prey fish objects react to the predator.
  • Listing 20. Prey fish object takes evasive action.
  • Listing 21. Test the separation again.
  • Listing 22. Restore the prey fish to the population.
  • Listing 23. Keep the prey fish in the playing field.
  • Listing 24. Erase the off-screen image and draw the circle.
  • Listing 25. Draw the prey fish on the off-screen image.
  • Listing 26. Cause the predator to slowly circle the cluster of prey fish.
  • Listing 27. Execute the attack.
  • Listing 28. Draw the predator on the off-screen image.
  • Listing 29. Copy off-screen image, insert time delay, etc.
  • Listing 30. Source code for the game-math library named GM01.
  • Listing 31. Source code for the game program named GM01test08.
  • Listing 32. Source code for the program named GM01test04.
  • Listing 33. Source code for the program named GM01test03.
  • Listing 34. Source code for the program named StringArt04.

Supplementary material

I recommend that you also study the other lessons in my extensive collection of online Java tutorials. You will find a consolidated index at www.DickBaldwin.com.

General background information

In this series, I will frequently refer you to an excellent interactive tutorial on Vector Math for 3D Computer Graphics by Dr. Bradley P. Kjell (see Resources) for the required technical background. Then I will help you learn how to incorporate the knowledge that you gain from Kjell’s tutorial into Java code with a heavy emphasis on object-oriented programming.

In the process, I will develop and explain a game-math library that you can use to experiment with and to confirm what you learn from the Kjell tutorial. The library will start out small and grow as we progress through more and more material in subsequent lessons.

For this lesson, I recommend that you concurrently study the Kjell tutorial through Chapter6 – Scaling and Unit Vectors.

Preview

You’ve been working hard if you’ve been studying these tutorials from the very beginning. It’s time to kick back and have a little fun with what you have learned. In this lesson, I will present four programs, (one of which is an interactive 3D game), that use the current capabilities of the game-math library to produce some fairly serious animation. The programs are named:

  • GM01test08
  • GM01test04
  • GM01test03
  • StringArt04

I will explain the game program in detail and will provide a brief description of the other three programs.

GM01test08

The first three programs are based on an artificial life (see Resources) concept frequently referred to as boids (see Resources), which is “a computer model of coordinated animal motion such as bird flocks and fish schools.”

 
Flocking fish
I got the idea for this game while watching a segment of a documentary series named Blue Earth on one of the cable channels. I even surprised myself as to how well I was able to simulate some of the scenes in that TV program using programming concepts similar to those used by Reynolds in boids.

A flocking game program

Although the flocking algorithms won’t be the same as those used in the original boids program written by Reynolds, the general concepts will be similar.

The first program named GM01test08 is an interactive 3D game program based on the flocking behavior of fish. This game simulates a school of prey fish being attacked and eaten by a predator. The objective for the player is to cause the predator to catch and eat all of the prey fish in a minimum amount of time.

A preview

I will get into a lot more detail about the game aspects of the program later. For a preview, Figure 1 shows the instant in time when a school of 500 prey fish (shown in red) have just been attacked by the blue predator shown near the center right.

Figure 1. A school of red prey fish being attacked by a blue predator.

The predator in Figure 1 has penetrated the school of fish by swimming from left to right, eating fish all along the way, and has emerged from the right side of the school. The prey fish, which were originally in a small tight cluster, have reacted to the presence of the predator by scattering in an attempt to avoid being eaten.

Fully three-dimensional

This game program is fully three-dimensional. Any or all of the fish are capable of swimming in any direction at any time in the 3D world. This is evidenced by the prey fish near the center that appear to have very short or non-existent tails. The fish that appear to have no tails actually have tails that are the same length as the tails on all of the other fish. These fish appear to have no tails because they are swimming either directly toward or directly away from the viewer. I will have more to say about this later.

GM01test04 and GM01test03

These two programs provide essentially the same behavior (as one another), except that one is a 2D program and the other is a 3D program. They are not game programs. Rather, they are demonstration programs that show another aspect of flocking behavior. In these two programs, a large number of predators pursue a single prey object.

A screen shot of the animated output from the 2D version is shown in Figure 2 and a screen shot of the output from the 3D version is shown in Figure 3.

Figure 2. Screen shot of the graphic output from GM01test04.

Figure 3. Screen shot of the graphic output from GM01test03.

I included these two programs in this lesson primarily to provide a good comparison between 2D and 3D, both from a programming viewpoint and a behavior viewpoint.

Eight predator objects and one prey object

In both versions shown in Figure 2 and Figure 3, you see eight predator objects (colored black) pursuing a red prey object. The actual length of every predator object is the same as the length of every other predator object and the length of the prey object is the same as the length of the predator objects.

Some significant differences

At first glance, the two versions may look the same. However, they are significantly different. In Figure 2, the prey and the predators are constrained to move in a single plane. Hence, the apparent length of each predator is the same as the length of every other predator and is also the same as the length of the prey regardless of the direction in which the object is moving.

However, in Figure 3, the predators and the prey are free to move in any direction in a 3D world. As a result, at any point in time, the objects may be moving parallel to or at an angle to the x-y plane. When the 3D world is projected onto the viewing plane, those objects that are moving at an angle to the viewing plane will be foreshortened. In the extreme case, (as shown earlier in Figure 1), when an object is moving directly toward or directly away from the viewer, its apparent length will go to zero. (At least one predator is almost in that state in Figure 3.)

In Figure 3 the apparent lengths of the predator and prey objects are all different indicating that they are all moving in slightly different directions in the 3D world.

The flocking algorithms in these two programs are much simpler than the algorithm used in Figure 1. I encourage you to experiment with the flocking algorithms for these two programs to see what you can produce that is new and different from my version of the two programs.

StringArt04

The fourth program named StringArt04 is completely different from the other three programs. In a previous lesson, I presented and explained a program that rotates a disk around an arbitrary anchor point in 3D space with the ability to rotate by different angles around each of the three axes. It can be very difficult to visualize exactly what is happening with such complex rotations. This program animates the rotation process and runs it in slow motion so that you can see exactly what is happening as the disk moves and rotates from its initial position and orientation to its final position and orientation.

A stop-action screen shot

The game program named GM01test08

This is an interactive 3D game program. In fact, this is the first game program that I have presented in this series on game math. This game simulates a school of prey fish being attacked and eaten by a predator. The objective for the player is to cause the predator to eat all of the prey fish in a minimum amount of time.

Player initiates attacks

The player in this game initiates a series of attacks by the predator by clicking the Attack button shown in Figure 5. The winning strategy for the player is to time the attacks in such a way as to minimize the elapsed time required for the predator to eat all of the prey fish. The final elapsed time depends on both strategy and chance. In other words, as is the case in many games, this game is part strategy and part chance.

A swirling cluster of prey fish

The prey fish, (shown in red), tend to swim in a large swirling cluster as shown in Figure 5 when they aren’t being harassed by the predator.

Figure 5. Prey fish in a large swirling cluster.

Predator behavior

The predator, (shown in blue in Figure 5), swims around the cluster, darting into and through the cluster when the user clicks the Attack button. During an attack, the predator attempts to eat as many prey fish as possible. When the predator attacks, the prey fish tend to scatter (as shown in Figure 1), breaking up their tight formation and making it more difficult for the predator to catch them. If the predator is allowed to swim around them again for a short period following an attack, they will form back into a cluster.

Proper timing is important

The attacks should be timed so that the prey fish are in a reasonably tight cluster at the beginning of each attack so that multiple prey fish will be eaten during the attack. However, allowing too much time between attacks is detrimental because it increases the total elapsed time. Thus, the player must make a tradeoff between elapsed time between attacks and the tightness of the cluster at the beginning of each attack. Another disadvantage of waiting too long between attacks will be explained later.

The graphical user interface

In addition to some other controls, the GUI provides an Attack button and an elapsed-time/kill-count indicator, shown in the bottom right in Figure 5. At any point in time, this indicator displays the elapsed time in milliseconds when the most recent prey fish was eaten along with the total number of prey fish that have been eaten. The two values are separated by a “/” character.

When all of the prey fish have been eaten by the predator, the elapsed time indicator shows the time in milliseconds required for the predator to catch and eat all of the prey fish. It also shows the number of prey fish that were eaten, which should match the value that the player entered into the upper-right text field before starting the game. (This field contains 500 in Figure 1 and 200 in Figure 5).

Not initially in attack mode

When the player clicks the Start button and the game begins, the predator is not in attack mode. Rather, the predator swims around the school of prey fish encouraging them to bunch up into a smaller and tighter cluster as shown in Figure 5. The purpose of this behavior is to increase the number of fish that will be eaten when an attack is made.

The attack will be more or less successful

This circling behavior on the part of the predator continues until the player clicks the Attack button, at which time the predator enters the attack mode and makes an attack on the cluster as shown in Figure 1. If the player clicks the Attack button too early, or doesn’t wait long enough between attacks, the prey fish will be in a loose cluster and the predator will eat very few fish during the attack.

The predator always has an impact

Even when the predator is not in attack mode, its presence has a significant effect on the behavior of the school of prey fish. As the predator swims around the prey fish, they tend to bunch up into a smaller and tighter cluster, but when the predator swims too close, the prey fish tend to panic and scatter, breaking up the tight cluster as shown in Figure 6.

Therefore, if the player waits too long to click the Attack button or waits too long between attacks, the predator will have spiraled in so close to the prey fish that they will break formation and begin to scatter, making it difficult for the predator to eat a large number of fish during the next attack. This is the other disadvantage of waiting too long to attack that I mentioned earlier.

Formation starting to break up due to closeness of the predator

Figure 6 shows the formation starting to break up because the predator has strayed too close to the cluster of prey fish.  (The fish in the upper right of the formation have pulled out of the cluster and have started to flee the predator.)

Figure 6. Formation starting to break up due to closeness of the predator.

An effective defense mechanism

 
Cleaning up the leftovers
The final three or four prey fish are often the most difficult to catch because they have more room to run without colliding with another fish.

The prey fish have a fairly effective defense mechanism and can do a reasonably good job of escaping the predator. The predator can increase the odds of catching the prey fish by attacking very fast. (You can modify the code that controls the speed of the attack.) If the predator goes into the formation slowly, the prey fish will simply run away as is starting to be the case in Figure 6.

Captured fish are removed from the population

When the predator is successful in eating a prey fish, that fish is removed from the prey-fish population causing the prey-fish population to decrease over time. Also, each time the predator eats a prey fish, the program emits an audible beep to provide feedback to the player.

The other GUI components

In addition to the elapsed time indicator and the Attack button, the GUI contains an input text field by which the player can specify the number of prey fish that will be in the population when the game begins. The GUI also contains a Start button and a Stop button. Finally, the GUI contains check boxes that allow the player to display points only, direction vectors only, or both for the prey fish. (Only the direction vector is displayed for the predator.)

Playing the game

In practice, the player specifies the number of randomly-placed prey fish that will be in the population and clicks the Start button to start the game. At this point, the prey fish swim in a large swirling cluster and the predator swims around them encouraging them to form a tighter cluster as shown in Figure 5.

Prey fish motion is random but each prey fish generally tends to spiral toward the center of the cluster. When the user clicks the Attack button, the predator turns and swims rapidly into and through the center of the cluster of prey fish. If the predator manages to get to within a specified distance from a prey fish, that prey fish will be deemed to have been eaten, and will be removed from the population. However, each prey fish will sense that the predator is coming and will try to escape. Whether or not an individual prey fish manages to escape the predator when an encounter between the two occurs is based on a random number generator.

The Start and Stop buttons

The animation continues until all of the fish have been eaten or the user clicks the Stop button. The user can click the Stop button at any time, change any of the parameters, and then click the Start button again to re-start the game with zero elapsed time and different parameters.

Displaying the vectors

The animation is most impressive when the direction vectors are displayed for the prey fish because the vectors provide a lot of visual information about how the prey fish are reacting to the predator.

A spherical playing-field boundary

In addition to the school of prey fish and the predator, the graphical output also shows a large circle drawn with broken lines, as shown in Figure 5. This circle represents the intersection of a sphere and the x-y plane.

The purpose of the sphere, which is centered on the origin, is to provide a soft boundary for keeping the prey fish and the predator inside the 3D playing field. The prey fish and the predator all have a tendency to remain inside the sphere, but they may occasionally stray outside the sphere. If they do, the program code will encourage them to return to the 3D playing field inside the sphere.

A 3D display

As mentioned earlier, this game is fully three dimensional. The prey fish and the predator are free to swim in any direction in 3D space. The tails on the prey fish and the predator appear longest when they are swimming parallel to the x-y plane. As they change their angle relative to the x-y plane, the tails appear to become shorter or longer and in some cases, they appear not to have a tail at all. The fish that appear to have no tails are either swimming directly toward the viewer or swimming directly away from the viewer.

This can best be observed by clicking the Stop button just as the fish scatter during an attack and freezing the state of the fish in the 3D world as shown in Figure 1. If you examine the image at that point, you are likely to see some fish with shorter tails than other fish. This is evident by some of the prey fish near the center of Figure 1 that appear to have no tails at all.

Enough talk, let’s see some code

A complete listing of this program is provided in Listing 31 near the end of the lesson.

Portions of this program are very similar to programs that I have explained in earlier lessons in this series. I won’t repeat those explanations here. Rather, I will concentrate mostly on the code that is new and different in this lesson.

Abbreviated constructor for the GUI class

Listing 1 shows an abbreviated constructor for the GUI class. (Much of the code in the constructor has been explained before, and was deleted from Listing 1 for brevity.)

Listing 1. Abbreviated constructor for the GUI class in GM01test08.

  GUI(){//constructor

//Code deleted for brevity

    //Register this object as an action listener on all
    // three buttons.
    startButton.addActionListener(this);
    attackButton.addActionListener(this);
    stopButton.addActionListener(this);

    //Make the drawing color RED at startup.
    g2D.setColor(Color.RED);

  }//end constructor

If you examine Listing 31, you will see that the GUI class implements the ActionListener interface. Therefore, an object of the GUI class can be registered as an action listener on a button. Listing 1 registers the object of the GUI class as a listener on all three of the buttons shown in Figure 6.

Beginning of the actionPerformed method

The actionPerformed method that begins in Listing 2 is called whenever the player clicks any of the three buttons shown in Figure 6.

Listing 2. Beginning of the actionPerformed method in GM01test08.

  public void actionPerformed(ActionEvent e){

    if(e.getActionCommand().equals("Start") && !animate){
      //Service the Start button.

      //Get several user input values.
      numberPoints = Integer.parseInt(
                             numberPointsField.getText());

      if(drawPointsBox.getState()){
        drawPoints = true;
      }else{
        drawPoints = false;
      }//end else

      if(drawVectorsBox.getState()){
        drawVectors = true;
      }else{
        drawVectors = false;
      }//end else

      //Initialize some working variables used in the
      // animation process.
      animate = true;
      baseTime = new Date().getTime();
      killCount = 0;

      //Enable the Attack button.
      attackButton.setEnabled(true);

      //Initialize the text in the timer field.
      timer.setText("0 / 0");

      //Cause the run method belonging to the animation
      // thread object to be executed.
      new Animate().start();
    }//end if

Servicing the Start button

The code in Listing 2 is executed when the user clicks the Start button. All of this code is also very similar to code that I have explained in previous lessons, so further explanation beyond the embedded comments should not be necessary. However, I will emphasize the two statements shown in boldface.

The first boldface statement enables the Attack button. The second boldface statement instantiates a new object of the Thread class named Animate and causes the run method of the animation thread to be executed. I will explain the animation thread later.

Code that services the Attack button

The code in Listing 3 is executed when the player clicks the Attack button after it is enabled by the code in Listing 2.

Listing 3. Code that services the Attack button.

    if(e.getActionCommand().equals("Attack") && animate){

      attack = true;

      predatorVec = 
               predator.getDisplacementVector(preyCenter).
                                               scale(1.0);

      //Disable the Attack button. It will be enabled
      // again when the attack is finished.
      attackButton.setEnabled(false);
    }//end if

Listing 3 begins by setting the value of the variable named attack to true. As you will see later, this causes the predator to attack the prey fish in the animation loop.

Control the speed and direction of the predator attack

Then Listing 3 calls the getDisplacementVector method and the scale method of the game-math library to set the value of the variable named predatorVec to a direction that will be used to point the predator toward the center of the prey-fish cluster. In Listing 3, the scale factor is unity, so the application of the scale factor does nothing. However, you can modify this value to control the speed of the predator during the upcoming attack. If you increase the scale factor, the predator will move faster. If you decrease the scale factor, the predator will move more slowly.

Temporarily disable the Attack button

Finally, Listing 3 temporarily disables the Attack button so that it will have no effect if the player clicks it again during the attack. You could also remove this statement, slow down the attack, and experiment with multiple clicks on the Attack button during the course of an attack to see what happens.

Code that responds to the Stop button

The code in Listing 4 is executed when the player clicks the Stop button.

Listing 4. Code that services the Stop button.

    if(e.getActionCommand().equals("Stop") && animate){

      //This will cause the run method to terminate and
      // stop the animation. It will also clear the
      // attack flag so that the next time the animation
      // is started, the predator won't be in the 
      // attack mode.
      animate = false;
      attack = false;
    }//end if

  }//end actionPerformed

No explanation beyond the embedded comment should be required for this code.

Beginning of the Animate class and the run method

When the Start button is clicked, the code in Listing 2 instantiates a new object of the Thread class named Animate and causes the run method belonging to that object to be executed. Listing 5 shows the beginning of the Animate class and its run method.

Listing 5. Beginning of the Animate class and the run method.

  class Animate extends Thread{
    //Declare a general purpose variable of type Point3D.
    // It will be used for a variety of purposes.
    GM01.Point3D tempPrey;

    //Declare two general purpose variables of type
    // Vector3D. They will be used for a variety of
    // purposes.
    GM01.Vector3D tempVectorA;
    GM01.Vector3D tempVectorB;
    //--------------------------------------------------//

    public void run(){
      //This method is executed when start is called on
      // this Thread object.
      //Create a new empty container for the prey objects.
      //Note the use of "Generics" syntax.
      preyObjects = new ArrayList<GM01.Point3D>();

      //Create a new empty container for the vectors. The
      // only reason the vectors are saved is so that they
      // can be displayed later
      displayVectors = new ArrayList<GM01.Vector3D>();

Importance of the game-math library named GM01

The code in Listing 5 is straightforward and shouldn’t require an explanation beyond the embedded comments. However, I will emphasize the heavy use of the game-math library classes named GM01.Point3D and GM01.Vector3D in the code in Listing 5.

This entire game program is heavily dependent on the use of the ColMatrix3D, Point3D, Vector3D, and Line3D classes along with some of the static methods in the game-math library named GM01. If you were to start from scratch and write this game program without the availability of the game-math library, the program would be much longer and would be much more complex.

Create the population of prey fish

Listing 6 creates the population of prey fish and positions them at random locations in 3D space. Listing 6 also stores references to the prey fish objects in the preyObjects container, which was created in Listing 5.

Listing 6. Create the population of prey fish.

      for(int cnt = 0;cnt < numberPoints;cnt++){
        preyObjects.add(new GM01.Point3D(
          new GM01.ColMatrix3D(
                         100*(random.nextDouble()-0.5),
                         100*(random.nextDouble()-0.5),
                         100*(random.nextDouble()-0.5))));

        //Populate the displayVectors collection with
        // dummy vectors.
        displayVectors.add(tempVectorA);
      }//end for loop

In addition, Listing 6 populates a container named displayVectors that was also created in Listing 5 to store a direction vector belonging to each prey fish. This container is populated with dummy vectors in Listing 6, simply to set the size of the container to be the same as the size of the container containing references to the prey fish objects. (There is probably a more efficient way to set the size of that container.)

Note that each prey fish object is actually represented by an object of the game-math library class named GM01.Point3D.

Create and position the predator object

Listing 7 creates an object of the GM01-Point3D class that will represent the predator in the game.

Listing 7. Create and position the predator object.

      predator = new GM01.Point3D(
                     new GM01.ColMatrix3D(-100,100,-100));

The initial position given to the predator in Listing 7 causes it to appear in the game near the top center of the screen when the user clicks the Start button. You could also make the initial position of the predator random if you think that would improve the game.

A few more housekeeping details

Listing 8 takes care of a few more housekeeping details before the animation actually begins.

Listing 8. A few more housekeeping details.

      //Create a reference point to mark the origin. Among
      // other things, it will be used as the center of a
      // sphere, which in turn will be used to attempt to
      // keep the prey and predator objects from leaving
      // the playing field.
      GM01.Point3D origin =
            new GM01.Point3D(new GM01.ColMatrix3D(0,0,0));

      //Declare some variables that will be used to
      // compute and save the average position of all of
      // the prey objects.
      double xSum = 0;
      double ySum = 0;
      double zSum = 0;

Let the show begin

Listing 9 shows the beginning of the animation loop, which will continue to execute for as long as the value of the variable named animate is true.

Listing 9. Beginning of the animation loop.

      while(animate){
        //Compute and save the average position of all the
        // prey objects at the beginning of the loop. Save
        // the average position in the Point3D object
        // referred to by preyCenter.
        xSum = 0;
        ySum = 0;
        zSum = 0;

        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          tempPrey = preyObjects.get(cnt);
          xSum += tempPrey.getData(0);
          ySum += tempPrey.getData(1);
          zSum += tempPrey.getData(2);
        }//end for loop

        //Construct a reference point at the average
        // position of all the prey objects.
        preyCenter = new GM01.Point3D(
           new GM01.ColMatrix3D(xSum/preyObjects.size(),
                                ySum/preyObjects.size(),
                                zSum/preyObjects.size()));

The value of animate is set to true by clicking the Start button and is set to false by clicking the Stop button. Setting the value to false causes the run method to terminate, thereby stopping the animation.

Attack toward the geometric center

When the predator attacks, it will move to and through the geometric center of the cluster of prey fish, eating prey fish all along the way. The geometric center of the cluster of prey fish is computed and saved by the code in Listing 9 as the average location of all the prey fish in the population.

Note that a vector that points to the geometric center may or may not be a good indicator of the best direction for eating large numbers of prey fish during an attack. In Figure 1, for example, a predator attacking from the 11:00 o’clock position and heading toward the center would encounter quite a few prey fish. However, a predator attacking from the 8:00 o’clock position and heading toward the center would encounter far fewer prey fish.

You will also learn that although the predator will always attack in a direction pointing toward this geometric center, the positions of the prey fish can change after the center is computed and before the attack begins, causing the position of the actual geometric center to change, before the predator has an opportunity to attack. That can decrease the probability of a successful attack by the predator.

These issues form part of the strategy of the game that must be mastered by the player in order to earn a good score.

Cause all of the prey fish to spiral toward the center

Listing 10 shows the beginning of a fairly complicated algorithm that causes all of the prey fish to have a tendency to spiral toward the center of the cluster of prey fish.

Listing 10. Cause all of the prey fish to spiral toward the center.

        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          if(preyObjects.size() > 1){
            //Get the next prey object
            tempPrey = preyObjects.get(cnt);
            //Find the displacement vector from this prey
            // object to the preyCenter
            tempVectorA = tempPrey.getDisplacementVector(
                                              preyCenter);

The code in the body of the for loop that begins in Listing 10 is executed once during each animation cycle for each of the prey fish in the population of prey fish.

To begin with, the if statement prevents the tendency to spiral toward the geometric center from being applied when there is only one prey fish remaining in the population. I will leave it as an exercise for the student to think about this and to decide whether or not this is a good decision.

The code in Listing 10 gets a reference to the next prey fish object in the population and then gets and saves a reference to a displacement vector pointing from the prey fish to the geometric center.

Create a rotated vector.

Listing 11 creates, scales, and saves a new GM01.Vector3D object having the same component values as the displacement vector from Listing 11, but assigning those values to different axes.

Listing 11. Create a rotated vector.

            tempVectorB = new GM01.Vector3D(
              new GM01.ColMatrix3D(
                      tempVectorA.getData(1),
                      tempVectorA.getData(2),
                      tempVectorA.getData(0))).scale(1.2);

Once again, I will leave it as an exercise for the student to think about the length and direction of such a vector in relation to the original displacement vector. It might help to begin by drawing some vectors in 2D that swap components and then extending the same thinking to 3D. One thing is for certain, unless all of the components have the same value, the new vector created in Listing 11 has a different length and direction than the displacement vector created in Listing 10, and that would be true even if the vector from Listing 11 had not been scaled.

Move the prey fish object based on the sum of the vectors

Listing 12 begins by adding the two vectors that were created in Listing 10 and Listing 11, scaling the vector from Listing 10 before performing the addition. Note that the scale factors applied to the rotated vector in Listing 11 is different from the scale factor applied to the original displacement vector in Listing 12. The ratio of these two scale factors influences the tendency of the prey fish object to move toward the center relative to its tendency to move around the center.

Listing 12. Move the prey fish object based on the sum of the vectors.

            tempVectorA =
                  tempVectorA.scale(0.9).add(tempVectorB);

            tempPrey = tempPrey.addVectorToPoint(
                                  tempVectorA.scale(0.1));

Then Listing 12 calls the GM01.Point3D.addVectorToPoint method to relocate the prey fish object to a new position based on the scaled sum of the two vectors. The bottom line is that this will cause the prey fish object to spiral toward the geometric center that was computed in Listing 9 as the animation progresses.

Another scale factor is applied to the sum vector before using it to relocate the prey fish object. This scale factor controls the overall speed of the prey fish. Increasing the scale factor causes the prey fish to spiral faster. (Increasing the scale factor also causes some interesting patterns to appear and if you increase it too much, the prey fish will all leave the playing field.)

Save a clone of the relocated prey fish object

Listing 13 creates a clone of the relocated prey fish object and uses it to replace the original prey fish object in the container.

Listing 13. Save a clone of the relocated prey fish object.

            preyObjects.set(cnt,tempPrey.clone());

Creating and saving a reference to a clone instead of saving a reference to the relocated prey fish object may be overkill. However, I simply wanted to guard against the possibility of ending up with a corrupted object later due to the repeated use of the reference variable. I will leave it up to the student to think about this and to decide if this was a good or a bad decision.

Save a normalized direction vector

Listing 14 calls the GM01.Vector3D.normalize method to create a vector having a length of 15 units and the same direction as the vector that was used to relocate the prey fish object.

Listing 14. Save a normalized direction vector.

            displayVectors.set(
                 cnt,tempVectorA.normalize().scale(15.0));

This vector is saved and used for drawing later. Drawing this vector in conjunction with the point that represents the prey fish object produces the tails shown on the prey fish objects in Figure 1.

Code for when the population contains only one prey fish

Listing 15 shows the else clause that matches up with the if statement in Listing 10. This code is executed when the population has been reduced to only one prey fish object.

Listing 15. Code for when the population contains only one prey fish.

          }else{
            displayVectors.set(
                   cnt,new GM01.Vector3D(
                          new GM01.ColMatrix3D(10,10,0)));
          }//end else

        }//end loop to spiral prey objects toward center.

Listing 15 saves a dummy direction vector for drawing later. This is necessary to prevent a null pointer exception when the user specifies only one prey fish object and then clicks the Start button.

Not the end of the story

Listing 15 also signals the end of the for loop that causes the prey fish to spiral toward the center. The code in this for loop is sometimes referred to as a cohesion algorithm in flocking terminology. It causes the prey fish to stay together as a group.

This is not the end of the story, however. If the code in this cohesion algorithm were the only code controlling the behavior of the prey fish in this animation, they would simply continue spiraling toward the center, eventually producing a cluster that looks something like that shown in Figure 7 where 100 prey fish are trying to occupy the same location in 3D space.

Figure 7. 100 prey fish trying to occupy the same location in 3D space.

Interesting but also boring

While it may be interesting to watch the animation progress to this point, the animation becomes very boring when all of the prey fish cluster at the center. What we need is an additional algorithm that will cause each prey fish to attempt to maintain a respectable distance between itself and the other prey fish.

Prey fish maintaining a reasonable separation

For example, Figure 8 shows the result of temporarily making each prey fish immune to the presence of the predator, telling each prey fish to spiral toward the center, and also telling each prey fish to maintain a separation of ten units (pixels) between itself and all of the other prey fish.

Figure 8. 100 prey fish maintaining a reasonable separation in 3D space.

A 3D world

Remember, Figure 8 is a 3D world projected onto on a 2D screen display. Even if every prey fish is separated from every other prey fish by at least ten pixels (which is probably not the case as I will explain later), the projection of the 3D world onto the 2D display can make it appear that two or more prey fish occupy the same location.

Separate the prey fish

Listing 16 shows the beginning of a pair of nested for loops that attempt to keep the prey fish from colliding with one another by moving each prey fish object away from its close neighbors if necessary. In flocking terminology, this is often referred to as a separation algorithm.

Listing 16. Separate the prey fish.

        GM01.Point3D refPrey = null;
        GM01.Point3D testPrey= null;
        for(int row = 0;row < preyObjects.size();row++){
          refPrey = preyObjects.get(row);
          //Compare the position of the reference prey
          // object with the positions of each of the
          // other prey objects.
          for(int col = 0;col < preyObjects.size();col++){
            //Get another prey object for  proximity test.
            testPrey = preyObjects.get(col);

This algorithm gets a reference to each prey fish object (primary object) and compares its position with the positions of all the other prey fish objects (secondary objects). If the primary object is too close to a secondary object, the primary object is moved away from the secondary object.

Not a perfect algorithm

This is not a perfect algorithm however. A primary object can be moved away from all of its neighbors early in the execution of the algorithm, but a neighbor could be moved closer to the primary object later in the execution of the algorithm. While not perfect, the algorithm does a pretty respectable job of keeping the prey fish separated as evidenced by comparing the positions of the prey fish in Figure 7 with the positions of the prey fish in Figure 8. This separation algorithm was disabled in Figure 7 and was enabled in Figure 8.

Gets two objects that will be compared

Listing 16 gets a reference to a primary prey fish object in the outer loop that iterates on the counter named row and gets a reference to one of the secondary prey fish objects to which it will be compared in the inner loop that iterates on the counter named col.

The remainder of the inner loop

Listing 17 shows the remainder of the inner loop.

Listing 17. The remainder of the inner loop.

            //Don't test a prey object against itself.
            if(col != row){
              //Get the vector from the refPrey object to
              // the testPrey object.
              tempVectorA = refPrey.
                          getDisplacementVector(testPrey);

              //If refPrey is too close to testPrey, move
              // it away from the testPrey object.
              if(tempVectorA.getLength() < 10){
                //Move refPrey away from testPrey by a
                // small amount in the opposite direction.
                refPrey = refPrey.addVectorToPoint(
                         tempVectorA.scale(0.2).negate());

              }//end if on proximity test
            }//end if col != row
          }//end loop on col

It wouldn’t make any sense to compare the position of a prey fish with itself, so the boldface if statement in Listing 17 prevents that from happening.

Code is relatively straightforward

Since you already know how to use most of the methods in the game-math library, you should have no difficulty understanding the code in Listing 17. This code:

  • Gets the displacement vector that defines the separation between the primary object and the secondary object.
  • Compares the length of that vector with 10 units.
  • Moves the primary prey fish object in the opposite direction by 20-percent of the length of the separation vector if the separation is less than 10 units.

As mentioned earlier, however, moving the primary fish object away from one prey fish could cause it to be moved closer to a different prey fish object, so the separation algorithm is far from perfect.

Listing 17 signals the end of the inner loop that began in Listing 16.

Save the primary prey fish object and do another iteration

The primary prey fish object may or may not have been moved. Regardless, a clone of that object is created and saved in the container that contains the prey-fish population in Listing 18.

Listing 18. Save the primary prey fish object and do another iteration.

          preyObjects.set(row,refPrey.clone());
        }//end loop on row

After the object is saved in Listing 18, control is transferred back to the top of the outer for loop in Listing 16 and the next prey fish object in the population is compared with all of the other prey-fish objects in the population, making corrections to the position of the prey fish as necessary.

Prey fish objects react to the predator

As I mentioned earlier, each prey fish object has a reasonably good defense mechanism to avoid being eaten by the predator. However, in the final analysis, whether or not a prey fish will escape when it encounters the predator face to face is a random process. To some extent, success or failure to escape depends on how quickly the prey fish senses the presence of the predator.

Listing 19 is the beginning of a for loop in which the proximity of each prey fish to the predator is tested and evasive action on the part of the prey fish is taken if the distance between the two is less than 50 units.

Listing 19. Prey fish objects react to the predator.

        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          tempPrey = preyObjects.get(cnt);

          //Get a displacement vector from the prey object
          // to the predator.
          tempVectorA = tempPrey.getDisplacementVector(
                                                predator);

The code in Listing 19 gets the next prey fish in the population and gets a displacement vector describing the separation of the prey fish from the predator.

Prey fish object takes evasive action

If the prey fish is at least 50 units away from the predator, the prey fish simply goes on swimming without concern for the predator. However, if the prey fish is less than 50 units away from the predator, the prey fish takes evasive action in an attempt to escape the predator. The evasive action is accomplished in Listing 20, and this is where some of the random behavior comes into play.

Listing 20. Prey fish object takes evasive action.

          if(tempVectorA.getLength() < 50){
            tempVectorA = tempVectorA.negate().scale(
                                     random.nextDouble());
            tempPrey =
                   tempPrey.addVectorToPoint(tempVectorA);

The prey fish moves by a random distance

In listing 20, the negative of the displacement vector separating the prey fish from the predator is scaled by a random value ranging from 0 to 1. Then the prey fish is moved by the distance and direction specified by the resulting vector. If the random value is 0, the prey fish won’t be moved at all. If the random value is 1, the distance between the prey fish and the predator will be doubled.& For values between 0 and 1, the prey fish will be moved different distances away from the predator.

Did the prey fish escape?

Once the prey fish has taken the evasive action and has (possibly) moved away from the predator, the separation between the prey fish and the predator is tested again in Listing 21.

Listing 21. Test the separation again.

            tempVectorA =
                 tempPrey.getDisplacementVector(predator);

            if(tempVectorA.getLength() < 25){
              tempPrey = preyObjects.remove(cnt);
              displayVectors.remove(cnt);
              Toolkit.getDefaultToolkit().beep();
              killCount++;
              timer.setText(
                       "" 
                       + (new Date().getTime() - baseTime)
                       + " / " 
                       + killCount);

If the new separation is at least 25 units, the escape attempt is deemed successful and the prey fish will continue to live. (You can decrease or increase the threshold distance used in the test in Listing 21 to cause the prey fish object to be more or less successful in the escape attempt. If you make the threshold distance small enough, the prey fish will almost always escape.)

When the escape attempt is not successful…

When the escape attempt is not successful, the remaining code in Listing 21 is executed. The prey fish is deemed to have been eaten by the predator. Therefore, the prey fish and its direction vector are removed from the containers that contain them. This causes the prey fish to be removed from the population.

In addition, the program sounds a beep to notify the user of the successful attack by the predator and the kill count that is displayed in the bottom right of Figure 1 is incremented by one.

Display elapsed time and number of fish eaten

Finally, the code in Listing 21 displays the elapsed time in milliseconds since the Start button was clicked along with the number of fish that have been eaten. Note that the value that is displayed is the elapsed time when the most recent prey fish was eaten by the predator and is not the total elapsed time since the Start button was clicked. (The elapsed time value won’t change again until another prey fish is eaten.)

When all of the prey fish have been eaten, the final time that is displayed is the time that was required for the predator to eat all of the fish in the population and the final kill count that is displayed is the same of the original number of prey fish in the population. (Also note that the player must click the Stop button before starting a new game. Fixing this inconvenience will be a good exercise for the student.)

When the escape attempt is successful…

When the escape attempt is successful, a clone of the prey fish in its new location is stored in the preyObjects container and a normalized version of the fish’s direction vector is stored in the displayVectors container as shown in Listing 22.

Listing 22. Restore the prey fish to the population.

            }else{
              //The escape attempt was successful. Restore
              // a clone of the prey object in its new
              // location along with its direction vector
              // to the population.
              preyObjects.set(cnt,tempPrey.clone());
              //Save a normalized version of the direction
              // vector for drawing later.  It will
              // overwrite the previously saved vector and
              // if you watch closely it will show the
              // prey object running away from the
              // predator.
              displayVectors.set(
                 cnt,tempVectorA.normalize().scale(15.0));
            }//end else
          }//end if distance < 50
        }//end for loop on population

Listing 22 also signals the end of the for loop that began in Listing 19.

Keep the prey fish in the playing field

I have one more section of code to explain that deals with the behavior of the prey fish during each iteration of the animation loop. The code in Listing 23 causes prey fish that stray outside the playing field to return to the playing field.

Listing 23. Keep the prey fish in the playing field.

        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          tempPrey = preyObjects.get(cnt);

          if(tempPrey.getDisplacementVector(origin).
                             getLength() > 0.5*osiHeight){

            tempVectorA = 
                   tempPrey.getDisplacementVector(origin);
            tempPrey = tempPrey.addVectorToPoint(
                                  tempVectorA.scale(0.1));
            preyObjects.set(cnt,tempPrey.clone());
          }//end if prey object is out of bounds
        }//end for loop to process each prey object

The playing field is the interior of a sphere

The playing field is defined to be the interior of a sphere that is centered on the origin with a radius that is one-half the height of the off-screen image. The for loop in Listing 23 iterates once for each prey fish remaining in the population. The if statement in Listing 23 tests to determine if a prey fish is outside of the sphere.

If the prey fish is outside the sphere, the code in Listing 23 gives that prey fish a nudge back toward the origin and saves a clone of the prey fish in its new location.

Erase the off-screen image and draw the large circle

Listing 24 erases the off-screen image and draws the large circle that defines the
playing field shown in Figure 1.

Listing 24. Erase the off-screen image and draw the large circle.

        //Erase the screen
        g2D.setColor(Color.WHITE);
        GM01.fillRect(g2D,-osiWidth/2,osiHeight/2,
                                      osiWidth,osiHeight);


        //Draw a broken-line circle that represents the
        // intersection of the spherical boundary with the
        // x-y plane. Although the prey objects and the
        // predator can stray outside this sphere, they
        // prefer to be inside the sphere.
        GM01.Point3D tempPointA = new GM01.Point3D(
                   new GM01.ColMatrix3D(osiHeight/2,0,0));
        GM01.Point3D tempPointB;
        //The circle is defined by 39 points around the
        // circumference.
        g2D.setColor(Color.BLACK);
        for(int cnt = 0;cnt < 39;cnt++){
          tempPointB = 
           new GM01.Point3D(new GM01.ColMatrix3D(
           osiHeight/2*Math.cos((cnt*360/39)*Math.PI/180),
           osiHeight/2*Math.sin((cnt*360/39)*Math.PI/180),
           0));

          //Connect every third pair of points with a line
          if(cnt%3 == 0){
            new GM01.Line3D(
                         tempPointA,tempPointB).draw(g2D);
          }//end if
          //Save the old point.
          tempPointA = tempPointB;
        }//end for loop
        
        //Draw the final line required to close the circle
        new GM01.Line3D(tempPointA,new GM01.Point3D(
                           new GM01.ColMatrix3D(
                             osiHeight/2,0,0))).draw(g2D);

Although somewhat tedious, the code in Listing 24 is straightforward and shouldn’t require an explanation beyond the embedded comments.

Draw the prey fish on the off-screen image

Depending on the states of the two check boxes in Figure 1, Listing 25 draws the GM01.Point3D objects that represent the prey fish or GM01.Vector3D objects that represent the direction vectors for the prey fish, or both on the off-screen image.  (Note that only the vectors were drawn on the off-screen image in Figure 1.)

Listing 25. Draw the prey fish on the off-screen image.

        g2D.setColor(Color.RED);
        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          tempPrey = preyObjects.get(cnt);
          if(drawPoints){
            tempPrey.draw(g2D);//draw circle around point
          }//end if

          if(drawVectors){
            //Draw the vector with its tail at the point.
            displayVectors.get(cnt).draw(g2D,tempPrey);
          }//end if
        }//end for loop

There is nothing new in Listing 25, so I won’t bore you with a detailed explanation of the code.

It’s time to deal with the predator

So far, we have been dealing exclusively with code that controls the behavior of the prey fish. The time has come to deal with the code that controls the behavior of the predator.

Cause the predator to slowly circle the cluster of prey fish

When the player clicks the Attack button, a boolean variable named attack is set to true, causing the predator to enter attack mode during the next iteration of the animation loop. However, when the value of this variable is false, the predator is not in attack mode and its behavior is to swim slowly around the cluster of prey fish encouraging them to bunch up into a smaller and tighter cluster.

The code that accomplishes this circling behavior is shown in Listing 26.

Listing 26. Cause the predator to slowly circle the cluster of prey fish.

        //When the predator is not in attack mode, cause
        // it to slowly circle the cluster of prey
        // objects.
        if(!attack){
          //Get a displacement vector pointing from the
          // predator to the preyCenter.
          predatorVec = 
               predator.getDisplacementVector(preyCenter);

          //Create a vector that is rotated relative to
          // predatorVec. Note how each component in
          // this new vector is set equal to a different
          // component in predatorVec
          tempVectorB = new GM01.Vector3D(
            new GM01.ColMatrix3D(
                     predatorVec.getData(1),
                     predatorVec.getData(2),
                     predatorVec.getData(0))).scale(0.15);

          //Scale predatorVec and add the two vectors.
          // Then move the predator according to the sum
          // of the vectors.
          //Moving the prey object in the direction of
          // the sum of these two vectors produces a
          // motion that causes the predator to
          // spiral toward the preyCenter instead of
          // simply moving in a straight line toward the
          // preyCenter. The scale factors control the
          // relative motion between the two directions,
          // and are fairly sensitive.
          predatorVec = 
                predatorVec.scale(0.095).add(tempVectorB);

          predator = predator.addVectorToPoint(
                                  predatorVec.scale(1.0));

The code in Listing 26 is very similar to code that I explained earlier in conjunction with the behavior of the prey fish. Therefore, no explanation beyond the embedded comments should be required.

Execute the attack

When the user clicks the Attack button, the value of the variable named attack is set to true, causing the code in Listing 27 to be executed during subsequent iterations of the animation loop.

Listing 27. Execute the attack.

        }else{//attack is true
          predator = predator.addVectorToPoint(
                                 predatorVec.scale(0.25));

          //Check to see if the predator is outside the
          // spherical boundary that defines the playing
          // field.
          if(predator.getDisplacementVector(origin).
                             getLength() > 0.5*osiHeight){
            //Shift out of attack mode and start circling
            // the prey fish again.
            attack = false;
            attackButton.setEnabled(true);
          }//end if
        }//end else

The predator is in attack mode

The predator is in attack mode at the beginning of Listing 27, and will remain in attack mode using the same displacement vector to control its speed and direction until it leaves the spherical playing field. When it leaves the playing field, the value of the attack variable will be set to false, causing the predator to revert to non-attack mode.

As the predator encounters prey fish along its trip toward the edge of the playing field, the code in Listing 21 (explained earlier) will determine whether those prey fish escape or get eaten by the predator.

Control of speed and direction

The displacement vector that controls the speed and direction of the predator (predatorVec in Listing 27) is created in the actionPerformed method in Listing 3 in response to a click on the Attack button.

This vector is constructed to point to the most recently computed geometric center of the cluster of prey fish. Note however, that the vector may no longer point to the exact center of the cluster because the exact center of the cluster may have changed since it was last computed. In other words, the position of the geometric center of the prey-fish cluster changes as the predator attacks and causes the prey fish to scatter. As programmed, the predator is unable to respond to such changes and continues to move in the same direction at the same speed until it leaves the playing field.

 
An interesting upgrade
A program upgrade to cause the predator to accommodate such changes in the geometric center would be an interesting exercise for the student.

In other words, even though the prey fish scatter, the predator is constrained to move in a straight line across the playing field once an attack has begun.

Draw the predator on the off-screen image

Listing 28 sets the drawing color to BLUE and draws the predator’s direction vector on the off-screen image.

Listing 28. Draw the predator on the off-screen image.

        g2D.setColor(Color.BLUE);
        //Enable the following statement to draw a circle
        // around the point that represents the predator.
        //predator.draw(g2D);

        //Draw the predator's vector.
        predatorVec.normalize().scale(15.0).draw(
                                            g2D,predator);
        g2D.setColor(Color.RED);//restore red color

Copy off-screen image, insert time delay, etc

Listing 29 copies the off-screen image to the canvas and then causes the animation thread to sleep for 166 milliseconds.

Listing 29. Copy off-screen image, insert time delay, etc.

        //Copy the off-screen image to the canvas and then
        // do it all again.
        myCanvas.repaint();

        //Insert a time delay. Change the sleep time to
        // speed up or slow down the animation. 
        try{
          Thread.currentThread().sleep(166);
        }catch(Exception e){
          e.printStackTrace();
        }//end catch
      }//end animation loop
    }//end run method
  }//end inner class named Animate

Listing 29 also signals the end of the animation loop, the end of the run method, and the end of the inner class named Animate.

That is all of the code that I will explain for this program. You can view the remainder of the code in Listing 31 near the end of the lesson.

Not a graphics program

Even though this program produces quite a lot of 3D graphics, and those graphics are important in the playing of the game, this is not a graphics program. Rather, it is a 3D data-processing program that produces graphics as a side effect.

That is not to say that the graphics are unimportant. To the contrary, the graphics provide visual feedback to the player, which allows the player to implement a strategy for success. Without graphics, the game would be very boring. However, the programming effort to produce the graphics represents an almost trivial part of the total programming effort for this game.

An almost trivial part of the programming effort

Exclusive of the code required to draw the large circle shown in Figure 1, all of the graphics shown in Figure 1, Figure 5, Figure 6, Figure 7, and Figure 8 are produced by only two calls to game-math library methods named draw. In other words, all of the required graphics are produced by only two statements in the program code. (One additional statement is required if you want to display the small circles that represent the locations of the prey fish.)

If those two statements are removed, the program will still compile and run, and the game can still be played. However, without visual feedback, the game isn’t much fun. The lack of visual feedback eliminates the strategy aspect of the game, causing it to be solely a game of chance. Be that as it may, with or without visual feedback, the player can still click the Start button and then repetitively click the Attack button until the display in the bottom-right of Figure 1 shows that all of the prey fish have been eaten.

Why am I telling you this?

I’m telling you this to emphasize that the essential skills required to program this game (and probably most games for that matter) consist of skills in mathematics, programming logic, and several other technical areas. The ability to produce on-screen graphics is necessary for an enjoyable game, but, (given a good game-math library that supports graphics), producing on-screen graphics is almost a trivial part of the programming effort. In this series of lessons, you need to be mainly concentrating on learning mathematics and programming logic and treating the production of on-screen graphics almost as an afterthought.

The program named GM01test04

This animation program is designed to exercise many of the 2D features of the GM01 game-math library. The animation is generally based on the idea of a flocking behavior similar to that exhibited by birds and fish. A set of GM01.Point2D objects is created with random locations to act as predators. An additional GM01.Point2D object is also created to play the part of a prey object.

The prey object is drawn in red while the predators are drawn in black as shown in Figure 2. An algorithm is executed that attempts to cause the predators to chase the prey object without colliding with one another.

Even though the algorithm causes the predators to chase the prey object, it also tries to keep the predators from colliding with the prey object.

The user input GUI

A GUI is provided that contains an input text field for the number of predators plus a Start button and a Stop button. The GUI also contains check boxes that allow the user to elect to display points only, direction vectors only, or both. (Both the point and the direction vector is always displayed for the prey object.)

The user specifies the number of randomly-placed predators and clicks the Start button, at which time the animation begins and the predators start chasing the prey object. Prey-object motion is random.

The animation continues until the user clicks the Stop button. The user can click the Stop button, change any of the input parameters, and then click the Start button again to re-start the animation with different parameters such as the number of predator objects.

Swimming in formation

An unexpected result is that the algorithm seems to cause the predators to come together and swim in formation while chasing the prey object. The most common formation is hexagonal as shown in Figure 9, which shows 12 predators swimming in a hexagonal formation.

Figure 9. Twelve predators swimming in a hexagon formation in GM01test04.

Other formations appear as well

Some triangles, diamonds, and incomplete hexagons also appear. For example, Figure 10 shows six predators swimming in a diamond formation.

Figure 10. Six predators swimming in a diamond formation.

No explanation for this behavior

I haven’t given the matter a lot of thought, but at this point, I have no explanation for this behavior. Note that the tendency to swim in formation is more visually obvious when only the points are displayed. When the vectors are displayed, it is more difficult to pick out the formation.

Dogged determination

On the other hand, the animation is most impressive when the direction vectors are displayed, with or without points, because the vectors illustrate the dogged determination and undying focus that the predators maintain while chasing the prey object.

Won’t explain the code

Once you understand the code in the program named GM01test08 that I explained earlier in this lesson, you should have no difficulty understanding the code in this program. Therefore, I won’t explain the code in this program. I included this program in this lesson mainly to illustrate the differences between 2D and 3D from both a visual and programming viewpoint.

A complete listing of this program is provided in Listing 32 near the end of the lesson.

The program named GM01test03

This is a 3D update of the 2D program named GM01test04 discussed above.

A comparison of programming requirements

Basically all that was required to perform the update was to specify 3D classes from the game-math library in place of the 2D classes used in the 2D version of the program. In some cases, this in turn required that argument lists for constructors and methods be expanded from two dimensions to three dimensions. Just about everything else took care of itself.

A comparison of these two programs illustrates the value of the game-math library named GM01 and the ease with which you can switch back and forth between 2D and 3D programming when using the library.

A comparison of visual behavior

The visual behavior of this 3D version, as shown in Figure 3, is more realistic than the 2D version. This is particularly true when the prey object gets in the middle of the predators and the display is showing vectors. In the 2D version, a predator is constrained to swing around only in the plane of the screen.; However, in this 3D version, a predator is not subject to that constraint and is free to swing around in the most appropriate way as the prey object passes by.

This constraint causes the motion in the 2D version to be less fluid than the motion in the 3D version. This can best be demonstrated with only one predator because that makes it easy to see the behavior of an individual predator as the animation is running.

No swimming in formation

One very interesting thing that I have noticed is that unlike the 2D version, the predators in this 3D version don’t seem to have a tendency to form up and swim in formation while chasing the prey object. This may be because they have more options in terms of avoiding collisions while giving chase. However, that is pure speculation on my part since I don’t know why the predators tend to swim in formation in the 2D version anyway.

Won’t explain the code

As is the case with the earlier program named GM01test04, once you understand the code in the program named GM01test08, you should have no difficulty understanding the code in this program. Therefore, I won’t explain the code in this program. I included this program and the earlier 2D version in this lesson mainly to illustrate the differences between 2D and 3D from both a visual viewpoint and programming viewpoint.

A complete listing of this program is provided in Listing 33 near the end of the lesson.

The program named StringArt04

This program animates the behavior of the earlier program named
StringArt03
that I explained in an earlier lesson (see Resources). See the comments at the beginning of that program for a description of both programs.

The only significant difference in the behavior of the two programs is that this program slows the rotation process down and animates it so that the user can see it happening in slow motion. Of course, quite a few changes were required to convert the program from a static program to an animated program.

However, if you understand the code in the earlier program named StringArt03 and you understand the code in the program named GM01test08 that I explained earlier in this lesson, you should have no difficulty understanding the code in the program named StringArt04. Therefore, I won’t explain the code in this program. A screen shot of the program in action is shown in Figure 4. A complete listing of the program is provided in Listing 34.

Run the programs

I encourage you to copy the code from Listing 30 through Listing 34, compile the code, and execute the programs in conjunction with the game-math library named GM01.  Experiment with the code, making changes, and observing the results of your changes. Make certain that you can explain why your changes behave as they do.

Summary

In this lesson, you learned how to write your first interactive 3D game using the game-math library named GM01. You also learned how to write a Java program that simulates flocking behavior such as that exhibited by birds and fish and you learned how to incorporate that behavior into the game. Finally, you examined three other programs that illustrate various aspects of both 2D and 3D animation using the game-math library.

What’s next?

In the next lesson, you will learn the fundamentals of the vector dot product in both 2D and 3D. You will learn how to update the game-math library to support various aspects of the vector dot product, and you will learn how to write 2D and 3D programs that use the vector dot product methods in the game-math library.

Resources

Complete program listings

Complete listings of the programs discussed in this lesson are shown in Listing 30 through Listing 34 below.

Listing 30. Source code for the game-math library named GM01.

/*GM01.java 
Copyright 2008, R.G.Baldwin
Revised 02/24/08

This is a major upgrade to the game-math library. This
version upgrades the version named GM2D04 to a new version
named simply GM01.

The primary purpose of the upgrade was to add 3D 
capability for all of the 2D features provided by the
previous version. Because both 2D and 3D capabilities
are included, it is no longer necessary to differentiate
between the two in the name of the class. Therefore, this
version is named GM01.

Adding 3D capability entailed major complexity in one
particular area: drawing the objects. It is difficult to
draw a 3D object on a 2D screen. This requires a 
projection process to project each point in the 3D object
onto the correct location on a 2D plane. There are a 
variety of ways to do this. This 3D library uses an
approach often referred to as an oblique parallel
projection. See the following URL for technical 
information on the projection process:

http://local.wasp.uwa.edu.au/~pbourke/geometry/
classification/


In addition to adding 3D capability, this version also
eliminates the confusion surrounding the fact that the
default direction of the positive y-axis is going down
the screen instead of up the screen as viewers have become
accustomed to. When you use this library, you can program
under the assumption that the positive direction of the
y-axis is up the screen, provided you funnel all of your
drawing tasks through the library and don't draw
directly on the screen.

The name GMnn is an abbreviation for GameMathnn.

See the file named GM2D01.java for a general description 
of the game-math library. The library has been updated
several timews. This file is an update of GM2D04.

In addition to the updates mentioned above, this update
cleaned up some lingering areas of code inefficiency,
using the simplest available method to draw on an 
off-screen image. In addition, the following new methods 
were added:

The following methods are new static methods of the class
named GM01. The first method in the list deals with the
problem of displaying a 3D image on a 3D screen.

The last five methods in the list wrap the standard 
graphics methods for the purpose of eliminating the issue 
of the direction of the positive Y-axis.

GM01.convert3Dto2D
GM01.translate
GM01.drawLine
GM01.fillOval
GM01.drawOval
GM01.fillRect

The following methods are new instance methods of the 
indicated static top-level classes belonging to the class
named GM01.

GM01.Vector2D.scale
GM01.Vector2D.negate
GM01.Point2D.clone
GM01.Vector2D.normalize
GM01.Point2D.rotate
GM01.Point2D.scale

GM01.Vector3D.scale
GM01.Vector3D.negate
GM01.Point3D.clone
GM01.Vector3D.normalize
GM01.Point3D.rotate
GM01.Point3D.scale

Tested using JDK 1.6 under WinXP.
*********************************************************/
import java.awt.geom.*;
import java.awt.*;

public class GM01{
  //----------------------------------------------------//
  
  //This method converts a ColMatrix3D object to a
  // ColMatrix2D object. The purpose is to accept
  // x, y, and z coordinate values and transform those
  // values into a pair of coordinate values suitable for
  // display in two dimensions.
  //See http://local.wasp.uwa.edu.au/~pbourke/geometry/
  // classification/ for technical background on the
  // transform from 3D to 2D.
  //The transform equations are:
  // x2d = x3d + z3d * cos(theta)/tan(alpha)
  // y2d = y3d + z3d * sin(theta)/tan(alpha);
  //Let theta = 30 degrees and alpha = 45 degrees
  //Then:cos(theta) = 0.866
  //     sin(theta) = 0.5
  //     tan(alpha) = 1;
  //Note that the signs in the above equations depend
  // on the assumed directions of the angles as well as
  // the assumed positive directions of the axes. The
  // signs used in this method assume the following:
  //   Positive x is to the right.
  //   Positive y is up the screen.
  //   Positive z is protruding out the front of the
  //     screen.
  //   The viewing position is above the x axis and to the
  //     right of the z-y plane.
  public static GM01.ColMatrix2D convert3Dto2D(
                                   GM01.ColMatrix3D data){
    return new GM01.ColMatrix2D(
                  data.getData(0) - 0.866*data.getData(2),
                  data.getData(1) - 0.50*data.getData(2));
  }//end convert3Dto2D 
  //----------------------------------------------------//

  //This method wraps around the translate method of the
  // Graphics2D class. The purpose is to cause the
  // positive direction for the y-axis to be up the screen
  // instead of down the screen. When you use this method,
  // you should program as though the positive direction
  // for the y-axis is up.
  public static void translate(Graphics2D g2D,
                               double xOffset,
                               double yOffset){
    //Flip the sign on the y-coordinate to change the
    // direction of the positive y-axis to go up the
    // screen.
    g2D.translate(xOffset,-yOffset);
  }//end translate
  //----------------------------------------------------//

  //This method wraps around the drawLine method of the
  // Graphics class. The purpose is to cause the positive
  // direction for the y-axis to be up the screen instead
  // of down the screen. When you use this method, you
  // should program as though the positive direction for
  // the y-axis is up.
  public static void drawLine(Graphics2D g2D,
                              double x1,
                              double y1,
                              double x2,
                              double y2){
    //Flip the sign on the y-coordinate value.
    g2D.drawLine((int)x1,-(int)y1,(int)x2,-(int)y2);
  }//end drawLine
  //----------------------------------------------------//

  //This method wraps around the fillOval method of the
  // Graphics class. The purpose is to cause the positive
  // direction for the y-axis to be up the screen instead
  // of down the screen. When you use this method, you
  // should program as though the positive direction for
  // the y-axis is up.
  public static void fillOval(Graphics2D g2D,
                              double x,
                              double y,
                              double width,
                              double height){
    //Flip the sign on the y-coordinate value.
    g2D.fillOval((int)x,-(int)y,(int)width,(int)height);
  }//end fillOval
  //----------------------------------------------------//

  //This method wraps around the drawOval method of the
  // Graphics class. The purpose is to cause the positive
  // direction for the y-axis to be up the screen instead
  // of down the screen. When you use this method, you
  // should program as though the positive direction for
  // the y-axis is up.
  public static void drawOval(Graphics2D g2D,
                              double x,
                              double y,
                              double width,
                              double height){
    //Flip the sign on the y-coordinate value.
    g2D.drawOval((int)x,-(int)y,(int)width,(int)height);
  }//end drawOval
  //----------------------------------------------------//

  //This method wraps around the fillRect method of the
  // Graphics class. The purpose is to cause the positive
  // direction for the y-axis to be up the screen instead
  // of down the screen. When you use this method, you
  // should program as though the positive direction for
  // the y-axis is up.
  public static void fillRect(Graphics2D g2D,
                              double x,
                              double y,
                              double width,
                              double height){
    //Flip the sign on the y-coordinate value.
    g2D.fillRect((int)x,-(int)y,(int)width,(int)height);
  }//end fillRect
  //----------------------------------------------------//



  //An object of this class represents a 2D column matrix.
  // An object of this class is the fundamental building
  // block for several of the other classes in the
  // library.
  public static class ColMatrix2D{
    double[] data = new double[2];

    public ColMatrix2D(double data0,double data1){
      data[0] = data0;
      data[1] = data1;
    }//end constructor
    //--------------------------------------------------//

    //Overridden toString method.
    public String toString(){
      return data[0] + "," + data[1];
    }//end overridden toString method
    //--------------------------------------------------//

    public double getData(int index){
      if((index < 0) || (index > 1)){ 
        throw new IndexOutOfBoundsException();
      }else{
        return data[index];
      }//end else
    }//end getData method
    //--------------------------------------------------//

    public void setData(int index,double data){
      if((index < 0) || (index > 1)){ 
        throw new IndexOutOfBoundsException();
      }else{
        this.data[index] = data;
      }//end else
    }//end setData method
    //--------------------------------------------------//

    //This method overrides the equals method inherited
    // from the class named Object. It compares the values
    // stored in two matrices and returns true if the
    // values are equal or almost equal and returns false
    // otherwise. 
    public boolean equals(Object obj){
      if(obj instanceof GM01.ColMatrix2D &&
         Math.abs(((GM01.ColMatrix2D)obj).getData(0) - 
                                 getData(0)) <= 0.00001 &&
         Math.abs(((GM01.ColMatrix2D)obj).getData(1) - 
                                  getData(1)) <= 0.00001){
        return true;
      }else{
        return false;
      }//end else

    }//end overridden equals method
    //--------------------------------------------------//

    //Adds one ColMatrix2D object to another ColMatrix2D
    // object, returning a ColMatrix2D object.
    public GM01.ColMatrix2D add(GM01.ColMatrix2D matrix){
      return new GM01.ColMatrix2D(
                            getData(0)+matrix.getData(0),
                            getData(1)+matrix.getData(1));
    }//end add
    //--------------------------------------------------//

    //Subtracts one ColMatrix2D object from another
    // ColMatrix2D object, returning a ColMatrix2D object.
    // The object that is received as an incoming
    // parameter  is subtracted from the object on which
    // the method is called.
    public GM01.ColMatrix2D subtract(
                                 GM01.ColMatrix2D matrix){
      return new GM01.ColMatrix2D(
                            getData(0)-matrix.getData(0),
                            getData(1)-matrix.getData(1));
    }//end subtract
    //--------------------------------------------------//
  }//end class ColMatrix2D
  //====================================================//


  //An object of this class represents a 3D column matrix.
  // An object of this class is the fundamental building
  // block for several of the other classes in the
  // library.
  public static class ColMatrix3D{
    double[] data = new double[3];

    public ColMatrix3D(
                  double data0,double data1,double data2){
      data[0] = data0;
      data[1] = data1;
      data[2] = data2;
    }//end constructor
    //--------------------------------------------------//

    public String toString(){
      return data[0] + "," + data[1] + "," + data[2];
    }//end overridden toString method
    //--------------------------------------------------//

    public double getData(int index){
      if((index < 0) || (index > 2)){ 
        throw new IndexOutOfBoundsException();
      }else{
        return data[index];
      }//end else
    }//end getData method
    //--------------------------------------------------//

    public void setData(int index,double data){
      if((index < 0) || (index > 2)){ 
        throw new IndexOutOfBoundsException();
      }else{
        this.data[index] = data;
      }//end else
    }//end setData method
    //--------------------------------------------------//

    //This method overrides the equals method inherited
    // from the class named Object. It compares the values
    // stored in two matrices and returns true if the
    // values are equal or almost equal and returns false
    // otherwise. 
    public boolean equals(Object obj){
      if(obj instanceof GM01.ColMatrix3D &&
         Math.abs(((GM01.ColMatrix3D)obj).getData(0) - 
                                 getData(0)) <= 0.00001 &&
         Math.abs(((GM01.ColMatrix3D)obj).getData(1) - 
                                 getData(1)) <= 0.00001 &&
         Math.abs(((GM01.ColMatrix3D)obj).getData(2) - 
                                  getData(2)) <= 0.00001){
        return true;
      }else{
        return false;
      }//end else
     
    }//end overridden equals method
    //--------------------------------------------------//

    //Adds one ColMatrix3D object to another ColMatrix3D
    // object, returning a ColMatrix3D object.
    public GM01.ColMatrix3D add(GM01.ColMatrix3D matrix){
      return new GM01.ColMatrix3D(
                            getData(0)+matrix.getData(0),
                            getData(1)+matrix.getData(1),
                            getData(2)+matrix.getData(2));
    }//end add
    //--------------------------------------------------//

    //Subtracts one ColMatrix3D object from another
    // ColMatrix3D object, returning a ColMatrix3D object.
    // The object that is received as an incoming
    // parameter is subtracted from the object on which
    // the method is called.
    public GM01.ColMatrix3D subtract(
                                 GM01.ColMatrix3D matrix){
      return new GM01.ColMatrix3D(
                            getData(0)-matrix.getData(0),
                            getData(1)-matrix.getData(1),
                            getData(2)-matrix.getData(2));
    }//end subtract
    //--------------------------------------------------//
  }//end class ColMatrix3D
  //====================================================//
  //====================================================//


  public static class Point2D{
    GM01.ColMatrix2D point;

    public Point2D(GM01.ColMatrix2D point){//constructor
      //Create and save a clone of the ColMatrix2D object
      // used to define the point to prevent the point
      // from being corrupted by a later change in the
      // values stored in the original ColMatrix2D object
      // through use of its set method.
      this.point = new ColMatrix2D(
                       point.getData(0),point.getData(1));
    }//end constructor
    //--------------------------------------------------//

    public String toString(){
      return point.getData(0) + "," + point.getData(1);
    }//end toString
    //--------------------------------------------------//

    public double getData(int index){
      if((index < 0) || (index > 1)){ 
        throw new IndexOutOfBoundsException();
      }else{
        return point.getData(index);
      }//end else
    }//end getData
    //--------------------------------------------------//

    public void setData(int index,double data){
      if((index < 0) || (index > 1)){ 
        throw new IndexOutOfBoundsException();
      }else{
        point.setData(index,data);
      }//end else
    }//end setData
    //--------------------------------------------------//

    //This method draws a small circle around the location
    // of the point on the specified graphics context.
    public void draw(Graphics2D g2D){
      drawOval(g2D,getData(0)-3,
                   getData(1)+3,6,6);
    }//end draw

    //--------------------------------------------------//

    //Returns a reference to the ColMatrix2D object that
    // defines this Point2D object.
    public GM01.ColMatrix2D getColMatrix(){
      return point;
    }//end getColMatrix
    //--------------------------------------------------//

    //This method overrides the equals method inherited
    // from the class named Object. It compares the values
    // stored in the ColMatrix2D objects that define two
    // Point2D objects and returns true if they are equal
    // and false otherwise. 
    public boolean equals(Object obj){
      if(point.equals(((GM01.Point2D)obj).
                                         getColMatrix())){
        return true;
      }else{
        return false;
      }//end else

    }//end overridden equals method
    //--------------------------------------------------//

    //Gets a displacement vector from one Point2D object
    // to a second Point2D object. The vector points from
    // the object on which the method is called to the
    // object passed as a parameter to the method. Kjell
    // describes this as the distance you would have to
    // walk along the x and then the y axes to get from
    // the first point to the second point.
    public GM01.Vector2D getDisplacementVector(
                                      GM01.Point2D point){
      return new GM01.Vector2D(new GM01.ColMatrix2D(
                            point.getData(0)-getData(0),
                            point.getData(1)-getData(1)));
    }//end getDisplacementVector
    //--------------------------------------------------//

    //Adds a Vector2D to a Point2D producing a
    // new Point2D.
    public GM01.Point2D addVectorToPoint(
                                      GM01.Vector2D vec){
      return new GM01.Point2D(new GM01.ColMatrix2D(
                          getData(0) + vec.getData(0),
                          getData(1) + vec.getData(1)));
    }//end addVectorToPoint
    //--------------------------------------------------//

    //Returns a new Point2D object that is a clone of
    // the object on which the method is called.
    public Point2D clone(){
      return new Point2D(
                  new ColMatrix2D(getData(0),getData(1)));
    }//end clone
    //--------------------------------------------------//

    //The purpose of this method is to rotate a point
    // around a specified anchor point in the x-y plane.
    //The rotation angle is passed in as a double value
    // in degrees with the positive angle of rotation
    // being counter-clockwise.
    //This method does not modify the contents of the
    // Point2D object on which the method is called.
    // Rather, it uses the contents of that object to
    // instantiate, rotate, and return a new Point2D
    // object.
    //For simplicity, this method translates the
    // anchorPoint to the origin, rotates around the
    // origin, and then translates back to the
    // anchorPoint.
    /*
    See http://www.ia.hiof.no/~borres/cgraph/math/threed/
    p-threed.html for a definition of the equations
    required to do the rotation.

    x2 = x1*cos - y1*sin
    y2 = x1*sin + y1*cos
    */ 
    public GM01.Point2D rotate(GM01.Point2D anchorPoint,
                               double angle){
      GM01.Point2D newPoint = this.clone();

      double tempX ;
      double tempY;

      //Translate anchorPoint to the origin
      GM01.Vector2D tempVec = 
            new GM01.Vector2D(anchorPoint.getColMatrix());
      newPoint = 
              newPoint.addVectorToPoint(tempVec.negate());

      //Rotate around the origin.
      tempX = newPoint.getData(0);
      tempY = newPoint.getData(1);
      newPoint.setData(//new x coordinate
                      0,
                      tempX*Math.cos(angle*Math.PI/180) -
                      tempY*Math.sin(angle*Math.PI/180));

      newPoint.setData(//new y coordinate
                      1,
                      tempX*Math.sin(angle*Math.PI/180) +
                      tempY*Math.cos(angle*Math.PI/180));

      //Translate back to anchorPoint
      newPoint = newPoint.addVectorToPoint(tempVec);

      return newPoint;

    }//end rotate
    //--------------------------------------------------//

    //Multiplies this point by a scaling matrix received
    // as an incoming parameter and returns the scaled
    // point.
    public GM01.Point2D scale(GM01.ColMatrix2D scale){
      return new GM01.Point2D(new ColMatrix2D(
                          getData(0) * scale.getData(0),
                          getData(1) * scale.getData(1)));
    }//end scale
    //--------------------------------------------------//
  }//end class Point2D
  //====================================================//


  public static class Point3D{
    GM01.ColMatrix3D point;

    public Point3D(GM01.ColMatrix3D point){//constructor
      //Create and save a clone of the ColMatrix3D object
      // used to define the point to prevent the point
      // from being corrupted by a later change in the
      // values stored in the original ColMatrix3D object
      // through use of its set method.
      this.point = 
         new ColMatrix3D(point.getData(0),
                         point.getData(1),
                         point.getData(2));
    }//end constructor
    //--------------------------------------------------//

    public String toString(){
      return point.getData(0) + "," + point.getData(1) 
                                 + "," + point.getData(2);
    }//end toString
    //--------------------------------------------------//

    public double getData(int index){
      if((index < 0) || (index > 2)){ 
        throw new IndexOutOfBoundsException();
      }else{
        return point.getData(index);
      }//end else
    }//end getData
    //--------------------------------------------------//

    public void setData(int index,double data){
      if((index < 0) || (index > 2)){ 
        throw new IndexOutOfBoundsException();
      }else{
        point.setData(index,data);
      }//end else
    }//end setData
    //--------------------------------------------------//

    //This method draws a small circle around the location
    // of the point on the specified graphics context.
    public void draw(Graphics2D g2D){

      //Get 2D projection coordinate values.
      ColMatrix2D temp = convert3Dto2D(point);
      drawOval(g2D,temp.getData(0)-3,
                   temp.getData(1)+3,
                   6,
                   6);
    }//end draw
    //--------------------------------------------------//

    //Returns a reference to the ColMatrix3D object that
    // defines this Point3D object.
    public GM01.ColMatrix3D getColMatrix(){
      return point;
    }//end getColMatrix
    //--------------------------------------------------//

    //This method overrides the equals method inherited
    // from the class named Object. It compares the values
    // stored in the ColMatrix3D objects that define two
    // Point3D objects and returns true if they are equal
    // and false otherwise. 
    public boolean equals(Object obj){
      if(point.equals(((GM01.Point3D)obj).
                                         getColMatrix())){
        return true;
      }else{
        return false;
      }//end else

    }//end overridden equals method
    //--------------------------------------------------//

    //Gets a displacement vector from one Point3D object
    // to a second Point3D object. The vector points from
    // the object on which the method is called to the
    // object passed as a parameter to the method. Kjell
    // describes this as the distance you would have to
    // walk along the x and then the y axes to get from
    // the first point to the second point.
    public GM01.Vector3D getDisplacementVector(
                                      GM01.Point3D point){
      return new GM01.Vector3D(new GM01.ColMatrix3D(
                            point.getData(0)-getData(0),
                            point.getData(1)-getData(1),
                            point.getData(2)-getData(2)));
    }//end getDisplacementVector
    //--------------------------------------------------//

    //Adds a Vector3D to a Point3D producing a
    // new Point3D.
    public GM01.Point3D addVectorToPoint(
                                      GM01.Vector3D vec){
      return new GM01.Point3D(new GM01.ColMatrix3D(
                          getData(0) + vec.getData(0),
                          getData(1) + vec.getData(1),
                          getData(2) + vec.getData(2)));
    }//end addVectorToPoint
    //--------------------------------------------------//

    //Returns a new Point3D object that is a clone of
    // the object on which the method is called.
    public Point3D clone(){
      return new Point3D(new ColMatrix3D(getData(0),
                                         getData(1),
                                         getData(2)));
    }//end clone
    //--------------------------------------------------//

    //The purpose of this method is to rotate a point
    // around a specified anchor point in the following
    // order:
    // Rotate around z - rotation in x-y plane.
    // Rotate around x - rotation in y-z plane.
    // Rotate around y - rotation in x-z plane.
    //The rotation angles are passed in as double values
    // in degrees (based on the right-hand rule) in the
    // order given above, packaged in an object of the
    // class GM01.ColMatrix3D. (Note that in this case,
    // the ColMatrix3D object is simply a convenient
    // container and it has no significance from a matrix
    // viewpoint.)
    //The right-hand rule states that if you point the
    // thumb of your right hand in the positive direction
    // of an axis, the direction of positive rotation
    // around that axis is given by the direction that
    // your fingers will be pointing.
    //This method does not modify the contents of the
    // Point3D object on which the method is called.
    // Rather, it uses the contents of that object to
    // instantiate, rotate, and return a new Point3D
    // object.
    //For simplicity, this method translates the
    // anchorPoint to the origin, rotates around the
    // origin, and then translates back to the
    // anchorPoint.
    /*
    See http://www.ia.hiof.no/~borres/cgraph/math/threed/
    p-threed.html for a definition of the equations
    required to do the rotation.
    z-axis 
    x2 = x1*cos - y1*sin
    y2 = x1*sin + y1*cos

    x-axis
    y2 = y1*cos(v) - z1*sin(v)
    z2 = y1*sin(v) + z1* cos(v) 

    y-axis
    x2 = x1*cos(v) + z1*sin(v)
    z2 = -x1*sin(v) + z1*cos(v)
    */ 
    public GM01.Point3D rotate(GM01.Point3D anchorPoint,
                               GM01.ColMatrix3D angles){
      GM01.Point3D newPoint = this.clone();

      double tempX ;
      double tempY;
      double tempZ;

      //Translate anchorPoint to the origin
      GM01.Vector3D tempVec = 
            new GM01.Vector3D(anchorPoint.getColMatrix());
      newPoint = 
              newPoint.addVectorToPoint(tempVec.negate());

      double zAngle = angles.getData(0);
      double xAngle = angles.getData(1);
      double yAngle = angles.getData(2);

      //Rotate around z-axis
      tempX = newPoint.getData(0);
      tempY = newPoint.getData(1);
      newPoint.setData(//new x coordinate
                      0,
                      tempX*Math.cos(zAngle*Math.PI/180) -
                      tempY*Math.sin(zAngle*Math.PI/180));

      newPoint.setData(//new y coordinate
                      1,
                      tempX*Math.sin(zAngle*Math.PI/180) +
                      tempY*Math.cos(zAngle*Math.PI/180));

      //Rotate around x-axis
      tempY = newPoint.getData(1);
      tempZ = newPoint.getData(2);
      newPoint.setData(//new y coordinate
                      1,
                      tempY*Math.cos(xAngle*Math.PI/180) -
                      tempZ*Math.sin(xAngle*Math.PI/180));

      newPoint.setData(//new z coordinate
                      2,
                      tempY*Math.sin(xAngle*Math.PI/180) +
                      tempZ*Math.cos(xAngle*Math.PI/180));

      //Rotate around y-axis
      tempX = newPoint.getData(0);
      tempZ = newPoint.getData(2);
      newPoint.setData(//new x coordinate
                      0,
                      tempX*Math.cos(yAngle*Math.PI/180) +
                      tempZ*Math.sin(yAngle*Math.PI/180));

      newPoint.setData(//new z coordinate
                     2,
                     -tempX*Math.sin(yAngle*Math.PI/180) +
                     tempZ*Math.cos(yAngle*Math.PI/180));

      //Translate back to anchorPoint
      newPoint = newPoint.addVectorToPoint(tempVec);

      return newPoint;

    }//end rotate
    //--------------------------------------------------//

    //Multiplies this point by a scaling matrix received
    // as an incoming parameter and returns the scaled
    // point.
    public GM01.Point3D scale(GM01.ColMatrix3D scale){
      return new GM01.Point3D(new ColMatrix3D(
                          getData(0) * scale.getData(0),
                          getData(1) * scale.getData(1),
                          getData(2) * scale.getData(2)));
    }//end scale
    //--------------------------------------------------//
  }//end class Point3D
  //====================================================//
  //====================================================//


  public static class Vector2D{
    GM01.ColMatrix2D vector;

    public Vector2D(GM01.ColMatrix2D vector){//constructor
      //Create and save a clone of the ColMatrix2D object
      // used to define the vector to prevent the vector
      // from being corrupted by a later change in the
      // values stored in the original ColVector2D object.
      this.vector = new ColMatrix2D(
                     vector.getData(0),vector.getData(1));
    }//end constructor
    //--------------------------------------------------//

    public String toString(){
      return vector.getData(0) + "," + vector.getData(1);
    }//end toString
    //--------------------------------------------------//

    public double getData(int index){
      if((index < 0) || (index > 1)){
        throw new IndexOutOfBoundsException();
      }else{
        return vector.getData(index);
      }//end else
    }//end getData
    //--------------------------------------------------//

    public void setData(int index,double data){
      if((index < 0) || (index > 1)){ 
        throw new IndexOutOfBoundsException();
      }else{
        vector.setData(index,data);
      }//end else
    }//end setData
    //--------------------------------------------------//

    //This method draws a vector on the specified graphics
    // context, with the tail of the vector located at a
    // specified point, and with a small filled circle at
    // the head.
    public void draw(Graphics2D g2D,GM01.Point2D tail){

      drawLine(g2D,
               tail.getData(0),
               tail.getData(1),
               tail.getData(0)+vector.getData(0),
               tail.getData(1)+vector.getData(1));

      fillOval(g2D,
               tail.getData(0)+vector.getData(0)-3,
               tail.getData(1)+vector.getData(1)+3,
               6,
               6);
    }//end draw
    //--------------------------------------------------//

    //Returns a reference to the ColMatrix2D object that
    // defines this Vector2D object.
    public GM01.ColMatrix2D getColMatrix(){
      return vector;
    }//end getColMatrix
    //--------------------------------------------------//

    //This method overrides the equals method inherited
    // from the class named Object. It compares the values
    // stored in the ColMatrix2D objects that define two
    // Vector2D objects and returns true if they are equal
    // and false otherwise. 
    public boolean equals(Object obj){
      if(vector.equals((
                     (GM01.Vector2D)obj).getColMatrix())){
        return true;
      }else{
        return false;
      }//end else

    }//end overridden equals method
    //--------------------------------------------------//

    //Adds this vector to a vector received as an incoming
    // parameter and returns the sum as a vector.
    public GM01.Vector2D add(GM01.Vector2D vec){
      return new GM01.Vector2D(new ColMatrix2D(
                       vec.getData(0)+vector.getData(0),
                       vec.getData(1)+vector.getData(1)));
    }//end add
    //--------------------------------------------------//

    //Returns the length of a Vector2D object.
    public double getLength(){
      return Math.sqrt(
           getData(0)*getData(0) + getData(1)*getData(1));
    }//end getLength
    //--------------------------------------------------//

    //Multiplies this vector by a scale factor received as
    // an incoming parameter and returns the scaled
    // vector.
    public GM01.Vector2D scale(Double factor){
      return new GM01.Vector2D(new ColMatrix2D(
                                    getData(0) * factor,
                                    getData(1) * factor));
    }//end scale
    //--------------------------------------------------//

    //Changes the sign on each of the vector components
    // and returns the negated vector.
    public GM01.Vector2D negate(){
      return new GM01.Vector2D(new ColMatrix2D(
                                            -getData(0),
                                            -getData(1)));
    }//end negate
    //--------------------------------------------------//

    //Returns a new vector that points in the same
    // direction but has a length of one unit.
    public GM01.Vector2D normalize(){
      double length = getLength();
      return new GM01.Vector2D(new ColMatrix2D(
                                      getData(0)/length,
                                      getData(1)/length));
    }//end normalize
    //--------------------------------------------------//
  }//end class Vector2D
  //====================================================//


  public static class Vector3D{
    GM01.ColMatrix3D vector;
    
    public Vector3D(GM01.ColMatrix3D vector){//constructor
      //Create and save a clone of the ColMatrix3D object
      // used to define the vector to prevent the vector
      // from being corrupted by a later change in the
      // values stored in the original ColMatris3D object.
      this.vector = new ColMatrix3D(vector.getData(0),
                                    vector.getData(1),
                                    vector.getData(2));
    }//end constructor
    //--------------------------------------------------//

    public String toString(){
      return vector.getData(0) + "," + vector.getData(1) 
                                + "," + vector.getData(2);
    }//end toString
    //--------------------------------------------------//

    public double getData(int index){
      if((index < 0) || (index > 2)){
        throw new IndexOutOfBoundsException();
      }else{
        return vector.getData(index);
      }//end else
    }//end getData
    //--------------------------------------------------//

    public void setData(int index,double data){
      if((index < 0) || (index > 2)){ 
        throw new IndexOutOfBoundsException();
      }else{
        vector.setData(index,data);
      }//end else
    }//end setData
    //--------------------------------------------------//

    //This method draws a vector on the specified graphics
    // context, with the tail of the vector located at a
    // specified point, and with a small circle at the
    // head.
    public void draw(Graphics2D g2D,GM01.Point3D tail){

      //Get a 2D projection of the tail
      GM01.ColMatrix2D tail2D = convert3Dto2D(tail.point);

      //Get the 3D location of the head
      GM01.ColMatrix3D head = 
                      tail.point.add(this.getColMatrix());

      //Get a 2D projection of the head
      GM01.ColMatrix2D head2D = convert3Dto2D(head);
      drawLine(g2D,tail2D.getData(0),
                   tail2D.getData(1),
                   head2D.getData(0),
                   head2D.getData(1));

      //Draw a small filled circle to identify the head.
      fillOval(g2D,head2D.getData(0)-3,
                   head2D.getData(1)+3,
                   6,
                   6);

    }//end draw
    //--------------------------------------------------//

    //Returns a reference to the ColMatrix3D object that
    // defines this Vector3D object.
    public GM01.ColMatrix3D getColMatrix(){
      return vector;
    }//end getColMatrix
    //--------------------------------------------------//

    //This method overrides the equals method inherited
    // from the class named Object. It compares the values
    // stored in the ColMatrix3D objects that define two
    // Vector3D objects and returns true if they are equal
    // and false otherwise. 
    public boolean equals(Object obj){
      if(vector.equals((
                     (GM01.Vector3D)obj).getColMatrix())){
        return true;
      }else{
        return false;
      }//end else

    }//end overridden equals method
    //--------------------------------------------------//

    //Adds this vector to a vector received as an incoming
    // parameter and returns the sum as a vector.
    public GM01.Vector3D add(GM01.Vector3D vec){
      return new GM01.Vector3D(new ColMatrix3D(
                       vec.getData(0)+vector.getData(0),
                       vec.getData(1)+vector.getData(1),
                       vec.getData(2)+vector.getData(2)));
    }//end add
    //--------------------------------------------------//

    //Returns the length of a Vector3D object.
    public double getLength(){
      return Math.sqrt(getData(0)*getData(0) + 
                       getData(1)*getData(1) + 
                       getData(2)*getData(2));
    }//end getLength
    //--------------------------------------------------//

    //Multiplies this vector by a scale factor received as
    // an incoming parameter and returns the scaled
    // vector.
    public GM01.Vector3D scale(Double factor){
      return new GM01.Vector3D(new ColMatrix3D(
                                    getData(0) * factor,
                                    getData(1) * factor,
                                    getData(2) * factor));
    }//end scale
    //--------------------------------------------------//

    //Changes the sign on each of the vector components
    // and returns the negated vector.
    public GM01.Vector3D negate(){
      return new GM01.Vector3D(new ColMatrix3D(
                                            -getData(0),
                                            -getData(1),
                                            -getData(2)));
    }//end negate
    //--------------------------------------------------//

    //Returns a new vector that points in the same
    // direction but has a length of one unit.
    public GM01.Vector3D normalize(){
      double length = getLength();
      return new GM01.Vector3D(new ColMatrix3D(
                                      getData(0)/length,
                                      getData(1)/length,
                                      getData(2)/length));
    }//end normalize
    //--------------------------------------------------//
  }//end class Vector3D
  //====================================================//
  //====================================================//


  //A line is defined by two points. One is called the
  // tail and the other is called the head. Note that this
  // class has the same name as one of the classes in
  // the Graphics2D class. Therefore, if the class from
  // the Graphics2D class is used in some future upgrade
  // to this program, it will have to be fully qualified.
  public static class Line2D{
    GM01.Point2D[] line = new GM01.Point2D[2];

    public Line2D(GM01.Point2D tail,GM01.Point2D head){
      //Create and save clones of the points used to
      // define the line to prevent the line from being 
      // corrupted by a later change in the coordinate
      // values of the points.
      this.line[0] = new Point2D(new GM01.ColMatrix2D(
                        tail.getData(0),tail.getData(1)));
      this.line[1] = new Point2D(new GM01.ColMatrix2D(
                        head.getData(0),head.getData(1)));
    }//end constructor
    //--------------------------------------------------//

    public String toString(){
      return "Tail = " + line[0].getData(0) + "," 
             + line[0].getData(1) + "nHead = " 
             + line[1].getData(0) + "," 
             + line[1].getData(1);
    }//end toString
    //--------------------------------------------------//

    public GM01.Point2D getTail(){
      return line[0];
    }//end getTail
    //--------------------------------------------------//

    public GM01.Point2D getHead(){
      return line[1];
    }//end getHead
    //--------------------------------------------------//

    public void setTail(GM01.Point2D newPoint){
      //Create and save a clone of the new point to
      // prevent the line from being corrupted by a
      // later change in the coordinate values of the
      // point.
      this.line[0] = new Point2D(new GM01.ColMatrix2D(
                newPoint.getData(0),newPoint.getData(1)));
    }//end setTail
    //--------------------------------------------------//

    public void setHead(GM01.Point2D newPoint){
      //Create and save a clone of the new point to
      // prevent the line from being corrupted by a
      // later change in the coordinate values of the
      // point.
      this.line[1] = new Point2D(new GM01.ColMatrix2D(
                newPoint.getData(0),newPoint.getData(1)));
    }//end setHead
    //--------------------------------------------------//

    public void draw(Graphics2D g2D){
      drawLine(g2D,getTail().getData(0),
                   getTail().getData(1),
                   getHead().getData(0),
                   getHead().getData(1));
    }//end draw
    //--------------------------------------------------//
  }//end class Line2D
  //====================================================//


  //A line is defined by two points. One is called the
  // tail and the other is called the head.
  public static class Line3D{
    GM01.Point3D[] line = new GM01.Point3D[2];

    public Line3D(GM01.Point3D tail,GM01.Point3D head){
      //Create and save clones of the points used to
      // define the line to prevent the line from being 
      // corrupted by a later change in the coordinate
      // values of the points.
      this.line[0] = new Point3D(new GM01.ColMatrix3D(
                                        tail.getData(0),
                                        tail.getData(1),
                                        tail.getData(2)));
      this.line[1] = new Point3D(new GM01.ColMatrix3D(
                                        head.getData(0),
                                        head.getData(1),
                                        head.getData(2)));
    }//end constructor
    //--------------------------------------------------//

    public String toString(){
      return "Tail = " + line[0].getData(0) + "," 
                       + line[0].getData(1)  + "," 
                       + line[0].getData(2) 
                       + "nHead = " 
                       + line[1].getData(0) + "," 
                       + line[1].getData(1) + ","
                       + line[1].getData(2);
    }//end toString
    //--------------------------------------------------//

    public GM01.Point3D getTail(){
      return line[0];
    }//end getTail
    //--------------------------------------------------//

    public GM01.Point3D getHead(){
      return line[1];
    }//end getHead
    //--------------------------------------------------//

    public void setTail(GM01.Point3D newPoint){
      //Create and save a clone of the new point to
      // prevent the line from being corrupted by a
      // later change in the coordinate values of the
      // point.
      this.line[0] = new Point3D(new GM01.ColMatrix3D(
                                    newPoint.getData(0),
                                    newPoint.getData(1),
                                    newPoint.getData(2)));
    }//end setTail
    //--------------------------------------------------//

    public void setHead(GM01.Point3D newPoint){
      //Create and save a clone of the new point to
      // prevent the line from being corrupted by a
      // later change in the coordinate values of the
      // point.
      this.line[1] = new Point3D(new GM01.ColMatrix3D(
                                    newPoint.getData(0),
                                    newPoint.getData(1),
                                    newPoint.getData(2)));
    }//end setHead
    //--------------------------------------------------//

    public void draw(Graphics2D g2D){

      //Get 2D projection coordinates.
      GM01.ColMatrix2D tail = 
                           convert3Dto2D(getTail().point);
      GM01.ColMatrix2D head = 
                           convert3Dto2D(getHead().point);

      drawLine(g2D,tail.getData(0),
                   tail.getData(1),
                   head.getData(0),
                   head.getData(1));
    }//end draw
    //--------------------------------------------------//
  }//end class Line3D
  //====================================================//

}//end class GM01
//======================================================//

 

Listing 31. Source code for the game program named GM01test08.

/*GM01test08.java
Copyright 2008, R.G.Baldwin
Revised 02/28/08.

This is an interactive 3D game program  This game
simulates a school of prey fish being attacked and eaten
by a predator. The objective for the player is to cause
the predator to catch and eat all of the prey fish in a
minimum amount of time.

The strategy is for the player to initiate a series of
attacks by the predator, properly timed so as to minimize
the elapsed time required for the predator to catch and
eat all of the prey fish. The final elapsed time depends
both on player strategy and random chance. In other words,
as is the case in many games, this game is part strategy
and part chance.

The prey fish, (shown in red), swim in a large swirling
cluster. The predator, (shown in blue), swims around the
cluster, darting into the cluster on command to catch and
eat as many prey fish as possible during each attack.

When the predator attacks, the prey fish tend to scatter,
breaking up their tight formation and making it more
difficult for the predator to catch them. However, if the
predator is allowed to swim around them again for a short
period following an attack, they will form back into a
cluster. The attacks should be timed so that the prey fish
are in a reasonably tight cluster at the beginning of each
attack so that multiple prey fish will be caught and
eaten during the attack. However, allowing too much time
between attacks is detrimental to minimizing the total
elapsed time. Thus, the player must make a tradeoff
between elapsed time between attacks and the tightness of
the cluster during the attacks. Another disadvantage of
waiting too long between attacks is explained below.

In addition to some other controls, the GUI provides an
Attack button and an elapsed-time/kill-count indicator.
At any point in time, this indicator displays the elapsed
time in milliseconds when the most recent prey fish was
caught along with the total number of prey fish that have
been caught and eaten. The two values are separated by
a "/" character.

When all of the prey fish have been caught and eaten by
the predator, the elapsed time indicator shows the time
in milliseconds required to catch and eat all of the prey
fish.  It also shows the number of prey fish that were
caught and eaten, which should match one of the player
input values.

Initially, when the player clicks the Start button, the
predator is not in attack mode. The predator swims around
the school of prey fish encouraging them to bunch up
together in a smaller and tighter cluster. This behavior
continues until the player clicks the Attack button, at
which time the predator enters the attack mode and makes
an attack on the cluster.

If the player clicks the Attack button too early, or
doesn't wait long enough between attacks, the prey fish
will be in a loose cluster, and the attack will yield
very few fish.

If the player waits too long to click the Attack button,
or waits too long between attacks, the predator will have
spiraled in so close to the prey fish that they will break
formation and begin to scatter, making it difficult for
the predator to catch a large number of fish during the
attack. This is the other disadvantage of waiting too
long that I mentioned above.

The prey fish have a fairly effective defense mechanism
and can do a reasonably good job of escaping the predator.
The final three or four prey fish are usually the most 
difficult to catch.

When the predator is successful in catching a prey fish,
that fish is removed from the prey-fish population,
causing the prey-fish population to to shrink over time.

Each time the predator is successful in catching a prey
fish, the program emits an audible beep to provide
feedback to the player.
 
Even when the predator is not in the attack mode, its
presence has a significant effect on the behavior of the
school of prey fish. As the predator swims around the
school of prey fish, they tend to bunch up into a smaller
and tighter cluster, but when the predator swims too
close, the prey fish panic and tend to scatter, breaking
up the tight cluster.

In addition to the elapsed time indicator and the Attack
button, the GUI contains an input text field for the
number of prey fish that will be in the population plus a
Start button and a Stop button. The GUI also contains
check boxes that allow the player to display points only,
direction vectors only, or both for the prey fish. (Only
the direction vector is displayed for the predator.)

The player specifies the number of randomly-placed prey
fish that will be in the population and clicks the Start
button to start the animation. At this point, the prey
fish swim in a large swirling cluster and the predator
swims around them encouraging them to form a tighter
cluster  Prey fish motion is random but each fish
generally tends to spiral toward the center of the
cluster.

When the user clicks the Attack button, the behavior is
as described earlier.

The animation continues until the user clicks the Stop
button. The user can click the Stop button, change any of
the parameters, and then click Start again to re-start the
game with zero elapsed time and different parameters.

The animation is most impressive when the direction
vectors are displayed for the prey fish because the
vectors provide a lot of information about how the prey
fish are reacting to the predator.

In addition to the school of prey fish and the predator,
the graphical output also shows a large circle drawn
with broken lines. This circle represents the intersection
of a sphere and the x-y plane.

The purpose of the sphere, which is centered on the
origin, is to provide a soft boundary for keeping the
prey fish and the predator in the 3D playing field. The
prey fish and the predator all have a tendency to remain
inside the sphere, but they may occasionally stray outside
the sphere. If they do, the program code will encourage
them to return to the 3D playing field inside the sphere.

This game is fully 3D. The prey fish and the predator are
free to swim in any direction in 3D space. The tails on
the prey fish and the predator appear longest when they
are swimming parallel to the x-y plane. As they change
their angle relative to the x-y plane, the tails appear
to become shorter and shorter until in some cases, they
appear not to have a tail at all. This is best observed
by clicking the Stop button just as the fish scatter
during an attack and freezing the state of the fish in
the 3D world. If you examine the image at that point, you
are likely to see some fish with shorter tails than other
fish.

Tested using JDK 1.6 under WinXP.
*********************************************************/
import java.awt.*;
import javax.swing.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.util.*;

class GM01test08{
  public static void main(String[] args){
    GUI guiObj = new GUI();
  }//end main
}//end controlling class GM01test08
//======================================================//

class GUI extends JFrame implements ActionListener{

  int hSize = 400;//horizontal size of JFrame.
  int vSize = 450;//vertical size of JFrame
  Image osi;//an off-screen image
  int osiWidth;//off-screen image width
  int osiHeight;//off-screen image height
  MyCanvas myCanvas;//a subclass of Canvas
  Graphics2D g2D;//off-screen graphics context.
  int numberPoints = 0;//can be modified by the user.
  JTextField numberPointsField; //user input field.
  Random random = new Random();//random number generator

  //The following collection is used to store the prey
  // objects.
  ArrayList <GM01.Point3D> preyObjects;

  //The following collection is used to store the vectors
  // for display.
  ArrayList <GM01.Vector3D> displayVectors;

  //The following are used to support user input.
  Checkbox drawPointsBox;//User input field
  Checkbox drawVectorsBox;//User input field.
  JButton attackButton;
  boolean drawPoints = true;
  boolean drawVectors = true;

  //The following JTextField is used to display the
  // elapsed time and the number of prey fish that
  // have been caught and eaten.
  JTextField timer;

  long baseTime;//Used to compute the elapsed time.

  //Used to compute the number of fish caught and eaten.
  int killCount = 0;

  //Animation takes place while the following is true.
  boolean animate = false;

  //Attacks take place while the following is true.
  boolean attack = false;

  //Working variables used by the animation code.
  GM01.Vector3D predatorVec;
  GM01.Point3D preyCenter;
  GM01.Point3D predator;
  //----------------------------------------------------//

  GUI(){//constructor

    //Set JFrame size, title, and close operation.
    setSize(hSize,vSize);
    setTitle("Copyright 2008,R.G.Baldwin");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Instantiate the user input components.
    numberPointsField = new JTextField("100",5);
    JButton startButton = new JButton("Start");
    attackButton = new JButton("Attack");
    JButton stopButton = new JButton("Stop");
    timer = new JTextField("0",5);
    drawPointsBox = new Checkbox("Draw Points",false);
    drawVectorsBox = new Checkbox("Draw Vectors",true);

    //Instantiate a JPanel that will house the user input
    // components and set its layout manager.
    JPanel controlPanel = new JPanel();
    controlPanel.setLayout(new GridLayout(0,2));

    //Add the user input component and appropriate labels
    // to the control panel.
    controlPanel.add(new JLabel(" Number Prey Fish"));
    controlPanel.add(numberPointsField);
    controlPanel.add(drawPointsBox);
    controlPanel.add(drawVectorsBox);
    controlPanel.add(startButton);
    controlPanel.add(attackButton);
    controlPanel.add(stopButton);
    controlPanel.add(timer);

    //Add the control panel to the SOUTH position in the
    // JFrame.
    this.getContentPane().add(
                         BorderLayout.SOUTH,controlPanel);

    //Instantiate a new drawing canvas and add it to the
    // CENTER of the JFrame above the control panel.
    myCanvas = new MyCanvas();
    this.getContentPane().add(
                            BorderLayout.CENTER,myCanvas);

    //This object must be visible before you can get an
    // off-screen image.  It must also be visible before
    // you can compute the size of the canvas.
    setVisible(true);

    //Make the size of the off-screen image match the
    // size of the canvas.
    osiWidth = myCanvas.getWidth();
    osiHeight = myCanvas.getHeight();

    //Create an off-screen image and get a graphics
    // context on it.
    osi = createImage(osiWidth,osiHeight);
    g2D = (Graphics2D)(osi.getGraphics());

    //Translate the origin to the center.
    GM01.translate(g2D,0.5*osiWidth,-0.5*osiHeight);

    //Register this object as an action listener on all
    // three buttons.
    startButton.addActionListener(this);
    attackButton.addActionListener(this);
    stopButton.addActionListener(this);

    //Make the drawing color RED at startup.
    g2D.setColor(Color.RED);

  }//end constructor
  //----------------------------------------------------//

  //This method is called to respond to a click on any
  // of the three buttons.
  public void actionPerformed(ActionEvent e){

    if(e.getActionCommand().equals("Start") && !animate){
      //Service the Start button.

      //Get several user input values.
      numberPoints = Integer.parseInt(
                             numberPointsField.getText());

      if(drawPointsBox.getState()){
        drawPoints = true;
      }else{
        drawPoints = false;
      }//end else

      if(drawVectorsBox.getState()){
        drawVectors = true;
      }else{
        drawVectors = false;
      }//end else

      //Initialize some working variables used in the
      // animation process.
      animate = true;
      baseTime = new Date().getTime();
      killCount = 0;

      //Enable the Attack button.
      attackButton.setEnabled(true);

      //Initialize the text in the timer field.
      timer.setText("0 / 0");

      //Cause the run method belonging to the animation
      // thread object to be executed.
      new Animate().start();
    }//end if


    if(e.getActionCommand().equals("Attack") && animate){
      //Service the Attack button.

      //This will cause the predator to attack the prey
      // objects.
      attack = true;

      //Point the predator toward the center of the prey
      // fish formation. Reduce the scale factor to less
      // than 1.0 to slow down the attack and make it
      // easier for the prey fish to escape the predator.
      predatorVec = 
               predator.getDisplacementVector(preyCenter).
                                               scale(1.0);

      //Disable the Attack button. It will be enabled
      // again when the attack is finished.
      attackButton.setEnabled(false);
    }//end if


    if(e.getActionCommand().equals("Stop") && animate){
      //Service the Stop button.

      //This will cause the run method to terminate and
      // stop the animation. It will also clear the
      // attack flag so that the next time the animation
      // is started, the predator won't be in the 
      // attack mode.
      animate = false;
      attack = false;
    }//end if

  }//end actionPerformed
  //====================================================//

  //This is an inner class of the GUI class.
  class MyCanvas extends Canvas{
    //Override the update method to eliminate the default
    // clearing of the Canvas in order to reduce or
    // eliminate the flashing that that is often caused by
    // such default clearing.
    //In this case, it isn't necessary to clear the canvas
    // because the off-screen image is cleared each time
    // it is updated. This method will be called when the
    // JFrame and the Canvas appear on the screen or when
    // the repaint method is called on the Canvas object.
    public void update(Graphics g){
      paint(g);//Call the overridden paint method.
    }//end overridden update()

    //Override the paint() method. The purpose of the
    // paint method is to display the off-screen image on
    // the screen. This method is called by the update
    // method above.
    public void paint(Graphics g){
      g.drawImage(osi,0,0,this);
    }//end overridden paint()

  }//end inner class MyCanvas
  //====================================================//

  //This is an animation thread. The purpose of this
  // animation is to simulate something like a school of
  // prey fish and a predator. The animation behavior
  // is described in the opening comments for the program
  // as a whole.
  class Animate extends Thread{
    //Declare a general purpose variable of type Point3D.
    // It will be used for a variety of purposes.
    GM01.Point3D tempPrey;

    //Declare two general purpose variables of type
    // Vector3D. They will be used for a variety of
    // purposes.
    GM01.Vector3D tempVectorA;
    GM01.Vector3D tempVectorB;
    //--------------------------------------------------//

    public void run(){
      //This method is executed when start is called on
      // this Thread object.
      //Create a new empty container for the prey objects.
      //Note the use of "Generics" syntax.
      preyObjects = new ArrayList<GM01.Point3D>();

      //Create a new empty container for the vectors. The
      // only reason the vectors are saved is so that they
      // can be displayed later
      displayVectors = new ArrayList<GM01.Vector3D>();

      //Create a set of prey objects at random locations
      // and store references to the prey objects in the
      // preyObjects container.
      for(int cnt = 0;cnt < numberPoints;cnt++){
        preyObjects.add(new GM01.Point3D(
          new GM01.ColMatrix3D(
                         100*(random.nextDouble()-0.5),
                         100*(random.nextDouble()-0.5),
                         100*(random.nextDouble()-0.5))));

        //Populate the displayVectors collection with
        // dummy vectors.
        displayVectors.add(tempVectorA);
      }//end for loop

      //This object that will be the predator.
      // Position it initially near the top of the screen.
      predator = new GM01.Point3D(
                     new GM01.ColMatrix3D(-100,100,-100));

      //Create a reference point to mark the origin. Among
      // other things, it will be used as the center of a
      // sphere, which in turn will be used to attempt to
      // keep the prey and predator objects from leaving
      // the playing field.
      GM01.Point3D origin = 
            new GM01.Point3D(new GM01.ColMatrix3D(0,0,0));

      //Declare some variables that will be used to
      // compute and save the average position of all of
      // the prey objects.
      double xSum = 0;
      double ySum = 0;
      double zSum = 0;


      //This is the animation loop. The value of animate
      // is set to true by the Start button and is set to
      // false by the Stop button. Setting it to false
      // will cause the run method to terminate and stop
      // the animation.
      while(animate){
        //Compute and save the average position of all the
        // prey objects at the beginning of the loop. Save
        // the average position in the Point3D object
        // referred to by preyCenter.
        xSum = 0;
        ySum = 0;
        zSum = 0;

        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          tempPrey = preyObjects.get(cnt);
          xSum += tempPrey.getData(0);
          ySum += tempPrey.getData(1);
          zSum += tempPrey.getData(2);
        }//end for loop

        //Construct a reference point at the average
        // position of all the prey objects.
        preyCenter = new GM01.Point3D(
           new GM01.ColMatrix3D(xSum/preyObjects.size(),
                                ySum/preyObjects.size(),
                                zSum/preyObjects.size()));


        //Move all of the prey objects in a way that
        // causes them to spiral toward the preyCenter.
        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          //Stop spiraling toward the center when the
          // population of prey objects contains a
          // single object because there is no center at
          // that point.
          if(preyObjects.size() > 1){
            //Get the next prey object
            tempPrey = preyObjects.get(cnt);
            //Find the displacement vector from this prey
            // object to the preyCenter
            tempVectorA = tempPrey.getDisplacementVector(
                                              preyCenter);

            //Create a vector that is rotated relative to
            // tempVectorA. Note how each component in
            // this new vector is set equal to a different
            // component in tempVectorA. Scale it.
            tempVectorB = new GM01.Vector3D(
              new GM01.ColMatrix3D(
                      tempVectorA.getData(1),
                      tempVectorA.getData(2),
                      tempVectorA.getData(0))).scale(1.2);

            //Add the two vectors and move the prey object
            // according to the sum of the vectors. Scale
            // one of the vectors during the addition to
            // adjust the rate at which the object spirals
            // toward the center.
            //Moving the prey object in the direction of
            // the sum of these two vectors produces a
            // motion that causes the prey object to
            // spiral toward the preyCenter instead of
            // simply moving in a straight line toward the
            // preyCenter.
            tempVectorA = 
                  tempVectorA.scale(0.9).add(tempVectorB);
            //Apply an overall scale factor to the sum
            // vector before using it to move the prey
            // object.
            tempPrey = tempPrey.addVectorToPoint(
                                  tempVectorA.scale(0.1));

            //Save a clone of the prey object in its new
            // position. Save a clone instead of the
            // actual object as a safety measure to avoid
            // the possibility of corrupting the object
            // later when the reference variable is used
            // for some other purpose.
            preyObjects.set(cnt,tempPrey.clone());

            //Save a normalized version of the direction
            // vector for drawing later. Set the length
            // of the normalized vector to 15 units.
            displayVectors.set(
                 cnt,tempVectorA.normalize().scale(15.0));
          }else{
            //When the population consists of a single
            // prey object, save a dummy direction vector
            // for drawing later. This is necessary to
            // prevent a null pointer exception when the
            // user specifies only one prey object and
            // then clicks the Start button.
            displayVectors.set(
                   cnt,new GM01.Vector3D(
                          new GM01.ColMatrix3D(10,10,0)));
          }//end else

        }//end loop to spiral prey objects toward center.


        //Try to keep the prey objects from colliding with
        // one another by moving each prey object away
        // from its close neighbors.
        GM01.Point3D refPrey = null;
        GM01.Point3D testPrey= null;
        for(int row = 0;row < preyObjects.size();row++){
          refPrey = preyObjects.get(row);
          //Compare the position of the reference prey
          // object with the positions of each of the
          // other prey objects.
          for(int col = 0;col < preyObjects.size();col++){
            //Get another prey object for  proximity test.
            testPrey = preyObjects.get(col);
            //Don't test a prey object against itself.
            if(col != row){
              //Get the vector from the refPrey object to
              // the testPrey object.
              tempVectorA = refPrey.
                          getDisplacementVector(testPrey);

              //If refPrey is too close to testPrey, move
              // it away from the testPrey object.
              if(tempVectorA.getLength() < 10){
                //Move refPrey away from testPrey by a
                // small amount in the opposite direction.
                refPrey = refPrey.addVectorToPoint(
                         tempVectorA.scale(0.2).negate());

              }//end if on proximity test
            }//end if col != row
          }//end loop on col
          //The refPrey object may or may not have been
          // moved.  Save it anyway.
          preyObjects.set(row,refPrey.clone());
        }//end loop on row


        //Make each prey object react to the predator with
        // a defensive mechanism when the predator
        // approaches the prey object.
        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          tempPrey = preyObjects.get(cnt);

          //Get a displacement vector from the prey object
          // to the predator.
          tempVectorA = tempPrey.getDisplacementVector(
                                                predator);

          //If the prey object is within a certain
          // threshold distance from the predator, move
          // the prey object a random distance in the
          // opposite direction. Then test again to see if
          // the prey object is still within another
          // smaller threshold distance. If so, the prey
          // object was not successful in escaping the
          // predator and has been caught and eaten by the
          // predator. Remove the prey object from the
          // population and sound a beep to notify the
          // user of the successful attack by the
          // predator.
          if(tempVectorA.getLength() < 50){
            //The predator is within striking range of the
            // prey object. Make the prey object try to
            // escape by moving a random distance in the
            // opposite direction. The value returned by
            // the random number generator ranges from 0
            // to 1.0.
            tempVectorA = tempVectorA.negate().scale(
                                     random.nextDouble());
            tempPrey = 
                   tempPrey.addVectorToPoint(tempVectorA);

            //Check to see if the escape was successful
            // by testing against a smaller threshold
            // distance.
            //Get a new displacement vector from the prey
            // object to the predator.
            tempVectorA = 
                 tempPrey.getDisplacementVector(predator);
            //Decrease or increase the following
            // threshold distance to cause the prey
            // object to be more or less successful in the
            // escape attempt.
            if(tempVectorA.getLength() < 25){
              //Escape was not successful. The predator is
              // still within striking distance of the
              // prey object. Remove the prey object and
              // its direction vector from the population
              // and sound a beep. Also increase the
              // killCount by 1 for purposes of display
              // only.
              tempPrey = preyObjects.remove(cnt);
              displayVectors.remove(cnt);
              Toolkit.getDefaultToolkit().beep();
              killCount++;

              //Display the elapsed time in milliseconds
              // since the Start button was clicked along
              // with the number of fish that have been
              // caught and eaten. The value that is
              // displayed is the elapsed time when the
              // most recent prey fish was caught by the
              // predator and is not the total elapsed
              // time since the Start button was clicked.
              // When all prey fish have been caught, the
              // final time that is displayed is the time
              // required for the predator to catch all of
              // the fish in the population.
              timer.setText(
                       "" 
                       + (new Date().getTime() - baseTime)
                       + " / " 
                       + killCount);

            }else{
              //The escape attempt was successful. Restore
              // a clone of the prey object in its new
              // location along with its direction vector
              // to the population.
              preyObjects.set(cnt,tempPrey.clone());
              //Save a normalized version of the direction
              // vector for drawing later.  It will
              // overwrite the previously saved vector and
              // if you watch closely it will show the
              // prey object running away from the
              // predator..
              displayVectors.set(
                 cnt,tempVectorA.normalize().scale(15.0));
            }//end else
          }//end if distance < 50
        }//end for loop on population


        //Deal with prey objects that stray outside the
        // spherical boundary. Give them a nudge back
        // toward the origin.
        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          tempPrey = preyObjects.get(cnt);

          //Test to determine if the prey object is
          // outside of a sphere centered on the origin
          // with a radius equal to the maximum dimension
          // on the vertical axis.
          if(tempPrey.getDisplacementVector(origin).
                             getLength() > 0.5*osiHeight){
            //Enable the following statement to get an
            // audible beep each time a prey object goes
            // out of bounds.
            //Toolkit.getDefaultToolkit().beep();
            //Give the prey object a nudge back toward the
            // origin and save a clone of the prey object
            // in its new location.
            tempVectorA = 
                   tempPrey.getDisplacementVector(origin);
            tempPrey = tempPrey.addVectorToPoint(
                                  tempVectorA.scale(0.1));
            preyObjects.set(cnt,tempPrey.clone());
          }//end if prey object is out of bounds
        }//end for loop to process each prey object


        //Erase the screen
        g2D.setColor(Color.WHITE);
        GM01.fillRect(g2D,-osiWidth/2,osiHeight/2,
                                      osiWidth,osiHeight);


        //Draw a broken-line circle that represents the
        // intersection of the spherical boundary with the
        // x-y plane. Although the prey objects and the
        // predator can stray outside this sphere, they
        // prefer to be inside the sphere.
        GM01.Point3D tempPointA = new GM01.Point3D(
                   new GM01.ColMatrix3D(osiHeight/2,0,0));
        GM01.Point3D tempPointB;
        //The circle is defined by 39 points around the
        // circumference.
        g2D.setColor(Color.BLACK);
        for(int cnt = 0;cnt < 39;cnt++){
          tempPointB = 
           new GM01.Point3D(new GM01.ColMatrix3D(
           osiHeight/2*Math.cos((cnt*360/39)*Math.PI/180),
           osiHeight/2*Math.sin((cnt*360/39)*Math.PI/180),
           0));

          //Connect every third pair of points with a line
          if(cnt%3 == 0){
            new GM01.Line3D(
                         tempPointA,tempPointB).draw(g2D);
          }//end if
          //Save the old point.
          tempPointA = tempPointB;
        }//end for loop

        //Draw the final line required to close the circle
        new GM01.Line3D(tempPointA,new GM01.Point3D(
                           new GM01.ColMatrix3D(
                             osiHeight/2,0,0))).draw(g2D);


        //Restore the drawing color.
        g2D.setColor(Color.RED);
        //Draw the objects in the preyObjects container
        // and the vectors in the displayVectors
        // container.
        for(int cnt = 0;cnt < preyObjects.size();cnt++){
          tempPrey = preyObjects.get(cnt);
          if(drawPoints){
            tempPrey.draw(g2D);//draw circle around point
          }//end if

          if(drawVectors){
            //Draw the vector with its tail at the point.
            displayVectors.get(cnt).draw(g2D,tempPrey);
          }//end if
        }//end for loop


        //When the predator is not in attack mode, cause
        // it to slowly circle the cluster of prey
        // objects.
        if(!attack){
          //Get a displacement vector pointing from the
          // predator to the preyCenter.
          predatorVec = 
               predator.getDisplacementVector(preyCenter);

          //Create a vector that is rotated relative to
          // predatorVec. Note how each component in
          // this new vector is set equal to a different
          // component in predatorVec
          tempVectorB = new GM01.Vector3D(
            new GM01.ColMatrix3D(
                     predatorVec.getData(1),
                     predatorVec.getData(2),
                     predatorVec.getData(0))).scale(0.15);

          //Scale predatorVec and add the two vectors.
          // Then move the predator according to the sum
          // of the vectors.
          //Moving the prey object in the direction of
          // the sum of these two vectors produces a
          // motion that causes the predator to
          // spiral toward the preyCenter instead of
          // simply moving in a straight line toward the
          // preyCenter. The scale factors control the
          // relative motion between the two directions,
          // and are fairly sensitive.
          predatorVec = 
                predatorVec.scale(0.095).add(tempVectorB);

          predator = predator.addVectorToPoint(
                                  predatorVec.scale(1.0));

        }else{//attack is true
          //Predator is in attack mode now. Stay in attack
          // mode using the same displacement vector until
          // the predator traverses the entire playing
          // field. This displacement vector originally
          // pointed toward the preyCenter and was created
          // in the actionPerformed method in response to
          // a click on the Attack button. In other words,
          // even though the prey fish scatter, the
          // predator is constrained to move in a straight
          // line across the playing field once an attack
          // is begun. An interesting update might be to
          // allow the predator to adjust its direction
          // as the prey fish scatter and the location of
          // preyCenter changes during the traversal
          // across the playing field.
          predator = predator.addVectorToPoint(
                                 predatorVec.scale(0.25));

          //Check to see if the predator is outside the
          // spherical boundary that defines the playing
          // field.
          if(predator.getDisplacementVector(origin).
                             getLength() > 0.5*osiHeight){
            //Shift out of attack mode and start circling
            // the prey fish again.
            attack = false;
            attackButton.setEnabled(true);
          }//end if
        }//end else


        //Set the color to BLUE and draw the predator and
        // its displacement vector.
        g2D.setColor(Color.BLUE);
        //Enable the following statement to draw a circle
        // around the point that represents the predator.
        //predator.draw(g2D);

        //Draw the predator's vector.
        predatorVec.normalize().scale(15.0).draw(
                                            g2D,predator);
        g2D.setColor(Color.RED);//restore red color

        //Copy the off-screen image to the canvas and then
        // do it all again.
        myCanvas.repaint();

        //Insert a time delay. Change the sleep time to
        // speed up or slow down the animation. 
        try{
          Thread.currentThread().sleep(166);
        }catch(Exception e){
          e.printStackTrace();
        }//end catch
      }//end animation loop
    }//end run method
  }//end inner class named Animate
  //====================================================//

}//end class GUI
//======================================================//

 

Listing 32. Source code for the program named GM01test04.

/*GM01test04.java
Copyright 2008, R.G.Baldwin
Revised 02/25/08.

This animation program is designed to test many of the 2D
features of the GM01 game-math library.

The animation is generally based on the idea of a
flocking behavior similar to that exhibited by birds.

A set of Point2D objects is created with random locations.
An additional Point2D object known at the target is also
created. The target is drawn in red while the pursuers
are drawn in black.

An algorithm is executed that attempts to cause the points
to chase the target without colliding with one another.
Even though the algorithm causes the points to chase the
target, it also trys to keep the points from colliding 
with the target.

A GUI is provided that contains an input text field for
the number of points plus a Start button and a Stop
button. The GUI also contains check boxes that allow the
user to elect to display points only, direction vectors
only, or both. (The point and the direction vector is
always displayed for the target.)

The user specifies the number of randomly-placed points
and clicks the Start button, at which time the animation
begins and the points start chasing the target.  Target
motion is random. The animation continues until the user
clicks the Stop button. The user can click the Stop
button, change the number of points, and then click Start
again to re-start the animation with a different number
of points chasing the target.

The algorithm seems to cause the points to come together
and fly in formation while chasing the target. The most
common formation is hexagonal but sometimes triangles and
incomplete hexagons also appear. I have no explanation for
this behavior. The tendency to fly in formation is more
visually obvious when only the points are displayed.

On the other hand, the animation is most impressive when
the direction vectors are displayed, with or without
points, because the vectors illustrate the dogged
determination and undying focus that the pursuers maintain
while chasing the target.

Tested using JDK 1.6 under WinXP.
*********************************************************/
import java.awt.*;
import javax.swing.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.util.*;

class GM01test04{
  public static void main(String[] args){
    GUI guiObj = new GUI();
  }//end main
}//end controlling class GM01test04
//======================================================//

class GUI extends JFrame implements ActionListener{

  int hSize = 400;//horizontal size of JFrame.
  int vSize = 400;//vertical size of JFrame
  Image osi;//an off-screen image
  int osiWidth;//off-screen image width
  int osiHeight;//off-screen image height
  MyCanvas myCanvas;//a subclass of Canvas 
  Graphics2D g2D;//off-screen graphics context.
  int numberPoints = 0;//can be modified by the user.
  JTextField numberPointsField; //user input field.
  Random random = new Random();//random number generator

  //The following collection is used to store the points.
  ArrayList <GM01.Point2D> points;

  //The following collection is used to store the vectors
  // for display.
  ArrayList <GM01.Vector2D> vectors;

  //The following are used to support user input.
  Checkbox drawPointsBox;//User input field
  Checkbox drawVectorsBox;//User input field.
  boolean drawPoints = true;
  boolean drawVectors = true;

  //Animation takes place while the following is true.
  boolean animate = false;
  //----------------------------------------------------//

  GUI(){//constructor

    //Set JFrame size, title, and close operation.
    setSize(hSize,vSize);
    setTitle("Copyright 2008,R.G.Baldwin");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Instantiate the user input components.
    numberPointsField =new JTextField("6",5);
    JButton startButton = new JButton("Start");
    JButton stopButton = new JButton("Stop");
    drawPointsBox = new Checkbox("Draw Points",true);
    drawVectorsBox = new Checkbox("Draw Vectors",true);

    //Instantiate a JPanel that will house the user input
    // components and set its layout manager.
    JPanel controlPanel = new JPanel();
    controlPanel.setLayout(new GridLayout(0,2));

    //Add the user input component and appropriate labels
    // to the control panel.
    controlPanel.add(new JLabel(" Number Points"));    
    controlPanel.add(numberPointsField);
    controlPanel.add(drawPointsBox);
    controlPanel.add(drawVectorsBox);
    controlPanel.add(startButton);
    controlPanel.add(stopButton);

    //Add the control panel to the SOUTH position in the
    // JFrame.
    this.getContentPane().add(
                         BorderLayout.SOUTH,controlPanel);

    //Instantiate a new drawing canvas and add it to the
    // CENTER of the JFrame above the control panel.
    myCanvas = new MyCanvas();
    this.getContentPane().add(
                            BorderLayout.CENTER,myCanvas);

    //This object must be visible before you can get an
    // off-screen image.  It must also be visible before
    // you can compute the size of the canvas.
    setVisible(true);

    //Make the size of the off-screen image match the
    // size of the canvas.
    osiWidth = myCanvas.getWidth();
    osiHeight = myCanvas.getHeight();

    //Create an off-screen image and get a graphics
    // context on it.
    osi = createImage(osiWidth,osiHeight);
    g2D = (Graphics2D)(osi.getGraphics());

    //Translate the origin to the center.
    GM01.translate(g2D,0.5*osiWidth,-0.5*osiHeight);

    //Register this object as an action listener on the
    // startButton and the stopButton
    startButton.addActionListener(this);
    stopButton.addActionListener(this);

  }//end constructor
  //----------------------------------------------------//

  //This method is called to respond to a click on the
  // startButton or the stopButton.
  public void actionPerformed(ActionEvent e){

    if(e.getActionCommand().equals("Start") && !animate){
      numberPoints = Integer.parseInt(
                             numberPointsField.getText());

      if(drawPointsBox.getState()){
        drawPoints = true;
      }else{
        drawPoints = false;
      }//end else

      if(drawVectorsBox.getState()){
        drawVectors = true;
      }else{
        drawVectors = false;
      }//end else

      animate = true;
      new Animate().start();
    }//end if

    if(e.getActionCommand().equals("Stop") && animate){
      //This will cause the run method to terminate and
      // stop the animation.
      animate = false;
    }//end if

  }//end actionPerformed
  //====================================================//

  //This is an inner class of the GUI class.
  class MyCanvas extends Canvas{
    //Override the update method to eliminate the default
    // clearing of the Canvas in order to reduce or
    // eliminate the flashing that that is often caused by
    // such default clearing.
    //In this case, it isn't necessary to clear the canvas
    // because the off-screen image is cleared each time
    // it is updated. This method will be called when the
    // JFrame and the Canvas appear on the screen or when
    // the repaint method is called on the Canvas object.
    public void update(Graphics g){
      paint(g);//Call the overridden paint method.
    }//end overridden update()

    //Override the paint() method. The purpose of the
    // paint method is to display the off-screen image on
    // the screen. This method is called by the update
    // method above.
    public void paint(Graphics g){
      g.drawImage(osi,0,0,this);
    }//end overridden paint()

  }//end inner class MyCanvas
  //====================================================//

  //This is an animation thread.
  class Animate extends Thread{
    //Declare a general purpose variable of type Point2D.
    // It will be used for a variety of purposes.
    GM01.Point2D tempPoint;

    //Declare a general putpose variable of type Vector2D.
    // It will be used for a variety of purposes.
    GM01.Vector2D tempVector;

    public void run(){
      //This method is executed when start is called on
      // this thread.
      //Create a new empty container for the points.
      points = new ArrayList<GM01.Point2D>();

      //Create a new empty container for the vectors.
      vectors = new ArrayList<GM01.Vector2D>();

      //Create a set of Point objects at random locations
      // and store references to the points in the
      // ArrayList object..
      for(int cnt = 0;cnt < numberPoints;cnt++){
        points.add(new GM01.Point2D(
          new GM01.ColMatrix2D(
                         100*(random.nextDouble()-0.5),
                         100*(random.nextDouble()-0.5))));

        //Populate vectors collection with dummy vectors.
        vectors.add(tempVector);
      }//end for loop

      //Create a Point2D object that will be the target
      // that will be chased by the other Point2D objects.
      GM01.Point2D target = new GM01.Point2D(
          new GM01.ColMatrix2D(
                          100*(random.nextDouble()-0.5),
                          100*(random.nextDouble()-0.5)));

      //Create a Vector2D object that will be used to
      // control the motion of the target.
      GM01.Vector2D targetVec = new GM01.Vector2D(
             new GM01.ColMatrix2D(
               100*(random.nextDouble()-0.5),
               100*(random.nextDouble()-0.5))).scale(0.3);

      //Create a reference point to mark the origin.
      GM01.Point2D zeroPoint = 
              new GM01.Point2D(new GM01.ColMatrix2D(0,0));

      //Declare a variable that will be used to control
      // the update frequency of the target vector.
      int animationLoopCounter = 0;

      //This is the animation loop. The value of animate
      // is set to true by the Start button and is set to
      // false by the Stop button. Setting it to false
      // will cause the run method to terminate and stop
      // the animation.
      while(animate){
        animationLoopCounter++;

        //Try to keep the points from colliding with one
        // another.  Note, however, that this algorithm
        // is far from perfect in accomplishing that.
        for(int row = 0;row < points.size();row++){
          for(int col = 0;col < points.size();col++){
            GM01.Point2D refPoint = points.get(row);
            GM01.Point2D testPoint = points.get(col);

            if(col != row){
              //Get the distance of the testPoint from the
              // refPoint.
              tempVector = testPoint.
                          getDisplacementVector(refPoint);

              if(tempVector.getLength() < 25){
                //Modify testPoint to move it away from
                // the refPoint and save the modified
                // point.
                tempVector = tempVector.negate();
                tempPoint = testPoint.addVectorToPoint(
                                   tempVector.scale(0.1));
                points.set(col,tempPoint.clone());
              }else{
                //Do nothing.
              }//end else
            }//end if col != row
          }//end loop on col
        }//end loop on row

        //Move all of the points toward the target but try
        // to keep them separted from the target.
        for(int cnt = 0;cnt < points.size();cnt++){
          //Get the next point.
          tempPoint = points.get(cnt);
          //Find the distance from this point to the
          // target.
          tempVector = 
                  tempPoint.getDisplacementVector(target);
          if(tempVector.getLength() < 10){
            //Modify the point to move it away from the
            // target and save the modified point.
            tempVector = tempVector.negate();
            tempPoint = tempPoint.addVectorToPoint(
                                   tempVector.scale(0.1));
            //Save the modified point.
            points.set(cnt,tempPoint.clone());
          }else{
            //Modify the point to move it toward the
            // target and save the modified point.
            tempPoint = tempPoint.addVectorToPoint(
                                   tempVector.scale(0.1));
            points.set(cnt,tempPoint.clone());
          }//end else

          //Save a normalized version of the direction
          // vector for drawing later.
          vectors.set(
                  cnt,tempVector.normalize().scale(15.0));

        }//end for loop

        //Insert a delay.
        try{
          Thread.currentThread().sleep(166);
        }catch(Exception e){
          e.printStackTrace();
        }//end catch

        //Erase the screen
        g2D.setColor(Color.WHITE);
        GM01.fillRect(g2D,-osiWidth/2,osiHeight/2,
                                      osiWidth,osiHeight);
        g2D.setColor(Color.BLACK);

        //Draw the points and the vectors in the arrayList
        // objects.
        for(int cnt = 0;cnt < numberPoints;cnt++){
          tempPoint = points.get(cnt);
          if(drawPoints){
            tempPoint.draw(g2D);
          }//end if

          if(drawVectors){
            tempVector = vectors.get(cnt);
            tempVector.draw(g2D,tempPoint);
          }//end if
        }//end for loop


        //Cause the targets movements to be slightly less
        // random by using the same vector for seveal
        // successive movements
        if(animationLoopCounter%10 == 0){
          //Get a new vector
          targetVec = new GM01.Vector2D(
              new GM01.ColMatrix2D
               (100*(random.nextDouble()-0.5),
               100*(random.nextDouble()-0.5))).scale(0.3);
        }//else use the same vector again.

        //Test to see if this displacement vector will
        // push the target outside the limit. If so,
        // modify the vector to send the target back
        // toward the origin.
        if(target.getDisplacementVector(zeroPoint).
                              getLength() > 0.4*osiWidth){
          targetVec = target.getDisplacementVector(
                                    zeroPoint).scale(0.1);

        }//end if

        //Modify the location of the target point.
        target = target.addVectorToPoint(targetVec);

        //Set the color to RED and draw the target and its
        // vector.
        g2D.setColor(Color.RED);
        target.draw(g2D);
        targetVec.normalize().scale(15.0).draw(
                                              g2D,target);

        g2D.setColor(Color.BLACK);

        myCanvas.repaint();
      }//end while loop
    }//end run
  }//end inner class named Animate
  //====================================================//

}//end class GUI
//======================================================//

 

Listing 33. Source code for the program named GM01test03.

/*GM01test03.java
Copyright 2008, R.G.Baldwin
Revised 02/25/08.

This is a 3D update from the 2D version named GM01test04.
Basically all that was required to perform the update was
to specify 3D classes from the game-math library in place
of the 2D classes used in the 2D version. Virtually 
everything else took care of itself.

The visual behavior of this 3D version is more realistic
than the 2D version. This is particularly true when the
target gets in the middle of the pursuers and the display
is showing both points and vectors. In this 3D version,
a pursuer is free to swing around in any plane as the
target passes by whereas in the 2D version, a pursuer is
constrained to swing around only in the plane of the
screen. That causes the motion to be less fluid. This is
best demonstrated with only one pursuer because that makes
it easy to see the behavior of an individual pursuer.

One thing that I did notice, is that unlike the 2D
version, the pursuers in this 3D version don't seem to
have a tendency to form up and fly in formation while
chasing the target. This may be because they have more
options in terms of avoiding collisions while giving
chase, but that is pure speculation since I don't know
why they tend to fly in formation in the 2D version.

This animation program is designed to test many of the 3D
features of the GM01 game-math library.

The animation is generally based on the idea of a
flocking behavior similar to that exhibited by birds.

A set of Point3D objects is created with random locations.
An additional Point3D object known at the target is also 
created. The target is drawn in red while the pursuers
are drawn in black.

An algorithm is executed that attempts to cause the points
to chase the target without colliding with one another.
Even though the algorithm causes the points to chase the
target, it also trys to keep the points from colliding 
with the target.

A GUI is provided that contains an input text field for
the number of points plus a Start button and a Stop
button. The GUI also contains check boxes that allow the
user to elect to display points only, direction vectors
only, or both. (Both the point and the direction vector
are always displayed for the target.)

The user specifies the number of randomly-placed points
and clicks the Start button, at which time the animation
begins and the points start chasing the target.  Target
motion is random. The animation continues until the user
clicks the Stop button. The user can click the Stop
button, change the number of points, and then click Start
again to re-start the animation with a different number
of points chasing the target.

The animation is most impressive when the direction
vectors are displayed, with or without points, because the
vectors illustrate the dogged determination and undying
focus that the pursuers maintain while chasing the target.

Tested using JDK 1.6 under WinXP.
*********************************************************/
import java.awt.*;
import javax.swing.*;
import java.awt.geom.*;
import java.awt.event.*;
import java.util.*;

class GM01test03{
  public static void main(String[] args){
    GUI guiObj = new GUI();
  }//end main
}//end controlling class GM01test03
//======================================================//

class GUI extends JFrame implements ActionListener{

  int hSize = 400;//horizontal size of JFrame.
  int vSize = 400;//vertical size of JFrame
  Image osi;//an off-screen image
  int osiWidth;//off-screen image width
  int osiHeight;//off-screen image height
  MyCanvas myCanvas;//a subclass of Canvas 
  Graphics2D g2D;//off-screen graphics context.
  int numberPoints = 0;//can be modified by the user.
  JTextField numberPointsField; //user input field.
  Random random = new Random();//random number generator

  //The following collection is used to store the points.
  ArrayList <GM01.Point3D> points;

  //The following collection is used to store the vectors
  // for display.
  ArrayList <GM01.Vector3D> vectors;

  //The following are used to support user input.
  Checkbox drawPointsBox;//User input field
  Checkbox drawVectorsBox;//User input field.
  boolean drawPoints = true;
  boolean drawVectors = true;

  //Animation takes place while the following is true.
  boolean animate = false;
  //----------------------------------------------------//

  GUI(){//constructor

    //Set JFrame size, title, and close operation.
    setSize(hSize,vSize);
    setTitle("Copyright 2008,R.G.Baldwin");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Instantiate the user input components.
    numberPointsField =new JTextField("6",5);
    JButton startButton = new JButton("Start");
    JButton stopButton = new JButton("Stop");
    drawPointsBox = new Checkbox("Draw Points",true);
    drawVectorsBox = new Checkbox("Draw Vectors",true);

    //Instantiate a JPanel that will house the user input
    // components and set its layout manager.
    JPanel controlPanel = new JPanel();
    controlPanel.setLayout(new GridLayout(0,2));

    //Add the user input component and appropriate labels
    // to the control panel.
    controlPanel.add(new JLabel(" Number Points"));
    controlPanel.add(numberPointsField);
    controlPanel.add(drawPointsBox);
    controlPanel.add(drawVectorsBox);
    controlPanel.add(startButton);
    controlPanel.add(stopButton);

    //Add the control panel to the SOUTH position in the
    // JFrame.
    this.getContentPane().add(
                         BorderLayout.SOUTH,controlPanel);

    //Instantiate a new drawing canvas and add it to the
    // CENTER of the JFrame above the control panel.
    myCanvas = new MyCanvas();
    this.getContentPane().add(
                            BorderLayout.CENTER,myCanvas);

    //This object must be visible before you can get an
    // off-screen image.  It must also be visible before
    // you can compute the size of the canvas.
    setVisible(true);

    //Make the size of the off-screen image match the
    // size of the canvas.
    osiWidth = myCanvas.getWidth();
    osiHeight = myCanvas.getHeight();

    //Create an off-screen image and get a graphics
    // context on it.
    osi = createImage(osiWidth,osiHeight);
    g2D = (Graphics2D)(osi.getGraphics());

    //Translate the origin to the center.
    GM01.translate(g2D,0.5*osiWidth,-0.5*osiHeight);

    //Register this object as an action listener on the
    // startButton and the stopButton
    startButton.addActionListener(this);
    stopButton.addActionListener(this);

  }//end constructor
  //----------------------------------------------------//

  //This method is called to respond to a click on the
  // startButton or the stopButton.
  public void actionPerformed(ActionEvent e){

    if(e.getActionCommand().equals("Start") &&!animate){
      numberPoints = Integer.parseInt(
                             numberPointsField.getText());

      if(drawPointsBox.getState()){
        drawPoints = true;
      }else{
        drawPoints = false;
      }//end else

      if(drawVectorsBox.getState()){
        drawVectors = true;
      }else{
        drawVectors = false;
      }//end else

      animate = true;
      new Animate().start();
    }//end if

    if(e.getActionCommand().equals("Stop") && animate){
      //This will cause the run method to terminate and
      // stop the animation.
      animate = false;
    }//end if

  }//end actionPerformed
  //====================================================//

  //This is an inner class of the GUI class.
  class MyCanvas extends Canvas{
    //Override the update method to eliminate the default
    // clearing of the Canvas in order to reduce or
    // eliminate the flashing that that is often caused by
    // such default clearing.
    //In this case, it isn't necessary to clear the canvas
    // because the off-screen image is cleared each time
    // it is updated. This method will be called when the
    // JFrame and the Canvas appear on the screen or when
    // the repaint method is called on the Canvas object.
    public void update(Graphics g){
      paint(g);//Call the overridden paint method.
    }//end overridden update()

    //Override the paint() method. The purpose of the
    // paint method is to display the off-screen image on
    // the screen. This method is called by the update
    // method above.
    public void paint(Graphics g){
      g.drawImage(osi,0,0,this);
    }//end overridden paint()

  }//end inner class MyCanvas
  //====================================================//

  //This is an animation thread.
  class Animate extends Thread{
    //Declare a general purpose variable of type Point3D.
    // It will be used for a variety of purposes.
    GM01.Point3D tempPoint;

    //Declare a general putpose variable of type Vector3D.
    // It will be used for a variety of purposes.
    GM01.Vector3D tempVector;

    public void run(){
      //This method is executed when start is called on
      // this thread.
      //Create a new empty container for the points.
      points = new ArrayList<GM01.Point3D>();

      //Create a new empty container for the vectors.
      vectors = new ArrayList<GM01.Vector3D>();

      //Create a set of Point objects at random locations
      // and store references to the points in the
      // ArrayList object..
      for(int cnt = 0;cnt < numberPoints;cnt++){
        points.add(new GM01.Point3D(
          new GM01.ColMatrix3D(
                         100*(random.nextDouble()-0.5),
                         100*(random.nextDouble()-0.5),
                         100*(random.nextDouble()-0.5))));
                         
        //Populate vectors collection with dummy vectors.
        vectors.add(tempVector);
      }//end for loop

      //Create a Point3D object that will be the target
      // that will be chased by the other Point3D objects.
      GM01.Point3D target = new GM01.Point3D(
          new GM01.ColMatrix3D(
                          100*(random.nextDouble()-0.5),
                          100*(random.nextDouble()-0.5),
                          100*(random.nextDouble()-0.5)));

      //Create a Vector3D object that will be used to
      // control the motion of the target.
      GM01.Vector3D targetVec = new GM01.Vector3D(
             new GM01.ColMatrix3D(
               100*(random.nextDouble()-0.5),
               100*(random.nextDouble()-0.5),
               100*(random.nextDouble()-0.5))).scale(0.3);

      //Create a reference point to mark the origin.
      GM01.Point3D zeroPoint = 
            new GM01.Point3D(new GM01.ColMatrix3D(0,0,0));

      //Declare a variable that will be used to control
      // the update frequency of the target vector.
      int animationLoopCounter = 0;

      //This is the animation loop. The value of animate
      // is set to true by the Start button and is set to
      // false by the Stop button. Setting it to false
      // will cause the run method to terminate and stop
      // the animation.
      while(animate){
        animationLoopCounter++;

        //Try to keep the points from colliding with one
        // another.
        for(int row = 0;row < points.size();row++){
          for(int col = 0;col < points.size();col++){
            GM01.Point3D refPoint = points.get(row);
            GM01.Point3D testPoint = points.get(col);

            if(col != row){
              //Get the distance of the testPoint from the
              // refPoint.
              tempVector = testPoint.
                          getDisplacementVector(refPoint);

              if(tempVector.getLength() < 25){
                //Modify testPoint to move it away from
                // the refPoint and save the modified
                // point.
                tempVector = tempVector.negate();
                tempPoint = testPoint.addVectorToPoint(
                                   tempVector.scale(0.1));
                points.set(col,tempPoint.clone());
              }else{
                //Do nothing.
              }//end else
            }//end if col != row
          }//end loop on col
        }//end loop on row

        //Move all of the points toward the target but try
        // to keep them separted from the target.
        for(int cnt = 0;cnt < points.size();cnt++){
          //Get the next point.
          tempPoint = points.get(cnt);
          //Find the distance from this point to the
          // target.
          tempVector = 
                  tempPoint.getDisplacementVector(target);
          if(tempVector.getLength() < 10){
            //Modify the point to move it away from the
            // target and save the modified point.
            tempVector = tempVector.negate();
            tempPoint = tempPoint.addVectorToPoint(
                                   tempVector.scale(0.1));
            //Save the modified point.
            points.set(cnt,tempPoint.clone());
          }else{
            //Modify the point to move it toward the
            // target and save the modified point.
            tempPoint = tempPoint.addVectorToPoint(
                                   tempVector.scale(0.2));
            points.set(cnt,tempPoint.clone());
          }//end else

          //Save a normalized version of the direction
          // vector for drawing later.
          vectors.set(
                  cnt,tempVector.normalize().scale(15.0));
                  
        }//end for loop

        //Insert a delay.
        try{
          Thread.currentThread().sleep(166);
        }catch(Exception e){
          e.printStackTrace();
        }//end catch

        //Erase the screen
        g2D.setColor(Color.WHITE);
        GM01.fillRect(g2D,-osiWidth/2,osiHeight/2,
                                      osiWidth,osiHeight);
        g2D.setColor(Color.BLACK);

        //Draw the points and the vectors in the arrayList
        // objects.
        for(int cnt = 0;cnt < numberPoints;cnt++){
          tempPoint = points.get(cnt);
          if(drawPoints){
            tempPoint.draw(g2D);
          }//end if

          if(drawVectors){
            tempVector = vectors.get(cnt);
            tempVector.draw(g2D,tempPoint);
          }//end if
        }//end for loop

        //Cause the targets movements to be slightly less
        // random by using the same vector for seveal
        // successive movements
        if(animationLoopCounter%10 == 0){
          //Get a new vector
          targetVec = new GM01.Vector3D(
              new GM01.ColMatrix3D
               (100*(random.nextDouble()-0.5),
               100*(random.nextDouble()-0.5),
               100*(random.nextDouble()-0.5))).scale(0.3);
        }//else use the same vector again.

        //Test to see if this displacement vector will
        // push the target outside the limit. If so,
        // modify the vector to send the target back
        // toward the origin.
        if(target.getDisplacementVector(zeroPoint).
                              getLength() > 0.4*osiWidth){
          targetVec = target.getDisplacementVector(
                                    zeroPoint).scale(0.1);
        }//end if

        //Modify the location of the target point.
        target = target.addVectorToPoint(targetVec);

        //Set the color to RED and draw the target and its
        // displacement vector.
        g2D.setColor(Color.RED);
        target.draw(g2D);
        targetVec.normalize().scale(15.0).draw(
                                              g2D,target);

        g2D.setColor(Color.BLACK);

        myCanvas.repaint();
      }//end while loop
    }//end run
  }//end inner class named Animate
  //====================================================//

}//end class GUI
//======================================================//

 

Listing 34. Source code for the program named StringArt04.

/*StringArt04.java
Copyright 2008, R.G.Baldwin
Revised 03/03/08

This program animates the behavior of the earlier program
named StringArt03.  See the comments at the beginning of
that program for a description of both programs. The only
significant difference in the behavior of the two
programs is that this program slows the rotation process
down so that the user can see it happening in real time.
Of course, quite a few changes were required to convert
the program from a static program to an animated program.

Tested using JDK 1.6 under WinXP.
*********************************************************/
import java.awt.*;
import javax.swing.*;
import java.awt.geom.*;
import java.awt.event.*;

class StringArt04{
  public static void main(String[] args){
    GUI guiObj = new GUI();
  }//end main
}//end controlling class StringArt04
//======================================================//

class GUI extends JFrame implements ActionListener{
  //Specify the horizontal and vertical size of a JFrame
  // object.
  int hSize = 300;
  int vSize = 470;
  Image osi;//an off-screen image
  int osiWidth;//off-screen image width
  int osiHeight;//off-screen image height
  MyCanvas myCanvas;//a subclass of Canvas 
  Graphics2D g2D;//off-screen graphics context.

  JTextField numberPointsField;//User input field.
  JTextField loopsField;//User input field.
  int numberPoints = 6;//Can be modified by the user.
  int loopLim = 1;//Can be modified by the user.

  JTextField zRotationField;//User input field
  JTextField xRotationField;//User input field
  JTextField yRotationField;//User input field
  double zRotation;//Rotation around z in degrees.
  double xRotation;//Rotation around x in degrees.
  double yRotation;//Rotation around y in degrees

  JTextField xAnchorPointField;//User input field
  JTextField yAnchorPointField;//User input field
  JTextField zAnchorPointField;//User input field
  double xAnchorPoint;//Rotation anchor point.
  double yAnchorPoint;//Rotation anchor point.
  double zAnchorPoint;//Rotation anchor point.

  GM01.Point3D anchorPoint;
//  boolean animate = false;

  //The following variable is used to refer to an array
  // object containing the points that define the
  // vertices of a geometric object.
  GM01.Point3D[] points;

  //----------------------------------------------------//

  GUI(){//constructor

    //Set JFrame size, title, and close operation.
    setSize(hSize,vSize);
    setTitle("Copyright 2008,R.G.Baldwin");
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    //Instantiate the user input components.
    numberPointsField =new JTextField("6");
    loopsField = new JTextField("1");
    zRotationField = new JTextField("0");
    xRotationField = new JTextField("0");
    yRotationField = new JTextField("0");
    xAnchorPointField = new JTextField("0");
    yAnchorPointField = new JTextField("0");
    zAnchorPointField = new JTextField("0");
    JButton button = new JButton("Animate");

    //Instantiate a JPanel that will house the user input
    // components and set its layout manager.
    JPanel controlPanel = new JPanel();
    controlPanel.setLayout(new GridLayout(0,2));

    //Add the user input components and appropriate labels
    // to the control panel.
    controlPanel.add(new JLabel(" Number Points"));
    controlPanel.add(numberPointsField);
    controlPanel.add(new JLabel(" Number Loops"));
    controlPanel.add(loopsField);
    controlPanel.add(new JLabel(
                               " Rotate around Z (deg)"));
    controlPanel.add(zRotationField);
    controlPanel.add(new JLabel(
                               " Rotate around X (deg)"));
    controlPanel.add(xRotationField);
    controlPanel.add(new JLabel(
                               " Rotate around Y (deg)"));
    controlPanel.add(yRotationField);
    controlPanel.add(new JLabel(" X anchor point"));
    controlPanel.add(xAnchorPointField);
    controlPanel.add(new JLabel(" Y anchor point"));
    controlPanel.add(yAnchorPointField);
    controlPanel.add(new JLabel(" Z anchor point"));
    controlPanel.add(zAnchorPointField);
    controlPanel.add(button);

    //Add the control panel to the SOUTH position in the
    // JFrame.
    this.getContentPane().add(
                         BorderLayout.SOUTH,controlPanel);

    //Create a new drawing canvas and add it to the
    // CENTER of the JFrame above the control panel.
    myCanvas = new MyCanvas();
    this.getContentPane().add(
                            BorderLayout.CENTER,myCanvas);

    //This object must be visible before you can get an
    // off-screen image.  It must also be visible before
    // you can compute the size of the canvas.
    setVisible(true);

    //Make the size of the off-screen image match the
    // size of the canvas.
    osiWidth = myCanvas.getWidth();
    osiHeight = myCanvas.getHeight();

    //Create an off-screen image and get a graphics
    // context on it.
    osi = createImage(osiWidth,osiHeight);
    g2D = (Graphics2D)(osi.getGraphics());


    //Translate the origin to the center of the off-screen
    // image.
    GM01.translate(g2D,0.5*osiWidth,-0.5*osiHeight);

    //Register this object as an action listener on the
    // button.
    button.addActionListener(this);

    //Cause the overridden paint method belonging to
    // myCanvas to be executed.
    myCanvas.repaint();

  }//end constructor
  //----------------------------------------------------//

  //The purpose of this method is to draw the appropriate
  // material onto the off-screen image.
  void drawOffScreen(Graphics2D g2D){
    //Erase the off-screen image and draw new axes.
    setCoordinateFrame(g2D);

    //The following ColMatrix3D object must be populated
    // with three rotation angles in degrees that
    // specify the following rotational angles in order
    // according to the right-hand rule as applied to
    // the indicated axis.
    // Rotate around z, in x-y plane
    // Rotate around x, in y-z plane
    // Rotate around y, in x-z plane
    GM01.ColMatrix3D angles = new GM01.ColMatrix3D(
                         zRotation,xRotation,yRotation);

    //The following code causes the anchor point to be
    // drawn and the points in the array to be rotated.
    g2D.setColor(Color.BLACK);
    anchorPoint.draw(g2D);
    for(int cnt = 0;cnt < numberPoints;cnt++){
      points[cnt] = points[cnt].rotate(
                                      anchorPoint,angles);
    }//end for loop

    //Implement the algorithm that draws lines connecting
    // points on the geometric object.
    GM01.Line3D line;

    //Begin the outer loop.
    for(int loop = 1;loop <= loopLim;loop++){
      //The following variable specifies the array
      // element containing a point on which a line will
      // start.
      int start = -1;

      //The following variable specifies the number of
      // points that will be skipped between the starting
      // point and the ending point for a line.
      int skip = loop;
      //The following logic causes the element number to
      // wrap around when it reaches the end of the
      // array.
      while(skip >= 2*numberPoints-1){
        skip -= numberPoints;
      }//end while loop

      //The following variable specifies the array
      // element containing a point on which a line will
      // end.
      int end = start + skip;

      //Begin inner loop. This loop actually constructs
      // the GM01.Line3D objects and causes visual
      // manifestations of those objects to be drawn on
      // the off-screen image. Note the requirement to
      // wrap around when the element numbers exceed the
      // length of the array.
      for(int cnt = 0;cnt < numberPoints;cnt++){
        if(start < numberPoints-1){
          start++;
        }else{
          //Wrap around
          start -= (numberPoints-1);
        }//end else

        if(end < numberPoints-1){
          end++;
        }else{
          //Wrap around.
          end -= (numberPoints-1);
        }//end else

        //Create some interesting colors.
        g2D.setColor(new Color(cnt*255/numberPoints,
                               127+cnt*64/numberPoints,
                               255-cnt*255/numberPoints));

        //Create a line that connects points on the
        // geometric object.
        line = new GM01.Line3D(points[start],points[end]);
        line.draw(g2D);
      }//end inner loop
    }//end outer loop
  }//end drawOffScreen
  //----------------------------------------------------//

  //This method is used to draw orthogonal
  // 3D axes on the image that intersect at the origin.
  private void setCoordinateFrame(
                        Graphics2D g2D){

    //Erase the screen
    g2D.setColor(Color.WHITE);
    GM01.fillRect(g2D,-osiWidth/2,osiHeight/2,
                                      osiWidth,osiHeight);

    //Draw x-axis in RED
    g2D.setColor(Color.RED);
    GM01.Point3D pointA = new GM01.Point3D(
                   new GM01.ColMatrix3D(-osiWidth/2,0,0));
    GM01.Point3D pointB = new GM01.Point3D(
                    new GM01.ColMatrix3D(osiWidth/2,0,0));
    new GM01.Line3D(pointA,pointB).draw(g2D);

    //Draw y-axis in GREEN
    g2D.setColor(Color.GREEN);
    pointA = new GM01.Point3D(
                  new GM01.ColMatrix3D(0,-osiHeight/2,0));
    pointB = new GM01.Point3D(
                   new GM01.ColMatrix3D(0,osiHeight/2,0));
    new GM01.Line3D(pointA,pointB).draw(g2D);

    //Draw z-axis in BLUE. Make its length the same as the
    // length of the x-axis.
    g2D.setColor(Color.BLUE);
    pointA = new GM01.Point3D(
                   new GM01.ColMatrix3D(0,0,-osiWidth/2));
    pointB = new GM01.Point3D(
                    new GM01.ColMatrix3D(0,0,osiWidth/2));
    new GM01.Line3D(pointA,pointB).draw(g2D);

  }//end setCoordinateFrame method
  //----------------------------------------------------//

  //This method is called to respond to a click on the
  // button.
  public void actionPerformed(ActionEvent e){
    //Get user input values and use them to modify several
    // values that control the drawing.
    numberPoints = Integer.parseInt(
                             numberPointsField.getText());

    loopLim = Integer.parseInt(loopsField.getText());

    //Rotation around z in degrees.
    zRotation = 
             Double.parseDouble(zRotationField.getText());
    //Rotation around x in degrees.
    xRotation = 
             Double.parseDouble(xRotationField.getText());
    //Rotation around y in degrees
    yRotation = 
             Double.parseDouble(yRotationField.getText());

    //Rotation anchor point values.
    xAnchorPoint = 
          Double.parseDouble(xAnchorPointField.getText());
    yAnchorPoint = 
          Double.parseDouble(yAnchorPointField.getText());
    zAnchorPoint = 
          Double.parseDouble(zAnchorPointField.getText());


    //The following object contains the 3D coordinates
    // of the point around which the rotations will
    // take place.
    anchorPoint = new GM01.Point3D(
             new GM01.ColMatrix3D(
               xAnchorPoint,yAnchorPoint,zAnchorPoint));


    //Instantiate a new array object with a length
    // that matches the new value for numberPoints.
    points = new GM01.Point3D[numberPoints];

    //Create a set of Point3D objects that specify
    // locations on the circumference of a circle that
    // is in the x-y plane with a radius of 50 units. Save
    // references to the Point3D objects in the array.
    for(int cnt = 0;cnt < numberPoints;cnt++){
      points[cnt] = new GM01.Point3D(
        new GM01.ColMatrix3D(
          50*Math.cos((cnt*360/numberPoints)*Math.PI/180),
          50*Math.sin((cnt*360/numberPoints)*Math.PI/180),
          0.0));
    }//end for loop that creates the points

    //Spawn an animation thread
    Animate animate = new Animate();
    animate.start();

  }//end actionPerformed
  //====================================================//


  //This is an inner class of the GUI class.
  class MyCanvas extends Canvas{
    //Override the update method to eliminate the default
    // clearing of the Canvas in order to reduce or
    // eliminate the flashing that that is often caused by
    // such default clearing.
    //In this case, it isn't necessary to clear the canvas
    // because the off-screen image is cleared each time
    // it is updated. This method will be called when the
    // JFrame and the Canvas appear on the screen or when
    // the repaint method is called on the Canvas object.
    public void update(Graphics g){
      paint(g);//Call the overridden paint method.
    }//end overridden update()

    //Override the paint() method. The purpose of the
    // paint method is to display the off-screen image on
    // the screen. This method is called by the update
    // method above.
    public void paint(Graphics g){
      g.drawImage(osi,0,0,this);
    }//end overridden paint()
  }//end inner class MyCanvas
  //====================================================//

  //This is an animation thread.
  class Animate extends Thread{
    public void run(){
      //Compute incremental rotation values.
      int steps = 100;//Number of steps in the animation.
      double zRotationInc = zRotation/steps;
      double xRotationInc = xRotation/steps;
      double yRotationInc = yRotation/steps;

      //Do the animated rotation in three distinct stages,
      // rotating around one axis during each stage.
      for(int axis = 0;axis < 3;axis++){
        for(int cnt = 0;cnt < steps;cnt++){
          //Select the axis about which the image will
          // be rotated during this step..
          if(axis % 3 == 0){
            zRotation = zRotationInc;
            xRotation = 0;
            yRotation = 0;
          }else if(axis % 3 == 1){
            zRotation = 0;
            xRotation = xRotationInc;
            yRotation = 0;
          }else if(axis % 3 == 2){
            zRotation = 0;
            xRotation = 0;
            yRotation = yRotationInc;
          }//end else

          //Draw a new off-screen image based on
          // user input values.
          drawOffScreen(g2D);
          //Copy off-screen image to canvas.
          myCanvas.repaint();

          //Sleep for ten milliseconds.
          try{
            Thread.currentThread().sleep(10);
          }catch(InterruptedException ex){
            ex.printStackTrace();
          }//end catch
        }//end for loop
      }//end for loop on axis
    }//end run
  }//end inner class named Animate
  //====================================================//

}//end class GUI
//======================================================//

 


Copyright

Copyright 2008, Richard G. Baldwin. Reproduction in whole or in part in any form or medium without express written permission from Richard Baldwin is prohibited.

About the author

Richard Baldwin is a college professor (at Austin Community College in Austin, TX) and private consultant whose primary focus is a combination of Java, C#, and XML. In addition to the many platform and/or language independent benefits of Java and C# applications, he believes that a combination of Java, C#, and XML will become the primary driving force in the delivery of structured information on the Web.

Richard has participated in numerous consulting projects and he frequently provides onsite training at the high-tech companies located in and around Austin, Texas. He is the author of Baldwin’s Programming Tutorials, which have gained a worldwide following among experienced and aspiring programmers. He has also published articles in JavaPro magazine.

In addition to his programming expertise, Richard has many years of practical experience in Digital Signal Processing (DSP). His first job after he earned his Bachelor’s degree was doing DSP in the Seismic Research Department of Texas Instruments. (TI is still a world leader in DSP.) In the following years, he applied his programming and DSP expertise to other interesting areas including sonar and underwater acoustics.

Richard holds an MSEE degree from Southern Methodist University and has many years of experience in the application of computer technology to real-world problems.

Baldwin@DickBaldwin.com

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories