ささみ雑記帳

少し長くなりそうな色々を書き留めておくためのノート。

Unityで3D空間内をTPSっぽくWASD移動するキャラをつくる

UnityでTPSっぽいゲームの原型をつくるスクリプトの話です。
操作プレイヤーとカメラの連動とか、1から組もうとすると私自身つまずくことがあるのでメモ。

(準備)とりあえずプロジェクトをつくる

  1. f:id:sasanon:20170917025157p:plain

できました。

(準備)キャラをつくる

どんなキャラでもよいのですが、最低限、向いている方向がわからないと困るので、
適当にキャラっぽい何かをつくります。

まず空のゲームオブジェクトPlayer(Position(0,0,0), Scale(1,1,1))をつくり、
その子オブジェクトとして、以下の4つのCubeをつくります。

名前 PositionX PositionY PositionZ ScaleX ScaleY ScaleZ
Body 0 0 0 1 1 1
RightEye -0.25 0.25 -0.5 0.1 0.1 0.1
LeftEye 0.25 0.25 -0.5 0.1 0.1 0.1
Mouth 0 -0.25 -0.5 0.5 0.1 0.1

そして、Projectウィンドウでマテリアルをつくり、
名前をFaceとして、Bodyと区別の付く色に変更。
RightEye, LeftEye, Mouthに適用します。

f:id:sasanon:20170917041302p:plain

こんな感じ。
…意外とかわいいかも。
以降は一塊のPlayerオブジェクトとして扱うので、Hierarchy上で畳んでおきます。

(準備)地面をつくる

Plane(Position(0,0,0), Scale(1,1,1))を生成します。
地面用のマテリアルをつくり、名前をGroundとして、Planeに適用します。
キャラが地面に埋まる状態となるので、PlayerのY座標を0.5にします。

f:id:sasanon:20170917041313p:plain

こんな感じ。
ここからスクリプトを追加して書いていきます。

プレイヤーをWASD移動させる

Player.csスクリプトを追加します。
WASD入力から移動方向ベクトル(velocity)をつくり、
それを足してtransform.position値を変化させる処理を書きます。

ポイントは移動方向ベクトルを一旦つくること。
これは拡張する際に色々使います。

// Player.cs
using UnityEngine;

// プレイヤー
public class Player : MonoBehaviour {

    [SerializeField] private Vector3 velocity;              // 移動方向
    [SerializeField] private float moveSpeed = 5.0f;        // 移動速度

    void Update () {
        // WASD入力から、XZ平面(水平な地面)を移動する方向(velocity)を得ます
        velocity = Vector3.zero;
        if(Input.GetKey(KeyCode.W))
            velocity.z += 1;
        if(Input.GetKey(KeyCode.A))
            velocity.x -= 1;
        if(Input.GetKey(KeyCode.S))
            velocity.z -= 1;
        if(Input.GetKey(KeyCode.D))
            velocity.x += 1;

        // 速度ベクトルの長さを1秒でmoveSpeedだけ進むように調整します
        velocity = velocity.normalized * moveSpeed * Time.deltaTime;

        // いずれかの方向に移動している場合
        if(velocity.magnitude > 0)
        {
            // プレイヤーの位置(transform.position)の更新
            // 移動方向ベクトル(velocity)を足し込みます
            transform.position += velocity;
        }
    }
}

できたらPlayerオブジェクトにアタッチして、実行してみる。

f:id:sasanon:20170917033122g:plain

こんな感じ。
こいつ…動くぞ…!

カメラをプレイヤーに追従させる

プレイヤーが動いたので、それに追従させてカメラを動かします。
PlayerFollowCamera.csスクリプトを追加します。
見下ろす回転と水平方向の回転を分けておきます。

// PlayerFollowCamera.cs
using UnityEngine;

// プレイヤー追従カメラ
public class PlayerFollowCamera : MonoBehaviour {
    [SerializeField] private Transform player;          // 注視対象プレイヤー

    [SerializeField] private float distance = 15.0f;    // 注視対象プレイヤーからカメラを離す距離
    [SerializeField] private Quaternion vRotation;      // カメラの垂直回転(見下ろし回転)
    [SerializeField] public  Quaternion hRotation;      // カメラの水平回転

    void Start ()
    {
        // 回転の初期化
        vRotation = Quaternion.Euler(30, 0, 0);         // 垂直回転(X軸を軸とする回転)は、30度見下ろす回転
        hRotation = Quaternion.identity;                // 水平回転(Y軸を軸とする回転)は、無回転
        transform.rotation = hRotation * vRotation;     // 最終的なカメラの回転は、垂直回転してから水平回転する合成回転

        // 位置の初期化
        // player位置から距離distanceだけ手前に引いた位置を設定します
        transform.position = player.position - transform.rotation * Vector3.forward * distance; 
    }

    void LateUpdate ()
    {
        // カメラの位置(transform.position)の更新
        // player位置から距離distanceだけ手前に引いた位置を設定します
        transform.position = player.position - transform.rotation * Vector3.forward * distance; 
    }
}

できたらMain Cameraオブジェクトにアタッチして、
Playerオブジェクトを参照させて、実行してみる。

f:id:sasanon:20170917034741g:plain

こんな感じ。
カメラを完全追従させているので、キャラの動きというよりは地面の存在によって動きがわかります。

カメラをマウスで回るようにする

PlayerFollowCamera.csスクリプトに回転速度のパラメータと、マウス入力で回転する処理を追加します。

    [SerializeField] private float turnSpeed = 10.0f;   // 回転速度
    void LateUpdate ()
    {
        // 水平回転の更新
        if(Input.GetMouseButton(0))
            hRotation *= Quaternion.Euler(0, Input.GetAxis("Mouse X") * turnSpeed, 0);

        // カメラの回転(transform.rotation)の更新
        // 方法1 : 垂直回転してから水平回転する合成回転とします
        transform.rotation = hRotation * vRotation;

        // カメラの位置(transform.position)の更新
        // player位置から距離distanceだけ手前に引いた位置を設定します
        transform.position = player.position - transform.rotation * Vector3.forward * distance; 
    }

無条件に動くと面倒だったので、ボタン押下中のみ(つまりドラッグ)動作にしてみました。
で、動かしてみる

f:id:sasanon:20170917050813g:plain

ぐるぐるぐーる

プレイヤーが移動方向を瞬時に向くようにする

Player.csスクリプトのUpdate()の後半に、
向きの制御を追加します。

        // いずれかの方向に移動している場合
        if(velocity.magnitude > 0)
        {
            // プレイヤーの回転(transform.rotation)の更新
            // 無回転状態のプレイヤーのZ+方向(後頭部)を、移動の反対方向(-velocity)に回す回転とします
            transform.rotation = Quaternion.LookRotation(-velocity);

            // プレイヤーの位置(transform.position)の更新
            // 移動方向ベクトル(velocity)を足し込みます
            transform.position += velocity;
        }

Quaternion.LookRotationは、無回転状態のVector3.forward(Z+方向)を、第1引数のベクトルの向きに回します。 このキャラは無回転状態のときZ+方向に後頭部があるので、後頭部を向かせたいベクトルを引数に入れています。

f:id:sasanon:20170917051845g:plain

進む方向に向いたぞ!
なんて前向きなやつなんだ!

プレイヤーが移動方向へ振り向くようにする

今書いたプレイヤーの瞬時振り向きを、ゆっくり振り向くようにしてみます。
Player.csスクリプトに振り向きの適用速度のパラメータを入れて、transform.rotationの式を書き換えます。

    [SerializeField] private float applySpeed = 0.2f;       // 回転の適用速度
        // いずれかの方向に移動している場合
        if(velocity.magnitude > 0)
        {
            // プレイヤーの回転(transform.rotation)の更新
            // 無回転状態のプレイヤーのZ+方向(後頭部)を、移動の反対方向(-velocity)に回す回転に段々近づけます
            transform.rotation = Quaternion.Slerp(transform.rotation,
                                                  Quaternion.LookRotation(-velocity),
                                                  applySpeed);
        }

Quaternion.Slerp(球面線形補間)を使って補間してます。
applySpeedは0~1の間で指定します。0だと回転せず、1は瞬時に向くようになります。

f:id:sasanon:20170917054229g:plain

くるっ。くるっ。

プレイヤーがカメラの向きに応じて移動方向を変えるようにする

Wを押したとき、本当はプレイヤーには「カメラが向いている奥行き方向」に進んで欲しいわけです。 そこで、カメラの水平回転(Y軸回転)を参照して、移動方向ベクトルを回してあげます。
Player.csスクリプトに参照用のパラメータを入れて、向きと移動の式を両方書き換えます。

    [SerializeField] private PlayerFollowCamera refCamera;  // カメラの水平回転を参照する用
        // いずれかの方向に移動している場合
        if(velocity.magnitude > 0)
        {
            // プレイヤーの回転(transform.rotation)の更新
            // 無回転状態のプレイヤーのZ+方向(後頭部)を、
            // カメラの水平回転(refCamera.hRotation)で回した移動の反対方向(-velocity)に回す回転に段々近づけます
            transform.rotation = Quaternion.Slerp(transform.rotation,
                                                  Quaternion.LookRotation(refCamera.hRotation * -velocity),
                                                  applySpeed);

            // プレイヤーの位置(transform.position)の更新
            // カメラの水平回転(refCamera.hRotation)で回した移動方向(velocity)を足し込みます
            transform.position += refCamera.hRotation * velocity;
        }

スクリプトを変更した後、Main Cameraオブジェクトを参照させて、実行。

f:id:sasanon:20170917060448g:plain

自由に動けそうな感じになってきた!

カメラの注視点を補正する

カメラの向いている方向に動けるになると一気にTPSぽくなったのですが、 同時にプレイヤーが画面の上の方に居すぎじゃない?という感覚が急に湧いてきたので、 カメラの注視点をプレイヤーの上空にずらしてみました。

        // カメラの位置(transform.position)の更新
        // player位置から距離distanceだけ手前に引いた位置を設定します(位置補正版)
        transform.position = player.position + new Vector3(0, 3, 0) - transform.rotation * Vector3.forward * distance; 

f:id:sasanon:20170917061344g:plain

実際には、今回は固定30度にしている見下ろし角度と合わせて調整すると良いと思います。
アクションゲームの場合、ジャンプや高所飛び降り、崖登り等に合わせて変化させるとそれらしくなりそうな予感。

最終的にこんなコードに

なりました。大体40行くらいずつですね。

// Player.cs
using UnityEngine;

// プレイヤー
public class Player : MonoBehaviour {

    [SerializeField] private Vector3 velocity;              // 移動方向
    [SerializeField] private float moveSpeed = 5.0f;        // 移動速度
    [SerializeField] private float applySpeed = 0.2f;       // 振り向きの適用速度
    [SerializeField] private PlayerFollowCamera refCamera;  // カメラの水平回転を参照する用

    void Update () {
        // WASD入力から、XZ平面(水平な地面)を移動する方向(velocity)を得ます
        velocity = Vector3.zero;
        if(Input.GetKey(KeyCode.W))
            velocity.z += 1;
        if(Input.GetKey(KeyCode.A))
            velocity.x -= 1;
        if(Input.GetKey(KeyCode.S))
            velocity.z -= 1;
        if(Input.GetKey(KeyCode.D))
            velocity.x += 1;

        // 速度ベクトルの長さを1秒でmoveSpeedだけ進むように調整します
        velocity = velocity.normalized * moveSpeed * Time.deltaTime;

        // いずれかの方向に移動している場合
        if(velocity.magnitude > 0)
        {
            // プレイヤーの回転(transform.rotation)の更新
            // 無回転状態のプレイヤーのZ+方向(後頭部)を、
            // カメラの水平回転(refCamera.hRotation)で回した移動の反対方向(-velocity)に回す回転に段々近づけます
            transform.rotation = Quaternion.Slerp(transform.rotation,
                                                  Quaternion.LookRotation(refCamera.hRotation * -velocity),
                                                  applySpeed);

            // プレイヤーの位置(transform.position)の更新
            // カメラの水平回転(refCamera.hRotation)で回した移動方向(velocity)を足し込みます
            transform.position += refCamera.hRotation * velocity;
        }
    }
}


// PlayerFollowCamera.cs
using UnityEngine;

// プレイヤー追従カメラ
public class PlayerFollowCamera : MonoBehaviour {
    [SerializeField] private float turnSpeed = 10.0f;   // 回転速度
    [SerializeField] private Transform player;          // 注視対象プレイヤー

    [SerializeField] private float distance = 15.0f;    // 注視対象プレイヤーからカメラを離す距離
    [SerializeField] private Quaternion vRotation;      // カメラの垂直回転(見下ろし回転)
    [SerializeField] public  Quaternion hRotation;      // カメラの水平回転

    void Start ()
    {
        // 回転の初期化
        vRotation = Quaternion.Euler(30, 0, 0);         // 垂直回転(X軸を軸とする回転)は、30度見下ろす回転
        hRotation = Quaternion.identity;                // 水平回転(Y軸を軸とする回転)は、無回転
        transform.rotation = hRotation * vRotation;     // 最終的なカメラの回転は、垂直回転してから水平回転する合成回転

        // 位置の初期化
        // player位置から距離distanceだけ手前に引いた位置を設定します
        transform.position = player.position - transform.rotation * Vector3.forward * distance; 
    }

    void LateUpdate ()
    {
        // 水平回転の更新
        if(Input.GetMouseButton(0))
            hRotation *= Quaternion.Euler(0, Input.GetAxis("Mouse X") * turnSpeed, 0);

        // カメラの回転(transform.rotation)の更新
        // 方法1 : 垂直回転してから水平回転する合成回転とします
        transform.rotation = hRotation * vRotation;

        // カメラの位置(transform.position)の更新
        // player位置から距離distanceだけ手前に引いた位置を設定します(位置補正版)
        transform.position = player.position + new Vector3(0, 3, 0) - transform.rotation * Vector3.forward * distance; 
    }
}