パート4

パートの概要

このパートでは、回復アイテムピックアップ、弾薬ピックアップ、プレイヤーが破壊できるターゲット、ジョイパッドのサポート、スクロールホイールで武器を変更する機能を追加します。

../../../_images/PartFourFinished.png

注釈

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

では、始めましょう!

ジョイパッド入力を追加する

注釈

Godotでは、ゲームコントローラーはジョイパッドと呼ばれます。これには、コンソールコントローラー、ジョイスティック(フライトシミュレーターなど)、ホイール(ドライビングシミュレーターなど)、VRコントローラーなどが含まれます!

まず、プロジェクトの入力マップでいくつかのことを変更する必要があります。プロジェクト設定を開き、[インプットマップ]タブを選択します。

次に、さまざまなアクションにいくつかのジョイパッドボタンを追加する必要があります。プラスアイコンをクリックして、ゲームパッドのボタン を選択します。

../../../_images/ProjectSettingsAddKey.png

任意のボタンレイアウトを自由に使用できます。選択したデバイスが 0 に設定されていることを確認してください。完成したプロジェクトでは、次のものを使用します:

  • movement_sprint: Device 0, Button 4 (L, L1)
  • fire: Device 0, Button 0 (PS Cross, XBox A, Nintendo B)
  • reload: Device 0, Button 0 (PS Square, XBox X, Nintendo Y)
  • flashlight: Device 0, Button 12 (D-Pad Up)
  • shift_weapon_positive: Device 0, Button 15 (D-Pad Right)
  • shift_weapon_negative: Device 0, Button 14 (D-Pad Left)
  • fire_grenade: Device 0, Button 1 (PS Circle, XBox B, Nintendo A).

注釈

スターターアセットをダウンロードした場合、これらは既に設定されています

入力に満足したら、プロジェクト設定を閉じて保存します。


Player.gd を開いて、ジョイパッド入力を追加しましょう。

まず、いくつかの新しいクラス変数を定義する必要があります。 以下のクラス変数を Player.gd に追加してください:

# You may need to adjust depending on the sensitivity of your joypad
var JOYPAD_SENSITIVITY = 2
const JOYPAD_DEADZONE = 0.15

これらのそれぞれが何をするかを見てみましょう:

  • JOYPAD_SENSITIVITY: これは、ジョイパッドのジョイスティックがカメラを動かす速度です。
  • JOYPAD_DEADZONE: ジョイパッドのデッドゾーン。ジョイパッドに応じて調整が必要な場合があります。

注釈

多くのジョイパッドは、特定のポイントを中心にジッタを起こします。これに対抗するために、半径JOYPAD_DEADZONE内の動きは無視します。この動きを無視しないと、カメラが揺れます。

また、JOYPAD_SENSITIVITY は定数ではなく変数として定義しています。これは後で変更するためです。

これで、ジョイパッド入力の処理を開始する準備ができました!


process_input で、input_movement_vector = input_movement_vector.normalized() の直前に次のコードを追加します:

# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:

    var joypad_vec = Vector2(0, 0)

    if OS.get_name() == "Windows":
        joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
    elif OS.get_name() == "X11":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))
    elif OS.get_name() == "OSX":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))

    if joypad_vec.length() < JOYPAD_DEADZONE:
        joypad_vec = Vector2(0, 0)
    else:
        joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

    input_movement_vector += joypad_vec
# Add joypad input if one is present
if Input.get_connected_joypads().size() > 0:

    var joypad_vec = Vector2(0, 0)

    if OS.get_name() == "Windows" or OS.get_name() == "X11":
        joypad_vec = Vector2(Input.get_joy_axis(0, 0), -Input.get_joy_axis(0, 1))
    elif OS.get_name() == "OSX":
        joypad_vec = Vector2(Input.get_joy_axis(0, 1), Input.get_joy_axis(0, 2))

    if joypad_vec.length() < JOYPAD_DEADZONE:
        joypad_vec = Vector2(0, 0)
    else:
        joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

    input_movement_vector += joypad_vec

私たちがやっていることを見てみましょう。

まず、接続されたジョイパッドがあるかどうかを確認します。

ジョイパッドが接続されている場合は、左スティックの軸を上/下/左/右に取得します。有線のXbox 360コントローラーにはOSによって異なるジョイスティック軸のマッピングがあるため、OSに基づいた異なる軸を使用します。

警告

このチュートリアルでは、XBox 360またはPlaystationの有線コントローラーを使用していることを前提としています。また、私は(現在)Macコンピューターにアクセスできないため、Macではジョイスティックの軸を変更する必要があるかもしれません。もしそうなら、GodotドキュメントリポジトリでGitHub issueを開いてください!よろしく!

次に、ジョイパッドのベクトルの長さが JOYPAD_DEADZONE 半径内にあるかどうかを確認します。もしそうなら、 joypad_vec を空のVector2に設定します。そうでない場合は、正確なデッドゾーンの計算にスケーリングされたラジアルデッドゾーンを使用します。

注釈

ジョイパッド/コントローラーのデッドゾーンの処理方法に関するすべてを説明する素晴らしい記事は、ここで見つけることができます: http://joshsutphin.com/2013/04/12/doing-thumbstick-dead-zones-right.html

この記事で提供されているスケーリングされた放射状デッドゾーンコードの翻訳バージョンを使用しています。この記事は素晴らしい読み物です。ぜひご覧ください!

最後に、joypad_vecinput_movement_vector に追加します。

ちなみに

input_movement_vector を正規化する方法を覚えていますか?これがその理由です! input_movement_vector を正規化しない場合、キーボードとジョイパッドの両方で同じ方向に押した場合、プレイヤーはより速く動いてしまいます!


process_view_input という新しい関数を作成し、次のように追加します:

func process_view_input(delta):

    if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
        return

    # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
    # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!

    # ----------------------------------
    # Joypad rotation

    var joypad_vec = Vector2()
    if Input.get_connected_joypads().size() > 0:

        if OS.get_name() == "Windows":
            joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
        elif OS.get_name() == "X11":
            joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))
        elif OS.get_name() == "OSX":
            joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))

        if joypad_vec.length() < JOYPAD_DEADZONE:
            joypad_vec = Vector2(0, 0)
        else:
            joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

        rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))

        rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))

        var camera_rot = rotation_helper.rotation_degrees
        camera_rot.x = clamp(camera_rot.x, -70, 70)
        rotation_helper.rotation_degrees = camera_rot
    # ----------------------------------
func process_view_input(delta):

   if Input.get_mouse_mode() != Input.MOUSE_MODE_CAPTURED:
       return

   # NOTE: Until some bugs relating to captured mice are fixed, we cannot put the mouse view
   # rotation code here. Once the bug(s) are fixed, code for mouse view rotation code will go here!

   # ----------------------------------
   # Joypad rotation

   var joypad_vec = Vector2()
   if Input.get_connected_joypads().size() > 0:

       if OS.get_name() == "Windows" or OS.get_name() == "X11":
           joypad_vec = Vector2(Input.get_joy_axis(0, 2), Input.get_joy_axis(0, 3))
       elif OS.get_name() == "OSX":
           joypad_vec = Vector2(Input.get_joy_axis(0, 3), Input.get_joy_axis(0, 4))

       if joypad_vec.length() < JOYPAD_DEADZONE:
           joypad_vec = Vector2(0, 0)
       else:
           joypad_vec = joypad_vec.normalized() * ((joypad_vec.length() - JOYPAD_DEADZONE) / (1 - JOYPAD_DEADZONE))

       rotation_helper.rotate_x(deg2rad(joypad_vec.y * JOYPAD_SENSITIVITY))

       rotate_y(deg2rad(joypad_vec.x * JOYPAD_SENSITIVITY * -1))

       var camera_rot = rotation_helper.rotation_degrees
       camera_rot.x = clamp(camera_rot.x, -70, 70)
       rotation_helper.rotation_degrees = camera_rot
   # ----------------------------------

何が起こっているのか見てみましょう:

まず、マウスモードを確認します。マウスモードが MOUSE_MODE_CAPTURED ではない場合、関数から戻りたいので、それ以降のコードをスキップします。

次に、joypad_vec という新しい Vector2 を定義します。これにより、正しいジョイスティックの位置が保持されます。 OSに基づいて、適切なジョイスティックの適切な軸にマップされるように値を設定します。

警告

上で述べたように、私は(現在)Macコンピューターにアクセスできないので、ジョイスティックの軸を変更する必要があるかもしれません。もしそうなら、GodotドキュメントリポジトリでGitHub issueを開いてください!よろしく!

次に、process_input とまったく同じように、ジョイパッドのデッドゾーンを考慮します。

次に、rotation_helper とプレイヤーの KinematicBodyjoypad_vec を使用して回転させます。

プレイヤーと rotation_helper の回転を処理するコードが _input のコードとまったく同じであることに注意してください。行ったことは、joypad_vecJOYPAD_SENSITIVITY を使用するように値を変更することだけです。

注釈

Windows上ではマウス関連のバグがいくつかあるため、process_view にマウスの回転をプットできません。 これらのバグが修正されると、これもおそらく process_view_input にマウスの回転をプットするように更新されます。

最後に、プレイヤーが逆さまに見えないようにカメラの回転を固定します。


最後に、_physics_processprocess_view_input を追加します。

process_view_input_physics_process に追加されると、ジョイパッドを使用してプレイできるようになります!

注釈

ジョイパッドのトリガーを使用しないことにしたのは、軸の管理をもう少し行う必要があるのと、ショルダーボタンを使用して発砲することが好まれるからです。

トリガーを使って発砲したい場合は、process_input で発砲方法を変更する必要があります。トリガーの軸の値を取得し、それが特定の値(例えば 0.8)を超えているかどうかをチェックする必要があります。もしそうなら、fire アクションが押された時と同じコードを追加します。

マウススクロールホイール入力の追加

ピックアップとターゲットの作業を開始する前に、もう1つの入力関連処理を追加しましょう。 マウスのスクロールホイールを使用して武器を変更する機能を追加しましょう。

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

var mouse_scroll_value = 0
const MOUSE_SENSITIVITY_SCROLL_WHEEL = 0.08

これらの新しい変数のそれぞれが何をしているのかを見てみましょう:

  • mouse_scroll_value: マウスのスクロールホイールの値。
  • MOUSE_SENSITIVITY_SCROLL_WHEEL: 単一のスクロールアクションがmouse_scroll_valueをどれだけ増加させるか。

では、次を _input に追加しましょう:

if event is InputEventMouseButton and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
    if event.button_index == BUTTON_WHEEL_UP or event.button_index == BUTTON_WHEEL_DOWN:
        if event.button_index == BUTTON_WHEEL_UP:
            mouse_scroll_value += MOUSE_SENSITIVITY_SCROLL_WHEEL
        elif event.button_index == BUTTON_WHEEL_DOWN:
            mouse_scroll_value -= MOUSE_SENSITIVITY_SCROLL_WHEEL

        mouse_scroll_value = clamp(mouse_scroll_value, 0, WEAPON_NUMBER_TO_NAME.size() - 1)

        if changing_weapon == false:
            if reloading_weapon == false:
                var round_mouse_scroll_value = int(round(mouse_scroll_value))
                if WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value] != current_weapon_name:
                    changing_weapon_name = WEAPON_NUMBER_TO_NAME[round_mouse_scroll_value]
                    changing_weapon = true
                    mouse_scroll_value = round_mouse_scroll_value

ここで何が起こっているのかを見てみましょう:

まず、イベントが InputEventMouseButton イベントであり、マウスモードが MOUSE_MODE_CAPTURED であるかどうかを確認します。 次に、ボタンのインデックスが BUTTON_WHEEL_UP インデックスなのか ``BUTTON_WHEEL_DOWN``インデックスなのかを確認します。

イベントのインデックスが実際にボタンホイールインデックスである場合、それが BUTTON_WHEEL_UP または BUTTON_WHEEL_DOWN インデックスであるかどうかを確認します。 アップまたはダウンに基づいて、mouse_scroll_valueMOUSE_SENSITIVITY_SCROLL_WHEEL を加算または減算します。

次に、マウススクロール値を固定して、選択可能な武器の範囲内に収まるようにします。

次に、プレイヤーが武器を変更しているか、リロードしているかを確認します。 プレイヤーがどちらも実行していない場合、mouse_scroll_value を丸めて int にキャストします。

注釈

mouse_scroll_valueint にキャストしているので、dictionaryのキーとして使用できます。 floatのままにしておくと、プロジェクトを実行しようとしたときにエラーが発生します。

次に、WEAPON_NUMBER_TO_NAME を使用して、round_mouse_scroll_value の武器名が現在の武器名と等しくないかどうかを確認します。 武器がプレイヤーの現在の武器と異なる場合、changing_weapon_name を割り当て、changing_weapontrue に設定して、プレイヤーが process_changing_weapon で武器を変更し、そして、mouse_scroll_valueround_mouse_scroll_value に設定します。

ちなみに

mouse_scroll_value を丸められたスクロール値に設定しているのは、プレイヤーがマウススクロールホイールを値のちょうど中間に置いておくのを望まないためです。 mouse_scroll_valueround_mouse_scroll_value に割り当てることにより、各武器が正確に同じ量のスクロールを行って変更されるようにします。


変更する必要があるもう1つは process_input``です。 武器を変更するためのコードで、\ ``changing_weapon = true 行 の直後に以下を追加します:

mouse_scroll_value = weapon_change_number

これで、キーボード入力でもスクロール値が変更されます。 この変更をしなかった場合、スクロール値が同期しなくなります。 スクロールホイールが同期していない場合、前方または後方へのスクロールは次/最後の武器に移行せず、スクロールホイールで変更された次/最後の武器に移行します。


スクロールホイールを使用して武器を変更できるようになりました! それを旋回させてみてください!

回復アイテムピックアップの追加

プレイヤーは体力と弾薬を手に入れたので、理想としてこれらのリソースを補充する方法が必要です。

Health_Pickup.tscn を開きます。

まだ展開されていない場合は、Holder を展開します。 2つのSpatialノードがあることに注目してください。1つは Health_Kit、もう1つは Health_Kit_Small と呼ばれます。

これは、実際には2つのサイズの回復アイテムピックアップを作成するためです。Health_Kit および Health_Kit_Small には、子として単一の MeshInstance のみがあります。

次に、Health_Pickup_Trigger を展開します。 これは Area ノードであり、プレイヤーが回復キットを拾うのに十分な距離を歩いたかどうかを確認するために使用します。 展開すると、サイズごとに1つずつ、2つのコリジョンシェイプが見つかります。回復アイテムピックアップのサイズに応じて異なるコリジョンシェイプサイズを使用するので、小さい回復アイテムピックアップの方が、そのサイズに近いトリガーコリジョンシェイプを持っています。

最後に注意することは、AnimationPlayer ノードを使用して、回復キットがゆっくりと動き回るようにすることです。

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

extends Spatial

export (int, "full size", "small") var kit_size = 0 setget kit_size_change

# 0 = full size pickup, 1 = small pickup
const HEALTH_AMOUNTS = [70, 30]

const RESPAWN_TIME = 20
var respawn_timer = 0

var is_ready = false

func _ready():

    $Holder/Health_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")

    is_ready = true

    kit_size_change_values(0, false)
    kit_size_change_values(1, false)
    kit_size_change_values(kit_size, true)


func _physics_process(delta):
    if respawn_timer > 0:
        respawn_timer -= delta

        if respawn_timer <= 0:
            kit_size_change_values(kit_size, true)


func kit_size_change(value):
    if is_ready:
        kit_size_change_values(kit_size, false)
        kit_size = value
        kit_size_change_values(kit_size, true)
    else:
        kit_size = value


func kit_size_change_values(size, enable):
    if size == 0:
        $Holder/Health_Pickup_Trigger/Shape_Kit.disabled = !enable
        $Holder/Health_Kit.visible = enable
    elif size == 1:
        $Holder/Health_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
        $Holder/Health_Kit_Small.visible = enable


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

このスクリプトの実行内容をクラス変数から見ていきましょう:

  • kit_size: 回復アイテムピックアップのサイズ。setget 関数を使用して、変更されたかどうかを確認していることに注目してください。
  • HEALTH_AMMOUNTS: 各サイズの各ピックアップに含まれる回復量。
  • RESPAWN_TIME:回復アイテムピックアップが再産出するのにかかる時間(秒単位)
  • ``respawn_timer``回復アイテムピックアップが再産出を待機している時間を追跡するために使用される変数。
  • is_ready: _ready 関数が呼び出されたかどうかを追跡する変数。

setget 関数は _ready の前に呼び出されるため、is_ready を使用しています。_ready が呼び出されるまで子ノードにアクセスできないため、最初のkit_size_change呼び出しを無視する必要があります。 最初の setget 呼び出しを無視しないと、デバッガでいくつかのエラーが発生します。

また、エクスポートされた変数の使用方法にも注目してください。 これは、エディタで回復アイテムピックアップのサイズを変更できるようにするためです。 これにより、エクスポートされた変数を使用してエディタでサイズを簡単に変更できるため、2つのサイズに対して2つのシーンを作成する必要がなくなります。

ちなみに

GDScriptの基本 を参照し、使用可能なエクスポートヒントのリストについては、GDScriptエクスポートセクションまでスクロールします。


_ready を見てみましょう:

まず、Health_Pickup_Trigger からの body_entered シグナルを trigger_body_entered 関数に接続します。 これにより、Area に入るボディが trigger_body_entered 関数をトリガーします。

次に、set_ 関数を使用できるように is_readytrue に設定します。

次に、kit_size_change_values を使用して、すべての可能なキットとそのコリジョンシェイプを非表示にします。 最初の引数はキットのサイズであり、2番目の引数はそのサイズでコリジョンシェイプとメッシュを有効にするか無効にするかです。

次に、選択したキットのサイズのみを表示し、kit_size_change_values を呼び出して kit_sizetrue を渡すので、kit_size のサイズが有効になります。


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

最初に行うことは、is_readytrue であるかどうかを確認することです。

is_readytrue の場合、kit_size_change_values を使用して kit_size に既に割り当てられているキットを無効にし、kit_sizefalse を渡します。

次に、渡された新しい値 valuekit_size に割り当てます。 次に kit_size_change_values を呼び出して kit_size を再度渡しますが、今回は2番目の引数を true にして、有効にします。kit_size を渡された値に変更したため、渡されたキットのサイズが見えるようになります。

is_readytrue でない場合、渡された valuekit_size を割り当てるだけです。


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

最初に行うのは、渡されたサイズを確認することです。有効/無効にするサイズに基づいて、異なるノードを取得します。

size に対応するノードのコリジョンシェイプを取得し、引数/変数で渡された enabled に基づいて無効にします。

注釈

enable の代わりに !enable を使用するのはなぜですか? これは、ノードを有効にしたいというときは true を渡すことができますが、CollisionShape は有効化ではなく無効化という考え方を使っているので、反転する必要があります。 それを反転させることで、コリジョンシェイプを有効にし、true が渡されたときにメッシュを表示できます。

次に、メッシュを保持する正しい Spatial ノードを取得し、その可視性を enable に設定します。

この関数は少しわかりにくいかもしれません。 このように考えてみてください: enabled を使用して size の適切なノードを有効/無効にします。 これにより、表示されていないサイズの回復アイテムは取得できず、適切なサイズのメッシュのみが表示されます。


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

最初に行うことは、入力した本体に add_health というメソッド/関数があるかどうかを確認することです。 もしそうなら、add_health を呼び出して、現在のキットサイズによって提供される回復量を渡します。

次に、respawn_timerRESPAWN_TIME に設定し、プレイヤーが再び健康になるまで待機する必要があります。 最後に、kit_size_change_values を呼び出して、kit_sizefalse を渡して、kit_size のキットが再産出するのを待つまで、見えないようにします。


プレイヤーがこの回復アイテムピックアップを使用する前に行う必要がある最後の作業は、Player.gd にいくつかの項目を追加することです。

Player.gd を開き、次のクラス変数を追加します:

const MAX_HEALTH = 150
  • MAX_HEALTH: プレイヤーが持つことができる体力の最大量。

次に、プレイヤーに add_health 関数を追加する必要があります。以下を Player.gd に追加します:

func add_health(additional_health):
    health += additional_health
    health = clamp(health, 0, MAX_HEALTH)

これを簡単に説明しましょう。

まず、プレイヤーの現在の体力に additional_health を追加します。次に、体力をクランプして、MAX_HEALTH よりも高い値、または 0 よりも低い値を取ることができないようにします。


これが完了すると、プレイヤーは回復アイテムを収集できるようになります!いくつかの Health_Pickup シーンをいくつか配置して試してみてください。インスタンス化された Health_Pickup シーンが選択されたときに、便利なドロップダウンからエディタで回復アイテムピックアップのサイズを変更できます。

弾薬ピックアップの追加

体力を追加できることはとても良いことですが、(現在)何ものも私たちを傷つけることはできないので、追加してもなにもご褒美を得ることができません。 次に弾薬箱を追加しましょう!

Ammo_Pickup.tscn を開きます。Health_Pickup.tscn とまったく同じように構成されていますが、メッシュとトリガーのコリジョンシェイプがメッシュサイズの違いを考慮してわずかに変更されていることに注意してください。

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

extends Spatial

export (int, "full size", "small") var kit_size = 0 setget kit_size_change

# 0 = full size pickup, 1 = small pickup
const AMMO_AMOUNTS = [4, 1]

const RESPAWN_TIME = 20
var respawn_timer = 0

var is_ready = false

func _ready():

    $Holder/Ammo_Pickup_Trigger.connect("body_entered", self, "trigger_body_entered")

    is_ready = true

    kit_size_change_values(0, false)
    kit_size_change_values(1, false)

    kit_size_change_values(kit_size, true)


func _physics_process(delta):
    if respawn_timer > 0:
        respawn_timer -= delta

        if respawn_timer <= 0:
            kit_size_change_values(kit_size, true)


func kit_size_change(value):
    if is_ready:
        kit_size_change_values(kit_size, false)
        kit_size = value

        kit_size_change_values(kit_size, true)
    else:
        kit_size = value


func kit_size_change_values(size, enable):
    if size == 0:
        $Holder/Ammo_Pickup_Trigger/Shape_Kit.disabled = !enable
        $Holder/Ammo_Kit.visible = enable
    elif size == 1:
        $Holder/Ammo_Pickup_Trigger/Shape_Kit_Small.disabled = !enable
        $Holder/Ammo_Kit_Small.visible = enable


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)

このコードは、回復アイテムピックアップとほぼ同じに見えることに気付いたかもしれません。 それはほとんど同じだからです! 変更されたのはごく一部であり、それが私たちが検討することです。

まず、HEALTH_AMMOUNTS から AMMO_AMOUNTS に変更することに注意してください。AMMO_AMOUNTS は、ピックアップが現在の武器に追加する弾薬クリップ/マガジンの数です。回復ポイントがいくつ付与されるかを表す HEALTH_AMMOUNTS の場合とは異なり、生の弾薬量ではなくクリップ全体を現在の武器に加算します)

注目すべき他の唯一のものは trigger_body_entered にあります。add_health の代わりに add_ammo と呼ばれる関数の存在を確認し、呼び出しています。

これら2つの小さな変更を除いて、他のすべては回復アイテムピックアップと同じです!


弾薬のピックアップを機能させるために必要なことは、プレイヤーに新しい関数を追加することだけです。Player.gd を開き、次の関数を追加します:

func add_ammo(additional_ammo):
    if (current_weapon_name != "UNARMED"):
        if (weapons[current_weapon_name].CAN_REFILL == true):
            weapons[current_weapon_name].spare_ammo += weapons[current_weapon_name].AMMO_IN_MAG * additional_ammo

この関数の動作について説明します。

最初に確認するのは、プレイヤーが UNARMED かどうかです。UNARMED にはノード/スクリプトがないため、ノード/スクリプトを current_weapon_name にアタッチする前に、プレイヤーが UNARMED になっていないことを確認する必要があります。

次に、現在の武器を補充できるかどうかを確認します。 現在の武器が補充可能であれば、現在の武器の AMMO_IN_MAG 値に追加する弾薬クリップの数(additional_ammo)を掛けることで、完全なクリップ/弾倉に相当する弾薬を武器に追加します。


これで、追加の弾薬を入手できるはずです! 1つ/両方/すべてのシーンに弾薬箱をいくつか置いて、試してみてください!

注釈

携行できる弾薬の量が制限されていないことに注意してください。 各武器が持ち込める弾薬の量を制限するには、各武器のスクリプトに追加の変数を追加し、add_ammo で弾薬を追加した後に武器の spare_ammo 変数をクランプする必要があります。

破壊可能なターゲットの追加

このパートを終了する前に、いくつかのターゲットを追加してみましょう。

Target.tscn を開き、シーンツリーのシーンを見てみます。

まず、RigidBody ノードではなく StaticBody ノードを使用していることに注意してください。 この背後にある理由は、壊れていないターゲットがどこにも移動しないことです。RigidBody を使用すると、静止している必要があるため、価値よりも面倒が勝っています。

ちなみに

また、StaticBodyRigidBody に重ねて使用することにより、パフォーマンスを少し改善できます。

もう1つ注意すべきことは、Broken_Target_Holder というノードがあることです。 このノードは、Broken_Target.tscn と呼ばれる生成/インスタンス化されたシーンを保持します。Broken_Target.tscn を開きます。

ターゲットが5つの部分に分割されていることに注目してください。各部分は RigidBody ノードです。 ターゲットがあまりにも多くのダメージを受け、破壊する必要がある場合、このシーンを産出/インスタンスします。 次に、壊れていないターゲットを非表示にします。そのため、粉々になったターゲットが産出/インスタンス化されたのではなく、粉々になったターゲットのように見えます。

Broken_Target.tscn を開いたまま、すべての RigidBody ノードに RigidBody_hit_test.gd をアタッチします。これにより、プレイヤーは壊れた破片を撃つことができ、それは弾丸に反応します。

さて、Target.tscn に戻り、Target StaticBody ノードを選択して、Target.gd という新しいスクリプトを作成します。

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

extends StaticBody

const TARGET_HEALTH = 40
var current_health = 40

var broken_target_holder

# The collision shape for the target.
# NOTE: this is for the whole target, not the pieces of the target.
var target_collision_shape

const TARGET_RESPAWN_TIME = 14
var target_respawn_timer = 0

export (PackedScene) var destroyed_target

func _ready():
    broken_target_holder = get_parent().get_node("Broken_Target_Holder")
    target_collision_shape = $Collision_Shape


func _physics_process(delta):
    if target_respawn_timer > 0:
        target_respawn_timer -= delta

        if target_respawn_timer <= 0:

            for child in broken_target_holder.get_children():
                child.queue_free()

            target_collision_shape.disabled = false
            visible = true
            current_health = TARGET_HEALTH


func bullet_hit(damage, bullet_transform):
    current_health -= damage

    if current_health <= 0:
        var clone = destroyed_target.instance()
        broken_target_holder.add_child(clone)

        for rigid in clone.get_children():
            if rigid is RigidBody:
                var center_in_rigid_space = broken_target_holder.global_transform.origin - rigid.global_transform.origin
                var direction = (rigid.transform.origin - center_in_rigid_space).normalized()
                # Apply the impulse with some additional force (I find 12 works nicely).
                rigid.apply_impulse(center_in_rigid_space, direction * 12 * damage)

        target_respawn_timer = TARGET_RESPAWN_TIME

        target_collision_shape.disabled = true
        visible = false

このスクリプトの動作について、クラス変数から説明します:

  • TARGET_HEALTH: 完全に回復したターゲットを破壊するために必要なダメージの量。
  • current_health: このターゲットが現在持っている体力の量。
  • broken_target_holder: 簡単に使用できるように Broken_Target_Holder ノードを保持する変数。
  • target_collision_shape: 壊れていないターゲットの CollisionShape を保持する変数。
  • TARGET_RESPAWN_TIME: ターゲットが再出現するのにかかる時間の長さ(秒単位)。
  • target_respawn_timer: ターゲットが壊れている時間を追跡する変数。
  • destroyed_target: 壊れたターゲットシーンを保持するための PackedScene

preload を使用する代わりに、エクスポートされた変数(PackedScene)を使用して破損したターゲットシーンを取得する方法に注意してください。エクスポートされた変数を使用すると、エディタからシーンを選択できます。別のシーンを使用する必要がある場合は、エディタで別のシーンを選択するのと同じくらい簡単です。 使用しているシーンを変更するためにコードに移動する必要はありません。


_ready を見てみましょう。

最初に行うことは、壊れたターゲットホルダーを取得し、それを broken_target_holder に割り当てることです。 ここでは、$ ではなく get_parent().get_node() を使用していることに注意してください。$ `` を使用する場合は、\ ``get_parent().get_node()$"../Broken_Target_Holder" に変更する必要があります。

注釈

これが書かれた時点では、親ノードを取得するために $ を使った $"../ NodeName" が使用できることに気づきませんでした。これが、get_parent().get_node() が代わりに使用されている理由です。

次に、コリジョンシェイプを取得し、target_collision_shape に割り当てます。 コリジョンシェイプが必要な理由は、メッシュが見えなくても、コリジョンシェイプが物理世界に存在するためです。 これにより、プレイヤーは見えないにもかかわらず、壊れていないターゲットとやり取りできるようになります。 これを回避するには、メッシュを表示/非表示にするときにコリジョンシェイプを無効/有効にします。


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

再産出には _physics_process のみを使用するため、最初に行うことは、target_respawn_timer0 より大きいかどうかを確認することです。

もしそうなら、それから delta を減算します。

次に、target_respawn_timer0 以下かどうかを確認します。 この背後にある理由は、target_respawn_timer から delta を削除し、それが 0 以下の場合は、ターゲットがここに到着したので、タイマーが終了したときに行うべきことは何でも効果的に実行できます。

この場合、ターゲットを再産出します。

最初に行うことは、壊れたターゲットホルダーのすべての子を削除することです。 これを行うには、broken_target_holder 内のすべての子を反復処理し、queue_free を使用してそれらを解放します。

次に、disabled ブール値を false に設定してコリジョンシェイプを有効にします。

次に、ターゲットとそのすべての子ノードを再び表示します。

最後に、ターゲットの体力(current_health)を TARGET_HEALTH にリセットします。


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

最初に行うことは、ターゲットの体力から弾丸が与えるダメージを差し引くことです。

次に、ターゲットの体力が 0 以下であるかどうかを確認します。 もしそうなら、ターゲットは死んだばかりで、壊れたターゲットを生成する必要があります。

まず、破壊された新しいターゲットシーンをインスタンス化し、それを新しい変数 clone に割り当てます。

次に、壊れたターゲットホルダーの子として clone を追加します。

ボーナス効果のために、すべてのターゲットピースを外側に爆発させます。 これを行うために、clone のすべての子を反復処理します。

各子について、最初に RigidBody ノードかどうかを確認します。 そうである場合、次に、子ノードを基準にしてターゲットの中心位置を計算します。 次に、子ノードが中心を基準とする方向を把握します。 これらの計算された変数を使用して、弾丸の損傷を力として使用して、計算された中心から中心から離れる方向に子を押します。

注釈

ダメージに 12 を掛けると、より劇的な効果が得られます。 ターゲットをどの程度爆発的に粉砕したいかに応じて、これを高い値または低い値に変更できます。

次に、ターゲットの再産出タイマーを設定します。 タイマーを TARGET_RESPAWN_TIME に設定しているため、再生成されるまで TARGET_RESPAWN_TIME 数がかかります。

次に、壊れていないターゲットのコリジョンシェイプを無効にし、ターゲットの可視性を false に設定します。


警告

エディタで Target.tscn にエクスポートされた destroyed_target 値を設定してください! そうしないと、ターゲットは破壊されず、エラーが発生します!

それが完了したら、1つ/両方/すべてのレベルに Target.tscn インスタンスをいくつか配置します。 十分なダメージを受けた後、それらが5つの破片に爆発することに気付くはずです。 しばらくすると、再びターゲット全体に再出現します。

最終ノート

../../../_images/PartFourFinished.png

これで、ジョイパッドを使用し、マウスのスクロールホイールで武器を変更し、体力と弾薬を補充し、武器でターゲットを破壊できます。

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

警告

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

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