A Little Physics in 2D

In this section, you'll use a very simple and limited version of physics. Games are all about being good fakes. They cheat wherever possible in order to avoid potentially heavy calculations. The behavior of objects in a game does not need to be 100 percent physically accurate; it just needs to be good enough to look believable. Sometimes you won't even want physically accurate behavior (that is, you might want one set of objects to fall downward, and another, crazier, set of objects to fall upward).

Even the original Super Mario Brothers used at least some basic principles of Newtonian physics. These principles are really simple and easy to implement. Only the absolute minimum required for implementing a simple physics model for your game objects will be discussed.

Newton and Euler, Best Friends Forever

Your main concern is with the motion physics of so-called point masses, which refers to the change in position, velocity, and acceleration of an object over time. Point mass means that you approximate all objects with an infinitesimally small point that has an associated mass. You do not have to deal with things like torque—the rotational velocity of an object around its center of mass—because that is a complex problem domain about which more than one complete book has been written. Just look at these three properties for an object:

  • The position of an object is simply a vector in some space—in your case, a 2D space. You represent it as a vector. Usually the position is given in meters.
  • The velocity of an object is its change in position per second. Velocity is given as a 2D velocity vector, which is a combination of the unit-length direction vector in which the object is heading and the speed at which the object will move, given in meters per second. Note that the speed just governs the length of the velocity vector; if you normalize the velocity vector by the speed, you get a nice unit-length direction vector.
  • The acceleration of an object is its change in velocity per second. You can either represent this as a scalar that only affects the speed of the velocity (the length of the velocity vector), or as a 2D vector, so that you can have different acceleration in the x- and y-axes. Here you'll choose the latter, as it allows you to use things such as ballistics more easily. Acceleration is usually given in meters per second per second (m/s2). No, that's not a typo—you change the velocity by some amount given in meters per second, each second.

When you know the properties of an object for a given point in time, you can integrate them in order to simulate the object's path through the world over time. This may sound scary, but you already did this with Mr. Nom and your Bob test. In those cases, you didn't use acceleration; you simply set the velocity to a fixed vector. Here's how you can integrate the acceleration, velocity, and position of an object in general:

Vector2 position = new Vector2();
Vector2 velocity = new Vector2();
Vector2 acceleration = new Vector2(0, -10);
while(simulationRuns) {
   float deltaTime = getDeltaTime();
   velocity.add(acceleration.x * deltaTime, acceleration.y * deltaTime);
   position.add(velocity.x * deltaTime, velocity.y * deltaTime);
}

This is called numerical Euler integration, and it is the most intuitive of the integration methods used in games. You start off with a position at (0,0), a velocity given as (0,0), and an acceleration of (0,−10), which means that the velocity will increase by 1 m/s on the y-axis. There will be no movement on the x-axis. Before you enter the integration loop, your object is standing still. Within the loop, you first update the velocity, based on the acceleration multiplied by the delta time, and then update the position, based on the velocity multiplied by the delta time. That's all there is to the big, scary word integration.

NOTE: As usual, that's not even half of the story. Euler integration is an “unstable” integration method and should be avoided when possible. Usually, one would employ a variant of the so-called verlet integration, which is just a bit more complex. For your purposes, however, the easier Euler integration is sufficient.

Force and Mass

You might wonder where the acceleration comes from. That's a good question, with many answers. The acceleration of a car comes from its engine. The engine applies a force to the car that causes it to accelerate. But that's not all. A car will also accelerate toward the center of the earth, due to gravity. The only thing that keeps it from falling through to the center of the earth is the ground, which it can't pass through. The ground cancels out this gravitational force. The general idea is this:

force = mass × acceleration

You can rearrange this to the following equation:

acceleration = force / mass

Force is given in the SI unit Newton. (Guess who came up with this.) If you specify acceleration as a vector, then you also have to specify the force as a vector. A force can thus have a direction. For example, the gravitational force pulls downward in the direction (0,−1). The acceleration is also dependent on the mass of an object. The greater the mass of an object, the more force you need to apply in order to make it accelerate as fast as an object of less weight. This is a direct consequence of the preceding equations.

For simple games you can, however, ignore the mass and force, and just work with the velocity and acceleration directly. In the preceding pseudocode, you set the acceleration to (0,−10) m/s per second (again, not a typo), which is roughly the acceleration of an object when it is falling toward the earth, no matter its mass (ignoring things like air resistance). It's true, ask Galileo!

Playing Around, Theoretically

Use the preceding example to play with an object falling toward earth. Let's assume that you let the loop iterate ten times, and that getDeltaTime() will always return 0.1 s. You'll get the following positions and velocities for each iteration:

time=0.1, position=(0.0,-0.1), velocity=(0.0,-1.0)
time=0.2, position=(0.0,-0.3), velocity=(0.0,-2.0)
time=0.3, position=(0.0,-0.6), velocity=(0.0,-3.0)
time=0.4, position=(0.0,-1.0), velocity=(0.0,-4.0)
time=0.5, position=(0.0,-1.5), velocity=(0.0,-5.0)
time=0.6, position=(0.0,-2.1), velocity=(0.0,-6.0)
time=0.7, position=(0.0,-2.8), velocity=(0.0,-7.0)
time=0.8, position=(0.0,-3.6), velocity=(0.0,-8.0)
time=0.9, position=(0.0,-4.5), velocity=(0.0,-9.0)
time=1.0, position=(0.0,-5.5), velocity=(0.0,-10.0)

After 1 s, your object will fall 5.5 m and have a velocity of (0,−10) m/s, moving straight down to the core of the earth (until it hits the ground, of course).

Your object will increase its downward speed without end, as you haven't factored in air resistance. (As mentioned before, you can easily cheat your own system.) You can simply enforce a maximum velocity by checking the current velocity length, which equals the speed of the object.

All-knowing Wikipedia indicates that a human in free fall can have a maximum, or terminal, velocity of roughly 125 mph. Converting that to meters per second (125 × 1.6 × 1000 / 3600), you get 55.5 m/s. To make your simulation more realistic, you can modify the loop, as follows:

while(simulationRuns) {
   float deltaTime = getDeltaTime();
   if(velocity.len()< 55.5)
      velocity.add(acceleration.x * deltaTime, acceleration.y * deltaTime);
   position.add(velocity.x * deltaTime, velocity.y * deltaTime);
}

As long as the speed of the object (the length of the velocity vector) is smaller than 55.5 m/s, you can increase the velocity by the acceleration. When you've reached the terminal velocity, you simply stop increasing it by the acceleration. This simple capping of velocities is a trick that is used heavily in many games.

You can add wind to the equation by adding another acceleration in the x direction, say (–1,0) m/s². For this, add the gravitational acceleration to the wind acceleration before you add it to the velocity:

Vector2 gravity = new Vector2(0,-10);
Vector2 wind = new Vector2(-1,0);
while(simulationRuns) {
   float deltaTime = getDeltaTime();
   acceleration.set(gravity).add(wind);
   if(velocity.len()< 55.5)
      velocity.add(acceleration.x * deltaTime, acceleration.y * deltaTime);
   position.add(velocity.x * deltaTime, velocity.y * deltaTime);
}

You can also ignore acceleration altogether and let your objects have a fixed velocity. You did exactly this in the BobTest. You changed the velocity of each Bob only if he hit an edge, and you did so instantly.

Playing Around, Practically

The possibilities, even with this simple model, are endless. Let's extend your little CannonTest so that you can actually shoot a cannonball. Here's what you want to do:

  • As long as the user drags his or her finger over the screen, the canon will follow it. That's how you can specify the angle at which you'll shoot the ball.
  • As soon as you receive a touch-up event, you can fire a cannonball in the direction the cannon is pointing. The initial velocity of the cannonball will be a combination of the cannon's direction and the speed the cannonball has from the start. The speed is equal to the distance between the cannon and the touch point. The further away you touch, the faster the cannonball will fly.
  • The cannonball will fly as long as there's no new touch-up event.
  • You can double the size of your view frustum to (0,0) to (9.6, 6.4) so that you can see more of your world. Additionally, you can place the cannon at (0,0). Note that all units of the world are now given in meters.
  • You can render the cannonball as a red rectangle of the size 0.2×0.2 m, or 20×20 cm—close enough to a real cannonball. The pirates among you may choose a more realistic size, of course.

Initially, the position of the cannonball will be (0,0)—the same as the cannon's position. The velocity will also be (0,0). Since you apply gravity in each update, the cannonball will simply fall straight down.

Once a touch-up event is received, set the ball's position back to (0,0) and its initial velocity to (Math.cos(cannonAngle),Math.sin(cannonAngle)). This will ensure that the cannonball flies in the direction the cannon is pointing. Also, set the speed simply by multiplying the velocity by the distance between the touch point and the cannon. The closer the touch point to the cannon, the more slowly the cannonball will fly.

Sounds easy enough, so now you can try implementing it. Copy over the code from the CannonTest to a new file, called CannonGravityTest.java. Rename the classes contained in that file to CannonGravityTest and CannonGravityScreen. Listing 8–3 shows the CannonGravityScreen.

Listing 8–3. Excerpt from CannonGravityTest

class CannonGravityScreen extends Screen {
    float FRUSTUM_WIDTH = 9.6f;
    float FRUSTUM_HEIGHT = 6.4f;
    GLGraphics glGraphics;
    Vertices cannonVertices;
    Vertices ballVertices;
    Vector2 cannonPos = new Vector2();
    float cannonAngle = 0;
    Vector2 touchPos = new Vector2();
    Vector2 ballPos = new Vector2(0,0);
    Vector2 ballVelocity = new Vector2(0,0);
    Vector2 gravity = new Vector2(0,-10);

Not a lot has changed. You simply doubled the size of the view frustum, and reflected that by setting FRUSTUM_WIDTH and FRUSTUM_HEIGHT to 9.6 and 6.2, respectively. This means that you can see a rectangle of 9.2×6.2 m of the world. Since you also want to draw the cannonball, add another Vertices instance, called ballVertices, which will hold the four vertices and six indices of the rectangle of the cannonball. The new members ballPos and ballVelocity store the position and velocity of the cannonball, and the member gravity is the gravitational acceleration, which will stay at a constant (0,−10) m/s² over the lifetime of your program.

    public CannonGravityScreen(Game game) {
        super(game);
        glGraphics = ((GLGame) game).getGLGraphics();
        cannonVertices = new Vertices(glGraphics, 3, 0, false, false);
        cannonVertices.setVertices(new float[] { -0.5f, -0.5f,
                                           0.5f, 0.0f,
                                          -0.5f, 0.5f }, 0, 6);
        ballVertices = new Vertices(glGraphics, 4, 6, false, false);
        ballVertices.setVertices(new float[]  { -0.1f, -0.1f,
                                                0.1f, -0.1f,
                                                0.1f,  0.1f,
                                               -0.1f,  0.1f }, 0, 8);
        ballVertices.setIndices(new short[] {0,  1, 2, 2, 3, 0}, 0, 6);
                                                
    }

In the constructor, simply create the additional Vertices instance for the rectangle of the cannonball. Define it in model space with the vertices (−0.1,−0.1), (0.1,−0.1), (0.1,0.1), and (−0.1,0.1). Use indexed drawing, and thus specify six vertices in this case.

    @Override
    public void update(float deltaTime) {
        List<TouchEvent> touchEvents = game.getInput().getTouchEvents();
        game.getInput().getKeyEvents();

        int len = touchEvents.size();
        for (int i = 0; i < len; i++) {
            TouchEvent event = touchEvents.get(i);

            touchPos.x = (event.x / (float) glGraphics.getWidth())
                    * FRUSTUM_WIDTH;
            touchPos.y = (1 - event.y / (float) glGraphics.getHeight())
                    * FRUSTUM_HEIGHT;
            cannonAngle = touchPos.sub(cannonPos).angle();

            if(event.type == TouchEvent.TOUCH_UP) {
                float radians = cannonAngle * Vector2.TO_RADIANS;
                float ballSpeed = touchPos.len();
                ballPos.set(cannonPos);
                ballVelocity.x = FloatMath.cos(radians) * ballSpeed;
                ballVelocity.y = FloatMath.sin(radians) * ballSpeed;
            }
        }

        ballVelocity.add(gravity.x * deltaTime, gravity.y * deltaTime);
        ballPos.add(ballVelocity.x * deltaTime, ballVelocity.y * deltaTime);
    }

The update() method has changed only slightly. The calculation of the touch point in world coordinates and the angle of the cannon are still the same. The first addition is the if statement inside the event-processing loop. In case you get a touch-up event, you prepare the cannonball to be shot. Transfomr the cannon's aiming angle to radians, as you'll use FastMath.cos() and FastMath.sin() later on. Next, calculate the distance between the cannon and the touch point. This will be the speed of the cannonball. Set the ball's position to the cannon's position. Finally, calculate the initial velocity of the cannonball. Use sine and cosine, as discussed in the previous section, to construct a direction vector from the cannon's angle. Multiply this direction vector by the cannonball's speed to arrive at the final cannonball velocity. This is interesting, as the cannonball will have this velocity from the start. In the real world, the cannonball would, of course, accelerate from 0 m/s to whatever it could reach given air resistance, gravity, and the force applied to it by the cannon. You can cheat here, though, as that acceleration would happen in a very tiny time window (a couple hundred milliseconds). The last thing you do in the update() method is update the velocity of the cannonball and, based on that, adjust its position.

@Override
    public void present(float deltaTime) {

        GL10 gl = glGraphics.getGL();
        gl.glViewport(0, 0, glGraphics.getWidth(), glGraphics.getHeight());
        gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        gl.glMatrixMode(GL10.GL_PROJECTION);
        gl.glLoadIdentity();
        gl.glOrthof(0, FRUSTUM_WIDTH, 0, FRUSTUM_HEIGHT, 1, -1);
        gl.glMatrixMode(GL10.GL_MODELVIEW);

        gl.glLoadIdentity();
        gl.glTranslatef(cannonPos.x, cannonPos.y, 0);
        gl.glRotatef(cannonAngle, 0, 0, 1);
        gl.glColor4f(1,1,1,1);
        cannonVertices.bind();
        cannonVertices.draw(GL10.GL_TRIANGLES, 0, 3);
        cannonVertices.unbind();

        gl.glLoadIdentity();
        gl.glTranslatef(ballPos.x, ballPos.y, 0);
        gl.glColor4f(1,0,0,1);
        ballVertices.bind();
        ballVertices.draw(GL10.GL_TRIANGLES, 0, 6);
        ballVertices.unbind();
    }

In the present() method, simply add the rendering of the cannonball rectangle. You do this after rendering the cannon's triangle, which means that you have to “clean” the model-view matrix before you can render the rectangle. Do this with glLoadIdentity(), and then use glTranslatef() to convert the cannonball's rectangle from model space to world space at the ball's current position.

    @Override
    public void pause() {

    }

    @Override
    public void resume() {

    }

    @Override
    public void dispose() {

    }        
}

If you run the example and touch the screen a couple of times, you'll get a pretty good feel for how the cannonball will fly. Figure 8–7 shows the output (which is not all that impressive, since it is a still image).

images

Figure 8–7. A triangle cannon that shoots red rectangles. Impressive!

That's enough physics for your purposes. With this simple model, you can simulate much more than cannonballs. Super Mario, for example, could be simulated in much the same way. If you have ever played Super Mario Brothers, then you will notice that Mario takes a bit of time before he reaches his maximum velocity when running. This can be implemented with a very fast acceleration and velocity capping, as in the preceding pseudocode. Jumping can be implemented in much the same way as shooting the cannonball. Mario's current velocity would be adjusted by an initial jump velocity on the y-axis (remember that you can add velocities like any other vectors). You would always apply a negative y acceleration (gravity), which makes him come back to the ground, or fall into a pit, after jumping. The velocity in the x direction is not influenced by what's happening on the y-axis. You can still press left and right to change the velocity of the x-axis. The beauty of this simple model is that it allows you to implement very complex behavior with very little code. You can use this type of physics when you write your next game.

Simply shooting a cannonball is not a lot of fun. You want to be able to hit objects with the cannonball. For this, you need something called collision detection, which you can investigate in the next section.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.142.130.250