Box2D C++ tutorials - Sensors
Last edited: July 14 2013Chinese 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
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, }; |
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 ); |

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); |

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); |

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); |
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 ); } }; |
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); |

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); |
