C#

c#테트리스 게임만들기

Barbarian developer 2024. 11. 4.

Position class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tetris
{
    //임에서 특정 위치를 나타내는 클래스
    public class Position
    {
        // 행 위치를 나타내는 속성 (y 좌표와 유사)
        public int Row { get; set; }

        // 열 위치를 나타내는 속성 (x 좌표와 유사)
        public int Column { get; set; }

        // 행과 열을 매개변수로 받아 Position 객체를 초기화하는 생성자
        public Position(int row, int column)
        {
            Row = row;        // 입력된 행 값을 Row 속성에 할당
            Column = column;  // 입력된 열 값을 Column 속성에 할당
        }
    }
}

 

  • 이 Position 클래스는 테트리스 게임에서 각 블록이나 타일의 위치를 표현하기 위해 사용됩니다.
  • Row와 Column 속성은 각각 위치의 세로(y 좌표)와 가로(x 좌표)를 나타냅니다.
  • 생성자는 Position 객체를 생성할 때 초기 행과 열 값을 설정하는 역할을 합니다.
  • 이 클래스는 간단하게 블록이 게임 보드에서 위치할 좌표를 추적하는 데 사용됩니다.

Block class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tetris
{
    // 테트리스의 블록을 나타내는 추상 클래스
    public abstract class Block
    {
        // 블록의 회전 상태를 나타내는 2차원 배열, 각 상태는 Position 배열로 구성
        protected abstract Position[][] Tiles { get; }

        // 블록의 초기 위치를 나타내는 추상 속성
        protected abstract Position StartOffset { get; }

        // 블록의 고유 식별자
        public abstract int Id { get; }

        // 현재 회전 상태를 나타내는 변수
        private int rotationState;

        // 블록의 현재 위치를 나타내는 오프셋
        private Position offset;

        // 생성자: 블록의 초기 위치를 설정
        public Block()
        {
            offset = new Position(StartOffset.Row, StartOffset.Column);
        }

        // 블록의 현재 타일 위치들을 반환
        public IEnumerable<Position> TilePositions()
        {
            foreach (Position p in Tiles[rotationState]) // 현재 회전 상태에 따라 타일을 가져옴
            {
                yield return new Position(p.Row + offset.Row, p.Column + offset.Column); // 오프셋을 적용하여 반환
            }
        }

        // 시계 방향으로 블록을 회전
        public void RotateCW()
        {
            rotationState = (rotationState + 1) % Tiles.Length; // 회전 상태를 순환
        }

        // 반시계 방향으로 블록을 회전
        public void RotateCCW()
        {
            rotationState = (rotationState - 1 + Tiles.Length) % Tiles.Length; // 음수가 되지 않도록 보정
        }

        // 반시계 방향으로 블록을 회전 (대체 구현)
        public void TotateCCW()
        {
            if (rotationState == 0)
            {
                rotationState = Tiles.Length - 1; // 현재 상태가 0이면 마지막 상태로 설정
            }
            else
            {
                rotationState--; // 그렇지 않으면 회전 상태 감소
            }
        }

        // 블록을 지정된 행과 열만큼 이동
        public void Move(int rows, int columns)
        {
            offset.Row += rows;
            offset.Column += columns;
        }

        // 블록의 회전 상태와 위치를 초기 상태로 재설정
        public void Reset()
        {
            rotationState = 0; // 회전 상태 초기화
            offset.Row = StartOffset.Row; // 행 오프셋 초기화
            offset.Column = StartOffset.Column; // 열 오프셋 초기화
        }
    }
}

 

 

  • 이 Block 클래스는 테트리스의 각 블록을 추상적으로 정의하는 기반 클래스입니다.
  • Tiles: 블록의 회전 상태를 나타내는 배열로, 각 회전 상태는 Position 배열로 표현됩니다.
  • StartOffset: 블록의 초기 위치를 정의합니다.
  • TilePositions(): 현재 블록의 모든 타일 위치를 계산하여 반환합니다.
  • RotateCW() 및 RotateCCW(): 블록을 시계 또는 반시계 방향으로 회전시킵니다.
  • Move(): 블록을 특정 행과 열만큼 이동합니다.
  • Reset(): 블록을 초기 상태로 되돌립니다.

이 클래스는 테트리스 게임에서 블록의 회전, 이동 및 초기화 기능을 처리하는 데 필요한 메서드와 속성을 정의합니다.

 

ZBlock class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tetris
{
    // 테트리스 게임의 'Z' 블록을 나타내는 클래스
    public class ZBlock : Block
    {
        // 'Z' 블록의 다양한 회전 상태를 정의하는 2차원 Position 배열
        private readonly Position[][] tiles = new Position[][]
        {
            // 기본 상태
            new Position[] { new(0, 0), new(0, 1), new(1, 1), new(1, 2) },
            
            // 시계 방향으로 90도 회전한 상태
            new Position[] { new(0, 2), new(1, 1), new(1, 2), new(2, 1) },
            
            // 180도 회전한 상태
            new Position[] { new(1, 0), new(1, 1), new(2, 1), new(2, 2) },
            
            // 시계 방향으로 270도 회전한 상태
            new Position[] { new(0, 1), new(1, 0), new(1, 1), new(2, 0) }
        };

        // 블록의 식별자. 테트리스 게임에서 'Z' 블록을 구분하는 값으로 사용
        public override int Id => 7;

        // 블록의 초기 위치를 나타냄. 게임 보드의 위쪽 중앙에서 시작하도록 설정
        protected override Position StartOffset => new Position(0, 3);

        // 'Tiles' 속성은 'tiles' 배열을 반환하여 블록의 회전 상태를 제공
        protected override Position[][] Tiles => tiles;
    }
}

 

 

  • tiles: Position[][] 배열로, Z 블록의 네 가지 회전 상태를 정의합니다. 각 Position[] 배열은 Z 블록이 각 회전 상태에서 차지하는 상대적 좌표를 지정합니다.
  • Id 속성: Z 블록을 다른 블록과 구분하기 위해 고유 식별자로 7을 사용합니다.
  • StartOffset 속성: 블록이 게임 보드에서 처음 나타나는 위치를 설정합니다. (0, 3)은 보드의 위쪽 중간에서 시작하도록 합니다.

Tiles 속성: Block 클래스에서 사용하기 위해 tiles 배열을 반환합니다. 이 배열은 블록의 현재 회전 상태에 따라 블록의 타일 위치를 제공합니다.

 

이하 다른 블록의 클래스도 좌표를 제외한 나머지 코드는 동일하므로 생략합니다.

 

BlockQueue class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tetris
{
    // 블록의 큐를 관리하는 클래스. 다음 블록을 미리 선택하고 업데이트하는 역할을 함
    class BlockQueue
    {
        // 게임에 사용할 블록 배열. 다양한 블록의 인스턴스를 포함
        private readonly Block[] blocks = new Block[]
        {
            new IBlock(),
            new JBlock(),
            new LBlock(),
            new OBlock(),
            new SBlock(),
            new TBlock(),
            new ZBlock(),
        };

        // 블록을 무작위로 선택하기 위한 Random 객체
        private readonly Random random = new Random();

        // 큐에서 사용할 다음 블록을 나타내는 속성
        public Block NextBlock { get; private set; }

        // 생성자: 처음에 랜덤으로 블록을 선택하여 초기화
        public BlockQueue()
        {
            NextBlock = RandomBlock(); // 초기 블록 설정
        }

        // 블록 배열에서 랜덤으로 하나의 블록을 반환하는 메서드
        private Block RandomBlock()
        {
            return blocks[random.Next(blocks.Length)]; // 블록 배열의 인덱스를 무작위로 선택
        }

        // 현재 블록을 반환하고 다음 블록을 업데이트하는 메서드
        public Block GetAndUpdate()
        {
            Block block = NextBlock; // 현재 블록 저장

            // 다음 블록이 현재 블록과 다를 때까지 무작위로 선택
            do
            {
                NextBlock = RandomBlock();
            }
            while (block.Id == NextBlock.Id); // 같은 블록이 연속으로 나오지 않도록 방지

            return block; // 현재 블록 반환
        }
    }
}

 

 

  • blocks 배열: 다양한 테트리스 블록 인스턴스를 포함하며, 무작위로 선택됩니다.
  • random 객체: 무작위 블록을 선택하기 위해 사용됩니다.
  • NextBlock 속성: 다음에 사용할 블록을 나타내며, GetAndUpdate() 메서드가 호출될 때마다 갱신됩니다.
  • RandomBlock() 메서드: blocks 배열에서 무작위로 하나의 블록을 반환합니다.
  • GetAndUpdate() 메서드:
    • 현재 NextBlock을 반환한 후, 새로운 블록을 무작위로 선택하여 NextBlock에 할당합니다.
    • 연속으로 동일한 블록이 나오지 않도록 조건(block.Id == NextBlock.Id)을 사용해 반복적으로 선택합니다.

이 클래스는 테트리스 게임에서 다음 블록을 예측하고 미리 준비하는 역할을 담당합니다.

 

GameState class

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Tetris
{
    // 테트리스 게임의 상태를 관리하는 클래스
    class GameState
    {
        // 현재 활성 블록
        private Block currentBlock;

        // 현재 활성 블록을 가져오거나 설정하는 속성. 블록이 설정될 때 초기화 및 이동 처리
        public Block CurrentBlock
        {
            get => currentBlock;
            private set
            {
                currentBlock = value;
                currentBlock.Reset(); // 블록 상태 초기화

                // 초기 위치에서 블록을 두 번 아래로 이동하여 보드에 맞추려 시도
                for (int i = 0; i < 2; i++)
                {
                    currentBlock.Move(1, 0);

                    if (!BlockFits())
                    {
                        currentBlock.Move(-1, 0); // 블록이 맞지 않으면 원위치
                    }
                }
            }
        }

        // 게임 보드 상태를 나타내는 그리드
        public GameGrid GameGrid { get; }
        
        // 블록 큐로 다음 블록을 관리
        public BlockQueue BlockQueue { get; }
        
        // 게임 종료 상태를 나타내는 속성
        public bool GameOver { get; private set; }

        // 현재 게임 점수
        public int Score { get; private set; }

        // 홀드된 블록
        public Block HeldBlock { get; private set; }
        
        // 블록 홀드 가능 여부를 나타내는 속성
        public bool CanHold { get; private set; }

        // 생성자: 초기 상태를 설정
        public GameState()
        {
            GameGrid = new GameGrid(22, 10); // 22행 x 10열 그리드 생성
            BlockQueue = new BlockQueue(); // 블록 큐 초기화
            CurrentBlock = BlockQueue.GetAndUpdate(); // 첫 블록 설정
        }

        // 현재 블록이 그리드에 맞는지 검사하는 메서드
        private bool BlockFits()
        {
            foreach (Position p in CurrentBlock.TilePositions())
            {
                if (!GameGrid.IsEmpty(p.Row, p.Column))
                {
                    return false; // 블록이 비어 있지 않은 공간에 위치하면 false 반환
                }
            }
            return true; // 모든 위치가 비어 있으면 true 반환
        }

        // 블록을 홀드하는 기능
        public void HoldBlock()
        {
            if (!CanHold)
            {
                return; // 홀드 불가능하면 메서드 종료
            }
            if (HeldBlock == null)
            {
                HeldBlock = currentBlock; // 현재 블록을 홀드
                CurrentBlock = BlockQueue.GetAndUpdate(); // 새로운 블록 가져옴
            }
            else
            {
                Block tmp = CurrentBlock;
                CurrentBlock = HeldBlock; // 기존 홀드 블록과 현재 블록을 교체
                HeldBlock = tmp;
            }

            CanHold = false; // 홀드 후에는 재홀드 불가
        }

        // 블록을 시계 방향으로 회전시키는 메서드
        public void RotateBlockCW()
        {
            currentBlock.RotateCW();

            if (!BlockFits())
            {
                CurrentBlock.RotateCCW(); // 맞지 않으면 회전을 되돌림
            }
        }

        // 블록을 반시계 방향으로 회전시키는 메서드
        public void RotateBlockCCW()
        {
            CurrentBlock.RotateCCW();

            if (!BlockFits())
            {
                CurrentBlock.RotateCW(); // 맞지 않으면 회전을 되돌림
            }
        }

        // 블록을 왼쪽으로 이동시키는 메서드
        public void MoveBlockLeft()
        {
            CurrentBlock.Move(0, -1);

            if (!BlockFits())
            {
                CurrentBlock.Move(0, 1); // 맞지 않으면 원위치
            }
        }

        // 블록을 오른쪽으로 이동시키는 메서드
        public void MoveBlockRight()
        {
            CurrentBlock.Move(0, 1);

            if (!BlockFits())
            {
                CurrentBlock.Move(0, -1); // 맞지 않으면 원위치
            }
        }
        
        // 게임 오버 상태를 검사하는 메서드
        private bool IsGameOver()
        {
            return !(GameGrid.IsRowEmpty(0) && GameGrid.IsRowEmpty(1)); // 상단 두 줄이 비어있지 않으면 게임 오버
        }

        // 블록을 현재 위치에 고정시키고 점수를 갱신하는 메서드
        private void PlaceBlock()
        {
            foreach (Position p in CurrentBlock.TilePositions())
            {
                GameGrid[p.Row, p.Column] = CurrentBlock.Id; // 블록 위치를 보드에 고정
            }

            Score += GameGrid.ClearFullRows(); // 꽉 찬 줄을 지우고 점수 갱신

            if (IsGameOver())
            {
                GameOver = true; // 게임 오버 상태 설정
            }
            else
            {
                CurrentBlock = BlockQueue.GetAndUpdate(); // 새로운 블록 설정
                CanHold = true; // 블록 홀드 가능 상태로 설정
            }
        }

        // 블록을 아래로 한 칸 이동시키는 메서드
        public void MoveBlockDown()
        {
            CurrentBlock.Move(1, 0);

            if (!BlockFits())
            {
                CurrentBlock.Move(-1, 0); // 맞지 않으면 원위치
                PlaceBlock(); // 블록 고정
            }
        }

        // 특정 위치에서 블록이 떨어질 수 있는 거리 계산
        private int TileDropDistance(Position p)
        {
            int drop = 0;

            while (GameGrid.IsEmpty(p.Row + drop + 1, p.Column))
            {
                drop++; // 떨어질 수 있는 만큼 거리 증가
            }

            return drop; // 최대 떨어질 거리 반환
        }

        // 현재 블록이 떨어질 수 있는 최소 거리 계산
        public int BlockDropDistance()
        {
            int drop = GameGrid.Rows; // 초기값을 최대 행 수로 설정

            foreach (Position p in CurrentBlock.TilePositions())
            {
                drop = System.Math.Min(drop, TileDropDistance(p)); // 최소 거리 계산
            }

            return drop; // 최소 거리 반환
        }

        // 블록을 즉시 바닥으로 떨어뜨리고 고정하는 메서드
        public void DropBlock()
        {
            CurrentBlock.Move(BlockDropDistance(), 0); // 블록을 최대로 떨어뜨림
            PlaceBlock(); // 블록 고정
        }
    }
}

 

  • CurrentBlock 속성: 현재 활성화된 블록을 나타내며, 새로운 블록 설정 시 초기화 및 위치 조정을 수행합니다.
  • BlockFits(): 현재 블록이 보드에 맞는지 검사합니다.
  • HoldBlock(): 블록을 홀드하고, 필요한 경우 홀드된 블록과 현재 블록을 교체합니다.
  • PlaceBlock(): 현재 블록을 보드에 고정하고 줄을 지우며 점수를 갱신합니다.
  • MoveBlockDown(), MoveBlockLeft(), MoveBlockRight(): 블록을 해당 방향으로 이동시키고, 맞지 않으면 원위치로 되돌립니다.
  • RotateBlockCW(), RotateBlockCCW(): 블록을 회전시키고, 보드에 맞지 않으면 회전을 되돌립니다.
  • IsGameOver(): 게임 오버 조건을 검사합니다.
  • BlockDropDistance() 및 TileDropDistance(): 블록이 떨어질 수 있는 최대 거리를 계산합니다.
  • DropBlock(): 블록을 즉시 바닥으로 떨어뜨리고 고정합니다.

Mainwindow xaml

<Window x:Class="Tetris.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:Tetris"
        mc:Ignorable="d"
        Title="Tetris" Height="600" Width="800"
        MinWidth="600" MinHeight="600"
        Foreground="white"
        FontFamily="Segoe UI Light" FontSize="28"
        KeyDown="Window_KeyDown">
    <!-- Window의 기본 설정: 클래스, 네임스페이스, 크기, 스타일 및 키 입력 이벤트 설정 -->

    <Grid>
        <!-- 전체 레이아웃을 위한 Grid 정의 -->
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/> <!-- 첫 번째 행은 콘텐츠에 맞춤 -->
            <RowDefinition Height="*"/>    <!-- 두 번째 행은 남은 공간 전체 차지 -->
        </Grid.RowDefinitions>

        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="*"/>     <!-- 첫 번째 열: 남은 공간 전체 -->
            <ColumnDefinition Width="auto"/>  <!-- 두 번째 열: 콘텐츠에 맞춤 -->
            <ColumnDefinition Width="*"/>     <!-- 세 번째 열: 남은 공간 전체 -->
        </Grid.ColumnDefinitions>

        <Grid.Background>
            <ImageBrush ImageSource="Assets/Background.png"/>
            <!-- 배경 이미지 설정 -->
        </Grid.Background>

        <!-- 게임 캔버스를 포함하는 Viewbox -->
        <Viewbox Grid.Row="1" Grid.Column="1" Margin="0,0,0,20">
            <Canvas x:Name="GameCanvas"
                    Background="#101010" <!-- 어두운 배경색 -->
                    Width="250"
                    Height="510"
                    ClipToBounds="True"
                    Loaded="GameCanvas_Loaded"/>
                    <!-- 게임 캔버스 로드 이벤트 설정 -->
        </Viewbox>

        <!-- 점수를 표시하는 텍스트 -->
        <TextBlock x:Name="ScoreText"
                   Grid.Row="0"
                   Grid.Column="1"
                   Text="Score: "
                   Margin="0, 10"
                   TextAlignment="Center"/>

        <!-- Hold 블록 영역 -->
        <StackPanel Grid.Row="1" Grid.Column="0"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Right">
            <TextBlock Text="Hold" TextAlignment="Center"/>
            <Image x:Name="HoldImage" Margin="20" Width="125"/>
        </StackPanel>

        <!-- Next 블록 영역 -->
        <StackPanel Grid.Row="1" Grid.Column="2"
                    VerticalAlignment="Center"
                    HorizontalAlignment="Left">
            <TextBlock Text="Next" TextAlignment="Center"/>
            <Image x:Name="NextImage" Margin="20" Width="125"/>
        </StackPanel>

        <!-- 게임 오버 메뉴 -->
        <Grid x:Name="GameOverMenu"
              Background="#CC000000" <!-- 반투명한 검은색 배경 -->
              Grid.RowSpan="2" <!-- 전체 행에 걸쳐 표시 -->
              Grid.ColumnSpan="3" <!-- 전체 열에 걸쳐 표시 -->
              Visibility="Hidden"> <!-- 초기 상태는 숨김 -->

            <StackPanel HorizontalAlignment="Center"
                        VerticalAlignment="Center">
                <TextBlock Text="Game Over" FontSize="48" TextAlignment="Center"/>
                <TextBlock x:Name="FinalScoreText" Text="Score: " FontSize="36" TextAlignment="Center"/>
                <Button Content="Play Again"
                        Background="LightGreen"
                        Margin="0,20,0,0"
                        Padding="5"
                        Click="PlayAgain_Click"/>
                        <!-- 'Play Again' 버튼 클릭 이벤트 -->
            </StackPanel>
        </Grid>
    </Grid>
</Window>

 

 

  • <Window>: 테트리스 애플리케이션의 메인 윈도우로, XAML 코드를 통해 UI의 구조와 스타일을 정의합니다.
  • <Grid>: 전체 UI 레이아웃을 구성하는 컨테이너입니다. 행과 열을 정의하여 UI 요소를 배치합니다.
  • <Viewbox>: GameCanvas를 포함하여 캔버스의 크기를 동적으로 조정할 수 있도록 합니다.
  • <TextBlock>: 점수와 게임 오버 메시지를 표시하는 텍스트 요소입니다.
  • <StackPanel>: Hold 및 Next 블록을 보여주는 컨테이너로, 세로 방향으로 요소를 정렬합니다.
  • <Grid x:Name="GameOverMenu">: 게임 오버 상태일 때 표시되는 메뉴입니다. 처음에는 Visibility가 Hidden으로 설정되어 숨겨져 있습니다.
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Threading.Tasks;

namespace Tetris
{
    /// <summary>
    /// MainWindow.xaml의 코드 비하인드 클래스
    /// 테트리스 게임의 주요 로직과 화면 렌더링을 담당
    /// </summary>
    public partial class MainWindow : Window
    {
        // 블록의 타일 이미지를 저장하는 배열
        private readonly ImageSource[] tileImages = new ImageSource[]
        {
            new BitmapImage(new Uri("Assets/TileEmpty.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/TileCyan.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/TileBlue.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/TileOrange.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/TileYellow.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/TileGreen.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/TilePurple.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/TileRed.png", UriKind.Relative)),
        };

        // 다음 블록과 홀드 블록 이미지를 저장하는 배열
        private readonly ImageSource[] blockImages = new ImageSource[]
        {
            new BitmapImage(new Uri("Assets/Block-Empty.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/Block-I.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/Block-J.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/Block-L.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/Block-O.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/Block-S.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/Block-T.png", UriKind.Relative)),
            new BitmapImage(new Uri("Assets/Block-Z.png", UriKind.Relative))
        };

        // 그리드의 각 셀에 대한 이미지 컨트롤을 저장하는 2차원 배열
        private readonly Image[,] imageControls;

        // 게임 진행 속도를 조절하기 위한 지연 시간 설정
        private readonly int maxDelay = 1000;
        private readonly int minDelay = 75;
        private readonly int delayDecrease = 25;

        // 게임 상태를 관리하는 객체
        private GameState gameState = new GameState();

        // 생성자: 윈도우를 초기화하고 게임 캔버스를 설정
        public MainWindow()
        {
            InitializeComponent();
            imageControls = SetupGameCanvas(gameState.GameGrid);
        }

        // 게임 캔버스를 설정하고 이미지 컨트롤 배열을 반환하는 메서드
        private Image[,] SetupGameCanvas(GameGrid grid)
        {
            Image[,] imageControls = new Image[grid.Rows, grid.Columns];
            int cellSize = 25;

            for (int r = 0; r < grid.Rows; r++)
            {
                for (int c = 0; c < grid.Columns; c++)
                {
                    Image imageControl = new Image
                    {
                        Width = cellSize,
                        Height = cellSize
                    };

                    // 이미지의 위치 설정
                    Canvas.SetTop(imageControl, (r - 2) * cellSize + 10);
                    Canvas.SetLeft(imageControl, c * cellSize);
                    GameCanvas.Children.Add(imageControl);
                    imageControls[r, c] = imageControl;
                }
            }
            return imageControls;
        }

        // 게임 보드를 그리는 메서드
        private void DrawGrid(GameGrid grid)
        {
            for (int r = 0; r < grid.Rows; r++)
            {
                for (int c = 0; c < grid.Columns; c++)
                {
                    int id = grid[r, c];
                    imageControls[r, c].Opacity = 1;
                    imageControls[r, c].Source = tileImages[id];
                }
            }
        }

        // 현재 블록을 그리는 메서드
        private void DrawBlock(Block block)
        {
            foreach (Position p in block.TilePositions())
            {
                imageControls[p.Row, p.Column].Opacity = 1;
                imageControls[p.Row, p.Column].Source = tileImages[block.Id];
            }
        }

        // 다음 블록 이미지를 그리는 메서드
        private void DrawNextBlock(BlockQueue blockQueue)
        {
            Block next = blockQueue.NextBlock;
            NextImage.Source = blockImages[next.Id];
        }

        // 홀드된 블록 이미지를 그리는 메서드
        private void DrawHeldBlock(Block heldBlock)
        {
            if (heldBlock == null)
            {
                HoldImage.Source = blockImages[0]; // 빈 블록 이미지
            }
            else
            {
                HoldImage.Source = blockImages[heldBlock.Id];
            }
        }

        // 블록의 고스트(예상 낙하 위치)를 그리는 메서드
        private void DrawGhostBlock(Block block)
        {
            int dropDistance = gameState.BlockDropDistance();

            foreach (Position p in block.TilePositions())
            {
                imageControls[p.Row + dropDistance, p.Column].Opacity = 0.25;
                imageControls[p.Row + dropDistance, p.Column].Source = tileImages[block.Id];
            }
        }

        // 전체 게임 상태를 그리는 메서드
        private void Draw(GameState gameState)
        {
            DrawGrid(gameState.GameGrid);
            DrawGhostBlock(gameState.CurrentBlock);
            DrawBlock(gameState.CurrentBlock);
            DrawNextBlock(gameState.BlockQueue);
            ScoreText.Text = $"Score: {gameState.Score}";
            DrawHeldBlock(gameState.HeldBlock);
        }

        // 게임 루프를 비동기로 실행하는 메서드
        private async Task GameLoop()
        {
            Draw(gameState);

            while (!gameState.GameOver)
            {
                int delay = Math.Max(minDelay, maxDelay - (gameState.Score * delayDecrease));
                await Task.Delay(delay); // 지정된 시간만큼 대기
                gameState.MoveBlockDown();
                Draw(gameState); // 블록 이동 후 화면 갱신
            }

            // 게임 오버 시 메뉴 표시
            GameOverMenu.Visibility = Visibility.Visible;
            FinalScoreText.Text = $"Score: {gameState.Score}";
        }

        // 키 입력을 처리하는 메서드
        private void Window_KeyDown(object sender, KeyEventArgs e)
        {
            if (gameState.GameOver)
            {
                return; // 게임 오버 상태면 입력 무시
            }

            switch (e.Key)
            {
                case Key.Left:
                    gameState.MoveBlockLeft();
                    break;
                case Key.Right:
                    gameState.MoveBlockRight();
                    break;
                case Key.Down:
                    gameState.MoveBlockDown();
                    break;
                case Key.Up:
                    gameState.RotateBlockCW();
                    break;
                case Key.Z:
                    gameState.RotateBlockCCW();
                    break;
                case Key.C:
                    gameState.HoldBlock();
                    break;
                case Key.Space:
                    gameState.DropBlock();
                    break;
                default:
                    return; // 지원되지 않는 키는 무시
            }

            Draw(gameState); // 키 입력 후 화면 갱신
        }

        // 게임 캔버스가 로드될 때 게임 루프를 시작하는 메서드
        private async void GameCanvas_Loaded(object sender, RoutedEventArgs e)
        {
            await GameLoop();
        }

        // "Play Again" 버튼 클릭 시 게임을 재시작하는 메서드
        private async void PlayAgain_Click(object sender, RoutedEventArgs e)
        {
            gameState = new GameState(); // 새 게임 상태 생성
            GameOverMenu.Visibility = Visibility.Hidden; // 게임 오버 메뉴 숨김
            await GameLoop(); // 새 게임 루프 시작
        }
    }
}

 

 

 

  • tileImages 및 blockImages: 각 타일과 블록의 이미지를 ImageSource 배열로 저장해 게임에서 사용.
  • GameLoop(): 게임의 주요 루프를 비동기로 실행해 일정 간격마다 블록을 아래로 이동시키고 화면을 갱신.
  • Window_KeyDown(): 사용자의 키 입력을 처리해 블록 이동, 회전, 낙하 등을 수행.
  • Draw() 메서드: 게임 보드, 현재 블록, 다음 블록, 홀드 블록, 고스트 블록 등을 화면에 그리는 역할.
  • GameCanvas_Loaded(): 게임 캔버스 로드 시 게임 루프를 시작.
  • PlayAgain_Click(): "Play Again" 버튼 클릭 시 새로운 게임을 시작

 

'C#' 카테고리의 다른 글

네트워크 프로그래밍  (0) 2024.11.04
Task  (0) 2024.11.03
대리자와 이벤트  (0) 2024.11.01
프로그래밍  (0) 2024.11.01
배열과 컬렉션 그리고 인덱서  (0) 2024.11.01

댓글