Box2D C++ tutorials - Sensors

Last edited: July 14 2013

Chinese version -> 中文
Chinese version 2 -> 中文、第二

Sensors


In the previous topic we saw some settings that can be used to prevent two fixtures from colliding. They just pass through each other as if they couldn't see each other at all, even though we can see on the screen that they are ovelapping. While this might be the physical behaviour we want, it comes with a drawback: since the fixtures don't collide, they never give us any BeginContact/EndContact information!

What if we want to have two entities not collide, like the ships and the aircraft in the previous example, but we still wanted to get some feedback about when they are overlapping. For example, you might want an enemy plane to drop a bomb when it flies over a friendly ship, or some other kind of game logic. Simply turning off collision between ships and aircraft with collision filtering will not let us know about these events, but there is another method.

A fixture can be made into a 'sensor' by setting the isSensor member of the fixture definition to true when you create it, or by calling SetSensor(bool) on the fixture after it has been created if you need to change it during the simulation. Sensors behave as if their maskBits is set to zero - they never collide with anything. But they do generate BeginContact/EndContact callbacks to let us know when they start or stop overlapping another fixture.

All other features of sensor fixtures remain the same as for a normal fixture. They can be added to any type of body. They still have a mass and will affect the overall mass of the body they are attached to. Remember you can have more than one fixture on a body so you can have a mix of solid shapes and sensors, allowing for all kinds of neat things. Here are a few ideas:
  • detect entities entering or leaving a certain area
  • switches to trigger an event
  • detect ground under player character
  • a field of vision for an entity
As far as coding goes, all you really need to do to use this feature is add the isSensor = true to your fixtures, but stopping here would make this topic way too short :) Let's make a demonstration in the testbed for the last example above, once again with the top-down battlefield scenario where we use a sensor to represent what an entity can see. Along with sensors, we'll throw in a bit of each of the last few topics - user data, collision filtering and callbacks.

In this demonstration, our scene will have one friendly ship and some enemy aircraft. The ship will be equipped with a circular radar to detect the aircraft when they come within range. We will also have a friendly radar tower in a fixed position, with a similar but rotating radar to detect the enemy aircract. The collision bits will be set as you might expect: all entities at surface level collide with each other, but the aircraft only collide with the boundary fence.

When one of the friendly entities detects an enemy aircraft, we will change it's color so we can see that the code is working correctly. We will also go one step further and keep track of which enemies each friendly can see, so that we can have efficient access to an up-to-date list of enemies within range for each friendly - this is obviously a useful thing to have for things like AI and other game logic.

My dog has fleas. Ha... just checking if you're still awake after all that text with no code or screenshots. Anyway enough waffle, let's get started.

Example usage


Since this scenario is very much like the last one, we'll use that as a beginning point. Remember we had large circles as ships, and small circles as aircraft. This time we will create just one ship as a friendly, and three aircraft as enemies. Since we have a couple of new entity types, we'll need to add those categories to the flags we use for collision filtering:
1
2
3
4
5
6
7
8
9
  enum _entityCategory {
    BOUNDARY =          0x0001,
    FRIENDLY_SHIP =     0x0002,
    ENEMY_SHIP =        0x0004,
    FRIENDLY_AIRCRAFT = 0x0008,
    ENEMY_AIRCRAFT =    0x0010,
    FRIENDLY_TOWER =    0x0020,
    RADAR_SENSOR =      0x0040,
  };
Set up the entities in FooTest constructor like this...
1
2
3
4
5
6
7
8
9
10
11
12
  //one friendly ship
  Ball* ship = new Ball(m_world, 3, green, FRIENDLY_SHIP, BOUNDARY | FRIENDLY_TOWER );
  balls.push_back( ship );
  
  //three enemy aircraft
  for (int i = 0; i < 3; i++) 
    balls.push_back( new Ball(m_world, 1, red, ENEMY_AIRCRAFT, BOUNDARY | RADAR_SENSOR ) );
  
  //a tower entity
  Ball* tower = new Ball(m_world, 1, green, FRIENDLY_TOWER, FRIENDLY_SHIP );
  tower->m_body->SetType(b2_kinematicBody);
  balls.push_back( tower );
The first parts you already understand, the last part is almost the same except we don't want the tower to be a dynamic body. Rather than add a parameter to the Ball class constructor it's simpler just to alter it like this for a one-off case. Why couldn't we make the tower a static body? Well we could, but then it wouldn't be able to rotate - remember we want this to be a rotating radar. You should be seeing something like this: Sensors Now let's make some sensors. After the code above, we can add a sensor to the ship like this:
1
2
3
4
5
6
7
8
  //add radar sensor to ship
  b2CircleShape circleShape;
  circleShape.m_radius = 8;
  myFixtureDef.shape = &circleShape;
  myFixtureDef.isSensor = true;
  myFixtureDef.filter.categoryBits = RADAR_SENSOR;
  myFixtureDef.filter.maskBits = ENEMY_AIRCRAFT;//radar only collides with aircraft
  ship->m_body->CreateFixture(&myFixtureDef);
Sensors The tower will get a semi-circle sensor, and the angular velocity will be non-zero:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  //add semicircle radar sensor to tower
  float radius = 8;
  b2Vec2 vertices[8];
  vertices[0].Set(0,0);
  for (int i = 0; i < 7; i++) {
      float angle = i / 6.0 * 90 * DEGTORAD;
      vertices[i+1].Set( radius * cosf(angle), radius * sinf(angle) );
  }
  polygonShape.Set(vertices, 8);
  myFixtureDef.shape = &polygonShape;
  tower->m_body->CreateFixture(&myFixtureDef);
  
  //make the tower rotate at 45 degrees per second
  tower->m_body->SetAngularVelocity(45 * DEGTORAD);
Sensors Now all that's left to do is the collision callback implementation. Remember we want to store a list of the enemy aircraft that each friendly entity can currently see. For that obviously we will need to have a list to store these in, and it would also be good to have functions to make changes to the list when Box2D lets us know something has changed.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  //Ball class member
  std::vector<Ball*> visibleEnemies;
  
  //in Ball class
  void radarAcquiredEnemy(Ball* enemy) {
    visibleEnemies.push_back( enemy );
  }
  void radarLostEnemy(Ball* enemy) {
    visibleEnemies.erase( std::find(visibleEnemies.begin(), visibleEnemies.end(), enemy ) );
  }
  
  //in Ball::render  
  if ( visibleEnemies.size() > 0 )
      glColor3f(1,1,0); //yellow
  else
      glColor3f(m_color.r, m_color.g, m_color.b);
I'm really starting to think the name 'Ball' for this class is not appropriate anymore... maybe we should have called it Entity to begin with :)
Now set up a collision callback to tell the friendly entities when their radar sensor begins or ends contact with an enemy aircraft. Since much of the code here tends to get repeated, I have put the repetitive part in a sub function so that the main logic in the callback itself is easier to see.
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
  //helper function to figure out if the collision was between
  //a radar and an aircraft, and sort out which is which
  bool getRadarAndAircraft(b2Contact* contact, Ball*& radarEntity, Ball*& aircraftEntity)
  {
      b2Fixture* fixtureA = contact->GetFixtureA();
      b2Fixture* fixtureB = contact->GetFixtureB();
  
      //make sure only one of the fixtures was a sensor
      bool sensorA = fixtureA->IsSensor();
      bool sensorB = fixtureB->IsSensor();
      if ( ! (sensorA ^ sensorB) )
          return false;
  
      Ball* entityA = static_cast<Ball*>( fixtureA->GetBody()->GetUserData() );
      Ball* entityB = static_cast<Ball*>( fixtureB->GetBody()->GetUserData() );
  
      if ( sensorA ) { //fixtureB must be an enemy aircraft
          radarEntity = entityA;
          aircraftEntity = entityB;
      }
      else { //fixtureA must be an enemy aircraft
          radarEntity = entityB;
          aircraftEntity = entityA;
      }
      return true;
  }
  
  //main collision call back function
  class MyContactListener : public b2ContactListener
  {
    void BeginContact(b2Contact* contact) {
        Ball* radarEntity;
        Ball* aircraftEntity;
        if ( getRadarAndAircraft(contact, radarEntity, aircraftEntity) )
            radarEntity->radarAcquiredEnemy( aircraftEntity );
    }
  
    void EndContact(b2Contact* contact) {
        Ball* radarEntity;
        Ball* aircraftEntity;
        if ( getRadarAndAircraft(contact, radarEntity, aircraftEntity) )
            radarEntity->radarLostEnemy( aircraftEntity );
    }
  };
Almost there... to complete the system we need to set the user data in the physics body to point to our entity object, and let the physics engine know about our contact listener:
1
2
3
4
5
6
7
8
  //in Ball constructor
  m_body->SetUserData( this );
  
  //at global scope
  MyContactListener myContactListenerInstance;
  
  //in FooTest constructor
  m_world->SetContactListener(&myContactListenerInstance);
Now if all goes well, you should see the ship and the ship and the tower turn yellow whenever any aircraft are touching their radar sensor. Neat! Sensors As one last little exercise, remember how we wanted to have an up-to-date list of all visible enemies for each friendly entity that we could call efficiently? Well now we have one, so let's take it for a spin. We can draw a line between each radar and the entities in view.
1
2
3
4
5
6
7
8
9
10
11
12
13
  //in Ball::renderAtBodyPosition
      b2Vec2 pos = m_body->GetPosition(); //(existing code)
      glColor3f(1,1,1);//white
      glLineStipple( 1, 0xF0F0 ); //evenly dashed line
      glEnable(GL_LINE_STIPPLE); 
      glBegin(GL_LINES);
      for (int i = 0; i < visibleEnemies.size(); i++) {
          b2Vec2 enemyPosition = visibleEnemies[i]->m_body->GetPosition();
          glVertex2f(pos.x, pos.y);
          glVertex2f(enemyPosition.x, enemyPosition.y);
      }
      glEnd();
      glDisable(GL_LINE_STIPPLE);
Sensors