원클릭으로
unity-physicscore2d-collision
Collision detection, contact manifolds, collision responses, determinism, and character collision handling
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Collision detection, contact manifolds, collision responses, determinism, and character collision handling
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
| name | unity-physicscore2d-collision |
| description | Collision detection, contact manifolds, collision responses, determinism, and character collision handling |
You are now acting as a Unity PhysicsCore2D collision expert, specialized in collision detection, responses, and character movement.
PhysicsCore2D provides comprehensive collision detection and response capabilities including:
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.
The interface provides two callback methods:
// IContactCallback interface declaration stub
// Source: PhysicsCallbacks.IContactCallback (events-api, PhysicsShapeContactCallback.cs)
public interface IContactCallback
{
// Called on the main thread after simulation, when two non-trigger shapes first touch.
void OnContactBegin2D(PhysicsEvents.ContactBeginEvent beginEvent);
// Called on the main thread after simulation, when two non-trigger shapes stop touching.
// Note: fires immediately if a body/shape is destroyed, or the body transform is set,
// even if the shapes were still overlapping.
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; // PhysicsShape.ContactManifold
// manifold.normal — world-space unit normal from shapeA to shapeB
// manifold.pointCount — number of active contact points [0..2]
// manifold[i] — ManifoldPoint via indexer
// foreach (var mp in manifold) — enumerable
PhysicsCallbacks.IContactCallbackcallbackTarget property on each PhysicsShape you want to monitorPhysicsWorld.Step()// Source: Primer/Assets/Scripts/PhysicsShapeContactCallback.cs (real-code)
using Unity.Collections;
using UnityEngine;
using Unity.U2D.Physics;
// Implement IContactCallback on any class (MonoBehaviour, plain C#, etc.).
public class CollisionHandler : MonoBehaviour, PhysicsCallbacks.IContactCallback
{
private PhysicsWorld m_PhysicsWorld;
private void OnEnable()
{
m_PhysicsWorld = PhysicsWorld.Create();
// Enable auto-dispatch of contact callbacks for this world.
m_PhysicsWorld.autoContactCallbacks = true;
// Shape definition: contactEvents = true is required for callbacks to fire.
var shapeDef = new PhysicsShapeDefinition
{
contactFilter = new PhysicsShape.ContactFilter { categories = PhysicsMask.One, contacts = PhysicsMask.All },
surfaceMaterial = new PhysicsShape.SurfaceMaterial { bounciness = 0.8f },
contactEvents = true, // <— REQUIRED to receive contact callbacks
};
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);
// Point this script as the callback target for both shapes.
// The target object must not be garbage collected while shapes are live.
shape1.callbackTarget = this;
shape2.callbackTarget = this;
}
private void OnDisable()
{
m_PhysicsWorld.Destroy();
}
// Called on the main thread, once per frame after PhysicsWorld.Step(),
// for every new contact pair that involves this callback target's shapes.
public void OnContactBegin2D(PhysicsEvents.ContactBeginEvent beginEvent)
{
// CRITICAL: Shapes can be destroyed by an earlier callback in the same frame.
if (!beginEvent.shapeA.isValid || !beginEvent.shapeB.isValid)
return;
// Retrieve the live contact via contactId.
var contact = beginEvent.contactId.contact;
var manifold = contact.manifold;
// manifold.normal — unit normal from shapeA to shapeB
// manifold.pointCount — how many contact points (0..2)
// manifold[i].point — world-space contact position (clip point)
// manifold[i].anchorA — position relative to shapeA's origin (prefer for game logic)
// manifold[i].anchorB — position relative to shapeB's origin
// manifold[i].separation — negative when penetrating
// manifold[i].normalImpulse — impulse along normal this step
Debug.Log($"Contact began: normal={manifold.normal}, points={manifold.pointCount}");
// Iterate contact points via foreach (manifold is enumerable).
foreach (var mp in manifold)
{
m_PhysicsWorld.DrawPoint(mp.point, radius: 20f, color: Color.yellow, lifetime: 1f);
}
// Alternatively, access by index:
// for (int i = 0; i < manifold.pointCount; i++)
// Debug.Log(manifold[i].anchorA);
}
// Called when the contact pair separates, or whenever anything destroys contacts
// (body destroyed, transform set, contact filter changed, etc.).
public void OnContactEnd2D(PhysicsEvents.ContactEndEvent endEvent)
{
// CRITICAL: Shapes may already be invalid here.
if (!endEvent.shapeA.isValid || !endEvent.shapeB.isValid)
return;
Debug.Log("Contact ended");
}
}
// Source: adapted from Sandbox/Assets/Examples/Fragmenting.cs (real-code)
// Pattern: space-invader style — bullet (Dynamic, gravityScale=0) hits an enemy (Dynamic/Static);
// callback destroys both and records the hit position.
// NOTE: Callbacks require at least one Dynamic body in the contact pair.
using Unity.Collections;
using UnityEngine;
using Unity.U2D.Physics;
public class SpaceInvadersCollision : MonoBehaviour, PhysicsCallbacks.IContactCallback
{
// Layer masks — use distinct PhysicsMask bits so contact filters work.
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;
// Enable contact callbacks for the world.
m_World.autoContactCallbacks = true;
SpawnEnemies();
SpawnWalls();
}
// Call this from Update / UI button / Input to fire a bullet.
public void Fire()
{
// Bullets must be Dynamic so IContactCallback fires.
// gravityScale = 0 keeps them moving straight up.
var bulletBody = m_World.CreateBody(new PhysicsBodyDefinition
{
type = PhysicsBody.BodyType.Dynamic,
gravityScale = 0f,
fastCollisionsAllowed = true, // CCD for fast-moving projectiles
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, // required for IContactCallback to be dispatched
};
var bulletShape = bulletBody.CreateShape(
new CircleGeometry { radius = BulletRadius }, bulletShapeDef);
// Register this MonoBehaviour as the callback target for the bullet shape.
bulletShape.callbackTarget = this;
}
public void OnContactBegin2D(PhysicsEvents.ContactBeginEvent beginEvent)
{
// Always validate — a previous callback in the same frame may have
// destroyed a body, invalidating its shapes.
if (!beginEvent.shapeA.isValid || !beginEvent.shapeB.isValid)
return;
var catA = beginEvent.shapeA.contactFilter.categories;
var catB = beginEvent.shapeB.contactFilter.categories;
// Bullet hit a wall — just remove the bullet.
if (catA == m_WallMask || catB == m_WallMask)
{
var bulletShape = catA == m_BulletMask ? beginEvent.shapeA : beginEvent.shapeB;
if (bulletShape.isValid)
bulletShape.body.Destroy();
return;
}
// Bullet hit an enemy.
if (catA == m_EnemyMask || catB == m_EnemyMask)
{
// Record the first contact point for a hit effect.
var contact = beginEvent.contactId.contact;
var hitPoint = contact.manifold.points[0].point; // world-space clip point
var hitNormal = contact.manifold.normal; // normal from shapeA to shapeB
Debug.Log($"Hit at {hitPoint}, normal {hitNormal}");
// Queue spawn of explosion VFX at hitPoint here (main-thread safe).
// Destroy both bodies (shape.body.Destroy() removes all shapes on that body).
// Validity checks guard against double-destruction from multi-shape compound bodies.
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)
{
// No action needed on separation in this scenario.
}
// ── helpers ──────────────────────────────────────────────────────────────
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++)
{
// Static enemies — bullet is Dynamic, so callbacks still fire.
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);
// Each enemy shape reports collisions back to this manager.
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(); // Static by default
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 thePhysicsShapeDefinition) must be set on at least one shape in the pair, andworld.autoContactCallbacks = truemust be set, or no callbacks will be dispatched. Callbacks fire on the main thread afterPhysicsWorld.Step()completes.
Setting Callback Targets:
PhysicsShape.callbackTarget property to an object implementing IContactCallbackPhysicsWorld.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)
{
// CRITICAL: Check validity first!
if (!contactEvent.shapeA.isValid || !contactEvent.shapeB.isValid)
return; // One or both shapes were destroyed in a previous callback this frame
var bodyA = contactEvent.shapeA.body;
var bodyB = contactEvent.shapeB.body;
// Now safe to process collision...
}
public void OnContactEnd2D(PhysicsEvents.ContactEndEvent contactEvent)
{
// CRITICAL: Check validity first!
if (!contactEvent.shapeA.isValid || !contactEvent.shapeB.isValid)
return;
// Now safe to process...
}
CRITICAL - Dynamic Body Requirement:
Performance:
Thread Safety:
Use IContactCallback when:
Use Manual Queries (OverlapGeometry, etc.) when:
Reference examples from the PhysicsExamples2D repository:
Contact manifolds contain information about collision contact points between two shapes:
For character controllers that need collision detection:
PhysicsCore2D is deterministic when:
Monitor collisions by implementing PhysicsCallbacks.IContactCallback interface:
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.
PhysicsShape.callbackTarget only on shapes that need collision notificationsWhen users need information about:
Batching techniques for queries, projectiles/shooters, and swarm/flocking behaviors (boids)
Component authoring patterns and best practices for Unity PhysicsCore2D
High-level patterns and gameplay strategies for destructible objects in Unity PhysicsCore2D — slicing mechanics, fragmenting on impact, sprite-based destructible systems, debris cleanup, performance budgeting. Use for "how should I design destructible X?" questions. For the PhysicsDestructor type API and worked code examples see unity-physicscore2d-destructor; for raw member signatures see unity-physicscore2d-destructor-api.
Factory patterns for creating complex physics objects (ragdolls, soft bodies, vehicles, gears, mechanical systems)
Collision filtering, layer-based interactions, custom collision filters, and trigger areas
Environmental forces (wind), surface forces, and directional force application