tools-unity-physics
SKILL.md
Unity Physics
Overview
Unity physics handles collision detection, rigidbody simulation, and character movement. This skill covers patterns to prevent common issues like floor penetration.
When to Use
- Character movement and controllers
- Projectile physics
- Collision detection
- Trigger zones
- Ragdoll systems
Character Controller Safety
Preventing Floor Penetration
public class SafeCharacterController : MonoBehaviour
{
[SerializeField] private float _moveSpeed = 5f;
[SerializeField] private float _gravity = -20f;
[SerializeField] private float _groundCheckDistance = 0.1f;
[SerializeField] private LayerMask _groundLayer;
private CharacterController _controller;
private Vector3 _velocity;
private bool _isGrounded;
private void Awake()
{
_controller = GetComponent<CharacterController>();
}
private void Update()
{
// Ground check with spherecast for reliability
_isGrounded = Physics.SphereCast(
transform.position + Vector3.up * (_controller.radius + 0.1f),
_controller.radius * 0.9f,
Vector3.down,
out _,
_groundCheckDistance + 0.1f,
_groundLayer
);
if (_isGrounded && _velocity.y < 0)
{
// Small negative to keep grounded
_velocity.y = -2f;
}
// Movement
Vector3 move = GetMoveInput() * _moveSpeed;
_controller.Move(move * Time.deltaTime);
// Gravity
_velocity.y += _gravity * Time.deltaTime;
_controller.Move(_velocity * Time.deltaTime);
// CRITICAL: Clamp to ground if penetrating
ClampToGround();
}
private void ClampToGround()
{
if (Physics.Raycast(transform.position + Vector3.up, Vector3.down, out var hit, 2f, _groundLayer))
{
if (transform.position.y < hit.point.y)
{
transform.position = new Vector3(
transform.position.x,
hit.point.y,
transform.position.z
);
}
}
}
private Vector3 GetMoveInput()
{
float h = Input.GetAxis("Horizontal");
float v = Input.GetAxis("Vertical");
return new Vector3(h, 0, v).normalized;
}
}
Physics-Based Character Movement
public class PhysicsCharacterController : MonoBehaviour
{
[SerializeField] private float _moveSpeed = 5f;
[SerializeField] private float _jumpForce = 8f;
[SerializeField] private float _groundDrag = 6f;
[SerializeField] private float _airDrag = 1f;
[SerializeField] private LayerMask _groundLayer;
private Rigidbody _rb;
private CapsuleCollider _collider;
private bool _isGrounded;
private void Awake()
{
_rb = GetComponent<Rigidbody>();
_collider = GetComponent<CapsuleCollider>();
// Prevent tunneling
_rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
_rb.interpolation = RigidbodyInterpolation.Interpolate;
}
private void FixedUpdate()
{
CheckGrounded();
ApplyDrag();
Move();
}
private void CheckGrounded()
{
_isGrounded = Physics.SphereCast(
transform.position + Vector3.up * (_collider.radius + 0.1f),
_collider.radius * 0.95f,
Vector3.down,
out _,
0.2f,
_groundLayer
);
}
private void ApplyDrag()
{
_rb.linearDamping = _isGrounded ? _groundDrag : _airDrag;
}
private void Move()
{
Vector3 moveDir = GetMoveInput();
if (_isGrounded)
{
_rb.AddForce(moveDir * _moveSpeed * 10f, ForceMode.Force);
}
else
{
_rb.AddForce(moveDir * _moveSpeed * 5f, ForceMode.Force);
}
// Clamp horizontal velocity
Vector3 flatVel = new Vector3(_rb.linearVelocity.x, 0, _rb.linearVelocity.z);
if (flatVel.magnitude > _moveSpeed)
{
Vector3 limitedVel = flatVel.normalized * _moveSpeed;
_rb.linearVelocity = new Vector3(limitedVel.x, _rb.linearVelocity.y, limitedVel.z);
}
}
}
Collision Detection
Safe Collision Handling
public class CollisionHandler : MonoBehaviour
{
public event Action<Collision> OnCollisionEnterSafe;
public event Action<Collider> OnTriggerEnterSafe;
private void OnCollisionEnter(Collision collision)
{
if (collision.gameObject == null) return;
if (collision.contactCount == 0) return;
OnCollisionEnterSafe?.Invoke(collision);
}
private void OnTriggerEnter(Collider other)
{
if (other == null) return;
if (!other.gameObject.activeInHierarchy) return;
OnTriggerEnterSafe?.Invoke(other);
}
}
Layer-Based Collision
public class CollisionLayerManager
{
// Define layers
public const int PlayerLayer = 8;
public const int EnemyLayer = 9;
public const int ProjectileLayer = 10;
public const int EnvironmentLayer = 11;
public static void Initialize()
{
// Player ignores other players
Physics.IgnoreLayerCollision(PlayerLayer, PlayerLayer, true);
// Projectiles pass through each other
Physics.IgnoreLayerCollision(ProjectileLayer, ProjectileLayer, true);
// Enemy projectiles don't hit enemies
// (Configure in project settings)
}
public static LayerMask GetPlayerHitLayers()
{
return (1 << EnemyLayer) | (1 << EnvironmentLayer);
}
public static LayerMask GetEnemyHitLayers()
{
return (1 << PlayerLayer) | (1 << EnvironmentLayer);
}
}
Preventing Tunneling
Continuous Collision Detection
public class ProjectilePhysics : MonoBehaviour
{
[SerializeField] private float _speed = 50f;
[SerializeField] private LayerMask _hitLayers;
private Rigidbody _rb;
private Vector3 _lastPosition;
private void Awake()
{
_rb = GetComponent<Rigidbody>();
// Use continuous for fast objects
_rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
_rb.interpolation = RigidbodyInterpolation.Interpolate;
}
private void Start()
{
_rb.linearVelocity = transform.forward * _speed;
_lastPosition = transform.position;
}
private void FixedUpdate()
{
// Manual raycast for very fast projectiles
var direction = transform.position - _lastPosition;
var distance = direction.magnitude;
if (distance > 0.01f)
{
if (Physics.Raycast(_lastPosition, direction.normalized, out var hit, distance, _hitLayers))
{
OnHit(hit);
}
}
_lastPosition = transform.position;
}
private void OnHit(RaycastHit hit)
{
// Handle impact
Destroy(gameObject);
}
}
SubStepping for Fast Movement
public class HighSpeedMovement : MonoBehaviour
{
[SerializeField] private float _speed = 100f;
[SerializeField] private LayerMask _collisionLayers;
private const float MaxStepDistance = 0.5f;
public void MoveTowards(Vector3 target)
{
Vector3 direction = (target - transform.position).normalized;
float totalDistance = Vector3.Distance(transform.position, target);
float distanceMoved = 0f;
while (distanceMoved < totalDistance)
{
float stepDistance = Mathf.Min(MaxStepDistance, totalDistance - distanceMoved);
Vector3 nextPosition = transform.position + direction * stepDistance;
// Check for collision in this step
if (Physics.Raycast(transform.position, direction, out var hit, stepDistance, _collisionLayers))
{
transform.position = hit.point - direction * 0.01f;
OnCollision(hit);
return;
}
transform.position = nextPosition;
distanceMoved += stepDistance;
}
}
private void OnCollision(RaycastHit hit)
{
// Handle collision
}
}
Physics Queries
SphereCast for Ground Detection
public class GroundChecker
{
private readonly float _radius;
private readonly float _distance;
private readonly LayerMask _groundMask;
public GroundChecker(float radius, float distance, LayerMask groundMask)
{
_radius = radius;
_distance = distance;
_groundMask = groundMask;
}
public bool IsGrounded(Vector3 position, out RaycastHit hitInfo)
{
Vector3 origin = position + Vector3.up * (_radius + 0.05f);
return Physics.SphereCast(
origin,
_radius * 0.9f,
Vector3.down,
out hitInfo,
_distance + 0.1f,
_groundMask,
QueryTriggerInteraction.Ignore
);
}
public float GetGroundAngle(RaycastHit hit)
{
return Vector3.Angle(hit.normal, Vector3.up);
}
}
OverlapSphere with Filtering
public class AreaDetector
{
private readonly Collider[] _results = new Collider[32];
public int DetectInRadius<T>(
Vector3 center,
float radius,
LayerMask layers,
List<T> output) where T : Component
{
output.Clear();
int count = Physics.OverlapSphereNonAlloc(
center,
radius,
_results,
layers,
QueryTriggerInteraction.Ignore
);
for (int i = 0; i < count; i++)
{
if (_results[i].TryGetComponent<T>(out var component))
{
output.Add(component);
}
}
return output.Count;
}
}
Physics Performance
Optimized Physics Settings
public class PhysicsOptimizer
{
public static void ApplyMobileSettings()
{
// Reduce solver iterations for mobile
Physics.defaultSolverIterations = 4;
Physics.defaultSolverVelocityIterations = 1;
// Increase sleep threshold
Physics.sleepThreshold = 0.05f;
// Reduce bounce threshold
Physics.bounceThreshold = 2f;
// Lower fixed timestep for mobile (45 Hz instead of 50)
Time.fixedDeltaTime = 1f / 45f;
}
public static void ApplyDesktopSettings()
{
Physics.defaultSolverIterations = 6;
Physics.defaultSolverVelocityIterations = 1;
Physics.sleepThreshold = 0.01f;
Time.fixedDeltaTime = 1f / 60f;
}
}
Physics Job System
using Unity.Jobs;
using Unity.Collections;
using Unity.Burst;
[BurstCompile]
public struct RaycastJob : IJobParallelFor
{
[ReadOnly] public NativeArray<RaycastCommand> Commands;
public NativeArray<RaycastHit> Results;
public void Execute(int index)
{
// Results are written by Unity's job system
}
}
public class BatchRaycaster
{
public void PerformBatchRaycasts(
Vector3[] origins,
Vector3[] directions,
float[] distances,
LayerMask layerMask,
RaycastHit[] results)
{
int count = origins.Length;
var commands = new NativeArray<RaycastCommand>(count, Allocator.TempJob);
var hits = new NativeArray<RaycastHit>(count, Allocator.TempJob);
for (int i = 0; i < count; i++)
{
commands[i] = new RaycastCommand(
origins[i],
directions[i],
QueryParameters.Default,
distances[i]
);
}
var handle = RaycastCommand.ScheduleBatch(commands, hits, 1);
handle.Complete();
hits.CopyTo(results);
commands.Dispose();
hits.Dispose();
}
}
Best Practices
- Use ContinuousDynamic for fast-moving objects
- SphereCast for ground - More reliable than raycast
- Clamp positions after physics update
- Use NonAlloc methods for queries (OverlapSphereNonAlloc)
- Configure layer matrix - Avoid unnecessary collisions
- Sleep inactive rigidbodies - Set appropriate thresholds
- Profile FixedUpdate - Physics runs at fixed rate
- Use compound colliders sparingly
- Batch physics queries with job system
- Test at low framerates - Tunneling is worse
Troubleshooting
| Issue | Solution |
|---|---|
| Objects fall through floor | Use ContinuousDynamic, clamp position |
| Jittery movement | Enable Interpolation |
| Collision missed | Increase solver iterations |
| Physics too slow | Reduce collider complexity |
| Character stuck | Check collider radius vs step offset |
| Floating point drift | Reset position periodically |
Weekly Installs
1
Repository
tjboudreaux/cc-…-gamedevGitHub Stars
1
First Seen
3 days ago
Security Audits
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1