※本記事にはアフィリエイトリンクが含まれています。
結論: 敵を一発で消す処理を、HPとダメージのやり取りに分けます。敵は自分のHPを持ち、弾は「1ダメージを与える」とだけ伝えます。この記事では、前回作った自動攻撃に、敵のHP 3と弾のダメージ1を追加します。
このシリーズでは、Brotatoのようなアリーナ型ゲームで使われる仕組みを、Godotで一つずつ小さく再現していきます。今回はその続きとして、敵のHPと弾のダメージを扱います。
- 対象:前回記事のプロジェクトを動かせる人
- 使用バージョン:Godot 4.7 系(4.7.x)
- 完成状態:敵が弾を3発受けると消える
1. 今回は「敵を消す」から「敵にダメージを与える」へ変える
前回の bullet.gd では、弾が敵に当たったら queue_free() を呼び、敵をすぐ削除していました。これだと、敵の強さを変えたり、武器ごとにダメージを変えたりできません。
今回は役割を2つに分けます。
| 担当 | やること |
|---|---|
| Enemy | HPを持ち、ダメージを受けたら減らす。0以下なら自分を削除する |
| Bullet | 敵にダメージ量を渡し、自分を削除する |
この分け方にすると、敵を「3発で倒れる敵」から「10発で倒れる敵」へ変えるとき、敵側のHPだけを変えれば済みます。
2. 変更するファイルは2つだけ
今回編集するのは、次の2ファイルです。
enemy.gd:HPとダメージ処理を追加するbullet.gd:敵を直接削除せず、ダメージを渡す
enemy.tscn と bullet.tscn のシーン構成、AttackTimer、敵のグループ設定は前回のまま使います。プロジェクトを新しく作り直す必要はありません。
3. EnemyにHPとダメージ処理を追加する
3-1. HPの初期値を決める
enemy.gd の SPEED の下に、敵の最大HPを表す定数と、現在HPを追加します。
const SPEED := 100.0
const MAX_HP := 3
var hp := MAX_HP
MAX_HP は変更しない値なので const にしています。hp はゲーム中に減る値なので var にします。
今回は、弾を3発受けると倒れるように MAX_HP を 3 にしました。数字を 5 にすれば、5発受けるまで倒れない敵になります。
3-2. take_damage 関数を作る
_ready() の下に、次の関数を追加します。
func take_damage(amount: int) -> void:
hp -= amount
if hp <= 0:
queue_free()
take_damage は「ダメージを受ける」という意味の関数です。引数 amount で受け取った数だけ hp を減らし、0以下になったときだけ敵を削除します。
この関数を使うと、弾側は敵のHPを直接書き換えなくて済みます。敵のHPの管理場所が enemy.gd にまとまるため、後で回復や防御力を足すときにも変更箇所を追いやすくなります。
3-3. enemy.gd を全体で確認する
ここまでの変更後は、enemy.gd 全体が次の形になります。
extends CharacterBody2D
const SPEED := 100.0
const MAX_HP := 3
var hp := MAX_HP
func _ready() -> void:
add_to_group("enemies")
func take_damage(amount: int) -> void:
hp -= amount
if hp <= 0:
queue_free()
func _physics_process(_delta: float) -> void:
var player: Node2D = get_tree().get_first_node_in_group("player")
if player == null:
return
velocity = (player.global_position - global_position).normalized() * SPEED
move_and_slide()
add_to_group("enemies") は消さないでください。プレイヤーと弾が敵を見つけるために使っています。
4. Bulletからダメージを渡す
4-1. 弾のダメージ量を決める
bullet.gd の SPEED の下に、ダメージ量を追加します。
const SPEED := 600.0
const DAMAGE := 1
弾1発のダメージを 1 にしています。武器を強くしたいときは、まずこの数字を変えるだけでも動作を確認できます。
4-2. 敵を直接削除しない
_on_body_entered の中にある次の処理を探します。
body.queue_free()
この行を削除し、代わりに take_damage を呼びます。
if body.is_in_group("enemies"):
if body.has_method("take_damage"):
body.call("take_damage", DAMAGE)
queue_free()
has_method("take_damage") は、相手にその関数があるか確認する処理です。call は、ノードが持っている関数を名前で呼び出します。
敵にダメージを渡した後、弾自身は queue_free() で削除します。弾が敵を貫通する仕様にしたい場合は、ここを後で変更できますが、今回は「1発当たると1ダメージ」という分かりやすい動きにします。
4-3. bullet.gd を全体で確認する
変更後の bullet.gd は次の形です。
class_name Bullet
extends Area2D
const SPEED := 600.0
const DAMAGE := 1
var direction := Vector2.ZERO
func _ready() -> void:
get_tree().create_timer(3.0).timeout.connect(queue_free)
func _physics_process(delta: float) -> void:
position += direction * SPEED * delta
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("enemies"):
if body.has_method("take_damage"):
body.call("take_damage", DAMAGE)
queue_free()
body_entered シグナルの接続は、前回の設定をそのまま使います。コードを書き換えただけでシグナルが自動接続されるわけではないため、弾が敵に当たらない場合は bullet.tscn の接続も確認してください。
5. F5で動かして、3発目で敵が消えるか確認する
enemy.gdを保存します。bullet.gdを保存します。- F5キーでメインシーンを実行します。
- ゲーム画面を一度クリックしてから、矢印キーでプレイヤーを動かします。
- 敵がプレイヤーへ近づき、弾が自動で飛ぶのを待ちます。
- 同じ敵に弾が当たるたび、敵が少しずつ耐えます。
- 3発目が当たると、その敵が消えます。
攻撃間隔は前回と同じ AttackTimer の 0.8 秒です。敵が3発受ける前に別の敵へ弾が飛ぶこともあるため、最初は敵の動きがゆっくり見える位置で確認すると分かりやすくなります。
6. 動かないときの確認ポイント
- 敵が一発で消える:
bullet.gdにbody.queue_free()が残っていないか確認します。 - 敵のHPが減らない:
enemy.gdにtake_damageがあり、名前の綴りがbody.call("take_damage", DAMAGE)と一致しているか確認します。 - 弾だけ消えて敵が無傷:
take_damageメソッドの有無と綴り、対象スクリプトを確認します。has_methodが false の場合は弾だけが消えます。敵がenemiesグループに入っていない場合は、この接触では弾も消えず、ダメージも与えません。 - 弾が敵に当たらない:
bullet.tscnのbody_entered接続と、Bullet・Enemy両方のCollisionShape2Dを確認します。 - 矢印キーを押しても動いていないように見える:
Camera2Dがプレイヤーを画面中央に追従させています。ワールド軸や敵の相対位置を見てください。実行中のゲーム画面をクリックしてから入力することも大切です。
7. 今回の小さな分離が、後の拡張を楽にする
敵の削除を弾の仕事にしてしまうと、敵のHP、無敵時間、死亡演出を弾側へ書き足すことになります。今回のように敵が take_damage を持っていれば、敵のルールを敵自身に置けます。
たとえば次の拡張は、今回の構造から自然につなげられます。
- 敵ごとに
MAX_HPを変える - ダメージを受けたときだけ色を点滅させる
- HPが0になったときにコインや経験値を生成する
- 弾ごとに
DAMAGEや速度を変える
一度に全部を足す必要はありません。まずは「3発受けたら消える」が確認できれば、HPの土台は完成です。
8. 付属プロジェクトの検証結果
検証時の auto_attack_hp_verified.zip は、前回の検証済みプロジェクトをコピーして作成しました。段階版ZIPは現在この記事からダウンロードできません。元のプロジェクトは変更していません。
Godot 4.7 stable で次を確認しています。
- プロジェクトとメインシーンを読み込める
- メインシーンを4秒間起動できる
- 敵の初期HPが3になる
take_damage(1)でHPが2になる- 弾を2発追加で当てるとHPが0になり、敵が削除される
- 弾自身も命中後に削除される
画面上の色、敵の見え方、エディタのメニュー名は、利用しているGodotの表示設定によって異なる場合があります。実機で確認した内容と、コードから確認した内容を分けて扱ってください。
9. まとめと次の一歩
- 敵のHPは
enemy.gdに置き、take_damageで減らします。 - 弾は敵を直接削除せず、ダメージ量を渡します。
MAX_HP = 3とDAMAGE = 1なら、3発で敵を倒せます。- 次は、敵を倒したときに経験値を落とし、プレイヤーがレベルアップする仕組みへ進められます。
10. さらに学ぶ
今回の内容をもう少し体系的に学びたい場合は、Godot 4とGDScriptによる2Dゲーム開発を扱う書籍も参考になります。
Godot 4 & GDScriptではじめる 2Dゲーム開発レシピ(Amazonで詳細を見る)
※本リンクはアフィリエイトリンクです。
統合版プロジェクトとの対応
この記事で扱う機能は、企画全体をまとめたauto_attack_final_verified.zipにも組み込んでいます。この記事のコードは段階的に追加するための説明なので、途中の記事と統合版全体のファイル構成は一致しない場合があります。
統合版では、通常型・ウェーブ1・強化前なら3発ですが、高速型やウェーブ、強化によって必要な命中回数が変わります。統合版には後続記事の機能も含まれます。記事の機能を確認するときは該当する操作を先に試し、最後にGodot 4.7でF5起動、入力、セーブ・ロード、再起動後の復元を確認してください。
