【Godot Engine】CollisionObject2D系ノードの違いを明確にする【GDScript】
動作検証:Godot Engine 4.2.0
CollisionObject2Dについて
CollisionObject2Dは当たり判定や衝突、物理演算を行うノードが継承する抽象基底クラスです。
そのものを使うわけではありませんが、CharacterBody2DやRigidBody2Dなどのノードが継承しています。
ノード一覧からも分かる通り継承関係は以下のようになっています。
当たり判定を行うために必要なこと
これらのノードは当たり判定を行うための形状(シェイプ)を必要とします。
そのためにCollisionShape2D(あるいはCollisionPolygon2D)を子ノードとして追加し、シェイプを設定することで役割を果たします。
ケースによっては特定のオブジェクトとだけ衝突させたい場合があるでしょう。
そんな時にcollision_layer
とcollision_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を検知しています。
class_name Enemy
extends CharacterBody2D
func _physics_process(_delta: float) -> void:
velocity = Vector2(100,0)
move_and_slide()
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
キャラクターを動かすならその名の通り、このノードです。
テンプレートのスクリプトが充実しているので基本的な動作はすぐに実装できます。
基本的な使い方は以下の通りです。
- キーボード入力を受け取り、進行方向のベクトルを求める
- それを何倍かして
velocity
(速度)を決める -
move_and_slide()
で動かす -
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()
などの便利な関数も用意されているので、壁ジャンプも簡単に実装できちゃいます。
以下のスクリプトで実装できます。これはテンプレートに一部変更を加えたものです。
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_monitor
をtrue
にしたのち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