PLATEAU SDK for Unityを使って久留米を歩いてみようハンズオン(2) - プレイヤーを作ってTPS風に動かそう

# はじめに

これの続きです。前回でマップを作りました(PLATEAUのデータを取り込んだだけ)。
今回はプレイヤーキャラクターを作って三人称視点で動かせるようにしていきます。

https://qiita.com/Sane21/items/7ab878b5f0efdd8ec61e

イメージはこんな感じ。

![pubg.png](https://www.famitsu.com/images/000/157/378/z_5afb92dc5aae7.jpg)
([「スマホ版『PUBG MOBILE』が本日5月16日よりサービス開始！ 事前登録数は130万を突破」ファミ通 2018.05.16](https://www.famitsu.com/news/201805/16157378.html) より抜粋)

一応、本記事だけでもTPSゲームの第1歩目くらいには機能するかなと思います。

# Playerキャラクターを動かす仕組みの概要
このようにプレイヤーのすぐ後ろにカメラを配置してあげることで三人称の視点を再現します。
UnityではHierarchyビューで設定した親子関係にあるObjectは連動して動いてくれるので、この状態でプレイヤーを移動させることで三人称での移動が実現できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/83178daf-4795-10ca-2c21-57d1f0e49c70.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/37a4901d-a114-2bf7-7869-0a7894c1e218.png)


# Playerキャラクターの作成

Hierarchyビュー上で右クリックをすると次のようなウィンドウが表示されます。ここから「3D Objects > Capsule」を選択してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/473ba612-0359-6236-dedb-d10b468fa4cb.png)

すると画面上に真っ白のカプセルが出現します。Cupsuleの名前は「Player」に変更しておきましょう。
これをSceneまたはInspectorビュー上で移動させてマップ上の初期位置にしたい場所まで持ってきましょう。(どこにあるか分からない場合は、Hierarchyビューで「Player」をダブルクリックするとその場所に飛ぶことができます。)
今回はJR久留米駅の前まで持ってきてみました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/5ad1254c-146f-a609-ec24-2d4105425597.png)

更にこの状態でCupsuleを出したときと同様にHierarchyビューで右クリックから今度は「Camera」を選択します。出現したカメラの名前は「PlayerCamera」にしましょう。このカメラがプレイヤーの眼としての役割を果たしてくれます。
PlayerとPlayerCameraを親子の関係にしましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/999cd90d-0e6e-633e-1714-21ac0a27efb7.png)

この状態では一人称視点になっています。三人称にするには、カメラがプレイヤーをプレイヤーの少し後ろから観測して上げる必要があります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/d08366c3-c8fd-4b31-2905-4d9209198f84.png)

カメラのInspectorの数値をいじってあげましょう。
PositionのY軸を少し上げてプレイヤーの上に、Z軸を少し下げてプレイヤーの後ろに下がらせます。
また、RotationのX軸を少しだけ回してあげてプレイヤーを見下ろす形にします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/23e0b450-062e-a1d9-b871-1017d6e84591.png)

これだけでもなんだかそれっぽい見た目になっています。


# Playerキャラクターの動きの作成
続いて、プレイヤーを動かすプログラムを書いていきます。

## プログラムを書く準備
まずはUnityのProjectビュー上で右クリックをして「Create > Folder」からフォルダを作成します。Assets配下に「Scripts」フォルダを作成しましょう。続けて、いま作成したScriptsフォルダ内で再度右クリックをし「Create > C# Script」からC#のファイルを作ります。名前は「Player」にしましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/249c9a09-f4f2-1382-10d4-7def6e260919.png)

こんな感じになっていたらOKです。このままPlayerをダブルクリックしてプログラムを書きに行きましょう。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/86222074-39e7-402c-6c4f-d7f856debd79.png)

別のタブでVisual Studio Codeか、Visual Studioのいずれかが開くと思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/223073c2-fb11-f0bf-4d73-7829f8fb6d0e.png)


コード全体を次のように書き換えます。
フィールド変数の`transform`ではプレイヤーの位置や回転などの情報を保持します。`Start`ではこれを初期化しています。

`Update`では`MoveByKey`でキーボード入力による移動を、`RotateByMouse`でマウス入力による回転を呼び出しています。まだ中身が空っぽなので何も動きません。この2つのメソッドを充実させていきましょう。

```csharp

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField] float moveSpeed = 0.0f;
    [SerializeField] float rotateSpeed = 0.0f;
    Transform transform = null;

    // Start is called before the first frame update
    void Start()
    {
        this.transform = gameObject.transform;
    }

    // Update is called once per frame
    void Update()
    {
        this.MoveByKey();
        this.RotateHeadByMouse();
    }

    private void MoveByKey()
    {

    }

    private void RotateHeadByMouse()
    {

    }
}
```

このプログラムをPlayerに付与してあげましょう。
Hierarchyから「Player」を選択し、Inspectorで「Add Component」をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/dce9d449-a0c4-556b-c929-0202f3d3d9a8.png)

現れたウィンドウで作成したプログラム「Player」を探してクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/c3351b63-1d40-9885-283c-3ae8ac2f56d7.png)

すると、このようなInspectorビューにこのような表示が追加されます。
プログラムで`[SerializeField]`をつけたフィールド変数はUnityEditorから数値をいじることができます。この数値をいじって移動速度`moveSpeed`や回転速度`rotateSpeed`を調整してあげましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/79dead22-0549-dca1-0d28-4e698c2342db.png)






## 縦横の移動

まずは移動を実現します。`MoveByKey`の中身を次のように書き換えてください。
`Input.GetAxis`は引数の`string`で指定した入力値を教えてくれます。

https://docs.unity3d.com/ja/2021.1/ScriptReference/Input.GetAxis.html

```csharp
    private void MoveByKey()
    {
        // 横方向の入力値を保持(A, ← or D, →)
        float horizontal = Input.GetAxis("Horizontal") * this.moveSpeed;
        // 縦方向のの入力値を保持(W, ↑ or S, ↓)
        float vertical = Input.GetAxis("Vertical") * this.moveSpeed;

        Vector3 currentPosition = this.transform.position;
        this.transform.position = new Vector3(currentPosition.x + horizontal, currentPosition.y, currentPosition.z + vertical);
    }
```

動作確認をしてみましょう。▷マークのプレイボタンを押すとゲームを開始できます。
※PlayerのMoveSpeedを0より大きくしていないと移動距離が掛け算で0になってしまい動きません。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/3029fb2e-3e44-663e-1cc9-08c9373f5b88.png)

「WASD」または「矢印キー」での移動操作ができるようになりました。
![move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/2ed15099-d4c4-6849-99d6-e2b458c86811.gif)


## 回転の実装(首振り)
このままでは方向転換はできませんので、Y軸方向に回転できるようにします。

次のようにします。移動と同様に`Input.Axis`でマウスの横方向の移動を検知し、その分だけ回転させます。

```csharp
    private void RotateHeadByMouse()
    {
        // マウスの横方向の移動を保持
        float rotate = Input.GetAxis("Mouse X") * this.rotateSpeed;
        Vector3 rotation = new Vector3(0, rotate, 0);
        this.transform.Rotate(rotation);
    }
```
マウスの移動に合わせて回転してくれるようになりました。

![rotate.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/03d42898-b0ff-5e21-9746-0f54ffdbbf74.gif)

しかし、挙動をよく見ると回転と移動方向が連動していないことに気づきます。

## 移動と回転の連動
それもそのはず。移動は方眼紙の縦横のように決まった線の上を移動しており、回転はそれとは関係なく回っているだけです。これを解消するにはどうするべきか。移動のなかで回転の情報も参照して移動方向を決定すれば良いのです。

`MoveByKey`メソッドを次のように書き換えます。
元のコードと変わったのは位置の更新部分。
もともとは、現在地に対してX軸方向は横の入力値`horizontal`、Z軸方向は縦の入力値を加算する単純なコードでした。
新しいコードでは、現在地に対して横向きの方向`right`に横の入力値を掛けた数`horizontal`、前向きの方向`forward`に縦の入力値`vertical`を掛けた数を足しています。
横向き、前向きの方向は単純にプレイヤーの位置を原点とした角度の計算で導出されています。




```csharp
    private void MoveByKey()
    {
        // 横方向の入力値を保持(A, ← or D, →)
        float horizontal = Input.GetAxis("Horizontal") * this.moveSpeed;
        // 縦方向のの入力値を保持(W, ↑ or S, ↓)
        float vertical = Input.GetAxis("Vertical") * this.moveSpeed;

        Vector3 currentPosition = this.transform.position;
        // this.transform.position = new Vector3(currentPosition.x + horizontal, currentPosition.y, currentPosition.z + vertical);
        this.transform.position = currentPosition + this.transform.forward * vertical + this.transform.right * horizontal;
    }
```

これでマウスで首を振って向いている方向に合わせて進むことができるようになりました。

![rotate_move.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/884897/25c29048-e1f6-76de-3bdc-d47862662820.gif)


# まとめ

ゲームとして最低限の操作ができるようになったかなと思います。

ただし、このままでは縦横無尽に建物を突き抜けていたり、街の見た目も簡素だったり遊ぶにはまだまだ課題がたくさんあります。

これらを次回から一つずつ解消していきます。

## おまけ 上下にも首を振る方法
実は首を上下に振れないままなので振れるようにしたコードも展開しておきます。
左右の回転と違って、首を上下に回すにはカメラのオブジェクトの管理も必要になって説明がやや面倒なのでシンプルにするため本編では省いていました。気になる人は参考にしてみてください。

<details><summary>Player.cs(追加版)</summary>

```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Player : MonoBehaviour
{
    [SerializeField] GameObject cameraObject = null;
    [SerializeField] float moveSpeed = 0.0f;
    [SerializeField] float rotateSpeed = 0.0f;
    [SerializeField] float upSpeed = 0.0f;
    Transform playerTransform = null;
    Transform cameraTransform = null;



    // Start is called before the first frame update
    void Start()
    {
        this.playerTransform = gameObject.transform;
        this.cameraTransform = this.cameraObject.transform;
    }

    // Update is called once per frame
    void Update()
    {
        this.MoveByKey();
        this.RotateHeadByMouse();
    }

    private void MoveByKey()
    {
        // 横方向の入力値を保持(A, ← or D, →)
        float horizontal = Input.GetAxis("Horizontal") * this.moveSpeed;
        // 縦方向のの入力値を保持(W, ↑ or S, ↓)
        float vertical = Input.GetAxis("Vertical") * this.moveSpeed;

        Vector3 currentPosition = this.playerTransform.position;
        this.playerTransform.position = currentPosition + this.playerTransform.forward * vertical + this.playerTransform.right * horizontal;
    }

    private void RotateHeadByMouse()
    {
        // マウスの横方向の移動を保持
        float rotate = Input.GetAxis("Mouse X") * this.rotateSpeed;
        Vector3 rotation = new Vector3(0, rotate, 0);
        this.playerTransform.Rotate(rotation);

        // マウスの縦方向の移動を保持 -> カメラを回転
        float up = Input.GetAxis("Mouse Y") * this.upSpeed;
        rotation = new Vector3(-up, 0, 0);
        this.cameraTransform.Rotate(rotation);

    }
}
```
</details>

# 連載記事一覧
[第1回「環境構築, 導入」](https://qiita.com/Sane21/items/7ab878b5f0efdd8ec61e)
[第2回 「プレイヤーを作ってTPS風に動かそう」](https://qiita.com/Sane21/items/8595e25b5536c317fabd)
第3回 建物に衝突するようにするか、見た目を整えるようにするかどっちかやります