プログラムでプレイヤーを動かす

いよいよコーディングです。前編で作成した入力アクションを使って、キャラクターを動かしていきます。

新しいスクリプトを追加するために、Playerノードを右クリックし、スクリプトをアタッチ(Attach Script)を選択します。ポップアップで、テンプレート(Template)をEmptyにセットしてから *作成*(Create)ボタンを押してください。

image0

まず、このクラスのプロパティから始めましょう。移動速度(speed)、重力を表す落下加速度(gravity)、そしてキャラクターを移動させるのに使う速度(velocity)を定義していきます。

extends KinematicBody

# How fast the player moves in meters per second.
export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
export var fall_acceleration = 75

var velocity = Vector3.ZERO

これらは移動体に共通するプロパティです。velocityは、速度と方向を組み合わせた3Dベクトルです。ここでは、フレーム間で値を更新して再利用したいので、プロパティとして定義しています。

注釈

距離はメートル単位なので、2D コードとは値がかなり異なります。2D では、1000単位(ピクセル)は画面の幅の半分にしか相当しませんが、3D では 1km になります。

それでは、動きをコーディングしてみましょう。まず、グローバルなInputオブジェクトを使って、_physics_process()で入力方向ベクトルを計算するところから始めます。

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

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

ここでは、_physics_process()仮想関数を使用してすべての計算を行います。_process()と同様に、フレームごとにノードを更新することができますが、運動体や剛体の移動などの物理関連のコードに特化した設計になっています。

参考

_process()_physics_process()の違いについては、 アイドル処理と物理処理 を参照してください。

まず、direction変数を Vector3.ZEROに初期化することからはじめます。次に、プレイヤーがmove_*の入力を一つ以上押しているかどうかを確認し、ベクトルのxz成分を適宜に更新します。これらの成分は地平面の軸に相当します。

この4つの条件は8つの可能性そして8つの可能な方向を与えます。

たとえば、プレイヤーが W と D の両方を同時に押した場合、ベクトルの長さは1.4程度になります。しかし、1 つのキーを押した場合は、1の長さになります。ベクトルの長さが一定であるようにしたいので、そのためにnormalize()メソッドを呼び出します。

#func _physics_process(delta):
    #...

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(translation + direction, Vector3.UP)

ここでは、方向が0より大きい長さを持つ場合、つまりプレイヤーが方向キーを押している場合にのみ、このベクトルを正規化(normalize)します。

この場合、Pivotノードも取得し、そのlook_at()メソッドを呼び出します。このメソッドは、空間上で「向く」(look at)先のグローバル座標と、上にあたる方向を引数に取ります。この場合、Vector3.UPという定数を使うことができます。

注釈

ノードのローカル座標は、 translation のように、親からの相対的な座標です。グローバル座標は、ビューポートで見ることができる、ワールドのメイン軸からの相対座標です。

3D では、ノードの位置を含むプロパティはtranslationとなります。これにdirectionを加えると、Playerは1メートル離れた位置を見ることができます。

次に、速度(velocity)を更新します。地面での速度と落下速度を別々に計算する必要があります。行は字下げを1タブ戻し、_physics_process()関数の中にありつつ、今書いた条件文の外にあるようにしてください。

func _physics_process(delta):
    #...
    if direction != Vector3.ZERO:
        #...

    # Ground velocity
    velocity.x = direction.x * speed
    velocity.z = direction.z * speed
    # Vertical velocity
    velocity.y -= fall_acceleration * delta
    # Moving the character
    velocity = move_and_slide(velocity, Vector3.UP)

垂直方向の速度については、落下加速度に毎フレームのデルタタイムを掛けたものを差し引きます。-=という演算子が使われていることに注目してください。これは variable = variable - ...の省略形になっています。

この行によってキャラクターは全てのフレームで落下します。キャラクターがすでに床の上にいる場合は、これは奇妙に思えるかもしれません。しかし、キャラクターを全てのフレームで地面に衝突させるためには、このようにしなければなりません。

物理エンジンは、移動および衝突が起こった場合にのみ、特定のフレームにおける壁、床、または他の物体との相互作用を検出することができます。 後でこのプロパティを使ってジャンプをコーディングします。

最後の行で、KinematicBody.move_and_slide()を呼び出しています。これは、KinematicBodyクラスの強力なメソッドで、キャラクターをスムーズに移動させることを可能にします。動作の途中で壁にぶつかっても、エンジンがスムーズな動きにしようとします。

この関数は2つのパラメータを受け取ります: 速度と上を指す方向です。関数はキャラクターを移動させ、衝突を適用した後に残った速度を返します。床や壁にぶつかると、その方向への速度が減少またはリセットされます。この場合、関数の戻り値を保存しておくことで、キャラクターが垂直方向の勢いを蓄積し、床を貫通してしまうのを防ぎます。

そして、これが床の上でキャラクターを動かすために必要なコードのすべてです。

参考までに、Player.gdの完全なコードを以下に示します。

extends KinematicBody

# How fast the player moves in meters per second.
export var speed = 14
# The downward acceleration when in the air, in meters per second squared.
export var fall_acceleration = 75

var velocity = Vector3.ZERO


func _physics_process(delta):
    var direction = Vector3.ZERO

    if Input.is_action_pressed("move_right"):
        direction.x += 1
    if Input.is_action_pressed("move_left"):
        direction.x -= 1
    if Input.is_action_pressed("move_back"):
        direction.z += 1
    if Input.is_action_pressed("move_forward"):
        direction.z -= 1

    if direction != Vector3.ZERO:
        direction = direction.normalized()
        $Pivot.look_at(translation + direction, Vector3.UP)

    velocity.x = direction.x * speed
    velocity.z = direction.z * speed
    velocity.y -= fall_acceleration * delta
    velocity = move_and_slide(velocity, Vector3.UP)

プレイヤーの動きをテストする

これから、Mainのシーンにプレーヤーを置いて、テストしてみます。そのためには、プレーヤーをインスタンス化し、カメラを追加する必要があります。2D とは異なり、3D では、ビューポートにカメラがない場合は何も見えません。

Playerのシーンを保存し、Mainのシーンを開いてみてください。エディタ上部のMainタブをクリックすると開けます。

image1

シーンを閉じていた場合は、ファイルシステム(FileSystem)ドックで、Main.tscnをダブルクリックして再び開いてください。

Playerをインスタンス化するには、Mainノードを右クリックして、子シーンをインスタンス化(Instantiate Child Scene)を選択してください。

image2

ポップアップで、Player.tscnをダブルクリックします。キャラクターがビューポートの中央に表示されるはずです。

カメラを追加する

次にカメラを追加しましょう。PlayerPivotで行ったように、基本的なリグを作成することにします。Mainノードをもう一度右クリックし、今度は子ノードを追加(Add Child Node)を選択します。新しい Position3Dを作成し、それを CameraPivot と名づけ、Cameraノードをその子として追加してください。シーン ツリーは、次のようになります。

image3

Cameraを選択しているとき左上に表示される プレビュー(Preview)チェック ボックスに注目してください。これをクリックすると、ゲーム内のカメラの投影をプレビューすることができます。

image4

ここでは、Pivotを使用して、クレーンに乗っているかのようにカメラを回転させることにします。まず、3D ビューを分割して、シーンを自由に移動できるようにし、カメラが見ているものを確認できるようにしましょう。

ビューポートのすぐ上のツールバーで、ビュー(View)、そして2 ビューポート(2 Viewports)をクリックしてください。また、Ctrl + 2 (macOSは Cmd + 2 )を押すことも可能です。

image5

下のビューでCameraを選択し、プレビューのチェックボックスをクリックしてカメラプレビューをオンにします。

\ image6

上のビューで、カメラをZ軸(青い方)に19ユニットほど移動させます。

\ image7

Here's where the magic happens. Select the CameraPivot and rotate it -45 degrees around the X axis (using the red circle). You'll see the camera move as if it was attached to a crane.

\ image8

:kbd:`F6`を押してシーンを実行し、矢印キーを押してキャラクターを移動することができます。

\ image9

透視投影(Perspective)を使用しているため、キャラクターの周囲の何もない空間が見えています。このゲームでは、ゲームプレイエリアをより適切に枠に収め、プレイヤーが距離を読みやすくするために、代わりに平行投影(Orthographic)を使用することにします。

再びCameraを選択し、インスペクター(Inspector)で、ProjectionOrthogonalに、Size19に設定します。これで、キャラクターはより平坦に見え、地面が背景を埋めるようになるはずです。

\ image10

これで、プレイヤーの動きとビューの両方が整いました。次に、モンスターを作成していきます。