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 / CharacterBody3Dvelocity を書き換えて移動させる

「逃走するかどうかの判断」と「逃走ベクトルの計算」だけを担当し、
ダメージ処理アニメーション攻撃ロジック は一切持ちません。
こうすることで、どんな敵にも「逃走行動」だけを後付けできるようになります。


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: 例えば 100
  • current_hp: 初期 HP(例: 100)
  • flee_hp_ratio_threshold: 0.3(HP 30 以下で逃走開始)
  • flee_speed: 300(プレイヤーより少し速いくらい)
  • override_velocity: とりあえず true(他に移動がない場合)

③ ダメージ処理から HP を連動させる

既に Enemy に HP ロジックを持っているなら、そこから FleeBehaviorcurrent_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を育ててみてください。