본문으로 바로가기

게임 제작 프레임워크 프로젝트의 시작

언리얼 엔진 코드를 분석하던 중 언리얼 엔진 코드를 참고하여 직접 게임 프레임워크를 작성해 보면 재밌을 것 같다는 생각이 들었습니다. 그리고 직접 프레임워크를 작성하다보면 그동안 공부하였던 C++의 기능들(스마트 포인터, 템플릿, STL의 다양한 컨테이너와 알고리즘 등)을 직접 프로젝트에 적용해볼 좋은 기회가 될 것이라고 생각하고 이번 프로젝트를 시작하게 되었습니다.

 

우선 게임 프레임워크를 직접 제작하는 첫 번째 단계에서는 Game 인스턴스를 생성하고, Actor와 Component 인스턴스를 생하여 Game Loop에서 매 프레임마다 사용자 입력게임 로직 실행출력 생성 을 하는 기능을 구현하고자 하였습니다. 그리고 2D 렌더링을 목표로 하였습니다. 그리고 이런 기초적인 기능으로 간단한 게임을 만들어 보는 것을 1단계 목표로 설정하였습니다. 그 이후에는 Audio System, UI, Scene, 3D 렌더링 파이프라인 구축, AI 등 필요에 맞는 기능들을 구현해보고자 하였습니다.

 

프레임워크 개발 프로세스

게임 프레임워크를 개발하는 과정에서 단순히 게임 프레임워크만 제작한다면 어떤 기능들이 필요할지 상당히 추상적이고 모호할 수 있다고 생각했습니다. 그리고 프레임워크는 결국 도구이기 때문에 프레임워크를 활용하여 하나의 게임을 만들어야지만 도구의 우수성 혹은 문제점을 충분히 발견할 수 있다고 생각했습니다. 그래서 게임 프레임워크를 제작하는 방식은 [목표하는 게임 선정][게임을 제작하기 위한 모듈 제작][목표한 게임 제작] 와 같은 프로세스를 가지고 게임 프레임워크의 모듈을 하나씩 제작하고자 했습니다.

 

예를 들어 Chess 2D 게임에서 체스말을 출력하기 위해서는 SpriteComponent라는 하나의 모듈이 필요합니다. 체스 말을 출력하는 기능을 게임 로직에 두는 것이 아니라 프레임 워크로 분리하여 개발하는 것입니다. 그리고 Chess 2D 게임을 목표로 선정하였기 때문에 Chess 2D 게임을 구현하기 위해서 필요한 기능들을 정리하면, 게임 제작 프레임워크가 갖추어야 할 기능들이 더욱 명확하게 정리할 수 있다고 생각했습니다. 이후 다양한 게임들을 개발하며 게임 제작 프레임워크 기능들을 계속 추가할 계획입니다.

 

프레임워크 1단계 개발 완료 및 Chess 2D 제작

앞서 언급한 프레임워크의 1단계 기본 기능들을 구현하였고, 이를 바탕으로 Chess 2D 게임을 다음과 같이 완성하였습니다. 우선 간단한 기능을 가진 프레임워크인 만큼 요구되는 기능이 다소 적은 보드류 게임을 개발하고자 했습니다. 보드류 게임중 개인적으로 좋아하고 사용가능한 리소스가 충분한 체스게임을 첫 번째 게임으로 선정하였습니다. 

프레임워크 역할

  • 언리얼 엔진과 비슷한 Actor - Component 관계를 활용하여 게임을 설계할 수 있습니다.
  • Sprite Component를 제공하여 2D게임을 제작할 수 있습니다.
  • Actor를 상속받아 사용자가 원하는 Actor의 모습을 구체화할 수 있습니다.
  • GameManager 클래스를 통해서 특정 게임의 규칙과 리소스를 관리할 수 있습니다.

체스 게임 구현 내용

  • 자체 제작한 게임 프레임워크를 활용하여 64개의 Square Actor와 32개의 Piece Actor를 생성하였습니다.
  • 모든 기물은 게임 규칙에 맞는 움직임을 가집니다.
  • 앙파상, 캐슬링 등 특수한 행마에 대해서도 구현하였습니다.

소스코드 링크

🔗 전체 소스코드(Github)

  • 게임 프레임워크와 관련된 코드는 Core 폴더에서
  • Chess게임과 관련된 코드는 Source 폴더에서 확인할 수 있습니다.

 

프레임워크 Core 구조

Game Class

  • 게임 초기화, 종료 및 게임의 메인 반복문을 담당합니다. 메인 반복문에서는 아래의 크게 아래의 3가지 동작을 실행합니다.
    • ProcessInput() : 플레이어의 입력을 받고 처리합니다.
    • UpdateGame() : 매 프레임 게임의 상황을 갱신합니다. 특히 모든 Actor의 Update() 함수를 호출합니다.
    • GenerateOutput() : Renderer를 활용한 출력을 담당합니다. 현재는 2D Sprite 출력만 진행합니다.
  • 출력을 위한 Renderer를 생성하며, 게임의 규칙 및 로직을 담당하는 GameManager를 생성합니다.
  • 모든 Actor객체가 저장되는 mActors 배열을 가지고 있으며, Actor를 생성 및 제거할 수 있습니다. Actor를 안전하게 관리하기 위해서 스마트 포인터를 활용하며, 재사용성을 높이기 위해서 템플릿을 활용하였습니다.
template<class T, class... Param>
std::shared_ptr<T> Game::CreateActor(Param&&... _Args)
{
    // Actor의 파생클래스가 아닐경우 컴파일 에러처리
    static_assert(std::is_base_of<Actor, T>::value, "Template argument T must be a derived class from the Actor class");

    std::shared_ptr<T> actor = std::make_shared<T>(std::forward<Param>(_Args)...);
    actor->Initialize();
    AddActorToArray(std::static_pointer_cast<Actor>(actor));
    return actor;
}
// 아래와 같이 사용할 수 있습니다.
// CreateActor<Knight>(...);
// CreateActor<Square>(...);

Renderer Class

  • SDL window와 SDL Renderer를 생성합니다.
  • 게임에 존재하는 모든 SpriteComponent를 그려질 순서대로 정렬된 동적 배열로 관리합니다.
  • 순서대로 저장된 SpriteComponent를 순서대로 화면에 출력합니다.

Actor Class

  • Unreal Engine처럼 각 게임에서 Actor를 상속받아 게임에 필요한 객체를 제작할 수 있습니다.
  • Actor는 EActive , EPaused , EDead 상태를 가집니다.
  • Actor는 자신의 Transform을 가집니다.
  • Actor는 필요한 Component를 생성하여 기능을 확장할 수 있습니다. Component를 생성하는 방법은 CreatActor 함수와 마찬가지로 스마트 포인터와 탬플릿을 활용하여 안정성과 재사용성을 높였습니다.
  • 그리고 Actor는 Component의 mUpdateOrder 순서대로 정렬된 동적 배열 mComponents 를 가집니다.

Component Class

  • 각 Component는 Actor에서 생성 및 관리됩니다. 모든 Component의 기본 클래스입니다.
  • 기본 Component는 그 자체로는 아무런 기능이 없는 추상 클래스입니다.
  • Component는 Update() , Initialize() , Shutdown() 인터페이스를 제공합니다.

SpriteComponent Class

  • Sprite를 그리기 위한 목적으로 Component 클래스를 상속받아서 만든 클래스입니다.
  • SpriteComponent는 Actor의 mComponents 배열에는 mUpdateOrder 순으로 저장되며, Renderer의 mSpriteComponents 에서는 mDrawOrder 순으로 저장됩니다.
  • Draw() 함수를 통해서 SpriteComponent가 가지고 있는 텍스처를 Actor의 위치, 크기, 회전에 맞게 화면에 출력하는 기능을 가졌습니다.
  • SetAble() 함수와 SetDisable() 함수를 활용하여 그리기 활성화 및 비활성화를 할 수 있습니다.

GameManager Class

  • 게임 규칙 및 플레이어를 관리하고 직접적인 게임의 로직을 담당하는 class입니다.
  • GameManager를 통해서 게임의 프레임워크 코드와 게임 로직 코드를 분리 및 연결할 수 있습니다.
  • Game이 생성될 때 GameManager가 생성되며 게임에 필요한 데이터들을 로딩합니다. 이번 2D Chess 프로젝트에서는 게임에 필요한 객체들을 모두 이곳에서 생성합니다.

Chess 2D 게임 구조

Piece Class

  • Actor의 자식 클래스로, 체스 기물들의 공통적인 특징들이 정의되어 있습니다.
    • Piece는 기물의 색상, 타입, SpriteComponent, 현재 공격 지역, 이동 가능 지역을 가집니다.
    • Piece의 턴을 시작하고, 파괴되고, 선택되는 등의 행동을 멤버 함수로 정의하였으며, 다음 이동 가능지역과 공격 지역을 탐색하는 SearchAttackAndMoveLocation() 함수를 순수 가상 함수로 선언하여 각 기물의 특성에 맞게 재정의하도록 강제하였습니다.
  • Pawn, Knight, Bishop, Rook, Queen, King 기물들은 Piece 클래스를 상속받아 각자 기물들의 행마 규칙에 맞게 Piece의 멤버 함수들을 재정의하였습니다.

Square Class

  • Square는 체스 판의 하나의 사각형을 의미합니다. GameManager 객체에서 8*8 2차원 배열 mBoard 를 활용하여 64개의 Square를 생성 및 관리합니다.
  • Square는 mNormalSprite , mSeletedSprite , mCandidatedSprite 3가지의 SpriteComponent를 가지며 상황에 맞게 Sprite를 출력할 수 있습니다.
  • Square는 현재 Square에 점유 중인 Piece의 정보와 공격하고 있는 Color의 정보 등을 가지고 있어서 기물들이 다음 움직임을 결정할 때 많이 참조됩니다.

Player Class

  • 체스에는 Black, White 두 가지 색상 2명의 플레이어가 존재합니다. GameManager에서 색상별 Player를 멤버 변수로 가집니다.
  • 플레이어는 자신의 차례에 LeftClickDown() 함수에서 아래의 두 멤버 함수를 호출하여 기물을 선택하고 이동 지역을 선택합니다.
    • SelectPieceForMove() : 이동할 기물 선택
    • SeletSquareToMove() : 이동할 지역 선택. 이동 후 턴 종료
  • 자신의 차례가 시작될 때 StartTurn() 함수가, 자신의 차례가 종료될 때 EndTurn() 함수가 호출되어 기물들의 공격 중인 지역을 담은 배열 mAttackedLocations 과 기물들의 다음 이동 가능 지역을 담은 배열 mMoveLocations 을 갱신됩니다.