Godot 4 でアクションRPGやローグライクを作っていると、MP(マナ)の管理ってけっこう面倒ですよね。
よくある実装だと、
- プレイヤーのスクリプトに
mpやmax_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_manaやregen_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 回復ロジックそのものはコンポーネントに閉じ込めておき、演出やゲーム性の部分だけを外側から合成していくイメージですね。
こんな感じで、「継承より合成」のスタイルで、どんどん小さなコンポーネントを積み上げていきましょう。
