モンスターをジャンプして踏みつける
このパートでは、モンスターをジャンプして踏みつける機能を追加します。次のレッスンでは、モンスターが地面でプレイヤーに当たったときにプレイヤーが死亡するようにします。
まず、物理的な相互作用に関連するいくつかの設定を変更する必要があります。 物理レイヤー の世界に入りましょう。
物理的相互作用のコントロール
物理ボディには、レイヤーとマスクという2つの補完的なプロパティが用意されています。レイヤーは、オブジェクトがどの物理レイヤー上に存在するかを定義します。
マスクは、物体が反応して検出するレイヤーを制御します。これは衝突検出に影響を与えます。2つの物体を相互作用させたい場合、少なくとも一方のマスクがもう一方のレイヤーに対応している必要があります。
もしこれが分かりにくい場合でも心配しないでください。すぐに3つの例を見ていきましょう。
重要なポイントは、レイヤーとマスクを使用して物理的な相互作用をフィルタリングし、パフォーマンスを制御し、コード内で追加の条件を不要にできることです。
デフォルトでは、すべての物理ボディとエリアはレイヤーとマスクのどちらも 1 に設定されています。これは全てお互いに接触することを意味します。
物理レイヤーは数字で表現されますが、名前をつけることで何が何なのかの経過を追うことができます。
レイヤー名の設定
物理レイヤーに名前を付けましょう。プロジェクト -> プロジェクト設定...(Project -> Project Settings)に移動してください。

左のメニューで、Layer Names -> 3D Physics (3D物理)のところまで移動します。右側に、それぞれのレイヤーの右横にフィールドがあるレイヤーのリストが表示されます。そこでレイヤーの名前を設定することができます。最初の3つのレイヤーにそれぞれ player, enemies, worldと名前を付けてください。

これで、物理ノードにそれらを割り当てることができます。
レイヤーとマスクの割り当て
Main シーンで Ground ノードを選択します。 インスペクター 内の Collision セクションを展開すると、ノードのレイヤーとマスクがボタンのグリッドとして表示されているのが確認できます。

Ground は world の一部で、3つ目のレイヤーなのがふさわしいです。Layer の最初の点灯しているボタンをクリックして off に切り替え、3つ目を on に切り替えます。そして、 Mask をクリックして off に切り替えます。

前に言及した通り、 Mask プロパティはノードが他の物理オブジェクトとの相互作用をリッスンできるようにするものですが、今はコリジョンは必要ありません。 Ground は何もリッスンする必要がありません; ただクリーチャー達が落ちないようにするためだけにあります。
なお、プロパティの右側にある "..." ボタンをクリックするとで名前付きチェックボックスのリストが表示されます。

次は Player と Mob です。 ファイルシステム ドックのファイルを ダブルクリックして player.tscn を開きましょう。
Playerノードを選び、 Collision -> Mask を "enemies" と "world" 両方に設定しましょう。最初のレイヤーは "player" なので、デフォルトの Layer プロパティはそのままで大丈夫です。

そして、 mob.tscn をダブルクリックして Mob シーンを開き、 Mob ノードを選択しましょう。
これのCollision -> Layerを "enemies" に設定し、Collision -> Maskを解除してマスクを空にします。

これらの設定はモンスターが互いをすり抜けることを意味します。もしモンスターを互いにぶつかりスライドさせたいなら、"enemies" マスクを on にします。
注釈
モブはXZ平面上でしか動かないので"world"レイヤーをマスクする必要はありません。また、デザイン上、モブには重力をかけません。
ジャンプ
ジャンプに必要なコードは 2 行のみです。 Player スクリプトを開きます。ジャンプの強さを制御する値が必要で _physics_process() にジャンプのコードを記述します。
スクリプトの一番上、fall_accelerationを定義した行の後に、jump_impulseを追加してください。
#...
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
// Don't forget to rebuild the project so the editor knows about the new export variable.
// ...
// Vertical impulse applied to the character upon jumping in meters per second.
[Export]
public int JumpImpulse { get; set; } = 20;
_physics_process() 内で、 move_and_slide() コードブロックの前に、以下のコードを追加します。
func _physics_process(delta):
#...
# Jumping.
if is_on_floor() and Input.is_action_just_pressed("jump"):
target_velocity.y = jump_impulse
#...
public override void _PhysicsProcess(double delta)
{
// ...
// Jumping.
if (IsOnFloor() && Input.IsActionJustPressed("jump"))
{
_targetVelocity.Y = JumpImpulse;
}
// ...
}
ジャンプに必要なのはこれだけです!
is_on_floor() メソッドは CharacterBody3D クラスのためのツールです。これはこのフレーム中にボディが床と衝突していた場合に true を返します。これが Player に重力を適用した理由です: モンスターのように浮遊させず床と接触させるためです。
キャラクターが床にいてプレイヤーが "jump" を押せば、即座に垂直方向の速度を付与します。ゲームでは、操作は即座に反応するのがとても重要で、またこのような瞬間的な加速は不自然でも気持ちがいい物です。
Y軸は上向きが正になることに注意してください。2Dでは下向きが正だったのとは異なります。
モブを踏みつける
次に押しつぶしの仕組みを追加してみましょう。キャラクターをモンスターの上で跳ねさせながら、同時にモンスターを倒せるようにするのです。
モンスターとの衝突を検出し、それを床との衝突と区別する必要があります。そうするには、Godotの group タグ機能を使用します。
mob.tscn シーンを再度開き Mob ノードを選択しましょう。 シグナルリストの右にある ノード ドックに行きます。 ノード ドックには2つのタブがあります。 シグナル は既に使った通りで、 グループ はノードにタグを割り当てることができます。
それをクリックしてタグ名を記入するフィールドを出しましょう。フィールドに "mob" と入力し、追加(Add) ボタンをクリックします。

シーン(Scene) ドックに、ノードが少なくとも1つのグループに属していることを示すアイコンが表示されます。
![]()
これで、コード上からグループを使用して床との衝突とモンスターとの衝突とを区別することができるようになりました。
押しつぶしの仕組みをコーディングする
Playerスクリプトに戻り、押しつぶしと跳ね返りをコーディングします。
スクリプトの先頭には、bounce_impulseという別のプロパティが必要です。敵を潰したとき、キャラクターをジャンプするときほど高く上げる必要はありません。
# Vertical impulse applied to the character upon bouncing over a mob in
# meters per second.
@export var bounce_impulse = 16
// Don't forget to rebuild the project so the editor knows about the new export variable.
// Vertical impulse applied to the character upon bouncing over a mob in meters per second.
[Export]
public int BounceImpulse { get; set; } = 16;
次に、_physics_process() に追加した上記の** ジャンプ ** コードブロックの後に、以下のループを追加します。Godotエンジンは move_and_slide() を使う時、キャラクターを滑らかに動かすためにボディを複数回連続的に動かすことがあります。そのため起こっただろう全ての衝突についてループしなければなりません。
ループの各反復で、モブに着地したかどうかをチェックします。着地した場合、そのモブを倒して跳ね返します。
このコードでは、与えられたフレームで衝突が発生しなかった場合、ループは実行されません。
func _physics_process(delta):
#...
# Iterate through all collisions that occurred this frame
for index in range(get_slide_collision_count()):
# We get one of the collisions with the player
var collision = get_slide_collision(index)
# If there are duplicate collisions with a mob in a single frame
# the mob will be deleted after the first collision, and a second call to
# get_collider will return null, leading to a null pointer when calling
# collision.get_collider().is_in_group("mob").
# This block of code prevents processing duplicate collisions.
if collision.get_collider() == null:
continue
# If the collider is with a mob
if collision.get_collider().is_in_group("mob"):
var mob = collision.get_collider()
# we check that we are hitting it from above.
if Vector3.UP.dot(collision.get_normal()) > 0.1:
# If so, we squash it and bounce.
mob.squash()
target_velocity.y = bounce_impulse
# Prevent further duplicate calls.
break
public override void _PhysicsProcess(double delta)
{
// ...
// Iterate through all collisions that occurred this frame.
for (int index = 0; index < GetSlideCollisionCount(); index++)
{
// We get one of the collisions with the player.
KinematicCollision3D collision = GetSlideCollision(index);
// If the collision is with a mob.
// With C# we leverage typing and pattern-matching
// instead of checking for the group we created.
if (collision.GetCollider() is Mob mob)
{
// We check that we are hitting it from above.
if (Vector3.Up.Dot(collision.GetNormal()) > 0.1f)
{
// If so, we squash it and bounce.
mob.Squash();
_targetVelocity.Y = BounceImpulse;
// Prevent further duplicate calls.
break;
}
}
}
}
たくさんの新機能があります。それらの詳細について説明します。
関数 get_slide_collision_count() と get_slide_collision() はどちらも CharacterBody3D クラスのもので move_and_slide() に関連します。
get_slide_collision() は KinematicCollision3D オブジェクトを返し、このオブジェクトは衝突がどこでどのように起こったかの情報を持っています。例えば、"mob"と衝突したかを 確認するためにはこのオブジェクトの get_collider プロパティを使用して、さらにそれの is_in_group() を呼びます: collision.get_collider().is_in_group("mob")。
注釈
is_in_group()メソッドはすべての Node で利用可能です。
モンスターに着地したかを確認するためにベクトル内積を使います: Vector3.UP.dot(collision.get_normal()) > 0.1 。衝突の方線(normal)は衝突が起きた平面に対して垂直な3Dベクトルです。内積によってこのベクトルを上方向と比べることができます。
内積では、結果が 0より大きいとき、2つのベクトルは90度未満の角度にあります。値が0.1より高いなら、モンスターのほぼ上にいることが分かります。
押しつぶしと跳ね返りを処理するロジックの後、 mob.squash() が重複して呼ばれないよう break 文でループを早めに終了させます、そうしなければ一度のキルでスコアを重複してカウントしてしまうような意図しないバグが起きてしまいます。
今は mob.squash() という未定義の関数を呼んでいるので、これをモブクラスに追加しなければなりません。
ファイルシステムにあるmob.gdスクリプトをダブルクリックして開きましょう。スクリプトの先頭で、squashedというシグナルを定義します。そしてスクリプトの最後に、シグナルを発信しモブを破棄する squash 関数を追加しましょう。
# Emitted when the player jumped on the mob.
signal squashed
# ...
func squash():
squashed.emit()
queue_free()
// Don't forget to rebuild the project so the editor knows about the new signal.
// Emitted when the player jumped on the mob.
[Signal]
public delegate void SquashedEventHandler();
// ...
public void Squash()
{
EmitSignal(SignalName.Squashed);
QueueFree();
}
注釈
C#を使用している場合、Godotは EventHandler で終わるすべてのシグナルに対して適切なイベントを自動的に作成します。 C# Signals を参照してください。
次のレッスンではスコアにポイントを加算するためにこのシグナルを使います。
これで、モンスターにジャンプしてキルできるようになっているはずです。 F5 を押してゲームを試し、 main.tscn をプロジェクトのメインシーンに設定することができます。
しかし、プレイヤーはまだ死にません。次のパートでは、この点を取り組みます。