Godot4.1.3でシューティングゲームを作っていきます。
敵のパラメータとベースクラスの設計
敵のパラメータについて整理するのと、敵のスクリプトのクラス設計をします。
github
本記事で実装したものをgithubで公開しています。
開発スタンスについて
必要なコードのみを最小限で実装し、不要なものは削除する。とっ散らかったり、いびつに感じる部分があれば、リファクタリングするというスタンスで開発をしています。
たまに先々必要になりそうだからとか、使わなくなったけど念のため残しておいたコードは、たいていそのままでは役に立たなかったり、ゴミになります。そういうコードは後で忘れたころに見たときに、何に使用するかわからず考えてしまって時間を無駄にする原因にもなるので、勇気をもって削除しようと思います。
やりたいこと
今回やりたいことが3つあります。
(1) 敵と敵生成に関するパラメータは「シナリオ要素」に設定する
現時点で、敵キャラクターのHPやスコア、速度などは、敵スクリプトに実装し、発生数、周期はシナリオ要素のスクリプトに実装しました。
しかし、パラメータを確認するためにあっちこっちのシーンを見て回るのは大変なので、パラメータはすべてシナリオ要素に設定するようにします。
言葉だけではイメージしにくいので、例として、下図、Stage1のシナリオシーンである、scenario_stage01.tscnを使用します。子ノードは「シナリオ要素」です。上から順番に実行されます。
図. シナリオのシーン(scenario_stage01.tscn)
シナリオシーンの子ノードのシナリオ要素を選択して、インスペクターを表示すると、敵や敵の生成に関するすべてのパラメータを設定および確認することができるようにする、というのが今回の目的です。
またシナリオ要素のノードの名称もパラメータをある程度反映すると、インスペクターを見る前にシナリオが把握できて良さそうです。
図. シナリオ要素のインスペクターのパラメータの例
(2) 敵スクリプトのベースクラスを作る
下記は敵スクリプトとして実装したtwin_body.gdですが、敵共通の処理と、twin_body固有の処理が混在しています。以下のコードで、マイナス(-)の差分で表示した部分は、HPの計算やスコアの機能など、敵として共通で持つべき機能なので、今回新規で作成する敵共通のクラスに移動します。
extends Area3D
-signal add_to_score(int)
@export var _d_speed_mps = 20.0
-@export var _d_hp = 1
-@export var _i_score = 100
func _physics_process(delta):
global_position += Vector3(0, 0, 1) * _d_speed_mps * delta
- if _d_hp <= 0:
- add_to_score.emit(_i_score)
- queue_free()
func _on_visible_on_screen_notifier_3d_screen_exited():
queue_free()
-# HPからダメージポイントを引き算する。余った分はreturn値で返す
-func calc_damage(laser_power) -> int:
- var remainder = 0
- if _d_hp <= laser_power:
- # ダメージの方が大きい
- remainder = laser_power - _d_hp
- _d_hp = 0
- else:
- # HPの方が大きい
- _d_hp -= laser_power
- remainder = 0
- # 余ったダメージポイントを返す
- return remainder
(3)直値をやめる
twin_bodyの初期位置は直値で設定していますしていますが、今後変更があるので、グローバル変数で宣言したものを使用するようにします。
シーケンス
敵twin_Bodyのインスタンス生成のシーケンスは下図のようになります。
敵スクリプトのベースクラスは、EnemyBaseで、HPとスコア関係はEnemyBaseが責務を持ちます。
_physics_processのシーケンスは以下です。
レーザーで撃墜された場合のシーケンスは以下です。
HPとスコアの責務を持つEnemyBaseがmay_laserから呼ばれています。
実装変更
敵スクリプトのベースクラスの作成
res://character/enemyに新規スクリプトでenemy_base.gdを作成し、下記のようにしました。基本的には、twin_body.gdから、敵として共通であるべき処理を抜き出したものです。hp、scoreの変数名を少し変更しました。
class_name EnemyBase extends Area3D
# 敵が共通して持つべきシグナル
signal add_to_score(int)
# 敵が共通して持つべきプロパティ
var d_enemy_hp = 1
var i_enemy_score = 100
# 撃墜チェック
# 敵 派生クラスの_physics_processからコールされることを想定
func check_destroyed():
if d_enemy_hp <= 0:
add_to_score.emit(i_enemy_score)
queue_free()
# ダメージ計算処理
# HPからダメージポイントを引き算する。余った分はreturn値で返す
# レーザースクリプトからコールされることを想定
func calc_damage(laser_power) -> int:
var remainder = 0
if d_enemy_hp <= laser_power:
# ダメージの方が大きい
remainder = laser_power - d_enemy_hp
d_enemy_hp = 0
else:
# HPの方が大きい
d_enemy_hp -= laser_power
remainder = 0
# 余ったダメージポイントを返す
return remainder
twin_Bodyスクリプトの修正
敵ベースクラスに移動したコードが多かったので、残ったコードはだいぶすっきりしました。
twin_body固有の移動処理と、イベント処理が残りました。
また、敵ベースクラスに移動した_physics_processメソッドの一部の処理は、「super.check_destroyed()」で呼び出します。
extends EnemyBase
# 固有のプロパティ
var d_speed_mps = 25.0
func _physics_process(delta):
# 撃墜チェック
super.check_destroyed()
# 移動処理
global_position += Vector3(0, 0, 1) * d_speed_mps * delta
func _on_visible_on_screen_notifier_3d_screen_exited():
queue_free()
敵登場位置の直値をグローバル変数で宣言
twin_bodyの初期位置は、スクリプトに直値で-200m、-25m~25mと書いてありましたが、他の敵キャラも使うのと、今後変更がありそうなので、グローバル変数に定義します。
res://global/global.gd
extends Node3D
+# ゲーム画面の見える範囲
+# ゲーム画面の上端をtop、下端をbotommとする
+var d_visible_top_y_m = -200.0
+var d_visible_top_min_x_m = -25.0
+var d_visible_top_max_x_m = 25.0
# gameのインスタンス
var node_game : Node3D = null
# 生成したインスタンスの追加先
var node_lasers : Node3D = null
var node_enemies : Node3D = null
var node_bullets : Node3D = null
# Playerのインスタンス
var node_player : Area3D = null
シナリオ要素の修正
敵及び敵キャラの生成のパラメータは、このシナリオ要素ですべて定義します。追加で定義した敵のパラメータは、敵インスタンスを生成したときに設定します。
また、初期位置の設定に使用していた直値を、グローバル変数に変更しました。
res://scenario/elements/produce_twin_body.gd
extends Node
# 敵生成パラメータ
@export var _i_produce_num = 10
@export var _d_produce_interval_sec = 0.5
+# 敵のパラメータ
+@export var _d_hp = 1
+@export var _i_score = 100
var _scn_enemy = preload("res://character/enemy/twin_body.tscn")
var _i_produced_num = 0; # 生産した数
func start_element():
$Timer_for_produce.start(_d_produce_interval_sec)
func is_blocking_to_move_on_next_scenario() -> bool:
# 敵キャラの生産が完了したら次のシナリオ要素に進ませるためfalseにする
return ( _i_produced_num < _i_produce_num )
func _on_timer_for_produce_timeout():
# 敵キャラクターのインスタンスを生成
var ins = _scn_enemy.instantiate()
+ # 敵パラメータの設定
+ ins.d_enemy_hp = _d_hp
+ ins.i_enemy_score = _i_score
- ins.position.z = -200
- ins.position.x = randf_range(-25, 25)
+ ins.position.z = g_val.d_visible_top_y_m
+ ins.position.x = randf_range( g_val.d_visible_top_min_x_m,
+ g_val.d_visible_top_max_x_m)
# add_to_scoreシグナルをgame.gdに接続する
ins.add_to_score.connect( g_val.node_game._on_add_to_score )
# 敵インスタンスをシーンツリーに追加
g_val.node_enemies.add_child(ins)
# 生産完了したら、タイマーを停止する
_i_produced_num += 1
if ( _i_produced_num >= _i_produce_num ):
$Timer_for_produce. stop()
終わりに
記事を書くと手間がかかりますが、細かいところまで考える必要があるので良いと思います。
以上です。