GodotでFPSを確認したいとき、Debug > Visible Collision Shapesみたいに「エディタの機能」に頼ったり、毎回シーンにLabelを置いてスクリプトを直書きしたりしていませんか?
しかも、プレイヤーシーンごとにFPS用Labelを複製していくと、UIノード階層がどんどん深くなっていきますよね。

「このシーンにはFPSいらない」「このシーンでは位置を右上にしたい」みたいな調整も、継承ベースで作っているとだんだん面倒になってきます。

そこで今回は、どのシーンにもポン付けできて、位置や表示フォーマットも簡単に変えられるコンポーネント指向のデバッグラベル、「FPSCounter」コンポーネントを作ってみましょう。
ノード階層にベタ書きするのではなく、「必要なシーンにだけアタッチする」独立コンポーネントとして設計していきます。

【Godot 4】どのシーンにもポン付けできる!「FPSCounter」コンポーネント

以下は、Godot 4 用の完全な GDScript コードです。
Control系のUIとして動作し、画面の四隅・マージン・フォーマットなどを@exportで柔軟に調整できるようにしています。

フルコード


extends Control
class_name FPSCounter
"""
FPSCounter コンポーネント
- 現在の FPS を画面隅に表示するデバッグ用ラベル
- どのシーンにも簡単にアタッチして使えるように設計
"""

# 表示する画面の隅
enum Corner {
	TOP_LEFT,
	TOP_RIGHT,
	BOTTOM_LEFT,
	BOTTOM_RIGHT,
}

@export var corner: Corner = Corner.TOP_LEFT:
	set(value):
		corner = value
		_update_anchor_and_position()

# 画面の隅からどれだけ離すか(ピクセル)
@export var margin: Vector2 = Vector2(8, 8):
	set(value):
		margin = value
		_update_anchor_and_position()

# 表示フォーマット
# {fps} が現在の FPS に置き換わる
@export var text_format: String = "FPS: {fps}"

# FPS の更新間隔(秒)
# 0.0 の場合は毎フレーム更新
@export_range(0.0, 5.0, 0.05)
var update_interval: float = 0.25

# FPS が 0 のときに非表示にするか
@export var hide_when_zero: bool = false

# 内部用ラベル
var _label: Label
var _time_accum: float = 0.0


func _ready() -> void:
	# このコンポーネント自体を UI のルートにしてもいいし、
	# 既存の CanvasLayer / Control の子にしてもOKです。
	#
	# 自身のサイズは自動でラベルのサイズに合わせるため、
	# レイアウトは "Shrink to Minimum" にしておくと扱いやすいです。

	# Label ノードを動的に生成して、このコンポーネントの子にします。
	_label = Label.new()
	_label.name = "FPSLabel"
	_label.text = "FPS: --"
	_label.autowrap_mode = TextServer.AUTOWRAP_OFF
	_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
	_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
	add_child(_label)

	# 自身のマウス入力をブロックしないようにする(UI の邪魔をしない)
	mouse_filter = Control.MOUSE_FILTER_IGNORE

	# 最初のレイアウト反映
	_update_anchor_and_position()


func _process(delta: float) -> void:
	if update_interval <= 0.0:
		# 毎フレーム更新するモード
		_update_fps_label()
		return

	# 一定間隔で更新するモード
	_time_accum += delta
	if _time_accum >= update_interval:
		_time_accum = 0.0
		_update_fps_label()


func _update_fps_label() -> void:
	var fps := Engine.get_frames_per_second()
	if hide_when_zero and fps <= 0:
		visible = false
		return

	visible = true
	# {fps} プレースホルダを置き換えて表示
	_label.text = text_format.format({ "fps": fps })

	# ラベルのサイズに合わせて自分のサイズも更新しておくと
	# レイアウトが崩れにくくなります
	custom_minimum_size = _label.size
	_update_anchor_and_position()


func _update_anchor_and_position() -> void:
	# 画面のどの隅に置くかを anchor + offset で制御します。
	# ここではシンプルに "隅から margin 分だけ離す" というレイアウトにしています。
	#
	# 注意:
	# - この Control が CanvasLayer 直下 or 画面全体を覆う UI の子である前提です。
	# - ビューポートリサイズ時にも正しく追従します。

	# 一旦、アンカーを固定値で決める
	match corner:
		Corner.TOP_LEFT:
			anchor_left = 0.0
			anchor_top = 0.0
			anchor_right = 0.0
			anchor_bottom = 0.0
			# 左上基準
			offset_left = margin.x
			offset_top = margin.y

		Corner.TOP_RIGHT:
			anchor_left = 1.0
			anchor_top = 0.0
			anchor_right = 1.0
			anchor_bottom = 0.0
			# 右上基準(右下方向に負のオフセット)
			offset_right = -margin.x
			offset_top = margin.y

		Corner.BOTTOM_LEFT:
			anchor_left = 0.0
			anchor_top = 1.0
			anchor_right = 0.0
			anchor_bottom = 1.0
			# 左下基準
			offset_left = margin.x
			offset_bottom = -margin.y

		Corner.BOTTOM_RIGHT:
			anchor_left = 1.0
			anchor_top = 1.0
			anchor_right = 1.0
			anchor_bottom = 1.0
			# 右下基準
			offset_right = -margin.x
			offset_bottom = -margin.y

	# ラベルのアラインメントはコーナーに合わせておくと違和感が減ります
	match corner:
		Corner.TOP_LEFT, Corner.BOTTOM_LEFT:
			_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
		Corner.TOP_RIGHT, Corner.BOTTOM_RIGHT:
			_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT

	# 縦方向のアラインメント
	match corner:
		Corner.TOP_LEFT, Corner.TOP_RIGHT:
			_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
		Corner.BOTTOM_LEFT, Corner.BOTTOM_RIGHT:
			_label.vertical_alignment = VERTICAL_ALIGNMENT_BOTTOM

使い方の手順

ここからは、実際にプロジェクトに組み込んで使うまでの手順を見ていきましょう。

手順①: スクリプトを保存する

  1. 上のコードをそのままコピーして、res://components/fps_counter.gd のようなパスに保存します。
  2. class_name FPSCounter を定義しているので、エディタの「ノードを追加」ダイアログから FPSCounter として直接追加できるようになります。

手順②: デバッグ用の CanvasLayer を用意する(推奨)

FPS 表示はゲームの UI とは少し役割が違うので、デバッグ専用のレイヤーに分けておくとスッキリします。

Main (Node2D or Node3D)
 ├── World (Node2D / Node3D / etc...)
 ├── UI (CanvasLayer)            # ゲームの本番UI
 └── DebugOverlay (CanvasLayer)  # デバッグ用のレイヤー
      └── FPSCounter (Control)
  • DebugOverlay (CanvasLayer) を 1 つ作り、その子に FPSCounter ノードを追加します。
  • CanvasLayer の layer を UI より前(例: 10)にしておくと、常に一番上に表示されます。

手順③: FPSCounter のパラメータを調整する

エディタで FPSCounter ノードを選択すると、インスペクタに以下のようなプロパティが表示されます。

  • corner: 表示する隅(TOP_LEFT / TOP_RIGHT / BOTTOM_LEFT / BOTTOM_RIGHT)
  • margin: 画面の隅からどれだけ離すか
  • text_format: 表示フォーマット。例: "FPS: {fps}""{fps} fps"
  • update_interval: 更新間隔(秒)。0 にすると毎フレーム更新
  • hide_when_zero: FPS が 0 のときに非表示にするか

例えば、右上に「120 fps」みたいにシンプルに出したい場合:

  • corner = TOP_RIGHT
  • text_format = "{fps} fps"
  • margin = (16, 16)

手順④: 具体的な使用例

例1: プレイヤーを動かしながら FPS を確認する

プレイヤーシーンはそのままにして、メインシーン側で FPSCounter を置くパターンです。

Main (Node2D)
 ├── Player (CharacterBody2D)
 │    ├── Sprite2D
 │    ├── CollisionShape2D
 │    └── PlayerMove (Script)
 ├── World (TileMap)
 └── DebugOverlay (CanvasLayer)
      └── FPSCounter (Control)

これなら、プレイヤーシーンを他プロジェクトに持っていっても、余計なデバッグ UI が混ざらないのできれいですね。

例2: 敵 AI の負荷を見るために敵シーンにだけ付ける

「敵だけを動かすテストシーン」で FPS を見たい場合、そのシーンにだけ FPSCounter を付けるのもアリです。

EnemyTestScene (Node2D)
 ├── EnemySpawner (Node)
 ├── EnemyContainer (Node2D)
 └── FPSCounter (Control)

このように「必要なシーンにだけコンポーネントをアタッチする」スタイルにすると、シーン継承を使わずに柔軟なデバッグ環境を作れます。

例3: 動く床テスト用シーンで FPS を常時チェック
MovingPlatformTest (Node2D)
 ├── MovingPlatform (Node2D)
 │    ├── Sprite2D
 │    └── CollisionShape2D
 └── FPSCounter (Control)

物理挙動やアニメーションの負荷をチェックしたいときも、テスト専用シーンに FPSCounter を 1 個置くだけで済みます。

メリットと応用

この FPSCounter コンポーネントを使うことで、次のようなメリットがあります。

  • シーン構造がスッキリ
    各 UI シーンに Label をバラバラに置く必要がなく、FPSCounter という 1 つのコンポーネントに集約できます。
  • 継承に縛られない
    「デバッグ UI 付きベースシーン」を継承していくスタイルではなく、必要なシーンにだけコンポーネントをアタッチする構成にできるので、シーンの再利用性が上がります。
  • フォーマットや位置の変更が一括で可能
    表示フォーマットや位置は @export で外から変えられるので、「このシーンでは右下」「このシーンでは左上」といったカスタマイズも簡単です。
  • パフォーマンスに配慮した更新
    update_interval を調整することで、毎フレーム更新せずに 0.25 秒ごとなどの間引き更新もできます。重い UI を追加したくないときに便利ですね。

コンポーネント指向で作っておくと、「やっぱり FPS だけじゃなくて、描画コール数やメモリも出したい」となったときにも拡張が簡単です。

改造案: FPS に応じてテキストカラーを変える

例えば、FPS が低いときに赤く表示して、状況を一目で分かるようにしたい場合、次のような関数を追加できます。


func _apply_color_by_fps(fps: int) -> void:
	# 60fps 以上なら緑、30~59fps は黄色、それ未満は赤
	if fps >= 60:
		_label.modulate = Color(0.2, 1.0, 0.2)  # green
	elif fps >= 30:
		_label.modulate = Color(1.0, 0.9, 0.2)  # yellow
	else:
		_label.modulate = Color(1.0, 0.3, 0.3)  # red

そして、_update_fps_label() の中で:


func _update_fps_label() -> void:
	var fps := Engine.get_frames_per_second()
	if hide_when_zero and fps <= 0:
		visible = false
		return

	visible = true
	_label.text = text_format.format({ "fps": fps })
	_apply_color_by_fps(fps)

と呼び出してあげれば、簡易的な「負荷インジケータ」としても使えるようになります。
このように、小さなコンポーネントを組み合わせていくことで、継承地獄に陥らずに柔軟なデバッグ環境を育てていけますね。