[C# 프로젝트] 테트리스 만들기 – Part 2. 테트리스 도형 정의하기, 도형 회전하기

프로젝트 다운로드

 안녕하세요. 언휴예요.

 이번 강의는 “[언제나 프로젝트] 테트리스 Part2″입니다.

 현재 작업한 내용은 다음과 같습니다.

  •  게임 공간 정의
  • 키보드로 도형 이동(좌, 우, 아래)
  • 타이머로 도형 내리기

 이번 강의에서 다룰 내용은 다음과 같습니다.

  • 테트리스 도형 모양 정의
  • 회전

테트리스 도형 모양 정의

  •  테트리스 벽돌은 모두 7가지 입니다. 
  •  테트리스 벽돌은 90도로 회전하여 4가지 형태로 모양이 변할 수 있습니다.
  •  테트리스 벽돌은 4X4 공간에 총 4개의 돌이 공간을 차지합니다.

 테트리스 벽돌 모양은 모두 7가지 종류로 변하는 값이 아닙니다. 이를 읽기 전용으로 정의할 거예요. 그리고 벽돌 모양 정의는 별도의 형식에서 정의할게요. 이는 개체를 만들기 위함이 아니라 벽돌 모양 정의만 담당합니다. 이러한 용도로 형식을 정의할 때 정적 클래스로 정의하는 것을 권합니다.

 먼저 테스트를 위해 한 가지 벽돌을 먼저 정의한 후에 맨 마지막에 7가지 벽돌을 정의합시다.

namespace 테트리스_만들기
{
    static class BlockValue
    {
        static public readonly int[,,,] bvals = new int[1,4, 4, 4]
        {
            {
                {
                    {0,0,1,0 },
                    {0,0,1,0 },
                    {0,0,1,0 },
                    {0,0,1,0 }
                },
                {
                    {0,0,0,0 },
                    {0,0,0,0 },
                    {1,1,1,1 },
                    {0,0,0,0 }
                },
                {
                    {0,0,1,0 },
                    {0,0,1,0 },
                    {0,0,1,0 },
                    {0,0,1,0 }
                },
                {
                    {0,0,0,0 },
                    {0,0,0,0 },
                    {1,1,1,1 },
                    {0,0,0,0 }
                }
            }
        };
    }
}

벽돌 형식 수정

 벽돌 형식에는 현재 어떤 모양의 벽돌인지 정보가 필요합니다. 그리고 여러 개의 벽돌 모양과 회전을 추가 구현할 것입니다. 따라서 벽돌 형식에는 현재 회전 정도를 기억하는 멤버를 요구합니다.

        internal int Turn
        {
            get;
            private set;
        }
        internal int BlockNum
        {
            get;
            private set;
        }

 도형 상태를 초기 상태로 만드는 Reset메서드에 회전 값을 랜덤으로 지정하는 코드와 도형 모양을 선택하는 코드를 작성합니다. 

현재 도형 모양은 1개만 정의하였습니다. 이에 맞게 정의하고 7개 모두 정의한 후에 수정하세요.

        internal void Reset()
        {
            Random random = new Random();
            X = GameRule.SX;
            Y = GameRule.SY;
            Turn = random.Next() % 4;
            BlockNum = 0;//random.Next()%7;
        }

 회전을 위한 메서드를 정의합니다. 나머지 연산을 이용하면 원하는 범위 값에서 회전할 수 있어요.

        internal void MoveTurn()
        {
            Turn = (Turn + 1) % 4;
        }

 현재까지 작성한 벽돌 모양을 정의한 소스 코드입니다.

using System;

namespace 테트리스_만들기
{
    class Diagram
    {        
        internal int X
        {
            get;
            private set;
        }
        internal int Y
        {
            get;
            private set;
        }
        internal int Turn
        {
            get;
            private set;
        }
        internal int BlockNum
        {
            get;
            private set;
        }
        
        internal Diagram()
        {
            Reset();
        }
        internal void Reset()
        {
            Random random = new Random();
            X = GameRule.SX;
            Y = GameRule.SY;
            Turn = random.Next() % 4;
            BlockNum = 0;//random.Next()%7;
        }
        internal void MoveLeft()
        {
            X--;
        }
        internal void MoveRight()
        {
            X++;
        }
        internal void MoveDown()
        {
            Y++;
        }
        internal void MoveTurn()
        {
            Turn = (Turn + 1) % 4;
        }
    }
}

Game 형식 수정

  현재 벽돌의 종류와 회전 정도를 알 수 있게 속성을 정의합니다.

        internal int BlockNum
        {
            get
            {
                return now.BlockNum;
            }
        }
        internal int Turn
        {
            get
            {
                return now.Turn;
            }
        }

 회전에 관한 기능을 정의합니다. 앞에서 작성했던 MoveLeft, MoveRight, MoveTurn을 참고하세요.

        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;
                        }
                    }
                }
            }
            now.MoveTurn();
            return true;
        }

 벽돌이 아래로 이동할 때 더 이상 이동하지 못하면 벽돌의 상태를 초기 상태로 설정합니다. 이 때 필요한 메서드를 제공합시다.

        internal void Next()
        {
            now.Reset();
        }

 현재까지 작성한 Game.cs 소스 코드입니다.

using System.Drawing;

namespace 테트리스_만들기
{
    class Game
    {
        Diagram now;
        internal Point NowPosition
        {
            get
            {
                if(now == null)
                {
                    return new Point(0, 0);
                }
                return new Point(now.X, now.Y);
            }
        }
        internal int BlockNum
        {
            get
            {
                return now.BlockNum;
            }
        }
        internal int Turn
        {
            get
            {
                return now.Turn;
            }
        }
        #region 단일체
        internal static Game Singleton
        {
            get;
            private set;
        }
        static Game()
        {
            Singleton = new Game();
        }
        Game()
        {
            now = new Diagram();
        }
        #endregion

        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;
                        }
                    }
                }
            }

            now.MoveLeft();
            return true;
        }

        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;
                        }
                    }
                }
            }
            now.MoveRight();
            return true;
        }

        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)
                        {
                            return false;
                        }
                    }
                }
            }
            now.MoveDown();
            return true;
        }
        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;
                        }
                    }
                }
            }
            now.MoveTurn();
            return true;
        }

        internal void Next()
        {
            now.Reset();
        }
    }
}

폼 수정

 이전 강의에서는 사각형 하나를 그렸습니다. 이제 벽돌 모양을 4X4 형태로 정의하였으니 이에 맞게 수정합시다.  현재 벽돌 모양 값과 회전 값도 얻어와야 현재 벽돌 값을 표현할 수 있어요.

        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);
                        //graphics.FillRectangle(Brushes.Green, now_rt);
                    }
                }
            }
        }

 비어있었던 “회전(MoveTurn)” 기능을 구현하세요. 단지 회전을 시도한 후에 성공했으면 다시 그려줄 영역을 얻어와서 무효화 시키는 일입니다.

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

 MoveDown 메서드에서 이동하지 못하였을 때 벽돌의 상태를 다음으로 설정하는 코드를 추가합니다.

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

 회전할 때 다시 그려야 할 영역을 계산하는 알고리즘은 다른 이동과 약간의 차이가 있습니다. 좌표 변경 없이 도형 모양이 바뀌는 것이니 이를 고려하여 작성하세요. 이미 앞에서 작성한 메서드를 참고하세요.

        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;
        }

 현재까지 작성한 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);
        }

        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);
                        //graphics.FillRectangle(Brushes.Green, 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: MoveDown();return;
                case Keys.Up: MoveTurn();return;
            }
        }

        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
            {
                game.Next();
                Invalidate();
            }
        }
        
        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();
        }
    }
}