메인 콘텐츠로 건너뛰기

"Advanced" 태그의 게시물 6개 게시물건

Advanced techniques and patterns

모든 태그 보기

런타임 이벤트 플로우 구축: 비주얼 에디터로는 부족할 때

TinyGiants
GES Creator & Unity Games & Tools Developer

절차적 던전 생성기가 방금 압력판 세 개와 가시 함정이 있는 방을 만들었다. 다음 방에는 잠긴 문에 연결된 레버 퍼즐이 있다. 그다음 방은 보스의 체력 페이즈에 따라 환경 위험이 활성화되는 보스 아레나다. 이런 이벤트 관계들은 에디트 타임에 존재하지 않았다. 던전 레이아웃은 플레이어가 30초 전에 입력한 시드에 의해 결정됐다.

이벤트를 어떻게 연결할 것인가?

전통적인 방식으로는 거대한 switch 문을 작성한다. 각 방 타입마다 이벤트 핸들러를 수동으로 구독하고 해제한다. 각 AI 난이도마다 다른 공격 패턴을 수동으로 체인한다. 각 모드 제작 콘텐츠마다 설정 파일을 수동으로 파싱해서 이벤트 연결로 번역한다. "수동"이 문제다 — 런타임에 토폴로지가 변할 때마다 이벤트 와이어링 로직을 재구현하고 있는 것이다.

비주얼 노드 에디터는 디자인 타임에 알려진 플로우에는 환상적이다. 하지만 게임이 실행될 때까지 존재하지 않는 플로우는 근본적으로 처리할 수 없다. 그리고 점점 더, 가장 흥미로운 게임 시스템들이 바로 이벤트 그래프가 동적인 시스템들이다.

실행 순서 버그: "누가 먼저 응답하느냐"에 숨겨진 위험

TinyGiants
GES Creator & Unity Games & Tools Developer

플레이어가 25 데미지를 받는다. 체력 시스템이 현재 HP에서 이를 차감한다. UI가 체력바를 갱신한다. 그런데 체력바에 75가 아니라 100이 표시된다. 20분 동안 코드를 들여다본 끝에 원인을 알게 된다: UI 리스너가 체력 시스템 리스너보다 먼저 실행된 것이다. UI는 이전 HP 값을 읽어서 렌더링했고, 그 후에야 체력 시스템이 값을 업데이트했다. 데이터가 올바르게 갱신됐을 때는 이미 프레임이 그려진 후였다.

이것이 바로 실행 순서 버그다. 이벤트 기반 아키텍처를 사용하는 게임을 출시해 본 적이 있다면, 자기도 모르게 이런 버그를 몇 개씩 함께 출시했을 가능성이 높다. 테스트할 때는 스크립트가 우연히 올바른 순서로 초기화되어 잘 동작하다가, 프로덕션에서 Unity가 다른 순서로 로딩하면서 깨지는 그런 종류의 버그다.

이건 드문 엣지 케이스가 아니다. 대부분의 이벤트 시스템이 가진 구조적 결함이다 — Unity의 UnityEvent와 표준 C# event 델리게이트를 포함해서. 왜 이런 일이 생기는지 이해하고 나면, 다시는 모른 척할 수 없게 된다.

시간 기반 이벤트: 코루틴이 지연과 반복에 적합하지 않은 이유

TinyGiants
GES Creator & Unity Games & Tools Developer

수류탄이 착지한 후 2초 뒤에 폭발을 지연시켜야 합니다. 충분히 간단합니다. 코루틴을 작성합니다. IEnumerator DelayedExplosion(), yield return new WaitForSeconds(2f), 폭발 로직 호출. 깔끔하게 정리하면 10줄 정도. 기분이 좋습니다.

그런데 디자이너가 "플레이어가 폭탄을 해제할 수 있어야 해요"라고 합니다. 좋아요, 이제 StopCoroutine()을 호출할 수 있도록 Coroutine 레퍼런스를 저장해야 합니다. 잠깐 — 코루틴이 시작되기 전에 해제하면? null 체크가 필요합니다. 대기 중에 게임 오브젝트가 파괴되면? 또 다른 null 체크. 코루틴이 완료되는 정확한 프레임에 해제하면? 경쟁 조건. 10줄이 25줄이 됐고, "해제 메시지 vs. 폭발 표시" 분기 처리는 시작도 하지 않았습니다.

이것이 Unity의 모든 시간 기반 이벤트의 이야기입니다. 첫 번째 구현은 깔끔합니다. 두 번째 요구사항이 코드를 두 배로 늘립니다. 세 번째는 직업 선택을 의심하게 만듭니다.

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

TinyGiants
GES Creator & Unity Games & Tools Developer

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

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

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

if-else 지옥 탈출: 비주얼 조건 로직의 올바른 접근법

TinyGiants
GES Creator & Unity Games & Tools Developer

모든 게임은 기본적으로 거대한 조건의 더미입니다. "적이 면역이 아니고 AND 플레이어에게 화염 버프가 있고 AND 랜덤 크리티컬 체크를 통과할 때만 화염 데미지를 준다." 프로토타이핑할 때는 콜백에 if문 하나 넣고 넘어갑니다. 30초. 동작함. 생산적인 느낌.

그러다 프로토타입이 프로덕션에 들어갑니다. 30초짜리 if문들이 번식하기 시작합니다. 하나가 다섯이 되고, 다섯이 오십이 되고, 오십이 "두 번째 보스의 루트 드롭률을 제어하는 조건이 대체 어디있지?"가 됩니다. 그리고 디자이너가 뒤에 서서 데미지 임계값을 0.3에서 0.25로 바꿀 수 있냐고 물어보고 있고, 당신은 리컴파일이 필요하다고 설명하고 있습니다.

if-else 지옥에 오신 것을 환영합니다. 인구: 3개월 넘게 유지된 모든 Unity 프로젝트.

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

TinyGiants
GES Creator & Unity Games & Tools Developer

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

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

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