Godot 4 でアクションRPGやローグライクを作っていると、MP(マナ)の管理ってけっこう面倒ですよね。
よくある実装だと、

  • プレイヤーのスクリプトに mpmax_mp を直書き
  • スキルごとに「MPが足りるか」「回復中か」などの条件分岐をゴリゴリ追加
  • 敵や味方ごとに似たような MP ロジックをコピペしてバグ地獄

さらに Godot 標準のやり方だと、

  • プレイヤー用のベースクラスを継承して「MP付きプレイヤー」「MPなしプレイヤー」を作る
  • ステータス管理用の親ノードを作って、その下に MP ノードをぶら下げる

…みたいに、継承ツリーやノード階層がどんどん深くなりがちです。
結果として、「このキャラはどこで MP を管理しているんだっけ?」とソースを追いかけ回すハメになります。

そこで今回は、どんなキャラにもポン付けできる、コンポーネント指向の MP 自動回復を用意しました。
Node に 1 個アタッチするだけで、時間経過で MP が自動回復する仕組みを追加できます。

【Godot 4】時間でじわっとマナ回復!「ManaRecharge」コンポーネント

この「ManaRecharge」コンポーネントは、

  • 最大 MP / 現在 MP の管理
  • 時間経過による自動回復
  • スキル使用時の MP 消費チェック
  • UI 連携用のシグナル(MP が変化したとき)

までをひとまとめにした、小さなステータス管理ノードです。
プレイヤーでも敵でも、動くタレットでも、MP を使うもの全部に共通で使えるようにしてあります。


フルコード: ManaRecharge.gd


extends Node
class_name ManaRecharge
## MP(マナ)を時間経過で自動回復させるコンポーネント。
## プレイヤー、敵、タレットなど「MPを使うもの」にアタッチして使います。
##
## 想定ユース:
## - スキル使用時に spend_mana() を呼ぶ
## - UI は mana_changed シグナルを監視してバーを更新
## - セーブ/ロード時は current_mana / max_mana を保存

## === 基本パラメータ ===

@export_range(0.0, 9999.0, 1.0)
var max_mana: float = 100.0:
	set(value):
		max_mana = max(value, 0.0)
		# 最大値変更時に現在値もクランプ
		current_mana = clamp(current_mana, 0.0, max_mana)
		emit_signal("mana_changed", current_mana, max_mana)

@export_range(0.0, 9999.0, 1.0)
var current_mana: float = 100.0:
	set(value):
		var clamped = clamp(value, 0.0, max_mana)
		if !is_equal_approx(clamped, current_mana):
			current_mana = clamped
			emit_signal("mana_changed", current_mana, max_mana)

## 1秒あたりに回復するMP量(パッシブ回復速度)
@export_range(0.0, 9999.0, 0.1)
var regen_per_second: float = 5.0

## MPが自動回復を開始するまでの待機時間(秒)
## 例: スキルを使ってから一定時間は回復しない、など。
@export_range(0.0, 60.0, 0.1)
var regen_delay: float = 1.5

## 自動回復を有効/無効にするフラグ
@export var auto_regen_enabled: bool = true

## ゲーム一時停止中も回復させるかどうか
@export var process_in_pause: bool = false

## === シグナル ===

## MPが変化したときに発火(UI更新などに利用)
signal mana_changed(current: float, max: float)

## MPが枯渇したとき(0になった瞬間)に発火
signal mana_depleted

## MPを消費できなかったとき(足りない等)に発火
signal mana_not_enough(requested: float, current: float)

## === 内部状態 ===

var _time_since_last_spend: float = 0.0

func _ready() -> void:
	# Pause時の挙動を設定
	process_mode = Node.PROCESS_MODE_PAUSABLE
	if process_in_pause:
		process_mode = Node.PROCESS_MODE_ALWAYS

	# current_mana の初期クランプ
	current_mana = clamp(current_mana, 0.0, max_mana)

func _process(delta: float) -> void:
	if !auto_regen_enabled:
		return

	# MP消費からの経過時間を更新
	_time_since_last_spend += delta

	# ディレイ経過後のみ回復を行う
	if _time_since_last_spend < regen_delay:
		return

	if current_mana >= max_mana:
		return

	# delta に応じてMPを回復
	var before := current_mana
	current_mana += regen_per_second * delta

	# 0→正の値になった/満タンになった等は mana_changed のシグナルでUIが拾える
	# ここでは特別な処理はしない
	# (必要ならここで「full_mana_reached」シグナルを足すのもあり)

## === パブリックAPI ===

## MPを消費する。
## 成功したら true を返し、失敗したら false を返す。
func spend_mana(amount: float) -> bool:
	if amount <= 0.0:
		return true  # 0以下の消費は常に成功扱い

	if current_mana < amount:
		emit_signal("mana_not_enough", amount, current_mana)
		return false

	current_mana -= amount

	# 消費したので、回復ディレイ用タイマーをリセット
	_time_since_last_spend = 0.0

	if is_equal_approx(current_mana, 0.0):
		emit_signal("mana_depleted")

	return true

## MPを即座に回復する(ポーションなど)。
## 正の値なら回復、負の値なら消費としても使える。
func add_mana(amount: float) -> void:
	if amount == 0.0:
		return
	current_mana += amount
	# 回復した場合はディレイはリセットしない(好みで変更可)

## MPを最大値まで全回復する。
func restore_full() -> void:
	current_mana = max_mana

## 現在のMP割合(0.0〜1.0)を返す。UIバーなどで利用。
func get_mana_ratio() -> float:
	if max_mana <= 0.0:
		return 0.0
	return current_mana / max_mana

## セーブデータなどから復元したいとき用のヘルパー。
func set_mana_values(new_current: float, new_max: float) -> void:
	max_mana = new_max
	current_mana = new_current
	_time_since_last_spend = 0.0

使い方の手順

ここでは代表的な例として、プレイヤーキャラに MP 自動回復を付ける手順を説明します。
もちろん敵やタレットにも同じノリで付けられます。

スクリプトをプロジェクトに追加
上記のコードを res://components/mana_recharge/ManaRecharge.gd などに保存します。
class_name ManaRecharge を定義しているので、後でノード追加メニューから直接追加できます。

プレイヤーシーンにコンポーネントをアタッチ
例として 2D のプレイヤーを想定します。

Player (CharacterBody2D)
├── Sprite2D
├── CollisionShape2D
└── ManaRecharge (Node)

Godot エディタで:

Player シーンを開く

Player を右クリック → 「子ノードを追加」

検索窓に「ManaRecharge」と入力して追加

パラメータを調整
ManaRecharge ノードを選択し、インスペクタから:

max_mana: 最大 MP(例: 100)

current_mana: 初期 MP(例: 50)

regen_per_second: 1 秒あたりの回復量(例: 3)

regen_delay: スキル使用後、何秒待ってから回復開始するか(例: 1.5)

auto_regen_enabled: 自動回復を有効にするか

process_in_pause: ポーズ中も回復させたいなら ON

プレイヤー側から MP を使う
スキル発動時に spend_mana() を呼ぶだけです。



# Player.gd (例)
extends CharacterBody2D

@onready var mana: ManaRecharge = $ManaRecharge

const FIREBALL_COST := 20.0

func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("cast_fireball"):
_try_cast_fireball()

func _try_cast_fireball() -> void:
# MPが足りていれば消費してスキル発動
if mana.spend_mana(FIREBALL_COST):
_cast_fireball()
else:
# 足りないときのフィードバック(SE、UI点滅など)
print("MPが足りない! 現在:", mana.current_mana)

func _cast_fireball() -> void:
# 実際のスキル生成処理(例)
print("ファイアボール発射!")


UI の MP バーと連携したい場合は、シグナル mana_changed をつなぎましょう。



# 例: MPバー制御用スクリプト(Control等にアタッチ)
extends Control

@export var player_path: NodePath
var _mana: ManaRecharge

@onready var mana_bar: TextureProgressBar = %ManaBar

func _ready() -> void:
var player = get_node(player_path)
_mana = player.get_node("ManaRecharge")
_mana.mana_changed.connect(_on_mana_changed)

# 初期表示
_on_mana_changed(_mana.current_mana, _mana.max_mana)

func _on_mana_changed(current: float, max: float) -> void:
mana_bar.max_value = max
mana_bar.value = current


他の具体例

例1: 敵メイジの MP 管理

EnemyMage (CharacterBody2D)
 ├── Sprite2D
 ├── CollisionShape2D
 ├── ManaRecharge (Node)
 └── EnemyAI (Node / Script)

EnemyAI スクリプト側で、魔法攻撃の前に mana.spend_mana() を呼ぶだけで、プレイヤーと同じロジックが使えます。

例2: 自動砲台(タレット)のチャージショット

Turret (Node2D)
 ├── Sprite2D
 ├── Area2D
 └── ManaRecharge (Node)

砲台が一定 MP 以上たまったら強力なショットを撃つ、という処理も、


# Turret.gd
@onready var mana: ManaRecharge = $ManaRecharge

const CHARGED_SHOT_COST := 50.0

func _process(delta: float) -> void:
	if mana.current_mana >= CHARGED_SHOT_COST:
		if mana.spend_mana(CHARGED_SHOT_COST):
			_fire_charged_shot()

のようにシンプルに書けます。


メリットと応用

この ManaRecharge コンポーネントを使う一番のメリットは、「MP ロジックをキャラ本体から切り離せる」ことです。

  • プレイヤー / 敵 / タレットなど、どのシーンにも同じコンポーネントをポン付けできる
  • MP の仕様変更(回復速度、ディレイ、UI 連携など)を 1 ファイルだけ直せば全キャラに反映される
  • 「MPを持たない敵」は単に ManaRecharge を付けないだけでOK(継承クラスを分ける必要なし)
  • シーン構造が浅いままでも、MP という機能を後付けで合成できる

また、レベルデザインの観点でも扱いやすくなります。

  • ステージごとに max_manaregen_per_second を変えるだけで、難易度調整が可能
  • ボス戦だけ regen_delay を長くして「一気に打ち切るとしばらく無防備」などのギミックも簡単
  • UI 側はシグナルだけを見ていればよいので、キャラの中身を知らなくても連携できる

これらはすべて、「継承で巨大な PlayerBase を作る」のではなく、「MP 回復という機能をコンポーネントとして合成する」ことで得られるメリットですね。

改造案: 「MPが満タンになったらエフェクトを出す」

例えば、「MP が満タンになった瞬間にエフェクトを出したい」という場合、ManaRecharge に小さなフックを追加するだけで済みます。


# ManaRecharge.gd の末尾あたりに追加する例

signal mana_full

func _process(delta: float) -> void:
	if !auto_regen_enabled:
		return

	_time_since_last_spend += delta
	if _time_since_last_spend < regen_delay:
		return

	if current_mana >= max_mana:
		return

	var before := current_mana
	current_mana += regen_per_second * delta

	# ここで「満タンになった瞬間」を検出
	if before < max_mana and is_equal_approx(current_mana, max_mana):
		emit_signal("mana_full")

こうしておけば、プレイヤー側で


func _ready() -> void:
	$ManaRecharge.mana_full.connect(_on_mana_full)

func _on_mana_full() -> void:
	# キラキラエフェクトを再生するなど
	print("MPが満タンになった!")

のように、演出を好きなだけ足していけます。
MP 回復ロジックそのものはコンポーネントに閉じ込めておき、演出やゲーム性の部分だけを外側から合成していくイメージですね。

こんな感じで、「継承より合成」のスタイルで、どんどん小さなコンポーネントを積み上げていきましょう。