2020/11/16(月) [n年前の日記]
#1 [godot] Godot EngineでHUDシーンを作成
Godot Engine 3.2.3 x64 を使って3D表示の簡単なシューティングゲームっぽいものを作る。
今回はプレイヤーや敵のHPを表示するためのHUDシーンを作成する。そのついでに、プレイヤーや敵に弾が当たったらHPが減るような処理も追加する。
今回はプレイヤーや敵のHPを表示するためのHUDシーンを作成する。そのついでに、プレイヤーや敵に弾が当たったらHPが減るような処理も追加する。
◎ フォントファイルをインポート。 :
画面に文字情報を表示するためには何かしらのフォントファイルが必要になる。Godot Engine はフォントファイルとして ttf や otf が使えるので、それらファイルをプロジェクトフォルダにインポートしてやればフォントが使えるようになる。
今回は、ドットコロンさん(?)がCC0で公開している Vegurフォント Version 0.701 を利用させてもらうことにした。ありがたや。
_Vegur | ドットコロン
vegur_0701.zip をDLして解凍すると、Vegur-Bold.otf, Vegur-Light.otf, Vegur-Regular.otf の3ファイルが得られる。太字の Vegur-Bold.otf を使うことにする。
ファイル一覧ウインドウに、Vegur-Bold.otf をドラッグアンドドロップ。これでファイルがコピーされてインポートされる。
件のフォントファイルを res://imports/ 以下に置くことにした。違う場所にコピーされてしまった時は、ファイル一覧ウインドウ内でファイルをドラッグすれば置き場所を変更できる。
今回は、ドットコロンさん(?)がCC0で公開している Vegurフォント Version 0.701 を利用させてもらうことにした。ありがたや。
_Vegur | ドットコロン
vegur_0701.zip をDLして解凍すると、Vegur-Bold.otf, Vegur-Light.otf, Vegur-Regular.otf の3ファイルが得られる。太字の Vegur-Bold.otf を使うことにする。
ファイル一覧ウインドウに、Vegur-Bold.otf をドラッグアンドドロップ。これでファイルがコピーされてインポートされる。
件のフォントファイルを res://imports/ 以下に置くことにした。違う場所にコピーされてしまった時は、ファイル一覧ウインドウ内でファイルをドラッグすれば置き場所を変更できる。
◎ HUD用のシーンを作成。 :
シーンを新規作成して以下のような構成でノードを追加。HUDのようなものは、ルートノードを Control にしておけばいいのだろうか。ちょっと自信無し。
ノードをリネーム。
ノードをリネーム。
Hud (Control) │ ├─ PlayerHp (Label) │ └─ EnemyHp (Label)
◎ Labelのプロパティを変更していく。 :
Labelノードを設定して文字表示をする。PlayerHp (Label) を選択して、Textプロパティに「Player: 50/50」を入力。これは画面の左上に表示する予定。
EnemyHp (Label) を選択して、Textプロパティに「Enemy: 990/990」を入力。これは画面の右上に表示したいので、Align を Right にしておく。これで右詰め表示されるはず。
フォントを設定する。Custom Fonts → Font の横の「空」をクリックして「新規 DynamicFont」を選択。「DynamicFont」をクリックすればプロパティが表示されるので、Font Data に Vegur-Bold.otf を指定すれば Vegurフォントが使われるようになる。
表示位置を調整。それらしい感じになった。
EnemyHp (Label) を選択して、Textプロパティに「Enemy: 990/990」を入力。これは画面の右上に表示したいので、Align を Right にしておく。これで右詰め表示されるはず。
フォントを設定する。Custom Fonts → Font の横の「空」をクリックして「新規 DynamicFont」を選択。「DynamicFont」をクリックすればプロパティが表示されるので、Font Data に Vegur-Bold.otf を指定すれば Vegurフォントが使われるようになる。
- Size で文字サイズを指定。
- Use Filter を有効にすれば文字が少し滑らかになって表示される。
- Font Color Shadow を有効にすれば文字に影がつく。
- Shadow Offset X, Y で影のオフセット位置を変更できる。
表示位置を調整。それらしい感じになった。
◎ Hudシーンを保存。 :
◎ スクリプトを書く。 :
Hudに、スクリプトファイルもアタッチする。res://scripts/ の下に Hud.gd として保存。
Hud.gd にスクリプトを記述していく。
スクリプトの内容は以下。とりあえず、PlayerHp、EnemyHp の表示を更新するためのメソッドだけ用意しておく。
_Hud.gd
Hud.gd にスクリプトを記述していく。
スクリプトの内容は以下。とりあえず、PlayerHp、EnemyHp の表示を更新するためのメソッドだけ用意しておく。
_Hud.gd
extends Control func _ready(): pass # Replace with function body. #func _process(delta): # pass func update_player_hp(hp, hpmax): $PlayerHp.text = "Player: %d/%d" % [hp, hpmax] func update_enemy_hp(hp, hpmax): $EnemyHp.text = "Enemy: %d/%d" % [hp, hpmax]
◎ MainシーンにHudシーンを追加。 :
MainシーンにHudシーンのインスタンスを追加する。Main.tscn を開いて、Mainノードを選択。上のほうにあるインスタンス追加ボタンをクリックして、res://assets/Hud.tscn を選べば Hud が追加される。
◎ Mainシーンにスクリプトを用意。 :
Mainシーンにもスクリプトファイルを用意する。Mainを選んでスクリプトをアタッチ。res://scripts/Main.gd として保存した。
スクリプトを記述する。
内容は以下。
_Main.gd
スクリプトを記述する。
内容は以下。
_Main.gd
extends Node var player_hp_max = 0 var enemy_hp_max = 0 func _ready(): player_hp_max = $Player.hp enemy_hp_max = $EnemyZako.hp #func _process(delta): # pass func damage_player(): var hp = $Player.hp $Hud.update_player_hp(hp, player_hp_max) func damage_enemy(): var hp = $EnemyZako.hp $Hud.update_enemy_hp(hp, enemy_hp_max)
- 初期化時に Player や Enemy のHPを取得して変数に記録。
- Player 又は Enemy がダメージを受けた際、現在のHPを使ってHP表示を更新するメソッドを用意。
◎ プレイヤーのスクリプトを修正。 :
Mainシーンのスクリプト内で、Player や Enemy は HP を持っていることを前提にした記述を追加したけれど、まだ Player側ではそんな変数を持たせてないのでそのあたりを追加する。
プレイヤーのスクリプトファイル、res://scripts/Player.gd を編集。内容は以下。
_Player.gd
プレイヤーがダメージを受けた時に発行されるカスタムシグナル、player_damaged にメソッドを接続する。Player を選択して、ノードタブをクリック → シグナルをクリック → player_damaged() を右クリックして「接続」。
Main を選択して、メソッド名の欄に「damage_player」と入力して「接続」。メソッド名に括弧はつけなくていい。
これで、プレイヤーがダメージを受けると Main.gd の damage_player() が呼ばれる状態になった。
プレイヤーのスクリプトファイル、res://scripts/Player.gd を編集。内容は以下。
_Player.gd
extends KinematicBody signal player_damaged signal player_died # export (PackedScene) var PlayerBullet var PlayerBullet = preload("res://assets/PlayerBullet.tscn") const SPEED = 32 var velocity = Vector3() var bullets var hp = 50 var damage = 0 func _ready(): hp = 50 # bullets = get_tree().root.get_node("Main/PlayerBullets") bullets = $"/root/Main/PlayerBullets" #func _process(delta): # pass func _physics_process(delta): # move velocity = Vector3() if Input.is_action_pressed("ui_right"): velocity.x = 1 elif Input.is_action_pressed("ui_left"): velocity.x = -1 if Input.is_action_pressed("ui_down"): velocity.z = 1 elif Input.is_action_pressed("ui_up"): velocity.z = -1 # Do not multiply by delta when using move_and_slide() velocity = velocity.normalized() * SPEED move_and_slide(velocity) if damage > 0: hp -= damage damage = 0 if hp <= 0: hp = 0 emit_signal("player_died") emit_signal("player_damaged") func _on_ShotTimer_timeout(): var bullet = PlayerBullet.instance() var pos = Vector3(translation.x, translation.y, translation.z - 1.5) bullet.translation = pos bullets.add_child(bullet)
- 最初のあたりで、カスタムシグナルとして signal player_damaged と signal player_died を用意した。
- hp という変数を用意した。
- damage という変数を用意した。
- 別の何かが damage に 0以外の値を入れたら、自分はその分ダメージを受けたのだ、ということにする。
- hp から damage 分を引いて、hp が 0 になってなければ自分はまだ生きてる。0なら死んでる。
- ダメージを受けた時は emit_signal("player_damaged") を呼んで player_damaged シグナルを発行する。
- 死んだ時は emit_signal("player_died") を呼んで player_died シグナルを発行する。
プレイヤーがダメージを受けた時に発行されるカスタムシグナル、player_damaged にメソッドを接続する。Player を選択して、ノードタブをクリック → シグナルをクリック → player_damaged() を右クリックして「接続」。
Main を選択して、メソッド名の欄に「damage_player」と入力して「接続」。メソッド名に括弧はつけなくていい。
これで、プレイヤーがダメージを受けると Main.gd の damage_player() が呼ばれる状態になった。
◎ 敵のスクリプトを修正。 :
敵シーンにもプレイヤーシーンと同様の修正を施していく。
res://scripts/EnemyZako.gd を開いて修正。内容は以下。
_EnemyZako.gd
修正内容は、Player.gd に対して行ったこととほとんど同じで、カスタムシグナル名が違う程度。
ただ、Enemy は自分でプレイヤーの弾(Areaノード)と当たったかどうかを調べるので、プレイヤーの弾が attack_point という変数を持っていることを前提にして、その値で hp を減らす処理をしている。
敵がダメージを受けた時に発行されるカスタムシグナル、enemy_damaged にメソッドを接続する。EnemyZako を選択して、ノードタブをクリック → シグナルをクリック → enemy_damaged() を右クリックして「接続」。
Main を選択して、メソッド名の入力欄に「damage_enemy」と入力して「接続」。
これで、敵がダメージを受けると Main.gd の damage_enemy() が呼ばれる状態になった。
res://scripts/EnemyZako.gd を開いて修正。内容は以下。
_EnemyZako.gd
extends Area signal enemy_damaged signal enemy_died export (PackedScene) var enemybullet var base_pos = Vector3() var angle = 0 export var move_w = 22 export var move_h = 12 var bullets var hp = 990 var damage = 0 var attack_point = 10 func _ready(): base_pos = translation angle = 0 bullets = get_tree().root.get_node("Main/EnemyBullets") #func _process(delta): # pass func _physics_process(delta): move(delta) func move(delta): angle += delta translation.x = base_pos.x + move_w * cos(deg2rad(angle * 90)) translation.z = base_pos.z + move_h * sin(deg2rad(angle * 70)) func _on_ShotTimer_timeout(): var angle_add = 12 var angle = 90 - angle_add * 3 var spd = 0.2 for i in range(7): _shot(translation, angle, spd) _shot(translation, angle, spd * 0.8) angle += angle_add func _shot(pos, angle, spd): var bullet = enemybullet.instance() bullet.translation = pos bullet.set_dir_and_speed(angle, spd) bullets.add_child(bullet) func _on_EnemyZako_area_entered(area): if is_queued_for_deletion(): return # print("playerbullet hit the enemy") if area.is_in_group("playerbullets"): hp -= area.attack_point if hp <= 0: hp = 0 emit_signal("enemy_died") emit_signal("enemy_damaged") func _on_EnemyZako_body_entered(body): if is_queued_for_deletion(): return # print("player hit the enemy") if body.is_in_group("player"): body.damage = attack_point
修正内容は、Player.gd に対して行ったこととほとんど同じで、カスタムシグナル名が違う程度。
ただ、Enemy は自分でプレイヤーの弾(Areaノード)と当たったかどうかを調べるので、プレイヤーの弾が attack_point という変数を持っていることを前提にして、その値で hp を減らす処理をしている。
敵がダメージを受けた時に発行されるカスタムシグナル、enemy_damaged にメソッドを接続する。EnemyZako を選択して、ノードタブをクリック → シグナルをクリック → enemy_damaged() を右クリックして「接続」。
Main を選択して、メソッド名の入力欄に「damage_enemy」と入力して「接続」。
これで、敵がダメージを受けると Main.gd の damage_enemy() が呼ばれる状態になった。
◎ 敵弾のスクリプトを修正。 :
敵弾のスクリプト res://scripts/EnemyBullet.gd を修正する。内容は以下。
_EnemyBullet.gd
_EnemyBullet.gd
extends Area var velocity = Vector3() var direction = 0 var speed = 3 var attack_point = 10 func _ready(): pass # Replace with function body. #func _process(delta): # pass func _physics_process(delta): translate(velocity * speed * 60 * delta) func set_dir_and_speed(angle, spd): direction = angle speed = spd var x = cos(deg2rad(angle)) var z = sin(deg2rad(angle)) velocity = Vector3(x, 0, z) func _on_KillTimer_timeout(): queue_free() func _on_EnemyBullet_body_entered(body): if is_queued_for_deletion(): return # print("EnemyBullet Hit!") if body.is_in_group("player"): body.damage = attack_point queue_free()
- 変数 attack_point を追加して、プレイヤーへの攻撃力を入れておく。
- プレイヤーと当たった時は、プレイヤーが変数 damage を持っているはずだから、damage に自分の攻撃力を入れてやる。
- プレイヤー側は、damage が0以外なら何かが当たってダメージを受けたという処理をする。
◎ プレイヤーの弾のスクリプトを修正。 :
プレイヤーの弾のスクリプト res://scripts/PlayerBullet.gd を修正する。内容は以下。
_PlayerBullet.gd
これで、プレイヤーの弾が敵に当たったり、敵弾がプレイヤーに当たったりすると、それぞれのHPが減って、HUD上の表示が更新される状態になった。
しかし、このままだとプレイヤーに敵弾に当たったことが分かりづらい。次回はプレイヤーや敵がダメージを受けた時の簡単なエフェクト(?)をつけてみる。
_PlayerBullet.gd
extends Area var velocity = Vector3() var speed = 90 var attack_point = 10 func _ready(): velocity = Vector3(0, 0, -1) #func _process(delta): # pass func _physics_process(delta): translate(velocity * speed * delta) func _on_Timer_timeout(): queue_free() func _on_PlayerBullet_area_entered(area): if is_queued_for_deletion(): return # print("PlayerBullet Hit!") queue_free()
- 変数 attack_point を追加して、敵への攻撃力を入れておく。
- 敵と当たった時は、敵側でこの attack_point を参照して処理をする。
これで、プレイヤーの弾が敵に当たったり、敵弾がプレイヤーに当たったりすると、それぞれのHPが減って、HUD上の表示が更新される状態になった。
しかし、このままだとプレイヤーに敵弾に当たったことが分かりづらい。次回はプレイヤーや敵がダメージを受けた時の簡単なエフェクト(?)をつけてみる。
[ ツッコむ ]
以上です。