🤔

【Godot Engine】CollisionObject2D系ノードの違いを明確にする【GDScript】

2024/01/24に公開

動作検証:Godot Engine 4.2.0

CollisionObject2Dについて

CollisionObject2Dは当たり判定や衝突、物理演算を行うノードが継承する抽象基底クラスです。
そのものを使うわけではありませんが、CharacterBody2DやRigidBody2Dなどのノードが継承しています。

ノード一覧からも分かる通り継承関係は以下のようになっています。

当たり判定を行うために必要なこと

これらのノードは当たり判定を行うための形状(シェイプ)を必要とします。
そのためにCollisionShape2D(あるいはCollisionPolygon2D)を子ノードとして追加し、シェイプを設定することで役割を果たします。

ケースによっては特定のオブジェクトとだけ衝突させたい場合があるでしょう。
そんな時にcollision_layercollision_maskを設定します。

つまり、collision_layerでオブジェクトの当たり判定が属するレイヤーを決めた後
collision_maskで衝突と検知の対象となるレイヤーを決めるわけです。

例えば以下のようにcollison_layer/maskが設定されているとします。

Scene collision_layer collision_mask
Player 1 2
Enemy 2 1

これは
・PlayerはEnemyに対して衝突し、Enemyを検知できる
・EnemyはPlayerに対して衝突し、Playerを検知できる
ということ意味しています。

ここで「PlayerがEnemyとマスクしているのになぜEnemy側でもマスクする必要があるのか?」
という疑問が生じるかもしれません。

もし仮にEnemy側でマスクをしなかった場合、EnemyはPlayerと衝突時にPlayerを
押し退ける上に、Enemy側でPlayerをmove_and_collide等のメソッドで検知できません。


双方でマスクされているケース


Enemy側でマスクしていないケース

もちろん、双方でマスクしない場合は互いに貫通します。

collision_layer/maskとビット演算

collision_layer/maskは複数設定することができます。インスペクタ上で設定すれば直感的ですが、スクリプトで設定する際はビット列として考えるほうが理にかなっています。

まず、collision_layer/maskはint型ですので、設定パターンを整数で表現する必要があります。
collision_layer = 2と代入すればインスペクタ上で[2]と設定したのと同じことですが、
collision_layer = 3を代入した場合はどうでしょうか。実はこれは[1,2]に設定することになります。
では[2,3]に設定したい場合はどうでしょうか。2+3=5でcollision_layer = 5でしょうか?

違います。collision_layer = 6が正解です。
ここでビット演算の知識が役に立ちます。

まず、Godot Engineでcollision_layer/maskの設定可能な最大パターン数は2^32です。

インスペクタ上でcollision_layerが[2]に設定されているとき、
最下位ビットを1ビット目として見ると2ビット目が1ということなので、
そのパターンを32bitのビット列で表現すると 0b000...010 となります。
同様に[3]に設定されているときは、
3ビット目が1ということなので 0b000...100 と表現できます。

なので[2,3]に設定したい場合は、そのビット列の和である
0b010 + 0b100 = 0b110(10進数で6) を代入すればよいということになります。

次のようなイメージを浮かべるとわかりやすいでしょうか。

[1,3,5,7]に設定したい場合はcollision_layer = 0b1010101と書けばいいわけです。

考え方としては以上ですが、別の考え方も紹介します。

10進数の和で求める

インスペクタ上でcollision_layer/maskの番号にマウスをかざしてみてください。
ビットの対応する値が表示されますよね。
あとはその番号に対応した値の和を求めればOKです。

シンプルですがcollision_layer = 20と書かれていてもあまりピンと来ないですね。
このくらいの数であれば20 = 16 + 4と見なして[5,3]に設定されているとわかるかもしれないですが、数が大きくなったらコメントを正確に残しておく必要があるでしょう。

move_and_collideについて

PhysicsBody2Dを継承したノード(Area2D以外のCollisionObject2D)はmove_and_collideというメソッドを持っています。
このメソッドの第一引数に1フレームあたりの移動量をVector2で与えて_physics_process内で呼び出すとオブジェクトが動き出します。
戻り値は現在衝突中のKinematicCollision2Dが得られますが、これ自体は衝突したオブジェクトではなく、衝突に関する情報みたいなものです。オブジェクトを取得するにはKinematicCollision2Dのget_collider()を呼び出すことで得られます。

ただしここで注意点があります。move_and_collideは「自分から」衝突したKinematicBody2Dを返すのであって、「自分に」衝突したKinematicBody2Dを返すわけではありません。
※CharacterBody2D専用メソッドのget_last_slide_collision()では向こう側からの衝突も検知できます。

const SPEED = 300
func _physics_process(delta: float) -> void:
	var direction := Input.get_vector("ui_left","ui_right","ui_up","ui_down")
	var kc := move_and_collide(SPEED*delta*direction)
	if kc:
		print(kc.get_collider().name)

deltaは前回の_physics_process呼び出しから現在までの時間差です。デフォルトの物理ティック(物理フレーム)数は60ですので、1/60≒0.0167秒毎に_physics_processが呼び出されることになります。
物理フレーム数はプロジェクト設定 -> 物理 -> 一般 -> 1秒あたりの物理ティック数
から変更できます。

衝突したオブジェクトを識別する

衝突したオブジェクトをクラス名をもとに識別する方法を紹介します。
class_nameでクラス名を宣言することで、そのクラス名はどこからでも参照でき、オブジェクトの識別に役立ちます。
以下の例ではPlayer側でEnemyを検知しています。

Enemy.gd
class_name Enemy
extends CharacterBody2D

func _physics_process(_delta: float) -> void:
	velocity = Vector2(100,0)
	move_and_slide() 
Player.gd
extends CharacterBody2D

const SPEED = 300
func _physics_process(_delta: float) -> void:
	var direction := Input.get_vector("ui_left","ui_right","ui_up","ui_down")
	velocity = direction*SPEED
	move_and_slide()

	var kc := get_last_slide_collision()
	if kc and kc.get_collider() is Enemy:
		print("Player collided with the Enemy!")

CollisionObject2Dを継承するノード

CharacterBody2D

キャラクターを動かすならその名の通り、このノードです。
テンプレートのスクリプトが充実しているので基本的な動作はすぐに実装できます。

基本的な使い方は以下の通りです。

  1. キーボード入力を受け取り、進行方向のベクトルを求める
  2. それを何倍かしてvelocity(速度)を決める
  3. move_and_slide()で動かす
  4. get_last_slide_collision()で衝突を検知する
extends CharacterBody2D

const SPEED = 300.0

func _physics_process(_delta: float) -> void:
	var direction := Input.get_vector("ui_left","ui_right","ui_up","ui_down")
	velocity = SPEED*direction # px/s
	move_and_slide()
	var collider := get_last_slide_collision()
	if collider:
		print("Collider: %s"%collider.get_collider().name)

CharacterBody2Dはmove_and_slide()が呼び出されるとvelocityに応じて動き出します。
衝突の有無は戻り値(bool)で得られます。move_and_collideと違ってdeltaを乗算する必要はありません。

もちろんmove_and_collideで動かすこともできますが、is_on_floor()などの関数は最後に呼び出されたmove_and_slide()をもとに判定されるので、move_and_slide()で実装したほうが良さそうです。

is_on_wall()などの便利な関数も用意されているので、壁ジャンプも簡単に実装できちゃいます。

以下のスクリプトで実装できます。これはテンプレートに一部変更を加えたものです。

player.gd
extends CharacterBody2D

const SPEED = 300.0
const JUMP_VELOCITY = -400.0
var gravity: int = ProjectSettings.get_setting("physics/2d/default_gravity")

func _physics_process(delta: float) -> void:
	if not is_on_floor():
		velocity.y += gravity * delta

	if Input.is_action_just_pressed("ui_accept") and (is_on_floor() or is_on_wall()):
		velocity.y = JUMP_VELOCITY

	var direction := Input.get_axis("ui_left", "ui_right")
	if direction:
		velocity.x = direction * SPEED
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	move_and_slide()

RigidBody2D

CharacterBody2Dが自由に動かせる物体だとすればこっちは重力などの外部からの力で振る舞いが決まる物体です。操作して動かすというよりは力を加えて動かします。デフォルトの状態ではシーンに追加されればすぐに自由落下します。
あくまでapply_force等のメソッドで力を加えて運動をシミュレートすることが目的のノードですのでmove_and_collideは呼び出さないほうがいいでしょう。

衝突検知はcontact_monitortrueにしたのちmax_contacts_reportedに適当な値にセットしてからget_colliding_bodies()(あるいは専用シグナル)で検知します。

使いこなすとなると力学の知識も求められますが、慣性が絡むゲームなどでは重宝します。

StaticBody2D

外部の力による影響を受けない物体です。
主に床や壁といった動かすつもりのない物体の実装に適しています。

move_and_collideで動かしても他の物体を押し退けることは無い・・・のですが、
positionを変更して動かした場合はRigidBody2D相手なら押せてしまいます。
しかし力を加えている訳では無いのか、慣性で進み続けることは無いようです。
公式ドキュメントには「手動で動かしても経路上の他のボディに影響を与えない」とありますが、力を加えないという意味合いでしょうか。逆にAnimatableBody2Dでは力が加わったような動きをします。

extends StaticBody2D # or AnimatableBody2D
const SPEED = 300
func _physics_process(delta: float) -> void:
	var direction := Input.get_vector("ui_left", "ui_right", "ui_up", "ui_down")
	position += SPEED*delta*direction

AnimatableBody2D

StaticBody2Dと同じく外部の力の影響を受けません。
しかし動かした場合、経路上の他のボディに影響を「与える」ようです。

sync_to_physicsというプロパティを持っており、ドキュメントには
「動作が物理フレームと同期されるよ。AnimationPlayerで動かすときに便利だよ。trueにしたらmove_and_collideを呼び出さないでね。」
といったことが書かれています。
動作が物理フレームと同期されるのは良いことのはずで、デフォルトのままtrueで良いかと思われます。
ただこの状態でmove_and_collideを呼び出すとエラーになるのでこれに関してはpositionを変更して動かす必要があります。

ちなみにStaticBody2Dと違ってRigidBody2Dと衝突すると力が加わり慣性で移動を続けます。

Area2D

その名の通り領域を持ち、出入りする物体(あるいは別の領域)を検知できます。
シグナルで出入りを検知することもできますし、get_overlapping_bodies()で取得することもできます。
ちなみにArea2DはCollisionObject2Dを継承していますが、PhysicsBody2Dは継承していないのでmove_and_collideを持たず、衝突した物体が止まることもありません。

Discussion