【c#】Unityでテトリスを作ってみた


WebGLでブラウザで動くように書き出したページはこちら。Win/MacのChrome/FireFox/Safari/Edgeでは動作確認しました。スマホのブラウザで開いても動いているようですが、キーボードがないので動かせません。
キーボードの右左下でブロック(ミノ)を動かし、上で即落下固定させます。sで右回転、aで左回転、hでホールドします。

Unity上で動かしてみたい場合は、空のプロジェクトを作成し、UIのTextを2つ配置(1つはゲーム本体を表示するfieldTextObject、もう1つは情報を表示するsubTextObject)します。次に空のGameObjectを作成し、それに対しGameBehaviourScriptという名前のscriptを追加した後、下記のソースを貼り付け、Field Text ObjectとSub Text Objectを先ほど作成したUIのTextに割り当てれば動くと思います。あと、フォントを等幅のものにしないとやりにくいです。

テキストで表現していることからも分かる通り、ビジュアルには無頓着です。どちらかといえば、クラス設計等の参考にしていただければ嬉しいのですが、MonoBehaviourを継承したクラスに機能を盛り込んだので、なんだかわかりにくくなってしまったような気がしています。

Wikipediaを参考に、現在の一般的なテトリスの機能は入れられたかなと思っていますが、レベル調整などはしていません。

細かい各クラスやメソッドの解説は追ってページを作成したいと思っています。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using UnityEngine;
using UnityEngine.UI;

public class GameBehaviourScript : MonoBehaviour
{
    public int level;
    public GameObject fieldTextObject;
    public GameObject subTextObject;

    Text fieldText;
    Text subText;
    bool requireUpdateText = false;
    bool stopInput = false;
    float stopInputTime = 0.1f;
    bool downFlag = false;
    bool lockFlag = false;
    int spinCount = 0;
    readonly Field field = new Field();
    Coroutine lockCoroutine = null;

    void Start()
    {
        fieldText = fieldTextObject.GetComponent<Text>();
        subText = subTextObject.GetComponent<Text>();
        UpdateText();
        StartCoroutine(DelayMethod(GetDownTimeSpan(), () =>
        {
            downFlag = true;
        }));
    }

    void Update()
    {
        if(field.isGameOver == false)
        {
            if (stopInput == false)
            {
                if (Input.GetKey(KeyCode.LeftArrow) && field.Move(-1)) requireUpdateText = true;
                if (Input.GetKey(KeyCode.RightArrow) && field.Move(1)) requireUpdateText = true;
                if (Input.GetKey(KeyCode.DownArrow) && field.Down()) requireUpdateText = true;
                if (Input.GetKey(KeyCode.S) && field.Spin(1)
                 || Input.GetKey(KeyCode.A) && field.Spin(-1))
                {
                    spinCount++;
                    if (spinCount < 10 && lockCoroutine != null)
                    {
                        StopCoroutine(lockCoroutine);
                        lockCoroutine = StartCoroutine(DelayMethod(0.5f, () =>
                        {
                            lockFlag = true;
                        }));
                    }
                    requireUpdateText = true;
                    stopInputTime = 0.2f;
                }
                if (Input.GetKey(KeyCode.H) && field.Hold())
                {
                    requireUpdateText = true;
                    stopInputTime = 0.2f;
                }
                if (Input.GetKey(KeyCode.UpArrow))
                {
                    field.Fall();
                    requireUpdateText = true;
                    stopInputTime = 0.2f;
                    if (lockCoroutine != null) StopCoroutine(lockCoroutine);
                    lockCoroutine = null;
                }
                if (requireUpdateText)
                {
                    stopInput = true;
                    StartCoroutine(DelayMethod(stopInputTime, () =>
                    {
                        stopInput = false;
                        stopInputTime = 0.1f;
                    }));
                }
            }
            if (downFlag)
            {
                downFlag = false;
                StartCoroutine(DelayMethod(GetDownTimeSpan(), () =>
                {
                    downFlag = true;
                }));
                if (field.Down())
                {
                    requireUpdateText = true;
                }
                else if (lockCoroutine == null)
                {
                    lockCoroutine = StartCoroutine(DelayMethod(0.5f, () =>
                    {
                        lockCoroutine = null;
                        lockFlag = true;
                    }));
                }
            }
            if (lockFlag)
            {
                lockFlag = false;
                field.TryLock();
                spinCount = 0;
            }
        }
        if (requireUpdateText) UpdateText();
    }
    void UpdateText()
    {
        fieldText.text = field.GetDisplayText();
        subText.text = GetSubText();
        requireUpdateText = false;
    }
    float GetDownTimeSpan()
    {
        if(level > 99) level = 99;
        return 1f - ((float)level) * 0.01f;
    }

    public string GetSubText()
    {
        return $@"NEXT:{field.GetNextMinoShape(1)}
HOLD:{field.hold.mino?.shape}
LEVEL:{level}";
    }

    public IEnumerator DelayMethod(float waitTime, Action action)
    {
        yield return new WaitForSeconds(waitTime);
        action();
    }
}
public class Field
{

    static int FIELD_ROW = 20;
    static int FIELD_COL = 10;

    public (Mino? mino, bool held) hold { get; private set; }
    public bool isGameOver { get; private set; } = false;

    List<List<Element>> matrix = new List<List<Element>>() { };
    ControlledMino controlledMino;
    List<int> minoList;
    int minoCount;

    public Field()
    {
        for (var i = 0; i < FIELD_ROW; i++)
        {
            var add = new List<Element>();
            for (var j = 0; j < FIELD_COL; j++)
            {
                add.Add(new Element());
            }
            matrix.Add(add);
        }
        /*デバッグ用 初期配置でブロックを置く
        var change = new (int row, int col)[]
        {
            (15,4),(15,5),
            (16,5),
            (17,0),(17,1),(17,2),(17,3),(17,5),(17,6),(17,7),(17,8),(17,9),
            (18,0),(18,1),(18,2),(18,5),(18,6),(18,7),(18,8),(18,9),
            (19,0),(19,1),(19,2),(19,3),(19,5),(19,6),(19,7),(19,8),(19,9),
        };
        foreach(var rc in change)
        {
            matrix[rc.row][rc.col].isFull = true;
            matrix[rc.row][rc.col].color = Color.Yellow;
        }
        */
        minoList = Enumerable.Range(0, 7).OrderBy(i => Guid.NewGuid()).ToList();
        minoList.AddRange(Enumerable.Range(0, 7).OrderBy(i => Guid.NewGuid()).ToList());
        minoCount = -1;
        controlledMino = new ControlledMino(GetNewMino());
    }
    public Mino GetNewMino()
    {
        minoCount++;
        if (minoCount == 7)
        {
            minoCount = 0;
            minoList.RemoveRange(0, 7);
            minoList.AddRange(Enumerable.Range(0, 7).OrderBy(i => Guid.NewGuid()).ToList());
        }
        switch (minoList[minoCount] % 7)
        {
            case 0:
                return new I_Mino();
            case 1:
                return new O_Mino();
            case 2:
                return new S_Mino();
            case 3:
                return new Z_Mino();
            case 4:
                return new J_Mino();
            case 5:
                return new L_Mino();
            default:
                return new T_Mino();
        }
    }
    public Shape GetNextMinoShape(int next)
    {
        return (Shape)Enum.ToObject(typeof(Shape), minoList[minoCount + next]);
    }

    public string GetDisplayText()
    {
        if (isGameOver) return "GAME OVER";
        StringBuilder displayText = new StringBuilder();
        for (var row = 0; row < matrix.Count(); row++)
        {
            for (var col = 0; col < matrix[row].Count(); col++)
            {
                displayText.Append(GetDisplayText(row, col));
            }
            displayText.Append(Environment.NewLine);
        }
        return displayText.ToString();
    }

    public string GetDisplayText(int row, int col)
    {
        if (matrix[row][col].isFull) return $"<color={matrix[row][col].color.ToString()}>■</color>";
        if (controlledMino.size.Any(size => row == size.row && col == size.col)) return $"<color={controlledMino.mino.color.ToString()}>■</color>";
        int fallCount = 0;
        while (CanMove(controlledMino.size, new Position(fallCount + 1, 0)))
        {
            fallCount++;
        }
        if (controlledMino.size.Any(size => row == size.row + fallCount && col == size.col)) return "<color=White>□</color>";
        return "・";
    }

    public bool Move(int col)
    {
        if (CanMove(controlledMino.size, new Position(0, col)))
        {
            controlledMino.position.col += col;
            return true;
        }
        return false;
    }
    public bool Spin(int direction)
    {
        int spin = (controlledMino.mino.spin + direction + 4) % 4;
        Position[] spinnedSize = controlledMino.mino.sizes[spin];

        Position[] positionAdjustArray = {
            new Position(0,0),
            new Position(0,1),
            new Position(0,2),
            new Position(0,-1),
            new Position(0,-2),
            new Position(1,1),
            new Position(1,-1),
            new Position(2,1),
            new Position(2,-1),
            new Position(-1,1),
            new Position(-1,-1),
        };
        foreach (var positionAdjust in positionAdjustArray)
        {
            if (CanMove(spinnedSize, controlledMino.position, positionAdjust))
            {
                controlledMino.position.col += positionAdjust.col;
                controlledMino.position.row += positionAdjust.row;
                controlledMino.mino.spin = spin;
                return true;
            }
        }
        return false;
    }
    public bool CanMove(Position[] sizes, params Position[] positions)
    {
        return sizes.All(size => positions.Sum(position => position.col) + size.col >= 0
                && positions.Sum(position => position.row) + size.row >= 0
                && positions.Sum(position => position.col) + size.col < FIELD_COL
                && positions.Sum(position => position.row) + size.row < FIELD_ROW
                && matrix[positions.Sum(position => position.row) + size.row][positions.Sum(position => position.col) + size.col].isFull == false);
    }
    public bool Down()
    {
        bool moved = false;
        if (CanMove(controlledMino.size, new Position(1, 0)))
        {
            controlledMino.position.row += 1;
            moved = true;
        }
        return moved;
    }
    public bool TryLock()
    {
        bool locked = false;
        if (CanMove(controlledMino.size, new Position(1, 0)) == false)
        {
            Lock();
            locked = true;
        }
        return locked;
    }

    public void Fall()
    {
        while (CanMove(controlledMino.size, new Position(1, 0)))
        {
            controlledMino.position.row += 1;
        }
        Lock();
    }

    private void Lock()
    {
        Position[] size = controlledMino.size;
        foreach (var position in size)
        {
            matrix[position.row][position.col].isFull = true;
            matrix[position.row][position.col].color = controlledMino.mino.color;
        }
        Clear();
        controlledMino = new ControlledMino(GetNewMino());
        foreach (var position in controlledMino.size)
        {
            if (matrix[position.row][position.col].isFull) isGameOver = true;
        }
        hold = (hold.mino, false);
    }
    public void Clear()
    {
        int[] clearRows = matrix.Select((r, i) => new { Row = r, Index = i })
            .Where(row => row.Row.All(masu => masu.isFull))
            .Select(row => row.Index)
            .OrderBy(i => i)
            .ToArray();
        if (clearRows.Count() > 0)
        {
            if (clearRows.Count() == 4) Debug.Log("TETRIS");
            foreach (var i in clearRows)
            {
                matrix.RemoveAt(i);
                var add = new List<Element>();
                for (var j = 0; j < 10; j++)
                {
                    add.Add(new Element());
                }
                matrix.Insert(0, add);
            }
        }
    }
    public bool Hold()
    {
        if (hold.held == false)
        {
            Mino temp = null;
            if (hold.mino != null)
            {
                temp = hold.mino;
            }
            hold = (controlledMino.mino,true);
            controlledMino = new ControlledMino(temp ?? GetNewMino());
            return true;
        }
        return false;
    }
}
public class Element
{
    public bool isFull = false;
    public Color color = Color.Black;
}

public enum Color
{
    Lightblue,
    Yellow,
    Green,
    Red,
    Blue,
    Orange,
    Purple,
    Black
}
public class ControlledMino
{
    public Mino mino;
    public Position position;

    public Position[] size
    {
        get
        {
            return mino.size.Select(size => new Position(size.row + position.row, size.col + position.col)).ToArray();
        }
    }

    public ControlledMino(Mino mino)
    {
        this.mino = mino;
        position = new Position(0, 3);
    }
}
public enum Shape
{
    I,
    O,
    S,
    Z,
    J,
    L,
    T
}
public abstract class Mino
{
    public abstract Shape shape { get; }
    public abstract Color color { get; }
    public int spin = 0;

    public Position[] size
    {
        get
        {
            return this.sizes[this.spin];
        }
    }
    public abstract Position[][] sizes { get; }
}
public class I_Mino : Mino
{
    public override Shape shape => Shape.I;
    public override Color color => Color.Lightblue;
    public override Position[][] sizes => new Position[][]{
        //  0123
        //0□□□□
        //1■■■■
        //2□□□□
        //3□□□□
        new Position[]{
            new Position(1, 0),
            new Position(1, 1),
            new Position(1, 2),
            new Position(1, 3)
        },
        //  0123
        //0□□■□
        //1□□■□
        //2□□■□
        //3□□■□
        new Position[]{
            new Position(0, 2),
            new Position(1, 2),
            new Position(2, 2),
            new Position(3, 2)
        },
        //  0123
        //0□□□□
        //1■■■■
        //2□□□□
        //3□□□□
        new Position[]{
            new Position(1, 0),
            new Position(1, 1),
            new Position(1, 2),
            new Position(1, 3)
        },
        //  0123
        //0□■□□
        //1□■□□
        //2□■□□
        //3□■□□
        new Position[]{
            new Position(0, 1),
            new Position(1, 1),
            new Position(2, 1),
            new Position(3, 1)
        },
    };
}
public class O_Mino : Mino
{
    public override Shape shape => Shape.O;
    public override Color color => Color.Yellow;
    //  0123
    //0□■■□
    //1□■■□
    //2□□□□
    //3□□□□
    public override Position[][] sizes => new Position[][]{
        new Position[]{
            new Position(0, 1),
            new Position(0, 2),
            new Position(1, 1),
            new Position(1, 2)
        },
        new Position[]{
            new Position(0, 1),
            new Position(0, 2),
            new Position(1, 1),
            new Position(1, 2)
        },
        new Position[]{
            new Position(0, 1),
            new Position(0, 2),
            new Position(1, 1),
            new Position(1, 2)
        },
        new Position[]{
            new Position(0, 1),
            new Position(0, 2),
            new Position(1, 1),
            new Position(1, 2)
        },
    };
}
public class S_Mino : Mino
{
    public override Shape shape => Shape.S;
    public override Color color => Color.Green;
    public override Position[][] sizes => new Position[][]{
        //  0123
        //0□■■□
        //1■■□□
        //2□□□□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(0, 2),
            new Position(1, 0),
            new Position(1, 1)
        },
        //  0123
        //0□■□□
        //1□■■□
        //2□□■□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(1, 1),
            new Position(1, 2),
            new Position(2, 2)
        },
        //  0123
        //0□□□□
        //1□■■□
        //2■■□□
        //3□□□□
        new Position[]{
            new Position(1, 1),
            new Position(1, 2),
            new Position(2, 0),
            new Position(2, 1)
        },
        //  0123
        //0■□□□
        //1■■□□
        //2□■□□
        //3□□□□
        new Position[]{
            new Position(0, 0),
            new Position(1, 0),
            new Position(1, 1),
            new Position(2, 1)
        },
    };
}
public class Z_Mino : Mino
{
    public override Shape shape => Shape.Z;
    public override Color color => Color.Red;
    public override Position[][] sizes => new Position[][]{
        //  0123
        //0■■□□
        //1□■■□
        //2□□□□
        //3□□□□
        new Position[]{
            new Position(0, 0),
            new Position(0, 1),
            new Position(1, 1),
            new Position(1, 2)
        },
        //  0123
        //0□□■□
        //1□■■□
        //2□■□□
        //3□□□□
        new Position[]{
            new Position(0, 2),
            new Position(1, 1),
            new Position(1, 2),
            new Position(2, 1)
        },
        //  0123
        //0□□□□
        //1■■□□
        //2□■■□
        //3□□□□
        new Position[]{
            new Position(1, 0),
            new Position(1, 1),
            new Position(2, 1),
            new Position(2, 2)
        },
        //  0123
        //0□■□□
        //1■■□□
        //2■□□□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(1, 0),
            new Position(1, 1),
            new Position(2, 0)
        },
    };
}
public class J_Mino : Mino
{
    public override Shape shape => Shape.J;
    public override Color color => Color.Blue;
    public override Position[][] sizes => new Position[][]{
        //  0123
        //0■□□□
        //1■■■□
        //2□□□□
        //3□□□□
        new Position[]{
            new Position(0, 0),
            new Position(1, 0),
            new Position(1, 1),
            new Position(1, 2)
        },
        //  0123
        //0□■■□
        //1□■□□
        //2□■□□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(0, 2),
            new Position(1, 1),
            new Position(2, 1)
        },
        //  0123
        //0□□□□
        //1■■■□
        //2□□■□
        //3□□□□
        new Position[]{
            new Position(1, 0),
            new Position(1, 1),
            new Position(1, 2),
            new Position(2, 2)
        },
        //  0123
        //0□■□□
        //1□■□□
        //2■■□□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(1, 1),
            new Position(2, 0),
            new Position(2, 1)
        },
    };
}
public class L_Mino : Mino
{
    public override Shape shape => Shape.L;
    public override Color color => Color.Orange;
    public override Position[][] sizes => new Position[][]{
        //  0123
        //0□□■□
        //1■■■□
        //2□□□□
        //3□□□□
        new Position[]{
            new Position(0, 2),
            new Position(1, 0),
            new Position(1, 1),
            new Position(1, 2)
        },
        //  0123
        //0□■□□
        //1□■□□
        //2□■■□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(1, 1),
            new Position(2, 1),
            new Position(2, 2)
        },
        //  0123
        //0□□□□
        //1■■■□
        //2■□□□
        //3□□□□
        new Position[]{
            new Position(1, 0),
            new Position(1, 1),
            new Position(1, 2),
            new Position(2, 0)
        },
        //  0123
        //0■■□□
        //1□■□□
        //2□■□□
        //3□□□□
        new Position[]{
            new Position(0, 0),
            new Position(0, 1),
            new Position(1, 1),
            new Position(2, 1)
        },
    };
}
public class T_Mino : Mino
{
    public override Shape shape => Shape.T;
    public override Color color => Color.Purple;
    public override Position[][] sizes => new Position[][]{
        //  0123
        //0□■□□
        //1■■■□
        //2□□□□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(1, 0),
            new Position(1, 1),
            new Position(1, 2)
        },
        //  0123
        //0□■□□
        //1□■■□
        //2□■□□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(1, 1),
            new Position(1, 2),
            new Position(2, 1)
        },
        //  0123
        //0□□□□
        //1■■■□
        //2□■□□
        //3□□□□
        new Position[]{
            new Position(1, 0),
            new Position(1, 1),
            new Position(1, 2),
            new Position(2, 1)
        },
        //  0123
        //0□■□□
        //1■■□□
        //2□■□□
        //3□□□□
        new Position[]{
            new Position(0, 1),
            new Position(1, 0),
            new Position(1, 1),
            new Position(2, 1)
        },
    };
}

public class Position
{
    public int row;
    public int col;

    public Position(int row, int col)
    {
        this.row = row;
        this.col = col;
    }
}

Programming Blog