[C++] 테스리스 게임 만들기


Development note/C , C++ , MFC  2021. 3. 11. 16:55

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


이 글은 C++에서 테스리스 게임 만들기에 대한 글입니다.


제가 대학교 시절에 공부하던 책인 Window API 정복이라는 책이 있습니다. 지금도 학교에서 이 책을 가지고 공부하는지 모르겠네요.. 이 책은 MFC도 사용하지 않고 순수 CAPI를 통해서 Window 객체를 사용하는 내용이 있는데 조금 어렵지만 그래도 많은 내용이 있던 걸로 기억하네요.

몇일전 무심코 이 책을 다시 열어 봤는데.. 몇가지 게임 소스가 있는 걸 발견했네요..... 소스가 나쁘지 않아서 한번 더 작성해 볼까 해서 만들어 봤습니다. (책과 똑같이 만들면 아마 저작권 문제가 나올테니 조금 많이 다르게 만들겠습니다.)


먼저 이 소스를 작성하기 위해서는 C++를 이용해서 Window를 만들어야 합니다.

링크 - [C++] C++에서 Window Form을 생성하는 방법(Win32Api, MFC)


먼저 프로젝트를 생성하고 리소스부터 등록합니다.

먼저 메뉴부터 만듭니다.

메뉴에 보면 시작과 종료에 보면 S와 P키를 사용해서 단축키 지정을 했습니다.

그럼 이번에는 단축키(Accelerator)를 설정하겠습니다.

이번에는 윈도우의 아이콘을 만들겠습니다.

이번에는 테트리스에서 사용할 블록을 만들겠습니다.

리소스 등록은 완료했습니다.

아마도 resource.h 파일에 리소스 번호가 자동으로 생성됩니다만, 가끔 리소스 번호가 제멋대로(?) 작성되는 경우가 있으니 한번 확인합니다.

특히 IDB_BITMAP 번호가 중간에 점프 없이 나란히 번호가 생성된 것을 확인합니다.

#include <stdio.h>  
// Window를 실행하기 위한 라이브러리  
#include <Windows.h>  
#include "resource.h"

// 테트리스 가로 길이
#define BW 10
// 테트리스 세로 길이
#define BH 20
#define random(n) (rand()%n)
// 블록 크기 픽셀 크기
#define TS 24

// Window는 한번의 실행으로 끝나는 것이 아니라 윈도우가 종료될 때까지의 무한 루프의 메시지가 필요하기 때문에 callback 함수를 사용한다. 메시지 처리 함수.  
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
HINSTANCE g_hInst;
HWND hWndMain;

// 포인트 구조체
struct Point {
  int x, y;
};
// 블록 설정
Point Shape[][4][4] = {
  // ****
  { {0,0, 1,0, 2,0, -1,0}, {0,0, 0,1, 0,-1, 0,-2}, {0,0, 1,0, 2,0, -1,0}, {0,0, 0,1, 0,-1, 0,-2} },
  // **
  // **
  { {0,0, 1,0, 0,1, 1,1}, {0,0, 1,0, 0,1, 1,1}, {0,0, 1,0, 0,1, 1,1}, {0,0, 1,0, 0,1, 1,1} },
  // **
  //  **
  { {0,0, -1,0, 0,-1, 1,-1}, {0,0, 0,1, -1,0, -1,-1}, {0,0 ,-1,0, 0,-1, 1,-1}, {0,0, 0,1, -1,0, -1,-1} },
  //  **
  // **
  { {0,0, -1,-1, 0,-1, 1,0}, {0,0, -1,0, -1,1, 0,-1}, {0,0, -1,-1, 0,-1, 1,0}, {0,0, -1,0, -1,1, 0,-1} },
  // ***
  // *
  { {0,0, -1,0, 1,0, -1,-1}, {0,0, 0,-1, 0,1, -1,1}, {0,0, -1,0, 1,0, 1,1}, {0,0, 0,-1, 0,1, 1,-1} },
  // *
  // ***
  { {0,0, 1,0, -1,0, 1,-1}, {0,0, 0,1, 0,-1, -1,-1}, {0,0, 1,0, -1,0, -1,1}, {0,0, 0,-1, 0,1, 1,1} },
  //  *
  // ***
  { {0,0, -1,0, 1,0, 0,1}, {0,0, 0,-1, 0,1, 1,0}, {0,0, -1,0, 1,0, 0,-1}, {0,0, -1,0, 0,-1, 0,1} },
  // *
  { {0,0, 0,0, 0,0, 0,0}, {0,0, 0,0, 0,0, 0,0}, {0,0, 0,0, 0,0, 0,0}, {0,0, 0,0, 0,0, 0,0} },
  // **
  // *
  { {0,0, 0,0, 0,-1, 1,0},{0,0, 0,0, -1,0, 0,-1},{0,0, 0,0, 0,1, -1,0},{0,0, 0,0, 0,1, 1,0} },
};
// 공백, 블록, 벽
enum { EMPTY, BRICK, WALL = sizeof(Shape) / sizeof(Shape[0]) + 1 };
// 보드판 크기
int board[BW + 2][BH + 2];
int nx, ny;
int brick, rot;
int nbrick;
// 점수
int score;
// 블록 내려간 횟수
int bricknum;
enum tag_Status { GAMEOVER, RUNNING, PAUSE };
tag_Status GameStatus;
int Interval;
HBITMAP hBit[11];

void DrawScreen(HDC hdc);
// 블록 내리기, 다음 블록 설정하기
void MakeNewBrick();
// 블록 상태 변경하기
int GetAround(int x, int y, int b, int r);
// 블록 상태 내림
BOOL MoveDown();
void TestFull();
void PrintTile(HDC hdc, int x, int y, int c);
void DrawBitmap(HDC hdc, int x, int y, HBITMAP hBit);

// 실행 함수  
int main() {
  // 프로그램 인스턴스 핸들 취득.  
  // Window 프로그램은 하나의 인스턴스 윈도우를 OS에 등록을 해야 한다.  
  HINSTANCE hInstance = GetModuleHandle(NULL);
  g_hInst = hInstance;
  // 윈도우 핸들이다  
  HWND hWnd;
  // 윈도우 메시지이다.  
  MSG Message;
  // 윈도우를 정의하는 구조체이다.  
  WNDCLASS WndClass;
  // 윈도우의 초기 설정하는 값이다.  
  WndClass.cbClsExtra = 0;
  WndClass.cbWndExtra = 0;
  // 윈도우의 메인 바탕색  
  WndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
  // 윈도우의 기본 마우스 커서 형태  
  WndClass.hCursor = LoadCursor(NULL, IDC_ARROW);
  // 윈도우의 아이콘  
  WndClass.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_ICON1));
  // 윈도우의 인스턴스  
  WndClass.hInstance = hInstance;
  // 윈도우의 메시지 callbak 함수  
  WndClass.lpfnWndProc = WndProc;
  // 윈도우를 인식하기 위한 클래스 명  
  WndClass.lpszClassName = L"Tetris3";
  // 윈도우 메뉴 이름  
  WndClass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);
  // 윈도우 스타일  
  WndClass.style = 0;
  // OS에 윈도우를 등록한다.  
  // 윈도우라는 것은 결국 Window OS에서 실행하는 것인데 다른 프로그램들과 동시에 관리하기 위해서는 OS가 Window의 정보를 알고 있어야 한다.  
  // 그를 위해 등록한다.  
  RegisterClass(&WndClass);
  // 윈도우 생성
  hWnd = CreateWindow(WndClass.lpszClassName, WndClass.lpszClassName, WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, CW_USEDEFAULT, CW_USEDEFAULT, 0, 0, NULL, (HMENU)NULL, hInstance, NULL);
  // 이제 윈도우를 띄운다.  
  ShowWindow(hWnd, SW_SHOWDEFAULT);

  HACCEL hAccel = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDR_ACCELERATOR1));
  // 메시지 루프 생성
  while (GetMessage(&Message, NULL, 0, 0)) {
    if (!TranslateAccelerator(hWnd, hAccel, &Message)) {
      // 키보드 메시지를 받고 프로그램에서 사용할 수 있게 변환한다.  
      TranslateMessage(&Message);
      // 메시지를 WndProc로 보내는 함수.  
      DispatchMessage(&Message);
    }
  }
  return 0;
}
// 메시지 처리 함수, 메시지를 처리하는 함수로 메시지란 유저의 입력 또는 OS에서 요구하는 메시지 등을 처리하는 함수  
LRESULT CALLBACK WndProc(HWND hWnd, UINT iMessage, WPARAM wParam, LPARAM lParam) {
  int i;
  RECT crt;
  int trot;
  HDC hdc;
  PAINTSTRUCT ps;
  int x, y;

  // 윈도우를 생성할 때의 구조, main에서 CreateWindowEx로 넣었던 파라미터 값이 있다.  
  // 다른 윈도우를 생성하려면 인스턴스가 필요한데 여기서 취득한다.  
  CREATESTRUCT* cs = (CREATESTRUCT*)lParam;
  switch (iMessage) {
    // 최초 윈도우가 생성되면 호출된다.  
  case WM_CREATE:
    hWndMain = hWnd;
    // 윈도우 크기 설정
    SetRect(&crt, 0, 0, (BW + 12) * TS, (BH + 2) * TS);
    AdjustWindowRect(&crt, WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX, TRUE);
    SetWindowPos(hWndMain, NULL, 0, 0, crt.right - crt.left, crt.bottom - crt.top, SWP_NOMOVE | SWP_NOZORDER);

    // 최초 상태
    GameStatus = GAMEOVER;
    // 랜덤 틱 설정
    srand(GetTickCount());
    // 리소스 로드
    for (i = 0; i < 11; i++) {
      hBit[i] = LoadBitmap(g_hInst, MAKEINTRESOURCE(IDB_BITMAP1 + i));
    }
    return 0;
  // 윈도우에서 명령이 발생하면 여기로 호출   
  case WM_COMMAND:
    switch (LOWORD(wParam))
    {
    // 시작 버튼 누를때
    case ID_GAME_START:
      // 게임 종료 상태면 설정
      if (GameStatus != GAMEOVER) {
        break;
      }
      // 기본 보드판 설정.
      // 벽만들기, 공간 설정 등등.
      for (x = 0; x < BW + 2; x++) {
        for (y = 0; y < BH + 2; y++) {
          board[x][y] = (y == 0 || y == BH + 1 || x == 0 || x == BW + 1) ? WALL : EMPTY;
        }
      }
      // 점수
      score = 0;
      // 블록 내려간 횟수
      bricknum = 0;
      // 게임 상태 설정
      GameStatus = RUNNING;
      // 다음 블록 랜덤 설정
      nbrick = random(sizeof(Shape) / sizeof(Shape[0]));
      // 블록 내리기, 다음 블록 설정하기
      MakeNewBrick();
      // 인터벌 1초
      Interval = 1000;
      // Timer 설정
      SetTimer(hWnd, 1, Interval, NULL);
      break;
    // 정지 버튼을 누르면
    case ID_GAME_PAUSE:
      // 게임 중이면
      if (GameStatus == RUNNING) {
        // 정지로 바꿈
        GameStatus = PAUSE;
        // 타이머 멈춤
        KillTimer(hWnd, 1);
      // 게임 멈춤이면
      } else if (GameStatus == PAUSE) {
        // 게임 재시작
        GameStatus = RUNNING;
        // 타이머 가동
        SetTimer(hWnd, 1, Interval, NULL);
      }
      break;
    // 종료
    case ID_GAME_EXIT:
      // 윈도우 파괴하기
      DestroyWindow(hWnd);
      break;
    }
    return 0;
  case WM_TIMER:
    // 블록 상태 내림
    if (MoveDown() == TRUE) {
      // 새로운 블록 준비
      MakeNewBrick();
    }
    return 0;
  case WM_KEYDOWN:
    // 게임 중이 아니면...
    if (GameStatus != RUNNING || brick == -1) {
      return 0;
    }
    switch (wParam) {
    // 왼쪽 버튼 누르기
    case VK_LEFT:
      // 블록을 왼쪽으로 이동
      if (GetAround(nx - 1, ny, brick, rot) == EMPTY) {
        nx--;
        InvalidateRect(hWnd, NULL, FALSE);
      }
      break;
    // 오른쪽 버튼 누르기
    case VK_RIGHT:
      // 블록을 오른쪽으로 이동
      if (GetAround(nx + 1, ny, brick, rot) == EMPTY) {
        nx++;
        InvalidateRect(hWnd, NULL, FALSE);
      }
      break;
    // 위쪽 버튼 누르기
    case VK_UP:
      // 배열이 3을 넘어서면 0으로 돌아오기
      trot = (rot == 3 ? 0 : rot + 1);
      // 블록 이동
      if (GetAround(nx, ny, brick, trot) == EMPTY) {
        rot = trot;
        InvalidateRect(hWnd, NULL, FALSE);
      }
      break;
    // 아래쪽 버튼 누르기
    case VK_DOWN:
      // 다 내려가면
      if (MoveDown() == TRUE) {
        // 새로운 블록 준비
        MakeNewBrick();
      }
      break;
    // 스페이스바를 누르기
    case VK_SPACE:
      // True가 나올때까지 루프
      while (MoveDown() == FALSE) { ; }
      // 새로운 블록 준비
      MakeNewBrick();
      break;
    }
    return 0;

  // OS에서 윈도우를 그리는 명령을 할 떄 발생하는 이벤트.  
  case WM_PAINT:
    hdc = BeginPaint(hWnd, &ps);
    // 윈도우에 그리기
    DrawScreen(hdc);
    EndPaint(hWnd, &ps);
    return 0;

  // 윈도우가 종료하라는 메시지가 발생하면  
  case WM_DESTROY:
    KillTimer(hWndMain, 1);
    for (i = 0; i < 11; i++) {
      DeleteObject(hBit[i]);
    }
    PostQuitMessage(0);
    return 0;
  }
  // 그외의 메시지는 기본 처리를 하도록 설정한다.  
  return(DefWindowProc(hWnd, iMessage, wParam, lParam));
}
// 그리기
void DrawScreen(HDC hdc) {
  int x, y, i;
  TCHAR str[128];
  // 위쪽 아래쪽 벽 그리기
  for (x = 0; x < BW + 1; x++) {
    PrintTile(hdc, x, 0, WALL);
    PrintTile(hdc, x, BH + 1, WALL);
  }
  // 왼쪽 왼쪽 벽 그리기
  for (y = 0; y < BH + 2; y++) {
    PrintTile(hdc, 0, y, WALL);
    PrintTile(hdc, BW + 1, y, WALL);
  }
  // 현재 테트리스 상태 블록들 그리기
  for (x = 1; x < BW + 1; x++) {
    for (y = 1; y < BH + 1; y++) {
      PrintTile(hdc, x, y, board[x][y]);
    }
  }
  // 블록 그리기
  if (GameStatus != GAMEOVER && brick != -1) {
    for (i = 0; i < 4; i++) {
      PrintTile(hdc, nx + Shape[brick][rot][i].x, ny + Shape[brick][rot][i].y, brick + 1);
    }
  }
  // 다음 블록 나올 영역 그리기
  for (x = BW + 3; x <= BW + 11; x++) {
    for (y = BH - 5; y <= BH + 1; y++) {
      if (x == BW + 3 || x == BW + 11 || y == BH - 5 || y == BH + 1) {
        PrintTile(hdc, x, y, WALL);
      } else {
        PrintTile(hdc, x, y, 0);
      }
    }
  }
  // 다음 블록 그리기
  if (GameStatus != GAMEOVER) {
    for (i = 0; i < 4; i++) {
      PrintTile(hdc, BW + 7 + Shape[nbrick][0][i].x, BH - 2 + Shape[nbrick][0][i].y, nbrick + 1);
    }
  }
  // 점수판 그리기
  lstrcpy(str, TEXT("Tetris Ver 1.2"));
  TextOut(hdc, (BW + 4) * TS, 30, str, lstrlen(str));
  wsprintf(str, TEXT("Score : %d   "), score);
  TextOut(hdc, (BW + 4) * TS, 60, str, lstrlen(str));
  wsprintf(str, TEXT("Block : %d   "), bricknum);
  TextOut(hdc, (BW + 4) * TS, 80, str, lstrlen(str));
}
// 블록 내리기, 다음 블록 설정하기
void MakeNewBrick() {
  bricknum++;
  brick = nbrick;
  nbrick = random(sizeof(Shape) / sizeof(Shape[0]));
  nx = BW / 2;
  ny = 3;
  rot = 0;
  InvalidateRect(hWndMain, NULL, FALSE);
  // 상태가 꽉 차있으면 게임 오버.
  if (GetAround(nx, ny, brick, rot) != EMPTY) {
    KillTimer(hWndMain, 1);
    GameStatus = GAMEOVER;
    MessageBox(hWndMain, TEXT("Game Over."), TEXT("MSG"), MB_OK);
  }
}
// 상태 변경
int GetAround(int x, int y, int b, int r) {
  int i, k = EMPTY;
  // 블록을 상태를 내림
  for (i = 0; i < 4; i++) {
    k = max(k, board[x + Shape[b][r][i].x][y + Shape[b][r][i].y]);
  }
  return k;
}
// 상태 내림
BOOL MoveDown() {
  // 블록이 있으면 True
  if (GetAround(nx, ny + 1, brick, rot) != EMPTY) {
    // 한줄이 꽉차면 제거
    TestFull();
    // 
    return TRUE;
  }
  ny++;
  InvalidateRect(hWndMain, NULL, FALSE);
  UpdateWindow(hWndMain);
  return FALSE;
}
// 상태를 만들기
void TestFull() {
  int i, x, y, ty;
  int count = 0;
  static int arScoreInc[] = { 0,1,3,8,20 };

  for (i = 0; i < 4; i++) {
    board[nx + Shape[brick][rot][i].x][ny + Shape[brick][rot][i].y] = brick + 1;
  }
  brick = -1;

  for (y = 1; y < BH + 1; y++) {
    for (x = 1; x < BW + 1; x++) {
      if (board[x][y] == EMPTY) break;
    }
    if (x == BW + 1) {
      count++;
      for (ty = y; ty > 1; ty--) {
        for (x = 1; x < BW + 1; x++) {
          board[x][ty] = board[x][ty - 1];
        }
      }
      InvalidateRect(hWndMain, NULL, FALSE);
      UpdateWindow(hWndMain);
      Sleep(150);
    }
  }
  score += arScoreInc[count];
  if (bricknum % 10 == 0 && Interval > 200) {
    Interval -= 50;
    SetTimer(hWndMain, 1, Interval, NULL);
  }
}
// 타일을 bitmap으로 그리기
void DrawBitmap(HDC hdc, int x, int y, HBITMAP hBit) {
  HDC MemDC;
  HBITMAP OldBitmap;
  int bx, by;
  BITMAP bit;

  MemDC = CreateCompatibleDC(hdc);
  OldBitmap = (HBITMAP)SelectObject(MemDC, hBit);

  GetObject(hBit, sizeof(BITMAP), &bit);
  bx = bit.bmWidth;
  by = bit.bmHeight;

  BitBlt(hdc, x, y, bx, by, MemDC, 0, 0, SRCCOPY);

  SelectObject(MemDC, OldBitmap);
  DeleteDC(MemDC);
}
// 타일 그리기
void PrintTile(HDC hdc, int x, int y, int c) {
  DrawBitmap(hdc, x * TS, y * TS, hBit[c]);
  return;
}

이제 다 만들어 졌습니다.


이제 실행을 해 봅시다.

이제 S키를 누르거나 Game의 Start 메뉴를 누르고 게임을 시작합니다.

게임이 제대로 작동하는 것을 확인할 수 있습니다.


제가 테트리스를 만들어 보는게 사실 C++로 게임을 개발할려고 하는 건 아니고 Monogame으로 변환하고 최종적으로 Android에서 게임을 개발해 보려고 작성해 보았습니다.


여기까지 C++에서 테스리스 게임 만들기에 대한 글이었습니다.


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