Collision Jitter from Thin Objects: Why Your Floor is Bouncing
*Understanding how thin collision shapes cause physics jitter and best practices for stable floor placement*
Godot Version: 4.7
Project Type: 3D Space Ship Game with Physics Objects
Introduction
Have you ever noticed objects jittering or bouncing when they should be sitting still on a floor? Or a character that seems to vibrate when standing on a platform? This frustrating issue is often caused by collision shapes that are too thin.
In this post, we’ll explore why thin collision objects cause jitter, how to identify the problem, and best practices for creating stable floors and platforms in Godot 4.
The Problem: Jitter and Bouncing
Symptoms
You might experience:
Real-World Example
In our space ship game, we initially created floors with very thin collision shapes (0.05-0.1 units thick). When crates or the player stood on these floors, they would:
Why Thin Collision Shapes Cause Jitter
1. Physics Solver Precision
Godot’s physics engine (GodotPhysics) uses iterative solvers to resolve collisions. When a collision shape is very thin:
Example:
Thin floor: 0.05 units thick
Object penetration: 0.01 units
Penetration ratio: 20% of shape thickness ?
Thick floor: 0.2 units thick
Object penetration: 0.01 units
Penetration ratio: 5% of shape thickness ?
2. Contact Point Generation
Thin collision shapes generate fewer or less stable contact points:
3. Restitution and Damping
When objects collide with thin shapes:
4. Sub-stepping Issues
Godot’s physics sub-stepping (used for fast-moving objects) can cause issues with thin shapes:
The Physics Behind It
Penetration Resolution
When two objects collide, the physics engine:
1. Detects penetration (overlap)
2. Calculates separation vector
3. Applies impulses to separate objects
With thin shapes, step 2 becomes problematic:
# Simplified penetration resolution
var penetration_depth = collision_shape.thickness - object_radius
var separation_force = penetration_depth * solver_strength
# If thickness is 0.05 and penetration is 0.01:
# separation_force = 0.04 * strength (small margin for error)
# If thickness is 0.2 and penetration is 0.01:
# separation_force = 0.19 * strength (large margin, stable)
Contact Manifold Stability
A contact manifold is the set of contact points between two objects. Thin shapes create:
Best Practices for Floor Placement
1. Minimum Thickness Guidelines
Rule of Thumb: Floor collision shapes should be at least 0.1-0.2 units thick (in world units).
For different object sizes:
Our Codebase Examples:
“`18:19:main.tscn
[sub_resource type=”BoxShape3D” id=”Shape_Ground”]
size = Vector3(50, 0.1, 50)
This is at the minimum threshold. For better stability, we could use 0.15-0.2:
gdscript
Better: More stable
size = Vector3(50, 0.15, 50) # 50% thicker = more stable
Best: Very stable
size = Vector3(50, 0.2, 50) # 100% thicker = most stable
### 2. Positioning the Collision Shape
**Key Principle**: Position the collision shape so objects contact the **top face**, not the edges.
**Bad Positioning:**
gdscript
Collision shape centered on visual mesh
Objects contact edges, causing instability
CollisionShape3D.position = Vector3.ZERO # ?
**Good Positioning:**
gdscript
Collision shape positioned so top face aligns with visual floor
Objects contact the stable top face
CollisionShape3D.position = Vector3(0, -0.05, 0) # ?
**Our Implementation:**
22:24:ships/interiors/ship_deck.tscn
[node name=”FloorCollision” type=”CollisionShape3D” parent=”.”]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0)
shape = SubResource(“BoxShape3D_Floor”)
The collision shape is positioned at `y = -0.1`, which means:
- Visual floor is at `y = 0`
- Collision shape top face is at `y = 0` (since shape is 0.2 thick, centered at -0.1)
- Objects contact the stable top face ?
### 3. Shape Type Selection
**For Floors, Use BoxShape3D:**
gdscript
? Good: BoxShape3D for floors
var floor_shape = BoxShape3D.new()
floor_shape.size = Vector3(width, 0.2, depth) # Thick enough
? Avoid: PlaneMesh for collision (no thickness)
PlaneMesh is visual only, not a collision shape
?? Use with caution: ConcavePolygonShape3D
Only for StaticBody3D, can be less stable than BoxShape3D
### 4. StaticBody3D vs RigidBody3D for Floors
**Use StaticBody3D for floors:**
gdscript
? Good: StaticBody3D (immovable, optimized)
StaticBody3D
??? CollisionShape3D
? Avoid: RigidBody3D for floors (unless floor moves)
RigidBody3D adds unnecessary physics calculations
**Our Implementation:**
43:50:main.tscn
[node name=”Ground” type=”StaticBody3D” parent=”.”]
[node name=”MeshInstance3D” type=”MeshInstance3D” parent=”Ground”]
mesh = SubResource(“Mesh_Ground”)
[node name=”CollisionShape3D” type=”CollisionShape3D” parent=”Ground”]
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
shape = SubResource(“Shape_Ground”)
### 5. Collision Layers and Masks
Ensure proper collision layer setup:
gdscript
Floor (StaticBody3D)
collision_layer = 1 # World layer
Objects that should collide with floor
collision_mask = 1 # Include world layer in their mask
## Complete Floor Setup Example
Here's a complete, stable floor setup:
gdscript
Floor Scene Structure
StaticBody3D (name: “Floor”)
??? MeshInstance3D (visual floor)
? ??? PlaneMesh (size: Vector2(10, 10))
? ??? position: Vector3(0, 0, 0)
?
??? CollisionShape3D (collision)
??? BoxShape3D (size: Vector3(10, 0.2, 10))
??? position: Vector3(0, -0.1, 0) # Top face at y=0
**Key Points:**
1. **Thickness**: 0.2 units (stable)
2. **Position**: Collision shape positioned so top face aligns with visual floor
3. **Type**: StaticBody3D (optimized for immovable objects)
4. **Shape**: BoxShape3D (stable, predictable)
## Debugging Jitter Issues
### 1. Visualize Collision Shapes
Enable collision shape visualization in the editor:
- **View ? Debug Settings ? Visible Collision Shapes**
- Or use `CollisionShape3D.debug_color` in code
### 2. Check Shape Thickness
gdscript
func _ready() -> void:
var collision = $CollisionShape3D
if collision.shape is BoxShape3D:
var box = collision.shape as BoxShape3D
print(“Floor thickness: “, box.size.y)
if box.size.y < 0.1:
print(“?? WARNING: Floor too thin! Minimum recommended: 0.1”)
### 3. Monitor Physics Contacts
Add debug output to see contact stability:
gdscript
func _on_body_entered(body: Node3D) -> void:
if body is RigidBody3D:
print(“Object on floor: “, body.name)
print(” Position: “, body.global_position)
print(” Velocity: “, body.linear_velocity)
# Check for jitter (rapid velocity changes)
if body.linear_velocity.length() > 0.1 and body.sleeping == false:
print(” ?? Possible jitter detected”)
### 4. Test with Different Thicknesses
Create a test scene with multiple floors of different thicknesses:
gdscript
Test different thicknesses
var thicknesses = [0.05, 0.1, 0.15, 0.2, 0.3]
for thickness in thicknesses:
var floor = create_test_floor(thickness)
# Place object on floor and observe stability
## Common Mistakes and Fixes
### Mistake 1: Using PlaneMesh for Collision
gdscript
? Wrong: PlaneMesh has no thickness
var plane = PlaneMesh.new()
plane.size = Vector2(10, 10)
This creates a visual mesh, not a collision shape
**Fix:**
gdscript
? Correct: Use BoxShape3D
var box = BoxShape3D.new()
box.size = Vector3(10, 0.2, 10) # Has thickness
### Mistake 2: Centering Thin Shape on Visual
gdscript
? Wrong: Thin shape centered, objects contact edges
CollisionShape3D.position = Vector3.ZERO
shape.size = Vector3(10, 0.05, 10) # Too thin, centered
**Fix:**
gdscript
? Correct: Thick shape, positioned for top-face contact
CollisionShape3D.position = Vector3(0, -0.1, 0)
shape.size = Vector3(10, 0.2, 10) # Thick enough
### Mistake 3: Using RigidBody3D for Static Floors
gdscript
? Wrong: Unnecessary physics calculations
RigidBody3D (mass: 0, freeze: true) # Still processes physics
**Fix:**
gdscript
? Correct: Use StaticBody3D
StaticBody3D # Optimized, no physics processing
### Mistake 4: Non-Uniform Scaling
gdscript
?? Caution: Non-uniform scale can cause issues
floor.scale = Vector3(1, 0.5, 1) # Makes shape thinner!
**Fix:**
gdscript
? Better: Scale the shape resource, not the node
box.size = Vector3(10 * scale.x, 0.2, 10 * scale.z)
Keep thickness constant
## Performance Considerations
### Thickness vs Performance
- **Thicker shapes**: Slightly more collision calculations, but more stable
- **Thinner shapes**: Fewer calculations, but unstable and may require more solver iterations
**Recommendation**: Use the minimum stable thickness (0.1-0.2) for best balance.
### StaticBody3D Optimization
StaticBody3D is optimized for immovable objects:
- No velocity calculations
- No force integration
- Optimized collision detection
- Can be baked into collision space for even better performance
## Real-World Example: Fixing Our Floor
### Before (Problematic)
6:7:ships/interiors/simple_floor.tscn
[sub_resource type=”BoxShape3D” id=”BoxShape3D_1″]
size = Vector3(4, 0.1, 2)
**Issues:**
- Thickness of 0.1 is at minimum threshold
- Objects might jitter on fast-moving ships
- Contact points less stable
### After (Improved)
gdscript
Improved version
[sub_resource type=”BoxShape3D” id=”BoxShape3D_1″]
size = Vector3(4, 0.2, 2) # Doubled thickness
Position adjusted to maintain top-face contact
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.1, 0)
**Benefits:**
- More stable contact points
- Less jitter on moving ships
- Better penetration resolution
- Smoother physics interactions
## Advanced: Custom Floor Solutions
### For Moving Floors (Ships)
When floors are on moving RigidBody3D (like ships), consider:
1. **Thicker collision shapes** (0.2-0.3) for extra stability
2. **Inertial dampeners** (see [blog_inertial_dampeners.md](blog_inertial_dampeners.md)) to prevent sliding
3. **Continuous collision detection** on objects:
gdscript
# On RigidBody3D objects that interact with floors
continuous_cd = RigidBody3D.CCD_MODE_CAST_RAY
“`
For Sloped Floors
For ramps or slopes:
For Multi-Level Floors
When stacking floors:
Testing Your Floor
Stability Test
# Place this script on a test RigidBody3D
extends RigidBody3D
var position_samples: Array[Vector3] = []
var jitter_threshold: float = 0.01 # 1cm movement = jitter
func _physics_process(_delta: float) -> void:
position_samples.append(global_position)
if position_samples.size() > 60: # 1 second at 60fps
position_samples.pop_front()
# Check for jitter (rapid position changes)
var variance = calculate_variance(position_samples)
if variance > jitter_threshold:
print("?? JITTER DETECTED: Variance = ", variance)
print(" Consider increasing floor thickness")
func calculate_variance(samples: Array[Vector3]) -> float:
if samples.size() < 2:
return 0.0
var mean = Vector3.ZERO
for pos in samples:
mean += pos
mean /= samples.size()
var variance = 0.0
for pos in samples:
variance += (pos - mean).length_squared()
variance /= samples.size()
return sqrt(variance)
Summary: Quick Reference
? Do’s
? Don’ts
?? Thickness Guidelines
| Object Size | Minimum Thickness | Recommended Thickness |
| Small (player, crate) | 0.1 | 0.15-0.2 |
| Medium (vehicle) | 0.15 | 0.2-0.3 |
| Large (ship deck) | 0.2 | 0.3-0.5 |
Conclusion
Thin collision shapes are a common source of physics jitter in Godot 4. By understanding why they cause problems and following best practices, you can create stable, smooth physics interactions.
Key Takeaways:
1. Thickness matters: Use at least 0.1-0.2 units for floors
2. Position matters: Align collision top face with visual floor
3. Type matters: Use StaticBody3D + BoxShape3D for floors
4. Test matters: Verify stability with actual game objects
The best floors are the ones players never think about-they just work smoothly, without jitter or bouncing. Follow these guidelines, and your floors will be rock-solid.
Want to see it in action? Check out our floor implementations:
main.tscn – Ground floor setupships/interiors/ship_deck.tscn – Ship deck floorships/interiors/simple_floor.tscn – Simple floor templateRelated Systems:
Leave a Reply