プレイヤーに死亡判定を設定する

敵キャラクターの上にジャンプすることで敵を倒すことはできるようになりましたが、プレイヤーはゲームオーバーになりません。これを修正していきましょう。

敵に殴られた場合と押し潰された場合とで、検知の仕方を変えたいです。プレイヤーが床を動いているときは死にますが、空中にいるときは倒されないようにしたいです。2種類の衝突を区別するためにベクトル演算ノードを使うことも可能です。しかし、ここでは代わりに Area3D ノードを使ってみましょう。こちらのノードのほうが Hitbox に適しています。

Areaノードによるヒットボックス

player.tscn シーンに戻り、新しい子ノード Area3D を追加します。MobDetector と名付け、 CollisionShape3D ノードをこれの子ノードとして追加します。

image0

インスペクター(Inspector)で、これにシリンダーの形状を割り当てます。

image1

ここでは、プレイヤーが地面やそれに近いところにいるときだけ衝突が起こるようにするトリックを使います。シリンダーの高さを小さくして、キャラクターの上部に移動させます。こうすることで、プレイヤーがジャンプしたときに、形状が高すぎて敵が衝突しにくくなります。

image2

また、シリンダーの幅は球よりも広くします。こうすると、衝突してモンスターのコリジョンボックスの上に押し出される前に、プレイヤーは当たることになります。

シリンダーの幅が広いほど、プレイヤーは殺されやすくなります。

次に、MobDetectorノードを再度選択し、インスペクター(Inspector)で、Monitorableプロパティをオフにしてください。これにより、他の物理ノードがその領域を検出できないようにします。補完的なMonitoringプロパティにより、衝突を検出することができます。次に、Collision -> Layerでレイヤーを削除して、敵レイヤーにマスクを設定します。

image3

領域は衝突を検出するとシグナルを発信します。この内の1つを``Player`` ノードへ接続します。 MobDetector を選択してから InspectorNode タブに移動し、 body_entered シグナルをダブルクリックして Player に接続してください

image4

CharacterBody3D または RigidBody3D ノードが入った際に、 MobDetectorbody_entered を発信します。"敵" の物理レイヤーをマスクするだけなので、 Mob ノードのみが検出されます.

コード的には、2つのことをします:ゲームを終了するために後で使用するシグナルを発信することと、プレーヤーを破壊することです。これらの操作をdie()関数でラップすることにより、コードに説明的なラベルを付けることができます。

# Emitted when the player was hit by a mob.
# Put this at the top of the script.
signal hit


# And this function at the bottom.
func die():
    hit.emit()
    queue_free()


func _on_mob_detector_body_entered(body):
    die()

ゲームオーバーを設定する

ゲームオーバーは Playerhit シグナルを使用して実装することができます。あとは Main ノードに接続して、シグナルに応じて MobTimer を止めるだけです。

main.tscn を開き、 Player ノードを選択し、 Node ドックで hit シグナルを Main ノードに接続します。

image5

これで、 _on_player_hit() 関数の中で Timer を 取得し、停止することができます。

func _on_player_hit():
    $MobTimer.stop()

今ゲームを試しに実行してみると、プレイヤー自身が死ぬとモンスターの出現が止まり、残ったモンスターはスクリーンから去っていきます。

また、プレイヤーが死んだときに、今度はゲームがクラッシュしたりエラーが表示されたりしなくなったことにも注目してください。MobTimerを停止しているため、 _on_mob_timer_timeout() 関数が実行されなくなったのです。

また、敵がプレイヤーに衝突して死ぬかどうかは、 PlayerMob の衝突判定のサイズと位置によって決まることに注意してください。プレイヤーの感覚に合ったゲーム体験を実現するためには、衝突判定を動かしたり、サイズを変更したりして調整する必要があるかもしれません。

まだ少し荒削りではありますが、完璧な 3D ゲームが試作できたことを誇りに思ってください。

ここから、スコア表示やリトライするオプションを追加し、最小限のアニメーションでゲームをより生き生きとしたものにするにはどうすればよいのか、学習していきましょう。

ここまでのコードの確認

以下は Main ノード、 Mob ノード、 Player ノードのスクリプトです。あなたが書いたコードと比較したりチェックしたりするのに使ってください。

main.gd から見ていきましょう。

extends Node

@export var mob_scene: PackedScene


func _on_mob_timer_timeout():
    # Create a new instance of the Mob scene.
    var mob = mob_scene.instantiate()

    # Choose a random location on the SpawnPath.
    # We store the reference to the SpawnLocation node.
    var mob_spawn_location = get_node("SpawnPath/SpawnLocation")
    # And give it a random offset.
    mob_spawn_location.progress_ratio = randf()

    var player_position = $Player.position
    mob.initialize(mob_spawn_location.position, player_position)

    # Spawn the mob by adding it to the Main scene.
    add_child(mob)

func _on_player_hit():
    $MobTimer.stop()

次に mob.gd を記載します。

extends CharacterBody3D

# Minimum speed of the mob in meters per second.
@export var min_speed = 10
# Maximum speed of the mob in meters per second.
@export var max_speed = 18

# Emitted when the player jumped on the mob
signal squashed

func _physics_process(_delta):
    move_and_slide()

# This function will be called from the Main scene.
func initialize(start_position, player_position):
    # We position the mob by placing it at start_position
    # and rotate it towards player_position, so it looks at the player.
    look_at_from_position(start_position, player_position, Vector3.UP)
    # Rotate this mob randomly within range of -45 and +45 degrees,
    # so that it doesn't move directly towards the player.
    rotate_y(randf_range(-PI / 4, PI / 4))

    # We calculate a random speed (integer)
    var random_speed = randi_range(min_speed, max_speed)
    # We calculate a forward velocity that represents the speed.
    velocity = Vector3.FORWARD * random_speed
    # We then rotate the velocity vector based on the mob's Y rotation
    # in order to move in the direction the mob is looking.
    velocity = velocity.rotated(Vector3.UP, rotation.y)

func _on_visible_on_screen_notifier_3d_screen_exited():
    queue_free()

func squash():
    squashed.emit()
    queue_free() # Destroy this node

最後に、最も長いスクリプトの player.gd を記載しておきます:

extends CharacterBody3D

signal hit

# How fast the player moves in meters per second
@export var speed = 14
# The downward acceleration while in the air, in meters per second squared.
@export var fall_acceleration = 75
# Vertical impulse applied to the character upon jumping in meters per second.
@export var jump_impulse = 20
# Vertical impulse applied to the character upon bouncing over a mob
# in meters per second.
@export var bounce_impulse = 16

var target_velocity = Vector3.ZERO


func _physics_process(delta):
    # We create a local variable to store the input direction
    var direction = Vector3.ZERO

    # We check for each move input and update the direction accordingly
    if Input.is_action_pressed("move_right"):
        direction.x = direction.x + 1
    if Input.is_action_pressed("move_left"):
        direction.x = direction.x - 1
    if Input.is_action_pressed("move_back"):
        # Notice how we are working with the vector's x and z axes.
        # In 3D, the XZ plane is the ground plane.
        direction.z = direction.z + 1
    if Input.is_action_pressed("move_forward"):
        direction.z = direction.z - 1

    # Prevent diagonal moving fast af
    if direction != Vector3.ZERO:
        direction = direction.normalized()
        # Setting the basis property will affect the rotation of the node.
        $Pivot.basis = Basis.looking_at(direction)

    # Ground Velocity
    target_velocity.x = direction.x * speed
    target_velocity.z = direction.z * speed

    # Vertical Velocity
    if not is_on_floor(): # If in the air, fall towards the floor. Literally gravity
        target_velocity.y = target_velocity.y - (fall_acceleration * delta)

    # Jumping.
    if is_on_floor() and Input.is_action_just_pressed("jump"):
        target_velocity.y = jump_impulse

    # Iterate through all collisions that occurred this frame
    # in C this would be for(int i = 0; i < collisions.Count; i++)
    for index in range(get_slide_collision_count()):
        # We get one of the collisions with the player
        var collision = get_slide_collision(index)

        # If the collision is with ground
        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

    # Moving the Character
    velocity = target_velocity
    move_and_slide()

# And this function at the bottom.
func die():
    hit.emit()
    queue_free()

func _on_mob_detector_body_entered(body):
    die()

次回のレッスンでは、スコア表示とリトライオプションを追加しましょう。