Get Your Personalized Game Dev Plan Tailored tips, tools, and next steps - just for you.

This page may contain affiliate links.

Beyond Tutorials: Crafting a Smooth Third-Person Character Controller in Godot

Posted by Gemma Ellison
./
July 11, 2025

Let’s face it: crafting a truly good third-person character controller can feel like wrestling a greased octopus. Everyone starts with the tutorials, but getting beyond that initial “hello world” and into a system that feels good is where the real challenge lies. This isn’t just about making a character move; it’s about creating an experience.

This article won’t rehash the basics you can find anywhere. Instead, we’ll dive into the crucial details that separate a clunky character from a smooth, responsive one, using Godot’s CharacterBody3D, input actions, and some surprisingly potent simple math. We’ll build a solid foundation for movement, tackle camera control that actually avoids obstacles, and provide actionable code snippets to get you started right.

The Core: CharacterBody3D and Input Actions

Forget KinematicBody3D. CharacterBody3D is the way to go. It’s built for character movement and collision, and using it properly is half the battle.

Input actions are your best friend. Don’t hardcode input events! Define actions like “move_forward,” “move_backward,” “jump,” etc., in your project settings. This gives you flexibility to remap controls later without changing your code, crucial for accessibility and player preference.

extends CharacterBody3D

@export var speed = 5.0
@export var jump_velocity = 4.5

func _physics_process(delta):
    # Get input direction
    var direction = Vector3.ZERO
    direction.x = Input.get_action_strength("move_right") - Input.get_action_strength("move_left")
    direction.z = Input.get_action_strength("move_backward") - Input.get_action_strength("move_forward")
    direction = direction.normalized()

    # Movement
    if direction:
        velocity.x = direction.x * speed
        velocity.z = direction.z * speed
    else:
        velocity.x = move_toward(velocity.x, 0, speed)
        velocity.z = move_toward(velocity.z, 0, speed)

    # Jumping
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        velocity.y = jump_velocity

    # Gravity
    if not is_on_floor():
        velocity.y -= gravity * delta

    move_and_slide()

This snippet gets you started. Notice the move_toward function. It’s vital for smooth stopping. Without it, your character will stop instantly, feeling jarring and unnatural.

Camera Control: Beyond SpringArm3D

SpringArm3D is a good starting point, but it often fails spectacularly when dealing with complex environments. The camera clips through walls, gets stuck, and generally makes a mess.

The solution? Raycasts. Cast a ray from the camera’s desired position towards the player. If the ray collides with something, move the camera to that collision point.

@export var target: Node3D
@export var camera: Camera3D
@export var distance = 5.0

func _physics_process(delta):
    var target_position = target.global_position
    var desired_camera_position = target_position - camera.global_transform.basis.z * distance

    var space_state = get_world_3d().direct_space_state
    var query = PhysicsRayQueryParameters3D.new()
    query.from = target_position
    query.to = desired_camera_position
    query.exclude = [target] # Important: Don't collide with the player!

    var result = space_state.intersect_ray(query)

    if result:
        camera.global_position = result.position
    else:
        camera.global_position = desired_camera_position

This code is a game-changer. It keeps the camera smoothly following the player while intelligently avoiding obstacles. The exclude = [target] line is crucial; otherwise, the camera will collide with the player itself.

Common Pitfall: Forgetting to set the exclude parameter. This is a classic mistake that leads to frustrating camera behavior.

Collision Handling: The Sticky Feet Problem

One of the biggest annoyances in third-person controllers is “sticky feet,” where the character gets stuck on tiny bumps or edges. This is often due to floating-point precision issues and the limitations of move_and_slide.

There are two main strategies to combat this:

  1. Larger Collision Shape: Slightly increase the size of your character’s collision shape. This helps “smooth out” those tiny bumps.

  2. Ground Snapping: Use a raycast downwards to detect the ground. If the ground is close enough, slightly adjust the character’s position to snap them to the ground. This prevents them from hovering slightly above the surface, which can lead to getting stuck.

@export var ground_snap_distance = 0.2

func _physics_process(delta):
    # ... (Movement Code) ...

    # Ground Snapping
    if not is_on_floor():
        var space_state = get_world_3d().direct_space_state
        var query = PhysicsRayQueryParameters3D.new()
        query.from = global_position
        query.to = global_position - Vector3.UP * ground_snap_distance
        query.exclude = [self]

        var result = space_state.intersect_ray(query)

        if result:
            global_position.y = result.position.y
            velocity.y = 0  # Reset vertical velocity when snapping to the ground

Ground snapping might seem like a small detail, but it makes a world of difference in the overall feel of the character controller.

Actionable Insights: Level Design Matters

The best character controller in the world will still feel clunky in a poorly designed level. Avoid overly complex geometry with lots of small, sharp edges. Use smooth, flowing curves and gentle slopes instead.

Pro Tip: Play your game constantly during level design. Pay close attention to how the character feels to control in each area. Make adjustments as needed. Level design and character controller development should be an iterative process.

Example Project: Get Started Now

To make it even easier to get started, I’ve created a simple Godot project with a basic third-person character controller that implements the techniques described above. You can download it [here](insert link to your github or similar). It’s a barebones project, but it provides a solid foundation to build upon.

This isn’t just about copying and pasting code. It’s about understanding the underlying principles and applying them to create a character controller that feels great. Go beyond the tutorials, experiment with different parameters, and build something truly unique. Good luck!