SDL로 제작한 근본 슈팅게임

2022년 08월 02일
제작기간 2022년 07월 02일 ~ 08월 02일
태그 CPP Project
GitHub
  • 언어: C++ 17
  • 라이브러리: SDL2
  • 빌드: Bazel
  • OS: macOS (Monterey)

SDL2 라이브러리를 이용하여 기본적인 기능을 보유한 슈팅 게임을 개발하였다. 개발 환경은 위와 같다. 이미지 어셋은 전부 직접 제작하였다.

동기

CPP을 써보고 싶었다. 별도의 엔진을 사용하지 않고 가볍게 개발을 해보고 싶었고, SDL 라이브러리를 이용하면 CPP로 게임을 개발하는데 편의를 볼 수 있을 것이라 판단했다. 모든 랜더링을 손으로 처리하기 보다는 라이브러리를 이용하여 조금 더 쉽게 경험해보고자 했다.

개발

큰 목표는 앞으로 SDL을 사용해서 게임을 만들 때 편하게 사용할 수 있는 게임 루프와 구조를 만들어두는 것이다. 그 부분은 shooting-game/core로 구분하였다. 게임을 만들면서 필요 기능을 확인하고자 했기 때문에 플레이도 함께 구현했다. 그 부분은 shooting-game/play에 해당한다.

core

게임의 큰 흐름은 아래와 같다.

int Game::OnExecute() {
  if (OnInit() == false) return -1;

  SDL_Event event;
  while (running) {
    while (SDL_PollEvent(&event)) {
      OnEvent(&event);
    }
    Event::OnRetainKeyEvents();
    if (isPause) {
      // Pass loops.
    } else {
      OnLoop();
      OnCollision();
    }
    OnRender();
    SDL_Delay(16);
  }
  OnCleanUp();
  return 0;
}

초기화 후 루프를 돌면서 게임이 진행된다. 게임의 로직 실행, 충돌 처리, 렌더링으로 나뉜다. 게임을 정지했다면 렌더링 외의 루프에서 일어나는 일은 무시한다.

void Game::OnLoop() {
  for (auto entity : EntityRegistry::GetInstance()->GetRegistry()) {
    if (entity->GetIsActive()) {
      entity->OnLoop();
    }
  }
}

게임 루프는 EntityRegistry에 등록된 entity 중 활성화 된 entity를 대상으로 작업을 수행한다. 충돌과 렌더링, 이벤트 인풋 처리도 마찬가지다.

Entity는 게임을 진행하는데 필요한 컴포넌트를 소지하고 있다. Transform, Collider, Surface(animation을 포함)를 통해서 게임 로직에 필요한 기능을 수행한다.

Transform은 Entity의 위치와 스케일을 관리하고, Collider는 충돌을 검색하는 용도이다. Surface는 Entity를 그려내는 컴포넌트로, 애니메이션을 담당하거나 Sprite sheet에서 필요한 Sprite만 보여주는 기능을 한다. 배경에서 사용할 수 있는 용도로 타일맵 기능도 추가하였다.

play

캐릭터를 움직이며 총을 쏠 수 있고, 총을 쏘며 등장하는 몬스터를 쏘는 기본적인 게임이다. 적을 처치하면 우상단의 점수에, 총알이나 적과 부딪히면 좌상단의 하트에 영향이 간다.

플레이어, 적, 총알, 걷는 이펙트, 적의 피격 이펙트 Entity로 게임이 구성되었다. 게임의 Entity들이 UI, Score 등의 서비스를 호출해야하기에 service_provider를 따로 두어 lazy initialization 되는 서비스를 제공하였다.

게임 시작 화면

비트맵 폰트로 제작한 문구이다. 기능이 유틸리티로 사용할 수 없을 정도여서 core가 아닌 play로 분류하였다. 텍스트는 추후 개선할 생각이다. Return 버튼으로 시작할 수 있다.

게임 화면

화살표 키로 이동, 스페이스로 공격이 가능하다. 부적을 던져서 꺼먹살이를 퇴치한다는 컨셉이다.

게임 오버 화면

하단에 있는 것이 점수다. Return 버튼으로 재시작, Escape 버튼으로 게임 종료를 할 수 있다.

CI Settings

깃허브에서 세팅하였다. bazel을 이용한 빌드, 테스트, 빌드 업로드(mac과 ubuntu)를 진행한다. 별도의 설치 과정이 필요하지 않고, bazelisk을 이용하여 빌드할 수 있기 때문에 굉장히 간단히 세팅이 가능하다. 그러나 SDL 빌드에서 쓰이는 시간이 꽤 길어 진행 시간이 짧지는 않다.

- name: Build
  run: bazelisk build //shooting-game

test의 경우, google test를 이용해 만들었는데 프로젝트 후반부에 도입하여서 온전한 기능을 하고 있지는 않다…🥺 지금 이 글을 쓰는 동안에도 테스트가 실패 중이다… 주말에 확인해서 고쳐야지…

후기

SDL의 다른 라이브러리(TTF, image 등)를 사용해보고 싶었지만 진행을 미뤘다. 라이브러리가 의존하는 다른 라이브러리를 모두 확인하며 CMake로 진행되는 빌드를 다 확인하기에 너무 번거로움이 느껴졌다. bazel 사용마저도 익숙하지 않아서 주위의 도움을 많이 받아가며 했는데, 의존하는 라이브러리를 모두 신경쓰려고 보니 점점 복잡해졌다. 우선적인 목표는 CPP에 대한 경험이었기에 적당히 필요한 기능은 만들어가며 사용했다.

텍스트 UI가 특히 마음에 걸린다. 지금은 비트맵 폰트로 제작하였고, 대문자 문자열만 정상적으로 출력하도록 만들었는데, 사용하기도 번거롭고 너무 주먹구구식의 구현이다. 후에 폰트를 임포트하여 사용할 수 있도록 가공하고, 모든 문자를 정상적으로 출력할 수 있게 하는 것이 이상적이다.

이 게임을 더 이어갈 때, 사운드 시스템과 폰트 시스템을 추가하고 비트맵을 사용하는 기존 방식을 png를 사용하도록 수정할 생각이다.

tree

.
├── .bazelrc
├── .clang-format
├── .gitattributes
├── .github
│   └── workflows
│       └── build-and-test.yml
├── .gitignore
├── BUILD
├── README.md
├── WORKSPACE
├── assets
│   ├── BUILD
│   ├── box.bmp
│   ├── brown_1px.bmp
│   ├── bullets.bmp
│   ├── effects
│   │   ├── BUILD
│   │   ├── blood.bmp
│   │   └── walk.bmp
│   ├── enemy.bmp
│   ├── hearts.bmp
│   ├── letters.bmp
│   ├── numbers.bmp
│   ├── player.bmp
│   └── tiles.bmp
├── docs
│   ├── design.md
│   └── diary.md
├── shooting-game
│   ├── BUILD
│   ├── core
│   │   ├── BUILD
│   │   ├── config.h
│   │   ├── coordination.cpp
│   │   ├── coordination.h
│   │   ├── coordination_test.cpp
│   │   ├── entity
│   │   │   ├── BUILD
│   │   │   ├── collider.cpp
│   │   │   ├── collider.h
│   │   │   ├── entity.cpp
│   │   │   ├── entity.h
│   │   │   ├── entity_test.cpp
│   │   │   ├── transform.cpp
│   │   │   ├── transform.h
│   │   │   └── visual
│   │   │       ├── BUILD
│   │   │       ├── animation.cpp
│   │   │       ├── animation.h
│   │   │       ├── surface.cpp
│   │   │       ├── surface.h
│   │   │       └── surface_test.cpp
│   │   ├── entity_registry.cpp
│   │   ├── entity_registry.h
│   │   ├── event.cpp
│   │   ├── event.h
│   │   ├── game.cpp
│   │   ├── game.h
│   │   ├── object_pool.cpp
│   │   ├── object_pool.h
│   │   └── object_pool_test.cpp
│   ├── play
│   │   ├── BUILD
│   │   ├── background.cpp
│   │   ├── background.h
│   │   ├── bullet
│   │   │   ├── BUILD
│   │   │   ├── bullet.cpp
│   │   │   ├── bullet.h
│   │   │   ├── bullet_pool.cpp
│   │   │   └── bullet_pool.h
│   │   ├── enemy
│   │   │   ├── BUILD
│   │   │   ├── enemy.cpp
│   │   │   ├── enemy.h
│   │   │   ├── enemy_blood_pool.cpp
│   │   │   ├── enemy_blood_pool.h
│   │   │   ├── enemy_hit_effect.cpp
│   │   │   ├── enemy_hit_effect.h
│   │   │   ├── enemy_spawner.cpp
│   │   │   └── enemy_spawner.h
│   │   ├── game_settings.h
│   │   ├── play_manager.cpp
│   │   ├── play_manager.h
│   │   ├── player
│   │   │   ├── BUILD
│   │   │   ├── player.cpp
│   │   │   ├── player.h
│   │   │   ├── player_walk_effect.cpp
│   │   │   └── player_walk_effect.h
│   │   ├── score_manager.cpp
│   │   ├── score_manager.h
│   │   ├── service_provider.cpp
│   │   ├── service_provider.h
│   │   └── ui
│   │       ├── BUILD
│   │       ├── game_over_ui.cpp
│   │       ├── game_over_ui.h
│   │       ├── game_start_ui.cpp
│   │       ├── game_start_ui.h
│   │       ├── hp_heart_ui.cpp
│   │       ├── hp_heart_ui.h
│   │       ├── hp_ui.cpp
│   │       ├── hp_ui.h
│   │       ├── number_ui.cpp
│   │       ├── number_ui.h
│   │       ├── score_ui.cpp
│   │       ├── score_ui.h
│   │       ├── text
│   │       │   ├── BUILD
│   │       │   ├── letter_ui.cpp
│   │       │   ├── letter_ui.h
│   │       │   ├── letter_ui_test.cpp
│   │       │   ├── text_ui.cpp
│   │       │   ├── text_ui.h
│   │       │   └── text_ui_test.cpp
│   │       ├── ui_manager.cpp
│   │       └── ui_manager.h
│   └── shooting-game.cpp
└── third_party
    ├── BUILD
    └── sdl
        ├── BUILD
        └── sdl2.BUILD