SDL로 제작한 Tic Tac Toe

2022년 03월 22일
제작기간 2022년 03월 15일 ~ 03월 22일
태그 CPP
GitHub

이번에 SDL 튜토리얼을 따라하다가 첫 게임다운 튜토리얼 챕터로 틱택토를 만들게 되었다. 3*3판에서 진행되는 오목 느낌이다.

빌드 시스템으로는 cmake, C++ 버전은 17을 사용하였다. C++을 거의 처음 사용해보아서 빌드 세팅이 정말 번거로웠다. 엔진을 쓸 때 자동으로 path를 관리해주는 것이 얼마나 편하고 얼마나 무책임한 일인지 뼈저리게 느낄 수 있는 시간이었다…😂

아마 brew가 없었으면 아직도 첫 줄을 치고 있었을 지도 모르겠다. 개발을 위한 모든 도구는 brew을 통해서 쉽고 깔끔하게 사용할 수 있었다. 이번에 메인으로 사용한 SDL도!

시작

이 틱택토 프로젝트는 SDL Tic Tac Toe 튜토리얼을 따라서 만들었다. 해당 튜토리얼에서는 각 칸에 번갈아서 X와 O를 그리는 부분까지만 안내하기에, 게임이라고 보기는 어려웠다.

그런 이유로 컴퓨터랑 간단한 플레이를 할 수 있도록 코드를 추가하였다. 상수로 바꾸거나 따로 함수를 두면 좋을 것 같은 부분들도 나름대로 개선을 진행하였다.

formatting은 google을 기준으로 지정하였으며, 컨벤션 또한 google을 참고하였다.

프로젝트

App은 게임의 전반적인 진행을 통솔하는 클래스로 만들었다. 게임 로직의 큰 뼈대와 틱택토 자체의 세부적인 로직까지 포함한다. (이렇게 보니 굉장히 난잡하다ㅜㅜ)

#ifndef _ADD_H_
#define _ADD_H_

#include <SDL.h>

#include "event.h"

namespace game {

class App : Event {
 private:
  bool isRunning;
  bool isOnGame;
  SDL_Surface* display;
  SDL_Surface* grid_image;
  SDL_Surface* x_image;
  SDL_Surface* o_image;
  int current_player;

 private:
  int grid[9];
  enum GRID_TYPE { NONE = 0, PLAYER, COMPUTER };

 private:
  const char* AssetPath = "../assets/";
  const int GridSize = 200;
  const int GridCount = 3;
  const int WholeGridCount = 9;

 public:
  App();
  ~App();

  int OnExcute();

 private:
  bool OnInit();
  void OnLoop();
  void OnCleanUp();

 private:
  void OnRender();
  bool InitSurfaces();

 private:
  void OnEvent(SDL_Event* event);
  void OnLButtonDown(int mX, int mY);
  void OnExit();

 private:
  void Reset();
  void SetCell(int id, int type);
  void OnAutoTurn();
  void CheckWinner();
  void EndGame(int winner);
  int GetGridIndexToDefendOrAttack(int turn);
  int GetEmptyGridIndexRandom();

 private:
  const char* GetFileDir(const char* fileName);
};

}  // namespace game

#endif

빌드 세팅에서 SDL path를 지정해준 것과 별개로 include path가 프로젝트가 아니라면 다시 지정해줄 필요가 있다. 비주얼 코드를 사용한다면 쉽게 c_cpp_properties.json라는 파일을 생성하여 include path를 명시해둘 수 있다.

이렇게 처음부터 (물론 라이브러리가 함께였지만 어쨌든) 게임로직을 작성해보니 엔진에서 제공해주는 기본적인 기능들이 어떤 식으로 실행이 되는지 한 눈에 볼 수 있었다. 아래가 게임 진행의 전부라고 볼 수 있는 부분이다.

int App::OnExcute() {
  if (OnInit() == false) {
    return -1;
  }

  SDL_Event event;
  while (isRunning) {
    while (SDL_PollEvent(&event)) {
      OnEvent(&event);
    }
    OnLoop();
    OnRender();
  }
  OnCleanUp();
  return 0;
}

초기화 후, 게임 종료가 선언될 때까지 루프를 돈다. 루프에서는 들어온 이벤트를 처리한 뒤, 그 외 루프 안에서 처리되어야할 일을 처리하고 화면을 그린다. 이벤트 처리는 상당히 복잡하지만 SDL에서는 이를 쉽게할 수 있는 기능을 제공해주어 위처럼 깔끔하게 다룰 수 있었다.

완성된 게임은 다음과 같다.

스크린샷

애니메이션이나 연출, UI는 전혀 없기 때문에 매우 부실하지만, 처음에 목표한 ‘번갈아서 턴을 진행하고 게임을 무사히 종료 시키기’는 무사히 실행되고 있다. 컴퓨터는 지금 자신의 턴에 랜덤한 위치에 선을 그으며 게임을 진행하지만, 후에 유리한 곳을 찾는 로직을 추가할 예정이다. 지금 당장은 다른 튜토리얼에서 소개하는 기능을 먼저 보고 싶어 패스한다.

빌드 세팅

CMake를 이용해서 빌드를 하기 위해서 CMakeLists.txt를 작성할 필요가 있다.

cmake_minimum_required(VERSION 3.10)

project(sdl-tic-tac-toe)
set(CMAKE_CXX_STANDARD 17)

# Deps

include(FetchContent)
FetchContent_Declare(
    googletest
    URL https://github.com/google/googletest/archive/609281088cfefc76f9d0ce82e1ff6c30cc3591e5.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)

# Build rules

include_directories(./include)
add_executable(tictactoe
  src/event.cpp
  src/surface.cpp
  src/app/app.cpp
  src/app/app_event.cpp
  src/app/app_game_logic.cpp
  src/app/app_render.cpp
  src/main.cpp
)

find_package(OpenGL REQUIRED)
find_package(SDL REQUIRED)
target_link_libraries(tictactoe OpenGL::GL SDL::SDL)

테스트, 포매터 모두 Google 것을 사용했다…라고 말하기엔 테스트 코드를 작성하지 않았다. 이번에 튜토리얼을 따라하기에 급급해서 당연히 신경써야할 것을 신경쓰지 못 한 느낌이다.

후기

처음 써보는 라이브러리였지만 문서가 너무 잘 정리되어있어서 편하게 사용해볼 수 있었다. 어색한 언어라 아직은 서툴지만 곧 익숙해질 수 있을 것 같다. 튜토리얼을 마저 보면서 SDL 라이브러리도 살펴보고, 동시에 C++과도 더 친숙해지고 싶다.