【Godot】2Dシューティングゲームを作るときのポイントまとめ

Godotで初めて2Dシューティングゲームを作ってみたので、少し悩んだポイントをまとめてみました
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/f838e459-4999-d42b-f8e0-d3fc84314309.gif)

プロジェクトファイルは以下からダウンロードできます
https://github.com/syun77/godot_stg_test

# ゲームオブジェクトとシーンの関係
Godotでは「ノード」という単位ですべて管理されているようです。
そしてノードは「シーン」という単位に配置されます。
さらに「シーンにシーンを配置できる」……という構造になっていて、プレイヤーや敵などは1つのシーンで作っておいて、それをメインゲームとなるシーンに配置する、という開発フローになっているようです。
![名称未設定_key.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/fc747e4d-8b46-7caf-e96b-d8bfdbec1274.png)

Unityに例えるとオブジェクトを複製する場合は通常 Prefab を使いますが、Godotの場合はシーンが Prefab であり、シーンとしてそのまま使うこともできる、ということになります。

# ゲームオブジェクトの構成
今回作ったゲームオブジェクトのノード構成は以下のようになりました。(例として プレイヤー[自機])
![Godot_Engine_-_TestGame_-_Player_tscn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/72215b6e-e7bf-5a52-771d-53f4d5eacb63.png)

```
Player (Area2D)
 +-- Sprite
 +-- CollisionShape2D
```

ゲームオブジェクトのベースとなるノードでGodotのサンプルとしてよく使われるのが `KinematicBody2D` なのですが、今回は `Area2D` を使用しました。
理由としては、`KinematicBody2D` の場合、オブジェクト同士にめり込みが発生した場合、自動で衝突応答(重ならないように押し出し)が行われてしまうためです。
（※ひょっとしたら何か回避方法があるかもしれませんが……）
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/777045b2-870c-7cc5-a83e-faebcefad94d.gif)
このように敵の中心から敵が出現したり、弾が発射されたりします。
そのため、今回のシューティングでは、衝突応答をせず衝突検知のみが必要なので `Area2D` を使うことにしました。

ちなみに `KinematicBody2D` を使用する場合は `move_and_slide()` を使うと、重なりが発生せずいい感じに移動させることができるようです。

`KinematicBody2D`については、以下の記事に詳しくまとめられていますね。

* [KinematicBody2D の基本](https://qiita.com/ninetones/items/d47395dd4af772b97280)

## 衝突検知には `area_entered` シグナルを使用する
シグナルをまだ完全に理解してはいないのですが、他のゲームエンジンでのイベントやコールバックに該当するのがシグナルのようです。
`Area2D` の場合、`area_entered()` というシグナルがあり、これを利用すると衝突検知が実装できます。

![Godot_Engine_-_TestGame_-_Bullet_tscn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/4212fd33-fc14-7089-2151-aa9994f84ac2.png)

このシグナルを作成して、スクリプト側には以下のように記述します。

```gd:Bullet.gd
func _on_Bullet_area_entered(area):
  if area.name == "Player":
    area.hit(1) # プレイヤーに1ダメージ
    destroy() # 自分も消える
```

このシグナルの引数には `Area2D` が渡されるので、`name` プロパティ(ノード名)が "Player" の場合に衝突処理を行う……、という実装にしました。
おそらく`Area2D`の「Layer」や「Mask」を使用することでより最適な書き方になる気がしますが、このあたり調査ができておらず今後の課題です。

なお、ノード名で判定する場合、敵キャラのように同時に複数のインスタンスが登場すると問題が発生します。理由は「同じシーンに同じノード名は存在できないため」です。
具体的には "Enemy" が複数生成されると、"＠Enemy@2"、"＠Enemy@3"、……というような名前となります。

これにより発生するコリジョン抜けを対処するには、以下のように記述します。

```gd:Shot.gd
func _on_Shot_area_entered(area):
  if "Enemy" in area.name or "Boss" in area.name:
    area.hit(1) # 敵に1ダメージ
    queue_free() # 自分も消える

```

`"Enemy" in area.name` という記述にすることで、文字列 "area.name" に "Enemy" という文字が含まれるかどうか、という判定をすることができます。


## 2021.2.22 追記：コリジョンレイヤーとマスクについて
コリジョンレイヤーとマスクについて調べました
→ [【Godot】コリジョンレイヤーとマスクについて](https://qiita.com/2dgames_jp/items/062ab83ea1fcdb2c7f5e)

この仕組みを使えば、ノード名で判定せずに衝突検知ができます。

# インスタンスの生成
ゲームオブジェクトを生成するには、対象のシーンを`preload()`で読み込み `instance()` を呼び出す、という記述となります。

```gd:Player
extends Area2D

# ショットシーンを読み込み
const Shot = preload("res://Scenes/Shot.tscn")
……

func _process(delta):
  ……
  
  # Spaceキーを押したらショットを発射
  if Input.is_action_pressed(("ui_select")):
    # ショット生成
    var shot = Shot.instance()
			
    # 位置と移動量(90度方向に1000の速さ)を設定
    shot.start(position.x, position.y, 90, 1000)
			
    # ルートノードを取得
    var main_node = get_owner()
    main_node.add_child(shot)
```

`instance()` を呼び出すだけではメモリ上に配置されるだけなので、どこかのノードに `add_child()` でぶら下げることで表示されるようになります。ここでは `get_owner()` でノードの所有者を取得してそれにぶら下げていますが、しっかりと作る場合はオブジェクトプールを管理するノードにぶら下げるのが良いのかもしれません。

# インスタンスの破棄
インスタンスの破棄は `queue_free()` を呼び出します。

```gd
func destroy():
	# 消滅
	queue_free()
```

これによりノードツリーから削除され、メモリ上からも消えるようです

# Mainノードの構成
今回はメインとなるシーンを以下のようなノード構成としました。

```
Main (Node2D): メインシーン
 +-- Player (Area2D): プレイヤー
 +-- Boss (Area2D): ボス
 +-- HPBar (TextureProgress): ボスのHP
 +-- Label (Label): キャプション
```

![Godot_Engine_-_TestGame_-_Main_tscn____.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/fb1539c4-916f-9785-5ef4-8f4fcefa480f.png)

Playerは Shot(自機が発射する弾) を生成し、BossはEnemyを生成します。
BossのHPに連動して、HPBarのゲージの幅が変化し、ゲームオーバーやゲームクリア時に、Labelのテキストが変化する……という実装となっています。

すべてのノードは Mainノードに直接ぶら下がるようにしました。

# プレイヤーの移動
Area2Dを使用する場合、物理エンジンを使わず移動するので、直接 `position` の値を変化させて移動させるようにしました。

```gd:Player.gd
func _process(delta):
	# 移動速度
	var spd = 500 * delta
	if Input.is_action_pressed("ui_select"):
		# Spaceキーを押しているとスロー
		spd = 200 * delta
	
	var v = Vector2()
	if Input.is_action_pressed("ui_up"):
		v.y -= 1 # 上に移動
	if Input.is_action_pressed("ui_down"):
		v.y += 1 # 下に移動
	if Input.is_action_pressed("ui_left"):
		v.x -= 1 # 左に移動
	if Input.is_action_pressed("ui_right"):
		v.x += 1 # 右に移動
	
	position += v.normalized() * spd
```

`KinematicBody2D` を使用する場合には、移動処理は `move_and_collide()` などを使用することになりそうですが、`Area2D` を使用する場合には `position` を直接変更して問題なさそうです。

## 入力ボタン設定
デバイスからの入力を判定するボタン（キー）は、メニューの「プロジェクト > プロジェクト設定」から、`インプットマップ`のタブを選ぶことで確認することができます。
![Godot_Engine_-_TestGame_-_HPBar_tscn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/a41f0d1d-f3ea-73cf-8629-48658327e511.png)

# スクリプトとシーンの切り替え
エディタの上にある「2D」と「Script」をクリックすることで切り替えられます。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/6eade216-4325-4f34-14b7-87e5d6ec88f9.gif)

# 「出力」ウィンドウを非表示にする
スクリプトを編集するときに非表示にしておきたい「出力」ウィンドウ。これを非表示にするには、左下の「出力」の文字をクリックします。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/d2446798-8ae5-b1f9-d25f-126e36f4dfdd.gif)

# HPバーの作り方
HPバーは、`TextureProgress` を使うと簡単に実装できます。
Textureの Over と Under に画像を登録して……
![Godot_Engine_-_TestGame_-_HPBar_tscn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/c44269a9-2908-fe5f-4305-a52fa5509b16.png)

※画像サイズは実際に表示するサイズに合わせます。例えば横幅 400px にしたい場合は、幅を 400px にした画像を用意します。

そうすると、valueの値を書き換えることで幅が変化します。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f38323238326164382d383962372d623361652d306661642d6632373032336330656233372e676966.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/0dd63ed5-c5c6-f40d-fa0f-2c3151500d18.gif)

以下はボスからHPバーの値を書き換えるコードです

```gd:Boss.gd
func _physics_process(delta):
	……

	# HPバー更新
	var hpbar:TextureProgress = $"../HPBar"
	var hpratio = _hpratio() # 残りHPの割合を 0.0〜1.0 で取得
	# valueに 0〜100 を設定することで幅が変化する
	hpbar.value = 100 * hpratio
	
	# HPバーの色を更新
	hpbar.tint_progress = lerp(Color.red, Color.limegreen, hpratio)
```

# シーンを単体で確認する方法
Godotの便利な機能として、シーン単体を簡単に確認することができます。
まずは確認したいシーンを開きます。ここでは "Enemy.tscn" シーンを開きます。
![Godot_Engine_-_TestGame_-_Enemy_tscn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/c3865d2a-0d57-4489-4837-cb0f472c5491.png)

そして、右上の「編集したシーンを実行」のボタンをクリックします
![Godot_Engine_-_TestGame_-_Enemy_tscn.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/6f56fbc5-42ee-2460-5438-b6a5f250334b.png)

すると、シーン単体を動作確認できます。
![shot.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/bc6bde74-f9ff-fff7-a65c-d0596dcfe37b.gif)

エディタで設定した値を反映することもできるので、敵の弾幕パターンを確認するなどのときにはとても便利なのではないかと思います。

なお、実行中のアプリケーションに対してエディタの値をリアルタイムで反映するには、変数の前に `export` をつける必要があります

```gd:Enemy.gd
# 変数 "id" を実行時に編集できるようにする
export var id = 0
```

# 常にロードされるシーンを設定する
Godotでは `preload()` で外部のシーンを読み込むことができますが、C++のような staticクラス・static変数・シングルトンを利用するには、常にロードする設定にしておくと便利です。

設定方法としては、メニューから「プロジェクト > プロジェクト設定」を選択して……
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f65373761366336662d323232372d633736352d643866382d6239353139366338656331352e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/8d1cec09-aff0-dc77-9293-c93351b7f138.png)
「自動読み込み」タブを選び、対象のパスとノード名を指定して、「追加」ボタンをクリックします。

# シーンの遷移
シーンの遷移は `get_tree().change_scene()` で対象のシーンを指定することで行えるようです。

```gd:Main.gd
func _process(delta):
  ……	

  if is_instance_valid(_player) == false:
    # プレイヤーが消滅したら ”GAME OVER” のキャプションテキストを設定
    _caption.text = "GAME OVER\n\nPRESS SPACE KEY"
    if Input.is_action_just_pressed("ui_select"):
      # リスタート (Mainシーンの読み込み)
      get_tree().change_scene("res://Scenes/Main.tscn")
```

# コルーチンの利用方法
指定秒数後に弾を発射する、という処理をしたい場合は以下の記述で実装できます

```gd:Enemy.gd
# 弾を撃つ
func bullet(deg, speed, delay=0):
  if delay > 0:
    # 遅延して発射する
    yield(get_tree().create_timer(delay), "timeout")
	
  var bullet = Bullet.instance()
  bullet.start(position.x, position.y, deg, speed)
  # ルートノードを取得
  var main_node = get_parent()
  main_node.add_child(bullet)
```

`yield(get_tree().create_timer([遅延秒数]), "timeout")`

ただこれを使うと実行しているコルーチンを停止して、新しくコルーチンを作る方法がわからなかったので、敵のAI処理は以下のように記述しました。

```gd:Enemy.gd
export var id = 0
var id_previous = 0
var _wait = 0
# 初期化フラグ
var is_init = false
var func_ai = null

func wait(t):
	_wait += t

func ai_1():
	# 高速狙い撃ち弾を撃つコルーチン
	while true:
		wait(1)
		yield()
		while true:
			for i in range(5):
				aim(700, i * 0.05)
			wait(0.8)
			yield()

# その他様々な AIのコルーチン
func ai_2():
func ai_3():
func ai_4():
……

func _physics_process(delta):
	
	if id != id_previous:
		# idが切り替わった
		id_previous = id
		is_init = false # 初期化し直す
		_wait = 0
	
	if is_init == false:
		# 初期化
		is_init = true
		# コルーチンを生成
		func_ai = ai_1() # ここを "id" の値で呼び出しを切り替える
		
	_wait -= delta
	if _wait <= 0:
		if func_ai:
			func_ai = func_ai.resume()
```
コルーチンを変数 `func_ai` に格納して `resume()` を呼び出すことでコルーチンの切り替えをできるようにしました。ただこの方法だと `_process()` の呼び出しに遅延が出てしまうので、弾幕が不揃いになってあまりキレイにならないという問題があります。
見た目の問題なので今回はそのままとしましたが、ひょっとしたら何かもっと良い方法があるのかもしれません。

# プロジェクトの構成
今回のプロジェクトは以下のような構成としました。

```
godot_stg_test
  +-- *.png: 各種画像リソース
  +-- Scene
  |    +-- *.tscn: 各種シーンファイル
  |
  +-- Scripts
       +-- *.gd: 各種スクリプトファイル
```

Unityにならって、"Scenes"、"Scripts" フォルダを作成しましたが、Godotの場合は シーンとスクリプトは同一フォルダにした方が良さそうです。

# ウィンドウサイズの変更
メニューから プロジェクト > プロジェクト設定 を選択します。
`一般 > Display > Window` から WidthとHeightの値を変更すると画面サイズが変更できます。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f34373732626436322d393135642d323565642d653935632d3730363137373233353465652e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/ece82067-6665-800f-7bff-ef3752d3dda5.png)

# フォントサイズの変更
フォントサイズを変更する場合は、Scaleの値を変更することになりますが、線形補間処理となるため文字がぼやけてしまいます。
そのままサイズを変更する方法がわからなかったので、Custom Fonts にフォント (DynamicFont) を追加することで対応しました。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f62613638643134392d376163372d626663662d646264652d3231396432366239303032662e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/b2050624-c932-fb70-fc5e-fa672295efd4.png)

インスペクタから `Custom Fonts > Font` に "新規 DynamicFont" を設定して
……

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f61383366663435392d656130362d613230332d333166352d6232366462356331386261642e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/307b0071-0dc2-f38a-2b6f-f528bbaacbd1.png)
任意のフォントリソースを追加して……

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f64316663383962662d363633302d663430332d383436632d3230373031313831366537632e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/7ef55f57-412f-b5d1-523c-985330140b80.png)

`Font > Font Data` にフォントリソースを登録します。

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f31396137363439652d313964302d666136352d313366642d3931646466363535356335362e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/a409ffee-0b4c-5710-78e6-b28a50b63a55.png)
すると、`Font > Settings > Size` からフォントサイズを変更できます。

# パーティクルの実装
色々実装方法はありそうですが、今回は `CPUParticle2D` で実装してみました
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f33323038313035342d356462342d396338612d653438362d6136353264666136386332312e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/abcc523d-6b63-3614-48dd-e92f2ce45969.png)

`Drawing の Texture` に画像を設定するとアニメーションが再生されるようになります。
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f38623335393537632d336239362d626631392d353462662d3339326331323966666330382e676966.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/84bf323a-67c0-52a1-8ed6-07ef5919623b.gif)
## パーティクルのパラメータ設定例

* `Direction > Spread` の値を「180」（発射範囲を±180度）
* `Initial Velocity > Velocity`を「200」（初期速度）
* `Initial Velocity > Velocity Random`を「1」(完全にランダム)

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f63353331393638362d623133392d343864652d643662322d6239363438353235303634322e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/c1904d52-fd92-11ce-2345-0361398e9ce4.png)

* Amount: 16
* Time > Lifetime: 2
* Time > One Shot: チェックを入れる
* Time > Speed Scale: 2
* Time > Explosiveness: 1
* Time > Lifetime Random: 0.5

![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f33323064623166322d666135342d373666632d303336632d3139323934363861346232332e706e67.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/96cebee7-165a-d327-98dc-1067673d05ec.png)

## パーティクルにアタッチするスクリプト
アタッチするスクリプト `Particle.gd` には以下のように記述しました

```gd:Particle.gd
extends CPUParticles2D

func start(x, y, c):
    # 位置を設定
    position = Vector2(x, y)

    # 色を設定
    color = c

    # 放出開始
    emitting = true

func _ready():
    pass # Replace with function body.

func _process(delta):
    if emitting == false:
        # 放出が終了
        queue_free()
```

## パーティクルを呼び出すスクリプト
Enemy.gd からのパーティクル呼び出しは、以下のようにしました

* Particle.tscn をプリロードする
* destroy() 関数を作成する
* 敵破壊時に destroy() を呼び出す

```gd:Enemy.gd
extends Area2D

# パーティクルをプリロード
var Particle = preload("res://Scenes/Particle.tscn")

func destroy():
    var p = Particle.instance()
    p.start(position.x, position.y, Color.green)
    var main_node = get_parent()
    main_node.add_child(p)
    queue_free()
```

敵が消滅するときに、このパーティクルが生成されます
![68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f34333737322f39653032636532342d613239322d663534322d623739382d3461646461633830396161362e676966.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/43772/f1b40f09-0346-8d87-8ea1-2bc7c59ff7eb.gif)

## 2021.2.22 追記
2Dパーティクルについてさらに調べてみました
→[【Godot】2Dパーティクルの使い方](https://qiita.com/2dgames_jp/items/2a37c75e9bf65687bfaf)

# リンク
さらに調査したことをまとめてみました
↓
↓
↓

* [【Godot】Godot EngineのTIPS](https://qiita.com/2dgames_jp/items/d150aa1cc98f200292ab)