
Unity Coroutine Memory Leak Bug: How to Identify and Fix Coroutine Reference Issues
February 9, 2026
Unity Transform Parenting Bug: World Position/LossyScale Inconsistencies
February 10, 2026One of the most perplexing bugs in Unity’s 2D physics system is the Physics Material 2D property application bug. This issue manifests when physics materials assigned to 2D colliders fail to apply their friction, bounciness, or other physical properties correctly, resulting in objects that slide indefinitely, bounce inconsistently, or behave identically regardless of their material assignments. Unlike crashes or error messages, this bug subtly undermines the physical realism of your 2D game, making surfaces feel “wrong” and movement systems behave unpredictably.
Understanding the Bug: Root Causes
The Physics Material 2D property bug stems from multiple interacting systems within Unity’s 2D physics engine (Box2D integration). The issues aren’t always consistent, making diagnosis particularly challenging:
1. Collider Combination Rule Conflicts
Unity uses combination rules (Average, Minimum, Maximum, Multiply) to determine how two colliding surfaces interact. These rules can produce unexpected results:
// Physics Material 2D settings PhysicsMaterial2D iceMaterial = new PhysicsMaterial2D(); iceMaterial.friction = 0.1f; iceMaterial.frictionCombine = PhysicsMaterial2D.Combine.Multiply; PhysicsMaterial2D rubberMaterial = new PhysicsMaterial2D(); rubberMaterial.friction = 0.8f; rubberMaterial.frictionCombine = PhysicsMaterial2D.Combine.Average; // When ice collides with rubber: // Multiply: 0.1 * 0.8 = 0.08 (ultra-low friction) // Average: (0.1 + 0.8) / 2 = 0.45 (moderate friction) // The bug can occur when combination rules don't apply as expected
2. Rigidbody2D vs. Collider2D Priority Conflicts
Physics properties can be set at multiple levels, creating ambiguity:
// Multiple places where friction can be set: Rigidbody2D rb = GetComponent(); rb.sharedMaterial = materialA; // Rigidbody material Collider2D collider = GetComponent(); collider.sharedMaterial = materialB; // Collider material // Bug: Unity might use one, the other, or a combination // The priority isn't always clear or consistent
3. Static vs. Dynamic Collider Differences
Static colliders (without Rigidbody2D) behave differently than dynamic ones:
- Static colliders: Material properties may be ignored or scaled
- Kinematic Rigidbody2D: Different physics interactions
- Dynamic Rigidbody2D: Full material support (in theory)
4. Contact Filter Overrides
Contact filters and layer collision matrices can override material properties:
// Contact filter that might ignore material properties
ContactFilter2D filter = new ContactFilter2D();
filter.useLayerMask = true;
filter.layerMask = LayerMask.GetMask("Ground");
// When using this filter, material properties might be bypassed
Collider2D[] results = new Collider2D[10];
collider.OverlapCollider(filter, results);
5. Scale-Dependent Property Application
Physics material properties can be affected by transform scale:
// Object with non-uniform scale transform.localScale = new Vector3(2, 1, 1); // Material properties might apply differently // on scaled axes, particularly friction
Visual Symptoms and Impact
Symptom 1: Infinite Sliding on “High-Friction” Surfaces
// Material configured for high friction PhysicsMaterial2D highFrictionMat = new PhysicsMaterial2D(); highFrictionMat.friction = 1.0f; // Maximum friction highFrictionMat.frictionCombine = PhysicsMaterial2D.Combine.Maximum; // Applied to ground collider groundCollider.sharedMaterial = highFrictionMat; // Bug: Character/object slides indefinitely as if on ice // Console shows material assigned but friction not applied
Symptom 2: Inconsistent Bounce Behavior
// Bouncy material setup PhysicsMaterial2D bouncyMat = new PhysicsMaterial2D(); bouncyMat.bounciness = 0.9f; // Very bouncy bouncyMat.bounceCombine = PhysicsMaterial2D.Combine.Maximum; // Applied to ball ballCollider.sharedMaterial = bouncyMat; // Bug: Ball bounces inconsistently // - Sometimes bounces correctly // - Sometimes doesn't bounce at all // - Bounce height varies unpredictably
Symptom 3: Identical Behavior Across Different Materials
// Create distinct materials
PhysicsMaterial2D slippery = CreateMaterial("Slippery", 0.1f, 0.1f);
PhysicsMaterial2D sticky = CreateMaterial("Sticky", 0.9f, 0.1f);
PhysicsMaterial2D bouncy = CreateMaterial("Bouncy", 0.3f, 0.8f);
// Assign to different surfaces
iceSurface.sharedMaterial = slippery;
glueSurface.sharedMaterial = sticky;
trampoline.sharedMaterial = bouncy;
// Bug: All surfaces behave identically
// Objects slide/slow/bounce the same on all surfaces
Symptom 4: Properties Only Work in Specific Scenarios
// Material works in some cases but not others
void TestMaterial() {
// Works: When object is instantiated at runtime
GameObject runtimeObj = Instantiate(prefab);
runtimeObj.GetComponent().sharedMaterial = mat;
// Doesn't work: When object is in scene at start
// Works: When colliding with certain object types
// Doesn't work: When colliding with others
}
Common Reproduction Scenarios
Scenario 1: Platformer with Variable Surface Friction
public class SurfaceManager : MonoBehaviour {
public PhysicsMaterial2D iceMaterial;
public PhysicsMaterial2D grassMaterial;
public PhysicsMaterial2D mudMaterial;
void AssignSurfaceMaterials() {
// Assign based on surface type
foreach (Collider2D collider in GetComponentsInChildren()) {
switch (collider.tag) {
case "Ice":
collider.sharedMaterial = iceMaterial; // 0.1 friction
break;
case "Grass":
collider.sharedMaterial = grassMaterial; // 0.5 friction
break;
case "Mud":
collider.sharedMaterial = mudMaterial; // 0.8 friction
break;
}
}
}
// Bug: All surfaces feel the same to the player
// Character slides equally on ice and mud
}
Scenario 2: Pinball or Bouncing Game Physics
public class PinballBumper : MonoBehaviour {
public float bounceForce = 10f;
public PhysicsMaterial2D bumperMaterial;
void Start() {
GetComponent().sharedMaterial = bumperMaterial;
bumperMaterial.bounciness = 0.9f;
bumperMaterial.bounceCombine = PhysicsMaterial2D.Combine.Maximum;
}
void OnCollisionEnter2D(Collision2D collision) {
// Expected: Ball bounces with material bounciness
// Bug: Ball bounces weakly regardless of material setting
// Must use manual force application instead
Rigidbody2D rb = collision.rigidbody;
if (rb != null) {
Vector2 direction = (rb.position - (Vector2)transform.position).normalized;
rb.AddForce(direction * bounceForce, ForceMode2D.Impulse);
}
}
}
Scenario 3: Physics-Based Puzzle Game
public class PuzzleSurface : MonoBehaviour {
[SerializeField] private PhysicsMaterial2D lowFrictionMat;
[SerializeField] private PhysicsMaterial2D highFrictionMat;
public void SetSlippery(bool slippery) {
GetComponent().sharedMaterial =
slippery ? lowFrictionMat : highFrictionMat;
// Debug: Material appears assigned
Debug.Log($"Material set: {GetComponent().sharedMaterial.name}");
Debug.Log($"Friction: {GetComponent().sharedMaterial.friction}");
// Bug: Puzzle pieces slide the same regardless
// Must implement custom friction simulation
}
}
Workarounds and Solutions
1. The Material Application Order Fix (Most Reliable)
Apply materials in a specific order and verify application:
public class PhysicsMaterialApplicator : MonoBehaviour {
public PhysicsMaterial2D material;
void Start() {
ApplyMaterialWithVerification();
}
void ApplyMaterialWithVerification() {
// Get all colliders on this GameObject and children
Collider2D[] colliders = GetComponentsInChildren();
foreach (Collider2D collider in colliders) {
// Method 1: Direct assignment (often works)
collider.sharedMaterial = material;
// Method 2: Recreate material instance (workaround for bug)
PhysicsMaterial2D instanceMaterial = new PhysicsMaterial2D();
instanceMaterial.name = material.name + "_Instance";
instanceMaterial.friction = material.friction;
instanceMaterial.bounciness = material.bounciness;
instanceMaterial.frictionCombine = material.frictionCombine;
instanceMaterial.bounceCombine = material.bounceCombine;
collider.sharedMaterial = instanceMaterial;
// Method 3: Force physics update
StartCoroutine(ForcePhysicsUpdate(collider));
}
// Also apply to Rigidbody2D if present
Rigidbody2D rb = GetComponent();
if (rb != null) {
rb.sharedMaterial = material;
}
}
IEnumerator ForcePhysicsUpdate(Collider2D collider) {
// Disable and re-enable collider to force physics refresh
collider.enabled = false;
yield return null; // Wait one frame
collider.enabled = true;
// Alternative: Change layer temporarily
int originalLayer = collider.gameObject.layer;
collider.gameObject.layer = LayerMask.NameToLayer("Ignore Raycast");
yield return null;
collider.gameObject.layer = originalLayer;
}
#if UNITY_EDITOR
void OnValidate() {
// Apply material in editor for immediate feedback
if (!Application.isPlaying && material != null) {
ApplyMaterialWithVerification();
}
}
#endif
}
2. Custom Friction Simulation System
Implement your own friction system when materials fail:
public class CustomFrictionSystem : MonoBehaviour {
private Dictionary<Collider2D, SurfaceProperties> surfaceProperties =
new Dictionary<Collider2D, SurfaceProperties>();
void Start() {
// Initialize surface properties for all colliders
Collider2D[] allColliders = FindObjectsOfType();
foreach (Collider2D collider in allColliders) {
SurfaceProperties props = new SurfaceProperties();
// Extract from material if available
if (collider.sharedMaterial != null) {
props.friction = collider.sharedMaterial.friction;
props.bounciness = collider.sharedMaterial.bounciness;
}
// Store custom tags or identifiers
if (collider.CompareTag("Ice")) props.friction = 0.1f;
if (collider.CompareTag("Mud")) props.friction = 0.8f;
surfaceProperties[collider] = props;
}
}
void FixedUpdate() {
// Apply custom friction to all rigidbodies
Rigidbody2D[] rigidbodies = FindObjectsOfType();
foreach (Rigidbody2D rb in rigidbodies) {
ApplyCustomFriction(rb);
}
}
void ApplyCustomFriction(Rigidbody2D rb) {
// Check what surfaces we're contacting
ContactPoint2D[] contacts = new ContactPoint2D[10];
int contactCount = rb.GetContacts(contacts);
float totalFriction = 0f;
int frictionSources = 0;
for (int i = 0; i < contactCount; i++) { Collider2D otherCollider = contacts[i].collider; if (surfaceProperties.ContainsKey(otherCollider)) { totalFriction += surfaceProperties[otherCollider].friction; frictionSources++; } } if (frictionSources > 0) {
// Calculate average friction
float averageFriction = totalFriction / frictionSources;
// Apply friction force (opposing velocity)
Vector2 frictionForce = -rb.velocity * averageFriction * rb.mass;
rb.AddForce(frictionForce);
// Debug visualization
Debug.DrawRay(rb.position, frictionForce * 0.1f, Color.red);
}
}
// Custom bounce implementation
public void ApplyCustomBounce(Rigidbody2D rb, Collision2D collision) {
if (surfaceProperties.ContainsKey(collision.collider)) {
float bounciness = surfaceProperties[collision.collider].bounciness;
// Calculate reflection velocity
Vector2 incomingVelocity = rb.velocity;
Vector2 normal = collision.contacts[0].normal;
Vector2 reflectedVelocity = Vector2.Reflect(incomingVelocity, normal);
// Apply bounciness
rb.velocity = reflectedVelocity * bounciness;
}
}
class SurfaceProperties {
public float friction = 0.4f;
public float bounciness = 0.2f;
}
}
3. Rigidbody2D Material Priority Enforcement
Ensure Rigidbody2D materials take priority over collider materials:
public class MaterialPriorityEnforcer : MonoBehaviour {
private Rigidbody2D rb;
private Collider2D[] colliders;
void Start() {
rb = GetComponent();
colliders = GetComponents();
EnforceMaterialPriority();
}
void EnforceMaterialPriority() {
if (rb == null) return;
// Method 1: Set Rigidbody2D material and nullify collider materials
PhysicsMaterial2D primaryMaterial = GetPrimaryMaterial();
if (primaryMaterial != null) {
rb.sharedMaterial = primaryMaterial;
// Remove materials from colliders to avoid conflicts
foreach (Collider2D collider in colliders) {
collider.sharedMaterial = null;
}
}
// Method 2: Sync materials between Rigidbody2D and all colliders
SyncMaterials();
}
void SyncMaterials() {
// Create a master material on the Rigidbody2D
PhysicsMaterial2D masterMaterial = new PhysicsMaterial2D();
masterMaterial.name = $"{gameObject.name}_MasterMaterial";
// Determine properties from the first collider with a material
foreach (Collider2D collider in colliders) {
if (collider.sharedMaterial != null) {
masterMaterial.friction = collider.sharedMaterial.friction;
masterMaterial.bounciness = collider.sharedMaterial.bounciness;
masterMaterial.frictionCombine = collider.sharedMaterial.frictionCombine;
masterMaterial.bounceCombine = collider.sharedMaterial.bounceCombine;
break;
}
}
// Apply to Rigidbody2D
rb.sharedMaterial = masterMaterial;
// Apply to all colliders
foreach (Collider2D collider in colliders) {
collider.sharedMaterial = masterMaterial;
}
}
PhysicsMaterial2D GetPrimaryMaterial() {
// Priority order:
// 1. Existing Rigidbody2D material
// 2. First collider with a material
// 3. Create default material
if (rb.sharedMaterial != null) {
return rb.sharedMaterial;
}
foreach (Collider2D collider in colliders) {
if (collider.sharedMaterial != null) {
return collider.sharedMaterial;
}
}
// Create default material
PhysicsMaterial2D defaultMat = new PhysicsMaterial2D();
defaultMat.name = "DefaultMaterial";
defaultMat.friction = 0.4f;
defaultMat.bounciness = 0.2f;
return defaultMat;
}
}
4. Material Property Validation System
Continuously monitor and correct material properties:
public class PhysicsMaterialValidator : MonoBehaviour {
private Dictionary<Collider2D, PhysicsMaterial2D> expectedMaterials =
new Dictionary<Collider2D, PhysicsMaterial2D>();
void Start() {
// Record expected materials for all colliders
Collider2D[] allColliders = FindObjectsOfType();
foreach (Collider2D collider in allColliders) {
if (collider.sharedMaterial != null) {
expectedMaterials[collider] = collider.sharedMaterial;
}
}
// Start validation coroutine
StartCoroutine(ValidateMaterialsContinuously());
}
IEnumerator ValidateMaterialsContinuously() {
while (true) {
yield return new WaitForSeconds(1f); // Check every second
foreach (var kvp in expectedMaterials) {
Collider2D collider = kvp.Key;
PhysicsMaterial2D expected = kvp.Value;
if (collider == null) continue;
// Check if material is still assigned
if (collider.sharedMaterial == null) {
Debug.LogWarning($"Material lost on {collider.gameObject.name}, reapplying");
collider.sharedMaterial = expected;
continue;
}
// Check if properties match
if (!MaterialsMatch(collider.sharedMaterial, expected)) {
Debug.LogWarning($"Material properties changed on {collider.gameObject.name}, correcting");
// Reapply correct properties
collider.sharedMaterial.friction = expected.friction;
collider.sharedMaterial.bounciness = expected.bounciness;
collider.sharedMaterial.frictionCombine = expected.frictionCombine;
collider.sharedMaterial.bounceCombine = expected.bounceCombine;
}
}
}
}
bool MaterialsMatch(PhysicsMaterial2D a, PhysicsMaterial2D b) {
if (a == null || b == null) return false;
return Mathf.Abs(a.friction - b.friction) < 0.001f &&
Mathf.Abs(a.bounciness - b.bounciness) < 0.001f &&
a.frictionCombine == b.frictionCombine &&
a.bounceCombine == b.bounceCombine;
}
#if UNITY_EDITOR
void OnDrawGizmosSelected() {
// Visualize material assignments
foreach (var kvp in expectedMaterials) {
if (kvp.Key != null) {
// Color code by friction
float friction = kvp.Value.friction;
Color color = Color.Lerp(Color.blue, Color.red, friction);
// Draw wireframe with material color
Gizmos.color = color;
Gizmos.DrawWireCube(kvp.Key.bounds.center, kvp.Key.bounds.size);
// Label with friction value
UnityEditor.Handles.Label(
kvp.Key.bounds.center,
$"Friction: {friction:F2}\nBounce: {kvp.Value.bounciness:F2}"
);
}
}
}
#endif
}
5. Combination Rule Workaround
Manually handle material combination logic:
public class ManualCombinationHandler : MonoBehaviour {
private Dictionary<Collider2D, SurfaceType> surfaceTypes =
new Dictionary<Collider2D, SurfaceType>();
void Start() {
InitializeSurfaceTypes();
}
void InitializeSurfaceTypes() {
Collider2D[] allColliders = FindObjectsOfType();
foreach (Collider2D collider in allColliders) {
// Determine surface type from tags, layers, or materials
SurfaceType type = DetermineSurfaceType(collider);
surfaceTypes[collider] = type;
}
}
void OnCollisionEnter2D(Collision2D collision) {
// Get both colliders in collision
Collider2D colliderA = collision.collider;
Collider2D colliderB = collision.otherCollider;
if (!surfaceTypes.ContainsKey(colliderA) ||
!surfaceTypes.ContainsKey(colliderB)) {
return;
}
SurfaceType typeA = surfaceTypes[colliderA];
SurfaceType typeB = surfaceTypes[colliderB];
// Apply custom combination logic
float combinedFriction = CombineFriction(typeA, typeB);
float combinedBounciness = CombineBounciness(typeA, typeB);
// Apply to collision
ApplyCombinedProperties(collision, combinedFriction, combinedBounciness);
}
float CombineFriction(SurfaceType a, SurfaceType b) {
// Custom combination logic
switch (a.frictionCombine) {
case CombineMode.Average:
return (a.baseFriction + b.baseFriction) / 2f;
case CombineMode.Minimum:
return Mathf.Min(a.baseFriction, b.baseFriction);
case CombineMode.Maximum:
return Mathf.Max(a.baseFriction, b.baseFriction);
case CombineMode.Multiply:
return a.baseFriction * b.baseFriction;
default:
return a.baseFriction;
}
}
void ApplyCombinedProperties(Collision2D collision, float friction, float bounciness) {
Rigidbody2D rb = collision.rigidbody;
if (rb == null) return;
// Apply friction (simplified)
Vector2 frictionForce = -rb.velocity.normalized * friction * rb.mass;
rb.AddForce(frictionForce, ForceMode2D.Force);
// Apply bounce
if (bounciness > 0) {
Vector2 normal = collision.contacts[0].normal;
Vector2 incomingVelocity = rb.velocity;
Vector2 reflectedVelocity = Vector2.Reflect(incomingVelocity, normal) * bounciness;
// Only apply bounce if it increases velocity away from surface
float normalVelocity = Vector2.Dot(incomingVelocity, normal);
if (normalVelocity < 0) { // Moving toward surface
rb.velocity = new Vector2(rb.velocity.x, reflectedVelocity.y);
}
}
}
SurfaceType DetermineSurfaceType(Collider2D collider) {
SurfaceType type = new SurfaceType();
// Extract from material
if (collider.sharedMaterial != null) {
type.baseFriction = collider.sharedMaterial.friction;
type.baseBounciness = collider.sharedMaterial.bounciness;
type.frictionCombine = ConvertCombine(collider.sharedMaterial.frictionCombine);
type.bounceCombine = ConvertCombine(collider.sharedMaterial.bounceCombine);
}
// Override based on tag or layer
if (collider.CompareTag("Ice")) {
type.baseFriction = 0.1f;
type.frictionCombine = CombineMode.Minimum;
} else if (collider.CompareTag("Rubber")) {
type.baseBounciness = 0.8f;
type.bounceCombine = CombineMode.Maximum;
}
return type;
}
CombineMode ConvertCombine(PhysicsMaterial2D.Combine physicsCombine) {
switch (physicsCombine) {
case PhysicsMaterial2D.Combine.Average: return CombineMode.Average;
case PhysicsMaterial2D.Combine.Minimum: return CombineMode.Minimum;
case PhysicsMaterial2D.Combine.Maximum: return CombineMode.Maximum;
case PhysicsMaterial2D.Combine.Multiply: return CombineMode.Multiply;
default: return CombineMode.Average;
}
}
class SurfaceType {
public float baseFriction = 0.4f;
public float baseBounciness = 0.2f;
public CombineMode frictionCombine = CombineMode.Average;
public CombineMode bounceCombine = CombineMode.Average;
}
enum CombineMode { Average, Minimum, Maximum, Multiply }
}
Prevention Strategies
1. Optimal Physics Material Setup
public class OptimalMaterialSetup : MonoBehaviour {
[MenuItem("Tools/Setup Optimal Physics Materials")]
static void SetupMaterials() {
// Create and configure optimal materials
CreateMaterial("Ice", 0.05f, 0.1f,
PhysicsMaterial2D.Combine.Minimum, PhysicsMaterial2D.Combine.Average);
CreateMaterial("Rubber", 0.8f, 0.9f,
PhysicsMaterial2D.Combine.Maximum, PhysicsMaterial2D.Combine.Maximum);
CreateMaterial("Wood", 0.5f, 0.3f,
PhysicsMaterial2D.Combine.Average, PhysicsMaterial2D.Combine.Average);
CreateMaterial("Metal", 0.4f, 0.1f,
PhysicsMaterial2D.Combine.Average, PhysicsMaterial2D.Combine.Minimum);
}
static PhysicsMaterial2D CreateMaterial(string name, float friction, float bounce,
PhysicsMaterial2D.Combine frictionCombine, PhysicsMaterial2D.Combine bounceCombine) {
PhysicsMaterial2D mat = new PhysicsMaterial2D();
mat.name = name;
mat.friction = friction;
mat.bounciness = bounce;
mat.frictionCombine = frictionCombine;
mat.bounceCombine = bounceCombine;
// Save to Assets folder
string path = $"Assets/PhysicsMaterials/{name}.physicsMaterial2D";
UnityEditor.AssetDatabase.CreateAsset(mat, path);
return mat;
}
}
// Runtime validation of material settings
public class MaterialSettingsValidator : MonoBehaviour {
void Start() {
ValidateAllMaterials();
}
void ValidateAllMaterials() {
PhysicsMaterial2D[] allMaterials = Resources.FindObjectsOfTypeAll();
foreach (PhysicsMaterial2D mat in allMaterials) {
// Check for invalid values
if (mat.friction < 0 || mat.friction > 1) {
Debug.LogError($"Material {mat.name} has invalid friction: {mat.friction}");
}
if (mat.bounciness < 0 || mat.bounciness > 1) {
Debug.LogError($"Material {mat.name} has invalid bounciness: {mat.bounciness}");
}
// Check combination rules
if (mat.frictionCombine == PhysicsMaterial2D.Combine.Multiply &&
mat.friction > 0.5f) {
Debug.LogWarning($"Material {mat.name}: Multiply combine with high friction may cause issues");
}
}
}
}
2. Material Assignment Best Practices
- Assign at design time: Materials assigned in editor are more reliable
- Use instances: Create material instances at runtime rather than reusing assets
- Clear conflicts: Set collider.sharedMaterial = null before assigning new material
- Verify assignment: Check that sharedMaterial property actually changed
3. Editor Validation Tool
#if UNITY_EDITOR
public class PhysicsMaterialInspector : EditorWindow {
[MenuItem("Tools/Physics Material Inspector")]
static void ShowWindow() {
GetWindow("Physics Material Inspector");
}
void OnGUI() {
if (GUILayout.Button("Scan Scene for Material Issues")) {
ScanScene();
}
if (GUILayout.Button("Fix All Material Assignments")) {
FixMaterials();
}
}
void ScanScene() {
Collider2D[] allColliders = FindObjectsOfType();
int issuesFound = 0;
foreach (Collider2D collider in allColliders) {
// Check 1: Missing material on dynamic objects
Rigidbody2D rb = collider.GetComponent();
if (rb != null && rb.bodyType == RigidbodyType2D.Dynamic) {
if (collider.sharedMaterial == null && rb.sharedMaterial == null) {
Debug.LogWarning($"Dynamic object {collider.gameObject.name} has no physics material", collider.gameObject);
issuesFound++;
}
}
// Check 2: Material on static collider (often ignored)
if (rb == null && collider.sharedMaterial != null) {
Debug.LogWarning($"Static collider {collider.gameObject.name} has material but no Rigidbody2D", collider.gameObject);
issuesFound++;
}
// Check 3: Conflicting Rigidbody2D and Collider materials
if (rb != null && collider.sharedMaterial != null && rb.sharedMaterial != null) {
if (collider.sharedMaterial != rb.sharedMaterial) {
Debug.LogWarning($"Material conflict on {collider.gameObject.name}: Rigidbody and Collider have different materials", collider.gameObject);
issuesFound++;
}
}
}
EditorUtility.DisplayDialog("Scan Complete",
$"Found {issuesFound} material issues", "OK");
}
void FixMaterials() {
Collider2D[] allColliders = FindObjectsOfType();
foreach (Collider2D collider in allColliders) {
Rigidbody2D rb = collider.GetComponent();
// Fix: Use Rigidbody2D material for everything
if (rb != null && rb.sharedMaterial != null) {
collider.sharedMaterial = rb.sharedMaterial;
EditorUtility.SetDirty(collider);
}
}
EditorUtility.DisplayDialog("Fix Complete",
"Applied Rigidbody2D materials to all colliders", "OK");
}
}
#endif
Debugging Techniques
Real-time Material Property Monitor
public class MaterialDebugger : MonoBehaviour {
void OnGUI() {
GUILayout.BeginArea(new Rect(10, 10, 300, 400));
GUILayout.Label("Physics Material Debugger", EditorStyles.boldLabel);
// Show all colliders with materials
Collider2D[] colliders = FindObjectsOfType();
foreach (Collider2D collider in colliders) {
if (collider.sharedMaterial != null) {
GUILayout.BeginHorizontal();
GUILayout.Label(collider.gameObject.name, GUILayout.Width(150));
GUILayout.Label($"F: {collider.sharedMaterial.friction:F2}");
GUILayout.Label($"B: {collider.sharedMaterial.bounciness:F2}");
GUILayout.EndHorizontal();
}
}
GUILayout.EndArea();
}
void OnDrawGizmos() {
// Visualize friction as color intensity
Collider2D[] colliders = FindObjectsOfType();
foreach (Collider2D collider in colliders) {
if (collider.sharedMaterial != null) {
float friction = collider.sharedMaterial.friction;
float bounce = collider.sharedMaterial.bounciness;
// Color: Red for high friction, Blue for high bounce
Color color = new Color(friction, 0, bounce, 0.3f);
Gizmos.color = color;
Gizmos.DrawCube(collider.bounds.center, collider.bounds.size);
// Draw arrow showing expected slide direction
if (friction < 0.3f) {
Gizmos.color = Color.cyan;
Vector3 start = collider.bounds.center;
Vector3 end = start + Vector3.right * 2f * (1 - friction);
Gizmos.DrawLine(start, end);
Gizmos.DrawSphere(end, 0.1f);
}
}
}
}
}
When to Use Which Solution
| Scenario | Recommended Solution | Performance Impact |
|---|---|---|
| Simple materials that occasionally fail | Material Application Order Fix | Low |
| Critical friction/bounce gameplay | Custom Friction Simulation | Medium |
| Complex objects with multiple colliders | Rigidbody2D Priority Enforcement | Low |
| Persistent material corruption | Material Property Validation | Low |
| Advanced combination rule needs | Combination Rule Workaround | Medium |
Performance Considerations
- Custom friction systems add FixedUpdate overhead proportional to collider count
- Material validation has minimal impact if done infrequently (every 1-5 seconds)
- Multiple material instances increase memory but improve reliability
- Continuous property checking in FixedUpdate can become expensive with many objects
Recommendation: Start with the Material Application Order Fix. It resolves most cases with minimal overhead. Only implement custom systems if you need precise control or have persistent issues.
Testing Methodology
[TestFixture]
public class PhysicsMaterialTests {
[UnityTest]
public IEnumerator Test_Material_Friction_Applies_Correctly() {
// Create test slope
GameObject slope = CreateSlope(30f);
// Create two identical boxes with different materials
GameObject boxLowFriction = CreateBox("LowFrictionBox");
PhysicsMaterial2D lowFrictionMat = CreateMaterial("LowFriction", 0.1f, 0f);
boxLowFriction.GetComponent().sharedMaterial = lowFrictionMat;
GameObject boxHighFriction = CreateBox("HighFrictionBox");
PhysicsMaterial2D highFrictionMat = CreateMaterial("HighFriction", 0.8f, 0f);
boxHighFriction.GetComponent().sharedMaterial = highFrictionMat;
// Position at top of slope
Vector3 slopeTop = slope.transform.position + Vector3.up * 2f;
boxLowFriction.transform.position = slopeTop;
boxHighFriction.transform.position = slopeTop + Vector3.right * 2f;
// Record start positions
Vector3 startLow = boxLowFriction.transform.position;
Vector3 startHigh = boxHighFriction.transform.position;
// Run physics for 3 seconds
yield return new WaitForSeconds(3f);
// Measure distance traveled
float distanceLow = Vector3.Distance(startLow, boxLowFriction.transform.position);
float distanceHigh = Vector3.Distance(startHigh, boxHighFriction.transform.position);
// Low friction box should travel significantly farther
Assert.Greater(distanceLow, distanceHigh * 1.5f,
$"Low friction box traveled {distanceLow}, high friction {distanceHigh}");
// Cleanup
GameObject.Destroy(slope);
GameObject.Destroy(boxLowFriction);
GameObject.Destroy(boxHighFriction);
}
}
Conclusion
The Physics Material 2D property bug represents a fundamental disconnect between Unity’s material assignment system and Box2D’s internal physics processing. The bug manifests not as a single failure point but as inconsistent, context-dependent property application that undermines the physical consistency essential for polished gameplay.
The most effective approach combines:
- Defensive material assignment with verification and reapplication
- Clear priority rules between Rigidbody2D and Collider2D materials
- Validation systems to detect and correct material corruption
- Custom physics simulation for critical gameplay elements
For most projects, implementing the Material Application Order Fix—with careful attention to assignment order, material instances, and combination rules—will resolve 80% of material-related issues. For games where physics behavior is critical (racing games, pinball, physics puzzles), implementing a custom friction/bounce system provides reliable, predictable results.
Remember that physics materials are more than visual metadata—they’re fundamental to how players experience your game’s world. Surfaces that feel “right” (ice is slippery, rubber bounces, carpet slows movement) contribute immensely to immersion and polish. By addressing the Physics Material 2D bug comprehensively, you ensure that your game’s physical interactions are as intentional and reliable as its visual design.










