Godot 4 で敵AIを書くとき、ついこんな構成にしがちですよね。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── AnimationPlayer ├── HealthBar ├── NavAgent ├── FollowPlayerAI(スクリプト) ├── FleeAI(別シーンではこっち) └── PatrolAI(さらに別の敵ではこれ)
敵ごとに継承ツリーを増やしたり、Enemy.gd の中に「追跡」「巡回」「逃走」ロジックを全部書いて if state == ... だらけになっていく…あるあるですね。
こうなると、
- 「この敵だけ逃げるAIを付けたい」→ 既存クラスを継承して一部だけ書き換え…
- 「HPが減ったら逃げるけど、普段は追いかける」→ 1ファイルがどんどん巨大に…
- 同じ逃走ロジックを別の敵で再利用したいのに、コピペが増える…
といった「継承&巨大スクリプト問題」にハマりがちです。
そこで今回は、どのキャラにもポン付けできる「逃走AI」をコンポーネント化してしまいましょう。
HPが減ったらプレイヤーから全力で逃げる、という挙動を 1つの独立コンポーネント に閉じ込めて、必要なノードにアタッチするだけで使えるようにします。
【Godot 4】HPが減ったら全力ダッシュで逃げろ!「FleeBehavior」コンポーネント
今回作る FleeBehavior コンポーネントの役割はシンプルです。
- 自分の HP が閾値より下がったら「逃走モード」に入る
- ターゲット(主にプレイヤー)から「逆方向」へ移動ベクトルを計算する
CharacterBody2D/CharacterBody3Dのvelocityを書き換えて移動させる
「逃走するかどうかの判断」と「逃走ベクトルの計算」だけを担当し、
ダメージ処理 や アニメーション や 攻撃ロジック は一切持ちません。
こうすることで、どんな敵にも「逃走行動」だけを後付けできるようになります。
GDScript フルコード(2D 用 FleeBehavior)
extends Node
class_name FleeBehavior
## HP が減ったとき、ターゲットと逆方向へ全力で逃走させるコンポーネント(2D版)。
## CharacterBody2D にアタッチして使うことを想定しています。
## === 設定パラメータ ===
@export var max_hp: float = 100.0:
## このコンポーネントが「現在HP」を自前で持つ場合の最大HP
## 既存の Health コンポーネントを使う場合は 0 のままでもOKです。
set(value):
max_hp = max(value, 1.0)
@export var current_hp: float = 100.0:
## 現在のHP。外部から直接書き換えてもOKです。
## 例: enemy_flee_behavior.current_hp -= damage
set(value):
current_hp = clamp(value, 0.0, max_hp)
@export_range(0.0, 1.0, 0.05)
var flee_hp_ratio_threshold: float = 0.3
## HPがこの割合(0.0~1.0)を下回ると「逃走モード」に入る。
## 例: 0.3 → 最大HPの30%以下で逃走開始。
@export var flee_speed: float = 300.0
## 逃走時の移動速度(ピクセル/秒)。
## 普段の移動速度より「ちょっと速い」くらいにするとそれっぽいです。
@export var acceleration: float = 1200.0
## 現在の速度から逃走速度までどれくらいの加速度で変化させるか。
## 0 にすると即座に flee_speed まで切り替わります。
@export var target_path: NodePath
## 逃げる対象(主にプレイヤー)の NodePath。
## シーン上の Player ノードをドラッグ&ドロップで指定します。
@export var only_when_target_visible: bool = false
## true のとき、ターゲットが一定距離以内にいる場合のみ逃走する簡易判定。
## 本格的な視界判定があるなら、そちらから is_forced_flee を操作してもOKです。
@export var visible_distance: float = 500.0
## only_when_target_visible = true のとき、
## この距離以内にターゲットがいなければ逃走しません。
@export var override_velocity: bool = true
## true: CharacterBody2D.velocity をこのコンポーネントが完全に上書きする
## false: 逃走ベクトルを "加算" する形にして、他の移動と合成する
@export var debug_draw_direction: bool = false
## エディタのデバッグ再生などで、逃走ベクトルを矢印で描画します。
## === 内部状態 ===
var _body: CharacterBody2D
var _target: Node2D
var _is_fleeing: bool = false
var _current_velocity: Vector2 = Vector2.ZERO
func _ready() -> void:
## 親ノードが CharacterBody2D であることを前提にします。
_body = owner as CharacterBody2D
if _body == null:
push_warning("FleeBehavior: owner is not a CharacterBody2D. This component expects to be a child of a CharacterBody2D.")
if target_path != NodePath():
_target = get_node_or_null(target_path) as Node2D
if _target == null:
push_warning("FleeBehavior: target_path is set but node was not found or is not Node2D.")
## 初期速度として親の velocity をコピーしておく
if _body:
_current_velocity = _body.velocity
func _process(delta: float) -> void:
if _body == null:
return
_update_flee_state()
_update_target_reference()
if not _is_fleeing:
return
if _target == null:
# ターゲットがいない場合は何もしない
return
# 逃走方向ベクトルを計算(ターゲット → 自分 の方向)
var to_self: Vector2 = (_body.global_position - _target.global_position)
if to_self == Vector2.ZERO:
# 同じ座標にいる場合はランダムな方向に逃げる
to_self = Vector2.RIGHT.rotated(randf() * TAU)
var flee_direction: Vector2 = to_self.normalized()
var desired_velocity: Vector2 = flee_direction * flee_speed
if acceleration <= 0.0:
_current_velocity = desired_velocity
else:
# 線形補間で滑らかに速度を変化させる
_current_velocity = _current_velocity.move_toward(desired_velocity, acceleration * delta)
# 親の velocity を更新
if override_velocity:
_body.velocity = _current_velocity
else:
_body.velocity += _current_velocity
# CharacterBody2D に実際の移動をさせる
_body.move_and_slide()
if debug_draw_direction:
queue_redraw()
func _update_flee_state() -> void:
## HP から「逃走モード」かどうかを判定する
if max_hp <= 0.0:
_is_fleeing = false
return
var hp_ratio := current_hp / max_hp
_is_fleeing = hp_ratio <= flee_hp_ratio_threshold
# ターゲットの視界判定が有効な場合、距離チェックも行う
if _is_fleeing and only_when_target_visible and _target:
var distance_to_target := _body.global_position.distance_to(_target.global_position)
if distance_to_target > visible_distance:
_is_fleeing = false
func _update_target_reference() -> void:
## ターゲットがシーンから消えた場合などに備えて毎フレーム確認
if target_path == NodePath():
return
if _target == null or not is_instance_valid(_target):
_target = get_node_or_null(target_path) as Node2D
func apply_damage(amount: float) -> void:
## 外部からダメージを与えるためのヘルパー。
## 既に別の Health コンポーネントがある場合は使わなくてもOKです。
current_hp -= amount
func heal(amount: float) -> void:
## 回復用のヘルパー。逃走を止めたいときに HP を戻す用途にも使えます。
current_hp += amount
func force_flee() -> void:
## 外部から強制的に逃走モードにする。
_is_fleeing = true
func stop_flee() -> void:
## 外部から強制的に逃走モードを解除する。
_is_fleeing = false
func _draw() -> void:
if not debug_draw_direction or not _body:
return
if _current_velocity.length() <= 0.1:
return
# 速度方向に矢印を描画(ローカル座標で簡易表示)
var from := Vector2.ZERO
var to := _current_velocity.normalized() * 32.0
draw_line(from, to, Color.RED, 2.0)
# 矢印の先端
var head_size := 6.0
var left := to + Vector2(-head_size, -head_size).rotated(to.angle())
var right := to + Vector2(-head_size, head_size).rotated(to.angle())
draw_line(to, left, Color.RED, 2.0)
draw_line(to, right, Color.RED, 2.0)
使い方の手順
ここでは 2D の敵キャラが「HP が 30% を切ったらプレイヤーから逃げる」例で説明します。
① シーン構成:敵キャラにコンポーネントをぶら下げる
敵シーンの構成はこんな感じにします。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── HealthBar (任意) └── FleeBehavior (Node) ← このコンポーネントを追加
ポイント:
Enemy本体はCharacterBody2DにしておくFleeBehavior.gdを新しいNodeにアタッチし、Enemyの子として配置する- 他の AI(巡回など)があっても OK。移動ベクトルを合成したい場合は
override_velocity = falseにします
② プレイヤーをターゲットとして指定する
プレイヤーシーンの例:
Player (CharacterBody2D) ├── Sprite2D └── CollisionShape2D
エディタ上で、Enemy の子にある FleeBehavior を選択し、インスペクタで以下を設定します。
target_path: シーンツリーからPlayerノードをドラッグ&ドロップmax_hp: 例えば 100current_hp: 初期 HP(例: 100)flee_hp_ratio_threshold: 0.3(HP 30 以下で逃走開始)flee_speed: 300(プレイヤーより少し速いくらい)override_velocity: とりあえず true(他に移動がない場合)
③ ダメージ処理から HP を連動させる
既に Enemy に HP ロジックを持っているなら、そこから FleeBehavior の current_hp を更新してあげればOKです。
# Enemy.gd(Enemy: CharacterBody2D にアタッチされているスクリプトの例)
extends CharacterBody2D
@onready var flee_behavior: FleeBehavior = $FleeBehavior
var max_hp := 100.0
var hp := 100.0
func apply_damage(amount: float) -> void:
hp = max(hp - amount, 0.0)
# FleeBehavior にも HP を伝える
if flee_behavior:
flee_behavior.current_hp = hp
もし HP 処理をまるごと FleeBehavior に任せたいなら、敵のスクリプト側はこんな感じでも構いません。
func apply_damage(amount: float) -> void:
if flee_behavior:
flee_behavior.apply_damage(amount)
④ 他の AI と組み合わせる例
例えば、普段はプレイヤーを追いかける ChaseBehavior コンポーネントがあり、
HP が減ったら逃げるようにしたい場合、シーン構成はこうなります。
Enemy (CharacterBody2D) ├── Sprite2D ├── CollisionShape2D ├── ChaseBehavior (Node) └── FleeBehavior (Node)
この場合、
ChaseBehavior: 通常時にvelocityを設定FleeBehavior: HP が減ったときだけvelocityを「加算」する
というイメージで動かしたいので、FleeBehavior.override_velocity = false にしておきます。
これで「追いかけようとするけど、HP が減ると逆方向ベクトルが強くかかって逃げる」ような動きができます。
メリットと応用
FleeBehavior をコンポーネントとして切り出すことで、敵の AI 設計がかなりスッキリします。
- 継承地獄から解放
「逃げる敵用のクラス」「逃げない敵用のクラス」を分ける必要がなく、
どの敵シーンでもFleeBehaviorを子ノードとして付けるだけで逃走AIを追加できます。 - シーン構造が読みやすい
シーンツリーを見れば「この敵は FleeBehavior を持っているから HP が減ると逃げるんだな」と一目で分かります。 - レベルデザインが楽
フィールドごとに「この敵は臆病」「この敵は最後まで戦う」といったバリエーションを作るとき、
シーンインスタンス側でflee_hp_ratio_thresholdだけ変えればOKです。 - 他のコンポーネントとの合成がしやすい
追跡、巡回、射撃、逃走…といった行動をそれぞれコンポーネント化しておけば、
「追いかけるけど HP が減ったら逃げる遠距離タイプ」みたいな敵をノードの組み合わせだけで作れます。
「深い継承ツリー」より「薄いノード階層+コンポーネントの合成」のほうが、
後から挙動を差し替えたり、バリエーションを増やしたりするときに圧倒的に楽ですね。
改造案:逃走時にランダムなジグザグを加える
単純に逆方向へ一直線に逃げるだけだと、ちょっと機械的に見えることがあります。
そこで、逃走ベクトルに少しだけランダムな「ジグザグ成分」を加える改造案です。
func _get_flee_direction_with_wiggle(base_direction: Vector2, intensity: float = 0.3) -> Vector2:
## base_direction: ターゲットから見て「逃げるべき」基本方向(正規化済み)
## intensity: 0.0 ~ 1.0 くらいの範囲でジグザグの強さを指定
if intensity <= 0.0:
return base_direction
# -1.0 ~ 1.0 のランダム値を横方向に加える
var random_side := randf_range(-1.0, 1.0)
var side_vector := base_direction.rotated(sign(random_side) * PI * 0.5)
var mixed := (base_direction + side_vector * abs(random_side) * intensity).normalized()
return mixed
_process() 内で flee_direction を計算するときに、
var flee_direction: Vector2 = _get_flee_direction_with_wiggle(to_self.normalized(), 0.4)
のように差し替えると、「ちょっと蛇行しながら逃げる」ような挙動になって、
敵が少しだけ生き物っぽく見えるようになります。
このように、逃走ロジックがコンポーネントとして独立していると、
「逃げ方」だけを差し替える・バリエーションを増やすといった改造がやりやすくなりますね。
ぜひ自分のプロジェクト用に、FleeBehavior をベースにしたカスタム逃走AIを育ててみてください。
