【Godot】Godot EngineのFPSを安定させるフレームレート管理と最適化手法

作成: 2025-12-10最終更新: 2025-12-16

FPSを安定させるフレームレート管理の基本と、物理補間やObject Poolingなどの最適化手法を解説。

導入:なぜあなたのゲームはカクつくのか?

Godot Engineで開発を進める中で、多くの開発者が直面する壁、それがフレームレート(FPS)の不安定さです。特にアクションゲームや多数のオブジェクトが動くシーンで発生する「カクつき」や「ジッター」は、プレイヤーの没入感を著しく損ない、ゲームの品質を根本から揺るがします。

「V-Syncを有効にしたのに滑らかにならない」「_process_physics_processの使い分けが分からない」「最適化のためにObject Poolingを導入すべきか迷っている」…こうした悩みは、Godotのレンダリングと物理演算の仕組みを深く理解することで解決できます。

この記事は、単なる機能紹介に留まりません。FPSが不安定になる根本原因を解き明かし、具体的な解決策を実践的なコードと表形式の比較で徹底的に解説します。この記事を読めば、あなたは以下の知識を習得し、自信を持ってパフォーマンス最適化に取り組めるようになります。

  • 課題解決: FPSの不安定さ(ジッター)を解消する「物理補間」の具体的な実装方法を学べる。
  • 正しい理解: Godotの物理演算と描画の仕組み、そしてdeltaの正しい使い方を完全に理解できる。
  • 的確な判断: プロファイリングに基づき、Object Poolingなどの最適化手法をいつ、どのように適用すべきか判断できるようになる。
  • 実践的知識: V-Syncの各モードの違いやEngine.max_fpsの活用法など、一歩進んだフレームレート管理術を習得できる。

FPSが不安定になる根本原因:物理ティック vs レンダリングフレーム

Godotにおけるパフォーマンス問題を理解する鍵は、物理ティック(Physics Tick)レンダリングフレーム(Rendering Frame) という2つの独立したサイクルの違いを認識することです。この非同期性が、ジッターの直接的な原因となります。

概念物理ティック (Physics Tick)レンダリングフレーム (Rendering Frame)
役割物理演算、衝突判定 (_physics_process)画面描画、入力処理 (_process)
実行頻度固定 (デフォルト60Hz)可変 (ハードウェア性能、V-Sync設定に依存)
目的物理シミュレーションの再現性と一貫性を保証可能な限り滑らかな映像をプレイヤーに提供
delta固定値 (1 / 物理ティックレート)可変値 (前のフレームからの経過時間)

なぜジッターが起きるのか?

例えば、物理が60Hz、モニターが144Hzの場合を考えます。物理エンジンは1/60秒ごとにオブジェクトの位置を更新しますが、画面は1/144秒ごとに描画しようとします。結果として、複数回の描画フレームでオブジェクトが同じ位置に留まり、次の物理ティックで突然位置がジャンプする「階段状の動き」としてプレイヤーの目に映ります。これがジッターの正体です。


解決策1:V-Syncとmax_fpsによるフレームレート制御

最も基本的な対策は、レンダリングフレームレートを制御することです。GodotはV-Sync(垂直同期)とEngine.max_fpsという2つの主要な方法を提供します。

V-Sync(垂直同期)の活用

V-Syncは、レンダリングフレームレートをモニターのリフレッシュレートに同期させる技術です。これにより、画面が途中で更新されることによる「ティアリング」を防ぎます。

プロジェクト設定の Display > Window > Vsync > Vsync Mode から設定できます。

V-Syncモード概要メリットデメリット
DisabledV-Syncを無効化。FPS上限がなくなり、入力遅延が最小化される。ティアリングが激しく発生する。
Enabled常にV-Syncを有効化。ティアリングを完全に防止できる。FPSがリフレッシュレートを下回ると、描画が次の同期タイミングまで待たされ、入力遅延が増加する。
AdaptiveFPSがリフレッシュレートを上回る時だけ同期。ティアリングを防ぎつつ、FPS低下時の入力遅延を回避できる。対応しているハードウェアが必要。
Mailbox常に最新のレンダリング済みフレームを保持し、同期タイミングで表示。ティアリングと入力遅延の両方を最小限に抑える。VRAM使用量が増加し、わずかな追加遅延が発生する可能性がある。

注意:外部設定の上書き GodotでV-Syncを設定しても有効にならない場合、NVIDIA Control PanelやAMD Radeon Softwareなどのグラフィックカード設定がGodotの設定を上書きしている可能性があります。ドライバー側の設定で「V-Sync: アプリケーションによるコントロール」を選択してください。

Engine.max_fpsによる手動制限

V-Syncに頼らず、特定のFPSに固定したい場合に有効です。例えば、意図的にレトロゲーム風の低いFPSにしたり、モバイルデバイスでのバッテリー消費を抑えたりする目的で使われます。

# GameController.gd

func _ready():
    # ゲームの最大FPSを60に設定
    # V-SyncがDisabledの場合にのみ有効
    Engine.max_fps = 60

max_fpsは、_ready()関数内などで一度設定すれば、ゲーム全体で有効になります。ただし、V-SyncがEnabledの場合、max_fpsは無視されることに注意してください。


解決策2:物理補間(Physics Interpolation)によるジッターの根絶

フレームレート制御だけでは、物理ティックとレンダリングフレームの非同期性という根本問題は解決しません。そこで登場するのが物理補間です。これは、固定された物理ティック間のオブジェクトの位置を、レンダリング時に滑らかに補間描画する技術であり、ジッターに対する最も効果的な解決策です。

プロジェクト設定で有効化する(推奨)

Godot 4.3以降、2Dと3Dの両方で物理補間が標準サポートされました。ほとんどの場合、以下の設定を有効にするだけで問題は解決します。

  1. プロジェクト > プロジェクト設定 を開く
  2. Physics > Common > Physics InterpolationOn に設定する

これだけで、CharacterBody2D/3DRigidBody2D/3Dなど、物理演算の影響を受けるほとんどのノードの動きが自動的に補間され、非常に滑らかになります。

手動での補間実装(カメラ追従など)

物理ノードではないオブジェクト(例:カメラ、UI要素)を物理ノードに追従させる場合、手動での補間が必要になります。Engine.get_physics_interpolation_fraction() を使うことで、現在のレンダリングフレームが物理ティック間でどの位置にあるか(0.0〜1.0)を取得できます。

以下は、物理補間を有効にしたプレイヤーキャラクターに、カメラが滑らかに追従する実践的なコード例です。

# SmoothCamera2D.gd
# Camera2Dノードにアタッチ

extends Camera2D

# 追従対象のノード(プレイヤーなど)をインスペクターから設定
@export var target: Node2D

# 追従の滑らかさ。値が小さいほど滑らかになる
@export var smoothing: float = 0.1

var _previous_target_position: Vector2

func _ready():
    if 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

    # 物理補間が有効な場合、ターゲットのレンダリング位置は常に補間されている
    # しかし、カメラのような非物理ノードは自前で補間する必要がある
    var interpolation_fraction = Engine.get_physics_interpolation_fraction()

    # 1フレーム前のターゲット位置と現在のターゲット位置を補間して、
    # カメラが目指すべき「真の」現在位置を計算する
    var interpolated_target_position = _previous_target_position.lerp(target.global_position, interpolation_fraction)

    # カメラ自身の位置を、計算された目標位置に向かってさらに滑らかに移動させる
    global_position = global_position.lerp(interpolated_target_position, smoothing)

func _physics_process(delta):
    if not target:
        return

    # 次のフレームの補間のために、物理ティック時点でのターゲット位置を保存しておく
    _previous_target_position = target.global_position

このコードでは、2段階のlerp(線形補間)を行っています。

  1. 物理ティック間のターゲットの動きを補間
  2. カメラ自身の動きをさらに滑らかにする

これにより、プレイヤーの動きが急であっても、カメラは映画のようにスムーズに追従します。


よくある間違いとベストプラクティス

最適化に取り組む上で、開発者が陥りがちな罠と、それを避けるためのベストプラクティスをまとめました。

トピックよくある間違い(アンチパターン)ベストプラクティス
処理の記述場所移動処理やロジックをすべて_processに書いてしまう。物理的な動きは_physics_process描画や入力に関する処理は_process に書く。deltaの役割を正しく理解する。
最適化の順序「重そうだから」という推測でObject Poolingなどを導入する。「推測するな、計測せよ」。Godotのプロファイラでボトルネックを特定してから、的確な最適化手法を選択する。
Object Pooling生成頻度の低いオブジェクト(ボス、UI)にまで適用し、コードを複雑化させる。プロファイラでノード生成がボトルネックだと確認できた短命・高頻度なオブジェクト(弾丸、エフェクト)にのみ適用する。
物理オブジェクトの削除_physics_process内でqueue_free()remove_child()を直接呼び出し、エラーを発生させる。call_deferred("queue_free")を使い、現在の物理ステップ完了後に安全に削除処理を予約する。
物理ティックレートジッターを解消しようと、物理ティックレートを可変にしたり、極端に高くしたりする。物理ティックレートは固定(デフォルト60Hz)のままにし、ジッター対策は物理補間に任せる。

上級テクニック:Object Poolingの実装

プロファイリングの結果、ノードの生成・破棄(instantiate() / queue_free())がパフォーマンスのボトルネックになっていると判明した場合、Object Poolingが有効な解決策となります。

以下は、AutoLoad(シングルトン)として利用できる汎用的なObject Poolingマネージャーの実装例です。

# ObjectPoolManager.gd (AutoLoadに登録)
extends Node

var _pool: Dictionary = {}

# プールを事前に生成しておく
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]
        # 最初は非アクティブ状態でプールに格納
        instance.set_process(false)
        instance.set_physics_process(false)
        instance.visible = false
        add_child(instance)
        _pool[scene].append(instance)

# プールからオブジェクトを取得
func get_object(scene: PackedScene) -> Node:
    if _pool.has(scene) and not _pool[scene].is_empty():
        var instance = _pool[scene].pop_front()
        # 再利用のためにアクティブ化
        instance.set_process(true)
        instance.set_physics_process(true)
        instance.visible = true
        return instance
    else:
        # プールが空なら新規生成(フォールバック)
        return scene.instantiate()

# オブジェクトをプールに戻す
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:
        # 非アクティブ化してプールに戻す
        instance.set_process(false)
        instance.set_physics_process(false)
        instance.visible = false
        # 物理的な衝突を避けるため、画面外へ移動
        if instance is Node2D:
            instance.global_position = Vector2(1e8, 1e8)
        # 親をObjectPoolManagerに戻す
        if instance.get_parent() != self:
            instance.get_parent().remove_child(instance)
            add_child(instance)
        _pool[scene_key].append(instance)
    else:
        # プール管理外のオブジェクトは通常通り破棄
        instance.queue_free()

# --- 使用例 ---
# Player.gd

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

func _ready():
    # ゲーム開始時に弾丸を50個プールしておく
    ObjectPoolManager.pre_populate_pool(BULLET_SCENE, 50)

func _fire():
    # プールから弾丸を取得
    var bullet = ObjectPoolManager.get_object(BULLET_SCENE)
    
    # 取得した弾丸をゲームワールドの適切な場所(例: Y-Sortノード)に配置
    var game_world = get_tree().get_root().get_node("GameWorld")
    bullet.reparent(game_world)
    
    # 弾丸の初期設定
    bullet.global_position = $Muzzle.global_position
    bullet.rotation = global_rotation
    bullet.fire()

# Bullet.gd

func on_hit_or_timeout():
    # 破棄する代わりにプールに戻す
    ObjectPoolManager.return_object(self)

重要なポイント:reparent() プールされたオブジェクトは、取得後に必ずreparent()を使ってゲームワールドの適切な親ノード(Y-Sortが有効なノードなど)に移動させる必要があります。これにより、正しい描画順序と親子関係が保証されます。


まとめと次のステップ

安定したFPSは、高品質なゲーム体験の基盤です。今回解説した知識を体系的に活用することで、あなたのGodotプロジェクトは劇的に改善されるでしょう。

課題根本原因解決策
ジッター(カクつき)物理とレンダリングの非同期性物理補間を有効にするのが最善。
ティアリング(画面の裂け目)描画とモニター更新の非同期性V-Syncを有効にする(AdaptiveまたはMailboxを推奨)。
ノード生成の負荷短命オブジェクトの頻繁な生成・破棄プロファイラで確認後、ボトルネックであればObject Poolingを導入。
全体的なパフォーマンス低下不明な重い処理Godotプロファイラでボトルネックを特定し、アルゴリズムやシェーダーを改善。

次のステップ

  1. 現状分析: あなたのプロジェクトでデバッガーのMonitorsを有効にし、FPSの変動を確認してください。
  2. ジッター対策: 物理補間を有効にし、動きが滑らかになることを体感してください。カメラなど非物理ノードの追従には、手動補間コードを実装しましょう。
  3. ボトルネック特定: パフォーマンスが低いと感じるシーンでProfilerを実行し、最も時間を消費している関数を特定します。
  4. 的確な最適化: 特定した原因に応じて、この記事で学んだ解決策(アルゴリズム改善、Object Poolingなど)を適用し、再度プロファイリングして効果を測定してください。

これらの知識を武器に、プレイヤーを魅了する滑らかで快適なゲーム体験を創造してください。