Skip to content

Commit

Permalink
Event bookend (#837)
Browse files Browse the repository at this point in the history
Sensor and contact end touch events are now generated whenever a contact
is destroyed via user operations, such as:
- destroying a body or shape
- changing filters
- changing body type
- disabling a body
- connecting bodies via joints

Enable hot reloading on MSVC.
  • Loading branch information
erincatto authored Nov 10, 2024
1 parent 0f192cd commit 90c2781
Show file tree
Hide file tree
Showing 9 changed files with 328 additions and 146 deletions.
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ if (MSVC OR APPLE)
add_compile_options(-fsanitize=address -fsanitize-address-use-after-scope -fsanitize=undefined)
add_link_options(-fsanitize=address -fsanitize-address-use-after-scope -fsanitize=undefined)
endif()
else()
if(MSVC)
# enable hot reloading
add_compile_options("$<$<CONFIG:Debug>:/ZI>")
add_link_options("$<$<CONFIG:Debug>:/INCREMENTAL>")
endif()
endif()
endif()

Expand Down
1 change: 1 addition & 0 deletions docs/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ of the time step:

- body movement events
- contact begin and end events
- sensor begin and end events
- contact hit events

These events allow your application to react to changes in the simulation.
Expand Down
33 changes: 26 additions & 7 deletions docs/simulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1009,21 +1009,34 @@ for (int i = 0; i < sensorEvents.beginCount; ++i)
}
```

And there are events when a shape stops overlapping with a sensor.
And there are events when a shape stops overlapping with a sensor. Be careful with end
touch events because they may be generated when shapes are destroyed. Test the shape
ids with `b2Shape_IsValid`.

```c
for (int i = 0; i < sensorEvents.endCount; ++i)
{
b2SensorEndTouchEvent* endTouch = sensorEvents.endEvents + i;
void* myUserData = b2Shape_GetUserData(endTouch->visitorShapeId);
// process end event
if (b2Shape_IsValid(endTouch->visitorShapeId))
{
void* myUserData = b2Shape_GetUserData(endTouch->visitorShapeId);
// process end event
}
}
```

You will not get end events if a shape is destroyed. Sensor events should
be processed after the world step and before other game logic. This should
Sensor events should be processed after the world step and before other game logic. This should
help you avoid processing stale data.

There are several user operations that can cause sensors to stop touching. Such operations
include:
- destroying a body or shape
- changing the filter on a shape
- disabling a body
- setting the body transform
These may generate end-touch events and these events are included with simulation events available
after the next call to `b2World_Step`.

Sensor events are only enabled for a non-sensor shape if `b2ShapeDef::enableSensorEvents`
is true.

Expand Down Expand Up @@ -1065,11 +1078,17 @@ contain the two shape ids.
for (int i = 0; i < contactEvents.endCount; ++i)
{
b2ContactEndTouchEvent* endEvent = contactEvents.endEvents + i;
ShapesStopTouching(endEvent->shapeIdA, endEvent->shapeIdB);

// Use b2Shape_IsValid because a shape may have been destroyed
if (b2Shape_IsValid(endEvent->shapeIdA) && b2Shape_IsValid(endEvent->shapeIdB))
{
ShapesStopTouching(endEvent->shapeIdA, endEvent->shapeIdB);
}
}
```

The end touch events are not generated when you destroy a shape or the body that owns it.
Similar to `b2SensorEndTouchEvent`, `b2ContactEndTouchEvent` may be generated due to a user operation,
such as destroying a body or shape. These events are included with simulation events after the next `b2World_Step`.

Shapes only generate begin and end touch events if `b2ShapeDef::enableContactEvents` is true.

Expand Down
17 changes: 13 additions & 4 deletions include/box2d/types.h
Original file line number Diff line number Diff line change
Expand Up @@ -969,16 +969,21 @@ typedef struct b2SensorBeginTouchEvent
} b2SensorBeginTouchEvent;

/// An end touch event is generated when a shape stops overlapping a sensor shape.
/// You will not get an end event if you do anything that destroys contacts outside
/// of the world step. These include things like setting the transform, destroying a body
/// You will get an end event if you do anything that destroys contacts previous to the last
/// world step. These include things like setting the transform, destroying a body
/// or shape, or changing a filter or body type.
typedef struct b2SensorEndTouchEvent
{
/// The id of the sensor shape
/// @warning this shape may have been destroyed
/// @see b2Shape_IsValid
b2ShapeId sensorShapeId;

/// The id of the dynamic shape that stopped touching the sensor shape
/// @warning this shape may have been destroyed
/// @see b2Shape_IsValid
b2ShapeId visitorShapeId;

} b2SensorEndTouchEvent;

/// Sensor events are buffered in the Box2D world and are available
Expand Down Expand Up @@ -1013,15 +1018,19 @@ typedef struct b2ContactBeginTouchEvent
} b2ContactBeginTouchEvent;

/// An end touch event is generated when two shapes stop touching.
/// You will not get an end event if you do anything that destroys contacts outside
/// of the world step. These include things like setting the transform, destroying a body
/// You will get an end event if you do anything that destroys contacts previous to the last
/// world step. These include things like setting the transform, destroying a body
/// or shape, or changing a filter or body type.
typedef struct b2ContactEndTouchEvent
{
/// Id of the first shape
/// @warning this shape may have been destroyed
/// @see b2Shape_IsValid
b2ShapeId shapeIdA;

/// Id of the second shape
/// @warning this shape may have been destroyed
/// @see b2Shape_IsValid
b2ShapeId shapeIdB;
} b2ContactEndTouchEvent;

Expand Down
168 changes: 161 additions & 7 deletions samples/sample_events.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
#include <imgui.h>
#include <vector>

class SensorEvent : public Sample
class SensorFunnel : public Sample
{
public:
enum
Expand All @@ -24,7 +24,7 @@ class SensorEvent : public Sample
e_count = 32
};

explicit SensorEvent( Settings& settings )
explicit SensorFunnel( Settings& settings )
: Sample( settings )
{
if ( settings.restart == false )
Expand Down Expand Up @@ -56,7 +56,7 @@ class SensorEvent : public Sample
{ -8.26825142, 11.906374 }, { -16.8672504, 17.1978741 },
};

int count = sizeof( points ) / sizeof( points[0] );
int count = std::size( points );

// float scale = 0.25f;
// b2Vec2 lower = {FLT_MAX, FLT_MAX};
Expand Down Expand Up @@ -101,7 +101,6 @@ class SensorEvent : public Sample
float y = 14.0f;
for ( int i = 0; i < 3; ++i )
{
b2BodyDef bodyDef = b2DefaultBodyDef();
bodyDef.position = { 0.0f, y };
bodyDef.type = b2_dynamicBody;

Expand Down Expand Up @@ -317,7 +316,7 @@ class SensorEvent : public Sample

static Sample* Create( Settings& settings )
{
return new SensorEvent( settings );
return new SensorFunnel( settings );
}

Human m_humans[e_count];
Expand All @@ -328,7 +327,161 @@ class SensorEvent : public Sample
float m_side;
};

static int sampleSensorEvent = RegisterSample( "Events", "Sensor", SensorEvent::Create );
static int sampleSensorBeginEvent = RegisterSample( "Events", "Sensor Funnel", SensorFunnel::Create );

class SensorBookend : public Sample
{
public:
explicit SensorBookend( Settings& settings )
: Sample( settings )
{
if ( settings.restart == false )
{
g_camera.m_center = { 0.0f, 6.0f };
g_camera.m_zoom = 7.5f;
}

{
b2BodyDef bodyDef = b2DefaultBodyDef();
b2BodyId groundId = b2CreateBody( m_worldId, &bodyDef );
b2ShapeDef shapeDef = b2DefaultShapeDef();

b2Segment groundSegment = { { -10.0f, 0.0f }, { 10.0f, 0.0f } };
b2CreateSegmentShape( groundId, &shapeDef, &groundSegment );

groundSegment = { { -10.0f, 0.0f }, { -10.0f, 10.0f } };
b2CreateSegmentShape( groundId, &shapeDef, &groundSegment );

groundSegment = { { 10.0f, 0.0f }, { 10.0f, 10.0f } };
b2CreateSegmentShape( groundId, &shapeDef, &groundSegment );

m_isVisiting = false;
}

CreateSensor();

CreateVisitor();
}

void CreateSensor()
{
b2BodyDef bodyDef = b2DefaultBodyDef();

bodyDef.position = { 0.0f, 1.0f };
m_sensorBodyId = b2CreateBody( m_worldId, &bodyDef );

b2ShapeDef shapeDef = b2DefaultShapeDef();
shapeDef.isSensor = true;
b2Polygon box = b2MakeSquare( 1.0f );
m_sensorShapeId = b2CreatePolygonShape( m_sensorBodyId, &shapeDef, &box );
}

void CreateVisitor()
{
b2BodyDef bodyDef = b2DefaultBodyDef();
bodyDef.position = { -4.0f, 1.0f };
bodyDef.type = b2_dynamicBody;

m_visitorBodyId = b2CreateBody( m_worldId, &bodyDef );

b2ShapeDef shapeDef = b2DefaultShapeDef();
shapeDef.enableSensorEvents = true;

b2Circle circle = { { 0.0f, 0.0f }, 0.5f };
m_visitorShapeId = b2CreateCircleShape( m_visitorBodyId, &shapeDef, &circle );
}

void UpdateUI() override
{
float height = 90.0f;
ImGui::SetNextWindowPos( ImVec2( 10.0f, g_camera.m_height - height - 50.0f ), ImGuiCond_Once );
ImGui::SetNextWindowSize( ImVec2( 140.0f, height ) );

ImGui::Begin( "Sensor Bookend", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoResize );

if ( B2_IS_NULL( m_visitorBodyId ) )
{
if ( ImGui::Button( "create visitor" ) )
{
CreateVisitor();
}
}
else
{
if ( ImGui::Button( "destroy visitor" ) )
{
b2DestroyBody( m_visitorBodyId );
m_visitorBodyId = b2_nullBodyId;
m_visitorShapeId = b2_nullShapeId;
}
}

if ( B2_IS_NULL( m_sensorBodyId ) )
{
if ( ImGui::Button( "create sensor" ) )
{
CreateSensor();
}
}
else
{
if ( ImGui::Button( "destroy sensor" ) )
{
b2DestroyBody( m_sensorBodyId );
m_sensorBodyId = b2_nullBodyId;
m_sensorShapeId = b2_nullShapeId;
}
}

ImGui::End();
}

void Step( Settings& settings ) override
{
Sample::Step( settings );

b2SensorEvents sensorEvents = b2World_GetSensorEvents( m_worldId );
for ( int i = 0; i < sensorEvents.beginCount; ++i )
{
b2SensorBeginTouchEvent event = sensorEvents.beginEvents[i];

if ( B2_ID_EQUALS( event.visitorShapeId, m_visitorShapeId ) )
{
assert( m_isVisiting == false );
m_isVisiting = true;
}
}

for ( int i = 0; i < sensorEvents.endCount; ++i )
{
b2SensorEndTouchEvent event = sensorEvents.endEvents[i];

bool wasVisitorDestroyed = b2Shape_IsValid( event.visitorShapeId ) == false;
if ( B2_ID_EQUALS( event.visitorShapeId, m_visitorShapeId ) || wasVisitorDestroyed )
{
assert( m_isVisiting == true );
m_isVisiting = false;
}
}

g_draw.DrawString( 5, m_textLine, "visiting == %s", m_isVisiting ? "true" : "false" );
m_textLine += m_textIncrement;
}

static Sample* Create( Settings& settings )
{
return new SensorBookend( settings );
}

b2BodyId m_sensorBodyId;
b2ShapeId m_sensorShapeId;

b2BodyId m_visitorBodyId;
b2ShapeId m_visitorShapeId;
bool m_isVisiting;
};

static int sampleSensorBookendEvent = RegisterSample( "Events", "Sensor Bookend", SensorBookend::Create );

struct BodyUserData
{
Expand Down Expand Up @@ -927,7 +1080,8 @@ class Platformer : public Sample

b2ContactData contactData = {};
int contactCount = b2Body_GetContactData( m_platformId, &contactData, 1 );
g_draw.DrawString( 5, m_textLine, "Platform contact count = %d, point count = %d", contactCount, contactData.manifold.pointCount );
g_draw.DrawString( 5, m_textLine, "Platform contact count = %d, point count = %d", contactCount,
contactData.manifold.pointCount );
m_textLine += m_textIncrement;

g_draw.DrawString( 5, m_textLine, "Movement: A/D/Space" );
Expand Down
Loading

0 comments on commit 90c2781

Please sign in to comment.