NGMsoftware

NGMsoftware
로그인 회원가입
  • 커뮤니티
  • 자유 게시판
  • 커뮤니티

    자유롭게 글을 작성할 수 있는 게시판입니다.

    자유 게시판

    자유롭게 글을 작성할 수 있는 게시판입니다.


    소프트웨어 테트리스 게임 만들기. (소스 포함)

    페이지 정보

    본문

    안녕하세요. 소심비형입니다. 부록으로 간단한 캐주얼 게임들을 추가해 보려고 합니다. 첫번째로 테트리스구요. 다들 아시는 가위 바위 보나 사다리 타기메모리 셔플등등... 시간날 때마다 하나씩 만들어 보겠습니다. 요 몇일 테트리스를 어느정도 만들어 두느라 글을 하나도 못 적었네요. 아직 몇가지 구현은 하지 않았습니다만, 아래 동영상처럼 동작하게 됩니다. (버전이 여러개라서 아래 동영상과 완전 동일하지는 않습니다. 전체 소스는 첨부파일을 다운로드 받으세요.)

     

     

    솔루션 탐색기에서 솔루션의 루트에 Games 폴더를 하나 추가합니다. 그리고, 아래 그림처럼 Game.Tetris 프로젝트를 추가하세요. 이 프로젝트는 WinForm입니다.

    QUUqzYm.png

     

     

    게임의 보드판이 될 MainView와 MainControl을 추가합니다. MainView에서 디자인해도 되지만 우리는 MainControl에서 작업할겁니다. MainView는 윈폼이며, MainControl은 유저 컨트롤입니다. 유저 컨트롤에 작업하는 이유는 좀 더 이식성을 높이기 위한 방법이므로 크게 신경쓰지 않아도 됩니다.

    FCBVBqr.png

     

     

    앞으로 쓰여질 View들을 아래와 같이 미리 생성합니다. 각 뷰의 기능은 아래와 같습니다.

     InfoView 

     이 응용 프로그램의 간단한 소개를 Popup하는 View. 

     MainView or MainControl 

     테트리스 게임의 보드판.

     ManualView 

     테트리스 게임의 조작법 설명서.

     RankView 

     테트리스 게임 플레이어의 점수별 순위.

     

     

    MainView 또는 MainControl의 디자인을 아래와 같이 만듭니다.

    QedEgGR.png

     

     

    Game의 메뉴 구성은 기호에 맞게 작성해도 됩니다. 코드상에 구현되어 있으므로 MainView.cs.designer를 확인하면 될거 같습니다. Options와 Help의 메뉴도 직접 구성해보세요. 이제 테트리스에서 필요한 오브젝트 모델들을 생성하고, 각 오브젝트가 해야 할 비헤이비어를 추가해야 합니다. 메뉴에 있는 부수적인 기능들은 핵심 로직과는 상관 없으므로 차차 추가하기로 하고 우선은 모델을 만들도록 하겠습니다. 모델은 크게 3개로 나눌 수 있습니다. 테트리스의 가장 작은 단위인 셀과 셀의 조합으로 이루어진 블럭, 게임 보드판입니다. 아래와 같이 CellModel, BlockModel, BoardModel 클래스를 추가하세요.

    jZSn2ST.png

     

     

    우선, CellModel.cs부터 코드를 추가해 나가야겠죠. 아래는 전체 소스입니다. 강조 표시된 126라인부터 130라인이 중요합니다. 우선, 파라미터로 넘어오는 isCurrent가 false인 경우에만 동작하는 코드로 실제로 게임이 실행되는 영역이 아닌 Next Block을 표시하는 Panel에 블럭을 그리기 위한 루틴입니다. 그런데, 왜 코드가 복잡해 보이냐면...-_-; 애초에 설계를 잘 못한 부분이 있지 않나 생각되기도하고 급하게 만드느라 리팩토링도 없이 막 코딩부터 들어갔기에 그렇다고 말하고 싶군요. 아무튼, 셀이 아닌 블럭이 Next Block을 표시하는 Panel 컨트롤의 중간에 Block이 보여지게 하는 코드입니다.

    CellModel.cs

    using System;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    
    namespace Game.Tetris.Models
    {
        public class CellModel
        {
            private Point _point;
            private Size _size;
    
            public BlockModel Parent { get; set; }
            public Point Point { get { return _point; } }
            public int X { get { return _point.X; } set { _point.X = value; } }
            public int Y { get { return _point.Y; } set { _point.Y = value; } }
            public Size Size { get { return _size; } }
            public int Width { get { return _size.Width; } set { _size.Width = value; } }
            public int Height { get { return _size.Height; } set { _size.Height = value; } }
            /// <summary>
            /// 셀의 시작되는 컬러를 설정하거나 가져옵니다.
            /// <para>StartColor, EndColor를 이용하여 Gradient효과를 나타낼 수 있습니다.</para>
            /// </summary>
            public Color StartColor { get; set; }
            /// <summary>
            /// 셀의 끝나는 컬러를 설정하거나 가져옵니다.
            /// <para>StartColor, EndColor를 이용하여 Gradient효과를 나타낼 수 있습니다.</para>
            /// </summary>
            public Color EndColor { get; set; }
            /// <summary>
            /// 셀의 라인 컬러를 설정하거나 가져옵니다.
            /// </summary>
            public Color LineColor { get; set; }
    
            /// <summary>
            /// Constructor
            /// </summary>
            /// <param name="parent">이 셀이 속한 블럭입니다.</param>
            /// <param name="point">셀의 2차원 좌표입니다.</param>
            /// <param name="size">셀의 크기입니다.</param>
            /// <param name="startColor">셀의 내부를 채울 때 그라디언트의 시작 색상입니다.</param>
            /// <param name="endColor">셀의 내부를 채울 때 그라디언트의 마지막 색상입니다.</param>
            /// <param name="lineColor">셀 외각선의 색상입니다.</param>
            public CellModel(BlockModel parent, Point point, Size size, Color startColor, Color endColor, Color lineColor)
            {
                // 초기화 해야 할 변수들은 여기에서 처리한다.
                this.Parent = parent;
                this._point = point;
                this._size = size;
                this.StartColor = startColor;
                this.EndColor = endColor;
                this.LineColor = lineColor;
            }
    
            /// <summary>
            /// GDI+를 이용하여 셀을 그려줍니다.
            /// </summary>
            /// <param name="g">화면을 그리는 그래픽 개체입니다.</param>
            /// <param name="isCurrent">게임중인 Block에 속한 Cell이면 true, Next Block에 속한 Cell이면 false입니다.</param>
            public void Draw(Graphics g, bool isCurrent)
            {
                int x, y, width, height;
    
                if (isCurrent)
                {
                    width = Width;
                    height = Height;
                    x = X * Width;
                    y = Y * Height;
                }
                else
                {
                    width = (int)(Width * 0.7);
                    height = (int)(Height * 0.7);
                    x = Parent.Board.Parent.NextBlockBoard.Location.X + X / 2 * width;
                    y = Parent.Board.Parent.NextBlockBoard.Location.Y + (height * 3) - (Math.Abs(Y) * height);
                }
    
                Rectangle rect = new Rectangle(x, y, width, height);
                LinearGradientBrush lgb = new LinearGradientBrush(rect, StartColor, EndColor, 45f, true);
                g.FillRectangle(lgb, rect);
                g.DrawRectangle(new Pen(LineColor), rect);
                lgb.Dispose();
            }
        }
    }

     

     

    Block을 코딩하기 전에 블럭들을 찍어낼 수 있는 공장을 먼저 추가해야 합니다. 아래와 같이 Services에 BlockFactory.cs 클래스를 추가하고 블럭들을 생성합니다.

    BlockFactory.cs

    using System.Collections.Generic;
    using System.Drawing;
    
    namespace Game.Tetris.Services
    {
        /// <summary>
        /// 블럭 형식에 맞는 블럭을 생성합니다.
        /// </summary>
        public class BlockFactory
        {
            /// <summary>
            /// 블럭 형식에 따라 새로운 블럭을 생성하고 생성된 블럭을 반환합니다.
            /// </summary>
            /// <param name="block">형식을 가지는 블럭입니다.</param>
            /// <returns>블럭 형식으로 생성된 셀의 집합을 반환합니다.</returns>
            public static List<Models.CellModel> CreateInstance(Models.BlockModel block)
            {
                List<Models.CellModel> result = new List<Models.CellModel>();
                Models.CellModel cell;
                Color startColor;
                Color endColor;
                Color lineColor;
                Size cellSize = new Size(20, 20);
                int boardCenter = (block.Board.HorizontalLength - 1) / 2;
    
                switch (block.BlockType)
                {
                    case Models.BlockType.Square:
                        startColor = Color.Yellow;
                        endColor = Color.Orange;
                        lineColor = Color.Gray;
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
                        break;
                    case Models.BlockType.B:
                        startColor = Color.Red;
                        endColor = Color.DarkRed;
                        lineColor = Color.Gray;
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -3), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
                        break;
                    case Models.BlockType.C:
                        startColor = Color.SkyBlue;
                        endColor = Color.Blue;
                        lineColor = Color.Gray;
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -3), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
                        break;
                    case Models.BlockType.D:
                        startColor = Color.Green;
                        endColor = Color.LightGreen;
                        lineColor = Color.Gray;
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -3), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
                        break;
                    case Models.BlockType.Stick:
                        startColor = Color.Lime;
                        endColor = Color.LimeGreen;
                        lineColor = Color.Gray;
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -4), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -3), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
                        break;
                    case Models.BlockType.F:
                        startColor = Color.White;
                        endColor = Color.Gray;
                        lineColor = Color.MidnightBlue;
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -3), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
                        break;
                    case Models.BlockType.G:
                        startColor = Color.Pink;
                        endColor = Color.Purple;
                        lineColor = Color.Gray;
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -3), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -2), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter + 1, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
    
                        cell = new Models.CellModel(block, new Point(boardCenter, -1), cellSize, startColor, endColor, lineColor);
                        result.Add(cell);
                        break;
                    default:
                        break;
                }
    
                return result;
            }
        }
    }

     

     

    블럭 타입(형식)에 따라 셀을 자동으로 생성하여 반환하는 Factory입니다. 테트리스 블럭을 추가하거나, 기존 모양을 수정하고 싶으면, 이 팩토리 클래스를 수정하거나, 상속하여 확장하면 될거 같습니다. 이제 테트리스의 핵심이 되는 블럭을 만들 차례입니다. Behavior를 분리할까도 생각했었는데요. 코드량이 그리 많지도 않을뿐더러 계속 기능을 추가하거나 수정할 일이 발생하지 않을거 같아서 그냥 하나의 클래스에 모든 기능을 담았습니다. 블럭의 명칭을 어떻게 할까 고민하다가 시간상 그냥 A, B, C, D, E... 이렇게 했습니다. 몇가지 명확한 모양(Square, Stick...)은 이름을 지어주었구요^^;

    BlockModel.cs

    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    
    namespace Game.Tetris.Models
    {
        /// <summary>
        /// 블럭의 모양을 나타냅니다.
        /// </summary>
        public enum BlockType
        {
            /// <summary>
            /// 정사각형의 블럭입니다.
            /// </summary>
            Square,
            B,
            C,
            D,
            /// <summary>
            /// 긴 막대 모양의 블럭입니다.
            /// </summary>
            Stick,
            F,
            G
        }
    
        /// <summary>
        /// 블럭의 동작을 나타냅니다.
        /// </summary>
        public enum ActionType
        {
            /// <summary>
            /// 블럭이 회전하는 상태를 나타냅니다.
            /// </summary>
            Rotate,
            /// <summary>
            /// 블럭이 왼쪽으로 이동하는 상태를 나타냅니다.
            /// </summary>
            MoveLeft,
            /// <summary>
            /// 블럭이 오른쪽으로 이동하는 상태를 나타냅니다.
            /// </summary>
            MoveRight,
            /// <summary>
            /// 블럭이 아래로 이동하는 상태를 나타냅니다.
            /// </summary>
            MoveDown
        }
    
        public class BlockModel
        {
            public int Width { get; set; }
            public int Height { get; set; }
            public BoardModel Board { get; set; }
            public List<CellModel> Cells { get; set; }
            /// <summary>
            /// 블럭의 모양을 결정하는 타입을 가져오거나 설정합니다.
            /// </summary>
            public BlockType BlockType { get; set; }
    
            public BlockModel(BoardModel board, BlockType type)
            {
                this.Board = board;
                this.BlockType = type;
                this.Cells = Services.BlockFactory.CreateInstance(this);
    
                this.Width = Cells.Select(c => c.X).Distinct().Count() * Cells[0].Width;
                this.Height = Cells.Select(c => c.Y).Distinct().Count() * Cells[0].Height;
            }
    
            public void Left()
            {
                if (CanAction(ActionType.MoveLeft))
                {
                    foreach (CellModel cell in Cells)
                    {
                        cell.X = cell.X - 1;
                    }
                }
            }
    
            /// <summary>
            /// 블럭을 오른쪽으로 이동합니다.
            /// </summary>
            public void Right()
            {
                if (CanAction(ActionType.MoveRight))
                {
                    foreach (CellModel cell in Cells)
                    {
                        cell.X = cell.X + 1;
                    }
                }
            }
    
            /// <summary>
            /// 블럭을 아래로 이동합니다.
            /// </summary>
            public void Down()
            {
                if (CanAction(ActionType.MoveDown))
                {
                    foreach (CellModel cell in Cells)
                    {
                        cell.Y = cell.Y + 1;
                    }
                }
                else
                {
                    BuildBlock();
                }
            }
    
            public void LastDown()
            {
                while (true)
                {
                    if (CanAction(ActionType.MoveDown))
                    {
                        Down();
                    }
                    else
                    {
                        break;
                    }
                }
            }
    
            /// <summary>
            /// 블럭을 회전합니다.
            /// </summary>
            public void Rotate()
            {
                if (this.BlockType == Models.BlockType.Square)
                {
                    return;
                }
    
                if (!CanAction(ActionType.Rotate))
                {
                    return;
                }
    
                foreach (CellModel cell in Cells)
                {
                    int cellX = cell.X;
                    int cellY = cell.Y;
    
                    cell.Y = cellX + Cells.Where(c => c.Y > -Board.VerticalLength).FirstOrDefault().Y - Cells.Where(c => c.X > -Board.HorizontalLength).FirstOrDefault().X;
                    cell.X = Cells.Where(c => c.X < Board.HorizontalLength).FirstOrDefault().X + Cells.Where(c => c.Y > -Board.VerticalLength).FirstOrDefault().Y - cellY;
                }
            }
    
            /// <summary>
            /// 블럭이 동작을 할 수 있는지 여부를 반환합니다.
            /// </summary>
            /// <param name="actionType"><see cref="ActionType"/></param>
            /// <returns>블럭이 이동하거나 회전이 가능하면 true를 반환합니다.</returns>
            private bool CanAction(ActionType actionType)
            {
                bool result = true;
    
                int cellX = -1;
                int cellY = -1;
    
                foreach (CellModel cell in Cells)
                {
                    switch (actionType)
                    {
                        case ActionType.Rotate:
                            cellX = Cells.Where(c => c.X < Board.HorizontalLength).FirstOrDefault().X + Cells.Where(c => c.Y > -Board.VerticalLength).FirstOrDefault().Y - cell.Y;
                            cellY = cell.X + Cells.Where(c => c.Y > -Board.VerticalLength).FirstOrDefault().Y - Cells.Where(c => c.X > -Board.HorizontalLength).FirstOrDefault().X;
                            break;
                        case ActionType.MoveRight:
                            cellX = cell.X + 1;
                            cellY = cell.Y;
                            break;
                        case ActionType.MoveLeft:
                            cellX = cell.X - 1;
                            cellY = cell.Y;
                            break;
                        case ActionType.MoveDown:
                            cellX = cell.X;
                            cellY = cell.Y + 1;
                            break;
                    }
    
                    if (cellY < 0)
                    {
                        cellY = 0;
                    }
    
                    if (cellX < 0 || cellX >= Board.HorizontalLength || cellY >= Board.VerticalLength || Board.Grid[cellX, cellY] != null)
                    {
                        result = false;
                        break;
                    }
                }
    
                return result;
            }
    
            /// <summary>
            /// 블럭을 쌓아줍니다.
            /// </summary>
            private void BuildBlock()
            {
                foreach (CellModel cell in Cells)
                {
                    if (cell.Y <= 0)
                    {
                        Board.Parent.GameOver();
                        return;
                    }
    
                    Board.Grid[cell.X, cell.Y] = cell;
                }
                LineDelete();
                Board.Block = null;
            }
    
            /// <summary>
            /// 라인을 삭제합니다.
            /// </summary>
            private void LineDelete()
            {
                int deleteLineCount = 0;
    
                for (int y = 0; y < Board.VerticalLength; y++)
                {
                    bool isDelete = true;
    
                    for (int x = 0; x < Board.HorizontalLength; x++)
                    {
                        if (Board.Grid[x, y] == null)
                        {
                            isDelete = false;
                            break;
                        }
                    }
    
                    if (isDelete)
                    {
                        deleteLineCount++;
    
                        for (int x = 0; x < Board.HorizontalLength; x++)
                        {
                            Board.Grid[x, y] = null;
                        }
    
                        // 블럭들을 한칸씩 아래로 이동
                        for (int moveX = 0; moveX < Board.HorizontalLength; moveX++)
                        {
                            for (int moveY = y; moveY > 1; --moveY)
                            {
                                Board.Grid[moveX, moveY] = Board.Grid[moveX, moveY - 1];
                                if (Board.Grid[moveX, moveY] != null)
                                {
                                    Board.Grid[moveX, moveY].X = moveX;//새로운 X좌표값 대입
                                    Board.Grid[moveX, moveY].Y = moveY;//새로운 Y좌표값 대입
                                }
                            }
                        }
    
                        System.Threading.Thread.Sleep(100);
                    }
                }
    
                if (deleteLineCount > 0)
                {
                    Board.Parent.DeleteLine(deleteLineCount);
                }
            }
    
            public void Draw(Graphics g, bool isCurrent)
            {
                foreach (CellModel cell in Cells)
                {
                    cell.Draw(g, isCurrent);
                }
            }
        }
    }

     

     

    인터넷에서 적당한 Sound effect를 다운로드 하여 Resources에 추가 시킵니다.

    YLThZq1.png

     

     

    실제 게임이 이루어지는 보드판에 대한 전체 코드는 아래와 같습니다.

    BoardModel.cs

    namespace Game.Tetris.Models
    {
        public class BoardModel
        {
            public MainControl Parent { get; set; }
            public BlockModel Block { get; set; }
            public BlockModel NextBlock { get; set; }
            public int HorizontalLength { get; set; }
            public int VerticalLength { get; set; }
            public CellModel[,] Grid { get; set; }
    
            public BoardModel(MainControl parent, int horizontalLength, int verticalLength)
            {
                this.Parent = parent;
                this.HorizontalLength = horizontalLength;
                this.VerticalLength = verticalLength;
                this.Grid = new CellModel[horizontalLength, verticalLength];
            }
    
            /// <summary>
            /// 모든 블럭을 삭제합니다.
            /// </summary>
            public void Clear()
            {
                for (int x = 0; x < HorizontalLength; x++)
                {
                    for (int y = 0; y < VerticalLength; y++)
                    {
                        Grid[x, y] = null;
                    }
                }
    
                Block = null;
                NextBlock = null;
            }
    
            /// <summary>
            /// 그리드에 있는 블럭을 그려줍니다.
            /// </summary>
            /// <param name="g"></param>
            public void Draw(System.Drawing.Graphics g)
            {
                for (int x = 0; x < HorizontalLength; x++)
                {
                    for (int y = 0; y < VerticalLength; y++)
                    {
                        if (Grid[x, y] != null)
                        {
                            Grid[x, y].Draw(g, true);
                        }
                    }
                }
            }
        }
    }

     

     

    이제 게임을 실행하는 View를 코딩합니다.

    MainView.cs

    using System;
    using System.Drawing;
    using System.Threading;
    using System.Windows.Forms;
    
    namespace Game.Tetris
    {
        public partial class MainControl : UserControl
        {
            private System.Windows.Forms.Timer _gameTimer;
            private System.Windows.Forms.Timer _pauseTimer;
    
            public Panel NextBlockBoard { get { return this.pnlNextBlock; } }
    
            public Models.BoardModel BoardModel { get; set; }
    
            /// <summary>
            /// Are you ready?, Game Over!, Pause!등등... 게임 상태를 표시하는 <see cref="LabelControl"/>을 가져옵니다.
            /// </summary>
            public Label Description { get { return this.lblDescription; } }
    
            public NGM.Interface.IMainView MainView { get; set; }
    
            public MainControl(NGM.Interface.IMainView mainView)
            {
                this.MainView = mainView;
    
                InitializeComponent();
    
                _gameTimer = new System.Windows.Forms.Timer()
                {
                    Interval = 100,
                    Enabled = false
                };
                _gameTimer.Tick += _gameTimer_Tick;
    
                _pauseTimer = new System.Windows.Forms.Timer()
                {
                    Interval = 100,
                    Enabled = false
                };
                _pauseTimer.Tick += _pauseTimer_Tick;
            }
    
            private void _pauseTimer_Tick(object sender, EventArgs e)
            {
                if (this.Description.ForeColor == Color.Black)
                {
                    this.Description.ForeColor = Color.Red;
                }
                else
                {
                    this.Description.ForeColor = Color.Black;
                }
            }
    
            private void _gameTimer_Tick(object sender, EventArgs e)
            {
                if (BoardModel.NextBlock == null)
                {
                    NextBlock();
                }
                else
                {
                    if (BoardModel.Block == null)
                    {
                        BoardModel.Block = BoardModel.NextBlock;
                        BoardModel.NextBlock = null;
                        NextBlock();
                    }
    
                    BoardModel.Block.Down();
                    pnlBoard.Invalidate();
                }
            }
    
            /// <summary>
            /// 게임 오버가 되면 게임을 중단하고, 랭킹 화면을 보여줍니다.
            /// </summary>
            public void GameOver()
            {
                numLevel.Enabled = true;
                btnPlay.Enabled = true;
    
                _gameTimer.Enabled = false;
                this.lblDescription.Text = "Game Over!";
                this.lblDescription.Visible = true;
            }
    
            private void btnPlay_Click(object sender, EventArgs e)
            {
                numLevel.Enabled = false;
                btnPlay.Enabled = false;
    
                //레벨 초기화
                numLevel.Value = 1;
                //스코어 초기화
                lblScore.Text = "0";
                //삭제한 줄을 초기화
                lblDeleteLines.Text = "0";
    
                lblDescription.Visible = false;
    
                BoardModel.Clear();
                _gameTimer.Interval = 1000;
                _gameTimer.Enabled = true;
    
                this.Focus();
            }
    
            /// <summary>
            /// 다음에 올 블럭을 생성합니다.
            /// </summary>
            private void NextBlock()
            {
                Random random = new Random();
                BoardModel.NextBlock = new Models.BlockModel(BoardModel, (Models.BlockType)Enum.Parse(typeof(Models.BlockType), random.Next(7).ToString()));
                pnlNextBlock.Invalidate();
            }
    
            /// <summary>
            /// 라인을 삭제합니다.
            /// </summary>
            /// <param name="deleteLineCount">삭제할 라인의 수입니다.</param>
            public void DeleteLine(int deleteLineCount)
            {
                for (int i = 0; i < deleteLineCount; i++)
                {
                    this.lblDeleteLines.Text = (int.Parse(this.lblDeleteLines.Text) + 1).ToString();
                    this.LevelUp();
    
                    Thread thread = new Thread(new ThreadStart(this.ScoreUp));
                    thread.IsBackground = true;
                    thread.Start();
                }
            }
    
            /// <summary>
            /// 라인을 10개 지울 때마다 레벨업합니다. 레벨업 할 때마다 블럭이 내려오는 속도가 증가합니다.
            /// </summary>
            public void LevelUp()
            {
                int deleteLines = int.Parse(this.lblDeleteLines.Text);
    
                if (deleteLines % 10 == 0)
                {
                    this.numLevel.Value = this.numLevel.Value + 1;
                    SpeedUp();
                }
            }
    
            /// <summary>
            /// 블럭이 내려오는 속도를 증가시킵니다.
            /// </summary>
            public void SpeedUp()
            {
                if (_gameTimer.Interval > 100)
                {
                    _gameTimer.Interval = _gameTimer.Interval - 100;
                }
                else
                {
                    if (_gameTimer.Interval > 10)
                    {
                        _gameTimer.Interval = _gameTimer.Interval - 10;
                    }
                }
            }
    
            /// <summary>
            /// 스코어를 올려줍니다.
            /// </summary>
            public void ScoreUp()
            {
                int score = int.Parse(this.lblScore.Text);
                int level = (int)this.numLevel.Value;
                int deleteLines = int.Parse(this.lblDeleteLines.Text);
    
                for (int i = score; i <= score + ((10 * deleteLines) * level); i++)
                {
                    this.ScoreUpdate(i.ToString());
                    System.Threading.Thread.Sleep(10);
                }
            }
    
            /// <summary>
            /// 스코어를 올릴 때 Cross thread exception이 발생하므로, 위임자를 통해 처리합니다.
            /// <para>이렇게 함으로써, 게임 진행과 동시에 스코어를 증가시킬 수 있습니다.</para>
            /// </summary>
            /// <param name="score">점수입니다.</param>
            private void ScoreUpdate(string score)
            {
                this.lblScore.Invoke((Action)delegate
                {
                    this.lblScore.Text = score;
                });
            }
    
            private void MainControl_Load(object sender, EventArgs e)
            {
                BoardModel = new Models.BoardModel(this, 12, 22);
            }
    
            private void pnlBoard_SizeChanged(object sender, EventArgs e)
            {
                lblDescription.Location = new Point(pnlBoard.Width / 2 - lblDescription.Width / 2, pnlBoard.Height / 2 + lblDescription.Height);
            }
    
            private void lblDescription_SizeChanged(object sender, EventArgs e)
            {
                lblDescription.Location = new Point(pnlBoard.Width / 2 - lblDescription.Width / 2, pnlBoard.Height / 2 + lblDescription.Height);
            }
    
            private void pnlBoard_Paint(object sender, PaintEventArgs e)
            {
                Graphics g = e.Graphics;
                BoardModel.Draw(g);
                if (BoardModel.Block != null)
                {
                    BoardModel.Block.Draw(g, true);
                }
            }
    
            private void pnlNextBlock_Paint(object sender, PaintEventArgs e)
            {
                Graphics g = e.Graphics;
                if (BoardModel.Block != null)
                {
                    BoardModel.NextBlock.Draw(g, false);
                }
            }
    
            private void MainControl_KeyUp(object sender, KeyEventArgs e)
            {
                if (BoardModel.Block == null)
                {
                    return;
                }
    
                if (e.KeyCode == Keys.P)
                {
                    if (!this.Description.Visible)
                    {
                        _gameTimer.Stop();
                        _pauseTimer.Start();
                        this.Description.Text = "Pause!";
                        this.Description.Visible = true;
                    }
                    else
                    {
                        _gameTimer.Start();
                        _pauseTimer.Stop();
                        this.Description.Visible = false;
                    }
                }
    
                if (!lblDescription.Visible)
                {
                    switch (e.KeyCode)
                    {
                        case Keys.Up:
                            BoardModel.Block.Rotate();
                            break;
                        case Keys.Down:
                            BoardModel.Block.Down();
                            break;
                        case Keys.Left:
                            BoardModel.Block.Left();
                            break;
                        case Keys.Right:
                            BoardModel.Block.Right();
                            break;
                        case Keys.Space:
                            BoardModel.Block.LastDown();
                            break;
                    }
    
                    pnlBoard.Refresh();
                }
            }
    
            private void pnlBoard_Click(object sender, EventArgs e)
            {
                this.Focus();
            }
        }
    }

     

     

    이렇게 해서 중요한 코드 부분은 모두 알아보았습니다. 급하게 코딩하느라 날코딩과 MVP Design pattern과는 무관하게 작성되었습니다. 코드를 리팩토링 해볼까 생각도 했었지만... 이게 주가 아닌 부록이라 여기에서 멈춰야 할거 같네요. C#으로 간단하게 테트리스를 구현해봤습니다. 부록 2탄으로는 로또 자동 생성기를 해보도록 하겠습니다.

     

    개발자에게 후원하기

    MGtdv7r.png

     

    추천, 구독, 홍보 꼭~ 부탁드립니다.

    여러분의 후원이 빠른 귀농을 가능하게 해줍니다~ 답답한 도시를 벗어나 귀농하고 싶은 개발자~

    감사합니다~

    • 네이버 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 카카오스토리 공유하기
    추천1 비추천0

    첨부파일

    댓글목록

    등록된 댓글이 없습니다.