공용 스크립트 제작기🎈 - GameFlow 편

Unity 프로젝트의 공용으로 사용할 수 있는 스크립트를 제작하면서 어떻게 만들었는지, 어떤 점에 신경썼는지 설명하는 글입니다.
유니티 게임을 만들다보면 늘 느끼는 점이 있는데… Unity의 실행 순서의 불확실성 때문에 늘 고생한다는 점이다. 따라서 이를 통합할 수 있는 GameFlowManager를 만들기로 했다. 중앙 집중식 게임 플로우 관리 시스템이라고 멋있게 이름붙여보자.
💡예시
간단하게 먼저 사용 예시부터 소개해보자면 다음과 같다.
public class Player : UpdatableBase
{
public override int UpdateOrder => 100; // 실행 순서
public void Initialize()
{
Register(); // GameFlowManager에 등록
}
public override void OnUpdate(float deltaTime)
{
// 플레이어 로직
}
}
public class Execute : MonoBehaviour
{
private Player player;
void Awake()
{
// 객체 생성 및 초기화
player = new Player();
player.Initialize(); // GameFlowManager에 자동 등록
// 이제 GameFlowManager가 알아서 순서대로 업데이트!
}
void OnDestroy()
{
// 정리
player?.Dispose();
}
}
이렇게 사용하면 된다. 음 아직 이해가 안갈 수도 있겠지만 아래의 설명을 보면 명확해질 것이라 믿는다.
📊 클래스 다이어그램
GameFlowManager (싱글톤)
├─ Update Loop
├─ FixedUpdate Loop
└─ LateUpdate Loop
Game Flow Manager가 하위의 Update들을 보고 순차 실행하는 구조다. 맞다 사실 별거 없다.
📁 시스템 아키텍처
핵심 컴포넌트
인터페이스 계층
IUpdatable, IFixedUpdatable, ILateUpdatable 세 가지 인터페이스를 제공한다. 각 인터페이스는 보면 알겠지만 각각 유니티의 Update, FIxedUpdate, LateUpdate를 실행하도록 되어있다.
Base 클래스 계층
인터페이스 계층을 상속받아서 구현된 베이스 클래스이다. UpdatableBase, FixedUpdatableBase, LateUpdatableBase 세 가지 Base 클래스가 있고, 아래의 기능을 가지고 있다
- int UpdateOrder: 실행 우선순위를 정의(낮은 값일수록 먼저 실행)
- Register(): Initialize등에서 호출하여 GameFlowManager에게 등록한다
- Unregister(): Dispose()에서 자동으로 호출되며 GameFlowManager에게서 해제된다
- OnUpdate/OnFixedUpdate/OnLateUpdate: 상속받는 클래스에서 필수로 구현한다
- Dispose(): 반드시 호출해야하는 메소드이며, 호출하지 않았을 때 경고 표시한다
- OnDispose(): 상속받는 클래스에서 선택적으로 구현한다
GameFlowManager
Base 클래스를 상속받은 파생 클래스들을 모아서 관리하며 여기서 SortingOrder를 보고 순차적으로 처리한다.
// Update()
float deltaTime = Time.deltaTime;
for (int i = 0; i < updatables.Count; i++)
{
updatables[i].OnUpdate(deltaTime);
}
// FixedUpdate()
float fixedDeltaTime = Time.fixedDeltaTime;
for (int i = 0; i < fixedUpdatables.Count; i++)
{
fixedUpdatables[i].OnFixedUpdate(fixedDeltaTime);
}
// LateUpdate()
float deltaTime = Time.deltaTime;
for (int i = 0; i < lateUpdatables.Count; i++)
{
lateUpdatables[i].OnLateUpdate(deltaTime);
}
🔄 데이터 흐름
- 객체 생성
- Initialize() → Register() → GameFlowManager 등록됨
- GameFlowManager가 매 프레임 OnUpdate() 호출
- 해제 시 Dispose() → Unregister()(자동) → GameFlowManager 해제됨
- 소멸
🎯 신경 쓴 부분
Pending System
Update 실행 중에도 안전하게 등록/해제 가능하도록 했다. 따라서 아래와 같이 사용해도 문제가 없다.
public override void OnUpdate(float deltaTime)
{
if (shouldSpawnEnemy)
{
var enemy = new Enemy();
enemy.Initialize(); // 이 안에서 Register() 호출
// Pending 시스템이 다음 프레임에 안전하게 등록
}
}
Pending System이 무엇인가 하면 별건 아니고 반복문 중에 리스트 수정을 해도 문제 없도록 일단 메모해뒀다가 나중에 처리하도록 만들었다.
요청이 들어왔을 때, 현재 Update가 실행중이라면 Pending 리스트에 추가하고(메모) 실행중이 아니라면 바로 추가한다.
// 지금 Update 실행 중인가?
if (isUpdating)
{
// 실행 중이면 → Pending 리스트에 추가 (메모만 해둠)
if (!pendingAddUpdatables.Contains(updatable))
{
pendingAddUpdatables.Add(updatable);
// "이따가 추가해야지~" 메모
}
}
else
{
// 실행 중이 아니면 → 바로 추가
if (!updatables.Contains(updatable))
{
updatables.Add(updatable);
}
}
이후 업데이트 실행에서 Pending된 리스트들부터 먼저 업데이트 루프에 편승시킨 후 업데이트를 돌린다.
또한 새 객체가 등록되면 Sort 시스템을 돌린다.
게임 일시정지
IsPaused 상태를 만들어서 Update를 중단하게 만들었다. 따라서 별도의 deltaTime 조작 없이도 일시중지가 가능하다.
private void Update()
{
// 일시정지 상태면 실행 중단
if (_isPaused)
return;
(...)
}
결론
여러모로 쓸모있는 시스템을 만든 것 같다. 음 굿!