메인 콘텐츠로 건너뛰기

"Architecture" 태그의 게시물 7개 게시물건

Game architecture and design patterns

모든 태그 보기

크로스씬 이벤트: 아무도 말하지 않지만 모두가 겪는 영속화 문제

TinyGiants
GES Creator & Unity Games & Tools Developer

AudioManager가 배경 음악을 재생한다. OnLevelStart에 구독하여 플레이어가 새 지역에 진입하면 트랙을 변경한다. DontDestroyOnLoad 오브젝트에 AudioManager를 배치해서 씬 로드 간에 유지되도록 한다. 같은 씬에서만 테스트하고 있으니까 개발 중에는 모든 것이 잘 동작한다.

그러다 누군가 처음으로 Level 1에서 Level 2를 로드한다. 음악이 바뀌지 않는다. AudioManager는 살아있다 — DontDestroyOnLoad이 제 역할을 했다 — 하지만 이벤트 구독이 전환을 넘기지 못했다. 더 나쁜 경우: 이전 구독이 여전히 남아있고, 파괴된 Level 1의 이벤트 발생자를 가리키고 있어서, 다음에 뭔가가 호출하려 하면 게임플레이 도중에 MissingReferenceException이 터진다.

이것이 영속화 문제이며, 씬이 두 개 이상인 모든 Unity 프로젝트가 결국 맞닥뜨린다.

출시 후 발견되는 이벤트 시스템의 함정: 메모리 누수, 데이터 오염, 재귀 트랩

TinyGiants
GES Creator & Unity Games & Tools Developer

게임을 5분씩 테스트하고 있다. 잘 돌아간다. 그런데 QA가 리포트를 올린다: "30분 플레이 세션 동안 메모리 사용량이 꾸준히 증가합니다. 6개의 씬을 로드하면 프레임 레이트가 60에서 40으로 떨어집니다." 프로파일링해 본다. 12개여야 할 이벤트에 847개의 리스너가 등록되어 있다. 매 씬 로드마다 새로운 구독이 추가됐지만 이전 것은 제거되지 않았다. 오브젝트는 파괴됐지만, 델리게이트 참조가 살아남아 죽은 MonoBehaviour들을 메모리에 고정시키고 있어서 가비지 컬렉터가 건드릴 수 없다.

아니면 이런 것: "두 번째 Play Mode 세션에서 체력 값이 틀립니다. 첫 번째 실행은 정상입니다." Play를 누르고, 전투를 테스트하고, 멈춘다. 다시 Play. 플레이어가 100이 아닌 73 HP로 시작한다. 아무도 리셋하지 않아서 마지막 세션의 ScriptableObject 상태가 그대로 남아 있는 것이다.

혹은 고전적인 것: 게임이 3초 동안 멈추더니 Unity가 크래시한다. Event A의 리스너가 Event B를 Raise했다. Event B의 리스너가 Event A를 Raise했다. 스택 오버플로우. 하지만 때로는 크래시하지 않는다 — 그냥 멈춰서 아무런 에러도 표시하지 않은 채 CPU를 갈아먹는 무한 루프로 빠진다.

이것들은 가설이 아니다. 프로덕션 게임에서 출시된 걸 직접 본 버그들이다. 그리고 모두 같은 근본 원인을 갖고 있다: 개별적으로는 올바르게 보이지만 규모가 커지면 실패하는 이벤트 시스템 패턴.

병렬 vs 순차: 모든 이벤트 시스템에 필요한 두 가지 실행 패턴

TinyGiants
GES Creator & Unity Games & Tools Developer

플레이어가 죽습니다. 사망 사운드와 사망 파티클은 같은 순간에 시작해야 합니다 — 하나를 기다릴 이유가 없습니다. 하지만 화면 페이드는 리스폰 포인트 로드 전에 반드시 끝나야 합니다. 그리고 리스폰은 반드시 플레이어 텔레포트 전에 끝나야 합니다. 그리고 텔레포트는 반드시 화면 페이드 인 전에 끝나야 합니다.

하나의 이벤트로 트리거되는 같은 흐름 안에 병렬 AND 순차 실행. 불편한 진실은: Unity의 대부분의 이벤트 시스템은 정확히 하나의 패턴만 줍니다. 이벤트를 발생시키고, 모든 리스너가 응답하고, 끝. 그 응답들이 동시에 일어나야 하는지 엄격한 순서로 일어나야 하는지? 당신의 문제입니다.

그래서 해결합니다. 코루틴으로. 콜백으로. _hasFadeFinished라는 이름의 불리언으로. 그리고 어느새 여섯 개의 파일에 흩어진 임시 상태 머신을 만들어놨는데, 미래의 자신을 포함해 아무도 따라갈 수 없습니다.

이벤트 200개 돌파: 이벤트 관리가 무너지는 이유

TinyGiants
GES Creator & Unity Games & Tools Developer

새 Unity 프로젝트를 시작한다. 이벤트 10개를 만든다. OnPlayerDeath, OnScoreChanged, OnLevelComplete. 적절한 이름을 붙이고, 폴더에 넣고, 넘어간다. 인생이 좋다. 전체 이벤트 구조를 머릿속에 담을 수 있다.

6개월이 지났다. 이벤트가 200개다. Project 창은 ScriptableObject 파일의 벽이다. OnPlayerHealthDepleted가 필요한데 — 아니면 OnPlayerHPLow였나? 아니면 OnPlayerHealthZero? 전부 OnPlayer로 시작하는 이름들을 눈을 가늘게 뜨고 스크롤한다. 3분 후 포기하고 새로 하나 만든다, 원하는 이벤트가 이미 있는지조차 확신이 안 서니까.

모든 이벤트 기반 Unity 프로젝트가 결국 도달하는 곳이다. 이벤트 패턴이 잘못돼서가 아니라, 아무도 규모에서 이벤트를 관리하기 위한 도구를 만들지 않았기 때문이다. Unity는 Animation 창, Shader Graph, Timeline, Input System 디버거를 제공한다. 이벤트에는... Project 창뿐이다.

제로 리플렉션, 제로 GC: "고성능" 이벤트 시스템의 진짜 의미

TinyGiants
GES Creator & Unity Games & Tools Developer

Unity Asset Store의 모든 이벤트 시스템 플러그인이 설명 어딘가에 "고성능"을 써놓는다. "쉬운 사용"과 "완벽한 문서" 사이 어딘가에. 그런데 1ms와 0.001ms는 둘 다 사람 기준으로는 빠르지만, 하나가 다른 것보다 천 배 느리다. 플러그인이 "고성능"이라고 할 때, 실제로 무슨 뜻일까? 뭐에 비해서? 어떻게 측정했는데?

예전엔 이걸 신경 안 썼다. 대부분이 안 쓴다. 이벤트 연결하고, 게임 잘 돌아가고, 출시한다. 그런데 수백 개의 엔티티가 각각 여러 이벤트를 수신하는 모바일 프로젝트를 하게 되면서, 갑자기 "고성능"이 마케팅 체크박스가 아니라 — 60 FPS와 슬라이드쇼의 차이가 됐다.

이 글은 이벤트 시스템에서 "고성능"이 실제로 무엇을 의미해야 하는지, 왜 대부분의 구현이 부족한지, 그리고 GES가 Expression Tree 컴파일을 통해 어떻게 거의 제로 오버헤드를 달성하는지에 대한 이야기다. 실제 수치로, 허풍 없이.

Unity 제네릭 직렬화의 벽: 타입 안전한 이벤트에 보일러플레이트 세금은 불필요하다

TinyGiants
GES Creator & Unity Games & Tools Developer

GameEvent<T>를 만들었다. 깔끔하고, 타입 안전하고, 우아하다. 체력 업데이트용 GameEvent<float> 필드를 만들고 [SerializeField]를 붙였다. Inspector로 전환했다. 필드가 안 보인다. 그냥... 없다. Unity가 0으로 나누기를 요청한 것처럼 빈 패널로 쳐다보고 있다.

Unity의 가장 오래된 아키텍처 두통이다. 직렬화 시스템이 제네릭을 이해하지 못한다. 한 번도 이해한 적 없다. 타입 안전하고 데이터 기반인 이벤트 시스템을 만들려고 시도한 모든 개발자가 이 벽에 정면으로 부딪혔다.

사소한 불편이 아니다. 아키텍처 전체를 오염시키는 종류의 제한이다. 타입 안전성을 포기하거나, 보일러플레이트에 익사하거나, 아름다운 제네릭 설계가 Inspector에 절대 닿지 못한다는 걸 받아들여야 한다. 수년간 커뮤니티의 답은 "그냥 구체 클래스를 직접 작성해라"였다. 그런데 보일러플레이트가 100% 예측 가능하다면, 왜 사람이 쓰고 있는 건가?

보이지 않는 스파게티 코드에 작별을: 당신의 이벤트 시스템이 프로젝트를 망치고 있는 이유

TinyGiants
GES Creator & Unity Games & Tools Developer

메서드 이름 하나를 바꿨다. 딱 하나 — OnPlayerDiedOnPlayerDefeated로. 게임 디자이너가 표현을 부드럽게 해달라고 해서. Play를 눌렀다. 아무 일도 안 일어난다. 컴파일 에러도 없다. 경고도 없다. Inspector에서 UnityEvent로 연결되어 있던 씬 오브젝트 열 개가 그냥... 조용히 멈췄다. 3일 뒤 QA가 보고하거나, 더 최악의 경우 유저가 직접 발견하기 전까지는 알 수가 없다.

익숙한 상황이라면, 축하한다 — 보이지 않는 스파게티 코드를 만난 것이다. IDE에서 안 보이고, 컴파일러 경고도 안 뜨고, 의존성 그래프에도 나타나지 않는 종류의 기술 부채다. 그저 거기 앉아서 최악의 타이밍에 터지길 기다리고 있을 뿐이다.

이건 실력 문제가 아니다. 아키텍처 문제다. 그리고 대부분의 Unity 개발자가 인정하고 싶어하는 것보다 훨씬 흔한 일이다.