daybreaksnow's diary

私を私と呼びたい

Unityで2D STGを作る(3日目)

  • ボスの被ダメージ処理を追加
    • IPlayerShotインタフェースを作成し、自機弾、ボムに実装させる
    • ボスは被弾時にGameObjectからIPlayerShotを取得してダメージを取得
  • ボスの残ライフ表示バー作成
    // 残ライフに応じてバーの長さを調整
    private void updateLifeBarLength()
    {
        float maxLife = modeLifeMap[restModeNum];
        float per = life / maxLife;
        Vector3 newScale = new Vector3(per, lifeBarObj.transform.localScale.y, lifeBarObj.transform.localScale.z);
        lifeBarObj.transform.localScale = newScale;
        Debug.Log("newScale:" + newScale);
    }
    // 画像はpositionを中心にして描画されるので、画像の大きさに応じて位置をずらす
    private void updateLifeBarPos()
    {
        float leftX = GameManager.getLeftByScreen() + 50;
        float barWidth = lifeBarObj.GetComponent<SpriteRenderer>().bounds.size.x;
        Vector3 worldPos = GameManager.toWorldPoint(leftX + barWidth / 2, 0);
        lifeBarObj.transform.position = new Vector3(worldPos.x, lifeBarObj.transform.position.y, lifeBarObj.transform.position.z);
        Debug.Log("newPos:" + lifeBarObj.transform.position);
    }
  • ボスの残形態数を表すバーを作成
    public void addRestModeBar(int index)
    {
        // 画像サイズを使いたいので一旦インスタンス化
        GameObject icon = gameManager.instantiate("LifeBar", lifeBarObj.transform.position);
        // 残り形態のバーの配置領域
        float baseLeft = GameManager.getLeftByScreen() + 10;
        float baseRight = GameManager.getLeftByScreen() + 40;
        float areaWidth = baseRight - baseLeft;
        // 配置領域に収まるよう長さを調整
        // バーの長さ×残り形態 + スペース×(残り形態-1)が上記に収まるように
        // barWidth * restModeNum + space * (restModeNum - 1) = areaWidth;
        // barWidth = (areaWidth - space * (restModeNum - 1)) / restModeNum;
        float space = 2;
        float barWidth = (areaWidth - space * (restModeNum - 1)) / restModeNum;
        // scaleを調整してバーの長さを調整
        float originalWidth = icon.GetComponent<SpriteRenderer>().bounds.size.x;
        float scale = barWidth / originalWidth;
        icon.transform.localScale = new Vector3(scale, icon.transform.localScale.y, icon.transform.localScale.z);
        // indexに応じて配置
        float startX = baseLeft + barWidth / 2;
        float spacing = barWidth + space;
        float x = startX + (index - 1) * spacing;
        icon.transform.position = new Vector3(GameManager.toWorldPoint(x, 0).x, icon.transform.position.y, icon.transform.position.z);
        restModeBarIcons.Push(icon);
    }
  • ボスのモード切替を追加
 void OnTriggerEnter2D(Collider2D collider)
    {
        // 自機、自機弾以外とはそもそも接触判定をしないので考慮不要
        // 自機と当たった場合は自機側で処理するので何もしない
        if (collider.CompareTag("Player"))
        {
            return;
        }
        // 無敵中は終わり
        if (isInvincible())
        {
            return;
        }
        // ボムか自機弾と当たったらダメージを受ける
        IPlayerShot shot = collider.GetComponent<IPlayerShot>();
        life -= shot.GetDamage();
        // ライフバーの長さ調整
        updateLifeBarLength();
        updateLifeBarPos();
        if (life <= 0)
        {
            if (restModeNum == 0)
            {
                // TODO 時間をおいてから
                life = 0;
                clearAllEnemyShot();
                // ゲームクリア画面へ遷移
                SceneManager.LoadScene("EndScene");
            }
            else
            {
                // 次のゲージへ
                restModeNum--;
                life = modeLifeMap[restModeNum];
                updateLifeBarLength();
                updateLifeBarPos();
                loseRestModeBar();
                BossState nextState;
                switch (restModeNum)
                {
                    case 1: nextState = new Spell1State(this); break;
                    case 0: nextState = new Spell2State(this); break;
                    default: throw new Exception("illegal restModeNum:" + restModeNum);
                }
                state = new ConnectState(this, nextState);
                // 敵弾はクリア
                clearAllEnemyShot();
            }
        }
        Debug.Log("hit Boss");
    }

    private class ConnectState : BossState
    {
        private Boss1 boss;
        private BossState nextState;

        public ConnectState(Boss1 boss, BossState nextState)
        {
            this.boss = boss;
            this.nextState = nextState;
        }

        public void Update()
        {
            // モード切替時とその後の100Fは無敵
            boss.invincibleCount = 100;
            float moveX = 0;
            float moveY = 0;
            float speed = 4;
            //初期位置に移動
            // TODO スムーズに移動するように
            Vector2 baseWorldPos = new Vector2(boss.gameManager.getCenter().x, boss.getBaseY());
            if (Math.Abs(baseWorldPos.x - boss.transform.position.x) > speed * 2)
            {
                if (baseWorldPos.x < boss.transform.position.x)
                {
                    moveX = -speed;
                }
                else
                {
                    moveX = speed;
                }
            }
            if (Math.Abs(baseWorldPos.y - boss.transform.position.y) > speed * 2)
            {
                if (baseWorldPos.y < boss.transform.position.y)
                {
                    moveY = -speed;
                }
                else
                {
                    moveY = speed;
                }
            }
            boss.transform.Translate(moveX, moveY, 0);
            // 移動が終わったら次のステートへ
            if (moveX == 0 && moveY == 0)
            {
                boss.state = nextState;
            }
        }
    }
  • キー操作で移動するように、ショットは押しっぱなしで発射するように、低速移動を追加
  • 開発中は特定キー押下で無敵になるように
    Application.isEditorで判断できる
  • Resouces.loadの結果をキャッシュするように
    必要ないかもしれないが、開発時にResource ID out of range in GetResourceのエラーが出たので
   public GameObject instantiate(String prehabName, Vector3 worldPos)
    {
        String name = "Prefabs/" + prehabName;
        GameObject prehab;
        if (prehabCache.ContainsKey(name))
        {
            prehab = prehabCache[name];
        }
        else
        {
            prehab = (GameObject)Resources.Load(name);
            prehabCache.Add(name,prehab);
        }
        return (GameObject)Instantiate(prehab, worldPos, Quaternion.identity);
    }
  • 敵弾の画像サイズに応じて当たり判定も調整されるように
    public void setSprite(string spriteName)
    {
        SpriteRenderer renderer = GetComponent<SpriteRenderer>();
        renderer.sprite = gameManager.loadSprite(spriteName);
        // XXX 画像は縦横同じサイズという想定
        float spriteSize = renderer.bounds.size.x;
        // 画像サイズに応じて当たり判定を調整
        if (spriteSize != SIZE)
        {
            float scale = spriteSize / (float)EnemyShot.SIZE;
            GetComponent<CircleCollider2D>().radius = GetComponent<CircleCollider2D>().radius * scale;
        }
    }
  • ボスの通常弾幕を作成
private class Normal1State : BossState
    {
        private Boss1 boss;
        private GameManager gameManager;
        private float startAngle = 270; //真下
        private Vector2 moveVector;
        public Normal1State(Boss1 boss)
        {
            this.boss = boss;
            this.gameManager = boss.gameManager;
        }

        private int count;
        public void Update()
        {
            // 0~100は何もしない
            if (count < 100)
            {
            }
            // nWay弾を角度を変えて定期的に撃つだけ
            else if (count < 200)
            {
                if (count % 15 == 0)
                {
                    int n = 16;
                    for (int i = 0; i < n; i++)
                    {
                        float angle = startAngle + (360.0f / n) * i;
                        float speed = 10;
                        Vector2 moveVector = GameManager.getMoveVector(speed, angle);
                        GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position);
                        enemyShot.GetComponent<EnemyShot>().init(gameManager, new StraightShotStrategy(boss.gameManager, enemyShot, moveVector));
                    }
                    startAngle += 5;
                }
            }
            // 200~300は何もしない
            if (count < 300)
            {
            }
            // 300になったらランダムな方向に移動開始
            else if (count == 300)
            {
                float speed = 0.5f;
                float angle = UnityEngine.Random.Range(0, 360);
                moveVector = GameManager.getMoveVector(speed, angle);
                // ゲーム領域外に近寄らなすぎないように調整
                moveVector = boss.fixMoveVector(moveVector);
            }
            else if (count <= 400)
            {
                // 300~400は移動しながらショット
                boss.transform.Translate(moveVector);
                if (count % 15 == 0)
                {
                    int n = 16;
                    for (int i = 0; i < n; i++)
                    {
                        float angle = startAngle + (360.0f / n) * i;
                        float speed = 10;
                        Vector2 moveVector = GameManager.getMoveVector(speed, angle);
                        GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position);
                        enemyShot.GetComponent<EnemyShot>().init(gameManager, new StraightShotStrategy(boss.gameManager, enemyShot, moveVector));
                    }
                    startAngle -= 5;
                }
            }
            count++;
            // 400になったら最初に戻る
            if (count == 400)
            {
                count = 0;
            }
        }
    }
  • ボスのスペカ1を作成
    体力半分以上:nWay弾が画面端近くに行ったら戻ってくる(実際は指定フレーム数で戻るようにしてるだけで細かな調整によるもの)
    体力1/4以上:回転するnWay弾が画面端近くに行ったら戻ってくる(時計周りと反時計回りの繰り返し)
    体力1/4未満:時計回りと反時計周りのnWay戻り弾を同時に発射+ゆっくりとした自機狙い弾
    private class Spell1State : BossState
    {
        private Boss1 boss;
        private GameManager gameManager;
        private float maxLife;
        private float prevLife;
        private Spell1SubState subState;
        public Spell1State(Boss1 boss)
        {
            this.boss = boss;
            this.gameManager = boss.gameManager;
            maxLife = boss.life;
            prevLife = boss.life;
            subState = new FirstState(boss);
        }

        private interface Spell1SubState
        {
            void Update();
        }
        //体力半分以上
        private class FirstState : Spell1SubState
        {
            private int count;
            private float startAngle = 270; //真下
            private Vector2 moveVector;

            private Boss1 boss;
            private GameManager gameManager;
            public FirstState(Boss1 boss)
            {
                this.boss = boss;
                this.gameManager = boss.gameManager;
            }

            public void Update()
            {
                // 0~100は何もしない
                if (count < 100)
                {
                }
                // nWay弾を角度を変えて定期的に撃つだけ
                else if (count < 200)
                {
                    if (count % 20 == 0)
                    {
                        int n = 16;
                        const int stopStartCount = 75;
                        const int stopCount = 45;
                        const int slowCount = stopStartCount * 3 / 10;
                        const float returnSpeedRate = 0.8f;

                        for (int i = 0; i < n; i++)
                        {
                            float angle = startAngle + (360.0f / n) * i;
                            float speed = 9f;
                            Vector2 moveVector = GameManager.getMoveVector(speed, angle);
                            GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position);
                            enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnStraightShotStrategy(boss.gameManager, enemyShot, moveVector, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.GRAY_SPHERE_48));
                            enemyShot.GetComponent<EnemyShot>().setSprite(ImageManager.BLACK_SPHERE_48);
                        }
                        startAngle += 6;

                    }
                }
                // 200~300は何もしない
                if (count < 300)
                {
                }
                // 300になったらランダムな方向に移動開始
                else if (count == 300)
                {
                    float speed = 0.5f;
                    float angle = UnityEngine.Random.Range(0, 360);
                    moveVector = GameManager.getMoveVector(speed, angle);
                    // ゲーム領域外に近寄らなすぎないように調整
                    moveVector = boss.fixMoveVector(moveVector);
                }
                else if (count <= 400)
                {
                    // 300~400は移動しながらショット
                    boss.transform.Translate(moveVector);
                    if (count % 20 == 0)
                    {
                        int n = 16;
                        const int stopStartCount = 75;
                        const int stopCount = 45;
                        const int slowCount = stopStartCount * 3 / 10;
                        const float returnSpeedRate = 0.8f;

                        for (int i = 0; i < n; i++)
                        {
                            float angle = startAngle + (360.0f / n) * i;
                            float speed = 9f;
                            Vector2 moveVector = GameManager.getMoveVector(speed, angle);
                            GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position);
                            enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnStraightShotStrategy(boss.gameManager, enemyShot, moveVector, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.GRAY_SPHERE_48));
                            enemyShot.GetComponent<EnemyShot>().setSprite(ImageManager.BLACK_SPHERE_48);
                        }
                        startAngle -= 6;
                    }
                }
                count++;
                // 400になったら最初に戻る
                if (count == 400)
                {
                    count = 0;
                }
            }
        }

        //体力半分以下、1/4以上
        private class SecondState : Spell1SubState
        {
            private int count;
            private float startAngle = 270; //真下
            private Vector2 moveVector;

            private Boss1 boss;
            private GameManager gameManager;
            // 
            private int straightAngleSign = 1;
            public SecondState(Boss1 boss)
            {
                this.boss = boss;
                this.gameManager = boss.gameManager;
            }

            public void Update()
            {
                // 0~100は何もしない
                if (count < 100)
                {
                }
                else if (count < 250)
                {
                    if (count % 35 == 0)
                    {
                        int n = 16;
                        const float speed = 7.5f;
                        //反時計回り
                        const float angleSpeed = -1;
                        const int stopStartCount = 90;
                        const int stopCount = 45;
                        const int slowCount = 25;
                        const float returnSpeedRate = 0.8f;

                        for (int i = 0; i < n; i++)
                        {
                            float initialAngle = startAngle + (360.0f / n) * i;
                            GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position);
                            enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnRollingShotStrategy(boss.gameManager, enemyShot, speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.GRAY_SPHERE_48));
                            enemyShot.GetComponent<EnemyShot>().setSprite(ImageManager.BLACK_SPHERE_48);
                        }
                        startAngle += 6;
                    }
                }
                // 250~350は何もしない
                if (count < 350)
                {
                }
                // 350になったらランダムな方向に移動開始
                else if (count == 350)
                {
                    float speed = 0.5f;
                    float angle = UnityEngine.Random.Range(0, 360);
                    moveVector = GameManager.getMoveVector(speed, angle);
                    // ゲーム領域外に近寄らなすぎないように調整
                    moveVector = boss.fixMoveVector(moveVector);
                }
                else if (count <= 500)
                {
                    // 350~500は移動しながらショット
                    boss.transform.Translate(moveVector);
                    if (count % 30 == 0)
                    {
                        int n = 16;
                        const float speed = 7.5f;
                        //時計回り
                        const float angleSpeed = 1;
                        const int stopStartCount = 90;
                        const int stopCount = 45;
                        const int slowCount = 25;
                        const float returnSpeedRate = 0.8f;

                        for (int i = 0; i < n; i++)
                        {
                            float initialAngle = startAngle + (360.0f / n) * i;
                            GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position);
                            enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnRollingShotStrategy(boss.gameManager, enemyShot, speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.GRAY_SPHERE_48));
                            enemyShot.GetComponent<EnemyShot>().setSprite(ImageManager.BLACK_SPHERE_48);
                        }
                        startAngle += 6;
                    }
                }
                count++;
                // 500になったら最初に戻る
                if (count == 500)
                {
                    count = 0;
                    straightAngleSign = -straightAngleSign;
                }
            }
        }

        private class ThirdState : Spell1SubState
        {
            private int count;
            private float startAngle = 270; //真下
            private Vector2 moveVector;

            private Boss1 boss;
            private GameManager gameManager;
            public ThirdState(Boss1 boss)
            {
                this.boss = boss;
                this.gameManager = boss.gameManager;
            }

            public void Update()
            {
                // 0~100は何もしない
                if (count < 100)
                {
                }
                // 100になったらランダムな方向に移動開始
                else if (count == 100)
                {
                    float speed = 0.5f;
                    float angle = UnityEngine.Random.Range(0, 360);
                    moveVector = GameManager.getMoveVector(speed, angle);
                    // ゲーム領域外に近寄らなすぎないように調整
                    moveVector = boss.fixMoveVector(moveVector);
                }
                else if (count < 300)
                {
                    // 100~300は移動しながらショット
                    boss.transform.Translate(moveVector);
                    if (count % 30 == 0)
                    {
                        int n = 12;
                        const float speed = 5.5f;
                        const float angleSpeed = 0.5f;
                        const int stopStartCount = 135;
                        const int stopCount = 45;
                        const int slowCount = 30;
                        const float returnSpeedRate = 0.8f;

                        for (int i = 0; i < n; i++)
                        {
                            float initialAngle = startAngle + (360.0f / n) * i;
                            // 見栄えを考慮して描画順序を調整
                            if (i % 2 == 0)
                            {
                                createRetrunRollingShot(speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.RED_SPHERE_48, ImageManager.GRAY_SPHERE_48);
                                createRetrunRollingShot(speed, initialAngle, -angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.BLUE_SPHERE_48, ImageManager.GRAY_SPHERE_48);
                            }
                            else
                            {
                                createRetrunRollingShot(speed, initialAngle, -angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.BLUE_SPHERE_48, ImageManager.GRAY_SPHERE_48);
                                createRetrunRollingShot(speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, ImageManager.RED_SPHERE_48, ImageManager.GRAY_SPHERE_48);
                            }
                        }
                        startAngle -= 6;
                    }
                }
                count++;
                // 定期的に自機狙い弾を放つ
                if (count % 200 == 150)
                {
                    float speed = 0.75f;
                    float angle = GameManager.getPlayerAngle(boss.transform.position);
                    Vector2 moveVector = GameManager.getMoveVector(speed, angle);
                    GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position);
                    enemyShot.GetComponent<EnemyShot>().init(gameManager, new StraightShotStrategy(boss.gameManager, enemyShot, moveVector));
                }
                // 500になったら最初に戻る
                if (count == 500)
                {
                    count = 0;
                }
            }

            private GameObject createRetrunRollingShot(float speed, float initialAngle, float angleSpeed, int stopStartCount, int stopCount, int slowCount, float returnSpeedRate, String spriteName, String returnSpriteName)
            {
                GameObject enemyShot = gameManager.createEnemyShot("EnemyShot", boss.transform.position);
                enemyShot.GetComponent<EnemyShot>().init(gameManager, new ReturnRollingShotStrategy(boss.gameManager, enemyShot, speed, initialAngle, angleSpeed, stopStartCount, stopCount, slowCount, returnSpeedRate, returnSpriteName));
                enemyShot.GetComponent<EnemyShot>().setSprite(spriteName);

                return enemyShot;
            }
        }

        public void Update()
        {
            float firstLife = maxLife / 2;
            float secondLife = maxLife / 4;
            if (prevLife >= firstLife && boss.life < firstLife)
            {
                subState = new SecondState(boss);
            }
            else if (prevLife >= secondLife && boss.life < secondLife)
            {
                subState = new ThirdState(boss);
            }
            subState.Update();
            prevLife = boss.life;
        }
    }

スペカ1のラストのキャプチャ f:id:daybreaksnow:20200726025630p:plain