パート1

チュートリアルの紹介

../../../_images/FinishedTutorialPicture.png

このチュートリアルシリーズでは、シングルプレーヤーFPSゲームの作成方法を示します。

このチュートリアルシリーズでは、次の方法を説明します:

  • 移動、スプリント、ジャンプできる一人称キャラクターを作成する。
  • アニメーションの遷移を処理するためのシンプルなアニメーションステートマシンを作成する。
  • 一人称キャラクターに3つの武器を追加し、それぞれ異なる方法で弾丸の衝突を処理します:
    • ナイフ (Area を使用)
    • ピストル (Bulletシーン)
  • 一人称キャラクターに2種類の手榴弾を追加します:
    • 通常の手榴弾
    • 粘着性の手榴弾
  • つかんで投げる機能の RigidBody ノードを追加します
  • プレイヤーにジョイパッド入力を追加します
  • 弾薬を消費するすべての武器の弾薬とリロードを追加します。
  • 弾薬と回復アイテムのピックアップを追加します
    • 2つのサイズ: 大と小
  • 自動砲塔を追加します
    • これは、弾丸オブジェクトまたは Raycast を使用して起動できます
  • 十分なダメージを受けたときに壊れるターゲットを追加します
  • 銃が発射されたときに再生されるサウンドを追加します。
  • シンプルなメインメニューを追加します:
    • ゲームの実行方法を変更するためのオプションメニュー
    • レベル選択画面
  • とこからでもアクセスできるユニバーサルポーズメニューを追加します

注釈

このチュートリアルは初心者でも完了できますが、このチュートリアルシリーズを実行する前にGodotやゲーム開発に慣れていない場合は、最初のゲーム を完了することを強くお勧めします。

注意: 3Dゲームを作成することは、2Dゲームを作成するよりもはるかに困難です。 2Dゲームの作成方法がわからない場合は、3Dゲームの作成に苦労する可能性があります。

このチュートリアルでは、Godotエディタの使用経験、GDScriptの基本的なプログラミング経験、ゲーム開発の基本的な経験があることを前提としています。

このチュートリアルの開始アセットは次の場所にあります: Godot_FPS_Starter.zip

提供されるスターターアセットには、アニメーション化された3Dモデル、レベルを作成するための一連の3Dモデル、およびこのチュートリアル用に既に構成されているいくつかのシーンが含まれています。

提供されたすべてのアセット(特に明記しない限り)は、元々TwistedTwiglegによって作成され、Godotコミュニティによる変更/追加が行われました。このチュートリアルで提供されるすべての元のアセットは、MIT ライセンスの下でリリースされます。

これらのアセットは自由に使用できます。すべての元のアセットはGodotコミュニティに属し、他のアセットは以下にリストされているものに属します:

注釈

スカイボックスは、OpenGameArtの **StumpyStrust ** によって作成されます。使用されるスカイボックスは、CC0 の下でライセンスされています。

使用されるフォントは Titillium-Regular であり、SIL Open Font License, Version 1.1 の下でライセンスされています。

ちなみに

各パートの完成したプロジェクトは、各パートのページの下部にあります

パートの概要

このパートでは、環境内を移動できる一人称プレイヤーを作成します。

../../../_images/PartOneFinished.png

このパートの終わりまでに、ゲーム環境内を動き回り、スプリントし、マウスベースの一人称カメラで周囲を見回し、空中に飛び込み、フラッシュライトをオン/オフできる、働く一人称キャラクターができます。

すべてを準備する

Godotを起動し、スターターアセットに含まれるプロジェクトを開きます。

注釈

これらのアセットは、このチュートリアルで提供されるスクリプトを使用するために必ずしも必要ではありませんが、チュートリアルシリーズ全体で使用するセットアップ前のシーンがいくつかあるため、チュートリアルを理解しやすくなります。

まず、プロジェクト設定を開き、[インプットマップ]タブに移動します。いくつかのアクションがすでに定義されています。これらのアクションをプレイヤーに使用します。必要に応じて、これらのアクションにバインドされているキーを自由に変更してください。


スターターアセットにあるものを少し見てみましょう。

スターターアセットには、いくつかのシーンが含まれています。たとえば、res:// には14のシーンがありますが、このチュートリアルシリーズを通してほとんどのシーンにアクセスします。

とりあえず Player.tscn を開きましょう。

注釈

Assets フォルダにはたくさんのシーンといくつかのテクスチャがあります。必要に応じてこれらを見ることができますが、このチュートリアルシリーズでは Assets を探求しません。 `` Assets``には、各レベルで使用されるすべてのモデル、およびいくつかのテクスチャとマテリアルが含まれています。

FPS移動ロジックの作成

Player.tscn を開いたら、どのように設定されているかを簡単に見てみましょう

../../../_images/PlayerSceneTree.png

最初に、プレイヤーのコリジョン形状がどのように設定されているかに注目してください。プレイヤーのコリジョン形状として垂直ポインティングカプセルを使用することは、ほとんどの一人称ゲームでかなり一般的です。

プレイヤーが1つのポイントでバランスを保っているように感じないように、プレイヤーの「足」に小さな正方形を追加しています。

カプセルの底よりわずかに高い「足」が必要なので、わずかに底辺を持ち上げます。 「足」をどこに配置するかは、あなたのレベルとプレイヤーがどのように感じてほしいかによって異なります。

注釈

多くの場合、プレイヤーは端まで歩いて滑り落ちると、衝突形状が円形であることに気付くでしょう。カプセルの下部に小さな正方形を追加して、エッジ上およびその周辺での滑落を減らしています。

もう1つ注意すべき点は、Rotation_Helper の子であるノードの数です。これは、Rotation_Helper には、X 軸上で(上下に)回転させたいすべてのノードが含まれているためです。この理由は、PlayerY 軸上で、Rotation_helperX 軸上で回転できるようにするためです。

注釈

Rotation_helper を使用していなかった場合、X 軸と Y 軸の両方で同時に回転する場合があり、場合によっては3つの軸すべてでさらに回転状態に縮退する可能性があります。

詳細については、using transforms を参照してください


新しいスクリプトを Player ノードにアタッチし、それを Player.gd と呼びます。

動き回ったり、マウスで見回したり、ジャンプしたりする機能を追加して、プレイヤーをプログラムしましょう。次のコードを Player.gd に追加します:

extends KinematicBody

const GRAVITY = -24.8
var vel = Vector3()
const MAX_SPEED = 20
const JUMP_SPEED = 18
const ACCEL = 4.5

var dir = Vector3()

const DEACCEL= 16
const MAX_SLOPE_ANGLE = 40

var camera
var rotation_helper

var MOUSE_SENSITIVITY = 0.05

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

    Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)

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

func process_input(delta):

    # ----------------------------------
    # Walking
    dir = Vector3()
    var cam_xform = camera.get_global_transform()

    var input_movement_vector = Vector2()

    if Input.is_action_pressed("movement_forward"):
        input_movement_vector.y += 1
    if Input.is_action_pressed("movement_backward"):
        input_movement_vector.y -= 1
    if Input.is_action_pressed("movement_left"):
        input_movement_vector.x -= 1
    if Input.is_action_pressed("movement_right"):
        input_movement_vector.x += 1

    input_movement_vector = input_movement_vector.normalized()

    # Basis vectors are already normalized.
    dir += -cam_xform.basis.z * input_movement_vector.y
    dir += cam_xform.basis.x * input_movement_vector.x
    # ----------------------------------

    # ----------------------------------
    # Jumping
    if is_on_floor():
        if Input.is_action_just_pressed("movement_jump"):
            vel.y = JUMP_SPEED
    # ----------------------------------

    # ----------------------------------
    # Capturing/Freeing the cursor
    if Input.is_action_just_pressed("ui_cancel"):
        if Input.get_mouse_mode() == Input.MOUSE_MODE_VISIBLE:
            Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
        else:
            Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
    # ----------------------------------

func process_movement(delta):
    dir.y = 0
    dir = dir.normalized()

    vel.y += delta * GRAVITY

    var hvel = vel
    hvel.y = 0

    var target = dir
    target *= MAX_SPEED

    var accel
    if dir.dot(hvel) > 0:
        accel = ACCEL
    else:
        accel = DEACCEL

    hvel = hvel.linear_interpolate(target, accel * delta)
    vel.x = hvel.x
    vel.z = hvel.z
    vel = move_and_slide(vel, Vector3(0, 1, 0), 0.05, 4, deg2rad(MAX_SLOPE_ANGLE))

func _input(event):
    if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
        rotation_helper.rotate_x(deg2rad(event.relative.y * MOUSE_SENSITIVITY))
        self.rotate_y(deg2rad(event.relative.x * MOUSE_SENSITIVITY * -1))

        var camera_rot = rotation_helper.rotation_degrees
        camera_rot.x = clamp(camera_rot.x, -70, 70)
        rotation_helper.rotation_degrees = camera_rot
using Godot;
using System;

public class Player : KinematicBody
{
    [Export]
    public float Gravity = -24.8f;
    [Export]
    public float MaxSpeed = 20.0f;
    [Export]
    public float JumpSpeed = 18.0f;
    [Export]
    public float Accel = 4.5f;
    [Export]
    public float Deaccel = 16.0f;
    [Export]
    public float MaxSlopeAngle = 40.0f;
    [Export]
    public float MouseSensitivity = 0.05f;

    private Vector3 _vel = new Vector3();
    private Vector3 _dir = new Vector3();

    private Camera _camera;
    private Spatial _rotationHelper;

    // Called when the node enters the scene tree for the first time.
    public override void _Ready()
    {
        _camera = GetNode<Camera>("Rotation_Helper/Camera");
        _rotationHelper = GetNode<Spatial>("Rotation_Helper");

        Input.SetMouseMode(Input.MouseMode.Captured);
    }

    public override void _PhysicsProcess(float delta)
    {
        ProcessInput(delta);
        ProcessMovement(delta);
    }

    private void ProcessInput(float delta)
    {
        //  -------------------------------------------------------------------
        //  Walking
        _dir = new Vector3();
        Transform camXform = _camera.GlobalTransform;

        Vector2 inputMovementVector = new Vector2();

        if (Input.IsActionPressed("movement_forward"))
            inputMovementVector.y += 1;
        if (Input.IsActionPressed("movement_backward"))
            inputMovementVector.y -= 1;
        if (Input.IsActionPressed("movement_left"))
            inputMovementVector.x -= 1;
        if (Input.IsActionPressed("movement_right"))
            inputMovementVector.x += 1;

        inputMovementVector = inputMovementVector.Normalized();

        // Basis vectors are already normalized.
        _dir += -camXform.basis.z * inputMovementVector.y;
        _dir += camXform.basis.x * inputMovementVector.x;
        //  -------------------------------------------------------------------

        //  -------------------------------------------------------------------
        //  Jumping
        if (IsOnFloor())
        {
            if (Input.IsActionJustPressed("movement_jump"))
                _vel.y = JumpSpeed;
        }
        //  -------------------------------------------------------------------

        //  -------------------------------------------------------------------
        //  Capturing/Freeing the cursor
        if (Input.IsActionJustPressed("ui_cancel"))
        {
            if (Input.GetMouseMode() == Input.MouseMode.Visible)
                Input.SetMouseMode(Input.MouseMode.Captured);
            else
                Input.SetMouseMode(Input.MouseMode.Visible);
        }
        //  -------------------------------------------------------------------
    }

    private void ProcessMovement(float delta)
    {
        _dir.y = 0;
        _dir = _dir.Normalized();

        _vel.y += delta * Gravity;

        Vector3 hvel = _vel;
        hvel.y = 0;

        Vector3 target = _dir;

        target *= MaxSpeed;

        float accel;
        if (_dir.Dot(hvel) > 0)
            accel = Accel;
        else
            accel = Deaccel;

        hvel = hvel.LinearInterpolate(target, accel * delta);
        _vel.x = hvel.x;
        _vel.z = hvel.z;
        _vel = MoveAndSlide(_vel, new Vector3(0, 1, 0), false, 4, Mathf.Deg2Rad(MaxSlopeAngle));
    }

    public override void _Input(InputEvent @event)
    {
        if (@event is InputEventMouseMotion && Input.GetMouseMode() == Input.MouseMode.Captured)
        {
            InputEventMouseMotion mouseEvent = @event as InputEventMouseMotion;
            _rotationHelper.RotateX(Mathf.Deg2Rad(mouseEvent.Relative.y * MouseSensitivity));
            RotateY(Mathf.Deg2Rad(-mouseEvent.Relative.x * MouseSensitivity));

            Vector3 cameraRot = _rotationHelper.RotationDegrees;
            cameraRot.x = Mathf.Clamp(cameraRot.x, -70, 70);
            _rotationHelper.RotationDegrees = cameraRot;
        }
    }
}

これはかなり多いコードなので、関数によって機能を分解しましょう:

ちなみに

コードを手動で入力することで多くのことを学ぶことができるので、コードをコピーして貼り付けることはお勧めできませんが、このページから直接コードをコピーしてスクリプトエディタに貼り付けることができます。

If you do this, all the code copied will be using spaces instead of tabs.

スクリプトエディタでスペースをタブに変換するには、[編集]メニューをクリックし、[インデントをタブに変換]を選択します。これにより、すべてのスペースがタブに変換されます。[インデントをスペースに変換]を選択して、タブをスペースに戻すこともできます。


まず、クラス変数を定義して、プレイヤーが世界をどのように移動するかを指定します。

注釈

このチュートリアル全体を通して、関数の外部で定義された変数は「クラス変数」 と呼ばれます。これは、スクリプト内の任意の場所からこれらの変数にアクセスできるためです。

各クラス変数を見てみましょう:

  • GRAVITY: 重力がどれほど私たちを引き下げるか。
  • vel: KinematicBody の速度。
  • MAX_SPEED: 到達可能な最高速度。この速度に達すると、それ以上速くなりません。
  • JUMP_SPEED: ジャンプできる高さ。
  • ACCEL: どれだけ速く加速するか。値が高いほど、最高速度に早く到達します。
  • DEACCEL: どれだけ早く減速するか。値が高いほど、すぐに完全に停止します。
  • MAX_SLOPE_ANGLE: KinematicBody が「床」と見なす最も急な角度。
  • camera: Camera ノード。
  • rotation_helper: X軸上で(上下)回転させたいすべてを保持する Spatial ノード。
  • MOUSE_SENSITIVITY: マウスの感度。私のマウスでは 0.05 という値がうまく機能しますが、マウスの感度に応じて値を変更する必要があるかもしれません。

これらの変数の多くを微調整して、異なる結果を得ることができます。たとえば、GRAVITY を低くしたり、JUMP_SPEED を大きくしたりすることで、より「浮遊感のある」キャラクターを得ることができます。気軽に実験してください!

注釈

MOUSE_SENSITIVITY は他の定数と同様にすべての大文字で書かれていることに気づいたかもしれませんが、`` MOUSE_SENSITIVITY`` は定数ではありません。

この背後にある理由は、スクリプト全体で定数変数(変更できない変数)のように扱いたいが、後でカスタマイズ可能な設定を追加するときに値を変更できるようにすることです。なので、定数のように扱うことを思い出させるために、それはすべての大文字で命名されています。


_ready 関数を見てみましょう:

最初に camerarotation_helper ノードを取得し、それらを変数に保存します。

次に、マウスモードをキャプチャに設定して、マウスがゲームウィンドウから離れないようにする必要があります。

これにより、マウスが非表示になり、画面の中央に保持されます。これには2つの理由があります。1つ目の理由は、プレイヤーがプレイ中にマウスカーソルを見ないようにすることです。

2番目の理由は、カーソルをゲームウィンドウから離れたくないためです。カーソルがゲームウィンドウから離れると、プレイヤーがウィンドウの外側をクリックすると、ゲームのフォーカスが失われる場合があります。これらの問題が発生しないように、マウスカーソルをキャプチャします。

注釈

さまざまなマウスモードについては、入力ドキュメント を参照してください。このチュートリアルシリーズでは、MOUSE_MODE_CAPTUREDMOUSE_MODE_VISIBLE のみを使用します。


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

_physics_process で行っていることは、2つの関数 process_inputprocess_movement を呼び出すことだけです。

process_input は、プレイヤーの入力に関連するすべてのコードを保存する場所です。他の何より先に呼び出したいので、新鮮なプレイヤー入力を取扱います。

process_movement は、ゲームワールド内を移動できるように KinematicBody に必要なすべてのデータを送信する場所です。


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

最初に、空の Vector3 にdirを設定します。

dir は、プレイヤーが移動しようとする方向を格納するために使用されます。プレイヤーの以前の入力が単一の process_movement 呼び出しを超えてプレイヤーに影響を与えないようにするため、dir をリセットします。

次に、カメラのグローバルtransformを取得し、それを cam_xform 変数に保存します。

カメラのグローバルtransformが必要な理由は、方向ベクトルを使用できるようにするためです。多くの人が方向ベクトルを紛らわしく感じているので、それらがどのように機能するかを少し説明しましょう:


ワールド空間は、次のように定義できます。すべてのオブジェクトが配置される空間。一定の原点を基準にします。 2Dであろうと3Dであろうと、すべてのオブジェクトはワールド空間で位置を持っています。

別の言い方をすれば、ワールド空間とは、原点と呼ばれる単一の既知の固定点によってすべてのオブジェクトの位置、回転、スケールを測定できる宇宙空間です。

ゴドーでは、原点は (0, 0, 0) の位置で、回転は (0, 0, 0)、スケールは (1, 1, 1) です。

注釈

Godotエディタを開いて Spatial ベースのノードを選択すると、ギズモがポップアップします。各矢印は、デフォルトでワールド空間の方向を使用してポイントします。

ワールド空間の方向ベクトルを使用して移動する場合は、次のようにします:

if Input.is_action_pressed("movement_forward"):
    node.translate(Vector3(0, 0, 1))
if Input.is_action_pressed("movement_backward"):
    node.translate(Vector3(0, 0, -1))
if Input.is_action_pressed("movement_left"):
    node.translate(Vector3(1, 0, 0))
if Input.is_action_pressed("movement_right"):
    node.translate(Vector3(-1, 0, 0))
if (Input.IsActionPressed("movement_forward"))
    node.Translate(new Vector3(0, 0, 1));
if (Input.IsActionPressed("movement_backward"))
    node.Translate(new Vector3(0, 0, -1));
if (Input.IsActionPressed("movement_left"))
    node.Translate(new Vector3(1, 0, 0));
if (Input.IsActionPressed("movement_right"))
    node.Translate(new Vector3(-1, 0, 0));

注釈

ワールド空間の方向ベクトルを取得するために計算を行う必要がないことに注意してください。いくつかの Vector3 変数を定義し、各方向を指す値を入力できます。

以下は、2Dでワールド空間がどのように見えるかです:

注釈

次の画像は単なる例です。各矢印/長方形は方向ベクトルを表します

../../../_images/WorldSpaceExample.png

そして、3Dの場合は次のようになります:

../../../_images/WorldSpaceExample_3D.png

両方の例で、ノードの回転が方向矢印を変更しないことに注意してください。これは、ワールド空間が一定だからです。オブジェクトをどのように平行移動、回転、または拡大縮小しても、ワールド空間は 常に同じ方向 を指します。

ローカル空間は、オブジェクトの回転を考慮に入れるため、異なります。

ローカル空間は次のように定義できます。オブジェクトの位置が宇宙の原点である空間。原点の位置は多くの場所で N である可能性があるため、ローカル空間から派生した値は原点の位置によって変化します。

注釈

This question from Game Development Stack Exchange has a much better explanation of world space and local space.

https://gamedev.stackexchange.com/questions/65783/what-are-world-space-and-eye-space-in-game-development (このコンテキストではLocal spaceとEye spaceは本質的に同じものです)

Spatial ノードのローカル空間を取得するには、その Transform を取得する必要があるため、Transform から Basis を取得できます。

Basis には、XYZ の3つのベクトルがあります。これらの各ベクトルは、そのオブジェクトからの各ローカル空間ベクトルを指します。

Spatial ノードのローカル方向ベクトルを使用するには、次のコードを使用します:

if Input.is_action_pressed("movement_forward"):
    node.translate(node.global_transform.basis.z.normalized())
if Input.is_action_pressed("movement_backward"):
    node.translate(-node.global_transform.basis.z.normalized())
if Input.is_action_pressed("movement_left"):
    node.translate(node.global_transform.basis.x.normalized())
if Input.is_action_pressed("movement_right"):
    node.translate(-node.global_transform.basis.x.normalized())
if (Input.IsActionPressed("movement_forward"))
    node.Translate(node.GlobalTransform.basis.z.Normalized());
if (Input.IsActionPressed("movement_backward"))
    node.Translate(-node.GlobalTransform.basis.z.Normalized());
if (Input.IsActionPressed("movement_left"))
    node.Translate(node.GlobalTransform.basis.x.Normalized());
if (Input.IsActionPressed("movement_right"))
    node.Translate(-node.GlobalTransform.basis.x.Normalized());

2Dでローカル空間がどのように見えるかを次に示します:

../../../_images/LocalSpaceExample.png

そして、3Dの場合は次のようになります:

../../../_images/LocalSpaceExample_3D.png

ローカル空間モードを使用しているときに Spatial ギズモが表示するものを次に示します。左側のオブジェクトの回転に矢印がどのように追従するかに注目してください。これは、ローカル空間の3Dの例とまったく同じに見えます。

注釈

Spatial ベースのノードが選択されているときに T または小さなキューブボタンを押すと、ローカル空間モードとワールド空間モードを切り替えることができます。

../../../_images/LocalSpaceExampleGizmo.png

ローカルベクトルは、経験豊富なゲーム開発者にとっても混乱を招くので、すべてがあまり意味をなさない場合でも心配しないでください。ローカルベクトルについて覚えておくべき重要なことは、世界の視点から方向を与えるワールドベクトルを使用するのではなく、ローカル座標を使用してオブジェクトの視点から方向を取得することです。


さて、process_input に戻ります:

次に、input_movement_vector という新しい変数を作成し、それを空の Vector2 に割り当てます。これを使用して、仮想軸を作成し、プレイヤーの入力を動きにマッピングします。

注釈

これはキーボードだけでは過剰に思えるかもしれませんが、後でジョイパッド入力を追加するときに意味があります。

どの方向移動アクションが押されたかに基づいて、input_movement_vector に加算または減算します。

各方向の移動アクションをチェックした後、input_movement_vector を正規化します。これにより、input_movement_vector の値が `` 1`` 半径単位の円内に収まります。

次に、カメラのローカル Z ベクトルと input_movement_vector.ydir に追加します。これは、プレイヤーが前方または後方に押すと、カメラのローカル Z 軸を追加して、プレイヤーがカメラに対して前方または後方に移動するようにするためです。

注釈

カメラは -180 度回転しているため、Z 方向ベクトルを反転する必要があります。通常、前方は正のZ軸になるため、basis.z.normalized() を使用すると機能しますが、カメラのZ軸はプレイヤーの他の部分に対して後方に向いているため -basis.z.normalized() を使用しています。

カメラのローカル X ベクトルにも同じことを行い、input_movement_vector.y を使用する代わりに input_movement_vector.x を使用します。これにより、プレイヤーが左/右を押したときにプレイヤーがカメラに対して左/右に移動します。

次に、KinematicBodyis_on_floor 関数を使用して、プレイヤーが床にいるかどうかを確認します。そうである場合、"movement_jump"アクションが押されかどうかを確認します。もしそうなら、プレイヤーの Y 速度を JUMP_SPEED に設定します。

Yの速度を設定しているため、プレイヤーは空中にジャンプします。

次に、ui_cancel アクションを確認します。これは、escape ボタンが押されたときにマウスカーソルを解放/キャプチャできるようにするためです。そうしないと、カーソルを解放する方法がないため、ランタイムを終了するまでカーソルが動かなくなるからです。

カーソルを解放/キャプチャするには、マウスが表示されている(解放されている)かどうかを確認します。存在する場合はキャプチャし、存在しない場合は表示します(解放します)。

process_input のために今やっていることはそれだけです。プレイヤーにさらに複雑さを追加するため、後でこの関数に何度も戻ってきます。


それでは process_movement を見てみましょう:

まず、Y 値をゼロに設定することにより、dirY 軸上で移動しないようにします。

次に、dir を正規化して、1 半径単位の円内にいることを確認します。これにより、プレイヤーが直線で移動しているか斜めに移動しているかに関係なく、一定の速度で移動しています。正規化しなかった場合、プレイヤーは、まっすぐに行くときよりも斜めのときに速く動きます。

次に、プレイヤーの Y 速度に GRAVITY * delta を加算して、プレイヤーに重力を加えます。

その後、プレイヤーの速度を新しい変数(hvel と呼ばれる)に割り当て、Y 軸の動きを削除します。

次に、プレイヤーの方向ベクトルに新しい変数(target)を設定します。次に、これにプレイヤーの最大速度を掛けて、dir で指定された方向にプレイヤーがどれだけ移動するかを確認します。

その後、accel という名前の加速用の新しい変数を作成します。

次に、hvel のドット積を取り、プレイヤーが hvel に従って移動しているかどうかを確認します。hvel には Y の速度がないため、プレイヤーが前方、後方、左、または右に移動しているかどうかのみを確認します。

プレイヤーが hvel に従って移動している場合、プレイヤーが加速するように accelACCEL 定数に設定し、そうでない場合は accelDEACCEL 定数に設定して、プレイヤーは減速します。

次に、水平速度を補間し、プレイヤーの X および Z 速度を補間された水平速度に設定し、move_and_slide を呼び出して KinematicBody が物理世界(physics world)を通じてプレイヤーを動かします。

ちなみに

All the code in process_movement is exactly the same as the movement code from the Kinematic Character demo!


最後の関数は _input 関数で、ありがたいことにかなり短いです:

まず、処理するイベントが InputEventMouseMotion イベントであることを確認します。また、カーソルがキャプチャされているかどうかも確認します。キャプチャされていない場合は、回転しないようにします。

注釈

可能な入力イベントのリストについては、マウスと入力座標 を参照してください。

イベントが実際にマウスモーションイベントであり、カーソルがキャプチャされる場合、InputEventMouseMotion によって提供される相対的なマウスモーションに基づいて回転します。

まず、InputEventMouseMotion によって提供される相対的なマウスモーションの Y 値を使用して、X 軸上の rotation_helper ノードを回転させます。

次に、KinematicBody 全体を、相対的なマウスの動きの X 値によって Y 軸上で回転させます。

ちなみに

Godotは相対的なマウスの動きを Vector2 に変換します。マウスの動きはそれぞれ 1-1 です。右と左の動きはそれぞれ 1-1 です。

プレイヤーの回転用に、相対マウスモーションの X 値に -1 を掛けた値を使用するので、マウスモーションが左右に移動すると、プレイヤーが同じ方向に左右に回転します。

最後に、プレイヤーが上下逆さまに回転できないように、rotation_helperX 回転を -70 から 70 度の間に制限します。

ちなみに

回転変換の詳細については、using transforms を参照してください。


コードをテストするには、Testing_Area.tscn という名前のシーンを開きます(まだ開いていない場合)。次のいくつかのチュートリアルのパートを進めるときにこのシーンを使用するので、必ずシーンタブの1つで開いたままにしてください。

先に進み、Testing_Area.tscn を開いているタブで F6 を押すか、右上隅の再生ボタンを押す、もしくは F5 を押してコードをテストします。これで、マウスを使用して、歩き回ったり、空中をジャンプしたり、見回したりできるようになります。

プレイヤーに懐中電灯とスプリントのオプションを与える

武器を機能させる前に、追加すべきことがいくつかあります。

多くのFPSゲームには、スプリント(ダッシュ)と懐中電灯のオプションがあります。これらをプレイヤーに簡単に追加できるので、そうしましょう!

まず、プレイヤースクリプトにさらにいくつかのクラス変数が必要です:

const MAX_SPRINT_SPEED = 30
const SPRINT_ACCEL = 18
var is_sprinting = false

var flashlight
[Export]
public float MaxSprintSpeed = 30.0f;
[Export]
public float SprintAccel = 18.0f;
private bool _isSprinting = false;

private SpotLight _flashlight;

すべてのスプリント変数は、類似した名前の非スプリント変数とまったく同じように機能します。

is_sprinting は、プレイヤーが現在スプリントしているかどうかを追跡するブール値であり、flashlight はプレイヤーの懐中電灯用ノードを保持するために使用する変数です。

ここで、_ ready から始まる数行のコードを追加する必要があります。以下を _ready に追加します:

flashlight = $Rotation_Helper/Flashlight
_flashlight = GetNode<SpotLight>("Rotation_Helper/Flashlight");

これは Flashlight ノードを取得し、それを flashlight 変数に割り当てます。


ここで、process_input のコードの一部を変更する必要があります。process_input のどこかに以下を追加します:

# ----------------------------------
# Sprinting
if Input.is_action_pressed("movement_sprint"):
    is_sprinting = true
else:
    is_sprinting = false
# ----------------------------------

# ----------------------------------
# Turning the flashlight on/off
if Input.is_action_just_pressed("flashlight"):
    if flashlight.is_visible_in_tree():
        flashlight.hide()
    else:
        flashlight.show()
# ----------------------------------
//  -------------------------------------------------------------------
//  Sprinting
if (Input.IsActionPressed("movement_sprint"))
    _isSprinting = true;
else
    _isSprinting = false;
//  -------------------------------------------------------------------

//  -------------------------------------------------------------------
//  Turning the flashlight on/off
if (Input.IsActionJustPressed("flashlight"))
{
    if (_flashlight.IsVisibleInTree())
        _flashlight.Hide();
    else
        _flashlight.Show();
}

追加分について見ていきましょう:

プレイヤーが movement_sprint アクションを押しているときに is_sprintingtrue に設定し、movement_sprint アクションがリリースされたときに false を設定します。process_movement では、スプリント時にプレイヤーを高速化するコードを追加します。ここで process_inputis_sprinting 変数を変更します。

懐中電灯を処理するためにカーソルを解放/キャプチャするのと同じようなことをします。まず、flashlight アクションが押されたかどうかを確認します。もしそうなら、flashlight シーンツリーに表示されているかどうかを確認します。存在する場合は非表示にし、存在しない場合は表示します。


ここで process_movement のいくつかのことを変更する必要があります。まず、target * = MAX_SPEED を次のものに置き換えます:

if is_sprinting:
    target *= MAX_SPRINT_SPEED
else:
    target *= MAX_SPEED
if (_isSprinting)
    target *= MaxSprintSpeed;
else
    target *= MaxSpeed;

常に targetMAX_SPEED を掛ける代わりに、まずプレイヤーが全力疾走しているかどうかを確認します。プレイヤーが全力疾走している場合、代わりに targetMAX_SPRINT_SPEED を掛けます。

あとは、全力疾走時の加速度を変更するだけです。accel = ACCEL を次のように変更します:

if is_sprinting:
    accel = SPRINT_ACCEL
else:
    accel = ACCEL
if (_isSprinting)
    accel = SprintAccel;
else
    accel = Accel;

これで、プレイヤーが全力疾走しているときに、ACCEL の代わりに SPRINT_ACCEL を使用して、プレイヤーをより高速に加速させます。


Shift を押すとスプリントできるようになり、F を押すと懐中電灯のオンとオフを切り替えることができます!

試してみてください!スプリントに関連するクラス変数を変更して、スプリントの際にプレイヤーを速くしたり遅くしたりできます!

最終ノート

../../../_images/PartOneFinished.png

ふう!これは大変な仕事でした。これで、完全に機能する一人称キャラクターができました!

パート2 では、プレイヤーキャラクターにいくつかの銃を追加します。

注釈

この時点で、スプリントと懐中電灯を使用して、一人称視点からキネマティックキャラクターのデモを再現できましたしました!

ちなみに

現在、プレイヤースクリプトは、あらゆる種類の一人称ゲームを作成するのに理想的な状態になっています。例: ホラーゲーム、プラットフォーマーゲーム、アドベンチャーゲームなど!

警告

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

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