Godot でちょっと大きめのゲームを作り始めると、気になってくるのが「これ、メモリリークしてないよね…?」という不安ですよね。
標準のデバッガや Profiler でもある程度は追えますが、

  • 実機ビルド(PC / モバイル)での挙動をリアルタイムに見たい
  • 特定のシーンだけ、メモリの増減をウォッチしたい
  • テスターさんにも「メモリ増えてるかどうか」を視覚的に見てもらいたい

といった時に、毎回デバッガを接続するのは正直面倒です。
そこで登場するのが、今回紹介するコンポーネント指向な「MemoryMonitor」コンポーネントです。

ノード階層をゴリゴリ継承で増やすのではなく、「どのシーンにもポン付けできる監視コンポーネント」として設計してあるので、
プレイヤーシーンでも、メインシーンでも、デバッグ専用シーンでも、好きなところにアタッチして使えます。

【Godot 4】メモリリークをその場で見張る!「MemoryMonitor」コンポーネント

このコンポーネントは、

  • 現在の RAM 使用量(MB)を一定間隔で取得
  • 前回値と比較して「増え続けていないか」をざっくり監視
  • 画面にオーバーレイ表示(デバッグ HUD 的なやつ)
  • しきい値を超えて増え続けたら、色を変えて警告

といったことをやってくれます。
内部的には Performance クラスを使ってメモリ使用量を取得しており、PC でもモバイルでも動きます。


フルコード: MemoryMonitor.gd


extends CanvasLayer
class_name MemoryMonitor
## メモリ使用量を定期的に監視して、画面に表示するコンポーネント。
## 任意のシーンにアタッチして使えるデバッグ用 HUD です。

@export_range(0.1, 10.0, 0.1)
var update_interval: float = 0.5:
	## 何秒ごとにメモリ情報を更新するか
	## 数字を小さくするとリアルタイム性は増すが、更新負荷や数値の揺れも増えます。
	set(value):
		update_interval = max(0.1, value)

@export var show_peak_memory: bool = true:
	## 実行中に観測したピークメモリを表示するかどうか

@export var enable_leak_warning: bool = true:
	## 「増え続けているっぽい」場合に警告を出すかどうか

@export_range(1.0, 600.0, 1.0)
var leak_window_seconds: float = 60.0:
	## 何秒間のメモリ増加傾向を見るか
	## 例: 60秒間で一定以上増えていたら「怪しい」とみなす。

@export_range(1.0, 1024.0, 1.0)
var leak_threshold_mb: float = 50.0:
	## leak_window_seconds の間にどれだけ増えたら「怪しい」とみなすか (MB)

@export var anchor_top_left: Vector2 = Vector2(8, 8):
	## 画面左上からのオフセット (px)
	## UI と被る場合はここを調整してください。

@export var font_size: int = 16:
	## 表示フォントサイズ(テーマに依存)

@export var normal_color: Color = Color(0.9, 0.9, 0.9, 0.9)
@export var warning_color: Color = Color(1.0, 0.4, 0.2, 1.0)
@export var background_color: Color = Color(0.05, 0.05, 0.05, 0.7)

# 内部状態管理用
var _label: Label
var _panel: Panel
var _timer: Timer

var _current_mb: float = 0.0
var _peak_mb: float = 0.0

# 過去の測定値を保持して「増え続けているか」を見るための履歴
var _history: Array[Dictionary] = []  # [{time: float, mb: float}, ...]

func _ready() -> void:
	# 自身を CanvasLayer として画面にオーバーレイ表示
	layer = 100  # 他 UI より前面に出したい場合は大きめに

	_create_ui()
	_create_timer()
	_update_memory() # 初回更新

func _create_ui() -> void:
	# パネル + ラベルの簡易 HUD を作る
	_panel = Panel.new()
	add_child(_panel)
	_panel.name = "MemoryMonitorPanel"
	_panel.modulate = Color.WHITE
	_panel.self_modulate = Color.WHITE

	# パネルの見た目を簡易的に設定(テーマがあればそちらが優先されます)
	var style_box := StyleBoxFlat.new()
	style_box.bg_color = background_color
	style_box.corner_radius_top_left = 4
	style_box.corner_radius_top_right = 4
	style_box.corner_radius_bottom_left = 4
	style_box.corner_radius_bottom_right = 4
	_panel.add_theme_stylebox_override("panel", style_box)

	_label = Label.new()
	_panel.add_child(_label)
	_label.name = "MemoryLabel"
	_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
	_label.vertical_alignment = VERTICAL_ALIGNMENT_TOP
	_label.position = Vector2(8, 4)
	_label.autowrap_mode = TextServer.AUTOWRAP_OFF

	# フォントサイズはテーマ依存ですが、簡易的にオーバーライド
	var font := _label.get_theme_font("font")
	if font:
		_label.add_theme_font_override("font", font)
		_label.add_theme_font_size_override("font_size", font_size)

	# 初期位置
	_panel.position = anchor_top_left

func _create_timer() -> void:
	_timer = Timer.new()
	add_child(_timer)
	_timer.wait_time = update_interval
	_timer.one_shot = false
	_timer.timeout.connect(_on_timer_timeout)
	_timer.start()

func _on_timer_timeout() -> void:
	_update_memory()

func _process(_delta: float) -> void:
	# 画面サイズが変わったときなどに、位置を更新したい場合はここで対応できます。
	_panel.position = anchor_top_left

func _update_memory() -> void:
	# 現在のメモリ使用量を取得 (バイト) → MB に変換
	var bytes_used := Performance.get_monitor(Performance.MEMORY_STATIC) \
		+ Performance.get_monitor(Performance.MEMORY_DYNAMIC)
	# MEM_* の種類はプラットフォームによって挙動が異なることがありますが、
	# 「相対的な増減」を見る用途には十分です。
	var mb_used := float(bytes_used) / (1024.0 * 1024.0)

	_current_mb = mb_used
	_peak_mb = max(_peak_mb, _current_mb)

	# 履歴に追加
	_push_history(_current_mb)

	# テキスト更新
	_update_label_text()

	# パネルの色(警告状態)更新
	_update_panel_color()

func _push_history(mb: float) -> void:
	var now := Time.get_ticks_msec() / 1000.0
	_history.append({ "time": now, "mb": mb })

	# leak_window_seconds より古いデータは削除してメモリを圧迫しないようにする
	var cutoff := now - leak_window_seconds
	while _history.size() > 0 and _history[0]["time"] < cutoff:
		_history.pop_front()

func _get_leak_delta_mb() -> float:
	if _history.size() < 2:
		return 0.0

	var oldest := _history[0]
	var newest := _history[_history.size() - 1]
	return newest["mb"] - oldest["mb"]

func _is_leak_suspected() -> bool:
	if not enable_leak_warning:
		return false
	if _history.size() < 2:
		return false

	var delta := _get_leak_delta_mb()
	return delta >= leak_threshold_mb

func _update_label_text() -> void:
	var text := ""
	text += "Memory Monitor\n"
	text += "Current: %.2f MB\n" % _current_mb

	if show_peak_memory:
		text += "Peak:    %.2f MB\n" % _peak_mb

	if enable_leak_warning:
		var delta := _get_leak_delta_mb()
		text += "Δ%ds:   %+ .2f MB\n" % [int(leak_window_seconds), delta]

		if _is_leak_suspected():
			text += "[WARNING] Possible leak trend!\n"

	_label.text = text

	# パネルのサイズをラベルに合わせて調整(ざっくり)
	var font := _label.get_theme_font("font")
	var font_size := _label.get_theme_font_size("font_size")
	var lines := _label.text.split("\n", false)
	var max_width := 0.0
	for line in lines:
		if font:
			max_width = max(max_width, font.get_string_size(line, HORIZONTAL_ALIGNMENT_LEFT, -1.0, font_size).x)
	var height := float(lines.size()) * float(font_size + 2)

	_panel.custom_minimum_size = Vector2(max_width + 16, height + 8)

func _update_panel_color() -> void:
	# 警告中かどうかで文字色を変える
	var color := normal_color
	if _is_leak_suspected():
		color = warning_color

	_label.add_theme_color_override("font_color", color)

	# 背景色も少し変える(濃くする)
	var style_box := _panel.get_theme_stylebox("panel")
	if style_box is StyleBoxFlat:
		var sb := style_box as StyleBoxFlat
		var base_color := background_color
		if _is_leak_suspected():
			base_color = Color(background_color.r + 0.2, background_color.g, background_color.b, background_color.a)
		sb.bg_color = base_color


使い方の手順

コンポーネントらしく、「どのシーンにも好きなだけ付けられる」形にしてあります。具体的な例として、メインシーンに監視用 HUD を付けるパターンを見てみましょう。

手順①: スクリプトを用意する

  1. 上記コードをそのまま MemoryMonitor.gd として保存します。
    例: res://addons/debug/MemoryMonitor.gd など。
  2. Godot エディタを再読み込みすると、class_name MemoryMonitor により、
    ノード追加ダイアログで スクリプトクラス として選べるようになります。

手順②: メインシーンにアタッチする

例えば、以下のようなメインシーンを想定します。

Main (Node2D)
 ├── Player (CharacterBody2D)
 ├── EnemySpawner (Node)
 └── MemoryMonitor (CanvasLayer)
  1. Main シーンを開きます。
  2. シーンツリーで Main を選択し、「+」ボタンから新規ノードを追加します。
  3. CanvasLayer を追加し、名前を MemoryMonitor に変更します。
  4. その MemoryMonitor ノードに、先ほどの MemoryMonitor.gd をアタッチします。
    (もしくは、ノード追加ダイアログの「スクリプトクラス」から MemoryMonitor を直接選択して追加してもOKです)

手順③: 実行してメモリを眺める

ゲームを実行すると、画面左上にこんな感じのテキストが出ます:

Memory Monitor
Current: 123.45 MB
Peak:    150.32 MB
Δ60s:   + 12.34 MB
  • Current: 今この瞬間のメモリ使用量
  • Peak: 実行開始から今までで一番高かった値
  • Δ60s: 過去60秒(leak_window_seconds)でどれだけ増えたか

Δ60sleak_threshold_mb(デフォルト 50MB)を超えると、テキスト色が 警告色 に変わり、
さらに [WARNING] Possible leak trend! という行が追加されます。

手順④: プレイヤー専用のデバッグ HUD として使う例

「メインシーンには出したくないけど、デバッグ用プレイヤーシーンでは見たい」みたいな場合もありますよね。
その場合は、プレイヤーシーンにだけ MemoryMonitor を付けてしまいましょう。

Player (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 └── MemoryMonitor (CanvasLayer)
  • この構成なら、プレイヤーシーンを単体で実行したときだけメモリ HUD が出ます。
  • 本番用のメインシーンでは MemoryMonitor ノードを削除、もしくは visible = false にするだけで OK。

コンポーネントとして独立しているので、「プレイヤーにメモリ HUD が付いている」というより、
「プレイヤーシーンにデバッグ用 HUD コンポーネントを一個足しただけ」という感覚で運用できます。


メリットと応用

この MemoryMonitor コンポーネントを使うことで、

  • シーン構造を汚さずにデバッグ機能を追加できる
    継承で「DebugPlayer」「DebugMainScene」みたいな派生シーンを作らなくても、
    監視したいシーンに MemoryMonitor を一個足すだけで OK です。
  • レベルデザイン中の「なんか重くなってきた?」を即座に確認できる
    新しい敵やエフェクトを追加したときに、メモリが右肩上がりになっていないかをその場で確認できます。
  • テスターや非エンジニアにも「やばさ」が伝わる
    「このステージで 5 分放置したら Δ60s が +200MB になりました」みたいな報告がしやすくなります。
  • 実機(Android / iOS / コンソール)での挙動を可視化
    エディタのプロファイラが使えない環境でも、HUD 表示ならログをスクショで共有できます。

コンポーネント指向で作ってあるので、「MemoryMonitor を消したらゲームロジックには一切影響しない」のもポイントですね。
深い継承ツリーにデバッグコードを書き込むスタイルだと、あとからの削除がつらくなりがちですが、
こういう「外付けコンポーネント」なら、ノードごとポイっと捨てるだけです。

改造案: 一定以上増えたら自動でログを吐く

もう一歩踏み込んで、「怪しい増加を検知したら print でログを残す」機能を追加してみましょう。
以下の関数を MemoryMonitor に追加し、_update_memory() の最後あたりから呼び出すだけです。


func _log_if_leak_suspected() -> void:
	if not enable_leak_warning:
		return

	if _is_leak_suspected():
		var delta := _get_leak_delta_mb()
		push_warning(
			"[MemoryMonitor] Possible leak trend: +%.2f MB over %.0f seconds (Current: %.2f MB, Peak: %.2f MB)" % [
				delta, leak_window_seconds, _current_mb, _peak_mb
			]
		)

そして _update_memory() の末尾に


	_log_if_leak_suspected()

と追記しておけば、エディタの Output やログファイルに「いつ、どれくらい増えたか」が残るようになります。
ここからさらに、Slack / Discord 通知を飛ばしたり、CSV に書き出したり…といった拡張も簡単ですね。

継承にメモリ監視ロジックを埋め込むのではなく、「MemoryMonitor を1個足すだけ」で完結する構成にしておくと、
プロジェクトが大きくなってもデバッグ機能の出し入れが楽になります。ぜひ、自分のプロジェクト用にカスタマイズしてみてください。