🎮

Godotでプラットフォームゲーム基礎作成 (Godot 4.3 対応)その1

2024/10/13に公開

1. はじめに

Godotを使ってプラットフォームゲームを作成するための基本的な要素と手順を紹介します。
初心者を対象に、ゲーム開発の基本を学びながら、自分で簡単なプラットフォームゲームを作成できるようになります。
Youtubeでもこの記事の内容に沿ってゲームを作っているので動画を見ながら取り組んでみてください。

https://youtu.be/C3LLw0ONT5A

チャンネル登録していただけると嬉しいです。

環境

今回の動画では下記のバージョンを前提にゲーム作成を行います。

  • Godot:4.3

2. プロジェクトの設定

それではまず初めにプロジェクトの設定をしましょう。

画面の設定

表示 > ウィンドウ から下記の通り設定します。

項目
ピューポートの幅 1280
ピューポートの高さ 720
ストレッチ canvas_items

テクスチャフィルタの設定

レンダリング > テクスチャ からデフォルトのテクスチャフィルタをNearestにします。
ドット絵をメインで扱う場合はデフォルト設置だとボヤけてしまうのでこちらを設定します。

レイヤーの名前設定

次にレイヤーの名称を設定します。レイヤーに名前をつけることでどのコリジョン(当たり判定)の管理がしやすくなります。

Layer Names > 2D物理より設定してください。

項目
Layer 1 プラットフォーム
Layer 2 プレイヤーの体
Layer 3 プレイヤーの攻撃範囲
Layer 4 敵の体
Layer 5 敵の攻撃範囲

入力キーの追加

基本的にはデフォルトのキーコンフィグを利用しますが、攻撃処理のキーの割り当てがないのと、ジョイパットにも対応できるように設定を追加します。
デフォルトのアクションに追加する場合は組み込みアクションを表示のトグルをONにします。
今回は新しくui_attackというアクションを追加し、Zキーを設定します。
またSwitchのコントローラーに対応するようにジョイパッドの設定を追加します。


3. ディレクトリ構成の説明

Godotプロジェクトのディレクトリ構成は、プロジェクトの整理と管理を容易にするために重要です。
以下は、一般的なディレクトリ構成の例です。

my_platformer_game/
├── assets/
│   ├── sprites/
│   │   ├── player.png
│   │   └── enemy.png
│   ├── tiles/
│   │   └── tileset.png
│   ├── sounds/
│   │   ├── jump.wav
│   │   └── bgm.ogg
│   └── fonts/
│       └── font.tres
├── scenes/
│   ├── main_stage.tscn
│   ├── player.tscn
│   └── enemy.tscn
├── scripts/
│   ├── player.gd
│   ├── enemy.gd
│   └── main.gd
├── project.godot
└── README.md

ディレクトリの詳細

  • assets: ゲームで使用するすべてのアセット(画像、音声、フォントなど)を格納します。
    • sprites: スプライト画像を格納します。
    • tiles: タイルセット画像を格納します。
    • sounds: 効果音やBGMを格納します。
    • fonts: フォントファイルを格納します。
  • scenes: ゲームのシーンファイルを格納します。各シーンは .tscn ファイルとして保存されます。
  • scripts: ゲームのスクリプトファイルを格納します。各スクリプトは .gd ファイルとして保存されます。
  • project.godot: Godotプロジェクトファイルです。プロジェクトの設定が保存されています。
  • README.md: プロジェクトの説明や使用方法を記載したファイルです。

今回の最終的なディレクトリ構成

今回のチュートリアルを完了させると最終的には下記のディレクトリ構成になります。

my_platformer_game/
├── assets
│   └── sprites
│       ├── character
│       │   ├── enemy
│       │   │   └── enemy.png
│       │   └── player
│       │       ├── player_attack.png
│       │       ├── player_die.png
│       │       ├── player_hurt.png
│       │       ├── player_idle.png
│       │       ├── player_jump.png
│       │       └── player_walk.png
│       └── tiles
│           └── tileset.png
├── icon.svg
├── scenes
│   ├── character
│   │   ├── enemy.tscn
│   │   └── player.tscn
│   ├── hud
│   │   └── hud.tscn
│   └── stage
│       └── stage01.tscn
├── script
│   ├── character
│   │   ├── enemy
│   │   │   ├── enemy.gd
│   │   │   ├── enemy_hit_box.gd
│   │   │   └── enemy_hurt_box.gd
│   │   └── player
│   │       ├── player.gd
│   │       ├── player_hit_box.gd
│   │       └── player_hurt_box.gd
│   └── hud
│       └── health_bar.gd
└── project.godot

ディレクトリの作成

それでは先にディレクトリだけ作成してみましょう。

my_platformer_game/
├── assets
│   └── sprites
│       ├── character
│       │   ├── enemy
│       │   └── player
│       └── tiles
├── scenes
│   ├── character
│   ├── hud
│   └── stage
├── script
│   ├── character
│   │   ├── enemy
│   │   └── player
│   └── hud
└── project.godot

4. プレイヤーキャラクターの作成

プレイヤーのキャラクターを作成していきます。

素材のダウンロード

今回はフリーで利用ができる素材を使っていきます。下記の素材サイトよりダウンロードしてきましょう。このチュートリアルでは下記の3つの素材を利用しています。

素材をダウロードしましたら扱いやすいようにファイル名を変更します。
下記に変更したファイル名を記載します。

用途 変更前 変更後
プレイヤー Pink_Monster_Attack1_4.png player_attack.png
プレイヤー Pink_Monster_Death_8.png player_die.png
プレイヤー Pink_Monster_Hurt_4.png player_hurt.png
プレイヤー Pink_Monster_Idle_4.png player_idle.png
プレイヤー Pink_Monster_Jump_8.png player_jump.png
プレイヤー Pink_Monster_Walk_6.png player_walk.png
タイルマップ oak_woods_tileset.png tileset.png
敵キャラクター slime_frames_all.png enemy.png

ファイル名が変更できたら前項のディレクトリ構成になるように画像をインポートしましょう。

プレイヤーの作成

画像ファイルの準備ができたら早速プレイヤーキャラクターを作成します。
新規にシーンを作成し、ルートノードにCharacterBody2Dノードを作成し、
子ノードにAnimatedSprite2DノードとCollisionShape2Dノードを追加しましょう。
CharacterBody2DノードはPlayerに名前を変更しておきましょう。
シーンパネルでは下記のようなツリー状態になるようにしましょう。

Player
├── AnimatedSprite2D
└── CollisionShape2D

5. アニメーションの設定

まだ画像ファイル(スプライトシート)を設定していないので、キャラクターが表示されていないのでキャラクターの画像を設定します。

Sprite Framesの設定

AnimatedSprite2Dを選択し、インスペクターパネルのAnimationのSprite Framesから新規SpriteFramesを選択します。下部にアニメーションドックが表示されるので、アニメションを追加していきます。

アニメーション追加アイコンを押下して必要なアニメーションを追加していきます。

今回は7個のアニメーションを作成するので、アニメーションを追加して名前を図の通り設定しましょう。

次に各アニメーションに画像を割り当てていきます。
割り当てたいアニメーションの名前を選択したら、スプライトシートからフレームを追加するアイコンを押下します。

それではidleに画像を設定していきましょう。
画像選択のウィンドウが表示されるので、player_idle.pngを選択します。

Godotエディタが画像の分割を自動で判断してくれますが、今回は垂直方向にも4分割されているので、1分割に変更します。

分割されたら利用したい画像のフレームを再生したい順番にクリックしていきます。

選択し終わったら右下のフレームを追加ボタンを押します。
同じように他のアニメーションも追加していきましょう。

アニメーション 画像ファイル 選択フレーム
attack player_attack.png
die player_die.png
fall player_jump.png
hurt player_hurt.png
jump player_jump.png
walk player_walk.png

アニメーションで設定したフレームはデフォルトで持続時間が1となっているので、攻撃とジャンプはメリハリがつくようにフレームに対して時間を設定します。

アニメーションに画像が設定できたのでCollisionShape2Dノードに新規RectangleShape2Dを設定して当たり判定の設定をしましょう。

次はプログラムでアニメーションの制御していきます。
Playerノードに新しいスクリプトをアタッチして、下記のプログラムを書きます。

player.gd
extends CharacterBody2D

class_name Player

const SPEED = 100.0
const JUMP_VELOCITY = -400.0

@onready var animated_sprite_2d = $AnimatedSprite2D

enum PlayerState {IDLE, WALK, JUMP, FALL, ATTACK, HURT, DIE}
var _state: PlayerState = PlayerState.IDLE

func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	calculate_from_input()
	move_and_slide()
	transition_state()
	update_animation()

func calculate_from_input():

	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
		animated_sprite_2d.flip_h = velocity.x < 0
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

func transition_state():

	if is_on_floor():
		if velocity.x == 0:
			_state = PlayerState.IDLE
		else:
			_state = PlayerState.WALK	
	else:
		if velocity.y > 0:
			_state = PlayerState.FALL
		else:
			_state = PlayerState.JUMP

func update_animation():
	var animation:StringName 
	match _state:
		PlayerState.IDLE:
			animation = "idle"
		PlayerState.WALK:
			animation = "walk"
		PlayerState.JUMP:
			animation = "jump"
		PlayerState.FALL:
			animation = "fall"

	if animated_sprite_2d.animation == animation:
		return
	animated_sprite_2d.play(animation)

6. タイルマップの設定

次にキャラクターが移動する地面を作っていきましょう。
今回はGodot4.3で追加されたTileMapLayerを触っていきます。

まずは新しいシーンを作成し、ルートノードにNode2Dを追加し名前をMainStageにします。次に子ノードにTileMapLayerノードを追加します。
TileMapLayerノードが追加できたら、インスペクターパネルからTile Setに新規TileSetを選び、下部のTileSetからプラスアイコンのアトラスを選択し、tileset.pngを選択します。

タイル画像が16pxで区切られるため、TileMapから配置したいタイルを選択して、配置していきます。

一旦プレイヤーシーンをステージシーンに追加して動作を確認して見ましょう。
カメラがプレイヤーに追従するように追加したプレイヤーシーンのノードの子ノードにCamera2Dを追加して、Zoomを3.7に設定したらゲームをプレイして見ましょう。
どうでしょうか。プレイヤーが地面をすり抜けてしまったと思います。
これはまだタイルにコリジョンの設定が出来ていないからです。

でも安心してください。TileMapLayerでは簡単にコリジョンの設定ができます。
インスペクターパネルのTile SetからPhysics Layersのアコーディオンを開き、要素を追加します。そうするとタイルに対して物理の項目が追加されます。

この項目からタイルにたいしてコリジョンの範囲を設定することができます。
タイルの範囲全てにコリジョン判定を設定したい場合は、ペイントの物理レイヤーを選択し、設定したいタイルをクリックすると簡単に設定できます。

もう一度ゲームをプレイすると今度はちゃんとプレイヤーが地面の上で止まると思います。

7. 攻撃処理の追加

次はプレイヤーに攻撃処理を追加しましょう。
Playerノードの子ノードにArea2Dノードを追加して、名前をHitBoxに変えておきます。
当たり判定が何に対して衝突の判定とするかインスペクターパネルで設定できるので、CollisionのLayerを設定しましょう。
今回はプレイヤーの攻撃範囲なので、Layerは3、Maskは4を設定します。
この値はプロジェクト設定で名前を設定したものになりますので、3はプレイヤーの攻撃範囲、4は敵の体を指していることがわかります。

設定項目

改めて設定した項目を記載します。これはこの当たり判定が、プレイヤーの攻撃範囲に属していて、敵の体の当たり判定に対して衝突判定が行われることを意味しています。

  • Layer:3(プレイヤーの攻撃範囲)
  • Mask:4(敵の体)

次に子ノードにCollisionShape2Dを追加して名前をHitBoxCollisionにしてください。
こちらが攻撃の当たり判定の範囲になりますので、キャラクターの前に配置しましょう。
また、こちらの当たり判定は攻撃モーションの時にだけ有効化したいので、最初は無効化しておきましょう。Disabled のチェックボックスをオンにすることで無効化出来ます。

HurtBoxの作成

あとで敵側の攻撃も作っていくので先にプレイヤー側のダメージを受ける当たり判定を追加しておきましょう。
HitBoxと同じようにPlayerノードの子ノードにArea2Dノードを追加して、名前をHurtBoxに変えておきます。
子ノードにCollisionShape2Dを追加して名前をHurtBoxCollisionにしてください。
こちらもCollisionのLayerを設定しましょう。
今回はプレイヤーのダメージを受ける範囲なので、Layerは2、Maskは5を設定します。

  • Layer:2(プレイヤーの体)
  • Mask:5(敵の攻撃範囲)

準備ができたので次はプログラムに修正を加えていきます。

player.gd
extends CharacterBody2D

class_name Player

const SPEED = 100.0
const JUMP_VELOCITY = -400.0

@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var hit_box_collision: CollisionShape2D = $HitBox/HitBoxCollision # 追加

enum PlayerState {IDLE, WALK, JUMP, FALL, ATTACK, HURT, DIE}
var _state: PlayerState
var _is_attacking: bool = false # 追加

func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta

	calculate_from_input()
	update_collision_shape() # 追加
	move_and_slide()
	transition_state()
	update_animation()

## 追加 inputを受け付けるか判定
func can_input() -> bool:
	if _is_attacking:
		return false
	return true

func calculate_from_input():
	# 追加
	if not can_input():
		velocity.x = 0
		return
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
		animated_sprite_2d.flip_h = velocity.x < 0
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

# 追加 キャラクターの向きに合わせて攻撃判定を逆にする
func update_collision_shape():
	if animated_sprite_2d.flip_h:
		hit_box_collision.position.x = -abs(hit_box_collision.position.x)
	else:
		hit_box_collision.position.x = abs(hit_box_collision.position.x)

func transition_state():
	# 追加
	if _is_attacking:
		return

	if is_on_floor():
		# 追加
		if Input.is_action_just_pressed("ui_attack"):
			_state = PlayerState.ATTACK
			_is_attacking = true
			hit_box_collision.disabled = false
			return
		if velocity.x == 0:
			_state = PlayerState.IDLE
		else:
			_state = PlayerState.WALK	
	else:
		if velocity.y > 0:
			_state = PlayerState.FALL
		else:
			_state = PlayerState.JUMP

func update_animation():
	var animation:StringName 
	match _state:
		PlayerState.IDLE:
			animation = "idle"
		PlayerState.WALK:
			animation = "walk"
		PlayerState.JUMP:
			animation = "jump"
		PlayerState.FALL:
			animation = "fall"
		# 追加
		PlayerState.ATTACK:
			animation = "attack"

	if animated_sprite_2d.animation == animation:
		return
	animated_sprite_2d.play(animation)

# 追加
func interrupt_animation(animation: StringName) -> void:
	animated_sprite_2d.play(animation)

# 追加
func attack_finished() -> void:
	_is_attacking = false
	_state = PlayerState.IDLE
	hit_box_collision.disabled = true
	interrupt_animation("idle")
	
# 追加
func _on_animation_finished() -> void:
	
	match _state:
		PlayerState.ATTACK:
			attack_finished()

追加したプログラムでは攻撃アクションが行われたら、アニメーションが完了するまで他の動作を受け付けないように制御を行い、攻撃モーションが終わったら動作を受け付けるようにしています。
また歩きながら攻撃した際に横移動しないように制御も入れています。
攻撃モーションが終わったことはアニメーション終了のシグナルを設定することで検知するようにします。
animation_finishedのシグナルの受け側のメソッドに_on_animation_finishedを設定してください。

次は敵キャラクターの作成ですが、その前に敵にダメージを与えるプログラムを準備しておきましょう。
HitBoxに新しいスクリプトをアタッチし、下記のプログラムを書きましょう。

player_hit_box.gd
extends Area2D

class_name PlayerHitBox

var damage: int = 1

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	connect("area_entered", _on_area_enterd)

func _on_area_enterd(hurtbox: EnemyHurtBox) -> void:
	if hurtbox == null:
		return
	hurtbox.hit(damage)

まだ敵キャラクターを作成していないので、EnemyHurtBoxがエラーになると思いますが一旦無視しておきましょう。

8. 敵の追加

敵キャラクターを作成していきましょう。
新しいシーンを作成し、プレイヤー同様にノードを設定していきましょう。
ルートノードにはCharacterBody2Dノードを作成し、
子ノードにAnimatedSprite2DノードとCollisionShape2Dノードを追加しましょう。
あとはArea2dノードを2追加してそれぞれにCollisionShape2Dノードを追加します。
各ノードの名前は下記の画像の通り設定しましょう。

アニメーションの設定

プレイヤー同様にアニメーションを設定していきます。

アニメーション 画像ファイル 選択フレーム
die enemy.png
hurt enemy.png
idle enemy.png

コリジョンの設定

コリジョンにはそれぞれ下記を設定します。

CollisionShape2D

HurtBox

  • Layer:4(敵の体)
  • Mask:3(プレイヤーの攻撃範囲)
HitBox

  • Layer:5(敵の攻撃範囲)
  • Mask:2(プレイヤーの体)

敵のプログラム作成

敵キャラクターの準備が整ったのでプログラムを作成していきます。
まずEnemyノードに新しくスクリプトをアタッチします。

enemy.gd
extends CharacterBody2D

class_name Enemy

@onready var animated_sprite_2d: AnimatedSprite2D = $AnimatedSprite2D
@onready var hurt_box_collision: CollisionShape2D = $HurtBox/HurtBoxCollision
@onready var hit_box_collision: CollisionShape2D = $HitBox/HitBoxCollision

enum EnemyState {IDLE, HURT, DIE}
var _state: EnemyState
var _is_dying = false
var _is_control_locked = false
var health = 3

func _physics_process(delta: float) -> void:

	if not is_on_floor():
		velocity += get_gravity() * delta

	if _is_dying or _is_control_locked:
		return

	move_and_slide()
	update_animation()

func update_animation() -> void:
	var animation:StringName 
	match _state:
		EnemyState.IDLE:
			animation = "idle"

	if animated_sprite_2d.animation == animation:
		return
	animated_sprite_2d.play(animation)

func interrupt_animation(animation: StringName) -> void:
	animated_sprite_2d.play(animation)

func hurt_finished() -> void:
	_is_control_locked = false
	_state = EnemyState.IDLE
	interrupt_animation("idle")

func die() -> void:
	interrupt_animation("die")
	hurt_box_collision.call_deferred("set", "disabled", true)
	hit_box_collision.call_deferred("set", "disabled", true)
	_is_dying = true

func _on_take_damage(damage) -> void:
	health -= damage

	print("ライフ:%d" % health)
	if health > 0:
		_is_control_locked = true
		_state = EnemyState.HURT
		interrupt_animation("hurt")
		return
	die()

func _on_animation_finished() -> void:

	match _state:
		EnemyState.HURT:
			hurt_finished()

	if _is_control_locked:
		_is_control_locked = false

	if _is_dying:
		set_physics_process(false)
		queue_free()

HurtBoxにも新しくスクリプトをアタッチします。

enemy_hurt_box.gd
extends Area2D

class_name EnemyHurtBox

signal hurt(damage: int)

func hit(damage: int):
	print("ダメージ:%d" % damage)
	hurt.emit(damage)

シグナルの設定をしていきます。
敵キャラクターがお亡くなりになった時に消滅のアニメーションの後にノード削除などの処理を制御するためプレイヤー同様にanimation_finishedのシグナルを設定します。
受け側のメソッドに_on_animation_finishedを設定してください。

次にプレイヤーの攻撃を受けてダメージを反映させるためのシグナルを設定します。
カスタムシグナルのhurtに_on_take_damageを設定します。

プレイヤーからのシグナルの流れを説明すると下記の図のように処理が実行されます。

敵の攻撃とプレイヤーへのダメージ

同様に敵からのダメージも実装していきましょう。HitBoxにも新しくスクリプトをアタッチします。

enemy_hit_box.gd
extends Area2D

class_name EnemyHitBox

@onready var hit_box_collision: CollisionShape2D = $HitBoxCollision

var damage: int = 1

# Called when the node enters the scene tree for the first time.
func _ready() -> void:
	connect("area_entered", _on_area_enterd)

func _on_area_enterd(hurtbox: PlayerHurtBox) -> void:
	if hurtbox == null:
		return
	hurtbox.hit(damage)

プレイヤーシーンに戻り、プレイヤーのHurtBoxに新しくスクリプトをアタッチしましょう。

player_hurt_box.gd
extends Area2D

class_name PlayerHurtBox

signal hurt(damage: int)

func hit(damage: int):
	print("ダメージ:%d" % damage)
	hurt.emit(damage)

こちらもカスタムシグナルのhurtに_on_take_damageを設定します。
ダメージを受けている時にキャラクターが攻撃できないようにplayer.gdも修正していきます。

player.gd
extends CharacterBody2D

class_name Player

const MAX_HEALTH = 3 # 追加
const SPEED = 100.0
const JUMP_VELOCITY = -400.0

signal change_health # 追加

@onready var animated_sprite_2d = $AnimatedSprite2D
@onready var hit_box_collision: CollisionShape2D = $HitBox/HitBoxCollision

enum PlayerState {IDLE, WALK, JUMP, FALL, ATTACK, HURT, DIE}
var _state: PlayerState = PlayerState.IDLE
var _is_attacking: bool = false
var _is_dying: bool = false # 追加
var _is_control_locked: bool = false # 追加
var health = MAX_HEALTH # 追加

func _physics_process(delta: float) -> void:
	# Add the gravity.
	if not is_on_floor():
		velocity += get_gravity() * delta
	# 追加
	if _is_dying or _is_control_locked:
		return

	calculate_from_input()
	update_collision_shape()
	move_and_slide()
	transition_state()
	update_animation()

## inputを受け付けるか判定
func can_input() -> bool:
	if _is_attacking:
		return false
	return true

func calculate_from_input():
	if not can_input():
		velocity.x = 0
		return
	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
		animated_sprite_2d.flip_h = velocity.x < 0
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	if Input.is_action_just_pressed("ui_accept") and is_on_floor():
		velocity.y = JUMP_VELOCITY

func update_collision_shape():
	if animated_sprite_2d.flip_h:
		hit_box_collision.position.x = -abs(hit_box_collision.position.x)
	else:
		hit_box_collision.position.x = abs(hit_box_collision.position.x)

func transition_state():
	if _is_attacking:
		return

	if is_on_floor():
		if Input.is_action_just_pressed("ui_attack"):
			_state = PlayerState.ATTACK
			_is_attacking = true
			hit_box_collision.disabled = false
			return
		if velocity.x == 0:
			_state = PlayerState.IDLE
		else:
			_state = PlayerState.WALK	
	else:
		if velocity.y > 0:
			_state = PlayerState.FALL
		else:
			_state = PlayerState.JUMP

func update_animation():
	var animation:StringName 
	match _state:
		PlayerState.IDLE:
			animation = "idle"
		PlayerState.WALK:
			animation = "walk"
		PlayerState.JUMP:
			animation = "jump"
		PlayerState.FALL:
			animation = "fall"
		PlayerState.ATTACK:
			animation = "attack"

	if animated_sprite_2d.animation == animation:
		return
	animated_sprite_2d.play(animation)

func interrupt_animation(animation: StringName) -> void:
	animated_sprite_2d.play(animation)

func attack_finished() -> void:
	_is_attacking = false
	_state = PlayerState.IDLE
	hit_box_collision.disabled = true
	interrupt_animation("idle")

# 追加	
func hurt_finished() -> void:
	_is_control_locked = false
	_state = PlayerState.IDLE
	interrupt_animation("idle")

# 追加	
func _on_take_damage(damage) -> void:
	health -= damage
	change_health.emit()

	print("ライフ:%d" % health)
	if health > 0:
		_is_control_locked = true
		_state = PlayerState.HURT
		interrupt_animation("hurt")
		return

	die()

# 追加	
func die() -> void:
	interrupt_animation("die")
	_state = PlayerState.DIE
	_is_dying = true

func _on_animation_finished() -> void:
	
	match _state:
		PlayerState.ATTACK:
			attack_finished()
		PlayerState.HURT:
			hurt_finished()
	# 追加	
	if _is_dying:
		set_physics_process(false)
		queue_free()

9. 体力ゲージの作成

プレイヤーのダメージ処理が出来たので、今度はユーザに見えるように体力ゲージを用意して
体力に連動して反映されるようにしてみましょう。

新しくシーンを作成してルートノードにCanvasLayerを追加し、子ノードにProgressBarを追加し、名前をHealthBarにします。デフォルトではパーセント表示されるので、こちらを非表示にしましょう。
インスペクターパネルのShowPercentageのチェックを外すと非表示にできます。

次に体力がある場合は緑ゲージ、体力を失っているゲージは赤色に設定していきます。
Theme Overridesのスタイルから設定ができます。

Backgroud Fill

準備が整ったのでプログラムを書いていきます。
HealthBarに新規スクリプトをアタッチして下記プログラムを書いてみましょう。

health_bar.gd
extends ProgressBar

@export var player: Player

func _ready() -> void:
	player = get_node("../../Player")
	player.change_health.connect(update)
	update()

func update():
	value = player.health * 100 / player.MAX_HEALTH

プログラムが書き終わったらHUDシーンをステージシーンに追加してみましょう。
プレイヤーがダメージを受けると体力ゲージに反映されることがわかると思います。

10. まとめ

今回の記事ではGodotを使ってプラットフォームゲームを作成するための基本的な要素と手順を説明しました。この記事を参考にして、ぜひ自分だけのプラットフォームゲームを作成してみてください。
記事内では冗長的になるため細かい手順を省略しているところもありますが、その辺りは動画の方で確認いただければと思います。
さらに詳しい説明や具体例が必要な場合は、遠慮なくお知らせください!

Discussion