パート5

パートの概要

このパートでは、プレイヤーに手榴弾を追加し、プレイヤーにオブジェクトをつかんで投げる能力を与え、砲塔を追加します!

../../../_images/PartFiveFinished.png

注釈

チュートリアルのこの部分に進む前に、パート4 を終了していることが前提となります。パート4 からの完成したプロジェクトは、パート5の開始プロジェクトになります

では、始めましょう!

手榴弾の追加

まず、プレイヤーに手榴弾をいくつか与えてみましょう。Grenade.tscn を開きます。

ここで注意すべき点がいくつかあります。何よりもまず、手榴弾が RigidBody ノードを使用することです。 手榴弾に RigidBody ノードを使用して、(やや)現実的な方法で世界中をバウンドさせます。

2番目に注意することは Blast_Area です。 これは Area ノードで、手榴弾の爆発半径を表します。

最後に、最終的に注意することは 爆発 です。 これは Particles ノードで、手榴弾が爆発すると爆発効果を発します。ここで注意すべきことは、One shot が有効になっていることです。 これは、すべてのパーティクルを一度に放出するためです。 パーティクルは、ローカル座標の代わりにワールド座標を使用して放出されるため、Local Coords もチェックされていません。

注釈

必要に応じて、パーティクルの Process MaterialDraw Passes を調べることで、パーティクルの設定方法を確認できます。

手榴弾に必要なコードを書きましょう。Grenade を選択し、Grenade.gd という新しいスクリプトを作成します。次のように追加します:

extends RigidBody

const GRENADE_DAMAGE = 60

const GRENADE_TIME = 2
var grenade_timer = 0

const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0

var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles

func _ready():
    rigid_shape = $Collision_Shape
    grenade_mesh = $Grenade
    blast_area = $Blast_Area
    explosion_particles = $Explosion

    explosion_particles.emitting = false
    explosion_particles.one_shot = true

func _process(delta):

    if grenade_timer < GRENADE_TIME:
        grenade_timer += delta
        return
    else:
        if explosion_wait_timer <= 0:
            explosion_particles.emitting = true

            grenade_mesh.visible = false
            rigid_shape.disabled = true

            mode = RigidBody.MODE_STATIC

            var bodies = blast_area.get_overlapping_bodies()
            for body in bodies:
                if body.has_method("bullet_hit"):
                    body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))

            # This would be the perfect place to play a sound!


        if explosion_wait_timer < EXPLOSION_WAIT_TIME:
            explosion_wait_timer += delta

            if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
                queue_free()

クラス変数から始めて、何が起こっているのかを見てみましょう:

  • GRENADE_DAMAGE: 手榴弾が爆発したときに与えるダメージの量。
  • GRENADE_TIME: 手榴弾が作成/投擲されてから爆発するまでにかかる時間(秒単位)。
  • grenade_timer: 手榴弾が作成/投擲された時間を追跡する変数。
  • EXPLOSION_WAIT_TIME: 爆発後に手榴弾シーンを破棄する前に待機するために必要な時間(秒単位)
  • explosion_wait_timer: 手榴弾が爆発してから経過した時間を追跡する変数。
  • rigid_shape: 手榴弾の RigidBodyCollisionShape
  • grenade_mesh: 手榴弾の MeshInstance
  • blast_area: blast :ref:`Area <class_Area>`は、手榴弾が爆発したときに物を損傷するために使用されます。
  • explosion_particles: 手榴弾が爆発したときに出てくる Particles

EXPLOSION_WAIT_TIME がかなり奇妙な数字(0.48)であることに注意してください。 これは、EXPLOSION_WAIT_TIME を爆発パーティクルが放出される時間の長さに等しくしたいため、パーティクルが完了すると手榴弾を破棄/解放するためです。 パーティクルの寿命を取得し、それをパーティクルの速度スケールで割ることにより、EXPLOSION_WAIT_TIME を計算します。 これにより、爆発パーティクルが持続する正確な時間がわかります。


それでは、_ ready に注目しましょう。

まず、必要なすべてのノードを取得し、適切なクラス変数に割り当てます。

CollisionShape と:ref:MeshInstance <class_MeshInstance> を取得する必要があります。パート4 のターゲットと同様に、手榴弾の爆発時に手榴弾のメッシュを非表示にし、コリジョンシェイプを無効にするためです。

blast Area を取得する必要があるのは、手榴弾が爆発したときにその内部のすべてを破損できるようにするためです。 プレイヤーのナイフコードに似たコードを使用します。手榴弾が爆発したときにパーティクルを放出できるように、Particles が必要です。

すべてのノードを取得し、それらをクラス変数に割り当てた後、爆発パーティクルが放出されていないこと、およびそれらがワンショットで放出されるように設定されていることを確認します。 これは、パーティクルが期待どおりに動作することをさらに確実にするためです。


それでは、_process を見てみましょう。

まず、grenade_timerGRENADE_TIME よりも小さいかどうかを確認します。もしそうなら、delta を加算して戻ります。 これは、手榴弾が爆発する前に GRENADE_TIME 秒待たなければならず、RigidBody が動き回れるようにするためです。

grenade_timerGRENADE_TIMER 以上の場合、手榴弾が十分に長く待機していて爆発する必要があるかどうかを確認する必要があります。 これを行うには、explosion_wait_timer0 以下であるかどうかを確認します。 直後に deltaexplosion_wait_timer に追加するため、チェック対象のコードは、手榴弾が十分に長く待機して爆発する必要があるときに1回だけ呼び出されます。

手榴弾が爆発するのに十分な時間待機した場合、まず explosion_particles に放出するように指示します。次に、grenade_mesh を非表示にし、rigid_shape を無効にして、手榴弾を効果的に隠します。

次に RigidBody のモードを ``MODE_STATIC``に設定し、手榴弾が動かないようにします。

次に、blast_area 内のすべてのボディを取得し、それらが bullet_hit メソッド/関数を持っているかどうかを確認し、もしあれば、それを呼び出して GRENADE_DAMAGE と手榴弾を見ているボディからのtransformを渡します。 これにより、手榴弾で爆発した体が手榴弾の位置から外側に爆発します。

次に、explosion_wait_timerEXPLOSION_WAIT_TIME よりも小さいかどうかを確認します。 そうである場合、delta を `` explosion_wait_timer`` に加算します。

次に、explosion_wait_timerEXPLOSION_WAIT_TIME 以上かどうかを確認します。delta を加算したため、これは一度だけ呼び出されます。explosion_wait_timerEXPLOSION_WAIT_TIME 以上の場合、手榴弾は Particles を再生するのに十分な時間待機しており、手榴弾は不もう必要ないので解放/破棄できます 。


引き続き、粘着性手榴弾も設定してみしましょう。Sticky_Grenade.tscn を開きます。

Sticky_Grenade.tscnGrenade.tscn とほとんど同じですが、1つだけ追加されています。Sticky_Area と呼ばれる2つ目の Area です。Stick_Area を使用して、粘着性手榴弾が周囲と衝突し、何かに固着する必要があるときを検出します。

Sticky_Grenade を選択し、Sticky_Grenade.gd という新しいスクリプトを作成します。次のように追加します:

extends RigidBody

const GRENADE_DAMAGE = 40

const GRENADE_TIME = 3
var grenade_timer = 0

const EXPLOSION_WAIT_TIME = 0.48
var explosion_wait_timer = 0

var attached = false
var attach_point = null

var rigid_shape
var grenade_mesh
var blast_area
var explosion_particles

var player_body

func _ready():
    rigid_shape = $Collision_Shape
    grenade_mesh = $Sticky_Grenade
    blast_area = $Blast_Area
    explosion_particles = $Explosion

    explosion_particles.emitting = false
    explosion_particles.one_shot = true

    $Sticky_Area.connect("body_entered", self, "collided_with_body")


func collided_with_body(body):

    if body == self:
        return

    if player_body != null:
        if body == player_body:
            return

    if attached == false:
        attached = true
        attach_point = Spatial.new()
        body.add_child(attach_point)
        attach_point.global_transform.origin = global_transform.origin

        rigid_shape.disabled = true

        mode = RigidBody.MODE_STATIC


func _process(delta):

    if attached == true:
        if attach_point != null:
            global_transform.origin = attach_point.global_transform.origin

    if grenade_timer < GRENADE_TIME:
        grenade_timer += delta
        return
    else:
        if explosion_wait_timer <= 0:
            explosion_particles.emitting = true

            grenade_mesh.visible = false
            rigid_shape.disabled = true

            mode = RigidBody.MODE_STATIC

            var bodies = blast_area.get_overlapping_bodies()
            for body in bodies:
                if body.has_method("bullet_hit"):
                    body.bullet_hit(GRENADE_DAMAGE, body.global_transform.looking_at(global_transform.origin, Vector3(0, 1, 0)))

            # This would be the perfect place to play a sound!


        if explosion_wait_timer < EXPLOSION_WAIT_TIME:
            explosion_wait_timer += delta

            if explosion_wait_timer >= EXPLOSION_WAIT_TIME:
                if attach_point != null:
                    attach_point.queue_free()
                queue_free()

上記のコードは Grenade.gd のコードとほとんど同じなので、何が変更されたかを見てみましょう。

まず、いくつかのクラス変数があります:

  • attached: 粘着性手榴弾が PhysicsBody にアタッチされているかどうかを追跡するための変数。
  • attach_point: 粘着性手榴弾が衝突した位置にある Spatial を保持する変数。
  • player_body: プレイヤーの KinematicBody

粘着性手榴弾がヒットする可能性のある PhysicsBody に固着するために追加されました。また、プレイヤーは KinematicBody を必要とするため、プレイヤーが投げたときに粘着性手榴弾がプレイヤーにくっつかないようになります。


では、_ready の小さな変更を見てみましょう。_ready にコード行を追加したので、ボディが Stick_Area に入ると collided_with_body 関数が呼び出されます。


次に collided_with_body を見てみましょう。

まず、粘着性手榴弾がそれ自体と衝突しないようにします。sticky(粘着性) Area は、grenade(手榴弾) RigidBody に取り付けられていることを知らないので、衝突したbodyがそれ自体ではないことを確認することで、それ自体に固着しないようにする必要があります。自体と衝突した場合は、リターンすることで無視します。

次に、player_body に何かが割り当てられているかどうかを確認し、粘着性手榴弾が衝突したbodyがそれを投げたプレイヤーであるかどうかを確認します。粘着性手榴弾が衝突したbodyが実際に player_body である場合、リターンすることでそれを無視します。

次に、粘着性手榴弾が既に何かに固着しているかどうかを確認します。

粘着性手榴弾が固着していない場合は、attachedtrue に設定して、粘着性手榴弾が何かに固着したことを知らせます。

次に、新しい Spatial ノードを作成し、粘着性手榴弾が衝突したbodyの子にします。次に、Spatial の位置を粘着性手榴弾の現在のグローバルポジションに設定します。

注釈

Spatial を粘着性手榴弾が衝突したbodyの子として追加したため、そのbodyとひと続きになります。次に、この Spatial を使用して、粘着性手榴弾の位置を設定することができるので、衝突したbodyに対して常に同じ位置に配置されます。

次に、rigid_shape を無効にして、粘着性手榴弾が衝突したbodyを絶えず動かしてしまわないようにします。最後に、手榴弾が動かないようにモードを MODE_STATIC に設定します。


最後に、_process のいくつかの変更を見ていきましょう。

_process の一番上で、粘着性手榴弾が 固着してるかどうかを確認しています。

粘着性手榴弾が固着している場合、取り付けられたポイントが null と等しくないことを確認します。固着したポイントが null と等しくない場合、粘着性手榴弾のグローバルポジション(グローバル Transform のorigin)を attach_point に割り当てられた Spatial のグローバルポジション(グローバル Transform のorigin)に設定します。

他の唯一の変更点は、粘着性手榴弾を解放/破棄する前に、粘着性手榴弾が固着したポイントを保持しているかどうかを確認することです。 もしそうなら、固着ポイントで queue_free も呼び出すので、それも解放/破棄されます。

プレイヤーに手榴弾を追加する

次に、手榴弾を使用できるように、いくつかのコードを Player.gd に追加する必要があります。

まず、Player.tscn を開き、Rotation_Helper に到達するまでノードツリーを展開します。Rotation_HelperGrenade_Toss_Pos というノードがあることに注意してください。これは手榴弾を産出する場所です。

また、X 軸上でわずかに回転しているため、真っ直ぐではなく、わずかに上を向いています。Grenade_Toss_Pos の回転を変更することで、手榴弾が投げられる角度を変更できます。

さて、プレイヤーと手榴弾を機能させましょう。次のクラス変数を Player.gd に追加します:

var grenade_amounts = {"Grenade":2, "Sticky Grenade":2}
var current_grenade = "Grenade"
var grenade_scene = preload("res://Grenade.tscn")
var sticky_grenade_scene = preload("res://Sticky_Grenade.tscn")
const GRENADE_THROW_FORCE = 50
  • grenade_amounts: プレイヤーが現在運んでいる手榴弾の量(手榴弾の種類ごとの)。
  • current_grenade: プレイヤーが現在使用している手榴弾の名前。
  • grenade_scene: 先ほど取り組んだ手榴弾のシーン。
  • sticky_grenade_scene: 先ほど取り組んだ粘着性手榴弾のシーン。
  • GRENADE_THROW_FORCE: プレイヤーが手榴弾を投げる力。

これらの変数のほとんどは、武器のセットアップ方法に似ています。

ちなみに

よりモジュール化された手榴弾システムを作成することは可能ですが、たった2つの手榴弾を追加するだけの為には、その複雑さに価値はないことがわかりました。手榴弾の種類の多いより複雑なFPSを作成する場合は、武器のセットアップ方法と同様の手榴弾用のシステムを作成することをお勧めします。


_process_input にコードを追加する必要があります _process_input に以下を追加します:

# ----------------------------------
# Changing and throwing grenades

if Input.is_action_just_pressed("change_grenade"):
    if current_grenade == "Grenade":
        current_grenade = "Sticky Grenade"
    elif current_grenade == "Sticky Grenade":
        current_grenade = "Grenade"

if Input.is_action_just_pressed("fire_grenade"):
    if grenade_amounts[current_grenade] > 0:
        grenade_amounts[current_grenade] -= 1

        var grenade_clone
        if current_grenade == "Grenade":
            grenade_clone = grenade_scene.instance()
        elif current_grenade == "Sticky Grenade":
            grenade_clone = sticky_grenade_scene.instance()
            # Sticky grenades will stick to the player if we do not pass ourselves
            grenade_clone.player_body = self

        get_tree().root.add_child(grenade_clone)
        grenade_clone.global_transform = $Rotation_Helper/Grenade_Toss_Pos.global_transform
        grenade_clone.apply_impulse(Vector3(0, 0, 0), grenade_clone.global_transform.basis.z * GRENADE_THROW_FORCE)
# ----------------------------------

ここで何が起こっているのか見ていきましょう。

まず、change_grenade アクションが押されたかどうかを確認します。その場合は、プレイヤーが現在使用している手榴弾を確認します。プレイヤーが現在使用している手榴弾の名前に基づいて、current_grenade を逆のの手榴弾の名前に変更します。

次に、fire_grenade アクションが押されたばかりかどうかを確認します。その場合は、選択した現在の手榴弾タイプに対して、プレイヤーが 0 以上の手榴弾を持っているかどうかを確認します。

プレイヤーが 0 を超える手榴弾を持っている場合、現在の手榴弾の手榴弾数から1つ減らします。次に、プレイヤーが現在使用している手榴弾に基づいて、適切な手榴弾シーンをインスタンス化し、grenade_clone に割り当てます。

次に、ルートのノードの子として grenade_clone を追加し、そのグローバル TransformGrenade_Toss_Pos のグローバル Transform に設定します。最後に、手榴弾に衝撃を与え、手榴弾クローンの Z 方向ベクトルに対して前方に発射されるようにします。


プレイヤーは両方のタイプの手榴弾を使用できるようになりましたが、他のものを追加する前に追加する必要があるものがいくつかあります。

プレイヤーに手榴弾いくつ残っているかを示す方法も必要ですし、プレイヤーが弾薬を拾ってより多くの手榴弾を得る方法を追加する必要もあります。

まず、残っている手榴弾の数を示すために Player.gd のコードの一部を変更しましょう。process_UI を次のように変更します:

func process_UI(delta):
    if current_weapon_name == "UNARMED" or current_weapon_name == "KNIFE":
        # First line: Health, second line: Grenades
        UI_status_label.text = "HEALTH: " + str(health) + \
                "\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])
    else:
        var current_weapon = weapons[current_weapon_name]
        # First line: Health, second line: weapon and ammo, third line: grenades
        UI_status_label.text = "HEALTH: " + str(health) + \
                "\nAMMO: " + str(current_weapon.ammo_in_weapon) + "/" + str(current_weapon.spare_ammo) + \
                "\n" + current_grenade + ": " + str(grenade_amounts[current_grenade])

次に、UIにプレイヤーが残している手榴弾の数を示します。

まだ Player.gd の中に、プレイヤーに手榴弾を追加する関数を追加しましょう。次の関数を Player.gd に追加します:

func add_grenade(additional_grenade):
    grenade_amounts[current_grenade] += additional_grenade
    grenade_amounts[current_grenade] = clamp(grenade_amounts[current_grenade], 0, 4)

これで add_grenade を使用して手榴弾を追加でき、自動的に 4 個の手榴弾の最大値にクランプされます。

ちなみに

必要に応じて、4 を定数に変更できます。MAX_GRENADES のような新しいグローバル定数を作成してから、クランプを clamp(grenade_amounts[current_grenade], 0, 4) から clamp(grenade_amounts[current_grenade], 0,MAX_GRENADES) に修正します

プレイヤーが手に入れることができる手榴弾の数を制限したくない場合は、手榴弾をクランプしている行を完全に削除してください!

これで手榴弾を追加する関数ができましたので、AmmoPickup.gd を開いて使用しましょう!

AmmoPickup.gd を開き、trigger_body_entered 関数に移動します。次のように変更します:

func trigger_body_entered(body):
    if body.has_method("add_ammo"):
        body.add_ammo(AMMO_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

    if body.has_method("add_grenade"):
        body.add_grenade(GRENADE_AMOUNTS[kit_size])
        respawn_timer = RESPAWN_TIME
        kit_size_change_values(kit_size, false)

また、bodyに add_grenade 関数があるかどうかも確認しています。存在する場合、add_ammo を呼び出す場合と同じように呼び出します。

まだ定義していない新しい定数 GRENADE_AMOUNTS を使用していることに気づいたかもしれません。追加しましょう!次のクラス変数を他のクラス変数とともに AmmoPickup.gd に追加します:

const GRENADE_AMOUNTS = [2, 0]
  • GRENADE_AMOUNTS: 各ピックアップに含まれる手榴弾の量。

GRENADE_AMOUNTS の2番目の要素が 0 であることに注意してください。これは、小さな弾薬ピックアップがプレイヤーに追加の手榴弾を与えないようにするためです。


これで手榴弾を投げることができるはずです!試してみてください!

RigidBodyノードを取得してプレイヤーに投げる機能を追加する

次に、プレイヤーに RigidBody ノードをピックアップしてスローする機能を与えましょう。

Player.gd を開き、以下のクラス変数を追加してください:

var grabbed_object = null
const OBJECT_THROW_FORCE = 120
const OBJECT_GRAB_DISTANCE = 7
const OBJECT_GRAB_RAY_DISTANCE = 10
  • grabbed_object: つかんだ RigidBody ノードを保持する変数。
  • OBJECT_THROW_FORCE: プレイヤーがつかんだオブジェクトを投げる力。
  • OBJECT_GRAB_DISTANCE: プレイヤーがつかんだオブジェクトを保持するカメラからの距離。
  • OBJECT_GRAB_RAY_DISTANCE: Raycast が進む距離。これはプレイヤーのつかむ距離です。

これが完了したら、必要なのは process_input にコードを追加することだけです:

# ----------------------------------
# Grabbing and throwing objects

if Input.is_action_just_pressed("fire_grenade") and current_weapon_name == "UNARMED":
    if grabbed_object == null:
        var state = get_world().direct_space_state

        var center_position = get_viewport().size / 2
        var ray_from = camera.project_ray_origin(center_position)
        var ray_to = ray_from + camera.project_ray_normal(center_position) * OBJECT_GRAB_RAY_DISTANCE

        var ray_result = state.intersect_ray(ray_from, ray_to, [self, $Rotation_Helper/Gun_Fire_Points/Knife_Point/Area])
        if !ray_result.empty():
            if ray_result["collider"] is RigidBody:
                grabbed_object = ray_result["collider"]
                grabbed_object.mode = RigidBody.MODE_STATIC

                grabbed_object.collision_layer = 0
                grabbed_object.collision_mask = 0

    else:
        grabbed_object.mode = RigidBody.MODE_RIGID

        grabbed_object.apply_impulse(Vector3(0, 0, 0), -camera.global_transform.basis.z.normalized() * OBJECT_THROW_FORCE)

        grabbed_object.collision_layer = 1
        grabbed_object.collision_mask = 1

        grabbed_object = null

if grabbed_object != null:
    grabbed_object.global_transform.origin = camera.global_transform.origin + (-camera.global_transform.basis.z.normalized() * OBJECT_GRAB_DISTANCE)
# ----------------------------------

何が起こっているのか見ていきましょう。

最初に、押されたアクションが fire アクションであるかどうか、およびプレイヤーが使用中の 'weapon' が UNARMED かどうかを確認します。これは、プレイヤーが武器を使用していないときにのみ、プレイヤーがオブジェクトを拾い上げて投げることができるようにするためです。これは設計上の選択ですが、私はそれが UNARMED の使い道だと思っています。

次に、grabbed_objectnull かどうかを確認します。


grabbed_objectnull の場合、RigidBody を取得できるかどうかを確認します。

まず、現在の World から直接space state(空間の情報)を取得します。これにより、Raycast ノードを使用する代わりに、コードから完全にレイをキャストできます。

注釈

Godotでのレイキャストの詳細については レイキャスティング を参照してください。

次に、現在の Viewport サイズを半分に分割して、画面の中心を取得します。次に、カメラから project_ray_originproject_ray_normal を使用してレイの始点と終点を取得します。これらの関数がどのように機能するかについてもっと知りたい場合は、レイキャスティング を参照してください。

次に、レイをspace state(空間の情報)に送り、結果が得られるかどうかを確認します。プレイヤーとナイフの Area を2つの例外として追加し、プレイヤーが自分自身やナイフのコリジョン Area を運べないようにします。

次に、レイから結果が返されたかどうかを確認します。オブジェクトがレイと衝突していない場合、空のDictionaryが返されます。Dictionaryが空でない場合(つまり、少なくとも1つのオブジェクトが衝突した場合)、レイが衝突したコライダーが RigidBody であるかどうかを確認します。

レイが RigidBody と衝突した場合、レイが衝突したコライダーに grabbed_object を設定します。次に、衝突した RigidBody のモードを MODE_STATIC に設定し、手の中で動かないようにします。

最後に、取得した RigidBody のコリジョンレイヤーとコリジョンマスクを 0 に設定します。これにより、つかまれた RigidBody にコリジョンレイヤーやマスクがなくなります。つまり、保持している限り、衝突することはありません。


grabbed_objectnull でない場合、プレイヤーが保持している RigidBody を投げる必要があります。

まず、保持している RigidBody のモードを MODE_RIGID に設定します。

注釈

これは、すべてのリジッドボディが MODE_RIGID を使用するというかなり大きな仮定を行っています。このチュートリアルシリーズの場合はそれでいいのですが、他のプロジェクトではそうではないかもしれません。

異なるモードのリジッドボディがある場合、選択した RigidBody のモードをクラス変数に保存して、、それを拾う前のモードに戻すことができるようにする必要があります。

次に、衝撃を適用(apply_impulse)して前方に飛行させます。OBJECT_THROW_FORCE 変数で設定した力を使用して、カメラが向いている方向に飛行します。

次に、取得した RigidBody のコリジョンレイヤーとマスクを 1 に設定します。これにより、レイヤー 1 上のすべてのものと再び衝突できるようになります。

注釈

これもまた、すべてのリジッドボディがコリジョンレイヤー 1 にのみ存在し、すべてのコリジョンマスクがレイヤー 1 に存在するというかなり大きな仮定をしています。このスクリプトを他のプロジェクトで使用する場合、0 に変更する前に、変数に RigidBody のコリジョンレイヤー/マスクを保存する必要がある場合があります。プロセスを反転するときにそれらを元のコリジョンレイヤー/マスクに設定します。

最後に、プレイヤーが保持していたオブジェクトを正常に投げたので、grabbed_objectnull に設定します。


最後に行うことは、grabbed_objectnull に等しいかどうかを確認することです。これは、すべての つかむ/投げる 関連コードの外側で行います。

注釈

技術的には入力関連ではありませんが、つかんだオブジェクトを移動するコードをここに配置するのは簡単です。2行しかないため、つかむ/投げるコードはすべて1か所にあるからです

プレイヤーがオブジェクトを保持している場合、そのグローバルポジションをカメラのポジションとカメラが向いている方向の OBJECT_GRAB_DISTANCE に設定します。


これをテストする前に、_physics_process で何かを変更する必要があります。プレイヤーがオブジェクトを保持している間は、プレイヤーが武器を変更したりリロードしたりできないようにするため、_ physics_process を次のように変更します:

func _physics_process(delta):
    process_input(delta)
    process_view_input(delta)
    process_movement(delta)

    if grabbed_object == null:
        process_changing_weapons(delta)
        process_reloading(delta)

    # Process the UI
    process_UI(delta)

これで、プレイヤーはオブジェクトを保持している間、武器を変更したりリロードしたりできなくなります。

これで、UNARMED 状態にあるときにRigidBodyノードをつかんで投げることができます!試してみてください!

砲塔の追加

次に、プレイヤーを撃つための砲塔を作りましょう!

Turret.tscn を開きます。まだ展開されていない場合は、Turret を展開します。

砲塔が複数の部分に分割されていることに注目してください : BaseHeadVision_Area、および `` Smoke`` Particles ノードです。

Base を開くと、StaticBody とメッシュであることがわかります。Head を開くと、複数のメッシュ、StaticBodyRaycast ノードがあります。

Head で注意すべきことの1つは、レイキャスティングを使用している場合、レイキャストが砲塔が弾丸を発砲する場所になることです。また、FlashFlash_2 という2つのメッシュがあります。これらは、砲塔が発砲したときに簡単に表示される銃口フラッシュになります。

Vision_AreaArea であり、砲塔の視認能力として使用します。何かが Vision_Area に入ると、砲塔がそれを見ることができると仮定します。

SmokeParticles ノードで、砲塔が破壊されて修復されたときに再生されます。


シーンのセットアップ方法を確認したので、砲塔のコードの記述を始めましょう。Turret を選択し、Turret.gd という新しいスクリプトを作成します。以下を Turret.gd に追加します:

extends Spatial

export (bool) var use_raycast = false

const TURRET_DAMAGE_BULLET = 20
const TURRET_DAMAGE_RAYCAST = 5

const FLASH_TIME = 0.1
var flash_timer = 0

const FIRE_TIME = 0.8
var fire_timer = 0

var node_turret_head = null
var node_raycast = null
var node_flash_one = null
var node_flash_two = null

var ammo_in_turret = 20
const AMMO_IN_FULL_TURRET = 20
const AMMO_RELOAD_TIME = 4
var ammo_reload_timer = 0

var current_target = null

var is_active = false

const PLAYER_HEIGHT = 3

var smoke_particles

var turret_health = 60
const MAX_TURRET_HEALTH = 60

const DESTROYED_TIME = 20
var destroyed_timer = 0

var bullet_scene = preload("Bullet_Scene.tscn")

func _ready():

    $Vision_Area.connect("body_entered", self, "body_entered_vision")
    $Vision_Area.connect("body_exited", self, "body_exited_vision")

    node_turret_head = $Head
    node_raycast = $Head/Ray_Cast
    node_flash_one = $Head/Flash
    node_flash_two = $Head/Flash_2

    node_raycast.add_exception(self)
    node_raycast.add_exception($Base/Static_Body)
    node_raycast.add_exception($Head/Static_Body)
    node_raycast.add_exception($Vision_Area)

    node_flash_one.visible = false
    node_flash_two.visible = false

    smoke_particles = $Smoke
    smoke_particles.emitting = false

    turret_health = MAX_TURRET_HEALTH


func _physics_process(delta):

    if is_active == true:

        if flash_timer > 0:
            flash_timer -= delta

            if flash_timer <= 0:
                node_flash_one.visible = false
                node_flash_two.visible = false

        if current_target != null:

            node_turret_head.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))

            if turret_health > 0:

                if ammo_in_turret > 0:
                    if fire_timer > 0:
                        fire_timer -= delta
                    else:
                        fire_bullet()
                else:
                    if ammo_reload_timer > 0:
                        ammo_reload_timer -= delta
                    else:
                        ammo_in_turret = AMMO_IN_FULL_TURRET

    if turret_health <= 0:
        if destroyed_timer > 0:
            destroyed_timer -= delta
        else:
            turret_health = MAX_TURRET_HEALTH
            smoke_particles.emitting = false


func fire_bullet():

    if use_raycast == true:
        node_raycast.look_at(current_target.global_transform.origin + Vector3(0, PLAYER_HEIGHT, 0), Vector3(0, 1, 0))

        node_raycast.force_raycast_update()

        if node_raycast.is_colliding():
            var body = node_raycast.get_collider()
            if body.has_method("bullet_hit"):
                body.bullet_hit(TURRET_DAMAGE_RAYCAST, node_raycast.get_collision_point())

        ammo_in_turret -= 1

    else:
        var clone = bullet_scene.instance()
        var scene_root = get_tree().root.get_children()[0]
        scene_root.add_child(clone)

        clone.global_transform = $Head/Barrel_End.global_transform
        clone.scale = Vector3(8, 8, 8)
        clone.BULLET_DAMAGE = TURRET_DAMAGE_BULLET
        clone.BULLET_SPEED = 60

        ammo_in_turret -= 1

    node_flash_one.visible = true
    node_flash_two.visible = true

    flash_timer = FLASH_TIME
    fire_timer = FIRE_TIME

    if ammo_in_turret <= 0:
        ammo_reload_timer = AMMO_RELOAD_TIME


func body_entered_vision(body):
    if current_target == null:
        if body is KinematicBody:
            current_target = body
            is_active = true


func body_exited_vision(body):
    if current_target != null:
        if body == current_target:
            current_target = null
            is_active = false

            flash_timer = 0
            fire_timer = 0
            node_flash_one.visible = false
            node_flash_two.visible = false


func bullet_hit(damage, bullet_hit_pos):
    turret_health -= damage

    if turret_health <= 0:
        smoke_particles.emitting = true
        destroyed_timer = DESTROYED_TIME

これはかなり量のコードなので、機能ごとに分解してみましょう。最初にクラス変数を見てみましょう:

  • use_raycast: 砲塔が弾丸にオブジェクトを使用するかレイキャスティングを使用するかを変更できるように、エクスポートされたブール値。
  • TURRET_DAMAGE_BULLET: 1つの弾丸シーンが与えるダメージの量。
  • TURRET_DAMAGE_RAYCAST: 単一の Raycast 弾丸が与えるダメージの量。
  • FLASH_TIME: 銃口フラッシュメッシュが表示される時間(秒単位)。
  • flash_timer: 銃口のフラッシュメッシュが表示されている時間を追跡するための変数。
  • FIRE_TIME: 弾丸を発射するのに必要な時間(秒単位)。
  • fire_timer: 砲塔が最後に発射してから経過した時間を追跡するための変数。
  • node_turret_head: Head ノードを保持する変数。
  • node_raycast: 砲塔の頭部に接続された Raycast ノードを保持する変数。
  • node_flash_one: 最初の銃口フラッシュ MeshInstance を保持する変数。
  • node_flash_two: 2番目の銃口フラッシュ MeshInstance を保持する変数。
  • ammo_in_turret: 現在砲塔にある弾薬の量。
  • AMMO_IN_FULL_TURRET: 砲塔の全弾薬量。
  • AMMO_RELOAD_TIME: 砲塔のリロードにかかる時間。
  • ammo_reload_timer: 砲塔がリロードされている時間を追跡するための変数。
  • current_target: 砲塔の現在のターゲット。
  • is_active: 砲塔が目標を射撃できるかどうかを追跡するための変数。
  • PLAYER_HEIGHT: 足元を狙らわないようにターゲットに加算する高さの量。
  • smoke_particles: 煙のパーティクルノードを保持する変数。
  • turret_health: 砲塔が現在持っている体力の量。
  • MAX_TURRET_HEALTH: 完全に回復した状態の砲塔の体力の量。
  • DESTROYED_TIME: 破壊された砲塔が自己修復するのにかかる時間(秒単位)。
  • destroyed_timer: 砲塔が破壊された時間を追跡するための変数。
  • bullet_scene: 砲塔が発射する弾丸のシーン(プレイヤーのピストルと同じシーン)

ふう、これはかなりの数のクラス変数です!


次に _ready を見ていきましょう。

まず、ビジョンエリアを取得し、body_entered および body_exited シグナルをそれぞれ  body_entered_vision および body_exited_vision に接続します。

次に、すべてのノードを取得し、それぞれの変数に割り当てます。

次に、Raycast にいくつかの例外を追加して、砲塔がそれ自体を傷つけないようにします。

次に、_ ready の間は発射しないため、両方のフラッシュメッシュを開始時に非表示にします。

次に、smoke particleノードを取得し、それを smoke_particles 変数に割り当てます。また、emittingfalse に設定して、砲塔が破壊されるまでパーティクルが放出されないようにします。

最後に、砲塔の体力を MAX_TURRET_HEALTH に設定して、完全な体力で開始するようにします。


それでは、_physics_process を見てみましょう。

まず、砲塔がアクティブかどうかを確認します。 砲塔がアクティブな場合、発射コードを処理します。

次に、flash_timer がゼロより大きい場合、つまりフラッシュメッシュが表示されている場合、flash_timer からdeltaを減らします。delta を減算した後に flash_timer がゼロ以下になった場合、両方のフラッシュメッシュを非表示にする必要があります。

次に、砲塔にターゲットがあるかどうかを確認します。 砲塔にターゲットがある場合、砲塔の頭がプレイヤーの足元を狙わないように PLAYER_HEIGHT を加算して見るようにします。

次に、砲塔の体力がゼロより大きいかどうかを確認します。 そうであれば、砲塔に弾薬があるかどうかを確認します。

そうである場合は、fire_timer がゼロより大きいかどうかを確認します。 もしそうなら、砲塔は発砲できず、fire_timer から delta 分を減らす必要があります。fire_timer がゼロ以下の場合、砲塔は弾丸を発射できるため、fire_bullet 関数を呼び出します。

砲塔に弾薬がない場合、ammo_reload_timer がゼロより大きいかどうかを確認します。 そうである場合、ammo_reload_timer から delta を減算します。ammo_reload_timer がゼロ以下の場合、砲塔が弾薬を補充するのに十分な時間待機しているため、ammo_in_turretAMMO_IN_FULL_TURRET に設定します。

次に、砲塔がアクティブかどうか以外に、砲塔の体力が 0 以下かどうかを確認します。砲塔の体力がゼロ以下の場合、destroyed_timer がゼロより大きいかどうかを確認します。 もしそうなら、destroyed_timer から delta を減算します。

destroyed_timer がゼロ以下の場合、turret_healthMAX_TURRET_HEALTH に設定し、smoke_particles.transmitfalse に設定して煙のパーティクルの放出を停止します。


次に、fire_bullet を見てみましょう。

まず、砲塔がレイキャストを使用しているかどうかを確認します。

レイキャストを使用するためのコードは、パート2 のライフルのコードとほぼ同じなので、簡単に説明します。

最初にレイキャストでターゲットを確認し、何も邪魔されていない場合にレイキャストがターゲットにヒットするようにします。 次に、レイキャストを強制的に更新して、フレームパーフェクトコリジョンチェックを取得します。 次に、レイキャストが何かと衝突したかどうかを確認します。 その場合、衝突したボディに bullet_hit メソッドがあるかどうかを確認します。 もしそうなら、私たちはそれを呼び出して、レイキャストのtransformとともに単一のレイキャストの弾丸が与えるダメージを渡します。 次に、ammo_in_turret から 1 を引きます。

砲塔がレイキャストを使用していない場合、代わりに弾丸オブジェクトを生成します。 このコードは パート2 のピストルのコードとほぼ完全に同じであるため、レイキャストコードと同様に、簡単に説明します。

最初に弾丸のクローンを作成し、それを clone に割り当てます。 次に、それをルートノードの子として追加します。 弾丸のグローバルtransformを砲身の終端に設定し、そのままでは小さすぎるためにスケールアップし、砲塔の定数クラス変数を使用してダメージと速度を設定します。 次に、ammo_in_turret から 1 を引きます。

次に、使用した弾丸方式に関係なく、両方の銃口フラッシュメッシュを表示します。 flash_timerfire_timer をそれぞれ FLASH_TIMEFIRE_TIME に設定します。 次に、砲塔が弾薬の最後の弾丸を使用したかどうかを確認します。 その場合は、砲塔がリロードされるように ammo_reload_timerAMMO_RELOAD_TIME に設定します。


次に body_entered_vision を見てみますが、ありがたいことにそれはかなり短いです。

まず、 current_targetnull と等しいかどうかを確認することにより、砲塔が現在ターゲットを持っているかどうかを確認します。 砲塔にターゲットがない場合は、ビジョンに入ったばかりのボディ AreaKinematicBody であるかどうかを確認します。

注釈

砲塔は KinematicBody ノードでのみ発動するはずであると想定しています。これはプレイヤーが使用しているものだからです。

ビジョン Area に入ったばかりのボディが KinematicBody である場合、current_target にボディに設定し、is_active を `` true`` に設定します。


それでは、body_exited_vision を見てみましょう。

まず、砲塔にターゲットがあるかどうかを確認します。もしそうなら、砲塔の視界から出たばかりのボディ Area が砲塔のターゲットかどうかを確認します。

視界 Area を出たばかりのボディが砲塔の現在のターゲットである場合、current_targetnull に設定し、is_activefalse に設定し、砲塔には発射するターゲットがなくなったため、砲塔の発射に関連するすべての変数をリセットします。


最後に、bullet_hit を見てみましょう。

まず、砲塔の体力から弾丸が引き起こすダメージを差し引きます。

次に、砲塔が破壊されたかどうかを確認します(体力ゼロ以下)。砲塔が破壊された場合、煙のパーティクルの放出を開始し、destroyed_timer を `` DESTROYED_TIME`` に設定して、砲塔が修理されるまで待機させる必要があります。


ふう、コーディングが完了したので、砲塔を使用する準備ができるまでに最後にやるべきことは1つだけです。 まだ開いていない場合は Turret.tscn を開き、Base または Head から StaticBody ノードのいずれかを選択します。TurretBodies.gd という新しいスクリプトを作成し、選択した StaticBody のいずれかにアタッチします。

次のコードを TurretBodies.gd に追加します:

extends StaticBody

export (NodePath) var path_to_turret_root

func _ready():
    pass

func bullet_hit(damage, bullet_hit_pos):
    if path_to_turret_root != null:
        get_node(path_to_turret_root).bullet_hit(damage, bullet_hit_pos)

このコードは単に、path_to_turret_root で導かれるあらゆるノードで bullet_hit を呼び出します。エディタに戻り、NodePathTurret ノードに割り当てます。

次に、他の StaticBody ノード(Body または Head のいずれか)を選択し、TurretBodies.gd スクリプトを割り当てます。スクリプトがアタッチされたら、再び NodePathTurret ノードに割り当てます。


最後に行う必要があるのは、プレイヤーを傷つける方法を追加することです。すべての弾丸は bullet_hit 関数を使用するため、プレイヤーにその関数を追加する必要があります。

Player.gd を開き、次のように追加します:

func bullet_hit(damage, bullet_hit_pos):
    health -= damage

すべて完了したら、完全に機能する砲塔を実際に手にすべきです! 1つ/両方/すべてのシーンにいくつか配置して、試してみてください!

最終ノート

../../../_images/PartFiveFinished.png

これで RigidBody ノードを取得して手榴弾を投げることができます。 また、プレイヤーを攻撃するための砲塔もあります。

パート6 では、メインメニューと一時停止メニューを追加し、プレイヤーの再出現システムを追加し、任意のスクリプトから使用できるようにサウンドシステムを変更/移動します。

警告

迷子になったら、必ずコードをもう一度読んでください!

このパートの完成したプロジェクトは、ここからダウンロードできます: Godot_FPS_Part_5.zip