実践ガイド

【Godot 4.7】Brotato風にレベルアップで強化を選ぶ:攻撃速度か弾ダメージを上げよう

Godotの2Dアリーナゲームで攻撃速度と弾ダメージの強化を選ぶ画面

【Godot 4.7】Brotato風にレベルアップで強化を選ぶ:攻撃速度か弾ダメージを上げよう

結論: レベルアップ時にゲームを一時停止し、「攻撃速度」か「弾のダメージ」から1つを選べるようにします。選んだ強化だけをプレイヤーへ反映し、選択が終わったらゲームを再開します。

このシリーズでは、Brotatoのようなアリーナ型ゲームで使われる仕組みを、Godotで一つずつ小さく再現していきます。前回はレベルアップしたときに攻撃間隔を短くしました。今回は、レベルアップのたびに自動で同じ強化を適用するのではなく、プレイヤーが強化を選ぶ流れを作ります。

  • 対象:前回記事のレベルアップと攻撃速度強化を追加したプロジェクトを動かせる人
  • 使用バージョン:Godot 4.7 系(4.7.x)
  • 完成状態:レベルアップ時に選択パネルを表示し、攻撃速度または弾ダメージを1つ強化できる

1. 今回作る流れ

敵を倒して必要な経験値がたまると、次の順番で処理します。

  1. プレイヤーのレベルが上がる
  2. level_up_requested シグナルを送る
  3. Main が強化パネルを表示する
  4. ゲームを一時停止する
  5. ボタンで「攻撃速度」または「弾のダメージ」を選ぶ
  6. 選んだ強化をプレイヤーへ適用する
  7. パネルを閉じてゲームを再開する

レベルアップと強化の適用を分けるのがポイントです。Player はレベルアップが起きたことを知らせ、Main は選択画面を管理し、選択後の具体的な数値変更は再び Player に任せます。プレイ中断が頻繁にならないよう、付属プロジェクトではレベルアップに必要な経験値を10にしています。

2. Playerに2種類の強化値を持たせる

まず player.gd に、攻撃間隔と弾のダメージを保存します。

const START_ATTACK_INTERVAL := 0.8
const ATTACK_INTERVAL_UPGRADE := 0.2
const MIN_ATTACK_INTERVAL := 0.2
const START_BULLET_DAMAGE := 1
const BULLET_DAMAGE_UPGRADE := 1

var attack_interval := START_ATTACK_INTERVAL
var bullet_damage := START_BULLET_DAMAGE

攻撃速度はTimerの待ち時間です。値が小さいほど短い間隔で弾を発射します。一方、bullet_damage は弾1発が敵へ与えるダメージです。

強化後の値をHUDへ伝えられるよう、stats_changed の引数にもダメージを追加します。

const EXPERIENCE_TO_LEVEL_UP := 10

signal stats_changed(
	level: int,
	experience: int,
	experience_to_next: int,
	attack_interval: float,
	bullet_damage: int
)
signal level_up_requested

シグナルの宣言、接続する側、受け取る関数で引数の数を揃える必要があります。今回は攻撃間隔だけだったシグナルに、5番目の引数として bullet_damage を追加します。

3. レベルアップ時に選択を要求する

経験値が必要量に達したら、レベルを上げたあとで level_up_requested を送ります。

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()
	stats_changed.emit(
		level,
		experience,
		EXPERIENCE_TO_LEVEL_UP,
		attack_interval,
		bullet_damage
	)

この例は敵1体につき経験値1を受け取り、一度に1レベルずつ上がる前提です。大量の経験値で複数レベルが一度に上がると、シグナルが続けて発行されてもパネルは1回しか表示されません。複数回の選択が必要な場合は、未処理の強化回数を記録し、1回選ぶごとに次の選択を表示する仕組みを追加してください。ここでは、レベルが上がった直後に攻撃速度を自動変更していません。選択が終わるまで、現在の attack_interval と bullet_damage を保ちます。

4. 2つの強化処理を関数に分ける

プレイヤー側に、ボタンから呼び出す2つの関数を用意します。

func apply_attack_speed_upgrade() -> void:
	attack_interval = maxf(
		MIN_ATTACK_INTERVAL,
		attack_interval - ATTACK_INTERVAL_UPGRADE
	)
	$AttackTimer.wait_time = attack_interval
	stats_changed.emit(
		level,
		experience,
		EXPERIENCE_TO_LEVEL_UP,
		attack_interval,
		bullet_damage
	)


func apply_damage_upgrade() -> void:
	bullet_damage += BULLET_DAMAGE_UPGRADE
	stats_changed.emit(
		level,
		experience,
		EXPERIENCE_TO_LEVEL_UP,
		attack_interval,
		bullet_damage
	)

攻撃速度を上げるときだけ AttackTimer.wait_time を書き換えます。ダメージ強化ではTimerを変更せず、次に生成する弾へ新しいダメージ値を渡します。

5. 弾へ現在のダメージを渡す

これまで bullet.gd のダメージが定数だけだった場合は、実行時に変更できる変数へ置き換えます。

const DAMAGE := 1

var damage := DAMAGE

player.gd の弾生成処理で、プレイヤーが持つ現在のダメージを代入します。

var bullet: Bullet = BULLET_SCENE.instantiate()
get_parent().add_child(bullet)
bullet.global_position = global_position
bullet.damage = bullet_damage
bullet.direction = (target.global_position - global_position).normalized()

この代入を入れないと、HUDのダメージ表示だけが2になっても、実際の弾はずっと1ダメージのままです。強化値の表示とゲーム内の効果を同じ処理につなげる必要があります。

敵側では、受け取ったダメージをそのまま take_damage() へ渡します。

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()

6. Mainに強化パネルを追加する

main.tscn のHUDに、次のような PanelContainer を追加します。

Main
├─ Player
├─ SpawnTimer
└─ HUD
   ├─ StatusLabel
   └─ UpgradePanel
      └─ Layout
         ├─ Title
         ├─ Hint
         ├─ AttackSpeedButton
         └─ DamageButton

2つのボタンの表示文は、次のようにします。

  • AttackSpeedButton:攻撃速度を上げる
  • DamageButton:弾のダメージを上げる

パネルは通常は非表示にし、ゲームが一時停止してもボタンを押せるようにします。

[node name="UpgradePanel" type="PanelContainer" parent="HUD"]
process_mode = 3
visible = false

process_mode = 3 は、親の停止状態に関係なくこのUIを処理する設定です。これがないと、ゲームを一時停止したあとにボタンの入力まで止まることがあります。

7. パネル表示と一時停止をMainで管理する

main.gd でパネルを取得し、レベルアップのシグナルへ接続します。

@onready var upgrade_panel: PanelContainer = $HUD/UpgradePanel


func _ready() -> void:
	player.stats_changed.connect(_on_player_stats_changed)
	player.level_up_requested.connect(_on_player_level_up_requested)
	_on_player_stats_changed(
		player.level,
		player.experience,
		player.EXPERIENCE_TO_LEVEL_UP,
		player.attack_interval,
		player.bullet_damage
	)


func _on_player_level_up_requested() -> void:
	upgrade_panel.visible = true
	get_tree().paused = true

選択肢を表示する責任は Main に置きます。Player が直接UIを操作すると、プレイヤーの計算処理と画面レイアウトが密接に結びついてしまうためです。

8. ボタンを押したら強化してゲームを再開する

ボタンのシグナルを Main へ接続し、対応する強化関数を呼び出します。

func _on_attack_speed_button_pressed() -> void:
	player.apply_attack_speed_upgrade()
	_close_upgrade_panel()


func _on_damage_button_pressed() -> void:
	player.apply_damage_upgrade()
	_close_upgrade_panel()


func _close_upgrade_panel() -> void:
	upgrade_panel.visible = false
	get_tree().paused = false

順番は「強化を適用する」→「パネルを閉じる」→「ゲームを再開する」です。再開前に値を変更するので、再開したフレームから新しい強化値を使えます。

9. HUDに現在の強化値を表示する

main.gd の表示更新関数では、攻撃間隔とダメージの両方を表示します。

func _on_player_stats_changed(
	level: int,
	experience: int,
	experience_to_next: int,
	attack_interval: float,
	bullet_damage: int
) -> void:
	status_label.text = "Level %d  XP %d/%d  Attack %.1fs  Damage %d" % [
		level,
		experience,
		experience_to_next,
		attack_interval,
		bullet_damage
	]

開始時は次のように表示されます。

Level 1  XP 0/10  Attack 0.8s  Damage 1

ダメージを選んだ場合は、次のようになります。

Level 2  XP 0/10  Attack 0.8s  Damage 2

攻撃速度を選んだ場合は、攻撃間隔だけが変わります。

Level 2  XP 0/10  Attack 0.6s  Damage 1

10. 実行して強化を選ぶ

F5で実行し、次の順番で確認します。

  1. 開始時に Level 1 XP 0/10 Attack 0.8s Damage 1 と表示される
  2. 自動攻撃で敵を倒し、経験値を10まで増やす
  3. レベルアップ時に「レベルアップ:強化を1つ選択」パネルが表示される
  4. パネル表示中は、敵やプレイヤーの動きが一時停止する
  5. 「弾のダメージを上げる」を押す
  6. パネルが閉じ、ゲームが再開する
  7. HUDが Damage 2 になり、攻撃間隔は 0.8s のままになる

もう一度確認するときは、プロジェクトを再起動して「攻撃速度を上げる」を選びます。こちらではHUDが Attack 0.6s、Damage 1 になります。

11. うまくいかないときの確認ポイント

  • レベルアップしてもパネルが出ない:level_up_requested の宣言、emit()、connect()、_on_player_level_up_requested() の名前が一致しているか確認します。
  • パネルは出るがボタンを押せない:UpgradePanel の process_mode = 3 を確認します。ゲームを一時停止してもUIは処理できる必要があります。
  • 起動するがプレイヤーが動かない:main.tscn の Main が「一時停止中のみ処理」になっていないか確認します。親ノードがこの設定だと、Player が通常プレイ中の物理処理を継承して止まります。Main は通常処理、UpgradePanel だけを一時停止中も処理する設定にします。
  • ボタンを押してもゲームが再開しない:_close_upgrade_panel() で get_tree().paused = false を実行しているか確認します。
  • HUDのダメージだけ変わり、敵がすぐ倒れない:弾を生成するときに bullet.damage = bullet_damage を代入しているか確認します。
  • 攻撃速度を選んでも間隔が変わらない:apply_attack_speed_upgrade() で $AttackTimer.wait_time を更新しているか確認します。
  • 起動時にシグナルエラーが出る:stats_changed の宣言、connect()、_on_player_stats_changed() の引数5つが一致しているか確認します。
  • 矢印キーを押しても動いていないように見える:Camera2D がプレイヤーを画面中央に追従させています。ワールド軸や敵との相対位置を見てください。入力が届かない場合は、実行中のゲーム画面を一度クリックしてから矢印キーを押します。

12. 付属プロジェクトの検証結果

検証時の auto_attack_choice_smoother_verified.zip は、前回の攻撃速度強化プロジェクトをコピーして作成しました。元のプロジェクトは変更していません。段階版ZIPは現在この記事からダウンロードできません。

Godot 4.7 stable で次を確認しています。

  • プロジェクトとメインシーンを読み込める
  • メインシーンを4秒間起動できる
  • 矢印キー相当の入力でプレイヤーが移動する
  • 敵を倒すとプレイヤーの経験値が1増える
  • 10経験値でレベル2になり、強化パネルが表示される
  • 強化パネル表示中にゲームが一時停止する
  • ダメージ強化で bullet_damage が1から2になり、ゲームが再開する
  • 攻撃速度強化で attack_interval が0.8秒から0.6秒になり、ゲームが再開する
  • 選択後にHUDが現在の値へ更新される

これはコードとヘッドレス実行による確認です。画面の色、ボタンの配置、実際のキー入力は、Godotでプロジェクトを開いて画面でも確認してください。

13. まとめと次の一歩

  • レベルアップ時にゲームを一時停止し、強化を選ぶ画面を追加しました。
  • level_up_requested シグナルで、レベルアップ処理とUI表示を分離しました。
  • 攻撃速度と弾ダメージを別の関数で管理し、選んだ強化だけを反映しました。
  • UIを一時停止中も動かすには、パネルの process_mode が重要です。
  • 次は、強化候補を毎回ランダムに3つ表示したり、弾の数や移動速度を選べるようにしたりできます。

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

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

統合版のstats_changedは辞書を1つ渡し、候補は最大3件のランダム表示です。パネルの配置もこの記事の段階版と異なります。統合版には後続記事の機能も含まれます。記事の機能を確認するときは該当する操作を先に試し、最後にGodot 4.7でF5起動、入力、セーブ・ロード、再起動後の復元を確認してください。

← 記事一覧・検索