Java Programming Notes # 1718
- Preface
- Preview
- What Kjell tells us about projections
- Several alternatives are available
- The updated game-math library named GM03
- Discussion and sample code
- Run the program
- Summary
- What’s next?
- Resources
- Complete program listings
- Copyright
- About the author
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, Applications of the Vector Dot Product (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 apply the vector dot product to three different applications. You learned how to use the dot product to compute nine different angles of interest that a vector makes with various elements in 3D space. You learned how to use the dot product to find six of the infinite set of vectors that are perpendicular to a given vector, and you learned how to use the dot product to perform back-face culling.
What you will learn in this lesson
In this lesson, which is the first part of a multi-part miniseries on projections, you will learn how to use the game-math library to create a first-person game in a 3D world. Before getting into the technical details, I am going to give you some background.
What is a first-person shooter game?
Because I frequently teach an introductory programming course using the Alice programming language (see Resources), I often visit the Alice Community Forums. Many students also visit the forums. It seems that a large percentage of those students have a single programming goal in mind. That goal is to learn how to write a first-person shooter game, which they refer to as an FPS game. In this lesson, you will learn how to write a first-person game, but it may not be exactly what those students have in mind insofar as shooting is concerned. While it does involve shooting, there is a distinct lack of blood and gore being spilled, which is a staple of typical FPS games.
A first-person game, according to my understanding, is a game in which the player views the game world through the eyes of the main character (avatar), just as though the main character has a video camera attached to his head. The first first-person shooter game that I remember was the widely-distributed game of Doom (see Resources), which was released around 1993.
Early computer games
My first recollection of a computer game is from sometime during the 1970s. I visited a relative who was a college professor at a small college and he took me to visit the computer center at the college. As I recall they had a Digital Equipment Corporation computer with several CRT terminals available for student use. (In those days, I was programming Data General and Raytheon minicomputers in machine language doing digital signal processing.)
There were several primitive games installed on the computer at the college that I visited. They were played using character graphics on the CRT terminals. I remember two games in particular. One game was a Lunar Lander game in which the player attempted to cause something that looked vaguely like a spaceship to land on a target on the screen. The second game was a Cannonball game in which the player adjusted the elevation angle of a cannon in an attempt to cause the projectile to land on a target on the screen. Those games were definitely not first-person games.
A 3D interactive first-person version of the cannonball game
In this lesson, you will learn how to write a modern 3D interactive first-person version of the cannonball game using the updated game-math library named GM03.
Human cannonball approaching the safety net in Cannonball01
More specifically, in this game, the player’s avatar is a human cannonball with a video camera attached to her helmet. The objective is to adjust the horizontal (azimuth) firing angle, the vertical (elevation) firing angle, and the muzzle velocity of the cannon so that the human cannonball will land safely in the net when she is fired from the cannon.
As the avatar flies through the air, she keeps the camera trained on the center of the safety net and the player sees a 2D projection of the 3D world of the safety net on the computer screen. Sometimes she lands in the net and survives. At other times, she misses the net. The player sees it all from a first-person 3D animation perspective. Figure 1 shows a screen shot of the human cannonball successfully approaching the net for a soft landing.
Figure 1. Human cannonball approaching the safety net in Cannonball01.
Quite a lot of math is required
As you will see when we get into the details of the program, quite a lot of math is required to write this program. For example, the safety net that you see in Figure 1 is a screen shot at one instant in time of the video camera approaching the circular safety net at an angle in the 3D world. Figure 2 shows another screen shot of the game in progress. In Figure 2, the aim and muzzle velocity were not correct for a soft landing and the human cannonball sees the safety net pass by on her left before she crashes into the ground.
Figure 2. Human cannonball missing the safety net in Cannonball01.
A perspective projection
The 3D world is projected onto the 2D screen using a perspective projection, which can be mathematically challenging. The trajectory of the human cannonball is based on the actual trajectory of a projectile under the influence of constant gravity in 3D space. The list of mathematical requirements goes on and on but I won’t enumerate it here.
Before I continue with this game program, I’m going to give you a preview of what’s in store for you if you continue studying the lessons in this miniseries on projections.
What you will learn in the next lesson
In the next lesson, you will learn how the math behind a parallel oblique projection works. This type of projection was first presented in the lesson titled Math for Java Game Programmers, Venturing into a 3D World (see Resources) but I didn’t explain how it works at that time. An example output from this projection type is shown in Figure 3.
Figure 3. Parallel oblique projection of a 3D world onto a 2D plane.
More details about the parallel oblique projection
With respect to the parallel oblique projection, you will learn:
- About the angles named alpha and phi, which are critical to the solution of the parallel oblique projection equations, and which control the overall appearance of the projection.
- How to start with a general point in 3D space and derive the two equations required to project that point onto a projection plane that coincides with the x-y plane using a parallel oblique projection.
- How to adjust the values of the angles named alpha and phi to produce different variations of the parallel oblique projection known as Cavalier projections and Cabinet projections.
- How to cause the parallel oblique projection to degenerate into a parallel orthographic projection instead of a parallel oblique projection.
- How to write the Java code necessary to implement the two equations that are required to project the 3D point onto the 2D projection plane.
What you will learn in future lessons
In subsequent parts of this multi-part miniseries on projections, you will learn how to achieve the appearance of perspective when projecting a 3D world onto a 2D projection plane. In particular, you will learn about the math behind projections:
- With a fixed camera position looking straight down the z-axis as shown in Figure 4.
- With a fixed camera position where the camera position is above the x-z plane and to the right of the z-y plane as shown in Figure 5. (Compare Figure 5 with Figure 3.)
- With a variable camera position and a variable camera angle as shown in Figure 1, Figure 2, and Figure 6.
Figure 4. Perspective projection with a fixed camera position looking straight down the z-axis.
Figure 5. Perspective projection with the fixed camera position off the z-axis (compare with Figure 3).
Figure 6. Perspective projection with a variable camera position and a variable camera angle.
Sometimes our eyes play tricks on us
Because the images in Figure 3 through Figure 6 consist solely of straight lines with no other visual references, and the lines on the back side of the cube are not reduced in prominence relative to the lines on the front of the cube, I sometimes find that my eyes play tricks on me and I don’t perceive the image as it was intended to be perceived. If this happens to you, you may need to blink your eyes a few times and concentrate on the fact that the red circle at the intersection of two red lines is at the
top-front corner of the 3D cube.
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 Human cannonball approaching the safety net in Cannonball01.
- Figure 2 Human cannonball missing the safety net in Cannonball01.
- Figure 3 Parallel oblique projection of a 3D world onto a 2D plane.
- Figure 4 Perspective projection with a fixed camera position looking straight down the z-axis.
- Figure 5. Perspective projection with the fixed camera position off the z-axis (compare with Figure 3).
- Figure 6. Perspective projection with a variable camera position and a variable camera angle.
- Figure 7. The graphic display at startup for Cannonball01.
- Figure 8. User control panel.
- Figure 9. Result of using sliders to re-position the safety net.
- Figure 10. Horizontal (azimuth) firing angle aiming screen.
- Figure 11. Vertical (elevation) firing angle aiming screen.
- Figure 12. Birds-eye view of the human cannonball having overshot the safety net.
- Figure 13. Birds-eye view of the human cannonball having successfully landed in the safety net.
- Figure 14. Successful landing produced by default values of sliders.
Listings
- Listing 1. Beginning of the class named Display.
- Listing 2. Beginning of the constructor for the Display class.
- Listing 3. Elevate and tilt the camera.
- Listing 4. Beginning of the method named drawTheImage.
- Listing 5. Draw the guy wires.
- Listing 6. Define the points on a circle.
- Listing 7. Draw lines connecting the points.
- Listing 8. Draw special lines.
- Listing 9. Beginning of the method named stateChanged.
- Listing 10. Get the source of the ChangeEvent.
- Listing 11. Response to event fired by positionXslider.
- Listing 12. Rotate the cannon around its y-axis.
- Listing 13. Set the elevation angle of the cannon.
- Listing 14. The actionPerformed method.
- Listing 15. Beginning of the class named Animator.
- Listing 16. Beginning of the constructor for the Animator class.
- Listing 17. The remainder of the constructor.
- Listing 18. Beginning of the run method.
- Listing 19. Begin the animation loop.
- Listing 20. Increment time.
- Listing 21. Adjust the orientation of the camera.
- Listing 22. Draw the new image and sleep for a little while.
- Listing 23. Pause and then switch to the birds-eye view.
- Listing 24. Determine the impact location and draw a red or green circle.
- Listing 25. The member class named GUI.
- Listing 26. Source code for game-math library named GM03.
- Listing 27. Source code for the game program named Cannonball01.
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.
Preview
What Kjell tells us about projections
Hopefully by now you have studied Dr. Bradley Kjell’s tutorials (see Resources) through Chapter 11 in which he discusses projections. Dr. Kjell tells us:
“… 3D computer graphics consists of two activities: (1) Creating an imaginary world inside a computer, and (2) producing two dimensional images of that world from various viewpoints.
Producing a 2D image from a 3D image is an example of projection.”
He then proceeds to provide quite a lot of technical information about projecting one vector onto another vector in both 2D and 3D. The techniques that he describes are extremely useful in solving various engineering and physics problems that can be expressed in terms of vectors that represent force, work, etc.
We need much more
However, we need to understand much more that just the projection of one vector onto another vector to be able to produce two dimensional images of an imaginary 3D world. In particular, we need to understand how to project the imaginary 3D world onto a plane that represents the computer screen.
Several alternatives are available
There are several different ways to produce 2D images from the imaginary 3D world that we create inside the computer. For example, the online document titled Classification of 3D to 2D Projections by Paul Bourke (see Resources) lists about a half-dozen different ways to create 2D images from 3D worlds in the parallel category alone. That list completely leaves out another important projection category commonly known as perspective.
However, it is not the purpose of this lesson to get into the details of the different projection schemes. Instead, the purpose of this lesson is to pick a projection scheme and show you how to use that scheme to produce a working game program. I will get into the mathematical details of four different projection schemes in future lessons.
The updated game-math library named GM03
A complete listing of the updated game-math library named GM03 is provided in Listing 26. This listing includes the method named threeDto2DprojectionD that I will use to implement the perspective projection in the game program named Cannonball01. It also includes a number of other updates and modifications, some of which I will use in this lesson and some of which I won’t use. In any event, you will need a copy of the library source code in order to compile and run the game program named Cannonball01.
It is not my intention to explain the updates and modifications to the game-math library in this lesson. Rather, I will provide those explanations in future lessons. My intention for this lesson is to help you learn how to use the library based on what you have already learned and based on a cursory explanation of the new features in the library.
Discussion and sample code
|
A complete listing of the game program named Cannonball01 is provided in Listing 27 near the end of the lesson.
Instructions for playing the game
Brief instructions
- Use the two sliders at the top to adjust the x and z-coordinates of the safety net.
- Use the other three sliders to adjust the azimuth, elevation, and muzzle velocity of the cannon.
- Click the Fire button and watch as the human cannonball flies through the air on her way toward the safety net.
- Observe the impact location in the birds-eye view. If the impact location is outside the safety net, as indicated by a small red circle, repeat steps 2 and 3.
- Continue this process until the human cannonball lands in the net as indicated by a green circle inside the safety net and a beep emitted by the computer. (Note that the default settings of the sliders are such that simply starting the program and clicking the Fire button will cause the human cannonball to land successfully in the safety net.)
Detailed instructions for playing the game
When you start the game running, a graphic display showing the safety net will appear on the screen as shown in Figure 7. (Note that the blue lines won’t appear until you start making adjustments at the control panel. Also note that this display changes as you run the program.)
Figure 7. The graphic display at startup for Cannonball01.
The center line in Figure 7 shows the default firing angle of the cannon. The lines on either side show azimuth angles of plus and minus 45 degrees.
The user control panel
In addition to the graphic display showing the safety net in Figure 7, the user control panel shown in Figure 8 will also appear on the screen. It will appear to the right of the graphic display.
Figure 8. User control panel.
Re-position the safety net
Use the two sliders at the top of Figure 8 to adjust the x and z-coordinates of the safety net. For example, you might elect to move the safety net to the position shown in Figure 9.
Figure 9. Result of using sliders to re-position the safety net.
Adjust azimuth aiming direction of the cannon
Use the left middle slider in Figure 8 to adjust the azimuth aiming angle of the cannon.
As soon as you begin moving this slider, the graphic display will change to look similar to Figure 10.
Figure 10. Horizontal (azimuth) firing angle aiming screen.
The view shown in Figure 10 has the camera centered on the cannon at ground level. The gray material to the left is the edge view if the safety net. The vertical red line is the current aiming direction of the cannon. If you move this slider to center the red line on the safety net, the cannon will be pointing directly at the net.
Don’t trip on the guy wires
When making this adjustment, you must take into account that the three guy wires are not distinguishable from the actual circular safety net in the edge view shown in Figure 10. Therefore, the actual center of the safety net is not at the exact center of the horizontal gray line in Figure 10. You can switch back and forth between the azimuth slider and the position sliders to visually estimate the effect of the guy wires on the edge view of the safety net.
Adjust the elevation of the cannon
As soon as you begin moving the right middle slider in Figure 8, the graphic display will switch to something similar to that shown in Figure 11.
Figure 11. Vertical (elevation) firing angle aiming screen.
The red horizontal line in Figure 11 shows the vertical aiming angle of the cannon and the gray material shows the edge view of the safety net.
|
Two extreme cases
If you move the elevation slider to the 0 position, the red line and the safety net will merge in Figure 11, meaning that the cannon is aiming at the same elevation as the safety net (ground level). Of course, you will never hit the safety net that way due to the effect of gravity on the human cannonball unless you place the camera in the safety net and set the muzzle velocity to a small value.
|
Adjust the muzzle velocity
As soon as you begin adjusting the slider for muzzle velocity, the graphic display will switch back to that shown in Figure 7. However, this graphic display is not helpful in deciding upon the appropriate muzzle velocity. If you want to hit the safety net with the first shot, you may simply need to get out your calculator and solve the equations of motion, (which I will explain later) using the acceleration of gravity (32.174 ft/sec/sec) along with the values displayed below the sliders in Figure 8. Or, you can simply use trial and error.
Fire the cannon
When you click the Fire button in Figure 8, you will see an animated presentation of the safety net as seen by the human cannonball as she flies through the air, either landing in the safety net or landing outside the safety net. Examples of each case are shown in Figure 1 and Figure 2.
Observe the results from a birds-eye view
Shortly after the human cannonball lands, the graphic output will switch to a birds-eye view similar to that shown in Figure 12.
Figure 12. Birds-eye view of the human cannonball having overshot the safety net.
In Figure 12, the firing direction was okay, but the muzzle velocity was too high, or the elevation angle was too low, causing the human cannonball to overshoot the safety net. The impact point is shown by the red circle in Figure 12. (Note that if you specify a particularly bad set of aiming parameters, you can end up with the red circle completely outside the viewing area in which case it won’t be visible.)
Keep trying until you succeed
If the impact location is outside the safety net, (as indicated by a small red circle in Figure 12), adjust the aiming parameters and the muzzle velocity and click the Fire button to try again.
Continue this process until the human cannonball lands in the safety net as indicated by a green circle inside the safety net as shown in Figure 13.
Figure 13. Birds-eye view of the human cannonball having successfully landed in the safety net.
The computer will beep to indicate a successful landing
In addition to the green circle shown inside the safety net in Figure 13, the computer will also beep when the human cannonball successfully lands in the safety net.
The default parameters
As I mentioned earlier, if you simply click the Fire button without adjusting any of the sliders, the default values of the sliders will cause the human cannonball to land squarely in the center of the safety net as shown in Figure 14.
Figure 14. Successful landing produced by default values of sliders.
Description of the program
This is a modern interactive 3D version of the early cannonball games that appeared on computer terminals long before the advent of the personal computers.
A first-person game
The scenario is that of a human cannonball with a video camera mounted on her helmet. The player sees the scene through the video camera, which is essentially seeing the game through the eyes of the human cannonball. Hence, this is a first-person game.
A first-person shooter game
The playing field consists of the cannon (which is not visible) and a safety net located somewhere within the range of the cannon as shown in Figure 9. The objective is for the player to adjust the horizontal firing direction (azimuth), the vertical firing direction (elevation), and the muzzle velocity of the cannon to cause the human cannonball to land in the safety net when she is fired from the cannon. (Hence, this is a first-person shooter game, but possibly not of the genre that you were expecting.)
An animated view of the safety net during the flight
As the cannonball flies through the air, she keeps the video camera trained on the center of the safety net. This causes the image of the safety net to be displayed on the screen during the entire trajectory of the cannonball.
A birds-eye view of the playing field
After the cannonball lands, a birds-eye view of the playing field is displayed as shown in Figure 12 and Figure 13. If the cannonball landed in the safety net, a green circle appears in the net as shown in Figure 13 showing where the cannonball landed. A successful landing also causes the computer to beep.
If the cannonball landed outside of the net, a red circle marks the spot where the cannonball landed as shown in Figure 12. In this case, the computer does not beep.
If at first you don’t succeed…
At this point, the player can use the sliders shown in Figure 8 to adjust the parameters and fire the cannon again.
Construction of the safety net
The safety net consists of a circle represented by twelve points on its circumference with lines connecting all of the points to form a mesh. In addition, there are three outriggers that are intended to look like guy wires holding the net in place. (See an oblique view of the safety net in Figure 1 and a view from directly above in Figure 12.)
The user input panel
Because of physical space requirements, the graphical output and the user input panel (GUI) are displayed in two separate JFrame objects. As shown in Figure 8, the GUI provides sliders that allow the player to specify the following values:
- The x-coordinate of the center of the safety net.
- The z-coordinate of the center of the safety net. (The y-coordinate is always zero because the safety net and the cannon are both at ground level.)
- The horizontal direction that the cannon is pointing (azimuth);
- The vertical direction that the cannon is pointing (elevation).
- The muzzle velocity of the cannon.
In addition, the GUI provides five non-editable JTextField objects that display the current value of an associated slider. Finally, the GUI provides a JButton object labeled Fire that the user clicks to fire the cannon.
Projectile motion
The projectile motion is based on the equations of motion for a projectile in a vacuum with constant acceleration of gravity.
Different views of the playing field
When the user adjusts the x-coordinate or the z-coordinate of the net, the program provides a view of the playing field from a vantage point immediately above the cannon, which is at the origin as shown in Figure 7. In that case, the camera is pointed downward toward the expanse of the playing field. Three blue lines radiate from the cannon. The center line is in line with the default firing direction of the cannon. The other two lines form a 45-degree angle on either side of the center line.
As the user adjusts either coordinate value by moving the slider, the animated safety net moves within the playing field according to the current coordinate values as illustrated in Figure 9.
When the player adjusts the azimuth of the cannon, the view of the playing field reverts to ground level with the camera attached to the cannon and pointing toward the horizon as shown in Figure 10. A vertical red line shows the azimuth that the cannon (and the attached camera) are currently pointing. The user can rotate the cannon until it lines up with the center of the safety net to ensure that the cannon is pointing in the direction of the safety net.
When the player adjusts the elevation of the cannon, the view of the playing field is at ground level with the cannon attached to the cannon as shown in Figure 11. A horizontal red line shows the pointing elevation of the cannon (and the attached camera) relative to the safety net that is at ground level.
Adjusting the muzzle velocity
Because the muzzle velocity is simply a numeric quantity with no spatial representation, there is no animation when the user adjusts the muzzle velocity slider.
The program code for Cannonball01
Will explain in fragments
As is my custom, I will break the program down into fragments and explain the individual fragments. As mentioned earlier, a complete listing of the game program named Cannonball01 is provided in Listing 27 near the end of the lesson. Some portions of the program are very similar to the code that I have explained in previous lessons (see Resources), so I won’t bore you by explaining that code again in this lesson.
Beginning of the class named Display
The main method of the Cannonball01 class simply instantiates a new object of the class named Display. Listing 1 shows the beginning of the class named Display.
Listing 1. Beginning of the class named Display.
class Display extends JFrame implements ChangeListener,ActionListener{ //Save a reference to this object for access by the GUI. Display displayObj; //Instance variable declarations deleted for brevity. |
An object of the Display class is the graphical display shown in Figure 1. This class implements both the ChangeListener and the ActionListener interfaces. This requires that the class provide concrete definitions of the method declared in those two interfaces.
Implementation of the ChangeListener interface makes the object eligible to be registered as a listener on the sliders shown in Figure 8. When any of the sliders fires a ChangeEvent, the stateChanged method that is defined in the Display classis called. I will explain the stateChanged method later.
Implementation of the ActionListener interface makes the object eligible to be registered as a listener on the JButton object shown in Figure 8. When the button fires an ActionEvent, the actionPerformed method defined in the Display class is called. I will also explain the actionPerformed method later.
Instance variable declarations deleted for brevity
The Display class declares a relatively large number of instance variables that serve as working variables in the program.There is nothing unusual or exciting about these variables, so I deleted them from Listing 1 for brevity. You can view the variable declarations in Listing 27 near the end of the lesson.
Beginning of the constructor for the Display class
The constructor for the Display class begins in Listing 2. Once again, much of the code in the constructor is very similar to code that I have explained in previous lessons, so I deleted that code from Listing 2 for brevity.
Listing 2. Beginning of the constructor for the Display class.
Display(){//constructor //Code deleted for brevity //Instantiate the JFrame that contains the user input // components. Those user input components were // placed in a separate JFrame object simply because // there isn't sufficient space available to contain // them in the main display. new GUI(); //Register this object as a change listener on the // sliders. This can't be done until the GUI object // has been instantiated. positionXslider.addChangeListener(this); positionZslider.addChangeListener(this); azimuthSlider.addChangeListener(this); elevationSlider.addChangeListener(this); velocitySlider.addChangeListener(this); //Register this object as an action listener on the // Fire button. fireButton.addActionListener(this); |
The remaining code
The code in Listing 2:
- Instantiates a new object of the GUI class. (This object is shown in Figure 8.)
- Registers the object of the Display class as a listener on each of the sliders and the button shown in Figure 8.
This code is completely straightforward and should not require an explanation beyond the embedded comments in Listing 2.
Elevate and tilt the camera
The two boldface statements in Listing 3 elevate the camera to a position directly above the origin (the cannon) and tilt it downward by 45 degrees to produce the view shown in Figure 7 (except that the blue lines are created later). Also note that the camera is rotated by 180 degrees around the y-axis to cause it to be pointing along the positive z-axis.
Listing 3. Elevate and tilt the camera.
camera.setData(1,100); angle = new GM03.ColMatrix3D(45,180,0); //Project the scene onto the 2D off-screen image and // display it on the screen. drawTheImage(g2D); myCanvas.repaint(); }//end constructor |
Two of the instance variables that I deleted from Listing 1 were named camera and angle. The variable named camera refers to a GM03.Point3D object that controls the position of the camera. The variable named angle refers to a GM03.ColMatrix3D object that controls the orientation of the camera. The two boldface statements in Listing 3 simply establish initial values in those objects by calling methods from the game-math library named GM03. If you have studied the earlier lessons in this miniseries, you should have no trouble understanding the meaning of those statements.
Draw the image
The remaining code in Listing 3 calls the method named drawTheImage to cause the scene to be drawn on the off-screen image. I will have a lot to say about the method named drawTheImage later.
Then Listing 3 calls the standard repaint method, which in turn calls the overridden update method to cause the off-screen image to be copied to the screen. I have explained all of this in previous lessons.
Listing 3 also signals the end of the constructor for the Display class.
Beginning of the method named drawTheImage
The method named drawTheImage begins in Listing 4. The purpose of this method is to create the 3D scene and to project it onto the 2D off-screen image. In effect, each time this method is called, it produces a snapshot of the 3D scene projected onto a 2D plane using the current values of certain state variables that specify the current state of the 3D world. The appearance of motion (animation) is produced by calling this method repetitively and making changes to the state variables between calls to the method. (I will explain the animation later.)
Listing 4. Beginning of the method named drawTheImage.
void drawTheImage(Graphics2D g2D){ //Erase the screen g2D.setColor(Color.WHITE); GM03.fillRect(g2D, -osiWidth/2, osiHeight/2, osiWidth, osiHeight); |
This method begins by erasing the off-screen image. I have explained code like this in earlier lessons.
Draw the guy wires
Listing 5 draws three vectors that simulate guy wires and anchors holding up the net as shown in Figure 2.
Listing 5. Draw the guy wires.
g2D.setColor(Color.GRAY); new GM03.Vector3D(1.5*radius,0.0,1.5*radius). draw(g2D,new GM03.Point3D( centerPoint.getData(0), 0.0, centerPoint.getData(2)), type,scale,camera,angle); new GM03.Vector3D(-1.5*radius,0.0,1.5*radius). draw(g2D,new GM03.Point3D( centerPoint.getData(0), 0.0, centerPoint.getData(2)), type,scale,camera,angle); new GM03.Vector3D(0.0,0.0,-1.5*1.414*radius). draw(g2D,new GM03.Point3D( centerPoint.getData(0), 0.0, centerPoint.getData(2)), type,scale,camera,angle); |
Each guy wire is represented by a vector for display purposes
In essence, for each guy wire, the code in Listing 5:
- Instantiates a new GM03.Vector3D object with the appropriate x, y, and z values.
- Calls the draw method on the Vector3D object to cause the vector to be drawn on the off-screen image with its tail located at a point that will ultimately be the center of the safety net.
Now for the new material
Up to this point, there is nothing in Listing 5 that I haven’t explained in earlier lessons. What is new in Listing 5 is that the draw method of the GM03.Vector3D class has been updated to require four new parameters (shown in boldface in Listing 5) that you haven’t seen before:
- type – specifies the type of projection (3 in this program)
- scale – controls the overall size of the projected image
- camera – controls the position of the camera
- angle – controls the orientation of the camera.
Type is not intuitive
Of the four new parameters, only the type parameter is not intuitive on the basis of its name and the brief description given above. This parameter specifies the type of projection employed to project the 3D world onto the 2D viewing plane. (I will be explaining a lot more about the different projection types in future lessons.) For now, suffice it to say that the type value of 3 used in this program projects the 3D world onto the 2D viewing plane with perspective and with independent control over the position and the orientation of the camera.
Perspective
The perspective aspect of the projection is illustrated by Figure 1 where the circular safety net appears as an ellipse and the points where the straight lines join that are furtherest from the camera are pictured closer together than similar points on the side that is nearer the camera.
Independent camera control
The independent control over the position and orientation of the camera is illustrated by Figure 1 and Figure 2 showing two different views of the safety net. The two views are produced by the camera in two different positions with two different orientations. In Figure 1, the camera is flying directly toward the safety net, pointing directly at the center of the net. In Figure 2, the camera is flying by on the right side of the safety net, but is still pointing directly at the center of the net.
Creating the safety net
Listing 6 begins the creation of the safety net by defining twelve points that represent the circumference of a circle on the x-z plane and storing references to those points in an array.
Listing 6. Define the points on a circle.
int numberPoints = 12; GM03.Point3D[] points = new GM03.Point3D[numberPoints]; for(int cnt = 0;cnt < points.length;cnt++){ points[cnt] = new GM03.Point3D( centerPoint.getData(0) + radius*Math.cos( (cnt*360/numberPoints)*pi/180), centerPoint.getData(1), centerPoint.getData(2) + radius*Math.sin( (cnt*360/numberPoints)*pi/180)); }//end for loop |
You need to know a little about trigonometry to understand the code in Listing 6. (Explaining trigonometry is beyond the scope of this lesson.) If you do understand the trigonometry in Listing 6, there is nothing in Listing 6 that I haven’t explained in earlier lessons in this miniseries (see Resources).
Draw lines connecting the points
The twelve points that are defined in Listing 6 are the points where the straight lines are joined in Figure 1.
Listing 7 draws lines that connect every point to every other point to create the mesh shown in Figure 1.
Listing 7. Draw lines connecting the points.
g2D.setColor(Color.GRAY); for(int row = 0;row < points.length;row++){ for(int col = row;col < points.length;col++){ new GM03.Line3D(points[row],points[col]). draw(g2D,type,scale,camera,angle); }//end inner loop }//end outer loop |
The draw method for the GM03.Line3D class has been updated to require the four new parameters highlighted in boldface in Listing 7. These four new parameters are identical to the four new parameters required by the GM03.Vector3D class that I explained earlier. (The draw method of the GM03.Point3D class was also updated to require the same four new parameters.)
Except for the new parameters required by the draw method, there is nothing new or unusual in Listing 7. The code in Listing 7 draws the lines that connect the points shown in Figure 1 and other views of the safety net.
Draw special lines
The code in Listing 8 tests three different state variables (highlighted in boldface) to determine whether or not to draw the blue lines (guideLines) in Figure 7, the red vertical line (verticalSightLine) in Figure 10, or the red horizontal line (horizontalSightLine) in Figure 11.
Listing 8. Draw special lines.
if(guideLines){ //Draw three lines that radiate out from the origin // with an angle of 45 degrees between them. g2D.setColor(Color.BLUE); new GM03.Line3D(new GM03.Point3D(0.0,0.0,0.0), new GM03.Point3D(0.0,0.0,400.0)). draw(g2D,type,scale,camera,angle); new GM03.Line3D(new GM03.Point3D(0.0,0.0,0.0), new GM03.Point3D(400.0,0.0,400.0)). draw(g2D,type,scale,camera,angle); new GM03.Line3D(new GM03.Point3D(0.0,0.0,0.0), new GM03.Point3D(-400.0,0.0,400.0)). draw(g2D,type,scale,camera,angle); }//end if if(verticalSightLine){ //Draw a vertical sighting line at the center of the // display that is used to adjust the azimuth of the // cannon. g2D.setColor(Color.RED); GM03.drawLine(g2D,0.0,-osiHeight/2,0.0,osiHeight/2); }//end if if(horizontalSightLine){ //Draw a horizontal sighting line at the center of // the display that is used to adjust the elevation // of the cannon. g2D.setColor(Color.RED); GM03.drawLine(g2D,-osiWidth/2,0.0,osiWidth/2,0.0); }//end if }//end drawTheImage |
All three of these state variables are type boolean. You will see the code that sets the values of the variables to true and false later in this lesson.
Termination of the method named drawTheImage
Listing 8 also signals the end of the method named drawTheImage. When this method terminates, the projection of the 3D scene onto the 2D off-screen image is ready to be copied to the computer screen for viewing by the player.
Beginning of the method named stateChanged
As you saw in Listing 1, the Display class implements the interface named ChangeListener. This makes an object of the Display class eligible to serve as a listener on the sliders in Figure 8. As you saw in Listing 2, the object of the Display class is registered as a listener on all of the sliders.
Implementation of the ChangeListener interface requires the class named Display to define the event-handler method named stateChanged. The definition of the method named stateChanged begins in Listing 9. This method is called to respond to change events on any of the sliders. In other words, whenever the position of a slider is changed, this method is called.
Listing 9. Beginning of the method named stateChanged.
public void stateChanged(ChangeEvent e){ camera.setData(0,0); camera.setData(1,100); camera.setData(2,0); angle.setData(0,45); angle.setData(1,180); angle.setData(2,0); |
Changing the view of the playing field
The method begins by executing code that is very similar to code that I explained in Listing 3 to elevate the camera above the origin and tilt it downward by 45 degrees. Once again note that the horizontal orientation of the camera is set to cause it to be pointing in the direction of the positive z-axis (180 degrees). If one of the top two sliders or the bottom slider in Figure 8 is moved, this code will produce a view that is similar to that shown in Figure 7 (with or without the blue lines). Moving one of the center two sliders in Figure 8 will also produce this view, but it will be quickly changed to one of the views shown in Figure 10 or Figure 11 so you probably won’t see the view produced by the code in Listing 9 in those two cases.
Get the source of the event
When writing event-driven programs in Java, you have a variety of options for organizing your code. One option is to instantiate and register a separate event handler object on each of the potential sources of events. For example, I could have instantiated five different objects from classes that implement the ChangeListener interface and could have registered each one of those objects as a listener object on one of the sliders shown in Figure 8. In that case, there would be a one-to-one correspondence between the sliders and the listeners. The stateChanged method associated with each particular slider would be called when a particular slider fires an event. When a particular stateChanged method executes, there would be no question as to which slider fired the event.
Another option, which is the option that I elected to use in this program, is to register a single listener object on all of the event sources. As you saw in Listing 2, I registered the same listener object on all five sliders and also on the button shown in Figure 8. The distinction between the button and the sliders is made automatically by which event-handler method is called. However, regardless of which slider fires the event, the same stateChanged method is called. Therefore, the stateChanged method must determine which slider fired the event in order to be able to respond appropriately. This is accomplished by the code in Listing 10, which gets and saves a reference to the JSlider object that fired the event.
Listing 10. Get the source of the ChangeEvent.
JSlider source = (JSlider)e.getSource(); |
Unique name property values
You will see later than when the five JSlider objects shown in Figure 8 are instantiated, a unique value is assigned to the name property for each slider. The code in the stateChanged method uses the source of the event along with the unique names to identify the particular slider that fired the event and to take the appropriate action on the basis of that identification.
Response to event fired by positionXslider
For example, Listing 11 shows the response of the event handler when the source of the event is identified as the slider with a name property value of positionXslider. (This is the upper-left slider in Figure 8.)
Listing 11. Response to event fired by positionXslider.
if(source.getName().equals("positionXslider")){ //Set the x-coordinate of the net and display the // coordinate value. centerPoint.setData(0,source.getValue()); positionXoutput.setText("" + source.getValue()); guideLines = true;//Draw angular guidelines verticalSightLine = false;//No verticalSightLine. horizontalSightLine = false;//No horizontalSightLine |
And the response is…
The response to an event fired by this slider is:
- Set the x-coordinate of the center of the safety net to the current value indicated by the pointer on the slider.
- Display that value immediately below the slider.
- Set the value of the state variable named guideLines to true to cause the blue lines shown in Figure 7 to be displayed.
- Set the value of the state variable named verticalSightLine to false to prevent the vertical red line shown in Figure 10 from being displayed.
- Set the value of the state variable named horizontalSightLine to false to prevent the horizontal red line shown in Figure 11 from being displayed.
From this point forward in the explanation of this method, I will delete some of the repetitive code for brevity. You can view all of the code in Listing 27 near the end of the lesson.
Rotate the cannon around its y-axis
Listing 12 shows the response to an event fired by the middle-left slider in Figure 8.
Listing 12. Rotate the cannon around its y-axis.
}else if(source.getName().equals("positionZslider")){ //Code deleted for brevity. }else if(source.getName().equals("azimuthSlider")){ //Rotate the cannon around the y-axis. azimuthAngle = source.getValue(); //Display direction that the cannon is pointing. azimuthOutput.setText("" + source.getValue()); //Set the camera to ground level with no vertical // tilt. camera.setData(1,0.0); angle.setData(0,0.0); //Rotate the camera to point in the same direction // as the cannon. angle.setData(1,180 + source.getValue()); guideLines = false;//Do not draw angular guidelines verticalSightLine = true;//Draw verticalSightLine. horizontalSightLine = false;//No horizontalSightLine |
Change the horizontal aiming angle
The purpose of this slider is to change the horizontal aiming angle (azimuth) of the cannon and the attached camera. The code in Listing 12 produces the graphic output shown in Figure 10, which is used interactively to aim the cannon at the safety net.
The actions taken by the azimuthSlider code in Listing 12 is:
- Get and display the current value of the slider.
- Set the vertical position of the camera to ground level.
- Set the vertical rotation angle of the camera to zero.
- Set the horizontal rotation angle of the camera to 180 degrees plus the current slider value.
- Set the state variables named guideLines, verticalSightLine, and horizontalSightLine to cause only the red vertical line shown in Figure 10 to be displayed.
Set the elevation angle of the cannon
Listing 13 shows the code that is executed when the player moves either the right-center or the bottom-left slider in Figure 8.
Listing 13. Set the elevation angle of the cannon.
}else if(source.getName().equals("elevationSlider")){ //Code deleted for brevity. //Set the camera to ground level with a vertical // tilt equal to the elevation angle. Scale the // angle by 1/4 to keep the net within the bounds of // the display. camera.setData(1,0.0); angle.setData(0,-source.getValue()/4); //Code deleted for brevity. }else if(source.getName().equals("velocitySlider")){ //Code deleted for brevity. }//end if //Project the scene onto the 2D off-screen image and // display it on the screen. drawTheImage(g2D); myCanvas.repaint(); }//end stateChanged |
The only thing that is unique about the response to those two sliders is that it was necessary to divide the slider value by four to keep the view of both the red horizontal line and the safety net shown in Figure 11 within the viewing area of the canvas.
Call the method named drawTheImage
Finally, after adjusting a variety of state variables, Listing 13 calls the method named drawTheImage to cause the 3D world defined by those state variables to be projected onto the 2D off-screen image.
Then Listing 13 calls the repaint method to cause the off-screen image to be copied to the canvas. As a result, as you move a pointer on one of the sliders, the effect of that movement is displayed on the screen.
The actionPerformed method
Because the object of the Display class is registered as a listener object on the Fire button shown in the bottom right corner of Figure 8, the actionPerformed method, shown in Listing 14, is executed whenever the user clicks the button.
Listing 14. The actionPerformed method.
public void actionPerformed(ActionEvent e){ //Code deleted for brevity. animator = new Animator(); animator.start(); }//end actionPerformed |
Most of the code in the actionPerformed method is straightforward and I deleted it from Listing 14 for brevity. Basically, the purpose of the deleted code is to prepare the program for the animation that will follow. You can view the deleted code in Listing 27.
Let the show begin
The two boldface statements in Listing 14 do deserve an explanation. The first boldface statement instantiates a new object of the member class named Animator. I will explain that class shortly, but for now suffice it to say that the class is a Thread class.
The second boldface statement in Listing 14 calls the start method on the new Thread object, which in turn causes the run method belonging to the object to be executed. It is the run method that actually produces the animation, causing the human cannonball to fly toward the safety net.
Beginning of the class named Animator
The Animator class begins in Listing 15.
Listing 15. Beginning of the class named Animator.
class Animator extends Thread{ //Gradational acceleration constant. final double gravity = 32.174;//ft/sec/sec //The following velocity components are computed from // the user-specified velocity, azimuth, and // elevation values. double initialVelocityX; double initialVelocityY; double initialVelocityZ; //Store the camera angle and position here. GM03.ColMatrix3D cameraAngle; GM03.Point3D cameraPosition; //Unit vectors along the x, y, and z axes. GM03.Vector3D uVectorX = new GM03.Vector3D(1,0,0); GM03.Vector3D uVectorY = new GM03.Vector3D(0,1,0); GM03.Vector3D uVectorZ = new GM03.Vector3D(0,0,1); |
As is typical, the Animator class begins by declaring several instance variables that are use as working variables by the constructors and methods belonging to the class.
Beginning of the constructor for the Animator class.
The constructor for the Animator class begins in Listing 16.
Listing 16. Beginning of the constructor for the Animator class.
Animator(){//constructor //Compute the x,y, and z-components of the velocity. initialVelocityZ = velocity * Math.cos(Math.toRadians(azimuthAngle)); initialVelocityX = velocity * Math.sin(Math.toRadians(azimuthAngle)); initialVelocityY = velocity * Math.cos( Math.toRadians(elevationAngle)); |
The code in Listing 16 computes and saves the x, y, and z-components of the initial velocity with which the human cannonball will be fired out of the cannon.
Project slider values onto the axes
Basically, the code in Listing 16 uses the velocity from the Muzzle Velocity slider (see Figure 8) along with the values from the Cannon Azimuth and Cannon Elevation sliders to project the muzzle velocity onto the x, y, and z-axes of the 3D world.
Once again, you will need to understand trigonometry to be able to understand the code in Listing 16. If you do understand trigonometry, and you understand the material in the Kjell tutorial up through Chapter 11, you should have no trouble understanding the code in Listing 16. If you don’t meet one or both of those criteria, you will simply have to take my word for it that the code in Listing 16 produces the desired results.
The remainder of the constructor
The remainder of the constructor for the Animator class is shown in Listing 17.
Listing 17. The remainder of the constructor.
//Initialize the camera position and angle. cameraPosition = new GM03.Point3D(0,0,0); //Note that the camera is rotated by 180 degrees // around the y-axis to cause it to be shooting // along the positive z-axis. cameraAngle = new GM03.ColMatrix3D(0,180,0); //Copy the camera position and angle to the objects // used by the draw methods. camera = cameraPosition.clone(); angle = cameraAngle.clone(); //Project the scene onto the 2D off-screen image and // display it on the screen. drawTheImage(g2D); myCanvas.repaint(); }//end constructor |
The code in Listing 17 is straightforward and shouldn’t require an explanation beyond the embedded comments. I will point out, however, that as soon as the object is constructed, the methods named drawTheImage and repaint are called to cause the current state of the 3D world to be projected onto the 2D viewing plane and copied to the canvas.
Beginning of the run method
As I explained earlier, when the user clicks the Fire button in Figure 8, the actionPerformed method in Listing 14 is executed. The code in the actionPerformed method instantiates a new object of the Animator class and calls the start method on the new object. This, in turn, causes the run method that begins in Listing 18 to be executed.
Listing 18. Beginning of the run method.
public void run(){ double time = 0.0; //This is one of the factors that controls the // animation speed. double deltaTime = 0.01; //Position the camera at the origin. cameraX = 0.0; cameraY = 0.0; cameraZ = 0.0; |
Computing the position of the cannonball at equal increments of time
Later in the run method, the equations of motion for the trajectory of the human cannonball will be evaluated and the animation will show the view from the camera mounted on her helmet as she flies through that trajectory. The equations of motion involve, among other things, the time since she was fired from the cannon at a particular velocity in a particular direction. The variable named time keeps track of the absolute time since she was fired and the variable named deltaTime specifies the time increment between computations of her position in 3D space.
Animation speed
The value of this variable is one of the controlling factors of the animation speed. (You will see another controlling factor later.) Increasing this value will reduce the number of points along the trajectory at which her position is computed, and everything else being equal, will speed up the animation.
Begin the animation loop
The human cannonball (and the attached camera) starts at ground level, flies through the air in an arc-like trajectory, and impacts the safety net (or the ground) at the other end of the trip. If the computation is allowed to continue at that point, the camera will continue along a path that takes it into negative-y territory below the ground level.
The animation loop that begins in Listing 19 continues until the y-coordinate of the camera goes negative, at which time the loop terminates.
Listing 19. Begin the animation loop.
while(cameraY >= 0.0){ //Loop until the camera completes the trajectory // and goes slightly negative in terms of the // y-coordinate. //Compute and save the new position of the camera // based on the equations of motion. cameraX = initialVelocityX * time; cameraZ = initialVelocityZ * time; cameraY = initialVelocityY*time - (gravity*time*time)/2.0; cameraPosition.setData(0,cameraX); cameraPosition.setData(1,cameraY); cameraPosition.setData(2,cameraZ); //Copy the camera information saved locally to the // instance variable that is used by the various // draw methods. camera = cameraPosition.clone(); |
Evaluating the equations of motion
The boldface statements in Listing 19 evaluate the equations of motion to compute the current position of the human cannonball and the attached camera in 3D space at a specific point in time. I’m not going to explain the equations of motion. I will simply refer you to a good elementary physics book (or a comparable website) for that knowledge.
Then the code in Listing 19 does a little cleanup work to save the current position of the camera in 3D space. (It would probably be possible to streamline this code somewhat.)
Increment time
Note that the evaluation of the equations of motion in Listing 19 includes, among other things, the amount of time since the trip began.
Listing 20 increments the time so that the next time the equations of motion are evaluated, they will be evaluated with a larger value of time.
Listing 20. Increment time.
//Increment time. time += deltaTime; |
Adjust the orientation of the camera
Listing 21 computes and saves the angle from the camera to the center of the net.
Listing 21. Adjust the orientation of the camera.
GM03.Vector3D displacement = cameraPosition. getDisplacementVector(centerPoint); double alpha = Math.acos(displacement.normalize(). dot(uVectorX)); double theta = Math.acos(displacement.normalize(). dot(uVectorZ)); //Convert the angles to degrees and save them. cameraAngle.setData(0,Math.toDegrees(theta)); cameraAngle.setData( 1,180 + (90 - Math.toDegrees(alpha))); angle = cameraAngle.clone(); |
If you have studied the previous lessons in this miniseries and you understand trigonometry, you should have no difficulty with the code in Listing 21.
Draw the new image and sleep for a little while
Listing 22 projects the 3D world onto the 2D off-screen image, copies the 2D image to the screen and goes to sleep for 37 milliseconds. Then control goes back to the top of the while loop in Listing 19 where the value of the y-coordinate is tested to see if it is positive or negative.
Listing 22. Draw the new image and sleep for a little while.
//Project the scene onto the 2D off-screen image // and display it on the screen. drawTheImage(g2D); myCanvas.repaint(); //Put the thread to sleep temporarily. This is one // of the factors that controls the animation // speed. try{ Thread.currentThread().sleep(37); }catch(InterruptedException e){ e.printStackTrace(); }//end catch }//end animation loop |
If the y-coordinate is still positive, the process repeats and the human cannonball is moved another incremental step along her trajectory. If the y-coordinate is negative, this means that the human cannonball has reached the end of the trip and has impacted either the safety net or the ground. In either case, the loop terminates.
Pause and then switch to the birds-eye view
The animation loop has terminated meaning that the y-coordinate for the camera has gone slightly negative, signaling that it has reached the end of its trajectory. The code in Listing 23 causes the program to pause for two seconds to allow the player to absorb the impact from a 3D first-person view and then switches to the birds-eye view shown in Figure 12 (without the small red circle) to allow the player to more clearly see what happened.
Listing 23. Pause and then switch to the birds-eye view.
try{ Thread.currentThread().sleep(2000); }catch(InterruptedException e){ e.printStackTrace(); }//end catch //Enable the sliders when the cannonball lands to // allow the player to make adjustments and fire // the cannon again. positionXslider.setEnabled(true); positionZslider.setEnabled(true); azimuthSlider.setEnabled(true); elevationSlider.setEnabled(true); velocitySlider.setEnabled(true); //Pull up for a birds-eye view directly above the // safety net. camera.setData(0,centerPoint.getData(0)); camera.setData(1,175); camera.setData(2,centerPoint.getData(2)); angle.setData(0,90); angle.setData(1,180); angle.setData(2,0); guideLines = true;//Draw angular guideLines. verticalSightLine = false;//No verticalSightLine. horizontalSightLine = false;//No horizontalSightLine //Project the scene onto the 2D off-screen image. drawTheImage(g2D); |
After disconnecting the camera from the human cannonball’s helmet and moving it to the birds-eye view, the code in Listing 23 draws the new view onto the off-screen image.
Determine the impact location and draw a red or green circle
Listing 24 determines whether the impact location was inside or outside the safety net. If the impact was outside of the net, a small red circle is drawn on the off-screen image to identify the impact location as shown in Figure 12.
Listing 24. Determine the impact location and draw a red or green circle.
//Now determine if the human cannonball landed in // the safety net. if(cameraPosition. getDisplacementVector(centerPoint).getLength() < radius){ //The human cannonball hit the net. //Draw a green circle at the point of impact. g2D.setColor(Color.GREEN); cameraPosition.draw(g2D,type,scale,camera,angle); //Beep the computer to indicate success. Toolkit.getDefaultToolkit().beep(); }else{ //The human cannonball missed the net. //Draw a red circle at the point of impact and // do not beep the computer. g2D.setColor(Color.RED); cameraPosition.draw(g2D,type,scale,camera,angle); }//end else //Draw a blue circle at the origin. g2D.setColor(Color.BLUE); new GM03.Point3D(0,0,0).draw( g2D,type,scale,camera,angle); //Cause the overridden paint method belonging to // myCanvas to be executed. myCanvas.repaint(); }//end run method }//end inner class Animator |
If the impact was inside the safety net, listing 24 draws a small green circle on the off-screen image to show where the impact occurred as shown in Figure 13. In this case, the program also causes the computer to beep to indicate a successful landing.
Listing 24 also calls the repaint method to copy the off-screen image to the canvas on the computer screen.
The member class named GUI
That leaves one more class that I haven’t explained yet. This class is named GUI. An abbreviated version of the class is shown in Listing 25.
Listing 25. The member class named GUI.
class GUI extends JFrame{ GUI(){ //Instantiate a JPanel that will house the user // input components and set its layout manager. // Code deleted for brevity. }//end constructor }//end class GUI //====================================================// }//end class Display |
This class is a member class of the Display class consisting entirely of a constructor. An object of this class provides a home for the user input components shown in Figure 8, and is needed only because there isn’t sufficient space on the object of the Display class to contain all of the user input components. This object is displayed to the right of the object of the Display class on the screen.
All of the code in the GUI class is straightforward and I have deleted it from Listing 25 for brevity. You can view the code in Listing 27.
Run the program
I encourage you to copy the code from Listing 26 and Listing 27. Compile the code and execute the program named Cannonball01 in conjunction with the game-math library named GM03. 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 a little about first-person computer games in general. You also learned how to use the game-math library to write a first-person game in a 3D world.
What’s next?
In the next lesson in this miniseries, you will learn how to derive the equations required for a parallel oblique projection. You will learn about the two angles that are critical to the solution of those equations. You will learn about Cavalier and Cabinet projections, and you will learn how to write the Java code necessary to implement the equations.
Resources
- Index to Baldwin tutorials
- 1700 Math for Java Game Programmers, Getting Started
- 1702 Math for Java Game Programmers, Updating the Math Library for Graphics
- 1704 Math for Java Game Programmers, Working with Column Matrices, Points, and Vectors
- 1706 Math for Java Game Programmers, Vector Addition
- 1708 Math for Java Game Programmers, Putting the Game-Math Library to Work
- 1710 Math for Java Game Programmers, Venturing into a 3D World
- 1712 Math for Java Game Programmers, Our First 3D Game Program
- 1714 Math for Java Game Programmers, Getting Started with the Vector Dot Product
- 1716 Math for Java Game Programmers, Applications of the Vector Dot Product
- Vector Math for 3D Computer Graphics, An Interactive Tutorial by Dr. Bradley P. Kjell
- Classification of 3D to 2D Projections
- Alice – An Educational Software that teaches students computer programming in a 3D environment
- Doom (video game)
Complete program listings
Complete listings of the programs discussed in this lesson are shown in Listing 26 and Listing 27 below.
Listing 26. Source code for game-math library named GM03.
/*GM03.java Copyright 2008, R.G.Baldwin Revised 03/30/08 This is an update to the game-math library named GM02. The primary purpose of the update is to add the ability to project 3D worlds onto a 2D plane using perspective projection in addition to the existing parallel projection capability. (Some other more minor changes were made as well.) The following draw methods were modified in this version of the library and are not backward compatible with earlier programs. Note that these methods are overloaded. The first method in each pair calls the second method in the pair with dummy parameters for the last three parameters whenever the first version is called. The first overloaded version of each draw method is very useful when it is known that the value of type is either 0, 1, or 2. However, in some cases, it is easier to simply pass the last three parameters (using dummy parameters if necessary) and let the draw method ignore them than it is to perform a test for the type every time the draw method is called. GM03.Point3D.draw(Graphics2D g2D, int type) GM03.Point3D.draw(Graphics2D g2D, int type, double scale, Point3D camera, ColMatrix3D angle) GM03.Vector3D.draw(Graphics2D g2D, Point3D tail, int type) GM03.Vector3D.draw(Graphics2D g2D, Point3D tail, int type, double scale, Point3D camera, ColMatrix3D angle) GM03.Line3D.draw(Graphics2D g2D, int type) GM03.Line3D.draw(Graphics2D g2D, int type, double scale, Point3D camera, ColMatrix3D angle) The incompatibility mainly stems from the fact that much more information is required to draw a 3D perspective projection than is the case for the parallel projection that was included in earlier versions of the library. The value of the parameter named type in the parameter lists for the draw methods shown above specifies the type of projection that will be employed: type = 0, Calls threeDto2DprojectionA described below type = 1, Calls threeDto2DprojectionB described below type = 2, Calls threeDto2DprojectionC described below type = 3, Calls threeDto2DprojectionD described below When the value of type is 0, 1, or 2, the last three parameters are simply ignored in all three draw methods that require those parameters. However, they must be there and this causes this library to be incompatible with drawing programs written to be compatible with earlier versions of the game-math library. Even without those three parameters, the requirement to pass the type of projection format to the draw methods also causes the draw methods in this library to be incompatible with code written for compatibility with earlier versions of the library. Please see the comments at the beginning of the library class named GM01 for a general description of the library. The method convert3Dto2D was removed and replaced by the following four methods. These four methods are private and are not intended to be called from outside the library. threeDto2DprojectionA - This is a parallel oblique projection with a value of 45 degrees for alpha and a value of 30 degrees for phi (see lesson 1618 for an explanation of alpha and phi). The camera position is above the xz plane and to the right of the zy plane. threeDto2DprojectionB - This is a perspective projection with a fixed camera position looking straight down the z-axis. threeDto2DprojectionC - This is a perspective projection from 3D to 2D with a fixed camera position. The camera position is above the xz plane and to the right of the zy plane looking directly at the origin. This method is actually a combination of parallel oblique projection and perspective projection. threeDto2DprojectionD - This is a complex perspective projection with a variable camera position and a variable camera angle. The using programmer interfaces with these four methods through the six draw methods listed earlier, selecting the required type of projection on the basis of the value of the type parameter passed to the draw method. I also added several new methods to the library as described below. These methods are convenience methods and are not necessarily related to perspective projections. The following new method makes it possible to scale the individual components in a vector by different scale factors. GM03.Vector3D.scaleByComponent(ColMatrix3D factor) The following new method makes it possible to rotate a vector around a specified anchor point. GM03.Vector3D.rotate(Point3D anchorPoint, ColMatrix3D angles) The following new method makes it possible to get a clone of an existing ColMatrix3D object: GM03.ColMatrix3D clone() I also added overloaded constructors to the Point2D, Point3D and Vector3D classes to make it easier to instantiate objects of those classes. Tested using JDK 1.6 under WinXP. *********************************************************/ import java.awt.geom.*; import java.awt.*; public class GM03{ //----------------------------------------------------// //This is a parallel projection from 3D to 2D with a // fixed camera position. The camera position is above // the xz plane and to the right of the zy plane. //This method converts a Point3D object into a // Point2D 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: // xp = x + z * cos(phi)/tan(alpha) // yp = y + z * sin(phi)/tan(alpha) //Let phi = 30 degrees and alpha = 45 degrees //Then:cos(phi) = 0.866 // sin(phi) = 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. private static Point2D threeDto2DprojectionA( Point3D data){ return new Point2D( data.getData(0) - 0.866*data.getData(2), data.getData(1) - 0.50*data.getData(2)); }//end threeDto2DprojectionA //----------------------------------------------------// //This is a perspective projection with a fixed camera // position looking straight down the z-axis. //The purpose of this method is to convert the x, y, // and z components in a Point3D object into the x // and y components in a Point2D object using a // relatively simple perspective projection. private static Point2D threeDto2DprojectionB( Point3D data){ //Extract the important values from the incoming // parameters. double aX = data.getData(0); double aY = data.getData(1); double aZ = data.getData(2); //The following specifies a fixed camera position // looking straight down the z-axis from positive to // negative. double cZ = 600; //Compute the distance from the camera to the data // point. Point3D camera = new Point3D(0.0,0.0,cZ); double dZ = camera.getDisplacementVector(data). getLength(); if(dZ == 0.0){ dZ = 0.00001;//prevent DivideByZero error. }//end if //Compute the x,y coordinates on the 2D display. double bX =aX*(cZ/dZ); double bY =aY*(cZ/dZ); return new Point2D(bX,bY); }//end threeDto2DprojectionB //----------------------------------------------------// //This method provides a perspective projection from 3D // to 2D with a fixed camera position. The camera // position is above the xz plane and to the right of // the zy plane looking directly at the origin. This // method was created by combining the parallel // projection algorithm from threeDto2DprojectionA with // the perspective projection algorithm from // threeDto2DprojectionB. private static Point2D threeDto2DprojectionC( Point3D data){ //Compute projected x and y values using the algorithm // from threeDto2DprojectionA. double aX = (data.getData(0) - 0.866*data.getData(2)); double aY = (data.getData(1) - 0.50*data.getData(2)); double aZ = data.getData(2); //Apply the perspective algorithm from // threeDto2DprojectionB. Assume that the camera is // located at 0,0,600. double cZ = 600; //Compute the distance from the camera to the data // point. Point3D camera = new Point3D(0.0,0.0,cZ); double dZ = camera.getDisplacementVector(data). getLength(); if(dZ == 0.0){ dZ = 0.00001;//prevent DivideByZero error. }//end if //Compute the x,y coordinates on the 2D display. double bX =aX*(cZ/dZ); double bY =aY*(cZ/dZ); return new Point2D(bX,bY); }//end threeDto2DprojectionC //----------------------------------------------------// //This method provides a perspective projection from // 3D to 2D with a variable camera position, and a // variable camera angle. //The purpose of this method is to convert the x, y, // and z components in a Point3D object into the x // and y components in a Point2D object using a // perspective projection that allows individual // control over the camera position and camera angle. //BRIEF DESCRIPTION OF PARAMETERS //The scale parameter controls the overall size of the // projected image. //The three components of the camera parameter specify // the x, y, and z coordinates for the position of the // camera. //The three components of the angle parameter specify // rotation of the camera around each of its x, y, and // z axes respectively. The angles are specified in // degrees. //IMPORTANT: This method does not provide reliable // results if the camera allows the point being // projected to get behind it. However, camera rotation // can often be used to prevent the point from getting // behind the camera. For example, the camera can move // all the way around a point and view it from all // sides as long is the camera is rotated in such a way // as to cause it to continue pointing at the point. private static Point2D threeDto2DprojectionD( Point3D data, double scale, Point3D camera, ColMatrix3D angle){ //Rotate the point around the camera in the reverse // direction of the specified camera rotation to // make it appear that the camera has been rotated. //The rotate method requires the rotation angles to // be provided 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. //Rearrange the specified camera rotation angles to // comply with this requirement. ColMatrix3D tempAngles = new ColMatrix3D( -angle.getData(2), -angle.getData(0), -angle.getData(1)); //Create Point3D objects for the data point and // camera locations so that the rotate method of the // Point3D class can be called to perform the // rotation. Then perform the rotation. Point3D tempPoint = data.rotate(camera,tempAngles); //Extract the coordinate values for the rotated // data point. double aX = tempPoint.getData(0); double aY = tempPoint.getData(1); double aZ = tempPoint.getData(2); //Extract the coordinate values for the camera. double cX = camera.getData(0); double cY = camera.getData(1);; double cZ = camera.getData(2); //Compute the distance from the camera to the // data point. The first two distances will // be divided by this distance to provide // the visual effect of perspective. double dX = aX - cX; double dY = aY - cY; double dZ = camera.getDisplacementVector(tempPoint). getLength(); if(dZ == 0.0){ dZ = 0.00001;//prevent DivideByZero error. }//end if //Declare variables for storing the output. double bX; double bY; if(cZ < aZ){ //The point is at or behind the camera. Make sure // that it is no longer visible in the 2D. // projection. Note that this solution is far from // ideal, but I don't have a better solution. It // isn't a problem so long as the camera is outside // the 3D scene, but becomes a problem when the // camera ventures into the scene. bX = 10000*aX; bY = 10000*aY; }else{ //Compute the projected values of the 3D x and y // coordinate values on the 2D plane. Divide by // dZ to give the visual effect of perspective. bX = dX*scale/dZ; bY = dY*scale/dZ; }; return new Point2D(bX,bY); }//end threeDto2DprojectionD //----------------------------------------------------// //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 GM03.ColMatrix2D && Math.abs(((ColMatrix2D)obj).getData(0) - getData(0)) <= 0.00001 && Math.abs(((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 ColMatrix2D add(ColMatrix2D matrix){ return new 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 ColMatrix2D subtract(ColMatrix2D matrix){ return new ColMatrix2D( getData(0)-matrix.getData(0), getData(1)-matrix.getData(1)); }//end subtract //--------------------------------------------------// //Computes the dot product between two ColMatrix2D // objects and returns the result as type double. public double dot(ColMatrix2D matrix){ return getData(0) * matrix.getData(0) + getData(1) * matrix.getData(1); }//end dot //--------------------------------------------------// }//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 GM03.ColMatrix3D && Math.abs(((ColMatrix3D)obj).getData(0) - getData(0)) <= 0.00001 && Math.abs(((ColMatrix3D)obj).getData(1) - getData(1)) <= 0.00001 && Math.abs(((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 ColMatrix3D add(ColMatrix3D matrix){ return new 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 ColMatrix3D subtract(ColMatrix3D matrix){ return new ColMatrix3D( getData(0)-matrix.getData(0), getData(1)-matrix.getData(1), getData(2)-matrix.getData(2)); }//end subtract //--------------------------------------------------// //Computes the dot product between two ColMatrix3D // objects and returns the result as type double. public double dot(ColMatrix3D matrix){ return getData(0) * matrix.getData(0) + getData(1) * matrix.getData(1) + getData(2) * matrix.getData(2); }//end dot //--------------------------------------------------// //Returns a new ColMatrix3D object that is a clone of // the object on which the method is called. public ColMatrix3D clone(){ return new ColMatrix3D(getData(0), getData(1), getData(2)); }//end clone //--------------------------------------------------// }//end class ColMatrix3D //====================================================// //====================================================// public static class Point2D{ ColMatrix2D point; public Point2D(double x,double y){ this(new ColMatrix2D(x,y)); }//end overloaded constructor //--------------------------------------------------// public Point2D(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 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(((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 Vector2D getDisplacementVector( Point2D point){ return new Vector2D(new ColMatrix2D( point.getData(0)-getData(0), point.getData(1)-getData(1))); }//end getDisplacementVector //--------------------------------------------------// //Adds a Vector2D to a Point2D producing a // new Point2D. public Point2D addVectorToPoint(Vector2D vec){ return new Point2D(new 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 Point2D rotate(Point2D anchorPoint, double angle){ Point2D newPoint = this.clone(); double tempX ; double tempY; //Translate anchorPoint to the origin Vector2D tempVec = new 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 Point2D scale(ColMatrix2D scale){ return new Point2D(new ColMatrix2D( getData(0) * scale.getData(0), getData(1) * scale.getData(1))); }//end scale //--------------------------------------------------// }//end class Point2D //====================================================// public static class Point3D{ ColMatrix3D point; //The following is an overloaded constructor that is // intended to make it easier to instantiate objects // of this type. public Point3D(double x,double y,double z){ this(new ColMatrix3D(x,y,z)); }//end constructor //--------------------------------------------------// public Point3D(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 overloaded draw method calls the method that // follows with dummy parameters. public void draw(Graphics2D g2D,int type){ draw(g2D, type, 0, new Point3D(0,0,0), new ColMatrix3D(0,0,0)); }//end overloaded draw method //--------------------------------------------------// //This method draws a small circle around the location // of the point on the specified graphics context. //Note that this method is overloaded. public void draw(Graphics2D g2D, int type, double scale, Point3D camera, ColMatrix3D angle){ //Get 2D projection coordinate values. Point2D temp = null; //Test the incoming type value to determine which // projection method to call. if(type == 0){ //Get the values required to draw a parallel // projection ignoring the last three incoming // parameters. temp = threeDto2DprojectionA(this); }else if(type == 1){ //Get the values required to draw a simple // perspective projection ignoring the last three // incoming parameters. temp = threeDto2DprojectionB(this); }else if(type == 2){ //Get the values required to draw a perspective // projection ignoring the last three incoming // parameters. temp = threeDto2DprojectionC(this); }else if(type == 3){ //Use all of the incoming parameters and get the // values required to draw a complex perspective // projection. temp = threeDto2DprojectionD(this,scale,camera,angle); }//end else 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 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(((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 Vector3D getDisplacementVector(Point3D point){ return new Vector3D(new 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 Point3D addVectorToPoint(Vector3D vec){ return new Point3D(new 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. //In other words, the method begins by rotating the // point around the z-axis of the anchor point. Then // it rotates the point around the x-axis of the // anchor point. Finally, it rotates the point around // the y-axis of the anchor point. //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 GM03.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 Point3D rotate(Point3D anchorPoint, ColMatrix3D angles){ Point3D newPoint = this.clone(); double tempX ; double tempY; double tempZ; //Translate anchorPoint to the origin Vector3D tempVec = new 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 Point3D scale(ColMatrix3D scale){ return new 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{ ColMatrix2D vector; public Vector2D(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,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 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(( (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 Vector2D add(Vector2D vec){ return new 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 Vector2D scale(Double factor){ return new 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 Vector2D negate(){ return new 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 Vector2D normalize(){ double length = getLength(); return new Vector2D(new ColMatrix2D( getData(0)/length, getData(1)/length)); }//end normalize //--------------------------------------------------// //Computes the dot product between two Vector2D // objects and returns the result as type double. public double dot(Vector2D vec){ ColMatrix2D matrixA = getColMatrix(); ColMatrix2D matrixB = vec.getColMatrix(); return matrixA.dot(matrixB); }//end dot //--------------------------------------------------// //Computes and returns the angle between two Vector2D // objects. The angle is returned in degrees as type // double. public double angle(Vector2D vec){ Vector2D normA = normalize(); Vector2D normB = vec.normalize(); double normDotProd = normA.dot(normB); return Math.toDegrees(Math.acos(normDotProd)); }//end angle //--------------------------------------------------// }//end class Vector2D //====================================================// public static class Vector3D{ ColMatrix3D vector; //This is an overloaded constructor that is intended // to make it easier to instantiate objects of this // class. public Vector3D(double x,double y,double z){ this(new ColMatrix3D(x,y,z)); }//end constructor //--------------------------------------------------// public Vector3D(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 overloaded draw method calls the method that // follows with dummy parameters. public void draw(Graphics2D g2D, Point3D tail, int type){ draw(g2D, tail, type, 0, new Point3D(0,0,0), new ColMatrix3D(0,0,0)); }//end overloaded draw method //--------------------------------------------------// //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. The type of projection used is specified by // the value of the incoming type parameter. Note // that this method is overloaded. public void draw(Graphics2D g2D, Point3D tail, int type, double scale, Point3D camera, ColMatrix3D angle){ //Get a projection of the tail Point2D tail2D = null; if(type == 0){ //Parallel projection tail2D = threeDto2DprojectionA(tail); }else if(type == 1){ //Simple perspective projection tail2D = threeDto2DprojectionB(tail); }else if(type == 2){ //Simple perspective projection tail2D = threeDto2DprojectionC(tail); }else if(type == 3){ //complex perspective projection tail2D = threeDto2DprojectionD( tail,scale,camera,angle); }//end if //Get the 3D location of the head Point3D head = new Point3D( tail.point.add(this.getColMatrix())); //Get a projection of the head Point2D head2D = null; if(type == 0){ //Parallel projection head2D = threeDto2DprojectionA(head); }else if(type == 1){ //Simple perspective projection head2D = threeDto2DprojectionB(head); }else if(type == 2){ //Simple perspective projection head2D = threeDto2DprojectionC(head); }else if(type == 3){ //Complex perspective projection head2D = threeDto2DprojectionD(head,scale,camera,angle); }//end if 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 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(((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 Vector3D add(Vector3D vec){ return new 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 Vector3D scale(Double factor){ return new Vector3D(new ColMatrix3D( getData(0) * factor, getData(1) * factor, getData(2) * factor)); }//end scale //--------------------------------------------------// //Multiplies the components of this vector by three // scale factors received as an incoming parameter // and returns the scaled vector. public Vector3D scaleByComponent( ColMatrix3D factor){ return new Vector3D(new ColMatrix3D( getData(0) * factor.getData(0), getData(1) * factor.getData(1), getData(2) * factor.getData(2))); }//end scale //--------------------------------------------------// //Changes the sign on each of the vector components // and returns the negated vector. public Vector3D negate(){ return new 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 Vector3D normalize(){ double length = getLength(); return new Vector3D(new ColMatrix3D( getData(0)/length, getData(1)/length, getData(2)/length)); }//end normalize //--------------------------------------------------// //Computes the dot product between two Vector3D // objects and returns the result as type double. public double dot(Vector3D vec){ ColMatrix3D matrixA = getColMatrix(); ColMatrix3D matrixB = vec.getColMatrix(); return matrixA.dot(matrixB); }//end dot //--------------------------------------------------// //Computes and returns the angle between two Vector3D // objects. The angle is returned in degrees as type // double. public double angle(Vector3D vec){ Vector3D normA = normalize(); Vector3D normB = vec.normalize(); double normDotProd = normA.dot(normB); return Math.toDegrees(Math.acos(normDotProd)); }//end angle //--------------------------------------------------// //Note that a vector is not very conducive to rotation // because it has no location. However, it is possible // to create a Point3D object with the same components // as the vector, rotate the point around a specified // anchor, and then create a new vector having the // same components as the rotated point. See the // rotate method of the Point3D class for technical // details regarding the rotation of a point. public Vector3D rotate(Point3D anchorPoint, ColMatrix3D angles){ ColMatrix3D tempA = this.getColMatrix(); Point3D tempB = new Point3D(tempA); tempB = tempB.rotate(anchorPoint,angles); return new Vector3D(tempB.getColMatrix()); }//end rotate //--------------------------------------------------// }//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{ Point2D[] line = new Point2D[2]; public Line2D(Point2D tail,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 ColMatrix2D( tail.getData(0),tail.getData(1))); this.line[1] = new Point2D(new 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 Point2D getTail(){ return line[0]; }//end getTail //--------------------------------------------------// public Point2D getHead(){ return line[1]; }//end getHead //--------------------------------------------------// public void setTail(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 ColMatrix2D( newPoint.getData(0),newPoint.getData(1))); }//end setTail //--------------------------------------------------// public void setHead(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 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{ Point3D[] line = new Point3D[2]; public Line3D(Point3D tail,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 ColMatrix3D( tail.getData(0), tail.getData(1), tail.getData(2))); this.line[1] = new Point3D(new 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 Point3D getTail(){ return line[0]; }//end getTail //--------------------------------------------------// public Point3D getHead(){ return line[1]; }//end getHead //--------------------------------------------------// public void setTail(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 ColMatrix3D( newPoint.getData(0), newPoint.getData(1), newPoint.getData(2))); }//end setTail //--------------------------------------------------// public void setHead(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 ColMatrix3D( newPoint.getData(0), newPoint.getData(1), newPoint.getData(2))); }//end setHead //--------------------------------------------------// //This overloaded draw method calls the method that // follows with dummy parameters. public void draw(Graphics2D g2D, int type){ draw(g2D, type, 0, new Point3D(0,0,0), new ColMatrix3D(0,0,0)); }//end overloaded draw method //--------------------------------------------------// //Project a 3D line onto a 2D plane. The value of the // incoming parameter named type is used to determine // the type of projection that will be drawn. Note // this method is overloaded. public void draw(Graphics2D g2D, int type, double scale, Point3D camera, ColMatrix3D angle){ //Get projection coordinates. Point2D tail = null; Point2D head = null; if(type == 0){ //Parallel projection. tail = threeDto2DprojectionA(getTail()); head = threeDto2DprojectionA(getHead()); }else if(type == 1){ //Simple perspective projection. tail = threeDto2DprojectionB(getTail()); head = threeDto2DprojectionB(getHead()); }else if(type == 2){ //Simple perspective projection. tail = threeDto2DprojectionC(getTail()); head = threeDto2DprojectionC(getHead()); }else if(type == 3){ //Complex perspective projection. tail = threeDto2DprojectionD( getTail(),scale,camera,angle); head = threeDto2DprojectionD( getHead(),scale,camera,angle); }//end if drawLine(g2D,tail.getData(0), tail.getData(1), head.getData(0), head.getData(1)); }//end draw //--------------------------------------------------// }//end class Line3D //====================================================// }//end class GM03 //======================================================// |
Listing 27. Source code for the game program named Cannonball01.
/*Cannonball01.java Copyright 2008, R.G.Baldwin Revised 03/31/08 This is a modern interactive 3D version of the early cannonball games that appeared on computer terminals long before the advent of the personal computers. INSTRUCTIONS FOR PLAYING THE GAME 1. Adjust the x and z-coordinates of the safety net. 2. Adjust the azimuth, elevation, and muzzle velocity of the cannon. 3. Click the Fire button and watch as the human cannonball flies through the air on her way toward the safety net. 4. Observe the impact location in the birds-eye view. If the impact location is outside the safety net, as indicated by a small red circle, repeat steps 2 and 3. Continue this process until the human cannonball lands in the net as indicated by a green circle inside the safety net and a beep emitted by the computer. (Note that the default settings of the sliders are such that simply starting the program and clicking the Fire button will cause the human cannonball to land successfully in the safety net.) DESCRIPTION OF THE GAME The scenario is that of a human cannonball with a video camera mounted on her helmet. The playing field consists of the cannon (which is not visible) and a safety net located somewhere within the range of the cannon. The objective is for the player to adjust the firing direction (azimuth), the firing elevation, and the muzzle velocity of the cannon so as to cause the human cannonball (hereafter referred to simply as the cannonball) to land in the safety net when she is fired from the cannon. As the cannonball flies through the air, she keeps the video camera trained on the center of the safety net. This causes the image of the safety net to be displayed on the screen during the entire trajectory of the cannonball. After the cannonball lands, a birds-eye view of the playing field is displayed. If the cannonball landed in the safety net, a green circle appears in the net showing where the cannonball landed and the computer beeps. If the cannonball landed outside of the net, a red circle marks the spot where the cannonball landed. In this case, the computer does not beep. At this point, the player can adjust the parameters and fire the cannon again. The safety net consists of a circle represented by twelve points on its circumference with lines connecting all of the points to form a mesh. In addition, there are three outriggers that are intended to look like guy wires holding the net in place. Because of physical space requirements, the graphical output and the user input panel (GUI) are displayed in two separate JFrame objects. The GUI provides sliders that allow the player to specify the following values: The x-coordinate of the center of the safety net. The z-coordinate of the center of the safety net. (The y-coordinate is always zero because the safety net and the cannon are both at ground level.) The direction that the cannon is pointing (azimuth); The elvation of the cannon. The muzzle velocity of the cannon. In addition, the GUI provides five non-editable JTextField objects that display the current value of an associated slider. Finally, the GUI provides a JButton object that the user clicks to fire the cannon. The projectile motion is based on the equations of motion for a projectile in a vacuum. When the user adjusts the x-coordinate or the z-coordinate of the net, the program provides a view of the playing field from a vantage point immediately above the cannon, which is at the origin. Three blue lines radiate from the cannon. The center line is in line with the default firing direction of the cannon. The other two lines form a 45-degree angle on either side of the center line. As the user adjusts either coordinate by moving the slider, the safety net moves within the playing field accordingly. When the player adjusts the azimuth of the cannon, the view of the playing field reverts to ground level. A vertical red line shows the azimuth that the cannon is pointing. The user can rotate the cannon until it lines up with the center of the safety net to ensure that the cannon is pointing in the direction of the safety net. When the player adjusts the elevation of the cannon, the view of the playing field is at ground level. A horizontal red line shows the pointing elevation of the cannon relative to the safety net that is at ground level. Because the muzzle velocity is simply a numeric quantity with no spatial representation, there is no animation when the user adjusts the muzzle velocity slider. Tested using JDK 1.6 under WinXP. *********************************************************/ import java.awt.*; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.*; import javax.swing.event.ChangeListener; import javax.swing.event.ChangeEvent; class Cannonball01{ public static void main(String[] args){ new Display(); }//end main }//end controlling class Cannonball01 //======================================================// class Display extends JFrame implements ChangeListener,ActionListener{ //Save a reference to this object for access by the GUI. Display displayObj; //Specify the horizontal and vertical size of a JFrame // object. int hSize = 400; int vSize = 400; Image osi;//an off-screen image int osiWidth;//off-screen image width int osiHeight;//off-screen image height MyCanvas myCanvas;//a subclass of Canvas double pi = Math.PI;//a convenience variable //The following are 3D perspective drawing parameters. GM03.Point3D camera = new GM03.Point3D(0,0,0); GM03.ColMatrix3D angle = new GM03.ColMatrix3D(0,0,0); double scale = 300; //Specifies the type of projection. This value is // required by the draw methods in the GM03 game-math // library. final int type = 3; //Variables used to save the radius and location of the // safety net. The radius cannot be modified by the // player but the location can be modified by the // player. double radius = 10; GM03.Point3D centerPoint = new GM03.Point3D(0,0,100); Thread animator;//A reference to an animation thread Graphics2D g2D;//A reference to the off-screen image. //Determines whether to draw angular guidelines when // positioning the net. boolean guideLines; //Determines whether to draw a verticalSightLine used // for adjusting the azimuth of the cannon. boolean verticalSightLine; //Determines whether to draw a horizontalSightLine used // for adjusting the elevation of the cannon. boolean horizontalSightLine; //References to the current position of the camera. double cameraX; double cameraY; double cameraZ; //User input and data output components. JSlider positionXslider; JSlider positionZslider; JTextField positionXoutput; JTextField positionZoutput; JSlider azimuthSlider; JSlider elevationSlider; JTextField azimuthOutput; JTextField elevationOutput; JSlider velocitySlider; JTextField velocityOutput; JButton fireButton; //Values obtained from user input. double velocity; double azimuthAngle; double elevationAngle; //----------------------------------------------------// Display(){//constructor //Set JFrame size, title, and close operation. setSize(hSize,vSize); setTitle("Copyright 2008,R.G.Baldwin"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); //Create a new drawing canvas and add it to the // center of the JFrame. myCanvas = new MyCanvas(); this.getContentPane().add(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); 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. GM03.translate(g2D,0.5*osiWidth,-0.5*osiHeight); //Make a reference to this object available to the // constructor for the GUI class. displayObj = this; //Instantiate the JFrame that contains the user input // components. Those user input components were // placed in a separate JFrame object simply because // there isn't sufficient space available to contain // them in the main display. new GUI(); //Register this object as a change listener on the // sliders. This can't be done until the GUI object // has been instantiated. positionXslider.addChangeListener(this); positionZslider.addChangeListener(this); azimuthSlider.addChangeListener(this); elevationSlider.addChangeListener(this); velocitySlider.addChangeListener(this); //Register this object as an action listener on the // Fire button. fireButton.addActionListener(this); //Elevate the camera above the origin and tilt it // downward for initial adjustments. //Note that the camera is rotated by 180 degrees // around the y-axis to cause it to be shooting // along the positive z-axis. camera.setData(1,100); angle = new GM03.ColMatrix3D(45,180,0); //Project the scene onto the 2D off-screen image and // display it on the screen. drawTheImage(g2D); myCanvas.repaint(); }//end constructor //----------------------------------------------------// //The purpose of this method is to create the 3D scene // and project it onto the 2D off-screen image. void drawTheImage(Graphics2D g2D){ //Erase the screen g2D.setColor(Color.WHITE); GM03.fillRect(g2D, -osiWidth/2, osiHeight/2, osiWidth, osiHeight); //Draw three vectors that simulate guy wires and // anchors holding up the net. g2D.setColor(Color.GRAY); new GM03.Vector3D(1.5*radius,0.0,1.5*radius). draw(g2D,new GM03.Point3D( centerPoint.getData(0), 0.0, centerPoint.getData(2)), type,scale,camera,angle); new GM03.Vector3D(-1.5*radius,0.0,1.5*radius). draw(g2D,new GM03.Point3D( centerPoint.getData(0), 0.0, centerPoint.getData(2)), type,scale,camera,angle); new GM03.Vector3D(0.0,0.0,-1.5*1.414*radius). draw(g2D,new GM03.Point3D( centerPoint.getData(0), 0.0, centerPoint.getData(2)), type,scale,camera,angle); //Define points that represent a circle on the x-z // plane. Store the points in an array. int numberPoints = 12; GM03.Point3D[] points = new GM03.Point3D[numberPoints]; for(int cnt = 0;cnt < points.length;cnt++){ points[cnt] = new GM03.Point3D( centerPoint.getData(0) + radius*Math.cos( (cnt*360/numberPoints)*pi/180), centerPoint.getData(1), centerPoint.getData(2) + radius*Math.sin( (cnt*360/numberPoints)*pi/180)); }//end for loop //Draw lines that connect every point to every other // point to create the mesh in the net. g2D.setColor(Color.GRAY); for(int row = 0;row < points.length;row++){ for(int col = row;col < points.length;col++){ new GM03.Line3D(points[row],points[col]). draw(g2D,type,scale,camera,angle); }//end inner loop }//end outer loop //Test flags and draw certain lines on the display // according to the values of the flags. if(guideLines){ //Draw three lines that radiate out from the origin // with an angle of 45 degrees between them. g2D.setColor(Color.BLUE); new GM03.Line3D(new GM03.Point3D(0.0,0.0,0.0), new GM03.Point3D(0.0,0.0,400.0)). draw(g2D,type,scale,camera,angle); new GM03.Line3D(new GM03.Point3D(0.0,0.0,0.0), new GM03.Point3D(400.0,0.0,400.0)). draw(g2D,type,scale,camera,angle); new GM03.Line3D(new GM03.Point3D(0.0,0.0,0.0), new GM03.Point3D(-400.0,0.0,400.0)). draw(g2D,type,scale,camera,angle); }//end if if(verticalSightLine){ //Draw a vertical sighting line at the center of the // display that is used to adjust the azimuth of the // cannon. g2D.setColor(Color.RED); GM03.drawLine(g2D,0.0,-osiHeight/2,0.0,osiHeight/2); }//end if if(horizontalSightLine){ //Draw a horizontal sighting line at the center of // the display that is used to adjust the elevation // of the cannon. g2D.setColor(Color.RED); GM03.drawLine(g2D,-osiWidth/2,0.0,osiWidth/2,0.0); }//end if }//end drawTheImage //====================================================// //This method is called to respond to change events on // any of the sliders. public void stateChanged(ChangeEvent e){ //Elevate the camera above the origin and tilt it // downward //Note that the camera is rotated by 180 degrees // around the y-axis to cause it to be shooting along // the positive z-axis. camera.setData(0,0); camera.setData(1,100); camera.setData(2,0); angle.setData(0,45); angle.setData(1,180); angle.setData(2,0); //Get the ID of the slider that was the source of the // event. JSlider source = (JSlider)e.getSource(); //Take the appropriate action on the basis of the ID // of the source of the event. if(source.getName().equals("positionXslider")){ //Set the x-coordinate of the net and display the // coordinate value. centerPoint.setData(0,source.getValue()); positionXoutput.setText("" + source.getValue()); guideLines = true;//Draw angular guidelines verticalSightLine = false;//No verticalSightLine. horizontalSightLine = false;//No horizontalSightLine }else if(source.getName().equals("positionZslider")){ //Set the z-coordinate of the net and display the // coordinate value. centerPoint.setData(2,source.getValue()); positionZoutput.setText("" + source.getValue()); guideLines = true;//Draw angular guidelines verticalSightLine = false;//No verticalSightLine. horizontalSightLine = false;//No horizontalSightLine }else if(source.getName().equals("azimuthSlider")){ //Rotate the cannon around the y-axis. azimuthAngle = source.getValue(); //Display direction that the cannon is pointing. azimuthOutput.setText("" + source.getValue()); //Set the camera to ground level with no vertical // tilt. camera.setData(1,0.0); angle.setData(0,0.0); //Rotate the camera to point in the same direction // as the cannon. angle.setData(1,180 + source.getValue()); guideLines = false;//Do not draw angular guidelines verticalSightLine = true;//Draw verticalSightLine. horizontalSightLine = false;//No horizontalSightLine }else if(source.getName().equals("elevationSlider")){ //Set and display the elevation angle for the // cannon. elevationAngle = source.getValue(); elevationOutput.setText("" + source.getValue()); //Set the camera to ground level with a vertical // tilt equal to the elevation angle. Scale the // angle by 1/4 to keep the net within the bounds of // the display. camera.setData(1,0.0); angle.setData(0,-source.getValue()/4); guideLines = false;//Draw angular guidelines verticalSightLine = false;//No verticalSightLine. //Draw horizontalSightLine horizontalSightLine = true; }else if(source.getName().equals("velocitySlider")){ //Set and display the velocity value. velocity = source.getValue(); velocityOutput.setText("" + source.getValue()); guideLines = false;//Do not draw angular guidelines verticalSightLine = false;//No verticalSightLine. horizontalSightLine = false;//No horizontalSightLine }//end if //Project the scene onto the 2D off-screen image and // display it on the screen. drawTheImage(g2D); myCanvas.repaint(); }//end stateChanged //----------------------------------------------------// //This method is called whenever the user clicks the // fireButton public void actionPerformed(ActionEvent e){ //Disable the sliders while the cannonball is in // flight. positionXslider.setEnabled(false); positionZslider.setEnabled(false); azimuthSlider.setEnabled(false); elevationSlider.setEnabled(false); velocitySlider.setEnabled(false); //Set all of the parameters based on the current // values of the associated sliders. centerPoint.setData(0,positionXslider.getValue()); centerPoint.setData(2,positionZslider.getValue()); azimuthAngle = azimuthSlider.getValue(); elevationAngle = elevationSlider.getValue(); velocity = velocitySlider.getValue(); guideLines = false;//Do not draw angular guidelines verticalSightLine = false;//No verticalSightLine horizontalSightLine = false;//No horizontalSightLine. //Instantiate the animation thread and start it // running. animator = new Animator(); animator.start(); }//end actionPerformed //====================================================// //This is an inner class of the Display 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 the animation thread class for the program. It // is an inner class. class Animator extends Thread{ //Gradational acceleration constant. final double gravity = 32.174;//ft/sec/sec //The following velocity components are computed from // the user-specified velocity, azimuth, and // elevation values. double initialVelocityX; double initialVelocityY; double initialVelocityZ; //Store the camera angle and position here. GM03.ColMatrix3D cameraAngle; GM03.Point3D cameraPosition; //Unit vectors along the x, y, and z axes. GM03.Vector3D uVectorX = new GM03.Vector3D(1,0,0); GM03.Vector3D uVectorY = new GM03.Vector3D(0,1,0); GM03.Vector3D uVectorZ = new GM03.Vector3D(0,0,1); Animator(){//constructor //Compute the x,y, and z-components of the velocity. initialVelocityZ = velocity * Math.cos(Math.toRadians(azimuthAngle)); initialVelocityX = velocity * Math.sin(Math.toRadians(azimuthAngle)); initialVelocityY = velocity * Math.cos( Math.toRadians(elevationAngle)); //Initialize the camera position and angle. cameraPosition = new GM03.Point3D(0,0,0); //Note that the camera is rotated by 180 degrees // around the y-axis to cause it to be shooting // along the positive z-axis. cameraAngle = new GM03.ColMatrix3D(0,180,0); //Copy the camera position and angle to the objects // used by the draw methods. camera = cameraPosition.clone(); angle = cameraAngle.clone(); //Project the scene onto the 2D off-screen image and // display it on the screen. drawTheImage(g2D); myCanvas.repaint(); }//end constructor //--------------------------------------------------// //This method is called when the start method is // called on the animation thread. public void run(){ double time = 0.0; //This is one of the factors that controls the // animation speed. double deltaTime = 0.01; //Position the camera at the origin. cameraX = 0.0; cameraY = 0.0; cameraZ = 0.0; //Compute, draw, and sleep while(cameraY >= 0.0){ //Loop until the camera completes the trajectory // and goes slightly negative in terms of the // y-coordinate. //Compute and save the new position of the camera // based on the equations of motion. cameraX = initialVelocityX * time; cameraZ = initialVelocityZ * time; cameraY = initialVelocityY*time - (gravity*time*time)/2.0; cameraPosition.setData(0,cameraX); cameraPosition.setData(1,cameraY); cameraPosition.setData(2,cameraZ); //Copy the camera information saved locally to the // instance variable that is used by the various // draw methods. camera = cameraPosition.clone(); //Increment time. time += deltaTime; //Compute the angle from the camera to the center // of the net. GM03.Vector3D displacement = cameraPosition. getDisplacementVector(centerPoint); double alpha = Math.acos(displacement.normalize(). dot(uVectorX)); double theta = Math.acos(displacement.normalize(). dot(uVectorZ)); //Convert the angles to degrees and save them. cameraAngle.setData(0,Math.toDegrees(theta)); cameraAngle.setData( 1,180 + (90 - Math.toDegrees(alpha))); angle = cameraAngle.clone(); //Project the scene onto the 2D off-screen image // and display it on the screen. drawTheImage(g2D); myCanvas.repaint(); //Put the thread to sleep temporarily. This is one // of the factors that controls the animation // speed. try{ Thread.currentThread().sleep(37); }catch(InterruptedException e){ e.printStackTrace(); }//end catch }//end animation loop //The animation loop has terminated meaning that the // y-coordinate for the camera has gone slightly // negative, signaling that it has reached the end // of its trajectory. //Pause for a short period to allow the player to // view the impact in perspective before switching // to the birds-eye view. try{ Thread.currentThread().sleep(2000); }catch(InterruptedException e){ e.printStackTrace(); }//end catch //Enable the sliders when the cannonball lands to // allow the player to make adjustments and fire // the cannon again. positionXslider.setEnabled(true); positionZslider.setEnabled(true); azimuthSlider.setEnabled(true); elevationSlider.setEnabled(true); velocitySlider.setEnabled(true); //Pull up for a birds-eye view directly above the // safety net. camera.setData(0,centerPoint.getData(0)); camera.setData(1,175); camera.setData(2,centerPoint.getData(2)); angle.setData(0,90); angle.setData(1,180); angle.setData(2,0); guideLines = true;//Draw angular guideLines. verticalSightLine = false;//No verticalSightLine. horizontalSightLine = false;//No horizontalSightLine //Project the scene onto the 2D off-screen image. drawTheImage(g2D); //Now determine if the human cannonball landed in // the safety net. if(cameraPosition. getDisplacementVector(centerPoint).getLength() < radius){ //The human cannonball hit the net. //Draw a green circle at the point of impact. g2D.setColor(Color.GREEN); cameraPosition.draw(g2D,type,scale,camera,angle); //Beep the computer to indicate success. Toolkit.getDefaultToolkit().beep(); }else{ //The human cannonball missed the net. //Draw a red circle at the point of impact and // do not beep the computer. g2D.setColor(Color.RED); cameraPosition.draw(g2D,type,scale,camera,angle); }//end else //Draw a blue circle at the origin. g2D.setColor(Color.BLUE); new GM03.Point3D(0,0,0).draw( g2D,type,scale,camera,angle); //Cause the overridden paint method belonging to // myCanvas to be executed. myCanvas.repaint(); }//end run method }//end inner class Animator //====================================================// //This is an inner class of the Display class. An object // of this class provides a home for the user input // components. //An object of this class is needed only because there // isn't sufficient space on the object of the Display // class to contain all of the user input components. // This object is displayed to the right of the object // of the Display class on the screen. class GUI extends JFrame{ GUI(){ //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("Net Position X",JLabel.CENTER)); controlPanel.add( new JLabel("Net Position Z",JLabel.CENTER)); positionXslider = new JSlider(-150,150,0); positionXslider.setName("positionXslider"); positionXslider.setMinorTickSpacing(10); positionXslider.setMajorTickSpacing(75); positionXslider.setPaintTicks(true); positionXslider.setPaintLabels(true); positionZslider = new JSlider(0,300,100); positionZslider.setName("positionZslider"); positionZslider.setMinorTickSpacing(10); positionZslider.setMajorTickSpacing(75); positionZslider.setPaintTicks(true); positionZslider.setPaintLabels(true); controlPanel.add(positionXslider); controlPanel.add(positionZslider); positionXoutput = new JTextField("" + positionXslider.getValue()); positionXoutput.setEditable(false); positionZoutput = new JTextField("" + positionZslider.getValue()); positionZoutput.setEditable(false); controlPanel.add(positionXoutput); controlPanel.add(positionZoutput); controlPanel.add( new JLabel("Cannon Azimuth",JLabel.CENTER)); controlPanel.add( new JLabel("Cannon Elevation",JLabel.CENTER)); azimuthSlider = new JSlider(-150,150,0); azimuthSlider.setName("azimuthSlider"); azimuthSlider.setMinorTickSpacing(10); azimuthSlider.setMajorTickSpacing(75); azimuthSlider.setPaintTicks(true); azimuthSlider.setPaintLabels(true); controlPanel.add(azimuthSlider); elevationSlider = new JSlider(0,150,50); elevationSlider.setName("elevationSlider"); elevationSlider.setMinorTickSpacing(10); elevationSlider.setMajorTickSpacing(75); elevationSlider.setPaintTicks(true); elevationSlider.setPaintLabels(true); controlPanel.add(elevationSlider); azimuthOutput = new JTextField("" + azimuthSlider.getValue()); azimuthOutput.setEditable(false); elevationOutput = new JTextField("" + elevationSlider.getValue()); elevationOutput.setEditable(false); controlPanel.add(azimuthOutput); controlPanel.add(elevationOutput); controlPanel.add( new JLabel("Muzzle Velocity",JLabel.CENTER)); //Empty field controlPanel.add(new JLabel("",JLabel.CENTER)); velocitySlider = new JSlider(0,150,50); velocitySlider.setName("velocitySlider"); velocitySlider.setMinorTickSpacing(10); velocitySlider.setMajorTickSpacing(75); velocitySlider.setPaintTicks(true); velocitySlider.setPaintLabels(true); controlPanel.add(velocitySlider); controlPanel.add( new JLabel("Fire Button",JLabel.CENTER)); velocityOutput = new JTextField("" + velocitySlider.getValue()); velocityOutput.setEditable(false); controlPanel.add(velocityOutput); fireButton = new JButton("Fire"); controlPanel.add(fireButton); //Add the control panel to the JFrame. this.getContentPane().add( BorderLayout.CENTER,controlPanel); //Set the position and size of the object on the // screen. setBounds(displayObj.getWidth(),0,300,400); setTitle("Copyright 2008,R.G.Baldwin"); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setVisible(true); }//end constructor }//end class GUI //====================================================// }//end class Display //======================================================// |
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.