3DのTPSシューティングゲームを作りたい人の日記2

#はじめに
1つ目の記事を読んだ方からすればかなりお久しぶりです。更新遅くなってしまいました・・・。
[前回の日記はこちら](https://qiita.com/midorinohito/items/b1f9831151433c3417c6)

この記事は私がUnityで3DのTPSシューティングゲームを作る過程を残しておくために書いています(随時更新予定)。

プログラムを組む過程で詰まったところで参考にしたもの等を書いていくので、同じようなことで詰まっている方の助けになればと思います。

#作りたいもの
前回は、**スタート地点からゴール地点まで大事なものを敵を倒しながら運ぶゲーム**を目指して作ると言っていましたが、それだけだとつまらないかなと思ったので、次のように目標を変えようと思います。
**特定のアイテムを集めてゴールまで運ぶ**というのを目的としたゲームにしたいと思います。現在は、アイテムを集める過程でギミックや敵を倒さなければならないようにする予定です。

#進歩
今回は時間があいてしまったので、結構変更点があります。また、コード等は自分が苦戦した部分のみ説明します。

1. プレイヤーの3Dモデルの制作
1. プレイヤーのアニメーションの制作と実装
1. 前回までで作ったスクリプトを読みやすく書きかえる
1. プレイヤーの移動方法をCharacterControllerを使う方法から、ColliderとRigidbodyを使う方法に変更
1. 敵がプレイヤーを追従するように移動
プレイヤーのHPを画面に表示
1. プレイヤーの玉が敵に当たると敵がダメージを受けるようにした
1. 敵の玉がプレイヤーに当たるとプレイヤーのHPが減るようにした
1. 所持品を画面に表示できるようにした
![スクリーンショット 2020-11-03 155438.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688227/f7b501e8-4da1-3c0b-a86c-dd6c533eb70d.png)
[現在までの進歩動画]
(https://youtu.be/MIAT8AVLSZY)

#プレイヤーの移動について
この部分の実装をするうえで、苦戦したのは、地面にそってプレイヤーの移動を行う部分なので、その部分について説明します。
プレイヤーの移動自体は次のようなコードで行っています。

```c#
float inputHorizontal = Input.GetAxisRaw("Horizontal Stick-L");//コントローラーの縦方向の入力を代入
float inputVertical = Input.GetAxisRaw("Vertical Stick-L") * -1;//コントローラーの横方向の入力を代入
 
// カメラの方向から、X-Z平面の単位ベクトルを取得
Vector3 cameraForward = Vector3.Scale(Camera.main.transform.forward, new Vector3(1, 0, 1)).normalized;
// 方向キーの入力値とカメラの向きから、移動方向を決定
moveForward = cameraForward * inputVertical + Camera.main.transform.right * inputHorizontal;
moveForward = moveForward.normalized;//正規化する
//コントローラーのLスティックの入力があった場合
if(inputHorizontal != 0 || inputVertical != 0){
      isMove = true;//プレイヤーが動いているのでtrue
      transform.position += moveForward * moveSpeed * Time.deltaTime;
      anim.SetBool ("Run", true);//アニメーターのRunをtrueにする
}
```
しかし、このコードだと**プレイヤーの向いている方向に移動する**ことになるので、地面の高低差が考慮されていません。つまり、下の画像のように、高い場所への移動は違和感なく行えますが、高い場所から低い場所への移動は上手くできません。
![Sooting game 資料 (2).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/688227/97e69489-936f-231f-3b45-5c2465b0359c.png)
私は、ここでどうすれば地面に沿って移動するようにできるのか分からなくて結構調べました。その結果、Raycastを使うと、上手くできそうだということがわかりました。(後々見返したら銃を撃つためのスクリプトでも使っていたことに気が付きました・・・)
そこで、地面に沿って移動してもらうために、次のようなコードを追加しました。

```c#
public float distanceFromSurface;//Raycastの衝突したものからの高さ

//ここから下はUpdate関数の中
RaycastHit hitInfo;//衝突したものの情報が入る変数
//地面についていない場合、地面につくようにする
if (Physics.Raycast(transform.position, Vector3.down, out hitInfo, Mathf.Infinity))
{
    Vector3 newPos = transform.position;
    newPos.y = hitInfo.point.y + distanceFromSurface;
    transform.position = newPos;
}
```

この部分では、Raycastというものを使ってプレイヤーが地面に沿って移動するようにしています。今回の場合、Raycastを使うと、プレイヤーの真下にRay(まっすぐな光線)を出して、Rayが何かに衝突したら、衝突したものの位置に高さ(y座標)を合わせるという動作になっています。

<details><summary>プレイヤーを動かすためのプログラム全体</summary><div>

```c#
public class PlayerController : MonoBehaviour
{
    public float distanceFromSurface;//Raycastの衝突したものからの高さ
	public float JumpPower;//ジャンプ力
	public float moveSpeed;//移動速度
    private Vector3 moveForward;//移動方向の単位ベクトル
    private Animator anim;//Animator型の変数
    private PlayerStatus playerStatus;//プレイヤーのステータスを管理するスクリプト
    private bool isMove = false;//プレイヤーが移動していればtrue
 
	void Start () {
        anim = GetComponent<Animator>();//Animatorを変数に代入
        playerStatus = GetComponent<PlayerStatus>();//PlayerStatusスクリプトを変数に代入
        playerStatus.SetHp(playerStatus.GetHp());//プレイヤーの体力を設定
        playerStatus.SetItemBox();//アイテムボックスの初期化
    }
 
	void Update ()
	{
		float inputHorizontal = Input.GetAxisRaw("Horizontal Stick-L");//コントローラーの縦方向の入力を代入
        float inputVertical = Input.GetAxisRaw("Vertical Stick-L") * -1;//コントローラーの横方向の入力を代入
 
        // カメラの方向から、X-Z平面の単位ベクトルを取得
        Vector3 cameraForward = Vector3.Scale(Camera.main.transform.forward, new Vector3(1, 0, 1)).normalized;
        // 方向キーの入力値とカメラの向きから、移動方向を決定
        moveForward = cameraForward * inputVertical + Camera.main.transform.right * inputHorizontal;
        moveForward = moveForward.normalized;//正規化する
        //コントローラーのLスティックの入力があった場合
        if(inputHorizontal != 0 || inputVertical != 0){
            isMove = true;//プレイヤーが動いているのでtrue
            transform.position += moveForward * moveSpeed * Time.deltaTime;
            anim.SetBool ("Run", true);//アニメーターのRunをtrueにするE
        }
        //コントローラーのLスティックの入力がない場合
        else{
            isMove = false;//プレイヤーが止まっているのでfalse
            anim.SetBool ("Run", false);//アニメーターのRunをfalseにする
        }
        RaycastHit hitInfo;//衝突したものの情報が入る変数
        //地面についていない場合、地面につくようにする
        if (Physics.Raycast(transform.position, Vector3.down, out hitInfo, Mathf.Infinity))
        {
            Vector3 newPos = transform.position;
            newPos.y = hitInfo.point.y + distanceFromSurface;
            transform.position = newPos;
        }
    
        //銃を撃ってるときはカメラと同じ方向を向く
        if(Input.GetButton("Fire_R2")){
            Vector3 lookForward = Camera.main.transform.forward;//カメラの向きを変数に代入
            lookForward.y = 0.0f;//x-z平面方向だけカメラの方向を適応するため
            transform.rotation = Quaternion.LookRotation(lookForward);
        }
        // キャラクターの向きを進行方向に
        else if (moveForward != Vector3.zero) {
            transform.rotation = Quaternion.LookRotation(moveForward);
        }

    void OnTriggerEnter(Collider other)
    {
        if(other.CompareTag("item")){//itemに当たったら
            playerStatus.SetItem(other.gameObject);//アイテムを所持品に表示
        }
    }

    /// <summary>
	/// プレイヤーが動いているかどうかを返す
	/// </summary>
    /// <returns>動いていればtrue</returns>
    public bool GetIsMove(){
        return this.isMove;
    }

    /// <summary>
	/// プレイヤーのtime時間で移動する距離を返す
	/// </summary>
	/// <param name="time">時間(s)</param>
    /// <returns>プレイヤーの移動距離</returns>
    public Vector3 GetPlayerMoveVec(float time){
        return moveForward * moveSpeed * time;
    }
}
```

</div></details>
Raycastについてはこちらの記事がわかりやすかったので、参考にさせていただきました。
[おねむゲーマーの備忘録 　Unity 地表に沿ってオブジェクトを配置する](https://sleepygamersmemo.blogspot.com/2017/04/unity-raycast.html)


#敵の撃つ弾について
敵の撃つ弾はターゲット(プレイヤー)をゆるく追尾するような感じにしたいと思い、できるだけその気持ちに近い動作をするようにしました。
実際の動作としては、**プレイヤーが向いている方向にt秒進んだ場所に到達する弾を撃つ**ということになっています。
ただし、t=5とかにすると、とんでもない方向に弾を撃つことになるので、tはとても小さな値になります。tを小さくすれば、プレイヤーを狙って打ってくるように見えるので、プレイヤーが同じ方向に動き続けた場合、**確実に当たります**。そう、確実に当たってしまうのです。それでは**ゆるく**追尾したいのに出来ていないので、弾の速さを少し遅くすることで、それっぽく動くようにしてみました。

<details><summary>敵の撃つ弾についてのプログラム</summary><div>

```c#
public class EnemyShotGenerator : MonoBehaviour
{
    public GameObject target;//弾が狙うターゲット
    public GameObject enemyBulletPrefab;//敵が撃つ弾のPrefab
    private GameObject enemyObj;//敵のオブジェクト
    private EnemyController enemyController;//EnemyControllerスクリプト
    private float shotSpeed = 10.0f;//弾を撃つ速さ

    void Start(){
        enemyObj = transform.parent.gameObject;
        enemyController = enemyObj.GetComponent<EnemyController>();
        target = enemyController.target;
    }

    /// <summary>
	/// predTime秒後のtargetPosの位置に弾を飛ばす
	/// </summary>
    /// <param name="predTime">秒数</param>
    /// <param name="targetPos">弾の着弾する座標</param>
    void Shot2(float predTime, Vector3 targetPos){
        //敵の弾のインスタンス化
        GameObject enemyBullet = Instantiate(enemyBulletPrefab, enemyObj.transform.position, Quaternion.identity);
        //predTime*1.25秒後にtargetPosにつくのに必要な速さ
        shotSpeed = (enemyBullet.transform.position - targetPos).magnitude / (predTime*1.25f);
        //弾が向かうターゲットの位置を設定
        enemyBullet.GetComponent<EnemyShotController>().SetTargetAndPos(target, targetPos);
        //弾の向いている方向にshotSpeedの速さで弾を飛ばす
        enemyBullet.GetComponent<EnemyShotController>().ShotFront(shotSpeed);
        //1.5秒後にenemyBulletを破壊
        Destroy(enemyBullet, 1.5f);
    }
    /// <summary>
	/// プレイヤーの動きに合わせた弾を飛ばす
	/// </summary>
    public void Shot(){
        float predTime = 0.2f;//何秒後の位置に着弾するようにするか
        if(target.GetComponent<PlayerController>().GetIsMove()){
            //predTime秒後のターゲットの位置を予想
            Vector3 NewTargetPos = target.transform.position 
                                    + target.GetComponent<PlayerController>().GetPlayerMoveVec(predTime);
            //predTime秒後のターゲットの位置に弾を飛ばす
            Shot2(predTime, NewTargetPos);
        }
        else{
            //現在のターゲットの位置に弾を飛ばす
            Shot2(predTime, target.transform.position);
        }
    }
}
```

</div></details>

#今後
**1ステージのクリアまで作成する！**

プレイ画面から条件を達成してクリア画面に遷移するまでを作成していきたいと思います。