Unityでゴエモンインパクト戦を作ってみる Part1

#目的
- 設計の速さやそれを実装する速さを測定したい
- 良いアウトプットがしたい
- 良いフィードバックがほしい
- 人が楽しめるゲームを作りたい

#動機
- 就職活動の強みとなるように、Qiitaに定期的に投稿したい
- 昔からの夢は３Dゲーム制作（ソシャゲ開発はしていますが、このままだと一生2Dソシャゲ制作になると思った
- コンシューマ開発したい（いずれはUEに以降したい
- とある知り合い実況者が復活したので自分も負けないよう行動する

#結果
- Profilerで見たところ250fps以上出ている
- 割と思い通りに形に出来た
- 合計18時間で作成できた（約：3日

2020/07/23 16:00 ~ 23:00
Terrain（仮背景いずれは出現する敵によって動かしたりする）
カメラ
弾管理
弾方向

2020/07/24 14:00 ~ 19:00
弾の衝突
敵の当たり判定
敵被弾後の無敵時間
敵の喰らいアニメーション
UI(仮)
MVPに変更

2020/07/24 15:00 ~ 21:00
左パンチ（ジャブ）
右パンチ（ストレート）
百烈パンチ

 
#コード
※GitHubは準備中

MVPは端折っています

ゴエモンインパクト

```C#:GoemonImpact.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using Random = UnityEngine.Random;

namespace Goemon.Impacts
{
    /// <summary> Goemon Impact </summary>
    public class GoemonImpact : MonoBehaviour
    {
        /// <summary> インパクトのカメラ　操縦席の目 </summary>
        [SerializeField] private Camera _camera = default;
        /// <summary> 弾やパンチを出す場所 </summary>
        [SerializeField] private Transform actionTransform = default;
        /// <summary> 左手　パンチはカメラTransformの下から出すようにした </summary>
        [SerializeField] private ImpactHandLeft impactHandLeft = default;
        /// <summary> 右手 </summary>
        [SerializeField] private ImpactHandRight impactHandRight = default;
        /// <summary> 100烈出す場所 </summary>
        [SerializeField] private Transform oneHandredPunchTransform = default;
        
        
        /// <summary> 計算用差分カメラ回転値 </summary>
        private Vector3 _presentCamRotation;
        /// <summary> カメラ回転制限のためのカメラ初期値 </summary>
        private Quaternion initCameraRotation;
        /// <summary> 初期化 </summary>
        private bool initialze = false;
        /// <summary> モデル </summary>
        private GoemonImpactBattleModel model = null;
        /// <summary> 小判生成リスト </summary>
        private List<ImpactKoban> kobanList = new List<ImpactKoban>();
        /// <summary> 100烈パンチ生成リスト </summary>
        private List<GameObject> oneHandredPunchList = new List<GameObject>();
        /// <summary> 100烈パンチのカウント </summary>
        private int punchCounter = 0;
        
        /// <summary> 初期化 </summary>
        public void Initialize(GoemonImpactBattleModel battleModel)
        {
            this.model = battleModel;
            initialze = true;
            initCameraRotation = _camera.transform.rotation;
            kobanList = new List<ImpactKoban>();
            oneHandredPunchList = new List<GameObject>();
            impactHandLeft.onEndPunch += LeftPunchOnEnd;
            impactHandRight.onEndPunch += RightPunchOnEnd;
            punchCounter = 0;
        }

        /// <summary>
        /// 更新
        /// SceneManagerでUpdateする
        /// </summary>
        /// <param name="deltaTime">進んだ時間</param>
        public void ManualUpdate(float deltaTime)
        {
            // 初期化済みなら処理開始
            if (initialze)
            {
                // 小判生成リストがあれば小判の位置によって削除処理
                if (kobanList.Count > 0)
                {
                    var copyKobanList = new List<ImpactKoban>(kobanList);
                    foreach (var koban in copyKobanList)
                    {
                        // 小判を消す範囲
                        if (koban.gameObject.transform.position.x > actionTransform.position.x + 50.0f ||
                            koban.gameObject.transform.position.x < actionTransform.position.x - 50.0f ||
                            koban.gameObject.transform.position.y > actionTransform.position.y + 50.0f ||
                            koban.gameObject.transform.position.y < actionTransform.position.y - 50.0f ||
                            koban.gameObject.transform.position.z > actionTransform.position.z + 50.0f) 
                        {
                            kobanList.Remove(koban);
                            Destroy(koban.gameObject);
                        }
                    }
                }

                // 百烈パンチのオブジェクトを削除
                if (oneHandredPunchList.Count > 0)
                {
                    var copyPunchList = new List<GameObject>(oneHandredPunchList);
                    foreach (var punch in copyPunchList)
                    {
                        // パンチオブジェクトのAnimationが終わっていたら削除
                        if (!punch.GetComponent<Animation>().IsPlaying("100PunchParts"))
                        {
                            oneHandredPunchList.Remove(punch);
                            Destroy(punch);
                        }
                    }
                }

                // 百烈パンチのオブジェクト生成
                if (model.IsOneHundredPunch.Value)
                {
                    var resource = Resources.Load<GameObject>("Impacts/100PunchParts");
                    var obj = Instantiate(resource, oneHandredPunchTransform);
                    // 百烈を出す場所をランダムで決める
                    float positionX = Random.Range(-2.0f, 2.0f);
                    float positionY = Random.Range(-2.0f, 2.0f);
                    obj.transform.localPosition = new Vector3(positionX, positionY, 0.0f);
                    oneHandredPunchList.Add(obj);
                    punchCounter++;
                    if (punchCounter == 100)
                    {
                        punchCounter = 0;
                        model.SetOneHandredPunch(false);
                    }
                }
                
                // パンチしてないならカメラが動かせる
                if (model.ImpactLeftPunchCoolTime.Value == 0 && model.ImpactRightPunchCoolTime.Value == 0)
                {
                    // カメラ回転計算
                    _presentCamRotation.x = _camera.transform.eulerAngles.x;
                    _presentCamRotation.y = _camera.transform.eulerAngles.y;

                    float rotateX = _presentCamRotation.x;
                    float rotateY = _presentCamRotation.y;

                    if (Input.GetKey(KeyCode.UpArrow))
                    {
                        _presentCamRotation.x = _presentCamRotation.x - ConstImapct.CAMERA_X_MOVE;
                    }

                    if (Input.GetKey(KeyCode.DownArrow))
                    {
                        _presentCamRotation.x = _presentCamRotation.x + ConstImapct.CAMERA_X_MOVE;
                    }

                    if (Input.GetKey(KeyCode.LeftArrow))
                    {
                        _presentCamRotation.y = _presentCamRotation.y - ConstImapct.CAMERA_Y_MOVE;
                    }

                    if (Input.GetKey(KeyCode.RightArrow))
                    {
                        _presentCamRotation.y = _presentCamRotation.y + ConstImapct.CAMERA_Y_MOVE;
                    }

                    rotateX = Mathf.Clamp(Mathf.DeltaAngle(initCameraRotation.eulerAngles.x, _presentCamRotation.x),
                        -ConstImapct.ROTATE_X_LIMIT, ConstImapct.ROTATE_X_LIMIT);
                    rotateY = Mathf.Clamp(Mathf.DeltaAngle(initCameraRotation.eulerAngles.y, _presentCamRotation.y),
                        -ConstImapct.ROTATE_Y_LIMIT, ConstImapct.ROTATE_Y_LIMIT);

                    // カメラが向いている方向をUpdate
                    _camera.transform.rotation = Quaternion.Euler(rotateX, rotateY, 0);
                }
            }
        }

        /// <summary> 小判追加 </summary>
        /// <param name="addKoban">追加小判数</param>
        public void KobanAdd(int addKoban)
        {
            model.SetKobanCount(model.KobanCount.Value + addKoban);
        }
        
        /// <summary> 後更新処理 </summary>
        private void LateUpdate()
        {
            // パンチしてないならパンチと小判が打てる
            if (!model.IsOneHundredPunch.Value && model.ImpactLeftPunchCoolTime.Value == 0.0f && model.ImpactRightPunchCoolTime.Value == 0.0f)
            {
                // Xボタンで弱パンチ攻撃
                if (Input.GetKeyDown(KeyCode.X))
                {
                   // 敵にあたったら手を戻すフレームを数フレーム早くする
                   impactHandLeft.Punch();
                   //model.SetImpactLeftPunchCoolTime(impactHandLeft.PunchCoolTime);
                }
            
                // Zボタンで強パンチ攻撃
                if (Input.GetKeyDown(KeyCode.Z))
                {
                    // 敵にあたったら手を戻すフレームを数フレーム早くする
                    impactHandRight.Punch();
                    //model.SetImpactRightPunchCoolTime(impactHandRight.PunchCoolTime);
                }

                // Cボタンで弾発射
                if (Input.GetKeyDown(KeyCode.C))
                {
                    if (model.KobanCount.Value > 0)
                    {
                        if (kobanList.Count < ConstImapct.KOBAN_MAX)
                        {
                            // 弾上限値を超えていなければ弾を生成（プール処理がいいかな）
                            var kobanResource = Resources.Load<ImpactKoban>("Impacts/ImpactKoban");
                            var obj = Instantiate<ImpactKoban>(kobanResource, actionTransform);
                            // ListからRemoveしたいので弾の削除処理をImpactで登録
                            obj.onDestroy += KobanOnDestroy;
                            // カメラの向いている方向に弾を飛ばす為、カメラのTransformを渡す
                            obj.SetMoveVector3(_camera.transform);
                            // 生成リストに追加
                            kobanList.Add(obj);
                            model.SetKobanCount(model.KobanCount.Value - 1);
                        }
                        else
                        {
                            Debug.Log("打ちすぎ " + ConstImapct.KOBAN_MAX + "個まで");
                        }
                    }
                    else
                    {
                        Debug.Log("小判ねーぞ");
                    }
                }

                if (Input.GetKeyDown(KeyCode.V))
                {
                    model.SetOneHandredPunch(true);
                }
            }
        }

        // アクションに入れる処理
        /// <summary> 小判を削除するための登録用関数 </summary>
        /// <param name="koban">小判クラス</param>
        private void KobanOnDestroy(ImpactKoban koban)
        {
            kobanList.Remove(koban);
            Destroy(koban.gameObject);
        }

        /// <summary> 左パンチ終わり </summary>
        private void LeftPunchOnEnd()
        {
            model.SetImpactLeftPunchCoolTime(0.0f);
        }

        /// <summary> 右パンチ終わり </summary>
        private void RightPunchOnEnd()
        {
            model.SetImpactRightPunchCoolTime(0.0f);
        }
        
        /// <summary> インパクト（GameObject）削除 </summary>
        private void OnDestroy()
        {
            initialze = false;
            _camera = null;
            actionTransform = null;
        }
    }
}
```
巨大ボス

```C#:GiantEnemy.cs
using UnityEngine;

namespace Goemon.Impacts
{
    /// <summary> 巨大ボスクラス </summary>
    public class GiantEnemy : MonoBehaviour
    {
        [SerializeField] private BoxCollider boxCollider = default;
        [SerializeField] private Animator animator = default;

        private GoemonImpactBattleModel model = null;

        /// <summary>
        /// 初期化
        /// </summary>
        /// <param name="model"></param>
        public void Initialize(GoemonImpactBattleModel model)
        {
            this.model = model;
        }
        
        /// <summary>
        /// 更新処理(当たり判定などの物理演算用)
        /// </summary>
        /// <param name="deltaTime">frameの時間</param>
        public void ManualFixedUpdate(float deltaTime)
        {
            var isInvincibilityTime = false;
            if (model.EnemyInvincibilityTime.Value > 0)
            {
                isInvincibilityTime = true;
                model.SetEnemyInvincibilityTime(model.EnemyInvincibilityTime.Value - deltaTime);
                if (model.EnemyInvincibilityTime.Value < 0)
                {
                    model.SetEnemyInvincibilityTime(0);
                }
            }

            Collider[] colliders = Physics.OverlapBox(boxCollider.transform.position, boxCollider.transform.localScale);
            foreach (var collider in colliders)
            {
                // 小判を食らった場合
                if (collider.GetComponent<ImpactKoban>() != null)
                {
                    var koban = collider.GetComponent<ImpactKoban>();
                    
                    Debug.Log("ログ対決！これは「敵」だよ！！！！！");
                    
                    koban.onDestroy.Invoke(koban);
                    
                    if (!isInvincibilityTime)
                    {
                        Damage(koban.Damage, 1.0f);
                    }
                }

                // ジャブを食らった場合
                if (collider.GetComponentInParent<ImpactHandLeft>() != null)
                {
                    var impactLeftPunch = collider.GetComponentInParent<ImpactHandLeft>();
                    
                    Debug.Log("ジャブヒット！");
                    
                    impactLeftPunch.PunchEnd();
                    
                    if (!isInvincibilityTime)
                    {
                        Damage(impactLeftPunch.Damage, 0.2f);
                    }
                }
                
                // ストレートを食らった場合
                if (collider.GetComponentInParent<ImpactHandRight>() != null)
                {
                    var impactRightPunch = collider.GetComponentInParent<ImpactHandRight>();
                    
                    Debug.Log("ストレートヒット！");
                    
                    impactRightPunch.PunchEnd();
                    
                    if (!isInvincibilityTime)
                    {
                        Damage(impactRightPunch.Damage, 1.0f);
                    }
                }

                if (collider.GetComponentInParent<ImpactOneHundredPunchParts>() != null)
                {
                    var OneHundredPunch = collider.GetComponentInParent<ImpactOneHundredPunchParts>();
                    
                    Debug.Log("百烈ヒット！");
                    
                    OneHundredPunch.OnEnd();
                    
                    if (!isInvincibilityTime)
                    {
                        Damage(OneHundredPunch.Damage, 0.01f);
                    }
                }
            }
        }

        /// <summary> ダメージを受けた際の処理 </summary>
        /// <param name="damage">ダメージ数</param>
        /// <param name="invincibilityTime">ダメージを受けた後の無敵時間</param>
        private void Damage(int damage, float invincibilityTime)
        {
            model.SetEnemyHP(model.EnemyHp.Value - damage);
            if (model.EnemyHp.Value <= 0)
            {
                // 死んだら移動させる
                transform.position = new Vector3(0,0,-100);
            }
            else
            {
                model.SetEnemyInvincibilityTime(invincibilityTime);
                animator.SetTrigger("Damage");
            }
        }
    }
}

```
#課題
- 当たり判定を敵だけに実装しているため、オブジェクト指向にしたい
    - たまに弾は当たったのに、敵が当たっていない判定になるのが面倒くさいので敵にまとめてしまった
    - これだと、インパクトの喰らい判定どうするのという問題が出てくるため、いずれ変更したい
- 百烈パンチを生成でやっているので、予め用意したオブジェクトのONOFFでやりたい
- コックピットをUIから３Dモデルにしたい
- 弾を打ち出すところをカメラの下にしたい

#次回
敵AIを実装してみる（ワクワク）

#参考文献

インパクトの大きさ
https://dic.nicovideo.jp/b/a/%E3%82%B4%E3%82%A8%E3%83%A2%E3%83%B3%E3%82%A4%E3%83%B3%E3%83%91%E3%82%AF%E3%83%88/31-

３Dゲームで壁抜けしないコリジョンを実装してみた
https://qiita.com/HiShiG/items/a49ea295254318625c35

【Unity】”FPSみたいなカメラの実装”
https://qiita.com/Nekomasu/items/0a6aaf1f595cf05fbd0d

UnityのTerrainで大地を創る
https://qiita.com/yando/items/ef76c200bb50005170d5

進行方向に弾を発射する
https://qiita.com/Eureka/items/e0ca38903d7b56a29dcc

#動画
https://www.youtube.com/watch?v=HeuuuUme3Jc

※がんばれゴエモンシリーズは Konami Degital Entertainment の登録商品です