Radu Angelescu

dec. 28, 2015

Top down racing car physics model using box2D

Today’s article is about implementing a simple, easy configurable car model with the Box2D physics library. See above video of me playing with the new model.

So as we know, if we want to make an AI we first need to settle on an object model. Our object is a top down car, and in this article we will define the basic parametric model. Settling on an object model is actually a hard thing to do in games or real-time application in general, because you always get new ideas for improvements, usually you need to balance the improvements with the time needed to change the system and the deadlines :) .

I say this will be our model but it may happen that during development I find a better idea so it may change (you can’t always rely on the latest Carvatar github source, but you can always check the history for the version I used to write the article: commit 9ac674a).

Let’s start coding!

When I first started writing the code for the Carvatar car model I read and implemented this article and I think it’s actually great (the whole site is great). I used it when I started developing the framework and it also kind of taught me box2d (as I had experience with Havok, Bullet and Physx but almost none with box2d).

Unfortunately the 4-wheel car model presented on iforce2d was not what I was looking for because I found it is kind of cumbersome and I didn’t need something that complicated. The golden rule is to keep it simple because as the physics model becomes more complex, tuning the parameters to make it “fun” becomes a pain.

Less parameters that get you where you want is always a good thing (sometimes you actually go through the effort of combining parameters in high level ones just to decrease the number). Also keeping the parameters to be first order relations to the outputs is also always a good thing (but not always possible).

A big number of parameters can also influence the AI. It may need to have more depth (more hidden layers in case of neural networks, more code in case of FSM’s, bigger number of sensors.. the whole problem basically scales nonlinearly).

So our model is the simplest one yet: a dynamic rigid body in the shape of our car. If we want wheels we will add them just for the aesthetic factor (sprites that we rotate based on our input).

A very important piece of information: choosing the scale of your rigid bodies is not arbitrary. You should choose it wisely as the speed sensation is directly proportional to how many pixels you can see the object move in a time step and Box2D has upper limits for simulation speeds (for the numerical stability of the integration process). Unfortunately you can’t go too small either because that also causes problems with numerical stability.

Creating the car rigid body

As I talked some time ago, I started using TOML for my configuration files, so basically I am loading the parameters from a TOML.

The code has comments so it’s self explanatory (not exactly rocket science :) ).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
CarModel::CarModel(b2World* world,unsigned int id)
{
    m_id = id;
    //open the file
    std::ifstream ifs("carsetup.TOML");
    //make TOML parser
    toml::Parser parser(ifs);

    toml::Value documentRoot = parser.parse();
    toml::Value* params = documentRoot.find("carparams");

    // Populate our physic parameters
    m_maxForwardSpeed      = (float)params->find("max_forward_speed")->as<double>();
    m_maxBackwardSpeed   = (float)params->find("max_backward_speed")->as<double>();
    m_maxDriveForce        = (float)params->find("max_drive_force")->as<double>();
    m_maxLateralImpulse  = (float)params->find("max_lateral_impulse")->as<double>();
    m_angularDamping       = (float)params->find("angular_damping")->as<double>();
    m_angularFriction      = (float)params->find("angular_friction")->as<double>();
    m_bodyDensity            = (float)params->find("body_density")->as<double>();
    m_currentTraction      = (float)params->find("traction")->as<double>();
    m_steerTorque            = (float)params->find("steer_torque")->as<double>();
    m_steerTorqueOffset  = (float)params->find("steer_torque_offset")->as<double>();
    m_driftFriction        = (float)params->find("drift_friction")->as<double>();
    m_dragModifier         = (float)params->find("drag_modifier")->as<double>();
    m_steerAllowSpeed      = (float)params->find("steer_allow_speed")->as<double>();

    // Read our polygonal car body vertices
    toml::Value* carBodyParams = documentRoot.find("carbody");

    const toml::Array& bodyVertices = carBodyParams->find("body_vertices")->as<toml::Array>();

    b2Vec2 *vertices = new b2Vec2[bodyVertices.size()];
    unsigned int k = 0;
    for (const toml::Value& v : bodyVertices)
    {
        const toml::Array& bodyVerticesCoords = v.as<toml::Array>();
        vertices[k++].Set(bodyVerticesCoords.at(0).asNumber(), bodyVerticesCoords.at(1).asNumber());
    }

    // Create the car body definition
    b2BodyDef bodyDef;
    // Mark it as dynamic (it's a car so it will move :) )
    bodyDef.type = b2_dynamicBody;
    // This creates and adds the body to the world (adds it as the first element of the double linked list)
    m_body = world->CreateBody(&bodyDef);
    // We set the angular damping
    m_body->SetAngularDamping(m_angularDamping);

    // Create the poly shape from the vertices and link it to a fixture
    b2PolygonShape polygonShape;
    polygonShape.Set(vertices, bodyVertices.size());
    b2Fixture* fixture = m_body->CreateFixture(&polygonShape, m_bodyDensity);

    // Set the collision filter for our car (it should only collide with static, i.e walls)
    b2Filter filter;
    filter.categoryBits = CATEGORY_CAR;
    filter.maskBits     = CATEGORY_STATIC;

    fixture->SetFilterData(filter);

    // Set user data so we can identify our car in a possible collision, or sensor trigger
    fixture->SetUserData(new CarFUD(id));

    // Cleanup
    delete[] vertices;
}

I used collision filters because I don’t want the cars colliding each other. My first goal is to be able to have multiple cars on the same track (like a whole bunch of evolution genomes ;) ) at the same time.

The first AI problems I will tackle will be getting the best time in a lap and tuning an AI to play similar to a certain human player. For other AI problems, like learning to overtake and deal with other car collisions I change the code.

As you can see we need to setup the car with some user data because the collision event is of type b2Contact and it contains the box2D fixtures involved in the collision. We can get our user information from the fixture (and access our high level code). This method is used in almost all physics engines but usually the contact is called a manifold (which I like better :) ). Of course I could have used the void pointer to hold my id, but it’s always better (actually becomes mandatory) to have an extensible class system for your user defined data (if you have more than one type of user defined data, that is not an id, you can’t avoid this).

Creating our car update

Ok so we have the body but if we don’t apply forces, impulses or anything, it will just stay there. This could be ok if we were writing a parked car simulation :) but we want our cars to race.

We start by doing a broad stroke of our update:

1
2
3
4
5
6
void CarModel::update(float* controlState)
{
    updateFriction();
    updateDrive(controlState);
    updateTurn(controlState);
}

So we first update the friction (this is the part of the update that manages the physical state of the object without needing to know the controlState). It’s literally code that has to do with friction :) . After doing this, we update the drive (this handles breaking, reversing and accelerating based on input) and the update the turning (this handles turning right and left based on input of course :) ).

 Friction

For the friction part we first need to counter lateral velocity (so we can simulate the lateral friction of the wheels). We won’t counter all lateral velocity because we want our car to be able to drift :) .

A Drifting mechanic is always something cool in games as it makes you break less. Most people say that good racing games/tracks make you break less, somehow breaking is a fun spoiler, probably because it makes people come out of their flow.

We then need to apply some angular rotation friction and inertia so the torque doesn’t make the car spin for an infinite amount of time (simulating the lateral wheel friction on the angular part).

We then apply the drag coefficient and friction for the forward linear velocity.

Basically all the code in updateFriction makes the reaction forces that oppose our control/motor forces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void CarModel::updateFriction()
{
  //calculate the counter lateral impulse based on drift parameters
    b2Vec2 impulse = m_body->GetMass() * -getLateralVelocity();
    if (impulse.Length() > m_maxLateralImpulse)
        impulse *= m_maxLateralImpulse / impulse.Length();
    // apply the impulse
    m_body->ApplyLinearImpulse(m_driftFriction * impulse, m_body->GetWorldCenter(), true);

    //angular velocity
    m_body->ApplyAngularImpulse(m_angularFriction * m_body->GetInertia() * -m_body->GetAngularVelocity(), true);

    //forward linear velocity
    b2Vec2 currentForwardNormal = getForwardVelocity();
    float currentForwardSpeed = currentForwardNormal.Normalize();
    float dragForceMagnitude = -2 * currentForwardSpeed * m_dragModifier;

    m_body->ApplyForce(m_currentTraction * dragForceMagnitude * currentForwardNormal, m_body->GetWorldCenter(), true);
}

 Drive

The drive function is really easy. We get the desired speed from our controls, because we have different max speeds for going forward and for going backward. We get the forward velocity by projecting the body velocity on the lookat car vector. If the current speed is not the desired speed we apply a drive force and that’s it !

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
void CarModel::updateDrive(float* controlState)
{
    //wake the body, it could be in a sleep state
    m_body->SetAwake(true);
    //find desired speed
    float desiredSpeed = 0;
    if (controlState[OA_UP] > 0.0f)
        desiredSpeed = m_maxForwardSpeed;
    if (controlState[OA_DOWN] > 0.0f)
        desiredSpeed = m_maxBackwardSpeed;

    //find current speed in forward direction
    b2Vec2 currentForwardNormal = m_body->GetWorldVector(b2Vec2(0, 1));
    float currentSpeed = b2Dot(getForwardVelocity(), currentForwardNormal);

    //apply necessary force
    float force = (desiredSpeed > currentSpeed) ? m_maxDriveForce : -m_maxDriveForce;
    if (desiredSpeed != currentSpeed)
    {
        m_body->ApplyForce(m_currentTraction * force * currentForwardNormal, m_body->GetWorldCenter(), true);
    }
}

 Turn

The turn function looks very much like the drive function, only it operates on torque. Before using this I also tried making the torque proportional to forward speed (it is really easy to do , you can use code from the drive function to get the speed and then normalize it by dividing on maxSpeed and multiplying the result with the torque variable), hence the m_steerTorqueOffset. I didn’t like the results, but please feel free to try, I will keep the m_steerTorqueOffset in case I will revisit this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void CarModel::updateTurn(float* controlState)
{
    float desiredTorque = 0;
    // set our torque
    float torque = m_steerTorque  + m_steerTorqueOffset;
    if (controlState[OA_LEFT] > 0.0f)
        desiredTorque = torque;
    if (controlState[OA_RIGHT] > 0.0f)
        desiredTorque = -torque;

    // reverse the torque if we are going backwars
    if (controlState[OA_DOWN] > 0.0f)
        desiredTorque = -desiredTorque;

    m_body->ApplyTorque(desiredTorque, true);
}

So that is basically it! Feel free to check out the full code.

ghysics is a term I use to describe gameplay physics. ghysics is usually a very rough approximations of real physics. The focus is on making the model fun and as least computational complex as possible.