[C#] 퍼즐 게임 만들기


Development note/C#  2021. 2. 10. 13:51

안녕하세요. 명월입니다.

 

이 글은 C#으로 퍼즐 게임 만들기에 대한 글입니다.

 

사실 예전에 이 글을 작성해서 내렸었습니다만, 그냥 소스를 지워버리기에는 조금 아깝고 해서 조금 수정해서 올려봤습니다.

많은 분들이 프로그램을 공부할 때, 보통 게임을 많이 생각하면서 나도 이런 게임 만들어 봤으면 해서 시작하는 경우가 많이 있습니다. 저의 경우는 그냥 전공이다 보니....ㅎㅎㅎ. 게임이라는게 프로그램의 영역보다는 그래픽의 영역이 더 많습니다. 게임도 게임 나름이겠습디만 최근에는 좋은 라이브러리와 엔진이 많이 있어서 아마도 예전 보다는 더 개발하기 쉬울 것입니다.

제가 게임 개발자가 아니라 쉽게 이야기하기가 힘들겠네요.. 학교 다닐때는 재미로 몇번 게임 프로그램을 만들어 봤긴 했지만.. 그걸로 어디 명함 내밀기에는 어렵습니다......

 

퍼즐 게임은 단순한 숫자 맞추기 게임입니다.

단순하게 키보드로 녹색 버튼을 이동해서 위처럼 1부터 24까지 맞추는 게임입니다.

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Threading;
using System.Windows.Forms;

namespace Example
{
  // 퍼즐 박스
  class Box
  {
    // 퍼즐 박스 크기
    private const int BOX_SIZE = 50;
    // 폰트 크기
    private const int FONT_SIZE = 20;
    // 폰트 설정
    private Font FONT = new Font(new FontFamily("Arial"), FONT_SIZE, FontStyle.Bold, GraphicsUnit.Pixel);
    // 색 설정
    private Brush COLOR = new SolidBrush(Color.Black);
    // 이미지 가져오기
    private Image BOX = Bitmap.FromFile(AppDomain.CurrentDomain.BaseDirectory + "box.bmp");
    // 공객 이미지 가져오기
    private Image BLANK = Bitmap.FromFile(AppDomain.CurrentDomain.BaseDirectory + "blank.bmp");

    // 퍼즐의 번호, 기본 마스크 설정
    public Box(int number, Mask mask)
    {
      // 퍼즐 번호 설정
      this.Number = number;
      // 마스크 설정
      this.Mask = mask;
    }
    // 번호 취득
    public int Number
    {
      get;
      private set;
    }
    // 마스크 취득
    public Mask Mask
    {
      get;
      set;
    }
    // 그리기 (x 좌표, y 좌표)
    public void Render(int x, int y)
    {
      // 0이면 공백 퍼즐임
      if (Number == 0)
      {
        // Bitmap 그리기
        GDIBuffer.Graphics(g => g.DrawImage(BLANK, x, y, BOX_SIZE, BOX_SIZE));
        return;
      }
      // 글자의 위치 계산하기
      float pointX = x + (BOX_SIZE / 2 - (FONT_SIZE / 2 + (int)Math.Log10(Number) * FONT_SIZE / 2)) + (Number > 9 ? 5 : 0);
      float pointY = y + BOX_SIZE / 2 - FONT_SIZE / 2;
      // Bitmap 그리기
      GDIBuffer.Graphics(g => g.DrawImage(BOX, x, y, BOX_SIZE, BOX_SIZE));
      // 번호 그리기
      GDIBuffer.Graphics(g => g.DrawString(Number.ToString(), FONT, COLOR, new PointF(pointX, pointY)));
    }
  }
  // 마스크 클래스(Box와 그릴 위치 좌표)
  class Mask
  {
    // 마스크 생성
    public Mask(int index, int x, int y)
    {
      this.X = x;
      this.Y = y;
      this.Index = index;
      // Box 인스턴스 생성
      this.Box = new Box(index, this);
    }
    // 퍼즐 박스
    public Box Box
    {
      set;
      get;
    }
    // X좌표 위치 설정
    public int X
    {
      private set;
      get;
    }
    // Y좌표 위치 설정
    public int Y
    {
      private set;
      get;
    }
    // 마스크 위치 설정
    public int Index
    {
      private set;
      get;
    }
  }
  // 박스 테이블 (마스크 리스트를 상속)
  class BoxTable : List<Mask>
  {
    // 박스는 좌로 5 세로 5의 총 25개 설정
    private const int BOX_COUNT = 25;
    // 공백 박스 클래스
    private Box blankBox;
    // 섞기 초기 플러그
    public bool Init
    {
      get; set;
    }
    // 완료 플러그
    public bool Complete
    {
      get; set;
    }
    // 생성장
    public BoxTable()
    {
      //초기화 플러그 설정
      Init = true;
      // 완료 플러그 설정
      Complete = false;
      // 마스크 생성하기
      CreateMask();

    }
    // 블랭크 박스 이동하기
    public void MoveBlank(Keys key)
    {
      // 퍼즐이 맞춤이 완료되면 이동 금지
      if (Complete)
      {
        return;
      }
      // Blank 위치 찾아오기
      int pos = blankBox.Mask.Index;
      // 이동할 위치
      int mpos;
      // 오른쪽 이동
      if (key == Keys.Right)
      {
        // 오른쪽 끝이면 이동 못한다.
        if (pos % 5 == 4)
        {
          return;
        }
        // 이동
        mpos = pos + 1;
      }
      // 왼쪽 이동
      else if (key == Keys.Left)
      {
        // 왼쪽 끝이면 이동 못한다.
        if (pos % 5 == 0)
        {
          return;
        }
        // 이동
        mpos = pos - 1;
      }
      // 아래로 이동
      else if (key == Keys.Down)
      {
        // 아래쪽과 배열 차이는 5
        mpos = pos + 5;
      }
      // 위로 이동
      else if (key == Keys.Up)
      {
        // 위쪽과의 배열 차이는 5
        mpos = pos - 5;
      }
      else
      {
        // 그 외의 버튼은 관계없음
        return;
      }
      // 배열 범위를 넘어서면 리턴
      if (mpos < 0 || mpos >= BOX_COUNT)
      {
        return;
      }
      // 마스크의 박스 객체 교환(Swap)
      // 공백 박스가 있던 마스크에 이동할 박스를 넣는다.
      blankBox.Mask.Box = base[mpos].Box;
      // 기존의 이동한 박스에 공백 박스를 넣는다.
      base[mpos].Box = blankBox;

      // 각 박스의 마스크 재설정
      base[pos].Box.Mask = base[pos];
      base[mpos].Box.Mask = base[mpos];

      // 랜덤 섞기가 끝난 상황이라면
      if (!Init)
      {
        // 완료했는지 확인한다.
        if (Check())
        {
          // 완료 플로그 설정
          Complete = true;
          // 메시지 출력
          MessageBox.Show("Complete!!");
        }
      }
    }
    // 퍼즐 확인 함수
    private bool Check()
    {
      // 각 마스크의
      for (int i = 0; i < base.Count; i++)
      {
        // 번호가 순서대로 있으면
        if (base[i].Box.Number == i)
        {
          continue;
        }
        // 완료되지 않았다.
        return false;
      }
      // 끝
      return true;
    }
    // 마스크 생성
    private void CreateMask()
    {
      // x좌표, y좌표 설정
      int x = 0;
      int y = 0;
      // 총 25개
      for (int i = 0; i < BOX_COUNT; i++)
      {
        // 좌로 5개가 꽉차면 개행
        if (i % 5 == 0 && i != 0)
        {
          x = 0;
          y += 50;
        }
        // 마스크를 리스트에 넣는다.
        this.Add(new Mask(i, x, y));
        // 넣을 때마다 50씩 증가
        x += 50;
      }
      // 공객 박스는 맨 처음
      blankBox = this[0].Box;
    }
    // 섞기 함수
    public void Random()
    {
      // 0부터 4까지
      switch (new Random().Next(4))
      {
        // 0 이면 왼쪽 이동
        case 0: MoveBlank(Keys.Left); break;
        // 1 이면 오른쪽 이동
        case 1: MoveBlank(Keys.Right); break;
        // 2 이면 위쪽 이동
        case 2: MoveBlank(Keys.Up); break;
        // 3 이면 아래쪽 이동
        case 3: MoveBlank(Keys.Down); break;
      }
    }
    // 그리기
    public void Render()
    {
      // 마스크 취득
      foreach (var m in this)
      {
        // 마스크에 있는 박스를 그리기
        m.Box.Render(m.X, m.Y);
      }
    }
  }
  // 더블 버퍼링 클래스
  class GDIBuffer
  {
    // 싱글톤
    private static GDIBuffer instance = null;
    // 그래픽 객체
    private Graphics graphics;
    // 이미지 객체
    private Bitmap bitmap;
    // 생성자
    private GDIBuffer(Size size)
    {
      // 크기 별로 메모리 이미지 생성
      this.bitmap = new Bitmap(size.Width, size.Height);
      // 그래픽 객체 인스턴스 생성
      this.graphics = System.Drawing.Graphics.FromImage(this.bitmap);
    }
    // 싱글톤 초기화
    public static void Init(Size size)
    {
      // 기존 인스턴스가 있으면
      if (instance != null)
      {
        // 메모리 해지하고 재 생성한다.
        instance.graphics.Dispose();
        instance.bitmap.Dispose();
      }
      // 인스턴스 생성
      instance = new GDIBuffer(size);
    }
    // Graphic 취득 함수
    public static void Graphics(Action<Graphics> func)
    {
      // 싱글톤이 생성되 있지 않으면 return;
      if (instance == null)
      {
        return;
      }
      // lock을 걸고
      lock (instance.graphics)
      {
        // Graphic 객체를 넘긴다.
        func(instance.graphics);
      }
    }
    // 버퍼를 Graphic 객체로 그린다.
    public static void Render(Graphics g)
    {
      // 싱글톤이 생성되 있지 않으면 return;
      if (instance == null)
      {
        return;
      }
      // Graphic에 버퍼를 그린다.
      g.DrawImage(instance.bitmap, new Point(0, 0));
    }
  }
  // 폼 클래스
  class Program : Form
  {
    // 박스 테이블
    private BoxTable boxtable;
    // 생성자
    public Program()
    {
      // 윈도우 폼 설정 (크기 조절 안되는 창)
      this.FormBorderStyle = FormBorderStyle.FixedToolWindow;
      // 윈도우 사이즈 설정
      this.Size = new Size(265, 290);
      // 버퍼 초기화(윈도우 사이즈로 맞춘다.)
      GDIBuffer.Init(new Size(ClientRectangle.Width, ClientRectangle.Height));
      // 박스 테이블 인스턴스 생성
      boxtable = new BoxTable();
      // 폼의 키 이벤트
      this.KeyDown += (s, e) =>
      {
        // Black 퍼즐을 이동한다.
        boxtable.MoveBlank(e.KeyCode);
      };
      // 쓰레드 생성
      ThreadPool.QueueUserWorkItem((c) =>
      {
        // 무한 루프로 버퍼 및 화면에 그린다.
        while (true)
        {
          // 그리기..
          Render();
          // 1초에 24 프레임으로
          Thread.Sleep(1000 / 24);
        }
      });
      // 섞기 쓰레드 생성
      ThreadPool.QueueUserWorkItem((c) =>
      {
        // 게임 시작 메시지
        MessageBox.Show("Game start..");
        // 랜덤식으로 500번 섞기
        for (int i = 0; i < 500; i++)
        {
          // 위치로 랜덤식으로 이동
          boxtable.Random();
          // 0.01 단위로
          Thread.Sleep(10);
        }
        // 초기화 끝
        boxtable.Init = false;
      });
    }
    // 그리기 함수
    public void Render()
    {
      // 버퍼 프레임 업데이트
      FrameUpdate();
      // 화면에 버퍼 프레임을 그리기
      using (var g = CreateGraphics())
      {
        GDIBuffer.Render(g);
      }
    }
    // 버퍼 프레임 업데이트 함수
    private void FrameUpdate()
    {
      // 배경을 흰색으로 클리어
      GDIBuffer.Graphics(g => g.Clear(Color.White));
      // 퍼즐 box 그리기
      boxtable.Render();
    }
    // 싱글 스레드 어트리뷰트
    [STAThread]
    // 실행 함수	
    static void Main(string[] args)
    {
      // 환경에 맞는 스타일 설정	
      Application.EnableVisualStyles();
      Application.SetCompatibleTextRenderingDefault(false);
      // 메시지 루프에 인스턴스를 생성한다.
      Application.Run(new Program());
    }
  }
}

먼저 소스를 간단하게 설명하면 폼이 있고 더블 버퍼링 클래스(GDIBuffer)를 통해서 퍼즐 버튼을 그리고 마지막으로 폼에 그리는 것입니다.

버튼은 Visual studio를 이용해서 bitmap 파일을 만들었습니다.

링크 - [C#] 프로그래밍에 필요한 pixel 이미지 만들기 그리고 스트라이프 이미지 만들기

그리고 Mask 클래스는 바둑판처럼 위치를 관리하는 클래스이고 BoxTable은 그런 Mask 클래스를 관리하는 클래스입니다.

그리고 Mask에는 위 bitmap 이미지와 퍼즐 숫자를 그리는 Box를 넣고 빼는 역할을 합니다.

최종적으로 BoxTable로 Rendor 요청이 오면, Box를 Mask의 좌표로 그리는 역할을 하게 됩니다.

 

그리고 시작할 때, 500번의 이동으로 섞기를 시도합니다.

Game Start 팝업이 생기면 OK 버튼을 누르면 퍼즐이 섞이기 시작합니다.

섞이면 이제 키보드의 화살표 키를 누르면서 맞춥니다.

모든 퍼즐을 다 맞추면 Complete 메시지가 나옵니다.

OK를 누르면 퍼즐은 더이상 움직이지 않게 됩니다. 다시 시작하시려면 Window Form을 종료하시고 다시 실행하면 됩니다.

 

여기까지 C#으로 퍼즐 게임 만들기에 대한 글이었습니다.

 

궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.