Godot 4.xで学ぶ2Dゲーム開発の基礎:移動、壁、コイン取得の仕組み

2024年11月29日

はじめに

今回はGodot4.xの2Dゲームの基礎の一つ、あたり判定をやってみましょう。

あたり判定を作り、壁に衝突したり、コインをとったりする、そうした基礎の説明をしていきます。

物理エンジン用の移動処理

まずは、スプライトを一つ登録して、キー入力、WASDキーで上下左右に移動できるようにしていきましょう。

インプットマップはそれぞれ、"move_up"にWキー、"move_down"にSキー、"move_left"にAキー、"move_right"にDキー、です。

これが終われば、プレーヤーキャラクターのSprit2DをPlayerにし、画像を設定し、見えるサイズに調整します。

つづいて、RigidBody2Dを大元のノードとして追加します。さらにそこに子のCollisionSprite2Dと、Sprite2Dを追加します。

その後、RigidBody2Dは名前を変更し、Playerとしておきましょう。

最初に作る構造について説明する画像

この構造の理由とノードの説明を簡単にしましょう。

今回は物理エンジンを用いた制御をするので、RigidBody2Dを親にします。これで、2Dの物理エンジンにまつわる制御が可能です。

その子供として、ノードを紐づけることで、親が移動すると、子のノードもそれに追従します。

Sprite2Dは、このノード群の画像を担当させます。

続いて、CollisionSprite2Dは、RigidBody2Dの物理エンジン上の形を定義させるためのノードです。長方形なのか円なのか、そういったことが設定できます。

では、それぞれのノードの設定を、インスペクターで行って生きましょう。

Sprite2Dは、画像をファイルシステムに入れて、インスペクターで画像を設定、します。下記の画像a_05_002.pngを使う前提で話を進めます。

スプライト画像

こちらの画像は小さいので、Sprite2DのTransformでScaleのx、yの値をそれぞれ10にして大きくします。

その後、親のRigidBody2Dを選択して、画面中央に移動させましょう。

移動のさいは、間違って、Sprite2Dを移動させてしまうことが多いので、Sprite2Dを選択し、いったん操作できないようにロックをかけると、本体であるRigidBody2Dをドラッグして移動させやすいです。

ロックすると、2Dビューや、3Dビュー上での操作ができなくなるので、不用意に動かしたくないときはロックしましょう。

画像を設定して、中央に配置しなおす説明画像

CollisionShape2Dはインスペクターで、ShapeをまずCircleShapeを選択します。

そして、CircleShapeの大きさを調整します。Rediusを33pxにしましょう。

これで、あたり判定の形と大きさはこれぐらいだよ、という設定ができました。

CircleShapeの設定を説明する画像1

CircleShapeの設定を説明する画像2

最後にRigidBody2Dの設定です。インスペクタで、Gravity Scale を0にします。今回は重力はいらないので無効にします。

さらに、Linear > Damp 5に設定し、移動量が時間経過で減るようにします。これは床の摩擦力だと考えるといいでしょう。

重力と摩擦の設定する画像

つづいて、RididBody2Dのノード名をPlayerにし、スクリプトを作成、下記のようなコードにします。

extends RigidBody2D

# 移動速度を指定
@export var move_speed: float = 800.0

# 入力処理をまとめたもの
func get_input_vector() -> Vector2:
	var input_vector = Vector2.ZERO
	if Input.is_action_pressed("move_up"):    # Wキー
		input_vector.y -= 1
	if Input.is_action_pressed("move_down"):  # Sキー
		input_vector.y += 1
	if Input.is_action_pressed("move_left"):  # Aキー
		input_vector.x -= 1
	if Input.is_action_pressed("move_right"): # Dキー
		input_vector.x += 1
	return input_vector.normalized()

# _physics_process は物理エンジンに基づく処理を行う
func _physics_process(delta: float) -> void:
	var input_direction = get_input_vector()
	
	if input_direction != Vector2.ZERO:
		# 入力方向に力を加える
		apply_central_impulse(input_direction * move_speed * delta)

スクリプトの内容について説明します。

_physics_processは、物理エンジンに関わる更新処理を行うときに利用します。

get_input_vectorという関数を独自に使って、入力処理をまとめています。

大まかには下記の二段階であるため、スプライトの移動で説明した時と構成は同じです。

  1. キー入力にまつわる処理をしてその情報をまとめる
  2. 情報をもとに移動処理を行う

今回は、一つ目の工程を関数にまとめています。

状態の更新など、長々となってしまうものは、明瞭な区切りごとに関数を作って処理を分けると、見通しが良くなります。

では、一つ目のget_input_vectorの中を見ていきましょう。

var input_vector = Vector2.ZEROは、座標の変数を0で初期化しています。

この変数は、xとyのパラメータをもっています。Vector2となっているのは二次元用だからです。Vector3という三次元用のものもあります。

input_vector.x = 100のように、個別にxとyのパラメータを設定したり、利用したりすることができます。

私.右手のように、プログラムでは、私の右手、をこのように表すことがあります。.で、中のパラメータを見たり、中の処理を呼び出したりできます。

私.叫ぶ()ということも、雰囲気としてはありです。

今回の_physics_processの行頭では、var input_direction = get_input_vector()となっており、get_input_vectorの手前に何もありません。これは、主語を省いている、と考えると分かりやすいです。省略せずに書くと、self.get_input_vector()です。

話はもどって、キー入力に対応するようinput_vecotrの値を設定していくのがif文の個所です。

その後、最終結果をreturn input_vector.normalized()で、関数の呼び出して結果とし返しています。

さて、input_vectorのままではなく.normalized()がついているのはどういうことでしょう。

まず、おさらいです.をつけて、中の処理、normalizedを呼び出しています。

この処理は、ノーマライズ、正規化といいます。ベクトル(行列)の場合は、距離を1にするものです。

ノーマライズをしなかったら、どうなるかを考えてみましょう。

  1. Wキーを押しているとき、xは1、yは0、そのため距離は1
  2. WキーとDキーを押していると、xは1、yは1、そのため距離は2の平方根のため約1.414です

つまり、そのままにしていると、斜め移動は移動距離が多くなってしまうんです。

この移動距離を均等に1にしてくれるのがノーマライズです。この方が感覚的に優れていますね。

距離の計算は、xの二乗 + yの二乗 の平方根でもとまります。

ノーマライズをする、しないの比較説明画像

では、_physics_process関数の中を見てみましょう。

var input_direction = get_input_vector()は、get_input_vectorで計算した結果をinput_directionに代入しています。

では、次のif文、if input_direction != Vector2.ZERO:、ここは何をしているのでしょうか?

!=ですので、一致しない場合に、if文の中の処理をします。そのため、ここでは、input_directionの中のxとyの値が両方0でない場合、中の処理に進みます。

もっとざっくり表現すると、移動しているかどうか、確認しているわけです。

いった、実際に動かしてみましょう。

壁を作ってみよう

当たり判定があり、移動を阻害する壁を作ってみましょう。

動かない壁のため、StaticBody2Dを作ります。そこに、子ノードとしてCollisionShape2Dを作り、Shapeには、RectangleShape(長方形)を選択します。

壁の作成説明画像1

RectangleShapeの設定を押して、Sizeをxを400、yを40くらいにしてみましょう。

壁の作成説明画像2

Sprite2Dを追加して、絵も添えてみましょう。

壁の画像は下記を使ってみてください。

壁の画像

手順はSprite2Dを、StaticBody2Dの子として作ります。次に、ファイルシステムに、画像を入れます。Sprite2Dの画像を設定し、大きさが合わないのでScaleをx,yを10にします。

壁の画像

その後、壁、を適当な場所に移動させます。まず、CollisionShape2DとSprite2Dはロックし、StaticBody2Dを2Dビュー上で選択して移動させるといいでしょう。

壁の画像

壁はこんな形で簡単に作れます。ぶつかるかどうか試してみてください。

ちなみに、StaticBody2Dの中に複数のCollisionShape2DやSprite2Dをいれて、複雑な形にすることも可能です。

コインを配置してみよう

こんどは取得するコインを作ってみましょう。

Area2Dのノードを作り、その子供に、CollisionShape2D、Sprite2Dをつけます。

画像は下記のコインの画像を使ってみてください。CollisionShape2DやSprite2Dの手順はこれまでと変わりません。円の形がいいでしょう。

コインの画像

CollisionShape2DのShapeはCircleShape、Radiusは30にしました。

コインのノード構造の説明の画像

Area2Dの名前をCoinにしましょう。

Area2Dは、エリアに侵入した時にどうするか、設定できるノードです。

Coinのノードを選択し、スプライトを作成し、名前はcoin.gdとします。コードは下記のとおりです。

extends Area2D

# コインが取得されたときの信号を定義
signal coin_collected

# プレイヤーとの衝突を検知
func _on_area_entered(body):
	if body.name == "Player":  # プレイヤーのノード名を確認(名前を"Player"に設定してください)
		emit_signal("coin_collected")  # コイン取得の信号を発信
		queue_free()  # コインを削除(消える)

ここの内容は大まかには、Playerがエリアに侵入したら、"coin_collected"という信号を発信して、自身は消滅してください、というものです。

続いて、実際に、 _on_area_enterd関数が呼び出されるように設定をしましょう。

Coinノードを選択し、右上の、ノードを選択します。するとシグナル一覧が表示されます。

シグナルの表示の説明の画像

このシグナルから、Area2D > body_entered をダブルクリックします。

すると、ウィンドウが表示されるので、まずはCoin(接続元)のノードを選択しまず。そして、受信側メソッドの下の選択ボタンを押して、_on_area_entered を選択します。

シグナルと関数の接続の説明の画像

これで、PlayerがCoinのエリアに入ると、_on_area_entered が実行されます。試しに実行してみましょう。コインが消滅すれば成功です。

まだ、Playerは何が起こったか分かっていません。Playerにコインをとったよ、と知らせてあげるために、Playerのスクリプトを下記のように書き換えます。

extends RigidBody2D

# 移動速度を指定
@export var move_speed: float = 800.0

# スコアを管理する変数
var score: int = 0  

# 入力処理をまとめたもの
func get_input_vector() -> Vector2:
	var input_vector = Vector2.ZERO
	if Input.is_action_pressed("move_up"):    # Wキー
		input_vector.y -= 1
	if Input.is_action_pressed("move_down"):  # Sキー
		input_vector.y += 1
	if Input.is_action_pressed("move_left"):  # Aキー
		input_vector.x -= 1
	if Input.is_action_pressed("move_right"): # Dキー
		input_vector.x += 1
	return input_vector.normalized()

# _physics_process は物理エンジンに基づく処理を行う
func _physics_process(delta: float) -> void:
	var input_direction = get_input_vector()
	
	if input_direction != Vector2.ZERO:
		# 入力方向に力を加える
		apply_central_impulse(input_direction * move_speed * delta)
		
# コイン取得時の処理
func _on_coin_collected():
	score += 1
	print("スコア: ", score)

全部載せているので、主要なところを説明します。まず、冒頭にコイン獲得数を示すスコアの変数を定義しました。var score: int = 0の個所です。そして、_on_coin_collected という関数を作りました。ここで、コイン獲得時の処理を書いています。

さて、こんどは "coin_collected" が発信されたら _on_coin_collected が呼び出されるようにしましょう。

まず、Coinを選択し、ノードの画面から、coin.gd > coin_collected() をダブルクリックし、接続する関数のウィンドウを開きます。

次に、Playerのノードを選択し、接続する関数として _on_coin_collected を選択します。

これで実行し、コインを取得すると、アウトプットパネルに、"スコア : 1" と表示されます。

このように、シグナルを発信させることで、ノードをまたいで処理を起こす、引き継ぐことができます。

おわりに

今回は、2D物理エンジンの基礎とシグナルについて説明しました。

コインをコピーペーストして、複数配置してみるだけでもまた違った形になりますし、壁を増やしてステージづくりもしていけます。

今回は、2Dのアクションゲームの基本が詰まっています。さらに、シグナルについては、他の分野でも活躍する概念です。

次回は、ノードをまとめる、というのさらなる構造化について説明してみたいと思います。