ワンクリックで
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 職業分類に基づく
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
| 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: