[C# 프로젝트] 테트리스 만들기 – Part 4(Final). 꽉 찬 줄 지우기, Ending

프로젝트 다운로드

프로젝트 다운로드 -벽돌 모양에 따라 색상 변경 기능 추가

 안녕하세요. 언제나 휴일에 언휴예요.

 이번 강의는 테트리스 프로젝트의 마지막 부분입니다. 

 이번 강의에서는 꽉 찬 라인을 지우는 기능과 종료 조건을 체크하는 부분을 구현합니다.

Board 형식 수정

 Store 메서드에 라인이 꽉 찼는지 확인하는 메서드를 호출합니다. 이 때 주의할 점은 라인 체크는 아래부터 한다는 것입니다. 벽돌이 4X4공간에 배치하므로 벽돌이 있는 좌표에서 3칸 더 있다는 것을 고려하세요. 

 

        internal void Store(int bn, int turn, int x, int y)
        {
            for (int xx = 0; xx < 4; xx++)
            {
                for(int yy = 0; yy<4;yy++)
                {
                    if(((x+xx)>=0)&&(x+xx<GameRule.BX)&&(y+yy>=0)&&(y+yy<GameRule.BY))
                    {
                        board[x + xx, y + yy] += BlockValue.bvals[bn, turn, xx, yy];
                    }
                }
            }
            CheckLines(y+3);
        }

 벽돌을 4X4 공간에 정의하였지만 일부는 마지막 라인이 모두 비어있을 때도 있습니다. 따라서 라인을 체크할 때 보드 공간을 벗어나는 라인은 체크하지 않습니다. 그리고 꽉 찬 라인은 라인을 지워줍니다. 주의할 점은 라인을 지우면 윗 부분이 한 칸씩 아래로 채워지므로 다시 같은 라인을 점검해야 합니다. 이 부분을 주의해서 구현하세요.

여기에서는 점검할 라인은 입력 인자로 받은 y값과  4개의 라인을 점검하기 위한 Loop 변수 yy를 뺀 값(y-yy)입니다. Loop변수 yy 값이 1 증가하는 대신 점검할 라인 번호도 같이 1 증가시키고 있어요. 결국 (y-yy)는 같은 값으로 같은 라인을 점검할 수 있어요. 

        private void CheckLines(int y)
        {
            int yy = 0;
            for(yy=0;(yy<4);yy++)
            {
                if(y-yy<GameRule.BY)
                {                    
                    if (CheckLine(y - yy))
                    {
                        ClearLine(y - yy);
                        y++;
                    }
                }                
            }
        }

 한 줄이 꽉 찼는지 확인하는 방법은 해당 라인에 모든 x좌표에 돌이 있을 때예요. 그리고 꽉 찼을 때 지우는 방법은 한 칸 위의 값으로 변경하는 것을 반복하는 거예요.

        private void ClearLine(int y)
        {
            for (; y > 0; y--)
            {
                for (int xx = 0; xx < GameRule.BX; xx++)
                {
                    board[xx, y] = board[xx, y - 1];
                }
            }
        }

        private bool CheckLine(int y)
        {
            for(int xx= 0; xx<GameRule.BX;xx++)
            {
                if(board[xx,y]==0)
                {
                    return false;
                }
            }
            return true;
        }

 게임을 계속할 수 있게 보드를 초기화하는 메서드도 제공합시다.

        internal void ClearBoard()
        {
            for(int xx=0;xx<GameRule.BX;xx++)
            {
                for(int yy=0;yy<GameRule.BY;yy++)
                {
                    board[xx, yy] = 0;
                }
            }
        }

다음은 Board.cs 파일의 소스 코드입니다.

namespace 도형_이동
{
    class Board
    {
        internal static Board GameBoard
        {
            get;
            private set;
        }
        static Board()
        {
            GameBoard = new Board();
        }
        Board()
        {
        }
        int[,] board = new int[GameRule.BX, GameRule.BY];
        internal int this[int x,int y]
        {
            get
            {
                return board[x, y];
            }
        }
        internal bool MoveEnable(int bn,int tn,int x,int y)
        {
            for(int xx=0;xx<4;xx++)
            {
                for(int yy=0;yy<4;yy++)
                {
                    if(BlockValue.bvals[bn,tn,xx,yy]!=0)
                    {
                        if(board[x+xx,y+yy]!=0)
                        {
                            return false;
                        }
                    }
                }
            }
            return true;
        }
        internal void Store(int bn, int turn, int x, int y)
        {
            for (int xx = 0; xx < 4; xx++)
            {
                for(int yy = 0; yy<4;yy++)
                {
                    if(((x+xx)>=0)&&(x+xx<GameRule.BX)&&(y+yy>=0)&&(y+yy<GameRule.BY))
                    {
                        board[x + xx, y + yy] += BlockValue.bvals[bn, turn, xx, yy];
                    }
                }
            }
            CheckLines(y+3);
        }

        private void CheckLines(int y)
        {
            int yy = 0;
            for(yy=0;(yy<4);yy++)
            {
                if(y-yy<GameRule.BY)
                {                    
                    if (CheckLine(y - yy))
                    {
                        ClearLine(y - yy);
                        y++;
                    }
                }                
            }
        }

        private void ClearLine(int y)
        {
            for (; y > 0; y--)
            {
                for (int xx = 0; xx < GameRule.BX; xx++)
                {
                    board[xx, y] = board[xx, y - 1];
                }
            }
        }

        private bool CheckLine(int y)
        {
            for(int xx= 0; xx<GameRule.BX;xx++)
            {
                if(board[xx,y]==0)
                {
                    return false;
                }
            }
            return true;
        }

        internal void ClearBoard()
        {
            for(int xx=0;xx<GameRule.BX;xx++)
            {
                for(int yy=0;yy<GameRule.BY;yy++)
                {
                    board[xx, yy] = 0;
                }
            }
        }
    }
}

Game 형식 수정

 Next 메서드에서 Reset 상태의 벽돌이 시작 지점에 배치할 수 있는지 체크합니다. 이를 통해 게임의 종료 여부를 결정합니다. 메서드의 반환 형식도 수정해야 합니다.

        internal bool Next()
        {
            now.Reset();
            return gboard.MoveEnable(now.BlockNum, Turn, now.X, now.Y);
        }

 게임을 재시작하는 메서드를 제공합시다.

        internal void ReStart()
        {
            gboard.ClearBoard();
        }

 다음은 Game.cs 소스 파일의 코드 내용입니다.

using System.Drawing;

namespace 도형_이동
{
    class Game
    {
        Diagram now;
        Board gboard = Board.GameBoard;
        internal Point NowPosition
        {
            get
            {
                return new Point(now.X, now.Y);
            }
        }
        internal int BlockNum
        {
            get
            {
                return now.BlockNum;
            }
        }
        internal int Turn
        {
            get
            {
                return now.Turn;
            }
        }

        internal static Game Singleton
        {
            get;
            private set;
        }
        internal int this[int x,int y]
        {
            get
            {
                return gboard[x, y];
            }
        }
        static Game()
        {
            Singleton = new Game();
        }
        Game()
        {
            now = new Diagram();
        }
        internal bool MoveLeft()
        {               
            for(int xx=0;xx<4;xx++)
            {
                for(int yy=0;yy<4;yy++)
                {
                    if(BlockValue.bvals[now.BlockNum,Turn, xx,yy]!=0)
                    {
                        if (now.X + xx <= 0)
                        {
                            return false;
                        }
                    }
                }
            }

            if (gboard.MoveEnable(now.BlockNum, Turn, now.X - 1, now.Y))
            {
                now.MoveLeft();
                return true;
            }
            return false;
        }

        internal bool MoveRight()
        {
            for (int xx = 0; xx < 4; xx++)
            {
                for (int yy = 0; yy < 4; yy++)
                {
                    if (BlockValue.bvals[now.BlockNum, Turn, xx, yy] != 0)
                    {
                        if ((now.X + xx+1) >= GameRule.BX)
                        {
                            return false;
                        }
                    }
                }
            }
            if (gboard.MoveEnable(now.BlockNum, Turn, now.X + 1, now.Y))
            {
                now.MoveRight();
                return true;
            }
            return false;
        }

        internal bool MoveDown()
        {
            for (int xx = 0; xx < 4; xx++)
            {
                for (int yy = 0; yy < 4; yy++)
                {
                    if (BlockValue.bvals[now.BlockNum, Turn, xx, yy] != 0)
                    {
                        if ((now.Y + yy + 1) >=GameRule.BY)
                        {
                            gboard.Store(now.BlockNum, Turn, now.X, now.Y);
                            return false;
                        }
                    }
                }
            }
            if (gboard.MoveEnable(now.BlockNum, Turn, now.X, now.Y + 1))
            {
                now.MoveDown();
                return true;
            }
            gboard.Store(now.BlockNum, Turn, now.X, now.Y);
            return false;
        }

        internal bool MoveTurn()
        {
            for (int xx = 0; xx < 4; xx++)
            {
                for (int yy = 0; yy < 4; yy++)
                {
                    if (BlockValue.bvals[now.BlockNum, (Turn+1)%4, xx, yy] != 0)
                    {
                        if (((now.X + xx) < 0)|| ((now.X + xx) >= GameRule.BX)||((now.Y+yy)>=GameRule.BY))
                        {
                            return false;
                        }
                    }
                }
            }
            if (gboard.MoveEnable(now.BlockNum, (Turn + 1) % 4, now.X, now.Y))
            {
                now.MoveTurn();
                return true;
            }
            return false;
        }

        internal bool Next()
        {
            now.Reset();
            return gboard.MoveEnable(now.BlockNum, Turn, now.X, now.Y);
        }

        internal void ReStart()
        {
            gboard.ClearBoard();
        }
    }
}

Form1 형식 수정

 MoveDown 메서드에서 아래로 이동하지 못할 때 게임을 끝내야 하는지 체크하는 것으로 수정하세요.

        private void MoveDown()
        {
            if(game.MoveDown())
            {
                Region rg = MakeRegion(0, -1);
                Invalidate(rg);
            }
            else
            {
                EndingCheck();
            }
        }

 EndingCheck 메서드에서는 다음 도형이 가능한지 확인합니다. 만약 가능하지 않다면 종료 조건입니다. 타이머를 멈춘 후에 사용자에게 계속할 것인지 확인하고 이에 따라 다시 시작하거나 창을 닫습니다.

        private void EndingCheck()
        {
            if (game.Next())
            {
                Invalidate();
            }
            else
            {
                timer_down.Enabled = false;

                if (DialogResult.Yes == MessageBox.Show("계속 하실건가요?", "계속 진행 확인 창", MessageBoxButtons.YesNo))
                {
                    game.ReStart();
                    timer_down.Enabled = true;
                    Invalidate();
                }
                else
                {
                    this.Close();
                }
            }
        }

 Form1.cs 파일의 소스 코드입니다.

using System;
using System.Drawing;
using System.Windows.Forms;

namespace 도형_이동
{
    public partial class Form1 : Form
    {
        Game game;
        int bx;
        int by;
        int bwidth;
        int bheight;
        public Form1()
        {
            InitializeComponent();            
        }
        private void Form1_Load(object sender, EventArgs e)
        {            
            game = Game.Singleton;
            bx = GameRule.BX;
            by = GameRule.BY;
            bwidth = GameRule.B_WIDTH;
            bheight = GameRule.B_HEIGHT;
            this.SetClientSizeCore(GameRule.BX * GameRule.B_WIDTH, GameRule.BY * GameRule.B_HEIGHT);
        }

        private void Form1_Paint(object sender, PaintEventArgs e)
        {
            DoubleBuffered = true;
            DrawGraduation(e.Graphics);
            DrawDiagram(e.Graphics);
            DrawBoard(e.Graphics);
        }

        private void DrawBoard(Graphics graphics)
        {
            for(int xx=0;xx<bx;xx++)
            {
                for(int yy=0; yy<by;yy++)
                {
                    if(game[xx,yy]!=0)
                    {
                        Rectangle now_rt = new Rectangle(xx * bwidth + 2, yy * bheight + 2, bwidth - 4, bheight - 4);
                        graphics.DrawRectangle(Pens.Green, now_rt);
                        graphics.FillRectangle(Brushes.Red, now_rt);
                    }
                }
            }
        }

        private void DrawDiagram(Graphics graphics)
        {
            Pen dpen = new Pen(Color.Red, 4);
            Point now = game.NowPosition;
            int bn = game.BlockNum;
            int tn = game.Turn;
            for(int xx=0;xx<4;xx++)
            {
                for(int yy=0;yy<4;yy++)
                {
                    if(BlockValue.bvals[bn,tn,xx,yy]!=0)
                    {
                        Rectangle now_rt = new Rectangle((now.X+xx) * bwidth + 2, (now.Y+yy) * bheight + 2, bwidth - 4, bheight - 4);
                        graphics.DrawRectangle(dpen, now_rt);
                    }
                }
            }
        }

        private void DrawGraduation(Graphics graphics)
        {
            DrawHorizons(graphics);
            DrawVerticals(graphics);
        }

        private void DrawVerticals(Graphics graphics)
        {
            Point st = new Point();
            Point et = new Point();
            
            for (int cx = 0; cx < bx; cx++)
            {
                st.X = cx * bwidth;
                st.Y = 0;
                et.X = st.X;
                et.Y = by * bheight;

                graphics.DrawLine(Pens.Purple, st, et);
            }
        }

        private void DrawHorizons(Graphics graphics)
        {
            Point st = new Point();
            Point et = new Point();

            for (int cy = 0; cy < by; cy++)
            {
                st.X = 0;
                st.Y = cy * bheight;
                et.X = bx * bwidth;
                et.Y = cy * bheight;

                graphics.DrawLine(Pens.Green, st, et);
            }
        }



        private void Form1_KeyDown(object sender, KeyEventArgs e)
        {
            switch(e.KeyCode)
            {
                case Keys.Right: MoveRight(); return;
                case Keys.Left: MoveLeft(); return;
                case Keys.Space: MoveSSDown();return;
                case Keys.Up: MoveTurn();return;
                case Keys.Down: MoveDown(); return;
            }
        }

        private void MoveSSDown()
        {
            while (game.MoveDown())
            {
                Region rg = MakeRegion(0, -1);
                Invalidate(rg);
            }
            EndingCheck();    
        }

        private void MoveTurn()
        {
            if(game.MoveTurn())
            {
                Region rg = MakeRegion();
                Invalidate(rg);
            }
        }

        private void MoveDown()
        {
            if(game.MoveDown())
            {
                Region rg = MakeRegion(0, -1);
                Invalidate(rg);
            }
            else
            {
                EndingCheck();
            }
        }

        private void EndingCheck()
        {
            if (game.Next())
            {
                Invalidate();
            }
            else
            {
                timer_down.Enabled = false;

                if (DialogResult.Yes == MessageBox.Show("계속 하실건가요?", "계속 진행 확인 창", MessageBoxButtons.YesNo))
                {
                    game.ReStart();
                    timer_down.Enabled = true;
                    Invalidate();
                }
                else
                {
                    this.Close();
                }
            }
        }

        private void MoveLeft()
        {
            if (game.MoveLeft())
            {
                Region rg = MakeRegion(1, 0);
                Invalidate(rg);
            }
        }

        private Region MakeRegion(int cx, int cy)
        {
            Point now = game.NowPosition;

            int bn = game.BlockNum;
            int tn = game.Turn;
            Region region = new Region();
            for (int xx = 0; xx < 4; xx++)
            {
                for (int yy = 0; yy < 4; yy++)
                {
                    if (BlockValue.bvals[bn, tn, xx, yy] != 0)
                    {
                        Rectangle rect1 = new Rectangle((now.X + xx) * bwidth + 2, (now.Y + yy) * bheight + 2, bwidth - 4, bheight - 4);
                        Rectangle rect2 = new Rectangle((now.X + cx+xx) * bwidth, (now.Y + cy+yy) * bheight, bwidth, bheight);
                        Region rg1 = new Region(rect1);
                        Region rg2 = new Region(rect2);
                        region.Union(rg1);
                        region.Union(rg2);
                    }
                }
            }
            return region;
        }
        private Region MakeRegion()
        {
            Point now = game.NowPosition;
            int bn = game.BlockNum;
            int tn = game.Turn;
            int oldtn = (tn + 3) % 4;
            Region region = new Region();
            for (int xx = 0; xx < 4; xx++)
            {
                for (int yy = 0; yy < 4; yy++)
                {
                    if (BlockValue.bvals[bn, tn, xx, yy] != 0)
                    {
                        Rectangle rect1 = new Rectangle((now.X + xx) * bwidth + 2, (now.Y + yy) * bheight + 2, bwidth - 4, bheight - 4);
                        Region rg1 = new Region(rect1);
                        region.Union(rg1);                        
                    }
                    if (BlockValue.bvals[bn, oldtn, xx, yy] != 0)
                    {
                        Rectangle rect1 = new Rectangle((now.X + xx) * bwidth + 2, (now.Y + yy) * bheight + 2, bwidth - 4, bheight - 4);
                        Region rg1 = new Region(rect1);
                        region.Union(rg1);
                    }
                }
            }
            return region;
        }
        private void MoveRight()
        {
            if (game.MoveRight())
            {
                Region rg = MakeRegion(-1, 0);
                Invalidate(rg);
            }
        }

        private void timer_down_Tick(object sender, EventArgs e)
        {
            MoveDown();
        }
    }
}