| name | unity-physicscore2d-collision |
| description | Collision detection, contact manifolds, collision responses, determinism, and character collision handling |
Unity PhysicsCore2D Collision Expert
You are now acting as a Unity PhysicsCore2D collision expert, specialized in collision detection, responses, and character movement.
Overview
PhysicsCore2D provides comprehensive collision detection and response capabilities including:
- Contact point generation and manifolds
- Collision callbacks and events (via IContactCallback)
- Character controller collision handling
- Deterministic collision behavior
- Bounce and restitution control
PhysicsCallbacks.IContactCallback
API Reference: https://docs.unity3d.com/6000.5/Documentation/ScriptReference/Unity.U2D.Physics.PhysicsCallbacks.IContactCallback.html
PhysicsCallbacks.IContactCallback is the interface for receiving collision events during physics simulation. Callbacks are assigned per-shape by setting the PhysicsShape.callbackTarget property.
Callback Methods
The interface provides two callback methods:
public interface IContactCallback
{
void OnContactBegin2D(PhysicsEvents.ContactBeginEvent beginEvent);
void OnContactEnd2D(PhysicsEvents.ContactEndEvent endEvent);
}
Event struct members (both ContactBeginEvent and ContactEndEvent):
| Member | Type | Description |
|---|
shapeA | PhysicsShape | One of the shapes; may be invalid if destroyed in an earlier callback |
shapeB | PhysicsShape | The other shape; same caveat |
contactId | PhysicsShape.ContactId | Volatile contact handle — call .contact to get the PhysicsShape.Contact |
Accessing the contact manifold from a begin event:
var contact = beginEvent.contactId.contact;
var manifold = contact.manifold;
How Callbacks Work
- Create a class that implements
PhysicsCallbacks.IContactCallback
- Set the
callbackTarget property on each PhysicsShape you want to monitor
- When that shape begins or ends contact with another shape, the callbacks are invoked during
PhysicsWorld.Step()
Implementation Example
using Unity.Collections;
using UnityEngine;
using Unity.U2D.Physics;
public class CollisionHandler : MonoBehaviour, PhysicsCallbacks.IContactCallback
{
private PhysicsWorld m_PhysicsWorld;
private void OnEnable()
{
m_PhysicsWorld = PhysicsWorld.Create();
m_PhysicsWorld.autoContactCallbacks = true;
var shapeDef = new PhysicsShapeDefinition
{
contactFilter = new PhysicsShape.ContactFilter { categories = PhysicsMask.One, contacts = PhysicsMask.All },
surfaceMaterial = new PhysicsShape.SurfaceMaterial { bounciness = 0.8f },
contactEvents = true,
};
var body1 = m_PhysicsWorld.CreateBody(new PhysicsBodyDefinition
{ type = PhysicsBody.BodyType.Dynamic, position = new Vector2(-0.25f, 2f) });
var body2 = m_PhysicsWorld.CreateBody(new PhysicsBodyDefinition
{ type = PhysicsBody.BodyType.Dynamic, position = new Vector2(0.25f, 4f) });
var shape1 = body1.CreateShape(CircleGeometry.defaultGeometry, shapeDef);
var shape2 = body2.CreateShape(CircleGeometry.defaultGeometry, shapeDef);
shape1.callbackTarget = this;
shape2.callbackTarget = this;
}
private void OnDisable()
{
m_PhysicsWorld.Destroy();
}
public void OnContactBegin2D(PhysicsEvents.ContactBeginEvent beginEvent)
{
if (!beginEvent.shapeA.isValid || !beginEvent.shapeB.isValid)
return;
var contact = beginEvent.contactId.contact;
var manifold = contact.manifold;
Debug.Log($"Contact began: normal={manifold.normal}, points={manifold.pointCount}");
foreach (var mp in manifold)
{
m_PhysicsWorld.DrawPoint(mp.point, radius: 20f, color: Color.yellow, lifetime: 1f);
}
}
public void OnContactEnd2D(PhysicsEvents.ContactEndEvent endEvent)
{
if (!endEvent.shapeA.isValid || !endEvent.shapeB.isValid)
return;
Debug.Log("Contact ended");
}
}
Practical Example: Space Invaders Bullet Collision
using Unity.Collections;
using UnityEngine;
using Unity.U2D.Physics;
public class SpaceInvadersCollision : MonoBehaviour, PhysicsCallbacks.IContactCallback
{
private readonly PhysicsMask m_BulletMask = new(1);
private readonly PhysicsMask m_EnemyMask = new(2);
private readonly PhysicsMask m_WallMask = new(3);
private const float BulletSpeed = 20f;
private const float BulletRadius = 0.15f;
private PhysicsWorld m_World;
private Vector2 m_CannonPosition = new(0f, -4f);
private void OnEnable()
{
m_World = PhysicsWorld.defaultWorld;
m_World.autoContactCallbacks = true;
SpawnEnemies();
SpawnWalls();
}
public void Fire()
{
var bulletBody = m_World.CreateBody(new PhysicsBodyDefinition
{
type = PhysicsBody.BodyType.Dynamic,
gravityScale = 0f,
fastCollisionsAllowed = true,
position = m_CannonPosition + Vector2.up * (BulletRadius + 0.1f),
linearVelocity = Vector2.up * BulletSpeed,
});
var bulletShapeDef = new PhysicsShapeDefinition
{
contactFilter = new PhysicsShape.ContactFilter
{
categories = m_BulletMask,
contacts = m_EnemyMask | m_WallMask,
},
contactEvents = true,
};
var bulletShape = bulletBody.CreateShape(
new CircleGeometry { radius = BulletRadius }, bulletShapeDef);
bulletShape.callbackTarget = this;
}
public void OnContactBegin2D(PhysicsEvents.ContactBeginEvent beginEvent)
{
if (!beginEvent.shapeA.isValid || !beginEvent.shapeB.isValid)
return;
var catA = beginEvent.shapeA.contactFilter.categories;
var catB = beginEvent.shapeB.contactFilter.categories;
if (catA == m_WallMask || catB == m_WallMask)
{
var bulletShape = catA == m_BulletMask ? beginEvent.shapeA : beginEvent.shapeB;
if (bulletShape.isValid)
bulletShape.body.Destroy();
return;
}
if (catA == m_EnemyMask || catB == m_EnemyMask)
{
var contact = beginEvent.contactId.contact;
var hitPoint = contact.manifold.points[0].point;
var hitNormal = contact.manifold.normal;
Debug.Log($"Hit at {hitPoint}, normal {hitNormal}");
var bulletBody = catA == m_BulletMask ? beginEvent.shapeA.body : beginEvent.shapeB.body;
var enemyBody = catA == m_EnemyMask ? beginEvent.shapeA.body : beginEvent.shapeB.body;
if (bulletBody.isValid) bulletBody.Destroy();
if (enemyBody.isValid) enemyBody.Destroy();
}
}
public void OnContactEnd2D(PhysicsEvents.ContactEndEvent endEvent)
{
}
private void SpawnEnemies()
{
var enemyShapeDef = new PhysicsShapeDefinition
{
contactFilter = new PhysicsShape.ContactFilter
{
categories = m_EnemyMask,
contacts = m_BulletMask,
},
contactEvents = true,
};
for (int col = 0; col < 5; col++)
for (int row = 0; row < 3; row++)
{
var body = m_World.CreateBody(new PhysicsBodyDefinition
{
type = PhysicsBody.BodyType.Static,
position = new Vector2(col * 2f - 4f, row * 1.5f + 1f),
});
var shape = body.CreateShape(
PolygonGeometry.CreateBox(new Vector2(0.8f, 0.6f)), enemyShapeDef);
shape.callbackTarget = this;
}
}
private void SpawnWalls()
{
var wallShapeDef = new PhysicsShapeDefinition
{
contactFilter = new PhysicsShape.ContactFilter
{
categories = m_WallMask,
contacts = m_BulletMask,
},
};
var wallBody = m_World.CreateBody();
wallBody.CreateShape(PolygonGeometry.CreateBox(new Vector2(0.5f, 10f),
offset: new Vector2(-6f, 0f)), wallShapeDef);
wallBody.CreateShape(PolygonGeometry.CreateBox(new Vector2(0.5f, 10f),
offset: new Vector2( 6f, 0f)), wallShapeDef);
}
}
Key reminder: shape.contactEvents = true (or in the PhysicsShapeDefinition) must be set on at least one shape in the pair, and world.autoContactCallbacks = true must be set, or no callbacks will be dispatched. Callbacks fire on the main thread after PhysicsWorld.Step() completes.
Important Notes
Setting Callback Targets:
- Set
PhysicsShape.callbackTarget property to an object implementing IContactCallback
- Each shape can have its own callback target
- Multiple shapes can share the same callback target object
- Callbacks are invoked during
PhysicsWorld.Step()
CRITICAL - Always Check Shape Validity:
Multiple callbacks can fire in the same frame. If an earlier callback destroys a PhysicsBody, subsequent callbacks involving that body will have invalid shapes. Always check shape.isValid before accessing shape properties:
public void OnContactBegin2D(PhysicsEvents.ContactBeginEvent contactEvent)
{
if (!contactEvent.shapeA.isValid || !contactEvent.shapeB.isValid)
return;
var bodyA = contactEvent.shapeA.body;
var bodyB = contactEvent.shapeB.body;
}
public void OnContactEnd2D(PhysicsEvents.ContactEndEvent contactEvent)
{
if (!contactEvent.shapeA.isValid || !contactEvent.shapeB.isValid)
return;
}
CRITICAL - Dynamic Body Requirement:
- Callbacks only fire for collisions involving at least one Dynamic body
- Kinematic vs Kinematic collisions DO NOT trigger callbacks
- Static vs Static collisions DO NOT trigger callbacks
- If using only Kinematic bodies (like in Space Invaders), you must either:
- Change at least one body type to Dynamic (e.g., make bullets Dynamic), OR
- Use manual overlap queries (from unity-physicscore2d-queries skill) instead
Performance:
- Callbacks are called during physics simulation, so keep them lightweight
- Avoid allocating memory in callbacks
- Cache frequently accessed data
Thread Safety:
- Contact callbacks may be called from the physics simulation thread
- Be careful when accessing Unity components or GameObjects
- Consider queuing actions to execute on the main thread if needed
When to Use IContactCallback vs Manual Queries
Use IContactCallback when:
- You have at least one Dynamic body involved in collisions
- You need precise collision timing during physics simulation
- You want automatic collision detection with minimal overhead
- You need Begin/End lifecycle events
Use Manual Queries (OverlapGeometry, etc.) when:
- You're using only Kinematic bodies that don't trigger callbacks
- You need to check for potential collisions before they happen
- You need fine control over when collision checks occur
- You're implementing custom character controllers with kinematic bodies
Repository Examples
Reference examples from the PhysicsExamples2D repository:
Key Collision Topics
Contact Manifolds
Contact manifolds contain information about collision contact points between two shapes:
- Contact point positions
- Contact normals
- Penetration depths
- Contact IDs for tracking persistent contacts
Character Movement
For character controllers that need collision detection:
- Use kinematic bodies for direct position control
- Query for overlaps before moving
- Resolve collisions manually for fine control
- Handle slopes and steps appropriately
Determinism
PhysicsCore2D is deterministic when:
- Same initial conditions are used
- Same sequence of operations is performed
- Floating point precision is consistent
- Random seeds are controlled
Collision Callbacks via IContactCallback
Monitor collisions by implementing PhysicsCallbacks.IContactCallback interface:
- OnContactBegin2D - when collision starts (first contact between shapes)
- OnContactEnd2D - when collision ends (shapes separate)
See the "PhysicsCallbacks.IContactCallback" section above for detailed implementation examples.
Important: Callbacks only fire for collisions involving at least one Dynamic body. Kinematic-only collisions require manual overlap queries.
Best Practices
- Always check collision results before acting on them
- Use appropriate collision layers and filtering
- Consider using continuous collision detection for fast-moving objects
- Cache collision queries when performing multiple checks
- For character controllers, prefer swept queries over simple overlaps
- Set
PhysicsShape.callbackTarget only on shapes that need collision notifications
Related Skills
When users need information about:
- Collision filtering - Use unity-physicscore2d-filtering
- Shape properties - Use unity-physicscore2d-shapes-advanced
- Physics queries - Use unity-physicscore2d-queries
- Materials and friction - Use unity-physicscore2d-materials