5월부터 게임 엔진을 만들기 시작했다.
중간에 잠시 중단한 적도 있지만 지금까지 약 7개월 동안 게임 엔진을 개발했다.
그래서 현재까지 개발한 내용을 기록해보려고 한다.
현재까지 개발한 코드: https://github.com/airhood/CubicEngine
GitHub - airhood/CubicEngine
Contribute to airhood/CubicEngine development by creating an account on GitHub.
github.com
그래서 정확히 뭘 만들건데?
우선 정확히 어떤 역할을 하는 게임 엔진을 어떤 구조로 만들 것인지 정해야 한다.
프로젝트의 규모가 큰 만큼 다른 게임을 개발할 때처럼 대충 설계를 했다가는 나중에 가서 힘들어질 게 예상됐기 때문에 설계에 많은 시간을 투자했다.
그래서 나는 다른 상용 게임 엔진들의 구조를 살펴보았다.
대표적으로 Uniy, Unreal Engine 등이 있었다.
이번 프로젝트에서 게임 엔진을 직접 구현해보는 것에 초점을 맞출 것이기에 사용 엔진들의 설계를 가져와서 비슷하게 만들어도 될 것 같다는 생각이 들어서, 내가 주로 사용하던 Unity와 비슷하게 설계하였다.
다만 평소에 마음에 들지 않던 시스템은 내 마음대로 새롭게 설계하였다.
여러 엔진의 구조를 살펴본 결과 게임 제작의 편의성이 주 목적인 게임 엔진에서는 주로 게임 내에 존재할 물체, 즉 GameObject를 기반으로 각 GameObject가 게임 내에서 어떻게 행동할지 GameObject에 Component라는 것을 추가하여 조작한다는 것을 알게 되었다.
그래서 나는 이를 기반으로 GameObject, GameObject에 추가될 Component, GameObject들을 담고 있을 세계인 Scene의 클래스를 구현했다.
구현하는 과정에서 고민했던 것들 중 하나는 포인터 관리였다.
클래스 내부에는 객체들을 포인터가 저장되어 있을 것인데, C++에서는 객체를 해체해도 그 객체가 가지고 있던 포인터들은 자동으로 해체되지 않기 때문에 메모리 누수가 발생할 수 있기 때문이다.
메모리 누수 문제는 많은 양의 데이터를 처리하고 많은 양의 메모리를 사용하는 게임 엔진에서는 매우 치명적을 다가온다.
따라서 나는 객체를 해체할 때 내부에 있는 포인터 중에서 해체가 필요한 것들이 모두 해체되고 나서 원래 객체가 해체되도록 소멸자를 구성하였고, 이미 해체한 포인터를 다시 해체하는 것을 막기 위해서 해체한 메모리는 항상 nullptr로 값을 설정해 놓도록 하였다.
EngineCore
기본적인 클래스를 구현한 후에, 나는 게임 엔진의 전체적인 시스템을 조작할 수 있는 Core 클래스를 만들었다.
나는 게임 엔진을 사용자가 조작할 수 있는 Application 영역과, Core의 명령에 의해서만 작동하는 Engine 영역으로 나누었다.
Application 영역에는 앞에서 구현한 GameObject, Component와 같은 클래스들이 있고,
Core 영역에는 추후에 만들게 될 RenderManager, SceneManager, GameObjectManager와 같은 클래스가 있다.
이렇게 두 영역으로 나는 이유는 게임 엔진의 오류를 최소화하기 위해서이다.
내부 시스템을 모두 EngineCore를 통해 해결함으로써 Manager 클래서의 포인터가 여러군데 분포하여 포인터에 문제가 생기는 것을 막을 수 있고, EngineCore에만 Manager 클래서의 포인터가 저장되어 있기 때문에 이를 해체할 때에도 잘못된 해체로 인해 오류가 생길 가능성도 막을 수 있다.
Engien Life-Cycle
게임 엔진에 있는 함수들은 모두 특정 시간대에 특정 순서로 실행되어야 한다.
이를 결정하는 것이 바로 Life-Cycle이다.
각 게임 엔진마다 다른 Life-Cycle을 가지고 있다.
그래서 나도 내 게임 엔진의 Life-Cycle을 정해 보았다.
우선 게임 엔진이 실행되었을 때 최초로
Start() 이벤트가 실행된다.
매 프레임마다 실행되는 Cycle에서는
FrameTick() 이벤트
LateTick() 이벤트
가 실행된다.
물리 엔진은 성능 문제로 매 프레임마다 실행될 수 없기 때문에 따른 Life-Cycle에서 작동한다.
매 물리 엔진 Tick 마다
PhysicsTick() 이벤트가 실행된다.
그런데 내가 함수가 아니라 이벤트라고 말한 이유는 다양한 클래스에서 Life-Cycle이 돌아가야 하는데,
이를 각각 관리할 수 없기에 각 클래스에 이벤트들을 가진 base 클래스를 상속시킨 후에 EngineCore에서 한번에 실행시키는 방법으로 작동할 것이기 때문이다.
즉 해당 이벤트가 EngineCore에서 실행되면 이를 가진 모든 클래스에서 차례대로 이에 맞는 함수가 EngineCore에서 실행되는 것이다.
이렇게 Life-Cycle을 구성한 뒤에 각 클래스에 맞는 이벤트를 가진 base 클래스를 만들었고, 이를 상속시켰다.
그런 다음에 EngineCore에서 이벤트를 가진 클래스들의 이벤트에 맞는 함수를 실행시키기 위해서는 객체들의 포인터를 모두 EngineCore에서 가지고 있어야 할 것인데,
단순하게 EngineCore가 모든 포인터를 가진다면 관리가 어려워지기 때문에 종류별로 RenderManager, SceneManager, GameObjectManager 등으로 나누어서 각 Manager에 객체들의 포인터를 저장하도록 했고,
각 Manager의 포인터를 가지고 있는 EngineCore에서 Manager에 명령을 내려서 객체들의 이벤트에 맞는 함수를 실행시킬 수 있도록 구성하였다.
'C++' 카테고리의 다른 글
[C++] 캐스팅(Casting) (0) | 2024.10.16 |
---|---|
[C++] 추상 클래스(abstract class) (0) | 2024.10.14 |