パート2

パートの概要

このパートでは、操作できる武器をプレイヤーに提供します。

../../../_images/PartTwoFinished.png

このパートの終わりまでに、あなたはピストルを発射し、ライフルを持ち、ナイフを使用して攻撃できるプレイヤーになります。また、プレイヤーにはトランジションを含むアニメーションがあり、武器は環境内のオブジェクトと相互作用します。

注釈

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

では、始めましょう!

アニメーションを処理するシステムを作成する

最初に、変化するアニメーションを処理する方法が必要です。Player.tscn を開き、AnimationPlayer ノードを選択します(Player -> Rotation_Helper -> Model-> Animation_Player)。

AnimationPlayer_Manager.gd という新しいスクリプトを作成し、それを AnimationPlayer にアタッチします。

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

extends AnimationPlayer

# Structure -> Animation name :[Connecting Animation states]
var states = {
    "Idle_unarmed":["Knife_equip", "Pistol_equip", "Rifle_equip", "Idle_unarmed"],

    "Pistol_equip":["Pistol_idle"],
    "Pistol_fire":["Pistol_idle"],
    "Pistol_idle":["Pistol_fire", "Pistol_reload", "Pistol_unequip", "Pistol_idle"],
    "Pistol_reload":["Pistol_idle"],
    "Pistol_unequip":["Idle_unarmed"],

    "Rifle_equip":["Rifle_idle"],
    "Rifle_fire":["Rifle_idle"],
    "Rifle_idle":["Rifle_fire", "Rifle_reload", "Rifle_unequip", "Rifle_idle"],
    "Rifle_reload":["Rifle_idle"],
    "Rifle_unequip":["Idle_unarmed"],

    "Knife_equip":["Knife_idle"],
    "Knife_fire":["Knife_idle"],
    "Knife_idle":["Knife_fire", "Knife_unequip", "Knife_idle"],
    "Knife_unequip":["Idle_unarmed"],
}

var animation_speeds = {
    "Idle_unarmed":1,

    "Pistol_equip":1.4,
    "Pistol_fire":1.8,
    "Pistol_idle":1,
    "Pistol_reload":1,
    "Pistol_unequip":1.4,

    "Rifle_equip":2,
    "Rifle_fire":6,
    "Rifle_idle":1,
    "Rifle_reload":1.45,
    "Rifle_unequip":2,

    "Knife_equip":1,
    "Knife_fire":1.35,
    "Knife_idle":1,
    "Knife_unequip":1,
}

var current_state = null
var callback_function = null

func _ready():
    set_animation("Idle_unarmed")
    connect("animation_finished", self, "animation_ended")

func set_animation(animation_name):
    if animation_name == current_state:
        print ("AnimationPlayer_Manager.gd -- WARNING: animation is already ", animation_name)
        return true


    if has_animation(animation_name):
        if current_state != null:
            var possible_animations = states[current_state]
            if animation_name in possible_animations:
                current_state = animation_name
                play(animation_name, -1, animation_speeds[animation_name])
                return true
            else:
                print ("AnimationPlayer_Manager.gd -- WARNING: Cannot change to ", animation_name, " from ", current_state)
                return false
        else:
            current_state = animation_name
            play(animation_name, -1, animation_speeds[animation_name])
            return true
    return false


func animation_ended(anim_name):

    # UNARMED transitions
    if current_state == "Idle_unarmed":
        pass
    # KNIFE transitions
    elif current_state == "Knife_equip":
        set_animation("Knife_idle")
    elif current_state == "Knife_idle":
        pass
    elif current_state == "Knife_fire":
        set_animation("Knife_idle")
    elif current_state == "Knife_unequip":
        set_animation("Idle_unarmed")
    # PISTOL transitions
    elif current_state == "Pistol_equip":
        set_animation("Pistol_idle")
    elif current_state == "Pistol_idle":
        pass
    elif current_state == "Pistol_fire":
        set_animation("Pistol_idle")
    elif current_state == "Pistol_unequip":
        set_animation("Idle_unarmed")
    elif current_state == "Pistol_reload":
        set_animation("Pistol_idle")
    # RIFLE transitions
    elif current_state == "Rifle_equip":
        set_animation("Rifle_idle")
    elif current_state == "Rifle_idle":
        pass;
    elif current_state == "Rifle_fire":
        set_animation("Rifle_idle")
    elif current_state == "Rifle_unequip":
        set_animation("Idle_unarmed")
    elif current_state == "Rifle_reload":
        set_animation("Rifle_idle")

func animation_callback():
    if callback_function == null:
        print ("AnimationPlayer_Manager.gd -- WARNING: No callback function for the animation to call!")
    else:
        callback_function.call_func()

このスクリプトが何をしているのかを見てみましょう:


このスクリプトのクラス変数から始めましょう:

  • states: アニメーションの状態を保持するためのdictionary(辞書)。(詳細な説明は後ほど)
  • animation_speeds: アニメーションを再生したいすべての速度を保持するためのdictionary。
  • current_state: 現在のアニメーション状態の名前を保持する変数。
  • callback_function: コールバック関数を保持する変数。(詳細な説明は後ほど)

ステートマシンに精通している場合、states は基本的なステートマシンのように構成されていることに気付いているかもしれません。大まかに states を設定する方法は次のとおりです:

states は、キーが現在の状態の名前であり、値が遷移可能なすべてのアニメーション(states)を保持する配列である辞書です。たとえば、現在 Idle_unarmed 状態にある場合、Knife_equipPistol_equipRifle_equip、および Idle_unarmed にのみ移行できます。

現在の状態の可能な遷移状態に含まれていない状態に遷移しようとすると、警告メッセージが表示され、アニメーションは変更されません。以下の animation_ended でさらに説明するように、いくつかの状態から他の状態に自動的に移行することもできます。

注釈

このチュートリアルをシンプルにするために、「適切な」状態マシンを使用していません。ステートマシンについて詳しく知りたい場合は、次の記事を参照してください:

animation_speeds は、各アニメーションの再生速度です。一部のアニメーションは少し遅く、すべてをスムーズに見せるために、より速い速度で再生する必要があります。

ちなみに

すべての発射アニメーションが通常の速度よりも速いことに注意してください。後のためにこれを覚えておいてください!

current_state は、現在のアニメーション状態の名前を保持します。

最後に、callback_function は、アニメーションの適切なフレームで弾丸を産出するためにプレイヤーによって渡される FuncRef になります。FuncRef を使用すると、関数を引数として渡すことができ、他のスクリプトから関数を呼び出すことができます。これは後で使用する方法です。


_ready を見てみましょう。

最初に、set_animation 関数を使用してアニメーションを Idle_unarmed に設定しているので、必ずそのアニメーションから始めてください。

次に、animation_finished シグナルをこのスクリプトに接続し、animation_ended を呼び出すように割り当てます。これは、アニメーションが終了するたびに animation_ended が呼び出されることを意味します。


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

set_animation可能な場合は、アニメーションを animation_name という名前のアニメーションに変更します。言い換えると、現在のアニメーション状態が states で渡されたアニメーション状態名を持っている場合、そのアニメーションに変更します。

まず、渡されたアニメーション名が現在再生中のアニメーションと同じ名前であるかどうかを確認します。それらが同じ場合、コンソールに警告を書き込み、true を返します。

次に、AnimationPlayerhas_animation を使用して animation_name という名前のアニメーションを持っているかどうかを確認します。そうでない場合、false を返します。

Thirdly, we check whether current_state is set. If we have a state in current_state, then we get all the possible states we can transition to.

アニメーション名が、使用可能なトランジションのリストにある場合、current_state に渡されたアニメーション(animation_name)を設定しAnimationPlayer に blend timeを-1、speedをanimation_speedsで設定した速度で、そのアニメーションを再生するように指示し、true を返します。

注釈

Blend timeは、2つのアニメーションをブレンド/ミックスする時間です。

-1 の値を入力すると、新しいアニメーションが即座に再生され、既に再生されているアニメーションはすべて上書きされます。

1 の値を入力すると、1秒の間、新しいアニメーションが強さを増しつつ再生、つまり新しいアニメーションのみを再生する前に、2つのアニメーションを1秒間ブレンドします。これにより、アニメーション間のスムーズな移行が可能になります。これは、ウォーキングアニメーションからランニングアニメーションに変更する場合に最適です。

アニメーションを即座に変更するため、ブレンド時間を -1 に設定します。


animation_ended を見てみましょう。

animation_ended は、アニメーションの再生が終了したときに AnimationPlayer によって呼び出される関数です。

特定のアニメーションの状態については、終了時に別の状態に移行する必要がある場合があります。これを処理するために、可能なすべてのアニメーション状態をチェックします。必要に応じて、別の状態に移行します。

警告

独自のアニメーションモデルを使用している場合は、アニメーションがループするように設定されていないことを確認してください。アニメーションのループは、アニメーションの最後に到達して再びループしようとしているときに、animation_finished シグナルを送信しません。

注釈

animation_ended の遷移は理想的には states のデータの一部になりますが、チュートリアルを理解しやすくするために、各状態遷移を animation_ended でハードコーディングします。


最後に、animation_callback があります。この関数は、アニメーションの呼び出しメソッドトラックによって呼び出されます。callback_function に割り当てられた FuncRef がある場合、関数で渡されたものを呼び出します。callback_function に割り当てられた FuncRef がない場合、コンソールに警告を出力します。

ちなみに

Testing_Area.tscn を実行して、実行時の問題がないことを確認してください。ゲームが実行されているが、何も変わっていないように見える場合は、すべてが正常に動作しています。

アニメーションを準備する

アニメーションマネージャーが機能するようになったので、それをプレイヤースクリプトから呼び出す必要があります。ただし、その前に、発砲アニメーションにアニメーションコールバックトラックを設定する必要があります。

まだ開いていない場合は Player.tscn を開き、AnimationPlayer ノードに移動します(Player -> Rotation_Helper -> Model -> Animation_Player)。

3つのアニメーションにメソッド呼出しトラックをアタッチする必要があります: ピストル、ライフル、ナイフの発射アニメーションです。まずピストルから始めましょう。アニメーションドロップダウンリストをクリックし、"Pistol_fire"を選択します。

アニメーショントラックのリストの一番下までスクロールします。リストの最後の項目は Armature/Skeleton:Left_UpperPointer である必要があります。リストの上で、タイムラインの左側にある[トラックを追加]ボタンをクリックします

../../../_images/AnimationPlayerAddTrack.png

これにより、いくつかの選択肢があるウィンドウが表示されます。メソッド呼出しトラックを追加するため、「メソッド呼出しトラック」というオプションをクリックします。これにより、ノードツリー全体を表示するウィンドウが開きます。AnimationPlayer ノードに移動して選択し、[OK]を押します。

../../../_images/AnimationPlayerCallFuncTrack.png

これで、アニメーショントラックのリストの下部に、"AnimationPlayer"いう緑色のトラックが表示されます。次に、コールバック関数を呼び出すポイントを追加する必要があります。銃口が点滅し始めるポイントに達するまで、タイムラインをスクラブ(訳注:左右にごしごしするというニュアンスです)します。

注釈

タイムラインは、アニメーションのすべてのポイントが保存されるウィンドウです。小さなポイントのそれぞれは、アニメーションデータのポイントを表します。

To actually preview the "Pistol_fire" animation, select the Camera node underneath Rotation Helper and check the "Preview" box underneath Perspective in the top-left corner.

タイムラインをスクラブするということは、アニメーションの中で自分自身を動かすことを意味します。したがって、「ポイントに到達するまでタイムラインをスクラブする」と言うときは、タイムライン上のポイントに到達するまでアニメーションウィンドウを移動します。

また、銃の銃口は弾丸が出る端点です。銃口のフラッシュは、弾丸が発射されたときに銃口から漏れる光のフラッシュです。銃口は、銃身とも呼ばれます。

ちなみに

タイムラインをスクラブするとき、より細かく制御するには、Ctrl を押しながら、マウスホイールで前方にスクロールしてズームインします。後方にスクロールするとズームアウトします。

Step(s) の値をより低い/より高い値に変更することで、タイムラインのスクラブのスナップ方法を変更することもできます。

好きなポイントに到達したら、Animation Player の行を右クリックし、[キーを挿入]を押します。空の名前フィールドにanimation_callback と入力し、Enter を押します。

../../../_images/AnimationPlayerInsertKey.png

このアニメーションを再生しているとき、メソッド呼出しトラックはアニメーションの特定のポイントでトリガーされます。


ライフルとナイフの発射アニメーションでこのプロセスを繰り返しましょう!

注釈

このプロセスはピストルとまったく同じですが、もう少し詳しく説明します。迷った場合は、上記の手順に従ってください!別のアニメーションに対して行うだけで、内容はまったく同じです。

アニメーションドロップダウンから"Rifle_fire"アニメーションに移動します。リストの上にある[トラックを追加]ボタンをクリックして、アニメーショントラックリストの一番下に到達したら、メソッド呼出しトラックを追加します。銃口が点滅し始めるポイントを見つけて右クリックし、キーを挿入 を押して、トラック上のその位置にメソッド呼出しトラックポイントを追加します。

開いたポップアップの名前フィールドに”animation_callback”と入力し、Enter を押します。

次に、メソッド呼出しトラックをナイフアニメーションに適用する必要があります。"Knife_fire" アニメーションを選択し、アニメーショントラックの下部までスクロールします。リストの上にある[トラックを追加]ボタンをクリックして、メソッドトラックを追加します。次に、アニメーションコールバックメソッドポイントを配置するアニメーションの最初の3分の1あたりのポイントを見つけます。

注釈

実際にナイフを発射することはありません。アニメーションは、発射するというよりも刺すようなアニメーションです。このチュートリアルでは、銃の発射ロジックをナイフで再利用するため、アニメーションは他のアニメーションと一貫したスタイルで名前が付けられています。

そこからタイムラインを右クリックし、[キーを挿入]をクリックします。"animation_callback"を名前フィールドに入力し、Enter を押します。

ちなみに

必ず作業内容を保存してください!

これで、プレイヤースクリプトに発砲する機能を追加する準備がほぼ整いました。最後のシーンを設定する必要があります。それは、弾丸オブジェクトのシーンです。

弾丸シーンの作成

ビデオゲームで銃の弾丸を処理する方法はいくつかあります。このチュートリアルシリーズでは、2つの一般的な方法を検討します: オブジェクトとレイキャストです。


2つの方法の1つは、弾丸オブジェクトを使用することです。これは、世界を移動し、独自の衝突コードを処理するオブジェクトになります。このメソッドでは、銃が向いている方向に弾丸オブジェクトを作成/産出し、次に前進します。

この方法にはいくつかの利点があります。 1つ目は、弾丸をプレイヤーに保存する必要がないことです。弾丸を作成して先に進みむと、弾丸自体が衝突のチェック、衝突するオブジェクトへの適切なシグナルの送信、およびそれ自体の破壊を処理します。

別の利点は、より複雑な弾丸の動きができることです。時間が経つにつれて弾丸を少しでも落としたい場合は、弾丸制御スクリプトを使用して、弾丸をゆっくりと地面に向かって押します。また、オブジェクトを使用すると、弾丸がターゲットに到達するまでに時間がかかり、指したものがすぐにヒットすることはありません。実際の生活の中で、ある地点から別の地点に瞬時に移動するものはないため、これはより現実的です。

大きな欠点の1つはパフォーマンスです。各弾丸に独自のパスを計算させ、独自の衝突を処理させると、柔軟性が大幅に向上しますが、パフォーマンスが犠牲になります。この方法では、すべてのステップのすべての弾丸の動きを計算しています。これは数十の弾丸では問題にならないかもしれませんが、数百の弾丸がある場合には大きな問題になります。

パフォーマンスの低下にもかかわらず、多くの一人称シューティングゲームには何らかの形のオブジェクトの弾丸が含まれています。ロケットランチャーは、多くの一人称シューティングゲームでは、ロケットが瞬時にターゲットの位置で爆発するわけではないため、弾丸オブジェクトの典型的な例です。また、良く見かける手榴弾は一般的に爆発する前に世界のあちこちで跳ね返るので、これも弾丸オブジェクトの一例です。

注釈

確実にそうだとは言えませんが、これらのゲームは何らかの形で弾丸オブジェクトを使用している可能性があります: (これらは完全に私の観察によるものです。次のゲームのいずれにも関わったことはありません)

  • Halo (ロケットランチャー、フラグメンテーション手榴弾、狙撃ライフル、ブルートショットなど)
  • Destiny (ロケットランチャー、手榴弾、フュージョンライフル、スナイパーライフル、スーパームーブなど)
  • Call of Duty (ロケットランチャー、手榴弾、弾道ナイフ、クロスボウなど)
  • Battlefield (ロケットランチャー、手榴弾、クレイモア、迫撃砲など)

弾丸オブジェクトのもう1つの欠点はネットワークです。 弾丸オブジェクトは、(少なくとも)サーバーに接続されているすべてのクライアントと位置を同期する必要があります。

(チュートリアルシリーズ全体として)何らかのネットワーキング形式を実装していませんが、一人称シューティングゲームを作成する際、将来的に何らかのネットワーキングを追加する予定がある場合は、特に留意する必要があります。


弾丸の衝突を処理するもう1つの方法は、レイキャスティングです。

この手法は、時間の経過とともに軌道を変えることがほとんどない高速移動弾を持つ銃では非常に一般的です。

弾丸オブジェクトを作成して空間に送り出す代わりに、銃の銃身/銃口から前方に向かって光線を送り出します。レイキャストの原点を弾丸の開始位置に設定し、長さに基づいて弾丸が空間を「移動する」距離を調整できます。

注釈

確実にそうだとは言えませんが、これらのゲームは何らかの形でレイキャストを使用している可能性があります: (これらは完全に私の観察によるものです。次のゲームのいずれにも関わったことはありません)

  • Halo (アサルトライフル、DMR、バトルライフル、コヴナントカービン、スパルタンレーザーなど)
  • Destiny (オートライフル、パルスライフル、スカウトライフル、ハンドキャノン、機関銃など)
  • Call of Duty (アサルトライフル、軽機関銃、サブマシンガン、ピストルなど)
  • Battlefield (アサルトライフル、SMG、カービン銃、ピストルなど)

この方法の大きな利点の1つは、パフォーマンスが軽いことです。スペースを介して数百の光線を送信することは、コンピュータが数百の弾丸オブジェクトを送出するよりも計算がはるかに簡単です。

もう1つの利点は、何かを呼び出したときに正確に何かをヒットしたかどうかを即座に知ることができることです。ネットワーキングでは、インターネット上で弾丸の動きを同期する必要はなく、レイキャストがヒットしたかどうかを送信するだけで済むので、これは重要です。

ただし、レイキャスティングにはいくつかの欠点があります。大きな欠点の1つは、直線以外に光線を簡単に投じることができないことです。これは、レイの長さが長くても直線でしか発射できないことを意味します。さまざまな位置に複数の光線を投げることにより、弾丸の動きのような錯覚を作り出すことができますが、これをコードに実装するのが難しいだけでなく、パフォーマンスも重くなります。

もう1つの欠点は、弾丸が見えないことです。弾丸オブジェクトでは、メッシュをアタッチすると弾丸が空間を移動するのを実際に見ることができますが、レイキャストは即座に発生するため、弾丸を表示する適切な方法はありません。レイキャストの原点から、レイキャストが衝突したポイントまで線を引くことができます。これは、レイキャストを表示する一般的な方法の1つです。もう1つの方法は、理論的には弾丸が非常に速く動くため、レイキャストをまったく描画しないことです。


弾丸オブジェクトを設定してみましょう。これは、"Pistol_fire"アニメーションコールバック関数が呼び出されたときにピストルが作成するものです。

Bullet_Scene.tscn を開きます。シーンには、bulletと呼ばれる SpatialCollisionShape が含まれます。

Bullet_script.gd という新しいスクリプトを作成し、それを Bullet Spatial にアタッチします。

ルート(Bullet) でbulletオブジェクト全体を移動します。Area を使用して、何かと衝突したかどうかを確認します

注釈

RigidBody ではなく Area を使用するのはなぜですか? RigidBody を使用しない主な理由は、他の RigidBody ノードと弾丸が相互作用しないようにするためです。Area を使用することにより、他の弾丸含む他の RigidBody ノードが影響を受けないようにします。

もう一つの理由は、単に :ref:`Area <class_Area>`との衝突を検出する方が簡単だからです!

弾丸を制御するスクリプトは次のとおりです:

extends Spatial

var BULLET_SPEED = 70
var BULLET_DAMAGE = 15

const KILL_TIMER = 4
var timer = 0

var hit_something = false

func _ready():
    $Area.connect("body_entered", self, "collided")


func _physics_process(delta):
    var forward_dir = global_transform.basis.z.normalized()
    global_translate(forward_dir * BULLET_SPEED * delta)

    timer += delta
    if timer >= KILL_TIMER:
        queue_free()


func collided(body):
    if hit_something == false:
        if body.has_method("bullet_hit"):
            body.bullet_hit(BULLET_DAMAGE, global_transform)

    hit_something = true
    queue_free()

スクリプトを見てみましょう:


最初に、いくつかのクラス変数を定義します:

  • BULLET_SPEED: 弾丸が移動する速度。
  • BULLET_DAMAGE: 弾丸が衝突するあらゆるものに与えるダメージ。
  • KILL_TIMER: 何もヒットせずに弾丸が持続できる時間。
  • timer: 弾丸が生きている時間を追跡するためのfloat。
  • hit_something: 何かをヒットしたかどうかを追跡するためのブール値。

timerhit_something を除き、これらの変数はすべて、弾丸が世界と相互作用する方法を変更します。

注釈

キルタイマーを使用しているのは、弾丸が永遠に移動するケースがないためです。キルタイマーを使用することで、弾丸が永遠に移動してリソースを消費しないようにすることができます。

ちなみに

パート1 のように、すべて大文字のクラス変数がいくつかあります。この背後にある理由は パート1 で与えられた理由と同じです: これらの変数を定数のように扱いたいが、変更できるようにしたいのです。この場合、後でこれらの弾丸のダメージと速度を変更する必要があるため、定数ではなく変数にする必要があります。


_ready では、ボディがエリアに入ったときに collided 関数を呼び出すように、エリアの body_entered シグナルを自分自身に設定します。


_physics_process は弾丸のローカル Z 軸を取得します。ローカルモードでシーンを見ると、弾丸がローカル Z 軸の正の方向に向かっていることがわかります。

次に、その方向に向けて弾丸全体を変換し、速度とデルタ時間を乗算します。

その後、タイマーにデルタ時間を追加し、タイマーが KILL_TIME 定数以上の値に達したかどうかを確認します。達している場合は、queue_free を使用して弾丸を解放します。


collided では、まだ何かにヒットしているかどうかを確認します。

collided は、ボディが Area ノードに入ったときにのみ呼び出されることに注意してください。弾丸がまだ何かに衝突していない場合、弾丸が衝突した本体に bullet_hit という関数/メソッドがあるかどうかを確認します。もしそうなら、それを呼び出して、弾丸のダーメジと弾丸のグローバル変換を渡し、弾丸の方向と位置を取得します。

注釈

collided では、渡されるbodyは StaticBodyRigidBody、または KinematicBody になります

弾丸の hit_something 変数を true に設定したのは、弾丸が衝突したボディが `` bullet_hit`` 関数/メソッドを持っているかどうかに関係なく、弾丸はすでに何かに当たっているので、もう他の何かに当たらないようにする必要があるからです。

その後、queue_free を使用して弾丸を解放します。

ちなみに

何かにヒットしたらすぐに queue_free を使用して弾丸を解放するのに、なぜ hit_something 変数があるのか疑問に思うかもしれません。

何かをヒットしたかどうかを追跡する必要があるのは、queue_free がすぐにノードを解放しないため、Godotが解放する前に弾丸が別のボディと衝突する可能性があるためです。弾丸が何かに当たったかどうかを追跡することにより、弾丸が1つのオブジェクトにしか当たらないようにすることができます。


プレイヤーのプログラミングを再開する前に、Player.tscn を簡単に見てみましょう。再び Player.tscn を開きます。

Rotation_Helper を展開し、Gun_Fire_PointsGun_Aim_Point の2つのノードがあることに注目してください。

Gun_aim_point は、弾丸が狙うポイントです。画面の中心とどのように並んでおり、Z軸上で前方に一定距離引っ張られていることに注目してください。Gun_aim_point は、弾丸が進むにつれて確実に衝突するポイントとして機能します。

注釈

デバッグ用に非表示のメッシュインスタンスがあります。メッシュは、弾丸がどのターゲットを狙うかを視覚的に示す小さな球体です。

Gun_Fire_Points を開くと、武器ごとにさらに1つずつ、計3つの Spatial ノードがあります。

Rifle_Point を開くと Raycast ノードが見つかります。ここで、ライフルの弾丸のレイキャストを送り出します。レイキャストの長さは、弾丸の移動距離を決定します。

Raycast ノードを使用してライフルの弾丸を処理します。これは、大量の弾丸をすばやく発射するためです。(レイキャストの代わりに)弾丸オブジェクトを使用すると、古いマシンでパフォーマンスの問題が発生する可能性が非常に高くなります。

注釈

ポイントの位置がどこから来たのか疑問に思っているかも知れませんが、それらは各武器の先端の大まかな位置です。これは、AnimationPlayer に移動して、発砲アニメーションの1つを選択し、タイムラインをスクラブすることで確認できます。各武器のポイントは、ほとんどの場合、各武器の先端と一致する必要があります。

Knife_Point を開くと Area ノードが見つかります。私たちはナイフに Area を使用しています。なぜなら、私たちは身近なすべての体だけを気にし、ナイフを空間に発射しないからです。投げナイフを作っている場合は、ナイフのように見える弾丸オブジェクトを生成する可能性があります。

最後に、Pistol_Point があります。これが、弾丸オブジェクトを作成/インスタンス化するポイントです。弾丸は独自の衝突検出をすべて自分で処理するため、ここに追加のノードは必要ありません。

他の武器をどのように扱うか、そして弾丸をどこで生成するかを見てきたので、それらを機能させるための作業を始めましょう。

注釈

必要に応じてHUDノードを確認することもできます。そこに特別なものはなく、単一の Label を使用すること以外、これらのノードには触れません。 GUIノードの使用に関するチュートリアルについては、コントロールノードを使用したインターフェイスの設計 を確認してください。

最初の武器を作成する

ピストルから始めて、各武器のコードを書きましょう。

Pistol_Point (Player -> Rotation_Helper -> Gun_Fire_Points -> Pistol_Point) を選択し、Weapon_Pistol.gd という新しいスクリプトを作成します。

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

extends Spatial

const DAMAGE = 15

const IDLE_ANIM_NAME = "Pistol_idle"
const FIRE_ANIM_NAME = "Pistol_fire"

var is_weapon_enabled = false

var bullet_scene = preload("Bullet_Scene.tscn")

var player_node = null

func _ready():
    pass

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

    clone.global_transform = self.global_transform
    clone.scale = Vector3(4, 4, 4)
    clone.BULLET_DAMAGE = DAMAGE

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Pistol_equip")

    return false

func unequip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        if player_node.animation_manager.current_state != "Pistol_unequip":
            player_node.animation_manager.set_animation("Pistol_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true
    else:
        return false

スクリプトの仕組みを見ていきましょう。


最初に、スクリプトで必要なクラス変数を定義します:

  • DAMAGE: 1つの弾丸が与えるダメージの量。
  • IDLE_ANIM_NAME: ピストルのアイドルアニメーションの名前。
  • FIRE_ANIM_NAME: ピストルの発射アニメーションの名前。
  • is_weapon_enabled: この武器が使用中か有効かを確認するための変数。
  • bullet_scene: 先ほど作業した弾丸のシーン。
  • player_node: Player.gd を保持する変数。

これらの変数のほとんどを定義するのは、Player.gd で使用できるようにするためです。

作成する各武器にはこれらすべての変数が含まれるため("bullet_scene"を除く)、Player.gd でやり取りするための一貫したインターフェイスがあります。 各武器で同じ変数/関数を使用することで、使用している武器を知らなくてもそれらとやり取りできます。これにより、Player.gd の多くのコードを変更することなく武器を追加できるため、コードがよりモジュール化され、上手く動作します。

すべてのコードを Player.gd に書くこともできますが、そこに武器を追加すると Player.gd の管理がますます難しくなります。一貫したインターフェースを備えたモジュール設計を使用することで、Player.gd をきちんとした状態に保つとともに、武器の追加/削除/変更を容易にします。


_ready では、単にそれをパスします。

ただし、注意すべき点が1つあります。ある時点で Player.gd を書き込むという前提です。

Weapon_Pistol.gd の関数のいずれかを呼び出す前に、Player.gd が自身を渡すと前提しています。

これは、プレイヤーが自分自身を渡さないという(問題のある)状況につながる可能性がありますが(忘れているため)、プレイヤーを取得するためにシーンツリーを走査するには、長い文字列の get_parent の呼び出しが必要です。これは見た目が良く無く(get_parent().get_parent().get_parent() など)、Player.gd 内の各武器に自分自身を渡すことを忘れないでおくという考え方は比較的安全です。


次に fire_weapon を見てみましょう:

最初に行うことは、先ほど作成した弾丸シーンのインスタンス化です。

ちなみに

シーンをインスタンス化することにより、インスタンス化したシーン内のすべてのノードを保持する新しいノードを作成し、そのシーンを効果的に複製します。

次に、現在のシーンのルートの最初の子ノードに clone を追加します。これにより、現在ロードされているシーンのルートノードの子になります。

言い換えると、現在ロードされている/開かれているシーンの最初のノード(シーンツリーの一番上にあるもの)の子として clone を追加しています。現在ロードされている/開いているシーンが Testing_Area.tscn の場合、そのシーンのルートノードである Testing_Area の子として clone を追加します。

警告

サウンドの追加に関するセクションで後述するように、この方法は推定を行います。これについては、後ほど パート3 でサウンドを追加するセクションで説明します

Next we set the global transform of the clone to the Pistol_Point's global transform. The reason we do this is so the bullet is spawned at the end of the pistol.

You can see that Pistol_Point is positioned right at the end of the pistol by clicking the AnimationPlayer and scrolling through Pistol_fire. You'll find the position is more or less at the end of the pistol when it fires.

次に、弾丸のシーンはデフォルトでは少し小さすぎるため、4 の係数でスケールアップします。

次に、弾丸のダメージ(BULLET_DAMAGE)を1つのピストルの弾丸が与えるダメージの量(DAMAGE) 設定します。


では、equip_weapon を見てみましょう:

最初に行うことは、アニメーションマネージャーがピストルのアイドルアニメーション中であるかどうかを確認することです。ピストルのアイドルアニメーション中の場合、ピストルが正常に装備されているため、is_weapon_enabledtrue に設定し、true を返します。

ピストルの equip アニメーションは自動的にピストルのアイドルアニメーションに移行することがわかっているため、ピストルのアイドルアニメーション中の場合、ピストルは装備(equip)アニメーションの再生を完了している必要があります。

注釈

これらのアニメーションが遷移することは、Animation_Manager.gd で遷移させるコードを記述したので知っているはずです

次に、プレイヤーが Idle_unarmed アニメーション状態にあるかどうかを確認します。すべての装備解除アニメーションがこの状態になり、この状態から武器を装備できるため、プレイヤーが Idle_unarmed 状態にある場合、アニメーションを Pistol_equip に変更します。

Pistol_equipPistol_idle に移行することがわかっているため、武器を装備するために追加の処理を行う必要はありませんが、まだピストルを装備できていないので、`` false`` を返します。


最後に、 unequip_weapon を見てみましょう:

unequip_weaponequip_weapon に似ていますが、代わりに逆順にチェックしています。

最初に、プレイヤーがアイドルアニメーション状態になっているかどうかを確認します。次に、プレイヤーが Pistol_unequip アニメーションになっていないことを確認します。プレイヤーが Pistol_unequip アニメーションになっていない場合、pistol_unequip アニメーションを再生します。

注釈

プレイヤーがピストルのアイドルアニメーションにあるかどうかを確認し、その直後にプレイヤーが装備を解除していないことを確認する理由に疑問を抱くかもしれません。追加のチェックの背後にある理由は、(まれに) set_animation を処理する前に unequip_weapon を2回呼び出すことができるためです。

次に、プレイヤーが Idle_unarmed にあるかどうかを確認します。これは、Pistol_unequip から遷移するアニメーション状態です。プレイヤーが Idle_unarmed にいる場合、この武器を使用しなくなったため is_weapon_enabledfalse に設定し、ピストルを正常に装備解除したため true を返します。

プレイヤーが Idle_unarmed にない場合、ピストルの装備をまだ解除していないため false を返します。

他の2つの武器を作成する

ピストルに必要なコードがすべて揃ったので、次にライフルとナイフのコードを追加しましょう。

Rifle_Point``(``Player -> Rotation_Helper -> Gun_Fire_Points -> Rifle_Point)を選択し、Weapon_Rifle.gd という新しいスクリプトを作成して、次のように追加します:

extends Spatial

const DAMAGE = 4

const IDLE_ANIM_NAME = "Rifle_idle"
const FIRE_ANIM_NAME = "Rifle_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():
    pass

func fire_weapon():
    var ray = $Ray_Cast
    ray.force_raycast_update()

    if ray.is_colliding():
        var body = ray.get_collider()

        if body == player_node:
            pass
        elif body.has_method("bullet_hit"):
            body.bullet_hit(DAMAGE, ray.global_transform)

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Rifle_equip")

    return false

func unequip_weapon():

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        if player_node.animation_manager.current_state != "Rifle_unequip":
            player_node.animation_manager.set_animation("Rifle_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true

    return false

このほとんどは Weapon_Pistol.gd とまったく同じであるため、変更点のみを見ていきます: fire_weapon

最初に行うことは、Raycast ノードを取得することです。これは Rifle_Point の子です。

次に、force_raycast_update を使用して Raycast を強制的に更新します。これにより Raycast が呼び出されたときに衝突を検出し、フレームパーフェクトな3D物理世界とのコリジョンチェックを取得します。

次に、Raycast が何かと衝突したかどうかを確認します。

Raycast が何かと衝突した場合、最初に衝突したコリジョンbodyを取得します。これは、:ref`StaticBody <class_StaticBody>`、RigidBody、または KinematicBodyです。

次に、私たちは(おそらく)プレイヤーに自分自身の足を撃つ能力を与えたくないので、衝突したボディがプレイヤーではないことを確認したいと思います。

ボディがプレイヤーではない場合、bullet_hit という関数/メソッドがあるかどうかを確認します。もしそうなら、私たちはそれを呼び出して、この弾丸が与えるダメージの量(DAMAGE)、および Raycast のグローバルtransformを渡すので、どの方向から弾丸が来たかを知ることができます。


今、必要があるのはナイフのコードを書くことだけです。

Knife_Point``( ``Player -> Rotation_Helper -> Gun_Fire_Points -> Knife_Point)を選択し、Weapon_Knife.gd という新しいスクリプトを作成し、次のように追加します:

extends Spatial

const DAMAGE = 40

const IDLE_ANIM_NAME = "Knife_idle"
const FIRE_ANIM_NAME = "Knife_fire"

var is_weapon_enabled = false

var player_node = null

func _ready():
    pass

func fire_weapon():
    var area = $Area
    var bodies = area.get_overlapping_bodies()

    for body in bodies:
        if body == player_node:
            continue

        if body.has_method("bullet_hit"):
            body.bullet_hit(DAMAGE, area.global_transform)

func equip_weapon():
    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        is_weapon_enabled = true
        return true

    if player_node.animation_manager.current_state == "Idle_unarmed":
        player_node.animation_manager.set_animation("Knife_equip")

    return false

func unequip_weapon():

    if player_node.animation_manager.current_state == IDLE_ANIM_NAME:
        player_node.animation_manager.set_animation("Knife_unequip")

    if player_node.animation_manager.current_state == "Idle_unarmed":
        is_weapon_enabled = false
        return true

    return false

Weapon_Rifle.gd と同様に、唯一の違いは fire_weapon にあるため、それを見てみましょう:

最初に行うことは、Knife_PointArea 子ノードを取得することです。

次に、get_overlapping_bodies を使用して Area 内のすべてのコリジョンボディを取得します。これは Area に触れるすべてのボディのリストを返します。

次に、これらの各ボディを確認します。

まず、プレイヤーが自分自身を刺すことができないようにするために、体がプレイヤーではないことを確認します。ボディがプレイヤーの場合は、continue を使用するため、ジャンプして bodys の次のボディを確認します。

次のボディにジャンプしていない場合、ボディに bullet_hit 関数/メソッドがあるかどうかを確認します。もしそうなら、私たちはそれを呼び出し、1回のナイフスワイプが与えるダメージの量(DAMAGE)と Area のグローバルtransfomを渡します。

注釈

ナイフが正確に当たった場所の大まかな位置を計算することはできますが、Area の位置を使用するとそれで十分に機能するので、大まかな位置を計算するのに必要な余分な時間をかけてまで、 各ボディが努力する価値はありません。

武器を機能させる

Player.gd で武器を機能させましょう。

まず、武器に必要ないくつかのクラス変数を追加することから始めましょう:

# Place before _ready
var animation_manager

var current_weapon_name = "UNARMED"
var weapons = {"UNARMED":null, "KNIFE":null, "PISTOL":null, "RIFLE":null}
const WEAPON_NUMBER_TO_NAME = {0:"UNARMED", 1:"KNIFE", 2:"PISTOL", 3:"RIFLE"}
const WEAPON_NAME_TO_NUMBER = {"UNARMED":0, "KNIFE":1, "PISTOL":2, "RIFLE":3}
var changing_weapon = false
var changing_weapon_name = "UNARMED"

var health = 100

var UI_status_label

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

  • animation_manager: これは AnimationPlayer ノードと以前に書いたそのスクリプトを保持します。
  • current_weapon_name: 現在使用している武器の名前。可能な値は4つあります: UNARMEDKNIFEPISTOL、および RIFLE
  • weapons: すべての武器ノードを保持するdictionary。
  • WEAPON_NUMBER_TO_NAME: 武器の番号から名前に変換できるdictionary。これを武器の変更に使用します。
  • WEAPON_NAME_TO_NUMBER: 武器の名前からその番号に変換できるdictionary。これを武器の変更に使用します。
  • changing_weapon: 銃/武器を変更しているかどうかを追跡するブール値。
  • changing_weapon_name: 変更したい武器の名前。
  • health: プレイヤーの健康状態。チュートリアルのこの部分では、使用しません。
  • UI_status_label: 私たちがどれだけのhealthを持っているか、そして私たちの銃と予備の両方にどのくらいの弾薬を持っているかを示すラベル。

次に、_ready にいくつかの項目を追加する必要があります。新しい _ready 関数は次のとおりです:

func _ready():
    camera = $Rotation_Helper/Camera
    rotation_helper = $Rotation_Helper

    animation_manager = $Rotation_Helper/Model/Animation_Player
    animation_manager.callback_function = funcref(self, "fire_bullet")

    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

    weapons["KNIFE"] = $Rotation_Helper/Gun_Fire_Points/Knife_Point
    weapons["PISTOL"] = $Rotation_Helper/Gun_Fire_Points/Pistol_Point
    weapons["RIFLE"] = $Rotation_Helper/Gun_Fire_Points/Rifle_Point

    var gun_aim_point_pos = $Rotation_Helper/Gun_Aim_Point.global_transform.origin

    for weapon in weapons:
        var weapon_node = weapons[weapon]
        if weapon_node != null:
            weapon_node.player_node = self
            weapon_node.look_at(gun_aim_point_pos, Vector3(0, 1, 0))
            weapon_node.rotate_object_local(Vector3(0, 1, 0), deg2rad(180))

    current_weapon_name = "UNARMED"
    changing_weapon_name = "UNARMED"

    UI_status_label = $HUD/Panel/Gun_label
    flashlight = $Rotation_Helper/Flashlight

変更点を調べてみましょう。

最初に AnimationPlayer ノードを取得し、それを animation_manager 変数に割り当てます。次に、コールバック関数を FuncRef に設定し、プレイヤーの fire_bullet 関数を呼び出します。現時点では fire_bullet 関数を書いていませんが、すぐにそこへ到着します。

次に、すべての武器ノードを取得し、それらを weapons に割り当てます。これにより、名前(KNIFEPISTOL、または RIFLE)でのみ武器ノードにアクセスできます。

次に Gun_Aim_Point のグローバルな位置を取得し、プレイヤーの武器を回転させて照準を合わせます。

次に、weapons の各武器を通過します。

最初に武器ノードを取得します。武器ノードが null でない場合、その player_node 変数をこのスクリプト(Player.gd)に設定します。次に、look_at 関数を使用して gun_aim_point_pos を調べ、Y 軸上で 180 度回転させます。

注釈

カメラが後方を向いているため、これらすべての武器ポイントを Y 軸上で 180 度回転させます。これらのすべての武器ポイントを 180 度回転させなかった場合、すべての武器が後方に発砲します。

次に、current_weapon_namechanging_weapon_nameUNARMED に設定します。

最後に、HUDからUI Label を取得します。


新しい関数呼び出しを _physics_process に追加して、武器を変更できるようにします。新しいコードは次のとおりです:

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

ここで process_changing_weapons を呼び出します。


それでは、process_input に武器のすべてのプレイヤー入力コードを追加してみましょう。次のコードを追加します:

# ----------------------------------
# Changing weapons.
var weapon_change_number = WEAPON_NAME_TO_NUMBER[current_weapon_name]

if Input.is_key_pressed(KEY_1):
    weapon_change_number = 0
if Input.is_key_pressed(KEY_2):
    weapon_change_number = 1
if Input.is_key_pressed(KEY_3):
    weapon_change_number = 2
if Input.is_key_pressed(KEY_4):
    weapon_change_number = 3

if Input.is_action_just_pressed("shift_weapon_positive"):
    weapon_change_number += 1
if Input.is_action_just_pressed("shift_weapon_negative"):
    weapon_change_number -= 1

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

if changing_weapon == false:
    if WEAPON_NUMBER_TO_NAME[weapon_change_number] != current_weapon_name:
        changing_weapon_name = WEAPON_NUMBER_TO_NAME[weapon_change_number]
        changing_weapon = true
# ----------------------------------

# ----------------------------------
# Firing the weapons
if Input.is_action_pressed("fire"):
    if changing_weapon == false:
        var current_weapon = weapons[current_weapon_name]
        if current_weapon != null:
            if animation_manager.current_state == current_weapon.IDLE_ANIM_NAME:
                animation_manager.set_animation(current_weapon.FIRE_ANIM_NAME)
# ----------------------------------

武器を変更する方法から始めて、追加について見ていきましょう。

まず、現在の武器の番号を取得し、それを weapon_change_number に割り当てます。

次に、数字キー(キー1〜4)のいずれかが押されているかどうかを確認します。もしそうであれば、weapon_change_number をそのキーにマッピングされた値に設定します。

注釈

キー1が 0 にマッピングされる理由は、リストの最初の要素が1ではなくゼロにマッピングされるためです。ほとんどのプログラミング言語のほとんどのリスト/配列の添字(インデックス)は、1 ではなく 0 で始まります。詳細については、https://en.wikipedia.org/wiki/Zero-based_numbering を参照してください。

次に、shift_weapon_positive または shift_weapon_negative が押されているかどうかを確認します。それらの1つがあれば、weapon_change_number から 1 を加算/減算します。

プレイヤーが保有している武器数の外側に weapon_change_number をシフトした可能性があるため、プレイヤーが持つ武器の最大数を超えないようにクランプし、weapon_change_number0 もしくはそれ以上であることを保証します。

次に、プレイヤーが既に武器を変更していないことを確認します。プレイヤーが変更していない場合、プレイヤーが変更したい武器が新しい武器であり、プレイヤーが現在使用している武器ではないかどうかを確認します。プレイヤーが変更したい武器が新しい武器である場合は、weapon_change_numberchanging_weapon_name を武器に設定し、changing_weapontrue に設定します。

武器を発射するには、まず fire アクションが押されているかどうかを確認します。次に、プレイヤーが武器を変更していないことを確認します。次に、現在の武器の武器ノードを取得します。

現在の武器ノードが null ではなく、プレイヤーが IDLE_ANIM_NAME 状態にある場合、プレイヤーのアニメーションを現在の武器の FIRE_ANIM_NAME に設定します。


次に process_changing_weapons を追加しましょう。

次のコードを追加します:

func process_changing_weapons(delta):
    if changing_weapon == true:

        var weapon_unequipped = false
        var current_weapon = weapons[current_weapon_name]

        if current_weapon == null:
            weapon_unequipped = true
        else:
            if current_weapon.is_weapon_enabled == true:
                weapon_unequipped = current_weapon.unequip_weapon()
            else:
                weapon_unequipped = true

        if weapon_unequipped == true:

            var weapon_equipped = false
            var weapon_to_equip = weapons[changing_weapon_name]

            if weapon_to_equip == null:
                weapon_equipped = true
            else:
                if weapon_to_equip.is_weapon_enabled == false:
                    weapon_equipped = weapon_to_equip.equip_weapon()
                else:
                    weapon_equipped = true

            if weapon_equipped == true:
                changing_weapon = false
                current_weapon_name = changing_weapon_name
                changing_weapon_name = ""

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

最初に行うことは、武器を変更するための入力を受け取ったことを確認することです。これを行うには、changing_weaponstrue であることを確認します。

次に、変数(weapon_unequipped)を定義して、現在の武器が正常に装備解除されたかどうかを確認できるようにします。

次に、weapons から現在の武器を取得します。

現在の武器が null でない場合、武器が有効になっているかどうかを確認する必要があります。武器が有効になっている場合、unequip_weapon 関数を呼び出して、武装解除アニメーションを開始します。武器が有効になっていない場合、武器が正常に装備解除されたため、weapon_unequippedtrue に設定します。

現在の武器が null の場合、単純に weapon_unequippedtrue に設定できます。このチェックを行う理由は、UNARMED 用の武器スクリプト/ノードがないためですが、UNARMED 用のアニメーションも存在しないため、プレイヤーが変更したい武器の装備をすぐに開始できるからです。

プレイヤーが現在の武器の装備解除に成功した場合(weapon_unequipped == true)、新しい武器を装備する必要があります。

最初に、プレイヤーが新しい武器を正常に装備したかどうかを追跡するための新しい変数(weapon_equipped)を定義します。

次に、プレイヤーが変更したい武器を取得します。プレイヤーが変更したい武器が null ではない場合、それが有効になっているかどうかを確認します。有効になっていない場合は、equip_weapon 関数を呼び出して、武器の装備を開始します。武器が有効になっている場合、weapon_equippedtrue に設定します。

プレイヤーが変更したい武器が null の場合、wearmon_equippedtrue に設定します。これは、UNARMED のノード/スクリプトがなく、アニメーションもないためです。

最後に、プレイヤーが新しい武器を正常に装備したかどうかを確認します。プレイヤーがそうしている場合、プレイヤーはもはや武器の変更中ではないので、changeing_weaponfalse に設定します。また、現在の武器が変更されたため、current_weapon_namechanging_weapon_name に設定してから、changing_weapon_name を空の文字列に設定します。


ここで、プレイヤーにもう1つの関数を追加する必要があります。これで、プレイヤーは武器を発砲する準備が整いました!

AnimationPlayer の関数トラックで以前に設定したポイントで AnimationPlayer によって呼び出される fire_bullet を追加する必要があります:

func fire_bullet():
    if changing_weapon == true:
        return

    weapons[current_weapon_name].fire_weapon()

この関数が何をするのか見てみましょう:

まず、プレイヤーが武器を変更しているかどうかを確認します。プレイヤーが武器を変更している場合、私たちは撃つことを望まないので return します。

ちなみに

return を呼び出すと、関数の残りの部分が呼び出されなくなります。この場合、残りのコードを実行しないことにのみ関心があり、この関数を呼び出すときに返される変数を探していないため、変数を返しません。

次に、プレイヤーが fire_weapon 関数を呼び出して、現在使用している武器を発射するように指示します。

ちなみに

発砲のアニメーションの速度が他のアニメーションよりも速いと言ったことを覚えていますか?発射アニメーションの速度を変更することにより、武器が弾丸を発射する速度を変更できます!


Before we are ready to test our new weapons, we still have a bit of work to do.

いくつかのテスト対象の作成

スクリプトウィンドウに移動し、[ファイル]をクリックし、[新規スクリプト]を選択して、新しいスクリプトを作成します。このスクリプトに RigidBody_hit_test.gd という名前を付けて、RigidBody が拡張されていることを確認します。

次のコードを追加する必要があります:

extends RigidBody

const BASE_BULLET_BOOST = 9;

func _ready():
    pass

func bullet_hit(damage, bullet_global_trans):
    var direction_vect = bullet_global_trans.basis.z.normalized() * BASE_BULLET_BOOST;

    apply_impulse((bullet_global_trans.origin - global_transform.origin).normalized(), direction_vect * damage)

bullet_hit の仕組みを見ていきましょう:

まず、弾丸の順方向ベクトルを取得します。これは、どの方向から弾丸が RigidBody に当たるかを判断できるようにするためです。これを使用して、RigidBody を弾丸と同じ方向に押し出します。

注釈

方向ベクトルを BASE_BULLET_BOOST でブーストして、弾丸がもう少しパンチを込め、RigidBody ノードを目に見える方法で移動させる必要があります。弾丸が RigidBody と衝突したときの反応をより少なくしたりより多くしたい場合は、単に BASE_BULLET_BOOST を低い値または高い値に設定することができます。

次に、apply_impulse を使用して衝撃を加えます。

最初に、衝撃の位置を計算する必要があります。 apply_impulseRigidBody を基準にしたベクトルを取るため、RigidBody から弾丸までの距離を計算する必要があります。これを行うには、弾丸グローバル原点/位置から RigidBody のグローバル原点/位置を減算します。これにより、RigidBody から弾丸までの距離がわかります。このベクトルを正規化して、コライダーのサイズが、弾丸が RigidBody をどれだけ動かすかに影響しないようにします。

最後に、衝撃の力を計算する必要があります。このために、弾丸が向いている方向を使用し、弾丸のダメージを掛けます。これにより良い結果が得られ、より強力な弾丸の場合、より強力な結果が得られます。


次に、このスクリプトを、影響するすべての RigidBody ノードにアタッチする必要があります。

Testing_Area.tscn を開き、Cubes ノードを親とするすべてのキューブを選択します。

ちなみに

一番上のキューブを選択し、Shift を押しながら最後のキューブを選択すると、Godotはその間のすべてのキューブを選択します!

すべてのキューブを選択したら、"Script"セクションに到達するまでインスペクタを下にスクロールします。ドロップダウンをクリックして、「読み込み」を選択します。新しく作成した RigidBody_hit_test.gd スクリプトを開きます。

最終ノート

../../../_images/PartTwoFinished.png

これは大量のコードでした!しかし、今、すべての作業が完了すると、あなたはあなたの武器をテストすることができます!

これで、キューブで必要な数の弾丸を発射でき、それらが衝突する弾丸に応じて移動します。

パート3 では、武器に弾薬といくつかのサウンドを追加します!

警告

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

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