【Godot】Stabilizing FPS in Godot Engine: Frame Rate Management and Optimization Techniques

Created: 2025-12-10Last updated: 2025-12-16

Learn the fundamentals of frame rate management to stabilize FPS, along with optimization techniques like physics interpolation and Object Pooling.

Introduction: Why Is Your Game Stuttering?

One wall many developers face when developing with Godot Engine is frame rate (FPS) instability. The "stuttering" and "jitter" that occur especially in action games or scenes with many moving objects severely damages player immersion and fundamentally undermines game quality.

"V-Sync is enabled but it's not smooth," "I don't understand the difference between _process and _physics_process," "I'm unsure whether to implement Object Pooling for optimization"... These concerns can be resolved by deeply understanding how Godot's rendering and physics systems work.

This article goes beyond simple feature introductions. It unravels the root causes of FPS instability and thoroughly explains specific solutions with practical code and tabular comparisons. After reading this article, you'll gain the following knowledge and be able to confidently tackle performance optimization.

  • Problem solving: Learn concrete implementation methods for "physics interpolation" to eliminate FPS instability (jitter).
  • Correct understanding: Completely understand Godot's physics and rendering mechanisms, and the proper use of delta.
  • Accurate judgment: Be able to determine when and how to apply optimization techniques like Object Pooling based on profiling.
  • Practical knowledge: Master advanced frame rate management techniques like the differences between V-Sync modes and how to use Engine.max_fps.

Root Cause of FPS Instability: Physics Ticks vs Rendering Frames

The key to understanding performance issues in Godot is recognizing the difference between two independent cycles: Physics Ticks and Rendering Frames. This asynchronicity is the direct cause of jitter.

ConceptPhysics TickRendering Frame
RolePhysics calculations, collision detection (_physics_process)Screen rendering, input handling (_process)
Execution frequencyFixed (default 60Hz)Variable (depends on hardware performance, V-Sync settings)
PurposeGuarantee reproducibility and consistency of physics simulationProvide players with the smoothest possible visuals
deltaFixed value (1 / physics tick rate)Variable value (elapsed time since previous frame)

Why does jitter occur?

Consider the case where physics runs at 60Hz and the monitor at 144Hz. The physics engine updates object positions every 1/60th of a second, but the screen tries to render every 1/144th of a second. As a result, objects stay in the same position across multiple render frames, then suddenly jump to a new position at the next physics tick—appearing to the player as "staircase-like movement." This is what jitter is.


Solution 1: Frame Rate Control with V-Sync and max_fps

The most basic countermeasure is controlling the rendering frame rate. Godot provides two main methods: V-Sync (vertical synchronization) and Engine.max_fps.

Using V-Sync (Vertical Synchronization)

V-Sync is a technology that synchronizes the rendering frame rate with the monitor's refresh rate. This prevents "tearing" caused by the screen being updated mid-refresh.

Configure it in Project Settings under Display > Window > Vsync > Vsync Mode.

V-Sync ModeOverviewBenefitsDrawbacks
DisabledDisables V-Sync.No FPS cap, input latency minimized.Severe tearing occurs.
EnabledAlways enables V-Sync.Completely prevents tearing.When FPS drops below refresh rate, rendering waits for next sync timing, increasing input latency.
AdaptiveSyncs only when FPS exceeds refresh rate.Prevents tearing while avoiding input latency during FPS drops.Requires compatible hardware.
MailboxAlways keeps the latest rendered frame and displays at sync timing.Minimizes both tearing and input latency.Increases VRAM usage and may cause slight additional latency.

Note: External Setting Overrides If V-Sync settings in Godot don't take effect, graphics card settings like NVIDIA Control Panel or AMD Radeon Software may be overriding Godot's settings. Select "V-Sync: Application Controlled" in driver settings.

Manual Limiting with Engine.max_fps

Effective when you want to lock to a specific FPS without relying on V-Sync. Used for purposes like intentionally creating retro-style low FPS or reducing battery consumption on mobile devices.

# GameController.gd

func _ready():
    # Set game's maximum FPS to 60
    # Only effective when V-Sync is Disabled
    Engine.max_fps = 60

max_fps becomes effective game-wide once set in a function like _ready(). However, note that when V-Sync is Enabled, max_fps is ignored.


Solution 2: Eliminating Jitter with Physics Interpolation

Frame rate control alone doesn't solve the fundamental problem of physics tick and rendering frame asynchronicity. This is where physics interpolation comes in. It's a technique that smoothly interpolates object positions between fixed physics ticks during rendering—the most effective solution for jitter.

Enable in Project Settings (Recommended)

Since Godot 4.3, physics interpolation has been natively supported for both 2D and 3D. In most cases, simply enabling the following setting solves the problem.

  1. Open Project > Project Settings
  2. Set Physics > Common > Physics Interpolation to On

This alone automatically interpolates the movement of most nodes affected by physics—like CharacterBody2D/3D and RigidBody2D/3D—making them very smooth.

Manual Interpolation Implementation (Camera Following, etc.)

When making non-physics objects (like cameras or UI elements) follow physics nodes, manual interpolation is needed. Using Engine.get_physics_interpolation_fraction(), you can get where the current rendering frame is between physics ticks (0.0 to 1.0).

Here's practical code for a camera smoothly following a player character with physics interpolation enabled.

# SmoothCamera2D.gd
# Attach to Camera2D node

extends Camera2D

# Target node to follow (like player) - set from inspector
@export var target: Node2D

# Smoothness of following. Lower values are smoother
@export var smoothing: float = 0.1

var _previous_target_position: Vector2

func _ready():
    if target:
        # Initialize position to target
        global_position = target.global_position
        _previous_target_position = target.global_position
    else:
        push_warning("SmoothCamera2D: Target node is not set.")

func _process(delta):
    if not target:
        return

    # When physics interpolation is enabled, target's render position is always interpolated
    # However, non-physics nodes like cameras need their own interpolation
    var interpolation_fraction = Engine.get_physics_interpolation_fraction()

    # Interpolate between previous and current target position to calculate
    # the "true" current position the camera should aim for
    var interpolated_target_position = _previous_target_position.lerp(target.global_position, interpolation_fraction)

    # Move camera's own position smoothly toward the calculated target position
    global_position = global_position.lerp(interpolated_target_position, smoothing)

func _physics_process(delta):
    if not target:
        return

    # Save target position at physics tick time for next frame's interpolation
    _previous_target_position = target.global_position

This code performs two stages of lerp (linear interpolation):

  1. Interpolate target movement between physics ticks
  2. Further smooth the camera's own movement

This allows the camera to follow cinematically smoothly, even when player movement is sudden.


Common Mistakes and Best Practices

Here's a summary of traps developers commonly fall into when tackling optimization, and best practices to avoid them.

TopicCommon Mistake (Anti-pattern)Best Practice
Where to write processingWriting all movement and logic in _process.Write physical movement in _physics_process, rendering and input processing in _process. Understand delta's role correctly.
Optimization orderIntroducing Object Pooling because something "seems heavy.""Don't guess, measure." Use Godot's profiler to identify bottlenecks, then select appropriate optimization techniques.
Object PoolingApplying to infrequently spawned objects (bosses, UI), complicating code.Apply only to short-lived, high-frequency objects (bullets, effects) confirmed as bottlenecks by the profiler.
Deleting physics objectsDirectly calling queue_free() or remove_child() in _physics_process, causing errors.Use call_deferred("queue_free") to safely schedule deletion after the current physics step completes.
Physics tick rateMaking physics tick rate variable or extremely high to solve jitter.Keep physics tick rate fixed (default 60Hz) and leave jitter countermeasures to physics interpolation.

Advanced Technique: Implementing Object Pooling

If profiling reveals that node creation/destruction (instantiate() / queue_free()) is a performance bottleneck, Object Pooling is an effective solution.

Here's a generic Object Pooling manager implementation that can be used as an AutoLoad (singleton).

# ObjectPoolManager.gd (Register as AutoLoad)
extends Node

var _pool: Dictionary = {}

# Pre-populate the pool
func pre_populate_pool(scene: PackedScene, count: int):
    if not _pool.has(scene):
        _pool[scene] = []

    for i in range(count):
        var instance = scene.instantiate()
        instance.name = "%s_pooled_%d" % [scene.resource_path.get_file().get_basename(), i]
        # Initially inactive in pool
        instance.set_process(false)
        instance.set_physics_process(false)
        instance.visible = false
        add_child(instance)
        _pool[scene].append(instance)

# Get object from pool
func get_object(scene: PackedScene) -> Node:
    if _pool.has(scene) and not _pool[scene].is_empty():
        var instance = _pool[scene].pop_front()
        # Activate for reuse
        instance.set_process(true)
        instance.set_physics_process(true)
        instance.visible = true
        return instance
    else:
        # Create new if pool is empty (fallback)
        return scene.instantiate()

# Return object to pool
func return_object(instance: Node):
    if not is_instance_valid(instance):
        return

    var scene_key = null
    for key in _pool.keys():
        if key.can_instantiate() and instance.scene_file_path == key.resource_path:
            scene_key = key
            break

    if scene_key:
        # Deactivate and return to pool
        instance.set_process(false)
        instance.set_physics_process(false)
        instance.visible = false
        # Move off-screen to avoid physics collisions
        if instance is Node2D:
            instance.global_position = Vector2(1e8, 1e8)
        # Return parent to ObjectPoolManager
        if instance.get_parent() != self:
            instance.get_parent().remove_child(instance)
            add_child(instance)
        _pool[scene_key].append(instance)
    else:
        # Destroy objects not managed by pool normally
        instance.queue_free()

# --- Usage Example ---
# Player.gd

const BULLET_SCENE = preload("res://bullet.tscn")

func _ready():
    # Pre-pool 50 bullets at game start
    ObjectPoolManager.pre_populate_pool(BULLET_SCENE, 50)

func _fire():
    # Get bullet from pool
    var bullet = ObjectPoolManager.get_object(BULLET_SCENE)

    # Place retrieved bullet at appropriate place in game world (e.g., Y-Sort node)
    var game_world = get_tree().get_root().get_node("GameWorld")
    bullet.reparent(game_world)

    # Initialize bullet settings
    bullet.global_position = $Muzzle.global_position
    bullet.rotation = global_rotation
    bullet.fire()

# Bullet.gd

func on_hit_or_timeout():
    # Return to pool instead of destroying
    ObjectPoolManager.return_object(self)

Important Point: reparent() Pooled objects must use reparent() after retrieval to move to the appropriate parent node in the game world (like a Y-Sort enabled node). This ensures correct draw order and parent-child relationships.


Summary and Next Steps

Stable FPS is the foundation of a high-quality game experience. By systematically applying the knowledge explained here, your Godot project will dramatically improve.

ProblemRoot CauseSolution
Jitter (stuttering)Physics and rendering asynchronicityEnabling physics interpolation is best.
Tearing (screen tearing)Rendering and monitor update asynchronicityEnable V-Sync (Adaptive or Mailbox recommended).
Node creation overheadFrequent creation/destruction of short-lived objectsAfter confirming with profiler, if bottleneck, introduce Object Pooling.
Overall performance degradationUnknown heavy processingIdentify bottleneck with Godot Profiler, improve algorithms or shaders.

Next Steps

  1. Current state analysis: Enable debugger Monitors in your project and check FPS fluctuations.
  2. Jitter countermeasures: Enable physics interpolation and experience how movement becomes smoother. Implement manual interpolation code for following non-physics nodes like cameras.
  3. Bottleneck identification: Run the Profiler on scenes that feel slow and identify the functions consuming the most time.
  4. Targeted optimization: Based on identified causes, apply solutions learned in this article (algorithm improvements, Object Pooling, etc.), then profile again to measure effectiveness.

Armed with this knowledge, create smooth and comfortable game experiences that captivate your players.