Camera2DGDScriptGodot 45 min read

Smooth Camera Follow in Godot 4

The default Camera2D position smoothing feels floaty and unresponsive. Here's a proper camera system with lookahead, deadzone, and weighted smoothing.

The problem with basic lerp

Most Godot tutorials tell you to lerp the camera position toward the player. It works, but it creates a “floaty” feeling — the camera constantly lags behind, especially during fast direction changes. Players feel disconnected from their character.

bad_camera.gd — don't do this
# The "basic lerp" approach everyone uses
# Problem: always feels sluggish and unresponsive
func _process(delta):
    position = position.lerp(target.position, 5.0 * delta)

The fix: weighted smoothing + deadzone

The key insight is using two different smoothing weights: a fast weight when the player moves outside the deadzone, and a slower weight when they're inside it. This gives you snappy response during action and stability during idle.

smooth_camera.gd
extends Camera2D

@export var target: Node2D

# Deadzone — camera won't move for small player movements
@export var deadzone := Vector2(40.0, 30.0)

# Smoothing weights (higher = snappier)
@export var fast_weight := 12.0
@export var slow_weight := 3.0

# Lookahead — camera leads the player's movement
@export var lookahead_strength := 60.0
@export var lookahead_smoothing := 5.0

var _lookahead_target := Vector2.ZERO
var _current_lookahead := Vector2.ZERO

func _physics_process(delta: float) -> void:
    if not target:
        return

    var target_pos := target.global_position
    var diff := target_pos - global_position

    # Calculate lookahead based on player velocity
    if target is CharacterBody2D:
        var vel := (target as CharacterBody2D).velocity
        _lookahead_target = vel.normalized() * lookahead_strength

    _current_lookahead = _current_lookahead.lerp(
        _lookahead_target, lookahead_smoothing * delta
    )

    # Pick weight: fast outside deadzone, slow inside
    var weight := slow_weight
    if abs(diff.x) > deadzone.x or abs(diff.y) > deadzone.y:
        weight = fast_weight

    # Apply smoothed movement + lookahead offset
    var goal := target_pos + _current_lookahead
    global_position = global_position.lerp(goal, weight * delta)

Why this works

Deadzone

Prevents jitter when the player makes tiny movements. Camera stays locked until the player moves far enough.

Dual weights

Fast weight (12.0) snaps to the player during action. Slow weight (3.0) drifts gently during idle.

Lookahead

Camera leads the player's movement direction so they can see what's ahead. Essential for platformers.

Physics process

Using _physics_process instead of _process syncs with CharacterBody2D movement. No jitter.

Bonus: screen shake in 5 lines

Add this to the same script for juicy screen shake on hit:

smooth_camera.gd — add screen shake
var _shake_intensity := 0.0
var _shake_decay := 8.0

func shake(intensity: float = 8.0) -> void:
    _shake_intensity = intensity

# Add to end of _physics_process:
    _shake_intensity = lerp(_shake_intensity, 0.0, _shake_decay * delta)
    offset = Vector2(
        randf_range(-1, 1) * _shake_intensity,
        randf_range(-1, 1) * _shake_intensity
    )

Call camera.shake(12.0) from your damage handler and you're done. Buttery smooth, responsive, and juicy.

Want the full video walkthrough?

The complete “Smooth Camera Follow” course covers camera limits, multi-target tracking, cinematic pans, and advanced shake patterns — with a ready-to-use project file. 15 minutes, $3.99.

Get the Full Course →