[C#] 무료 게임 라이브러리 Monogame를 이용해서 퍼즐 게임 만들기


Development note/C#  2021. 3. 10. 16:26

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


이 글은 C#의 무료 게임 라이브러리 Monogame를 이용해서 퍼즐 게임 만들기에 대한 글입니다.


이전에 저는 C#으로 퍼즐 게임을 만든 적이 있습니다.

링크 - [C#] 퍼즐 게임 만들기


그때는 특별한 라이브러리를 통해서 만든 프로그램이 아니고 단순하게 윈폼에다가 더블 버퍼링을 만들어서 이미지를 윈도우 폼에 그리는 형태로 게임을 만들었습니다. 기본적으로 게임이란 것은 유저의 액션과 실행한 시간으로부터 Time 스트림의 차이로 화면에 보여주는 형태입니다.

즉, 우리가 어렸을 때 책에 각 액션의 그림을 그리고 책 페이지를 넘기는 형태로 움직이는 듯한 형태를 만든 놀이를 한 적이 있습니다. 게임도 비슷한 원리입니다만, 약 초당 60프레임(60장의 이미지(?))를 형태를 보여줌으로써 게임이 움직이는 것처럼 보이는 원리입니다.

그런 것을 따로 구현할 필요없이 하나의 프로그램 구조로 나타낸 것이 게임 프레임워크입니다.


이전에는 생 코딩(?)으로 게임을 만들었지만 이번에는 조금 더 부드러운(?) 프레임처리와 게임 리소스를 사용하기 위해 Monogame 라이브러리를 통해서 퍼즐 게임을 만들어 보겠습니다.


이미지 리소스는 이전에 사용한 것과 똑같은 bit 이미지를 사용하겠습니다.

먼저 MGCB Editor를 통해서 리소스 업데이트를 하겠습니다.

defaultfont는 그냥 일반 SpriteFont Description의 파일 생성했습니다. 이는 Monogame 프레임워크 내에서 글자를 draw하기 위해서 필요한 폰트 리소스입니다.


그리고 Game1 클래스 파일에 게임 소스를 작성하겠습니다.

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;

namespace ExampleGame
{
  // 마스크 클래스(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 Box
  {
    // 퍼즐의 번호, 기본 마스크 설정
    public Box(int number, Mask mask)
    {
      // 퍼즐 번호 설정
      this.Number = number;
      // 마스크 설정
      this.Mask = mask;
    }
    // 번호 취득
    public int Number
    {
      get;
      private set;
    }
    // 마스크 취득
    public Mask Mask
    {
      get;
      set;
    }
  }

  public class Game1 : Game
  {
    // 퍼즐 박스 크기
    public const int BOX_SIZE = 50;
    // 폰트 크기
    public const int FONT_SIZE = 18;
    // 화면 Graphics 관리 클래스
    private GraphicsDeviceManager _graphics;
    // 스크린 크기에 따라서 게임 화면이 자동으로 변경된다.
    private SpriteBatch _spriteBatch;

    // 박스는 좌로 5 세로 5의 총 25개 설정
    private const int BOX_COUNT = 25;
    // 공백 박스 클래스
    private Box _blankBox;
    private Mask[] _masks;
    // 섞기 초기 플러그
    private bool _init;
    // 완료 플러그
    private bool _complete;
    // 폰트 리소스
    private SpriteFont _font;
    // 퍼즐 블록 이미지 리소스
    private Texture2D _block;
    // 퍼즐 공간 이미지 리소스
    private Texture2D _blank;
    // 이전 버튼 상태
    private KeyboardState oldState;
    public Game1()
    {
      _graphics = new GraphicsDeviceManager(this);
      // 리소스 디렉토리 변경
      Content.RootDirectory = @"Content\bin\DesktopGL";
      IsMouseVisible = true;
    }
    // 초기화 함수
    protected override void Initialize()
    {
      // TODO: Add your initialization logic here
      base.Initialize();
      // 폼 크기 설정
      _graphics.PreferredBackBufferWidth = 250;
      _graphics.PreferredBackBufferHeight = 250;
      _graphics.ApplyChanges();
      //초기화 플러그 설정
      _init = true;
      // 완료 플러그 설정
      _complete = false;
      // 퍼즐은 가로 5, 세로 5로 설정
      _masks = new Mask[25];
      // 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;
        }
        // 마스크를 리스트에 넣는다.
        _masks[i] = new Mask(i, x, y);
        // 넣을 때마다 50씩 증가
        x += 50;
      }
      // 공객 박스는 맨 처음
      _blankBox = _masks[0].Box;

      // 섞기 쓰레드 생성
      System.Threading.ThreadPool.QueueUserWorkItem((c) =>
      {
        // 랜덤식으로 500번 섞기
        for (int i = 0; i < 500; i++)
        {
          // 위치로 랜덤식으로 이동
          Random();
          // 0.01 단위로
          System.Threading.Thread.Sleep(10);
        }
        // 초기화 끝
        _init = false;
        // 시작 메시지
        System.Windows.Forms.MessageBox.Show("Game start..");
      });
    }
    // 리소스 읽어오기
    protected override void LoadContent()
    {
      _spriteBatch = new SpriteBatch(GraphicsDevice);

      // TODO: use this.Content to load your game content here
      // 폰트 리소스 읽기
      _font = Content.Load<SpriteFont>("defaultfont");
      // 퍼즐 블록 이미지 리소스 읽기
      _block = Content.Load<Texture2D>("block");
      // 퍼즐 공간 이미지 리소스 읽기
      _blank = Content.Load<Texture2D>("blank");
    }
    // 리소스 값들 업데이트하기
    protected override void Update(GameTime gameTime)
    {
      // 키보드 이벤트 받기
      var state = Keyboard.GetState();
      // Esce 버튼이 눌리면 종료
      if (state.IsKeyDown(Keys.Escape))
      {
        Exit();
      }
      // 이전 상태가 오른쪽이 아니라면
      if (!oldState.IsKeyDown(Keys.Right) && state.IsKeyDown(Keys.Right))
      {
        // 이전 키보드 상태 저장
        oldState = state;
        // 오른쪽으로 이동
        MoveBlank(Keys.Right);
      }
      // 이전 상태가 왼쪽이 아니라면
      if (!oldState.IsKeyDown(Keys.Left) && state.IsKeyDown(Keys.Left))
      {
        // 이전 키보드 상태 저장
        oldState = state;
        // 왼쪽으로 이동
        MoveBlank(Keys.Left);
      }
      // 이전 상태가 위쪽이 아니라면
      if (!oldState.IsKeyDown(Keys.Up) && state.IsKeyDown(Keys.Up))
      {
        // 이전 키보드 상태 저장
        oldState = state;
        // 위쪽으로 이동
        MoveBlank(Keys.Up);
      }
      // 이전 상태가 아래쪽이 아니라면
      if (!oldState.IsKeyDown(Keys.Down) && state.IsKeyDown(Keys.Down))
      {
        // 이전 키보드 상태 저장
        oldState = state;
        // 아래쪽으로 이동
        MoveBlank(Keys.Down);
      }
      // 키를 떼어을 때 이전 키보드 상태 변경
      if (state.IsKeyUp(Keys.Right))
      {
        oldState = state;
      }
      if (state.IsKeyUp(Keys.Left))
      {
        oldState = state;
      }
      if (state.IsKeyUp(Keys.Up))
      {
        oldState = state;
      }
      if (state.IsKeyUp(Keys.Down))
      {
        oldState = state;
      }
      // TODO: Add your update logic here
      base.Update(gameTime);
    }
    // 그리기 함수
    protected override void Draw(GameTime gameTime)
    {
      // 배경색은 파란색
      GraphicsDevice.Clear(Color.CornflowerBlue);

      // TODO: Add your drawing code here
      // 화면 설정
      _spriteBatch.Begin();
      // 마스크 취득
      foreach (var m in _masks)
      {
        // 공간 버튼 블록
        if (m.Box.Number == 0)
        {
          // 그리기
          _spriteBatch.Draw(_blank, new Rectangle(m.X, m.Y, BOX_SIZE, BOX_SIZE), Color.White);
        }
        else
        {
          // 글자의 위치 계산하기
          float pointX = m.X + (BOX_SIZE / 2 - (FONT_SIZE / 2 + (int)Math.Log10(m.Box.Number) * FONT_SIZE / 2)) + (m.Box.Number > 9 ? 5 : 0);
          float pointY = m.Y + BOX_SIZE / 2 - 16;
          // Bitmap 그리기
          _spriteBatch.Draw(_block, new Rectangle(m.X, m.Y, BOX_SIZE, BOX_SIZE), Color.White);
          // 번호 그리기
          _spriteBatch.DrawString(_font, m.Box.Number.ToString(), new Vector2(pointX, pointY), Color.Black);
        }
      }
      // 화면 설정 종료
      _spriteBatch.End();

      base.Draw(gameTime);
    }
    // 섞기 함수
    private 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;
      }
    }
    // 블랭크 박스 이동하기
    private 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 = _masks[mpos].Box;
      // 기존의 이동한 박스에 공백 박스를 넣는다.
      _masks[mpos].Box = _blankBox;
      // 각 박스의 마스크 재설정
      _masks[pos].Box.Mask = _masks[pos];
      _masks[mpos].Box.Mask = _masks[mpos];
      // 랜덤 섞기가 끝난 상황이라면
      if (!_init)
      {
        // 완료했는지 확인한다.
        if (Check())
        {
          // 완료 플로그 설정
          _complete = true;
          // 메시지 출력
          System.Windows.Forms.MessageBox.Show("Complete!!");
        }
      }
    }
    // 퍼즐 확인 함수
    private bool Check()
    {
      // 각 마스크의
      for (int i = 0; i < _masks.Length; i++)
      {
        // 번호가 순서대로 있으면
        if (_masks[i].Box.Number == i)
        {
          continue;
        }
        // 완료되지 않았다.
        return false;
      }
      // 끝
      return true;
    }
  }
}

소스를 모두 작성했습니다. 이제 디버그로 확인해 보겠습니다.

게임이 시작되면 Start 메시지가 나오고 퍼즐이 자동으로 섞기게 됩니다.

퍼즐이 모두 섞이면 이제 게임을 시작합니다.

게임을 완료하면 complete 메시지가 표시됩니다.

완료하게 되면 메시지 박스가 조금 이상하게 표기가 됩니다만... 게임을 하는데는 크게 이상이 없습니다.


역시 프레임워크를 사용하는 것이 그냥 GDI로 그리는 것보다 편하네요.. 그리고 컨트롤 Keypad등을 다루는 부분도 게임 개발에 편하게 특화되어 있는 부분도 좋은 듯 싶네요.

Vector2 클래스로 GUI 좌표 계산하는 것도 쉽습니다. Vector3과 Vector4도 있습니다만, 아마 3D 게임 개발할 때 사용되는 좌표 계산 클래스 일 듯 싶습니다.

저도 아직 Monogame 프레임워크는 워낙 생소해서 어떻다라고 이야기하기에는 어렵네요.. 앞으로 게임 관련 글도 조금 쓰려고 하는데 정리해서 Monogame 프레임워크에 관한 글을 써볼까하는 생각도 하고 있습니다.


여기까지 C#의 무료 게임 라이브러리 Monogame를 이용해서 퍼즐 게임 만들기에 대한 글이었습니다.


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