LoginSignup
2
2

More than 1 year has passed since last update.

Unityでドクターマリオ風落ちものパズルの作り方

Posted at

Unity1週間ゲームジャム「そろえる」

unityroomで開催されていたイベント『Unity1週間ゲームジャム』に参加して、以下のゲームを投稿しました。

今回のゲームジャムではドクターマリオのゲームシステムを再現することに注力したのですが、

せっかくなのでその内容を解説したいと思います。

ゲームの内容については本家任天堂のドクターマリオをプレイするか、
上のツイートリンクから『Dr.TSUKUSHI』をプレイしてみてください(宣伝)。

参考にしたもの

今回、落ちものパズルを作るのが初めてだったので、
いろいろと調べてどうやって作るか事前準備をしていたのですが、
以下のnote記事が一番参考になりました。

ジャンル別ゲームの作り方 (落ち物パズル)

対象読者

Unityの使い方はもちろん、コルーチンなどを使用しますがそれらの解説は省きますので
Unityでゲームを制作した経験のある方への解説となります。

開発環境

Unity2020.3.25f1

サンプルプロジェクト

サンプルプロジェクトをGitHubに公開しています。
DrTsukushiSample

1. 準備

まず、カメラと画像の準備をします。

2Dドット絵ゲーム用にパッケージマネージャーから『2D Pixel Perfect』をインポートして、
MainCameraに「PixelPerfectCamera」をアタッチします。

PixelPerfectCameraのインスペクターの設定項目には、
「Assets Pixels Per Unit」には 1。
今回はゲームボーイアドバンスを意識していたので、
「Reference Resolustion」には X:240 Y:160。
さらに「Upscale Render Texture」「Crop Frame X,Y」にチェックを入れます。
bandicam 2022-05-24 23-23-09-321.png

画像としてステージ用の瓶、ウィルス3種類、カプセル3種類(連結時3種類、単独時3種類の計6種類)を用意し、
各画像の設定としてインスペクターで
「Pixels Per Unit」には 1(PixelPerfectCameraの数値と合わせることで、これによりTransformのPositionで1動かすごとに1ドットずつ動くようになります)。
「Filter Mode」は「Point (no filter)」にします。
bandicam 2022-05-24 23-41-02-273.png
とりあえず、瓶画像を中央に配置します。

また、ここから先に登場するスクリプトは、すべてMonoBehaviourを継承したスクリプトです。

2. ブロック

まず、プレイヤーが操作するカプセルや配置されたウィルスなどを制御する「Block.cs」と「Block.prefab」を作成します。

ブロックが配置できる最上段、左端の座標と各ブロックの間隔を以下のように定義します。

Block.cs
    const float LEFT_LIMIT_POS = -28f;
    const float UPPER_LIMIT_POS = 44f;
    const float BLOCK_DISTANCE = 8f;

BLOCK_DISTANCEが8fなのはブロックの画像サイズが「8x8」のためです。

ブロックの種類、回転状況、移動先をenum化します。

Block.cs
    // ブロックの種類
    public enum BlockType
    {
        CapRed,
        CapBlue,
        CapYellow,
        SingleCapRed,
        SingleCapBlue,
        SingleCapYellow,
        VirusRed,
        VirusBlue,
        VirusYellow,
    }

    // ブロックの回転状況
    public enum BlockRota
    {
        None,
        Up,
        Left,
        Down,
        Right,
    }

    // ブロックの移動先
    public enum BlockMove
    {
        Up,
        Left,
        Down,
        Right,
    }

インスペクターからリンクするフィールドとして以下を用意します。

Block.cs
    [SerializeField]
    SpriteRenderer renderer;
    [SerializeField]
    Sprite[] sprites;

spritesにはウィルス、カプセルの画像をBlockTypeの順番通りにリンクさせます。
bandicam 2022-05-29 12-36-13-087.png
以下のプロパティを用意します。

Block.cs
    // ブロック位置(0 = X位置, 1 = Y位置)
    public int[] BlockPos { get; set; }

    public const int BLOCK_POS_X_IDX = 0;
    public const int BLOCK_POS_Y_IDX = 1;

    // 現在のブロック種類
    public BlockType CurrentBlockType { get; private set; }

    // 現在のブロック回転状況
    public BlockRota CurrentBlockRota { get; private set; }

    // ブロックの消去を行うか
    public bool IsErase { get; set; }

    // ブロックの降下を行うか
    public bool IsFall { get; set; }

指定されたブロックの種類ごとにスプライトの設定を行うメソッドを用意します。

Block.cs
    // ブロック変化
    public void ChangeBlock(BlockType type)
    {
        CurrentBlockType = type;

        switch (CurrentBlockType)
        {
            case BlockType.CapRed: renderer.sprite = sprites[(int)BlockType.CapRed]; break;
            case BlockType.CapBlue: renderer.sprite = sprites[(int)BlockType.CapBlue]; break;
            case BlockType.CapYellow: renderer.sprite = sprites[(int)BlockType.CapYellow]; break;
            case BlockType.SingleCapRed: renderer.sprite = sprites[(int)BlockType.SingleCapRed]; break;
            case BlockType.SingleCapBlue: renderer.sprite = sprites[(int)BlockType.SingleCapBlue]; break;
            case BlockType.SingleCapYellow: renderer.sprite = sprites[(int)BlockType.SingleCapYellow]; break;
            case BlockType.VirusRed: renderer.sprite = sprites[(int)BlockType.VirusRed]; break;
            case BlockType.VirusBlue: renderer.sprite = sprites[(int)BlockType.VirusBlue]; break;
            case BlockType.VirusYellow: renderer.sprite = sprites[(int)BlockType.VirusYellow]; break;
        }
    }

指定された回転状態にするメソッドを用意します。

Block.cs
    // ブロック回転
    private void Rotate(BlockRota rota)
    {
        CurrentBlockRota = rota;

        switch (CurrentBlockRota)
        {
            case BlockRota.Up: transform.localRotation = Quaternion.Euler(0, 0, 0); break;
            case BlockRota.Left: transform.localRotation = Quaternion.Euler(0, 0, 90); break;
            case BlockRota.Down: transform.localRotation = Quaternion.Euler(0, 180, 180); break;
            case BlockRota.Right: transform.localRotation = Quaternion.Euler(0, 180, 90); break;
        }
    }

Blockプレハブ実体化時の初期セッティング用メソッドを用意します。

Block.cs
    // 初期セッティング
    public void Setting(BlockType type, BlockRota rota, int x, int y)
    {
        // ブロックを画面上に配置
        float bx = LEFT_LIMIT_POS + BLOCK_DISTANCE * x;
        float by = UPPER_LIMIT_POS - BLOCK_DISTANCE * y;
        transform.localPosition = new Vector3(bx, by);

        BlockPos = new int[] { x, y };

        ChangeBlock(type);

        Rotate(rota);
    }

とりあえずBlockプレハブは以上の実装として、後から必要に応じて追加します。

3. ブロックマネージャー

ブロックの生成やブロックの消去、落下処理を制御する「BlockManager.cs」を作成します。

ステージである瓶に配置できるブロック数を以下のように定義します。

BlockManager.cs
    const int HORIZONTAL_NUM = 8;  // 横列数
    const int VERTICAL_NUM = 15;   // 縦列数

インスペクターからリンクするフィールドとして以下を用意します。

BlockManager.cs
    [SerializeField]
    GameObject blockPrefab;
    [SerializeField]
    Player player;

blockPrefabには先ほど作成した「Block.prefab」をリンクします。
「Player」スクリプトは後で作成します。

また、以下のフィールドを追加します。

BlockManager.cs
    // ブロック配置情報
    Block[,] blockInfo = new Block[HORIZONTAL_NUM, VERTICAL_NUM];

    bool isErase;  // ブロックの消去があるか
    bool isFall;   // ブロックの降下があるか

    // 瓶の中のウィルスリスト(ゲームクリア判定用)
    List<Block> virusBlockList = new List<Block>();

初めに配置されたブロック情報を取得するメソッドと瓶の外か判定するメソッドを用意します。

BlockManager.cs
    // 配置されたブロック情報を取得
    private Block GetBlockInfo(int x, int y)
    {
        if (IsOutBottle(x, y))
        {
            return null;
        }

        return blockInfo[x, y];
    }

    // 瓶の外か判定
    private bool IsOutBottle(int x, int y)
    {
        return (x < 0 || x >= HORIZONTAL_NUM || y < 0 || y >= VERTICAL_NUM);
    }

上のメソッドをBlockスクリプトが利用できるようにFunc型のフィールドをBlock.csに追加します。

Block.cs
    Func<int, int, Block> GetBlockInfo;  // 配置ブロック情報取得用
    Func<int, int, bool> IsOutBottle;    // 瓶の外か判定用

さらにSettingメソッドに引数を追加してBlockManagerスクリプトから受け取れるようにします。

Block.cs
    public void Setting(BlockType type, BlockRota rota, int x, int y, Func<int, int, Block> getBlockInfo, Func<int, int, bool> isOutBottle)
    {
        ~省略~

        GetBlockInfo = getBlockInfo;
        IsOutBottle = isOutBottle;
    }

BlockManagerスクリプトに戻り、ブロック生成メソッドを用意します。

BlockManager.cs
    // ブロック生成
    private Block CreateBlock(Block.BlockType type, int x, int y)
    {
        // プレハブから生成
        GameObject block = GameObject.Instantiate(blockPrefab);
        block.transform.parent = transform;

        // ブロックスクリプトで初期セッティング
        Block blockCom = block.GetComponent<Block>();
        blockCom.Setting(type, Block.BlockRota.None, x, y, GetBlockInfo, IsOutBottle);

        // 配置情報に追加
        blockInfo[x, y] = blockCom;

        return blockCom;
    }

ステージ生成メソッドを用意します。

BlockManager.cs
    // ステージ生成
    private void CreateStage()
    {
        virusBlockList.Clear();

        // ウィルスブロックを生成しつつリストに追加
        virusBlockList.Add( CreateBlock(Block.BlockType.VirusRed, 2, 8) );
        virusBlockList.Add( CreateBlock(Block.BlockType.VirusBlue, 5, 10) );
        virusBlockList.Add( CreateBlock(Block.BlockType.VirusYellow, 7, 12) );
    }

ステージ生成の準備ができたところで、ヒエラルキー上に「BlockManager」オブジェクトを生成し、
そこにBlockManager.csコンポーネントをアタッチします。

とりあえずBlockManagerは以上の実装として、次のスクリプト作成に移ります。

4. プレイヤー

プレイヤー操作を制御する「Player.cs」を作成します。

プレイヤーによる入力間隔や操作ブロックの自動降下間隔を以下のように定義します。

Player.cs
    const float INPUT_INTERVAL = 0.2f;  // 入力間隔時間
    const float FALL_INTERVAL = 0.7f;   // 降下間隔時間

フィールドとして以下を用意します。

Player.cs
    Block[] playerBlock = new Block[2]; // 操作するブロック
    float inputMoveInterval = 0;        // 移動操作入力間隔カウンター
    float inputRotaInterval = 0;        // 回転操作入力間隔カウンター
    float fallInterval = 0;             // 降下間隔カウンター
    bool conflicted = false;            // 移動時に衝突したフラグ
    int moveIdx;                        // 現在の移動処理配列番号

プレイヤーのブロック操作が完了したことをBlockManagerに伝えるためのコールバックフィールドや、
BlockManagerのメソッドをPlayerスクリプトが利用できるようにFunc型のフィールドをPlayer.csに追加します。

Player.cs
    Action<Block[]> PlayerTurnEndCB;    // プレイヤー操作完了コールバック
    Func<int, int, Block> GetBlockInfo; // ブロック配置情報取得
    Func<int, int, bool> IsOutBottle;   // 瓶の外か判定

BlockManagerから呼び出す初期化用のメソッドを用意します。

Player.cs
    // 初期化
    public void Init(Action<Block[]> playerTurnEndCB, Func<int, int, Block> getBlockInfo, Func<int, int, bool> isOutBottle)
    {
        PlayerTurnEndCB = playerTurnEndCB;
        GetBlockInfo = getBlockInfo;
        IsOutBottle = isOutBottle;
    }

BlockManagerからプレイヤー操作用のブロックを渡すメソッドを用意します。

Player.cs
    // プレイヤー操作ブロックをセット
    public void SetPlayerBlock(Block[] blocks)
    {
        playerBlock[0] = blocks[0]; // 左
        playerBlock[1] = blocks[1]; // 右

        fallInterval = FALL_INTERVAL;
    }

次に毎フレーム呼ばれるメソッドを用意します。

Player.cs
    // プレイヤー操作処理
    private void PlayerControll()
    {
        // ブロックがなければ操作させない
        if (playerBlock[0] == null || playerBlock[1] == null)
        {
            return;
        }

        PlayerBlockRotaControll(); // 後で追加
        PlayerBlockMoveControll(); // 後で追加
        
        FallBlock(); // 後で追加
    }

このメソッドをMonoBehaviour由来のUpdateメソッドから呼び出します。

Player.cs
    private void Update()
    {
        PlayerControll();
    }

プレイヤーの移動入力を検知するメソッドを用意します。

Player.cs
    // ブロック移動入力検知
    private void PlayerBlockMoveControll()
    {
        // インターバル中は操作させない
        if (inputMoveInterval > 0)
        {
            inputMoveInterval -= Time.deltaTime;
            return;
        }

        // プレイヤーの移動入力検知
        float x = Input.GetAxisRaw("Horizontal");
        float y = Input.GetAxisRaw("Vertical");

        if (x != 0)
        {
            // 横入力

            if (x > 0)
            {
                // 右入力反映
                PlayerBlockMove(Block.BlockMove.Right); // 後で追加
            }
            else
            {
                // 左入力反映
                PlayerBlockMove(Block.BlockMove.Left); // 後で追加
            }

            inputMoveInterval = INPUT_INTERVAL;
        }
        else if (y != 0)
        {
            // 縦入力

            if (y < 0)
            {
                // 下入力反映
                PlayerBlockMove(Block.BlockMove.Down); // 後で追加
                inputMoveInterval = INPUT_INTERVAL / 2f;
                fallInterval = FALL_INTERVAL;
            }
        }
    }

プレイヤーの回転入力を検知するメソッドを用意します。

Player.cs
    // ブロック回転入力検知
    private void PlayerBlockRotaControll()
    {
        // インターバル中は操作させない
        if (inputRotaInterval > 0)
        {
            inputRotaInterval -= Time.deltaTime;
            return;
        }

        // プレイヤーの回転入力検知
        if (Input.GetButtonDown("Jump"))
        {
            // 回転入力反映
            PlayerBlockRotate(); // 後で追加
            inputRotaInterval = INPUT_INTERVAL;
        }
    }

プレイヤーの操作がなくても操作ブロックが自動で降下していくメソッドを用意します。

Player.cs
    // 自動降下
    private void FallBlock()
    {
        // 降下インターバル
        if (fallInterval > 0)
        {
            fallInterval -= Time.deltaTime;
            return;
        }

        // 自動降下処理
        fallInterval = FALL_INTERVAL;
        PlayerBlockMove(Block.BlockMove.Down);
    }

ブロック移動をゲームに反映するメソッドを用意します。

Player.cs
    // ブロック移動処理
    private void PlayerBlockMove(Block.BlockMove move)
    {
        conflicted = false; // 衝突フラグをオフ
        moveIdx = 1;        // 操作ブロックの配列番号を最初からにする

        // 操作ブロック2つを移動させる
        for (int i = 0; i < 2; i++)
        {
            if (playerBlock[i] == null)
            {
                break;
            }

            // 他の操作ブロックがすでに衝突しているなら以下の処理をしない
            if (conflicted)
            {
                break;
            }

            // ブロック移動
            playerBlock[i].Move(move, ConflictCallback, SuccessMoveCallback); // 後で追加
        }
    }

ブロックを移動した際に他のブロックや瓶の外に衝突した場合のコールバックメソッドを用意します。

Player.cs
    // 移動衝突時コールバック
    private void ConflictCallback(Block.BlockMove move)
    {
        if (move == Block.BlockMove.Down)
        {
            // 下移動

            // ブロックを元の位置に戻す
            for (int i = 0; i < moveIdx; i++)
            {
                playerBlock[i].Move(Block.ReverseMove(move)); // 後で追加
            }

            // プレイヤー操作完了
            PlayerTurnEndCB(playerBlock);
            playerBlock[0] = null;
            playerBlock[1] = null;
        }
        else
        {
            // 横移動

            // ブロックを元の位置に戻す
            for (int i = 0; i < moveIdx; i++)
            {
                playerBlock[i].Move(Block.ReverseMove(move)); // 後で追加
            }
        }

        // 衝突しました
        conflicted = true;
    }

ブロック移動に成功した場合のコールバックメソッドを用意します。
今回は配列番号のインクリメントのみを行っていますが、音を出すなどの追加処理を入れても良いと思います。

Player.cs
    // 移動成功時コールバック
    private void SuccessMoveCallback(Block.BlockMove move)
    {
        moveIdx++;

        // 移動音を出すとか
    }

Blockスクリプトに移動用のスクリプトを追加します。

Block.cs
    // ブロック移動
    public void Move(BlockMove move, Action<BlockMove> conflictCB = null, Action<BlockMove> successCB = null)
    {
        // 移動処理
        switch (move)
        {
            case BlockMove.Up:
                transform.localPosition += new Vector3(0, BLOCK_DISTANCE, 0);
                BlockPos[1] -= 1;
                break;

            case BlockMove.Left:
                transform.localPosition += new Vector3(-BLOCK_DISTANCE, 0, 0);
                BlockPos[0] -= 1;
                break;

            case BlockMove.Down:
                transform.localPosition += new Vector3(0, -BLOCK_DISTANCE, 0);
                BlockPos[1] += 1;
                break;

            case BlockMove.Right:
                transform.localPosition += new Vector3(BLOCK_DISTANCE, 0, 0);
                BlockPos[0] += 1;
                break;
        }

        // 移動先が瓶の外なら衝突
        if (IsOutBottle(BlockPos[0], BlockPos[1]))
        {
            if (conflictCB != null)
            {
                conflictCB(move);
                return;
            }
        }

        // 移動先にブロックがあるなら衝突
        Block moveToBlock = GetBlockInfo(BlockPos[0], BlockPos[1]);
        if (moveToBlock != null)
        {
            if (conflictCB != null)
            {
                conflictCB(move);
                return;
            }
        }

        // 移動成功
        if (successCB != null)
        {
            successCB(move);
        }
    }

移動先を反転させる静的メソッドも追加します。

Block.cs
    // 移動先の反対方向を取得
    public static BlockMove ReverseMove(BlockMove move)
    {
        switch (move)
        {
            case BlockMove.Up:
                return BlockMove.Down;

            case BlockMove.Left:
                return BlockMove.Right;

            case BlockMove.Down:
                return BlockMove.Up;

            case BlockMove.Right:
                return BlockMove.Left;
        }

        return BlockMove.Up;
    }

Playerスクリプトに戻り、ブロック回転をゲームに反映するメソッドを用意します。

Player.cs
    // ブロック回転処理
    private void PlayerBlockRotate()
    {
        if (playerBlock[0].CurrentBlockRota == Block.BlockRota.Up || playerBlock[0].CurrentBlockRota == Block.BlockRota.Down)
        {
            // カプセルの配置が縦の場合

            // 下段にあるカプセルのインデックス
            int fidx = (playerBlock[0].BlockPos[Block.BLOCK_POS_Y_IDX] > playerBlock[1].BlockPos[Block.BLOCK_POS_Y_IDX]) ? 0 : 1; 
            // 上段にあるカプセルのインデックス
            int sidx = (fidx == 0) ? 1 : 0;

            int px = playerBlock[fidx].BlockPos[Block.BLOCK_POS_X_IDX];
            int py = playerBlock[fidx].BlockPos[Block.BLOCK_POS_Y_IDX];

            if (GetBlockInfo(px + 1, py) == null && !IsOutBottle(px + 1, py))
            {
                // カプセル下段の「右」に動く先があるなら

                // 下段を右に移動
                playerBlock[fidx].Move(Block.BlockMove.Right);
                playerBlock[fidx].Rotate();
                // 上段を下に移動
                playerBlock[sidx].Move(Block.BlockMove.Down);
                playerBlock[sidx].Rotate();
            }
            else if (GetBlockInfo(px - 1, py) == null && !IsOutBottle(px - 1, py))
            {
                // カプセル下段の「左」に動く先があるなら

                // 下段は画像回転のみ
                playerBlock[fidx].Rotate();
                // 上段は左下に移動
                playerBlock[sidx].Move(Block.BlockMove.Down);
                playerBlock[sidx].Move(Block.BlockMove.Left);
                playerBlock[sidx].Rotate();
            }
        }
        else
        {
            // カプセルの配置が横の場合

            // 右にあるカプセルのインデックス
            int fidx = (playerBlock[0].BlockPos[Block.BLOCK_POS_X_IDX] > playerBlock[1].BlockPos[Block.BLOCK_POS_X_IDX]) ? 0 : 1;
            // 左にあるカプセルのインデックス
            int sidx = (fidx == 0) ? 1 : 0;

            int px = playerBlock[fidx].BlockPos[Block.BLOCK_POS_X_IDX];
            int py = playerBlock[fidx].BlockPos[Block.BLOCK_POS_Y_IDX];

            if (GetBlockInfo(px - 1, py - 1) == null && !IsOutBottle(px - 1, py - 1))
            {
                // カプセル右の「左上」に動く先があるなら

                // カプセル右を左上に移動
                playerBlock[fidx].Move(Block.BlockMove.Left);
                playerBlock[fidx].Move(Block.BlockMove.Up);
                playerBlock[fidx].Rotate();
                // カプセル左は画像回転のみ
                playerBlock[sidx].Rotate();
            }
        }
    }

BlockスクリプトにRotateメソッドをオーバーロードした、プレイヤーによる回転用のメソッドを用意します。

Block.cs
    // ブロック回転
    public void Rotate()
    {
        switch (CurrentBlockRota)
        {
            case BlockRota.Up:
                CurrentBlockRota = BlockRota.Left;
                break;

            case BlockRota.Left:
                CurrentBlockRota = BlockRota.Down;
                break;

            case BlockRota.Down:
                CurrentBlockRota = BlockRota.Right;
                break;

            case BlockRota.Right:
                CurrentBlockRota = BlockRota.Up;
                break;
        }

        Rotate(CurrentBlockRota);
    }

5. プレイヤーブロックの生成

BlockManagerからPlayerスクリプトを制御できるようにします。

BlockManagerにプレイヤー操作用のブロックを生成するメソッドを用意します。

BlockManager.cs
    // プレイヤー用ブロック生成
    private void CreatePlayerBlock()
    {
        // 生成先にブロックが既に存在するなら
        if (GetBlockInfo(3, 1) != null || GetBlockInfo(4, 1) != null)
        {
            // ゲームオーバー
            Debug.Log("Game Over");
            return;
        }

        // 左ブロック生成
        GameObject blockL = GameObject.Instantiate(blockPrefab);
        blockL.transform.parent = transform;

        Block blockLCom = blockL.GetComponent<Block>();
        Block.BlockType blockLType = GetRandomCap(); // 後で追加

        blockLCom.Setting(blockLType, Block.BlockRota.Left, 3, 1, GetBlockInfo, IsOutBottle);

        // 右ブロック生成
        GameObject blockR = GameObject.Instantiate(blockPrefab);
        blockR.transform.parent = transform;

        Block blockRCom = blockR.GetComponent<Block>();
        Block.BlockType blockRType = GetRandomCap(); // 後で追加

        blockRCom.Setting(blockRType, Block.BlockRota.Right, 4, 1, GetBlockInfo, IsOutBottle);

        // プレイヤーが操作できるようにブロックコンポーネントをプレイヤークラスに渡す
        player.SetPlayerBlock(new Block[] { blockLCom, blockRCom });
    }

カプセルの種類をランダムに選ばせるメソッドを用意します。

BlockManager.cs
    // カプセルをランダムに選ぶ
    private Block.BlockType GetRandomCap()
    {
        Block.BlockType type = Block.BlockType.CapRed;

        int id = UnityEngine.Random.Range(0, 3);

        switch (id)
        {
            case 0: type = Block.BlockType.CapRed; break;
            case 1: type = Block.BlockType.CapBlue; break;
            case 2: type = Block.BlockType.CapYellow; break;
        }

        return type;
    }

ステージを開始するメソッドを用意し、MonoBehaviour由来のStartメソッドから呼び出します。

BlockManager.cs
    private void Start()
    {
        StageStart();
    }

    // ステージ開始
    private void StageStart()
    {
        player.Init(PlayerTurnEndCallback, GetBlockInfo, IsOutBottle);

        CreateStage();

        CreatePlayerBlock();
    }

6. ブロックの消去、落下

最後に、プレイヤーによるブロック操作が終わった際の
ブロック消去、落下、そしてまたプレイヤー操作ブロックの生成に戻る処理を実装します。

BlockManagerにプレイヤー操作が終わった際のコールバック処理を追加します。

BlockManager.cs
    // カプセルを配置してプレイヤー操作が終わった際のコールバック処理
    private void PlayerTurnEndCallback(Block[] playerBlock)
    {
        // プレイヤーの操作していたブロックの情報を配置情報に渡す
        int leftX = playerBlock[0].BlockPos[Block.BLOCK_POS_X_IDX];
        int leftY = playerBlock[0].BlockPos[Block.BLOCK_POS_Y_IDX];

        blockInfo[leftX, leftY] = playerBlock[0];

        int rightX = playerBlock[1].BlockPos[Block.BLOCK_POS_X_IDX];
        int rightY = playerBlock[1].BlockPos[Block.BLOCK_POS_Y_IDX];

        blockInfo[rightX, rightY] = playerBlock[1];

        // ブロックの消去、降下の実行
        StartCoroutine(BlockEraseProcess()); // 後で追加
    }

ブロック消去のために、消去するブロックがあるかチェックするメソッドを追加します。

BlockManager.cs
    // ブロック消去チェック
    private void CheckEraseBlock()
    {
        // 配置できる場所をすべてチェック
        for (int h = 0; h < HORIZONTAL_NUM; h++)
        {
            for (int v = VERTICAL_NUM - 1; v >= 0; v--)
            {
                Block b = GetBlockInfo(h, v);
                if (b == null) continue;

                CheckHorizontal(b, h, v); // 後で追加
                CheckVertical(b, h, v);   // 後で追加
            }
        }
    }

横列に同色で4つ以上揃っているかチェックするメソッドを追加します。

BlockManager.cs
    // 横列に揃っているかチェック
    private void CheckHorizontal(Block b, int h, int v)
    {
        List<Block> sameBlockList = new List<Block>();

        // 同色のブロックが続いていたらリストに格納
        for (int i = h + 1; i < HORIZONTAL_NUM; i++)
        {
            Block t = GetBlockInfo(i, v);

            if (t == null) break;

            if (IsSameColorBlock(b.CurrentBlockType, t.CurrentBlockType)) // 後で追加
            {
                sameBlockList.Add(t);
            }
            else
            {
                break;
            }
        }

        // チェック対象含めて4つ以上なら消去フラグを立てる
        if (sameBlockList.Count >= 3)
        {
            b.IsErase = true;
            foreach (var s in sameBlockList)
            {
                s.IsErase = true;
            }

            isErase = true;
        }
    }

さらに縦列。

BlockManager.cs
    // 縦列に揃っているかチェック
    private void CheckVertical(Block b, int h, int v)
    {
        List<Block> sameBlockList = new List<Block>();

        // 同色のブロックが続いていたらリストに格納
        for (int i = v - 1; i >= 0; i--)
        {
            Block t = GetBlockInfo(h, i);

            if (t == null) break;

            if (IsSameColorBlock(b.CurrentBlockType, t.CurrentBlockType)) // 後で追加
            {
                sameBlockList.Add(t);
            }
            else
            {
                break;
            }
        }

        // チェック対象含めて4つ以上なら消去フラグを立てる
        if (sameBlockList.Count >= 3)
        {
            b.IsErase = true;
            foreach (var s in sameBlockList)
            {
                s.IsErase = true;
            }

            isErase = true;
        }
    }

2つのブロックが同色のブロックか判定するメソッドを追加します。

BlockManager.cs
    // 同色ブロックか判定
    private bool IsSameColorBlock(Block.BlockType b, Block.BlockType t)
    {
        switch (b)
        {
            case Block.BlockType.CapRed:
            case Block.BlockType.SingleCapRed:
            case Block.BlockType.VirusRed:
                return (t == Block.BlockType.CapRed || t == Block.BlockType.SingleCapRed || t == Block.BlockType.VirusRed);

            case Block.BlockType.CapBlue:
            case Block.BlockType.SingleCapBlue:
            case Block.BlockType.VirusBlue:
                return (t == Block.BlockType.CapBlue || t == Block.BlockType.SingleCapBlue || t == Block.BlockType.VirusBlue);

            case Block.BlockType.CapYellow:
            case Block.BlockType.SingleCapYellow:
            case Block.BlockType.VirusYellow:
                return (t == Block.BlockType.CapYellow || t == Block.BlockType.SingleCapYellow || t == Block.BlockType.VirusYellow);

        }

        return false;
    }

ブロックを消去する演出メソッドを追加します。

BlockManager.cs
    // ブロック消去演出
    private IEnumerator PlayEraseBlock()
    {
        // ブロック消去実行
        for (int h = 0; h < HORIZONTAL_NUM; h++)
        {
            for (int v = VERTICAL_NUM - 1; v >= 0; v--)
            {
                Block b = GetBlockInfo(h, v);
                if (b == null) continue;

                if (b.IsErase)
                {
                    // ウィルスリストに対象がいればリストから消去
                    if (virusBlockList.Contains(b))
                    {
                        virusBlockList.Remove(b);
                    }

                    // ブロック消去
                    Destroy(b.gameObject);
                    blockInfo[h, v] = null;
                }
            }
        }

        // カプセル変化
        for (int h = 0; h < HORIZONTAL_NUM; h++)
        {
            for (int v = VERTICAL_NUM - 1; v >= 0; v--)
            {
                Block b = GetBlockInfo(h, v);
                if (b == null) continue;

                // カプセルの相方ブロックを取得
                Block bs = null;
                switch (b.CurrentBlockRota)
                {
                    case Block.BlockRota.Up:
                        bs = GetBlockInfo(h, v + 1);
                        break;

                    case Block.BlockRota.Left:
                        bs = GetBlockInfo(h + 1, v);
                        break;

                    case Block.BlockRota.Down:
                        bs = GetBlockInfo(h, v - 1);
                        break;

                    case Block.BlockRota.Right:
                        bs = GetBlockInfo(h - 1, v);
                        break;

                    default:
                        continue;
                }

                // 相方が消えていれば変形
                if (bs == null)
                {
                    b.ChangeCap(); // 後で追加
                }
            }
        }

        // 少し間を開ける
        float time = 0;
        while (time < 0.25f)
        {
            time += Time.deltaTime;
            yield return null;
        }
    }

Blockスクリプトにカプセルの相方が消えた場合に変形させるメソッドを追加します。

Block.cs
    // カプセル変化(連結していない単体にする)
    public void ChangeCap()
    {
        // 変化処理
        switch (CurrentBlockType)
        {
            case BlockType.CapRed:
                renderer.sprite = sprites[(int)BlockType.SingleCapRed];
                CurrentBlockType = BlockType.SingleCapRed;
                break;

            case BlockType.CapBlue:
                renderer.sprite = sprites[(int)BlockType.SingleCapBlue];
                CurrentBlockType = BlockType.SingleCapBlue;
                break;

            case BlockType.CapYellow:
                renderer.sprite = sprites[(int)BlockType.SingleCapYellow];
                CurrentBlockType = BlockType.SingleCapYellow;
                break;
        }

        CurrentBlockRota = BlockRota.None;
        renderer.transform.localRotation = Quaternion.Euler(0, 0, 0);
    }

BlockManagerに戻り、続いてブロックを消去した後のブロック落下処理を追加していきます。

まず、落下するブロックがあるかのチェックメソッドを追加します。

BlockManager.cs
    // ブロック落下チェック
    private void CheckFallBlock()
    {
        // 最下段は落下先がないので2段目からチェック
        for (int v = VERTICAL_NUM - 2; v >= 0; v--)
        {
            for (int h = 0; h < HORIZONTAL_NUM; h++)
            {
                Block b = GetBlockInfo(h, v);

                // ウィルスは落下しない
                if (b == null ||
                    b.CurrentBlockType == Block.BlockType.VirusRed ||
                    b.CurrentBlockType == Block.BlockType.VirusBlue ||
                    b.CurrentBlockType == Block.BlockType.VirusYellow)
                    continue;

                // 一つ下のブロック
                Block db = GetBlockInfo(h, v + 1);

                // 右向きカプセルなら以下のチェックを行う
                if (b.CurrentBlockRota == Block.BlockRota.Right)
                {
                    // 一つ左のブロック(相方カプセルのはず)
                    Block lb = GetBlockInfo(h - 1, v);

                    if (lb.IsFall && (db == null || db.IsFall))
                    {
                        // 相方が落ちるなら、さらに落下先があるなら落下させる
                        b.IsFall = true;
                        isFall = true;
                    }
                    else
                    {
                        // 自分も相方も落下させない
                        b.IsFall = false;
                        lb.IsFall = false;
                    }
                }
                else
                {
                    if (db == null || db.IsFall)
                    {
                        // 落下先があるなら落下させる
                        b.IsFall = true;
                        // 左向きカプセルなら相方(右向き)が落ちるか分かるまで落下処理をオンにしない
                        if (b.CurrentBlockRota != Block.BlockRota.Left)
                        {
                            isFall = true;
                        }
                    }
                }
            }
        }
    }

ブロックが落下する演出メソッドを追加します。

BlockManager.cs
    // ブロック落下演出
    private IEnumerator PlayFallBlock()
    {
        // 落下移動
        for (int v = VERTICAL_NUM - 2; v >= 0; v--)
        {
            for (int h = 0; h < HORIZONTAL_NUM; h++)
            {
                Block b = GetBlockInfo(h, v);
                if (b == null || !b.IsFall) continue;

                // 落下処理
                b.IsFall = false;
                b.Move(Block.BlockMove.Down);

                // ブロック情報を下に移行
                blockInfo[h, v] = null;
                blockInfo[h, v + 1] = b;
            }
        }

        // 少し間を開ける
        float time = 0;
        while (time < 0.2f)
        {
            time += Time.deltaTime;
            yield return null;
        }
    }

すべてのウィルスを消してゲームクリアしたかの判定メソッドを追加します。

BlockManager.cs
    // ゲームクリア状態か判定
    private bool IsGameClear()
    {
        return virusBlockList.Count == 0;
    }

最後に、ブロック消去、落下の処理をコルーチンで順次行っていくメソッドを用意します。

BlockManager.cs
    // ブロック消去プロセス
    private IEnumerator BlockEraseProcess()
    {
        isErase = false;

        // ブロック消去チェック
        CheckEraseBlock();

        if (isErase)
        {
            // ブロック消去演出
            yield return StartCoroutine(PlayEraseBlock());

            if (IsGameClear())
            {
                // ゲームクリア
                Debug.Log("Game Clear");
                yield break;
            }

            // 落下ブロックがある限り落下処理を続ける
            do
            {
                isFall = false;

                // ブロック落下チェック
                CheckFallBlock();

                // ブロック落下演出
                if (isFall)
                {
                    yield return StartCoroutine(PlayFallBlock());
                }
            }
            while (isFall);

            // 消去するブロックがなくなるまで続ける
            StartCoroutine(BlockEraseProcess());
        }
        else
        {
            // 少し間を開ける
            float time = 0;
            while (time < 0.25f)
            {
                time += Time.deltaTime;
                yield return null;
            }

            // プレイヤーブロックを生成
            CreatePlayerBlock();
        }
    }

最後に

以上で実装は完了となります。

『Dr.TSUKUSHI』ではここにさらにキャラがカプセルを投げ入れる演出や、すべての色を兼ねるカプセル、縦列をすべて同色のカプセルにするギミックなどを取り入れています。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2