`
univasity
  • 浏览: 809762 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

[JSR-184][3D编程指南]Part IV:M3G built-in collision,light physics and camera perspec

    博客分类:
  • J2me
阅读更多

<!-- 整理收集自网络,收藏以便日后查阅 -->

 

Compliments of Redikod:
3D programming tutorial part four: M3G built-in collision, light physics and camera perspective

We've now reached part four in the "3D programming for mobile devices using M3G (JSR 184)" series of tutorials from Mikael Baros, Senior Programmer at Redikod. Building on the first three parts, he now takes you into the world of collisions and camera perspective. After explaining some theory, he guides you through creating a 3D Pong game.

Below you can download the source code and application package zip files for part four, as well as link to the first three installments of the tutorial.


Part four: M3G built-in collision, light physics and camera perspective

Source Code (Java classes and resources)>>

Application Package (JAR/JAD)>>

Here's Mikael with part four.


 

Introduction

Welcome to the fourth installment of this M3G tutorial series. This time around I'll teach you some very important things regarding the dynamics of any 3D game, namely collision and physics. I'll also show you how the camera perspective matrix works and what you can do by manipulating it.

As always, check here if you ever get lost:

First of all, and probably most importantly, the dedicated Mobile Java 3D web section on Sony Ericsson Developer World. Second, if you ever get stuck, go to the Sony Ericsson Mobile Java 3D forum . For everything else, use the Sony Ericsson World Developer web portal , there you will find the answers to your questions and more.

There are many goals in part four of this tutorial series, so I'll break them down for you. This is what you'll hopefully accomplish:

  • Learn about the perspective matrix and camera manipulation
  • Learn how to use M3G's built-in mechanism for fast collision detection
  • Gain a basic understanding of how physics can alter a game's feel towards being more realistic
  • Implement a very simple pong-like game in 3D
Maybe a bit too much? Nah, not really. You'll see that most of the things I'll show you today are really easy once you get a hang of them. This tutorial will be a bit theory-heavy to begin with, but will lighten up when we get to the actual game we're going to make.

Since the code is meant for educational purposes it isn't optimal nor does it cover all the errors that might occur. These are more advanced topics that will be addressed later on.

What you should know
Before reading this I'd like you to have basic knowledge of 3D math, more specifically vector and matrix operations. You should have of course read the first three tutorials as well, just to get acquainted with the M3G API and its rendering pipeline.

Camera and Perspective
Now, you might be wondering what's all this talk about perspective? Well, the perspective matrix is technically a mathematical entity in the 3D engine that defines a few parameters that alter our way of viewing our scene. But, instead of explaining the pure math behind it to you, I'll explain the principle.

When we are viewing 3D objects in M3G, we are doing it by placing a camera at a defined position in space, and view the world around us from there with a specific field of view. For instance; humans don't have the same field of view as birds. Some birds have a field of view of up to 360 degrees while humans usually have about 150–180 degrees. Now, the field of view or FOV (which is what I'll call it from now on, so remember it!) in a 3D game is basically a description of how the camera (think; player) sees the objects in our world. There are four parameters that I'll talk about today. These parameters are the same parameters that are needed in a call to the Camera.setPerspective method in M3G. Here is what it looks like:

 

setPerspective(float fovy, float aspectRatio, float near, float far)

 

The y-axis field of view, or fovy for short
Most APIs usually need a field of view, in degrees, to be able to construct a perspective matrix. M3G is no different. The first parameter of the setPerspective method is a float fovy. The fovy stands for the field of view on the y-axis. Normally, people associate this with the field of view on the x-axis (or how wide) you can see. However, M3G is interested in how high you can see. Normal values for this constant range from 45–90 degrees depending on what you're planning to use. If you have a hard time choosing, stick with a fovy of 60 degrees.
So what does the fovy do? Well, it simply tells the camera at what angle objects should be discarded for rendering. For instance, if an object is at a y-angle of 110 degrees up from the camera and our fovy is only 60 degrees, we won't be able to see it until we actually pitch the camera up enough (to about 50 degrees). This is a very crucial component in making a 3D game's visual component look realistic. A too small fovy gives the effect of not being able to see anything except that which is dead ahead while a too large value allows you to see much more than the normal human eye is accustomed to, and thus deemed too unrealistic. In this tutorial, you'll see how you can manipulate the fovy to change the view of your game depending on what you want to show.
The aspect ratio
This is a pretty simple parameter, since it's a fractional number that tells the engine the relation of the width against the height of the current screen. Most computer screens have a aspect ratio of 4:3 (that is, the height is ¾ of the width. 600/800 = 0.75 = ¾) while normal mobile phones have a wide variety of aspect ratios. To get this variable, all you need to do is to divide the current width of the screen, by the height.
I won't say more on this as it's pretty self-explanatory. You have to know the aspect ratio of your phone screen to be able to display geometry that isn't skewed in unnatural ways.
The near and far clipping planes
Another very easy parameter. The near and far clipping planes simply define how far away/close an object can be and still be rendered. So for instance, setting the near clipping plane to 0.1 and far to 50 means that all objects that are closer than 0.1 units to the camera won't get rendered. The same goes for objects that are 50 units or more away from the camera. Actually, 0.1 and 50 are pretty normal values but of course you can change these to whatever you need in your game. It all comes down to how you scale your game and use the floating units. In this tutorial, we'll use 0.1 as the near clipping plane parameter, and 50 as the far clipping plane.
Why, oh why, do I need to know this?
Well, there are many answers to the above question, but I'll start by answering the most obvious. Because the default camera in M3G (that is a camera that you don't extract from M3G file) doesn't have a realistic perspective matrix defined. If you try rendering geometry on screen by only using the default camera (Camera cam = new Camera()) you are in for a surprise. If you want to use your own camera (which you will want to 90% of the time, especially when using immediate mode rendering) you'll want to define your own perspective matrix. Also, another good thing about knowing the perspective matrix is that you can accomplish some pretty cool effects by just manipulating the perspective parameters.
So how do we create a pure, fresh, camera and assign it a perspective matrix? Like this:
float ar = (float)getWidth() / getHeight();
Camera cam = new Camera();   // This is our fresh camera
cam.setPerspective( 60.0f, // fovy
     ar,   // The aspect ratio
     0.1f,   // Near clipping plane
     50.0f ); // Far clipping plane
It wasn't that hard, now was it? Actually, a very similar piece of code is to be found in this tutorial's source code, in a method called loadCamera. You can check it out later.
Moving objects in three-dimensional space
To move objects you really don't need to do much and you probably have a good idea of how. I'll go through some very light and simple physics methods to move objects in 3D space.
Normally, when an object moves in 3D space you need to account for three different velocities: the x, y and z velocities. However, most games can combine the x and z velocities into one, it being the "forward" velocity. (For instance, if your forward velocity is 1.0, then all you need to do is transform it with sine and cosine and apply to x and z) In this tutorial, we'll make no such discrimination, instead we'll use all three distinctive velocities to describe a moving body in 3D space.
The easiest way to do this, is by using three vectors.
1. Coordinate vector
The first vector is the coordinate vector of an object. That's the crucial vector that holds the object's current position in 3D space by keeping track of its x, y and z coordinate. How else will you know where to render the object?
2. Velocity vector
The second vector is the velocity vector. It defines how many units the object's set of coordinates will move the next time movement is computed. So it also needs a x, y and z component. Basically what you do is, each time just add the x velocity to the x coordinate, the y velocity to the y coordinate and the z velocity to the z coordinate. So, if you have a spaceship moving one unit straight up on the y-axis each frame, you'd have a velocity vector of (0, 1, 0).
3. Acceleration vector
The last vector is the acceleration vector. Although we won't be using it in this example, it's still important. The acceleration vector works like the velocity vector, but it operates on the velocity vector rather than on the coordinates. This means that the acceleration vector doesn't manipulate the coordinates directly, it instead increases the velocity each frame, giving the player a feeling of acceleration. Normally, an object can't go from idle to top speed instantly. It needs time and to model a 3D game that resembles reality you need acceleration. Say that the spaceship from the above example wanted to start moving at a speed of one unit per frame and accelerate to a speed of 5 units per frame. That could be done by having a velocity vector of (0, 1, 0) and a acceleration vector of (0, 0.1, 0). This means that after 40 frames, our spaceship will have a velocity of 5 units.
Movement example
Now, let's put our spaceship example in code! This is what it could look like:
float[] spaceShipCoords = {0, 0, 0};
float[] spaceShipVel = {0, 1, 0};
float[] spaceShipAcc = {0, 0.1, 0};
while(gameLoopIsRunning)
{
for(int i = 0; i < 3; i++)
{
   spaceShipCoords[i] = (spaceShipVel[i] += spaceShipAcc[i]);
}
}
See how easy that was? Each game loop we both accelerate and move our spaceship.
Collision
Objects in the three dimensional space are usually opaque and collide with one another. A comet can crash into a planet and a (rather poor) driver's car can crash into a building. However, in our 3D games this is not as obvious as it is in the real world. This is because so far, we've only rendered objects and rendered objects are just mathematical representations of 3D models that get transformed into screen pixels. We haven't really considered how the different objects relate to one-another yet. So if we moved our spaceship into some kind of planet that might exist, the spaceship would just continue going through the planet, and appear on the other side. All because we have no collision. We'll change this now.
Collision by ray intersection
Ray intersection is a very intuitive and easy form of collision detection. At least for normal and simple collision cases. It works like this; the object you wish to check collision from, fires a ray from its center (or somewhere else on its body) in a given direction (usually the object's velocity vector, but as you'll see soon, this is certainly not always the case). This ray then travels through your 3D world until it actually hits something (like a laser). When it hits something, it reports the collision and tells you how far it had to travel until it hit an object. Depending on that distance, you'll determine if the hit was a collision or not. That's mostly because if you know there's a building one thousand feet in front of you, it doesn't mean that you'll run into it. (Well you might eventually, but you sure haven't done it yet)
The M3G approach
M3G gives us an exceptionally easy way to calculate collision. Each Group in our world (remember, a Group is nothing but a gathering of Nodes, which can be anything with exists in our world) has a method called pick. This method is the one that does all the collision for us. It works like this, say that you're working on the next megahit FPS for mobile phones and you want to start by checking if your main character collides with the walls in a room, or if he collides with monsters in the room. So what you do is you create two Groups. One Group for all walls, and one for all monsters. Then you just simply cast a ray in each group, from the character and towards his direction, and see if you collide with anything.
M3G handles the collision part of a game with two components. The pick method and the RayIntersection class.
The RayIntersection class
This is a fairly simple class that only holds information crucial to a ray cast in our 3D world. The method we'll go through today (and probably the only you'll need for light-weight collision) is the getDistance method. The getDistance method looks like this:

public float getDistance()
It's just as simple as it looks. It returns the distance to the object that this particular ray has collided with, scaled with the direction vector you supplied to the pick method (more on this later). So by just doing a simple check, you can see if you are close enough to an object in a Group to determine if you have a collision. Just look at this piece of code:
// The default collision distance in our game
float collisionDistance = 0.1f;
// This method returns a ray that has, or hasn't collided with a wall
RayIntersection ray = checkCollisionWithWalls();
// Check distance to wall (if we haven't collided, the distance will be large)
if(ray.getDistance() < collisionDistance)
{
// Get the wall we collided with
Node wall = ray.getIntersected();
// Do something...
}
See, not hard at all! That's all the magic in the RayIntersection class. Now let's see how we can actually get collision information filled out in it.
The Group.pick method
To fill out the necessary information in a RayIntersection class, you'll need to use the pick method (unless you want to fake a collision and fill out the info yourself. Perhaps you want to implement your own ray intersection algorithm?). The pick method comes in two variants and I'll show you both before proceeding:
public boolean pick(int scope, float ox, float oy, float oz, float dx, float dy, float dz, RayIntersection ri)
public boolean pick(int scope, float x, float y, Camera camera, RayIntersection ri)
First let's talk about the similarities. Well, both want a scope. The scope simply defines which objects the ray will deem as collidable. Each Node in the M3G API has a scope, so if you pass a 1 as the scope parameter to the pick method, all nodes with a scope equal to 1. By supplying -1 you test against all nodes in that group. Sometimes supplying a -1 is bad, as you don't want your algorithm calculating a collision with something you already know your object isn't close to. Always try to minimize the job the algorithm needs to do.
The second and last thing both have in common is a RayIntersection object. This is because they fill out the RayIntersection class supplied, in case of a collision.
Now, let's go through the first method. This method is the most used one, as it allows you to define a starting point of your ray. The ox, oy and oz are three components of the origin vector. That is, those three define the point in space from which the ray will start. Usually, you'll set this to the center of collision for your object. The next three components, the dx, dy and dz, compose the direction vector. This vector decides in which direction the ray will travel. You'll always want to supply this as a unit vector (a vector with the length of 1) as the pick method scales the distance reported by getDistance depending on the length of your direction vector. Sometimes, this is a must, but most of the time you won't need it, so just make sure your direction vector has a length of 1.
So, how does the first method look in code?
// Get our group with walls
Group walls = getWallGroup();
// Create a RayIntersection object for the method to fill out
RayIntersection ri = new RayIntersection();
// Get coordinates of our character
float[] coords = getPlayerCoords();
// Get direction of our character (where is his nose pointing?)
float[] dir = getPlayerDirection();
// Try colliding (method returns true if collision has taken place)
if(walls.pick(-1, coords[0], coords[1], coords[2], dir[0], dir[1], dir[2], ri))
{
// We've intersected something, check for distance
if(ri.getDistance() < acceptableDistance)
{
   // The player has collided with a wall. Make him explode...
   // Or something.
}
}
See, not as scary as you expected it to be! It's actually pretty easy, just define from where you're shooting the ray and the direction. M3G does the rest for you. Aren't we spoiled?
You might be wondering now, what the other method is for. It has its uses and it works a bit differently. I won't go over it here since it's of no use to us in this tutorial. However I highly recommend reading about it in the M3G API documentation.
Getting down and dirty
Now that I've taught you a few tricks, how about putting them to use? I have a game in mind that'll suit us just fine in complexity. Let's make 3D pong. We won't have two players, but rather you'll be bouncing the ball against the screen and your paddle will be deep in the screen. The playing field will consist of four walls that your ball will bounce against (AHA! Collision!). What we need to do now is break this simple idea down into a few key components.
The playing field and paddle
Our playing field, as I already told you, will consist of a paddle and four walls. We'll use the exact same class for generating planes as we did in part three of the tutorial , so now would be a good time to refresh your memory.
The paddle and the walls will all be simple textured planes. To make it distinguishable, the paddle will have a different texture than the walls. All of this will be created with the MeshFactory.createPlane method.
Here is the code snippet that creates the paddle, using the MeshFactory class we built in part three of this tutorial :
// Create a plane using our nifty MeshFactory class
paddle = MeshFactory.createPlane("/res/paddle.png", PolygonMode.CULL_BACK);
Nothing hard there, it should all be very familiar by now. The paddle will of course need a transform, with which we'll be able to move it. We'll call it trPaddle and this is how we initialize it.
// Set the paddle at its initial position
trPaddle = new Transform();
trPaddle.postTranslate(paddleCoords[0], paddleCoords[1], paddleCoords[2]);
paddle.setTransform(trPaddle);
The paddleCoords variable is a float vector that defines where the paddle is right now. Its contents are put into the trPaddle each time the paddle is moved.
Now, just to be sure that our paddle will be collidable, we'll need to set a few variables. First, we need a generic way of recognizing our paddle when we're using the RayIntersection.getIntersected method. It returns a Node class, remember? So, this is a good time to use the userID variable that each Object3D has. I created a few variables that define the walls and paddle, here they are:

// Wall constants
    public static final int TOP_WALL = 0;
    public static final int LEFT_WALL = 1;
    public static final int RIGHT_WALL = 2;
    public static final int BOTTOM_WALL = 3;
    public static final int PADDLE_WALL = 4;
    public static final int PLAYING_FIELD = 5;
Having those variables, we can simply call setUserID on the paddle and supply it the PADDLE_WALL constant. The last thing we need to do is to make sure our paddle is picking enabled. Remember that the collision method is called pick? Well, all objects are checked against two things in that method. First the scope (which we will set to -1, so we don't have to worry about it) and second the picking enabled flag. If an object isn't picking enabled, it simply won't be included in the collision calculation. The flag is easily toggled by invoking the setPickingEnabled method on the paddle object. Setting the userID and the picking flag looks like this:
// Make sure it's collidable
paddle.setPickingEnable(true);
paddle.setUserID(PADDLE_WALL);
There, all done. Our paddle is now both created and collidable. Now we just add it to a group (which I call the playingField) and later we can check collision against all entities in the playingField.
// Add to the playing field
playingField = new Group();
playingField.setUserID(PLAYING_FIELD);
playingField.addChild(paddle);
There's also some more code at the end of the createPaddle method, but I'll go over that piece a bit later when we talk about ball bouncing.
Our four walls are created in a very similar fashion, so I won't repeat the code. Just check the createWalls method in the source code if you want to see how it's done.
The ball
Some say the ball is the most crucial part of pong and some say the paddle. I'd say it's an equal mixture of both. Now what does our ball need to be? First of all, it should be a Mesh, that can be rendered onscreen (it helps if this Mesh, in turn, is actually a model of a ball). Second of all, it should hold enough physics so that our ball can move in 3D space and accelerate. Finally, it should be able to calculate its own progress through the 3D world.
Bouncing
In our very simple example, we don't need any hard theory for bouncing a ball off a wall. However, I'm going to give you some theory anyway, just in case you decide making the walls of the game rotate, or actually try doing something real and get things that will bounce off of irregular surfaces. Now, how do you bounce (mirror) a vector? What we need is to simply mirror the ball's direction vector when it hits a wall. We also have to mirror it in the correct plane (mirroring in the xz-plane won't do us much good if we collided with the right wall). This is all manageable with simple vector math. I won't go through the heavy math theory since I hope you already have a background in 3D math since you are trying to make a 3D game. Instead I'll just tell you about the approach.
First of all, I've created a VectorOps class that should be of help while you create and experiment with vectors and bouncing. It holds a few of the more basic operations you'd like to do with a vector such as projecting it onto a plane, the dot and cross products, normalizing, calculating the length, etc... I've also added the mirror method. That's the method that mirrors a vector around a given plane (the plane in this case only needs to be defined by its normal vector).
How does mirror work then? Well it's pretty simple actually. First, you project your source vector (from now on called v) onto the normal of the surface (from now on called n). Then by doing some simple vector math you realize that by using the projected vector (from now on called p) you can construct a new vector that's twice as long (called u). Now this new vector u together with the original vector v can be combined to form the mirrored vector. How? Well just look at this picture.
You can easily see that the mirrored vector v that we require can be computed from this picture. Check out the mirror method of VectorOps if you want to see how.
This means that we'll have to store the normal vectors of each wall (and paddle) in the game. This isn't hard at all since we only have four walls and a paddle, but for more complex projects this could be an issue. Nevertheless, we will construct the normal vector of a plane using the simple cross product of the two vectors that compose the plane. (Remember 3D math? Each plane can be described as being strung up by two vectors. By taking the cross product of these two vectors you get the normal vector of the plane)
In our application we'll only be using the normal vectors for mirroring calculations, and we won't worry about orientation and other things you would want to take into consideration when calculating normals. This also means that the left and right walls will have exactly the same normal. As will the top and bottom walls. Normals are calculated and put into an array of floats, so that our ball will be able to calculate the bounce by using them. The array will be called wallVec and here is a code snippet that shows how the left wall's normal vector is calculated and placed in the wallVec.
float[] v = VectorOps.vector(0.0f, 1.0f, 0.0f);
float[] u = VectorOps.vector(0.0f, 0.0f, 1.0f);
float[] normVec = VectorOps.calcNormal(v, u);
wallVec[LEFT_WALL][0] = v;
wallVec[LEFT_WALL][1] = u;
wallVec[LEFT_WALL][2] = normVec;
Nothing weird here really. As you all know the left wall is actually the yz plane translated a few units down on the x-axis, so that's why it's composed out of the pure y and z vectors. After the vectors are created the normal vector is calculated by calling the calcNormal method. It simply does a crossProduct of the two supplied vectors and then normalizes the result making the vector a unit vector.
The above is done for every wall in the playing field except for the camera's wall (the player's nose if you like). That collision is done differently (and in a much simpler way).
The vigilant reader and programmer will directly realize that the above is way too much for this simple application. Since our walls are the xz, xy and zy planes we could just do a branched if-statement to check which plane we collided with and mirror the coordinate that the wall represents. However, I wanted to give you a more elegant and general solution. The above solution works for any walls, no matter how they are translated. As I already said once, this isn't a optimization tutorial, so if you want the application to go a bit faster, remove the mirroring calculations and do a nested if clause.
The implementation
Since I've shown you the guidelines now, I'll show you the implementation of the ball's render method, that not only renders, but also moves the ball and checks for collision. Let's begin with looking at the first few lines:
// Clear transform
        trBall.setIdentity();
       
        // Check if we are moving
        if(moveVec != null)
        {
         // First rotate the ball a bit       
         //trBall.postRotate(rotated += 1.0f, 1.0f, 1.0f, 1.0f);
         // Normalize the movement vector
              ri = new RayIntersection();
              float[] nMove = VectorOps.normalize(moveVec);
As you can see the first lines are pretty straight-forward. What we do here is that we first clear all transformations in the ball's Transform class, called trBall. Then, we simply do a move check (the game starts with the ball being still). The ball also has a variable called rotated, that keeps track of how much the ball has rotated, to give the impression of a rolling and flying ball. This variable is used in the method postRotate, which you should know by heart now. After this the interesting part starts. We allocate the RayIntersection object we'll use for collision and normalize the ball's velocity vector (called moveVec). Remember why we normalize? Because the pick method always scales the distance-to-intersection by the length of your direction vector. If the length of the direction vector is 1, the distance-to-intersection gets unscaled. This is exactly how we want it. Let's continue on with the method, now comes the collision:
// See if there is any collision
if(walls.pick(-1, coords[0], coords[1], coords[2], nMove[0], nMove[1], nMove[2], ri) && ri.getDistance() <= 0.5f)
{
    // We have collided, get the surface we intersected
    Node n = ri.getIntersected();
            
    // Correct our movement vector by mirroring
    moveVec = VectorOps.mirror(moveVec, wallVectors[n.getUserID()][2]);
So there's the fabled call to Group.pick. As you see, it's performed on a group called walls, which is actually the playingField group passed to the ball's render method. So the walls group contains both all the walls and the paddle. We check if the pick method returns true and if the distance to object is smaller than 0.5, which is a pretty reasonable distance in this application.
If we have a collision, we first fetch the node we intersected. This is one of the walls from our walls Group. Then we fetch the wall's user ID. Remember those? We used them as indices into the wallVec array to store the normal vectors of the wall. Here we fetch them by invoking the getUserID method on the Node. When we have a normal vector we call the mirror method to mirror the ball's current direction vector around the intersected wall. That's all there is to it.
The above really isn't that much fun though. Since all walls are treated equally, the ball will just flatly bounce off of the paddle as well and will give us some really boring and static gameplay. That's why we'll make collision with the paddle a bit different. Check this piece of code out:
// Let user have contol over the movement by moving the paddle
if(n.getUserID() == M3GCanvas.PADDLE_WALL)
{
    // Add extra speed to a maximum amount depending on ball/paddle position
    float distX = (paddleCoords[0] - coords[0]) / 10.0f;
    float distY = (paddleCoords[1] - coords[1]) / 10.0f;
    moveVec[0] = Math.max(-0.3f, Math.min(moveVec[0] - distX, 0.3f));
    moveVec[1] = Math.max(-0.3f, Math.min(moveVec[1] - distY, 0.3f));
                
    // After 30 bounces it should be impossibly fast (HAH! A challenge!)
    moveVec[2] = moveVec[2] + 0.01f;
                
    // Increase number of bounces
    bounces++;
}
OK, so what are we doing here? Well first we're checking if we actually are bouncing against the paddle by comparing the Node's user ID to the PADDLE_WALL constant. Then we use some simple math to calculate the distance from the ball's center to the paddle's center. We use this result to skew the resulting direction vector to create more dynamic gameplay.
Another fun thing is to make the ball go faster with each bounce. As you know, speed in this game can be just as simple as the velocity along the z-axis, so for each bounce against the paddle, we accelerate the speed along the z-axis.
The last thing, incrementing the bounce's variable, is just used as "scoring". The best player has the most bounces. A pretty simple concept, but it works.
We've almost seen everything in the Ball.render method now, except for the bouncing against the camera wall (or the player's nose). This is done by the following code snippet. I won't comment it, as you should be able to figure out what it does by just looking at it.
// Check for bouncing against screen
if(coords[2] >= -0.7f)
{
    // Flip over the screen (same as for paddle)
    moveVec = VectorOps.mirror(moveVec, wallVectors[M3GCanvas.PADDLE_WALL][2]);
}
There! That's all there is to it. Not really much to talk about, is it?
Perspective and tube-view
I promised you that we'd use the perspective calculations a bit differently in this tutorial and make the game look a bit special. That's exactly what we'll do by just tweaking a single variable of the FOV. We'll change the fovy to a very large number, thus creating a strange shearing effect that looks quite good in this context. I've used 130 degrees here (maximum allowed value by M3G is 180) since it gives the feeling that you're actually looking down a very long and extended tube. You can play around with this value in the source code to see what kind of shearing you'll be able to get.
The game loop
Only one last thing to do now, and that's implement the game loop. You should be able to do this sleeping, with your hands tied to your back, but I'll go through the semantics once again. Let's first look at the whole game loop:
private void draw(Graphics g)
    {
        // Envelop all in a try/catch block just in case
        try
        {           
            // Get the Graphics3D context
            g3d = Graphics3D.getInstance();
           
         // First bind the graphics object. We use our pre-defined rendering hints.
         g3d.bindTarget(g, true, RENDERING_HINTS);
        
         // Clear background
         g3d.clear(back);
        
         // Bind camera at fixed position in origo
         g3d.setCamera(cam, trCam);
        
         // Render the playing field and ball
         g3d.render(playingField, identity);
         ball.render(g3d, playingField, wallVec, paddleCoords);
        
         // Check controls for paddle movement
         if(key[UP])
         {
             paddleCoords[1] += 0.2f;
             if(paddleCoords[1] > 3.0f)
                 paddleCoords[1] = 3.0f;
         }
         if(key[DOWN])
         {
             paddleCoords[1] -= 0.2f;
             if(paddleCoords[1] < -3.0f)
                 paddleCoords[1] = -3.0f;
         }
         if(key[LEFT])
         {
             paddleCoords[0] -= 0.2f;
             if(paddleCoords[0] < -3.0f)
                 paddleCoords[0] = -3.0f;
         }
         if(key[RIGHT])
         {
             paddleCoords[0] += 0.2f;
             if(paddleCoords[0] > 3.0f)
                 paddleCoords[0] = 3.0f;
         }
        
         // Set paddle's coords
         trPaddle.setIdentity();
         trPaddle.postTranslate(paddleCoords[0], paddleCoords[1], paddleCoords[2]);
         paddle.setTransform(trPaddle);
        
         // Quit if user presses fire
         if(key[FIRE])
             ball.start();
        }
        catch(Exception e)
        {
            reportException(e);
        }
        finally
        {
            // Always remember to release!
            g3d.releaseTarget();
        }
       
        // Do some old-fashioned 2D drawing       
        if(!ball.isMoving())
        {
            g.setColor(0);
            g.drawString("Press fire to start!", 2, 2, Graphics.TOP | Graphics.LEFT);
        }
        else
        {
            int red = Math.min(255, ball.getBounces() * 12);
            g.setColor(red, 0, 0);
            g.drawString("Score: " + ball.getBounces(), 2, 2, Graphics.TOP | Graphics.LEFT);
        }
    }
So first is the standard immediate mode rendering stuff. (If you can't remember, check part three of the tutorial ).
  • Get Graphics3D instance
  • Bind to Graphics target
  • Clear background
  • Set and update camera
What we do next is pretty simple too. We just render the whole playingField group, that holds all the wall meshes and their transforms. This way we compress the rendering of five entities into a single group. Scene graphs make for very clean code.
After rendering the playing field we call the Ball.render method, supply it the necessary values and it does the rest. We've already been over this method so I won't say anything here.
Next we check the controls. Nothing big here either. We move the paddle with the joystick and reset the ball's position with FIRE. After we've done all 3D graphics, we do some old-fashioned 2D drawing since we want the user to know his score right now and if the ball has gone out of bounds (and he needs to reset it).
That's the whole enchilada, as people in the fast food industry would most likely say. Now that you've seen the most intricate workings of the game, here are a few shots of it in action.

As you can see the choice of 130 as fovy gives us a pretty interesting tube/tunnel effect. Also, don't forget, this game is actually pretty fun for being so easy. With a bit of development you could make this game into a really addictive game. As an exercise, try making the walls of the playing field rotate! Do remember though; you will have to rotate all the normal vectors stored in the wallVec array every time you rotate the walls, for the mirroring to work properly.
Have fun and I hope you learned something.

-------------------------------------------------------
源码见附件:
分享到:
评论

相关推荐

    [JSR-184][3D编程指南]Part II: Light 3D theory and orientation

    【JSR-184】3D编程指南第二部分:光照3D理论与定向 在3D编程领域,理解和应用光照理论以及物体的定向是至关重要的。JSR-184,全称Java 3D API,是Java平台上的一个标准,它提供了用于创建和操作3D图形的强大工具。...

    [JSR-184][3D编程指南]Part V: Heightmap terrain rendering using M3G

    总之,【JSR-184】的3D编程指南Part V专注于使用M3G在移动设备上实现基于高度图的地形渲染,涉及图像处理、3D网格构建、纹理映射、光照以及移动平台的图形渲染技术。通过实践和理解这些概念,你将能够创建出逼真的3D...

    [JSR-184][3D编程指南]Part I: Quick jump into the world of Mobile Java 3D programming

    【JSR-184】是Java Micro Edition (Java ME)平台中的一项规范,全称为“Mobile 3D Graphics API”。这个规范旨在为移动设备提供3D图形编程接口,使得开发者能够在小型设备上构建丰富的三维应用程序,如游戏、虚拟...

    [JSR-184][3D编程指南]Part III: Particle systems and immediate mode rendering (2)

    【JSR-184】是Java Micro Edition (Java ME) 中的一个标准,全称为"Mobile 3D Graphics API",旨在为移动设备提供3D图形编程接口。这个标准允许开发者在小型设备上创建复杂的3D图形应用,比如游戏或者可视化工具。本...

    jsr_184_midlet.rar_DEMO_jsr184_jsr184-m3g.jar_m3g

    在提供的压缩包"jsr_184_midlet.rar_DEMO_jsr184_jsr184-m3g.jar_m3g"中,我们可以看到与JSR 184相关的几个关键元素: 1. **DEMO**:这是一个演示程序,用于展示JSR 184技术的实际应用。通过这个DEMO,开发者或用户...

    jsr-275-1.0-beta-2.jar

    java.lang.ClassNotFoundException: javax.measure.converter.ConversionException所需的jar

    undertow-websockets-jsr-2.1.7.Final-API文档-中文版.zip

    赠送jar包:undertow-websockets-jsr-2.1.7.Final.jar; 赠送原API文档:undertow-websockets-jsr-2.1.7.Final-javadoc.jar; 赠送源代码:undertow-websockets-jsr-2.1.7.Final-sources.jar; 赠送Maven依赖信息...

    undertow-websockets-jsr-2.1.7.Final-API文档-中英对照版.zip

    赠送jar包:undertow-websockets-jsr-2.1.7.Final.jar; 赠送原API文档:undertow-websockets-jsr-2.1.7.Final-javadoc.jar; 赠送源代码:undertow-websockets-jsr-2.1.7.Final-sources.jar; 赠送Maven依赖信息...

    mmapi(jsr-135)编程指导

    **JSR-135编程指导** JSR-135,全称为JavaTM Media Framework API,是Java ME(J2ME)平台中用于多媒体应用开发的重要规范。它为移动和嵌入式设备提供了处理音频、视频和图像的能力,使得开发者能够创建功能丰富的...

    jsr311-api-1.1.1-API文档-中文版.zip

    赠送jar包:jsr311-api-1.1.1.jar; 赠送原API文档:jsr311-api-1.1.1-javadoc.jar; 赠送源代码:jsr311-api-1.1.1-sources.jar; 赠送Maven依赖信息文件:jsr311-api-1.1.1.pom; 包含翻译后的API文档:jsr311-api...

    undertow-websockets-jsr-2.2.14.Final-API文档-中英对照版.zip

    赠送jar包:undertow-websockets-jsr-2.2.14.Final.jar; 赠送原API文档:undertow-websockets-jsr-2.2.14.Final-javadoc.jar; 赠送源代码:undertow-websockets-jsr-2.2.14.Final-sources.jar; 赠送Maven依赖信息...

    undertow-websockets-jsr-2.2.14.Final-API文档-中文版.zip

    赠送jar包:undertow-websockets-jsr-2.2.14.Final.jar; 赠送原API文档:undertow-websockets-jsr-2.2.14.Final-javadoc.jar; 赠送源代码:undertow-websockets-jsr-2.2.14.Final-sources.jar; 赠送Maven依赖信息...

    JSR-184.zip_JSR184 3D_jsr184

    用jsr184编写的手机3d编程实例,用户可以任意旋转箭头,放大缩小等等。包含如何使用数据定义mesh,如何操作camera如何旋转等等,程序功能较繁杂,但是界面较粗糙(数据定义的模型当然是越简单越好啦),学习意义大于...

    jackson-datatype-jsr310-2.11.4-API文档-中英对照版.zip

    赠送jar包:jackson-datatype-jsr310-2.11.4.jar; 赠送原API文档:jackson-datatype-jsr310-2.11.4-javadoc.jar; 赠送源代码:jackson-datatype-jsr310-2.11.4-sources.jar; 赠送Maven依赖信息文件:jackson-...

    3-D_Game_Development_on_JSR-184_v1_0_3

    《3-D Game Development on JSR-184 v1_0_3》是关于使用Java 3D技术在J2ME平台上开发3D游戏的一份重要资料,它为初学者提供了一个宝贵的入门教程。JSR-184,全称为Java ME 3D API,是Java Micro Edition(J2ME)平台...

    jackson-datatype-jsr310-2.12.5-API文档-中文版.zip

    赠送jar包:jackson-datatype-jsr310-2.12.5.jar; 赠送原API文档:jackson-datatype-jsr310-2.12.5-javadoc.jar; 赠送源代码:jackson-datatype-jsr310-2.12.5-sources.jar; 赠送Maven依赖信息文件:jackson-...

    webbeans--jsr-299

    标题:WebBeans -- JSR-299 描述:WebBeans是Gavin King的力作,专注于为Java EE平台提供上下文与依赖注入(Contexts and Dependency Injection)。 ### WebBeans (JSR-299) 知识点详解 #### 一、架构与合同 Web...

    jackson-datatype-jsr310-2.11.4-API文档-中文版.zip

    赠送jar包:jackson-datatype-jsr310-2.11.4.jar; 赠送原API文档:jackson-datatype-jsr310-2.11.4-javadoc.jar; 赠送源代码:jackson-datatype-jsr310-2.11.4-sources.jar; 赠送Maven依赖信息文件:jackson-...

Global site tag (gtag.js) - Google Analytics