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にしましょう。
これで、あたり判定の形と大きさはこれぐらいだよ、という設定ができました。
最後に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という関数を独自に使って、入力処理をまとめています。
大まかには下記の二段階であるため、スプライトの移動で説明した時と構成は同じです。
- キー入力にまつわる処理をしてその情報をまとめる
- 情報をもとに移動処理を行う
今回は、一つ目の工程を関数にまとめています。
状態の更新など、長々となってしまうものは、明瞭な区切りごとに関数を作って処理を分けると、見通しが良くなります。
では、一つ目の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にするものです。
ノーマライズをしなかったら、どうなるかを考えてみましょう。
- Wキーを押しているとき、xは1、yは0、そのため距離は1
- 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(長方形)を選択します。
RectangleShapeの設定を押して、Sizeをxを400、yを40くらいにしてみましょう。
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のアクションゲームの基本が詰まっています。さらに、シグナルについては、他の分野でも活躍する概念です。
次回は、ノードをまとめる、というのさらなる構造化について説明してみたいと思います。