Monday, April 1, 2013

Simple Gravity for your 3D game

Beginners out there are often making a really simple game and don't always need a physics engine. If you want to try out some basic gravity for your game, then something like this might work for you.

Any object in your 3D world is going to need a 3D position (a point, often represented by a vector class). Applying gravity to your 3D objects is actually really simple, assuming you're not trying planetary physics around a sphere (or a gravity well of some kind). Each frame during an update cycle you just add gravity to the velocity of your objects. And later in the update cycle you move all of your objects based on their velocity. Here's some C++ pseudocode of how this might work:

#include <vector>

class Object
{
public:
    Object();

    Vector3 m_position;
    Vector3 m_velocity;
    Matrix3 m_rotation;
    float   m_elasticity;
};

class SceneManager
{
public:
    void update(float timeDelta);

private:
    std::vector<Object> m_objects;
}

void SceneManager::update(float timeDelta)
{
    // Be careful here, I'll explain why after the code snippet
    for ( unsigned int i = 0; i < m_objects.size(); ++i )
    {
        applyGravity(m_objects[i]);

        updatePosition(m_objects[i]);
    }
}

void applyGravity(float timeDelta, Object& obj)
{
    static Vector3 gravity(0.0f, -9.8f, 0.0f);

    // Factor time when adding gravity, because 9.8m/s is a speed, and speed requires time
    obj.m_velocity += (timeDelta * gravity);
}

void updatePosition(Object& obj)
{
    obj.m_position += obj.m_velocity;
}
Be careful when looping through your game's object list and making changes, if your change of position does something like cause a collision with another object, and you are handling that immediately, you could wind up making changes to your list of objects while you're in the middle of iterating through them. The above sample will work, but if your 'applyGravity' and 'updatePosition' methods don't end up getting inlined then you're going to be incurring two function calls for every object in your scene. If those methods don't get inlined then I would recommend passing your entire list of objects that you want updated to the function and let the function loop through them and make the updates. It would like something like this:
void SceneManager::update(float timeDelta)
{
    applyGravity(m_objects);
    updatePosition(m_objects);
}

void applyGravity(float timeDelta, std::vector<Object>& objs)
{
    // A hard-coded gravity, you might want to have this somewhere convenient
    // where it can be changed at run-time. Depends on what you're trying to do.
    static const Vector3 gravity(0.0f, -9.8f, 0.0f);

    const float gravityThisFrame = (timeDelta * gravity);

    for ( unsigned int i = 0; i < objs.size(); ++i )
    {
        objs[i].m_velocity += gravityThisFrame;
    }
}

void updatePosition(std::vector<Object>& objs)
{
    for ( unsigned int i = 0; i < objs.size(); ++i )
    {
        objs[i].m_position += objs[i].m_velocity;
    }
}
Once you have gravity your next concern will probably be some kind of collision detection with the ground. How you do that will be determined by your game, but once you've detected a collision with the ground, a simple way to handle the bounce is to flip the velocity and reduce it by some amount. The more you reduce the velocity the less bouncy your object is.
void handleGroundCollision(Object& obj)
{
    objs[i].m_velocity.y *= -objs[i].m_elasticity.y;
}
The above method assumes your gravity is in the direction of the -Y axis. If you have a gravity going in a different direction then you may need to do a bit more of a complex calculation to bounce your object. The following method will handle basic collisions with any stationary object. In this case you need to pass the normal of the surface the object is hitting, if that is flat ground and your 'up' axis is +Y then the surface normal is Vector3(0.0f, 1.0f, 0.0f). Make sure your surface normal is normalized/unitized before using it in this method.
void Vector3::reflect( const Vector3& normal, float elasticity )
{
    const float dotProductTimesTwo = Dot(normal) * 2.0f; 
    x -= dotProductTimesTwo * normal.x * elasticity;
    y -= dotProductTimesTwo * normal.y * elasticity;
    z -= dotProductTimesTwo * normal.z * elasticity;
}

// Or if you don't have your own Vector class, use this:
void reflect( Vector3& velocity, const Vector3& normal, float elasticity )
{
    const float dotProductTimesTwo = Dot(normal) * 2.0f;
    velocity -= ((dotProductTimesTwo * normal) * elasticity);
}

Setting your object's elasticity to 0.0f would mean that it would not bounce at all. A value of 1.0f would mean a perfect bounce, which would have no loss of energy on the bounce. Any value higher than 1.0f would cause the ball to move with a higher velocity than when it bounced. Usually you're going to want something between 0.0f and 1.0f, feel free to experiment with it a bit.

One more optimization consideration to make with this. If you have a lot of objects it can become more and more expensive to loop through them all each frame. If you separate your object list, or have another one for only the objects that can be moved or affected by gravity, then that will limit how many objects you're having to loop through each frame. Additionally if you have some more advanced detection methods you may know when an object is at rest due to it sitting on top of another object, if that is the case you may not need to apply gravity to it. Be careful with that optimization though, if gets tricky if you start mucking around with the direction of gravity.

No comments:

Post a Comment