[C# 프로젝트] 테트리스 만들기 – Part 1. 키보드로 도형 제어하기, 타이머로 도형 아래로 이동

안녕하세요. 언휴예요.

이번 강의는 미니 프로젝트 “테트리스” 만들기 중에 첫 번째 파트입니다.

테트리스는 총 4개의 파트로 나누어져 있으며 동영상 강의 기준으로 80여분 요구합니다.

이번 강의에서는 사각형 하나를 키보드로 이동시키고 타이머로 내리기입니다.

GameRule 만들기

먼저 게임의 보드 공간의 폭과 너비, 게임 좌표, 시작 좌표를 정의할게요.

namespace 테트리스_만들기
{
    static class GameRule//동강에서는 static 클래스가 아닌 것으로 표현했었요.
    {
        internal const int B_WIDTH = 30;//게임 X좌표 1의 Pixel수
        internal const int B_HEIGHT = 30;//게임 Y좌표 1의 Pixel수
        internal const int BX = 12;//게임 보드의 폭(B_WIDTH*BX Pixels)
        internal const int BY = 20;//게임 보드의 높이(B_HEIGHT*BX Pixels)
        internal const int SX = 4;//시작 S 좌표
        internal const int SY = 0; //시작 Y 좌표
    }
}

벽돌 정의

 이제 벽돌을 정의합시다. 이번 강의 Part1에서는 벽돌 모양과 회전은 반영하지 않습니다. 화면에서도 단순히 사각형을 표시합니다.

 벽돌 형식을 Diagram 클래스로 정의할게요. 

    class Diagram
    {
    }

이번 강의에서 벽돌을 좌, 우, 아래로 이동시키는 실습입니다. 벽돌에는 현재 X, Y 좌표를 속성으로 캡슐화 합시다. 가져오기 블록은 다른 형식에서 접근할 수 있게 정의할게요.

        internal int X
        {
            get;
            private set;
        }
        internal int Y
        {
            get;
            private set;
        }

생성자에서는 시작 좌표로 설정합니다. 차후에 벽돌이 맨 아래에 이동 후에 새로운 벽돌이 시작 좌표로 설정하는 부분이 필요하므로 별도의 메서드를 만들어 호출하는 형태로 작성할게요.

        internal Diagram()
        {
            Reset();
        }

        internal void Reset()
        {
            X = GameRule.SX;
            Y = GameRule.SY;
        }

벽돌이 왼쪽, 오른쪽, 아래로 이동하는 메서드를 제공합시다.

        internal void MoveLeft()
        {
            X--;
        }
        internal void MoveRight()
        {
            X++;
        }
        internal void MoveDown()
        {
            Y++;
        }

 다음은 벽돌을 정의한 Diagram.cs 소스 코드입니다.

namespace 테트리스_만들기
{
    class Diagram
    {
        internal int X
        {
            get;
            private set;
        }
        internal int Y
        {
            get;
            private set;
        }
        internal Diagram()
        {
            Reset();
        }

        internal void Reset()
        {
            X = GameRule.SX;
            Y = GameRule.SY;
        }
        internal void MoveLeft()
        {
            X--;
        }
        internal void MoveRight()
        {
            X++;
        }
        internal void MoveDown()
        {
            Y++;
        }
    }
}

Game 클래스 정의

 Game 형식 개체는 단 하나만 존재하기에 단일체로 표현할게요. 

 생성자은 디폴트 가시성인 private으로 접근 지정하고 정적 멤버로 단일체를 생성합니다. 여기에서는 정적 생성자에서 단일체를 생성할게요.

    class Game
    {
        #region 단일체
        internal static Game Singleton
        {
            get;
            private set;
        }
        static Game()
        {
            Singleton = new Game();
        }
        Game()
        {
        }
        #endregion
    }

 멤버 필드로 벽돌 개체를 참조할 now를 선언하고 now의 현재 좌표를 속성으로 접근할 수 있게 정의할게요.

        Diagram now;
        internal Point NowPosition
        {
            get
            {
                if(now == null)
                {
                    return new Point(0, 0);
                }
                return new Point(now.X, now.Y);
            }
        }

 생성자에서 벽돌 개체를 생성하는 구문을 추가하세요.

        Game()
        {
            now = new Diagram();
        }

 벽돌을 왼쪽, 오른쪽, 아래 방향으로 이동하는 메서드를 제공합니다. 각 메서드에서는 현재 좌표에서 이동할 좌표가 보드 공간 좌표 이내인지 확인하여 이동 가능하면 벽돌을 이동시킵니다.

        internal bool MoveLeft()
        {
            if(now.X>0)
            {
                now.MoveLeft();
                return true;
            }
            return false;
        }

        internal bool MoveRight()
        {
            if ((now.X+1) <GameRule.BX)
            {
                now.MoveRight();
                return true;
            }
            return false;
        }

        internal bool MoveDown()
        {
            if ((now.Y+1) < GameRule.BY)
            {
                now.MoveDown();
                return true;
            }
            return false;
        }

 다음은 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);
            }
        }
        #region 단일체
        internal static Game Singleton
        {
            get;
            private set;
        }
        static Game()
        {
            Singleton = new Game();
        }
        Game()
        {
            now = new Diagram();
        }
        #endregion

        internal bool MoveLeft()
        {
            if(now.X>0)
            {
                now.MoveLeft();
                return true;
            }
            return false;
        }

        internal bool MoveRight()
        {
            if ((now.X+1) <GameRule.BX)
            {
                now.MoveRight();
                return true;
            }
            return false;
        }

        internal bool MoveDown()
        {
            if ((now.Y+1) < GameRule.BY)
            {
                now.MoveDown();
                return true;
            }
            return false;
        }
    }
}

Form 구현

이제 사용자와 상호 작용하는 Form (Form1 클래스)을 작성합시다.

 폼에서 자주 사용할 값을 멤버 필드로 선언할게요.

    public partial class Form1 : Form
    {
        Game game;
        int bx;//보드 폭
        int by;//보드 높이
        int bwidth; //X좌표 1의 x Pixels
        int bheight;//Y좌표 1의 y Pixels
        public Form1()
        {
            InitializeComponent();
        }
    }

Load 이벤트 핸들러를 등록하여 멤버 필드를 초기 설정합니다. GameRule 형식의 상수 멤버를 이용하여 설정하세요. 그리고 클라이언트 크기를 SetClientSizeCore 메서드를 호출하여 설정합니다.

        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;
            SetClientSizeCore(bx * bwidth, by * bheight);
        }

 Paint 이벤트 핸들러를 등록합니다. 그리고 여기에서는 모눈을 그리는 메서드와 벽돌을 그리는 메서드를 호출합니다. 물론 두 개의 메서드를 새로 추가합니다.

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

        private void DrawDiagram(Graphics graphics)
        {
        }

        private void DrawGraduation(Graphics graphics)
        {
        }

 벽돌은 현재 좌표를 얻어온 후에 Graphics 개체를 이용하여 사각형으로 그릴게요. 여기서는 사각형을 채우지 않았지만 원하시면 FillRectangle 메서드를 호출하세요.

        private void DrawDiagram(Graphics graphics)
        {
            Pen dpen = new Pen(Color.Red, 4);
            Point now = game.NowPosition;
            Rectangle now_rt = new Rectangle(now.X * bwidth+2, now.Y * 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 = st.Y;
                graphics.DrawLine(Pens.Green, st, et);
            }
        }

 KeyDown 이벤트 핸들러를 등록하여 키에 따라 오른쪽, 왼쪽, 아래로, 회전하는 메서드를 호출하게 구현하세요.

        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()
        {            
        }

        private void MoveDown()
        {
        }

        private void MoveLeft()
        {
        }

        private void MoveRight()
        {
        }

 아래로 이동하는 메서드는 먼저 도형이 아래로 이동이 가능한지 확인 후에 다시 그려주어야 할 영역을 계산하여 무효화 시킵니다.

        private void MoveTurn()
        {
            
        }

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

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

        private void MoveRight()
        {
            if (game.MoveRight())
            {
                Region rg = MakeRegion(-1, 0);
                Invalidate(rg);
            }
        }
        private Region MakeRegion(int cx, int cy)
        {
            return null;
        }

 다시 그려주어야 할 영역은 이전 벽돌이 있었던 사각형과 새롭게 그릴 사각형 영역입니다. 두 영역을 생성 후에 Union 메서드로 영역을 합치세요.

        private Region MakeRegion(int cx, int cy)
        {
            Point now = game.NowPosition;
            Rectangle rect1 = new Rectangle(now.X * bwidth, now.Y * bheight, bwidth, bheight);
            Rectangle rect2 = new Rectangle((now.X+cx) * bwidth, (now.Y+cy) * bheight, bwidth, bheight);
            Region rg1 = new Region(rect1);
            Region rg2 = new Region(rect2);
            rg1.Union(rg2);
            return rg1;
        }

 타이머를 Form1에 배치하세요. 도구 상자에서 끌어다 놓으시거나 Load 이벤트 핸들러에 타이머 개체를 생성하는 코드를 작성하세요. 여기에서는 도구 상자에서 끌어다 놓았을 때의 코드로 작성할게요.

속성 창에서 Timer 개체의 변수 명을 timer_down으로 지정할게요. Enable 속성을 True로 설정하시고 Tick 이벤트 핸들러를 등록하세요.

 그리고 Tick 이벤트 핸들러에서 MoveDown 메서드를 호출합니다.

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

 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;
            SetClientSizeCore(bx * bwidth, by * bheight);
        }

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

        private void DrawDiagram(Graphics graphics)
        {
            Pen dpen = new Pen(Color.Red, 4);
            Point now = game.NowPosition;
            Rectangle now_rt = new Rectangle(now.X * bwidth+2, now.Y * 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 = st.Y;
                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()
        {
            
        }

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

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

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

        private Region MakeRegion(int cx, int cy)
        {
            Point now = game.NowPosition;
            Rectangle rect1 = new Rectangle(now.X * bwidth, now.Y * bheight, bwidth, bheight);
            Rectangle rect2 = new Rectangle((now.X+cx) * bwidth, (now.Y+cy) * bheight, bwidth, bheight);
            Region rg1 = new Region(rect1);
            Region rg2 = new Region(rect2);
            rg1.Union(rg2);
            return rg1;
        }

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

 이상으로 테트리스 프로젝트 Part1 강의를 마칠게요.

프로젝트 파일 다운로드받기