daybreaksnow's diary

私を私と呼びたい

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

  • こまごまとした修正
    • スペカ名を右上に表示するように
    • ボス撃破エフェクト追加
    • ボス撃破後、少し待ってからクリア画面に行くように
    • ボスマーカーを追加
  • スペカ2実装。時止め処理追加
    単にグローバルなところに時止めフラグを追加し、ボス以外のUpdateでそのフラグを見て何もしないだけ。
    見栄えを考慮し、時止め中はボムが敵弾を消さないように(次にUpdateで処理をする際にDestroyするように)
  • WebGL用のビルド設定を追加
    ここで問題が発生。
    何故かProject Setting...→Player→Resolutionで指定したサイズになってくれない。
    ChromeでもFireFoxでも同じ。
    開発者ツールで見ると、gameControllerのdivのwidth、heightは指定したサイズ(1024*768)になっているが、Canvasのサイズが1163、872になってしまっている。
    Standaloneでビルドしてexeで実行すると意図した画面サイズになる。 これはググっても同様の症状が見当たらず解決していない。
    暫定対応として、以下を行った
  • index.htmlにCanvasのサイズ調整を行うボタンを追加
    参考:UnityのWebGLビルドのHTMLテンプレート|npaka|note WebGLTemplatesフォルダを作成し、ビルド時に任意のhtmlを使えるように。
    ここにボタンを配置し、クリック時に以下が実行されるように
function resize(canvas) {
    var canvas = document.getElementById('#canvas');
    canvas.width = 1024;
    canvas.height = 768;
    canvas.clientWidth = 1024;
    canvas.clientHeight = 768;
}
  • タイトルからゲーム開始時にScreen.widthと想定する幅を比較して違ったらダイアログを表示し、リサイズボタンを押させる
    上記の対応でゲーム部分は動作するが、ボタンの当たり判定がずれるという問題がある。
    これは致命的ではないので無視した。
    ただし、キーボードで常に選択できるようにはした
        if (UnityEngine.EventSystems.EventSystem.current.currentSelectedGameObject == null)
        {
            UnityEngine.EventSystems.EventSystem.current.SetSelectedGameObject(titleButton.gameObject);
        }

出来上がったのが以下

https://daybreaksnow.github.io

クオリティを上げるべきところはたくさんあるが、今回はマリン船長の誕生日お祝い企画ということで、いったんここで終わり。

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

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

soundPlayer = new GameObject("SoundPlayer");
audioSource = soundPlayer.AddComponent<AudioSource>();
AudioClip clip = (AudioClip)Resources.Load("Audio/" + audioName);
audioSource.PlayOneShot(clip);
  • 被弾時に徐々に消滅→画面下から出現、となるように
    PlayerにStateパターンを適用。 通常時→消滅中(くらいボム待ち)→復活中、の3ステートに
    ・PlayerのUpdate
    void Update()
    {
        state.Update();
        // 無敵時間制御
        if (invinsibleTime > 0)
        {
            invinsibleTime--;
            if (invinsibleTime == 0)
            {
                //透過状態をもとに戻す
                setAlpha(1.0f);
            }
        }
        //ボム時間制御
        if (bombModeTime > 0)
        {
            bombModeTime--;
        }
    }

・消滅待ちのState

        public void Update()
        {
            // ボムのみ使用可能
            if (player.isUseBomb())
            {
                player.bomb();
                // サイズをもとに戻す
                player.transform.localScale = new Vector3(1, 1, 1);
                // 通常モードに戻す
                player.state = new NormalState(player);
                Debug.Log("食らいボム成功");
                return;
            }
            // 徐々にサイズを小さくする。猶予は20Fくらいとして、0.05ずつ減らす
            Vector3 newScale = new Vector3(player.transform.localScale.x - 0.05f, player.transform.localScale.y - 0.05f, player.transform.localScale.z);
            // 消滅したら復活モードに移行
            if (newScale.x <= 0)
            {
                // サイズをもとに戻す
                player.transform.localScale = new Vector3(1, 1, 1);
                // 画面外の中央下に配置
                float x = (GameManager.getLeftByScreen() + GameManager.getRightByScreen()) / 2;
                float y = GameManager.getBottomByScreen() - GameManager.GAME_AREA_HEIGHT / 8;
                Vector3 newPos = GameManager.toWorldPoint(x, y);
                player.transform.position = newPos;
                if (player.life > 0)
                {
                    // 残機を減らす
                    player.life--;
                    gameManager.loseLifeIcon();
                    // ボム数を戻す
                    gameManager.clearBombIcon();
                    player.initBombCount();
                    // 復活モードに移行
                    player.state = new RecoverState(player);
                }
                else
                {
                    //タイトルに戻る
                    SceneManager.LoadScene("TitleScene");
                }
                return;
            }
            player.transform.localScale = newScale;
            Debug.Log(player.transform.localScale);
        }

・復帰中のState

        public void Update()
        {
            // 少しずつ画面上に上がる
            Vector3 move = new Vector3(0, speed, 0);
            player.transform.Translate(move);
            // 規定位置まで上がったら通常モードに戻す
            if (player.transform.position.y > baseY)
            {
                player.state = new NormalState(player);
            }
        }
  • 残機、ボムの残数の表示を追加
  • ボムの発射処理と敵弾の消滅処理を追加
  • メインシーンのBGM追加
            AudioSource mainBGMSource = soundPlayer.AddComponent<AudioSource>();
            mainBGMSource.loop = true;
            mainBGMSource.volume = mainBGMSource.volume / 5; //BGMなので小さめに
            AudioClip clip = (AudioClip)Resources.Load("Audio/" + "Main");
            mainBGMSource.clip = clip;
            mainBGMSource.Play();

昨日とあまり変わりはないが画面キャプチャ

f:id:daybreaksnow:20200725002626p:plain

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

マリン船長(Marine Ch. 宝鐘マリン - YouTube)にはまって何かコンテンツを作ろうと思い、学生時代に作ったことのあるSTGならサクッと作れるだろう、ということで開始。船長は東方好きなのでSTGシナジーもあるし。

利用するUnityのバージョンは2019.4.4。
事前知識としては以下のブログを読んだ程度。
Unityで0から2Dアクションバトルゲームを作ろう!①ゲーム画面の作成

今日やったことは以下

再配布禁止の素材を使うこともあると思うので、private repositoryで作成した

    private void fixPosition()
    {
        // 左端
        float width = GetComponent<SpriteRenderer>().bounds.size.x;
        float left = transform.position.x - width / 2;
        if (left < gameManager.GAME_AREA_LEFT)
        {
            transform.position = new Vector3(gameManager.GAME_AREA_LEFT + width / 2, transform.position.y, transform.position.z);
        }
        //右端
        float right = transform.position.x + width / 2;
        if (right > gameManager.GAME_AREA_RIGHT)
        {
            transform.position = new Vector3(gameManager.GAME_AREA_RIGHT - width / 2, transform.position.y, transform.position.z);
        }
        // 上端
        float height = GetComponent<SpriteRenderer>().bounds.size.y;
        float top = transform.position.y + height / 2;
        if (top > gameManager.GAME_AREA_TOP)
        {
            transform.position = new Vector3(transform.position.x, gameManager.GAME_AREA_TOP - height / 2, transform.position.z);
        }
        // 下端
        float bottom = transform.position.y - height / 2;
        if (bottom < gameManager.GAME_AREA_BOTTOM)
        {
            transform.position = new Vector3(transform.position.x, gameManager.GAME_AREA_BOTTOM + height / 2, transform.position.z);
        }

    }
  • 自機の弾画像作成、自機の弾発射処理作成
    updateの処理は単純にこれだけ
        //画面上にしか行かない前提
        Vector2 moveVector = new Vector2(0, speed);
        transform.Translate(moveVector);
        // 完全に画面外に出たら破棄
        if (!GetComponent<Renderer>().isVisible)
        {
            Destroy(gameObject);
        }
  • 背景画像のゲーム領域を透過するように
    弾がゲーム領域外に描画されないよう、ゲーム領域背景→プレイヤー等→全体背景と描画されるように。そのために全体背景のゲーム領域部分を塗りつぶして透過させた

ここまでの画面キャプチャはこんな感じ

f:id:daybreaksnow:20200723233033p:plain

[Redmine]Redmineのマイグレーションを行う

CentOS6からCentOS7にRedmineマイグレーションし、利用するDBMSMySQLからPostgreSQLに変更した際の作業。

マイグレーション
  • CentOS6
  • Redmine2.4
  • MySQL 14.14
マイグレーション
  • CentOS7
  • Redmine3.4.2
  • PostgreSQL9.2

手順

マイグレーションに失敗してもよいよう、検証用マシンを用意して以下の手順で行った。

Redmineのアップグレード

1. 検証機にマイグレーション元と同じRedmineの環境を用意、DBのダンプ、添付ファイルを復元
過去の記事の通り
[Linux]RedmineのDBのバックアップを取る - daybreaksnow's diary
2. 検証機のRedmineマイグレーション先のRedmineのバージョンにアップグレード
こちらを参考
アップグレード — Redmine Guide 日本語訳
2-1. Redmineのダウンロード、解凍、配置

wget http://www.redmine.org/releases/redmine-3.2.4.tar.gz
tar xvzf redmine-3.2.4.tar.gz
mv redmine-3.2.4 /var/lib/redmine-3.2.4

2-2. DB設定のコピー

cp -a /var/lib/redmine/config/configuration.yml /var/lib/redmine-3.2.4/config/configuration.yml
cp -a /var/lib/redmine/config/database.yml /var/lib/redmine-3.2.4/config/database.yml 

2-3. 添付ファイルコピー

cp -ar /var/lib/redmine/files/ /var/lib/redmine-3.2.4/

2-4. プラグインコピー

cp -ar /var/lib/redmine/plugins/ /var/lib/redmine-3.2.4/ 

2-5. 依存するツールをインストール

bundle install --without development test
bundle exec rake generate_secret_token

2-6. DBのマイグレーション

bundle exec rake db:migrate RAILS_ENV=production

2-7. プラグイン用DBのマイグレーション

bundle exec rake redmine:plugins:migrate RAILS_ENV=production

2-8. キャッシュクリア

bundle exec rake tmp:cache:clear tmp:sessions:clear

2-9.「RackBaseURI /redmine-3.2.4」の行を追加

vi /etc/httpd/conf.d/passenger.conf

2-10. httpdに公開

ln -s /var/lib/redmine-3.2.4/public redmine-3.2.4

2-7.のプラグイン用DBのマイグレーションでタグ付けプラグインがこけたので、そのプラグインは削除して実行した(対して使っていなかったので)

データベースの移行

MySQLダンプをPostgreSQL用に変換して取得することを考えていたが、RailsのDBはyaml_dbを使うとプレインテキストでバックアップが取れるということなのでyaml_dbを使った。
参考:
yaml_dbを使ってMySQLからPostgreSQLにRedmineを移行した - 烏龍茶より麦茶派です

1. yaml_dbのインストール
1-1. /var/lib/redmine/Gemfileに以下の行を追加

gem "yaml_db"

1-2. /var/lib/redmineで以下のコマンドを実行

bundle update
bundle install --without development test

2. yaml_dbでダンプを取る

RAILS_ENV=production bundle exec rake db:data:dump

これでredmine/db/data.ymlができる。これをマイグレーション先の同ディレクトリに持っていく
3. マイグレーション先で復元

RAILS_ENV=production bundle exec rake db:data:load

ここで2-7.で削除したプラグインが使っていたテーブルが復元できないと言われたので、手動でdata.ymlからそのテーブルの情報を削除した。

[Subversive]削除された履歴を復元する

削除→新規追加などしてしまい、過去の履歴が辿れなくなった場合の対応。
逆マージを使う。

状況

リビジョン20でファイル追加
リビジョン21でファイル更新
リビジョン22でファイル削除&同名ファイルを新規追加

1.逆マージ

チーム→マージを選択して、改訂で取り消したいリビジョン(22)を選択
f:id:daybreaksnow:20140411184549p:plain

Reverse mergeにチェックし、OK
f:id:daybreaksnow:20140411184601p:plain

成功すると、対象ファイルのマークが変わる
f:id:daybreaksnow:20140411184746p:plain

2.修正を反映

マージされたファイルにrev22の修正を反映

3.コミット

履歴を確認すると、20,21,23となっている。

f:id:daybreaksnow:20140411185029p:plain

なお、逆マージのコマンドは以下の通りとなる。

svn merge -c -8 .

[SVN]さくらのVPSにSVNリポジトリを立てる

参考:
Subversion環境をCentOSで構築 - 無理なものは無理
10分で作る、Subversionレポジトリ (CentOS 版) - ishikawa84g's blog

mod_dav_svnのインストール

必要なパッケージがインストールされているか確認

yum list installed | grep subversion
yum list installed | grep mod_dav_svn

subversionはインストール済みだったが、mod_dav_svnはなかった。

インストールする。

yum install mod_dav_svn

SVNリポジトリの作成

テスト用にsandboxというリポジトリを作る。trunkとかbranchはひとまず無視。

mkdir /var/www/svn
svnadmin create /var/www/svn/sandbox
chown -R apache:apache /var/www/svn

Apache の設定(Basic 認証)

BASIC認証用のユーザ、パスワード設定

htpasswdコマンドを使って、ユーザ、パスワードを設定する

htpasswd -c /etc/httpd/.htpasswd ユーザ名 

実行後、パスワードが聞かれるので入力。
パスワードはデフォルトではcrypt関数で暗号化されて保存される。(Cのcrypt関数のこと?)

  • cは新規ファイル作成という意味なので、複数ユーザを追加する際は、-cは除く。

-cが付いていると、既存のファイルがあっても上書きされてしまうので注意。

apacheSVNの連携

mod_dav_svnのインストールにより、以下のファイルが作成されている。これを編集する。

/etc/httpd/conf.d/subversion.conf

・Locationタグをコメントアウト
・Locationタグのパスを、アクセスしたいURLにする(reposをsvnに変更した)
・AuthUserFileのパスをさっき作ったパスに変更

<Location /svn>
   DAV svn
   SVNParentPath /var/www/svn

#   # Limit write permission to list of valid users.
#   <LimitExcept GET PROPFIND OPTIONS REPORT>
#      # Require SSL connection for password protection.
#      # SSLRequireSSL

      AuthType Basic
      AuthName "Authorization Realm"
      AuthUserFile /etc/httpd/.htpasswd
      Require valid-user
#   </LimitExcept>
</Location>

httpdを再起動すれば終わり。

今回の例だと、http://(IPアドレス)/svn/sandboxにアクセスすればリポジトリの中身が見える。