実践ガイド

【Godot 4.7】Brotato風ゲームに経験値ジェムを追加する:敵を倒した場所で成長しよう

【Godot 4.7】Brotato風ゲームに経験値ジェムを追加する:敵を倒した場所で成長しようのアイキャッチ

結論: 敵を倒した瞬間に経験値を直接加算する処理を、経験値ジェムのドロップへ置き換えます。プレイヤーがジェムに触れたときだけ経験値を受け取る形にすると、敵を倒す場所と拾いに行く判断がゲームに加わります。

このシリーズでは、Brotatoのようなアリーナ型ゲームの仕組みを、Godotで一つずつ小さく再現しています。前回は、敵の撃破時に回復アイテムを落とし、プレイヤーが拾うとHPを回復する処理を追加しました。

現在の enemy.gd では、敵が倒れた瞬間にプレイヤーの gain_experience() を呼んでいます。今回はこの直接加算をやめ、青い経験値ジェムを落とします。プレイヤーがジェムに触れると経験値が1増え、既存のレベルアップ処理が動きます。

ジェムを拾うために少し移動すると、敵の群れへ近づきすぎる危険もあります。敵を倒せばすぐに経験値が増える仕組みでは攻撃範囲の外へ出る理由がありませんが、ジェムを地面に残すと、回収のために進路を変える場面が生まれます。

この小さな寄り道が、プレイヤーの移動速度、敵の出現位置、残りHP、次のレベルまでの経験値を一度に確認するきっかけになり、数字を増やすだけの処理から、位置取りを考えるプレイへつながります。

実際のプレイでは、プレイヤーの周囲に敵が集まっているとき、ジェムを拾うために進む方向を変えるだけで接触ダメージを受ける可能性が高まり、HPが少ないときは回復アイテムの位置やゲームオーバーまでの距離も同時に考えながら、あえて経験値を後回しにする判断が必要になります。

  • 対象:前回記事の回復アイテムまで追加したプロジェクト
  • 使用バージョン:Godot 4.7 系(4.7.x)
  • 完成状態:敵の撃破位置に経験値ジェムが出現し、拾うと経験値が増える

1. 直接加算からジェム取得へ役割を分ける

今回の処理は、次の3つに分けます。

担当 やること
Enemy 撃破された場所に経験値ジェムを生成する
ExperienceGem プレイヤーとの接触を検知し、経験値を渡す
Player gain_experience() で経験値とレベルを管理する

プレイヤーが経験値の数値を直接増やすのは、player.gd の gain_experience() だけにします。ジェムは経験値の管理方法を知らず、「1経験値を渡す」役割だけを持ちます。

この順番を分けておくと、敵を倒したのに経験値が増えない、ジェムは表示されるのに拾えない、ジェムを拾ったのにレベルアップ画面が出ない、といった問題を、Enemy、ExperienceGem、Playerのどこで止まっているかに分けて調べられます。

たとえば、HPが少なく、敵が近く、次のレベルまであと1経験値という場面では、目の前のジェムを拾えばレベルアップできる一方で、移動中に接触ダメージを受ける危険もあるため、回復アイテムの位置、敵の進行方向、画面端までの距離、攻撃が次に向く敵の位置を確認しながら、拾う順番と逃げる方向を決めることになります。

この分け方なら、後から経験値の量が違うジェムを追加しても、変更するのはジェム側の定数だけです。

2. ExperienceGemのシーンを作る

ファイルシステムで新しいシーンを作り、ルートノードを Area2D にします。ルート名は ExperienceGem にしてください。

ExperienceGem (Area2D)
├── Sprite2D
└── CollisionShape2D

Sprite2D にはプロジェクトの icon.svg を設定し、青色または水色にします。回復アイテムと見分けられるように、色を変えることが大切です。大きすぎる場合はScaleを 0.16 前後に下げます。

CollisionShape2D には CircleShape2D を設定し、半径を10程度にします。

衝突レイヤーは、回復アイテムと同じ設定にします。

設定 値 意味
Collision Layer 0 ジェム自身を他のArea2Dへ見せない
Collision Mask 1 プレイヤーの標準レイヤーを検知する

プレイヤーのCollision Layerを変更している場合は、Collision Mask をその値へ合わせてください。

3. ジェムが拾われたら経験値を渡す

experience_gem.gd を作り、ExperienceGem にアタッチします。

extends Area2D

const EXPERIENCE_VALUE := 1


func _ready() -> void:
	body_entered.connect(_on_body_entered)


func _on_body_entered(body: Node2D) -> void:
	if not body.is_in_group("player"):
		return
	if not body.has_method("gain_experience"):
		return

	body.call("gain_experience", EXPERIENCE_VALUE)
	queue_free()

body_entered は、ジェムの範囲にプレイヤーのようなBody2Dが入ったときに発生します。プレイヤーは既存の player グループへ登録されているため、グループ名で対象を判定します。

経験値を渡したあとに queue_free() でジェムの削除を予約します。queue_free() は即時削除ではなくフレーム終了時の削除予約です。Godot公式の説明

4. シーンファイルの参照を確認する

エディタから作成した experience_gem.tscn は、最終的に次のような構成になります。IDはGodotが自動で割り当てる値と異なっていても構いません。

[gd_scene load_steps=4 format=3]

[ext_resource type="Script" path="res://experience_gem.gd" id="1_gem"]
[ext_resource type="Texture2D" path="res://icon.svg" id="2_icon"]

[sub_resource type="CircleShape2D" id="CircleShape2D_gem"]
radius = 10.0

[node name="ExperienceGem" type="Area2D"]
collision_layer = 0
collision_mask = 1
script = ExtResource("1_gem")

[node name="Sprite2D" type="Sprite2D" parent="."]
modulate = Color(0.3, 0.8, 1, 1)
scale = Vector2(0.16, 0.16)
texture = ExtResource("2_icon")

[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_gem")

手書きする場合は、res://experience_gem.gd と res://icon.svg が実際のファイル名と一致しているか確認します。エディタから作る場合は、シーンを experience_gem.tscn として保存してください。

5. Enemyから経験値の直接加算を外す

前回までの enemy.gd には、敵が倒れたときにプレイヤーへ経験値を渡す処理があります。

var player: Node = get_tree().get_first_node_in_group("player")
if player != null and player.has_method("gain_experience"):
	player.call("gain_experience", EXPERIENCE_VALUE)

このブロックは削除します。残したままジェムも生成すると、敵を倒した瞬間とジェムを拾った瞬間の2回、経験値が増えてしまいます。

代わりに、enemy.gd の定数へジェムのシーンを読み込みます。

const EXPERIENCE_GEM_SCENE := preload("res://experience_gem.tscn")

回復アイテムの記事で RECOVERY_ITEM_SCENE を追加済みなら、同じ場所に並べてください。

6. 敵の撃破位置にジェムを生成する

enemy.gd に、ジェムを生成する関数を追加します。

func _drop_experience_gem() -> void:
	var gem: Area2D = EXPERIENCE_GEM_SCENE.instantiate()
	get_parent().add_child(gem)
	gem.global_position = global_position

敵の親ノードへジェムを追加してから、敵と同じグローバル座標を設定します。get_parent() を使うことで、敵が Main の子として生成されている現在のシーン構成にそのまま置けます。

既存の take_damage() のHP 0の処理を次の形へ変更します。

func take_damage(amount: int) -> void:
	if is_queued_for_deletion():
		return
	hp -= amount
	if hp > 0:
		return

	_drop_experience_gem()
	_drop_recovery_item()
	queue_free()

前回の回復アイテムをまだ追加していない場合は、_drop_recovery_item() の行を削除してください。回復アイテムを追加済みなら、敵1体からジェムと回復アイテムの両方が出る構成になります。

敵を削除する queue_free() より前にドロップ処理を呼び出します。削除予約済みかを最初に確認することで、同じフレーム内に複数の弾が当たってもドロップが重複しないようにします。

敵を倒す場所がプレイヤーから遠いほど、ジェムを回収するための移動時間が増えます。目の前の敵を倒し続けるか、落ちたジェムを取りに戻るかという判断が生まれ、経験値を数字として受け取るだけだった状態に、アリーナを移動する理由が加わります。

7. 既存の経験値とレベルアップをそのまま使う

ジェムが呼ぶのは、プレイヤーに既にある gain_experience() です。現在の処理が次の形なら、基本的に変更はありません。

func gain_experience(amount: int) -> void:
	if amount <= 0:
		return

	experience += amount
	while experience >= EXPERIENCE_TO_LEVEL_UP:
		experience -= EXPERIENCE_TO_LEVEL_UP
		level += 1
		level_up_requested.emit()
	_emit_stats_changed()

経験値ジェムは、経験値を直接変更せず関数を呼び出します。そのため、経験値テーブルをレベルに応じて増やす記事の変更や、レベルアップ時の強化選択も引き続き利用できます。

gain_experience() がレベルアップの通知を出すため、ジェムを拾った瞬間に既存の選択パネルが表示されます。一度に複数レベル分の経験値を与える場合は、レベルアップ通知が複数回発行されます。選択画面が1回にまとめられないよう、未処理の選択回数を管理する仕組みが別途必要です。また、必要経験値がレベルで変わる実装では、この固定値の例をそのまま使わず、各レベルの必要量を再計算してください。

8. 近くに落ちたジェムを拾いやすくする

最初の段階では、プレイヤーがジェムへ移動して拾うだけで十分です。ジェムが小さくて拾いにくい場合は、CollisionShape2D の半径を14へ広げます。

敵がプレイヤーのすぐ近くで倒れるゲームでは、ジェムが自動的に拾われることもあります。これは、敵の撃破位置とプレイヤーの位置が近いためです。

あとから「経験値を吸い寄せる」機能を追加する場合は、ジェム側でプレイヤーとの距離を調べて移動させます。今回の記事では、まずArea2Dの接触だけに絞り、拾う処理が動くことを確認します。

まずは1個のジェムで、倒した場所に出現して拾えることを確認します。

9. よくあるエラーを確認する

ジェムが表示されない

EXPERIENCE_GEM_SCENE のパスが正しいか確認します。experience_gem.tscn の名前や保存場所が違うと、preload() でエラーになります。

ジェムが表示されるのに拾えない

CollisionShape2D にShapeが設定されているか、Collision Mask がプレイヤーのレイヤーを含んでいるか確認します。アイテムと同じく、Area2Dの body_entered が接続されていることも必要です。

敵を倒した瞬間に経験値が増える

enemy.gd に残っている直接加算のブロックを削除します。ジェムの生成と直接加算を同時に使うと、経験値が二重になります。

レベルアップ画面が出ない

ジェムを拾ったあとに gain_experience() が呼ばれているかを確認します。経験値が必要量に達していない場合は、レベルアップ画面は表示されません。敵を複数体倒して、必要経験値の分だけジェムを拾ってください。

10. 実行して確認する

今回の変更は、Godot 4.7で次の順番に確認します。

  1. 前回記事のプロジェクトを複製する
  2. ExperienceGem のシーンとスクリプトを作成する
  3. enemy.gd の直接経験値加算を削除する
  4. 敵の撃破時に _drop_experience_gem() を呼ぶ
  5. F5でメインシーンを実行する(F6は現在開いているシーンを実行)
  6. 敵を倒し、敵の位置に青いジェムが出ることを確認する
  7. プレイヤーをジェムへ移動させる
  8. ジェムが消え、HUDの経験値が1増えることを確認する
  9. 必要経験値までジェムを拾い、レベルアップ画面が表示されることを確認する
  10. レベルアップの選択後も、残ったジェムを拾えることを確認する
  11. 回復アイテムも追加済みなら、ジェムと回復アイテムが別々に働くことを確認する

ドロップ直後にジェムが消える場合は、敵とプレイヤーが近すぎる可能性があります。まず、ジェムの色とHP表示の変化を確認し、必要なら敵の撃破位置から少し離して生成します。

テストでは、敵を倒した瞬間の経験値が増えないこと、ジェムへ触れたあとにだけ経験値が増えること、必要量に達したときに既存の選択パネルが開くことを順番に確認し、直接加算を残していないかと、Area2Dの衝突設定がプレイヤーの移動中にも働くかを分けて見ます。

11. まとめ

今回は、敵の撃破時に経験値を直接加算せず、経験値ジェムを落とす形へ変更しました。

  • Area2D のジェムで拾う動作を検知する
  • ジェムは gain_experience() を呼ぶだけにする
  • 敵の撃破位置にジェムを生成する
  • 直接加算を残さず、経験値の二重取得を防ぐ
  • 既存のレベルアップと強化選択をそのまま利用する

これで、敵を倒したあとにジェムを拾いに行く流れが生まれました。次はジェムをプレイヤーへ吸い寄せる磁石効果を追加すると、敵の数が増えた後も経験値を回収しやすくなります。

参考:Godot 4とGDScriptの入門書

Godot 4とGDScriptの基本から2Dゲーム開発を学びたい場合は、今回のようなシーン構成やスクリプトの分割を体系的に確認できる書籍も参考になります。

※本記事にはアフィリエイトリンクが含まれています。

Godot 4 & GDScriptではじめる 2Dゲーム開発レシピ(Amazonで詳細を見る)

※本リンクはアフィリエイトリンクです。

統合版プロジェクトとの対応

この記事で扱う機能は、企画全体をまとめたauto_attack_final_verified.zipにも組み込んでいます。この記事のコードは段階的に追加するための説明なので、途中の記事と統合版全体のファイル構成は一致しない場合があります。

統合版ではMainがジェムを生成し、接触シグナルではなく距離20以下で取得します。衝突マスクは0で、この記事のbody_enteredと衝突設定は途中段階の例です。同時ヒット時の二重ドロップは実行確認が必要です。統合版には後続記事の機能も含まれます。記事の機能を確認するときは該当する操作を先に試し、最後にGodot 4.7でF5起動、入力、セーブ・ロード、再起動後の復元を確認してください。

← 記事一覧・検索