메인 콘텐츠로 건너뛰기

"Game Event System" 태그의 게시물 15개 게시물건

Game Event System related articles

모든 태그 보기

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

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

QA 테스터가 버그를 올린다: "플레이어가 열쇠를 줍는데 문이 열리지 않습니다."

간단하지? 아마 참조 누락이나 조건 오류일 것이다. 프로젝트를 열고, 열쇠를 줍고... 문이 잘 열린다. 내 컴퓨터에서는 동작한다. 테스터에게 재현 단계를 물어보니 "30% 정도의 확률로 발생하는데, 보통 세이브/로드 후에 그렇습니다"라고 한다.

이제 디버깅 지옥이다. 열쇠 획득 이벤트, 인벤토리 업데이트, 퀘스트 진행 확인, 문의 잠금 해제 조건을 잇는 체인 어딘가에서 간헐적으로 뭔가 실패하고 있다. 그런데 어느 링크에서? 이벤트가 Raise되지 않은 건가? Raise는 됐는데 리스너가 구독되지 않은 건가? 리스너는 구독됐는데 조건이 false로 평가된 건가? 조건은 맞았는데 로드 후 문의 상태가 오래된 건가?

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

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를 갈아먹는 무한 루프로 빠진다.

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

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

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라는 이름의 불리언으로. 그리고 어느새 여섯 개의 파일에 흩어진 임시 상태 머신을 만들어놨는데, 미래의 자신을 포함해 아무도 따라갈 수 없습니다.

보이지 않는 이벤트 체인: 볼 수 없는 것은 디버깅할 수 없다

TinyGiants
GES Creator & Unity Games & Tools Developer

플레이어가 죽습니다. 사망 사운드가 재생됩니다. 래그돌이 활성화됩니다. "You Died" UI 팝업이 나타납니다. 게임이 자동 저장됩니다. 분석 이벤트가 발생합니다. 리스폰 타이머가 카운트다운을 시작합니다. 하나의 이벤트 OnPlayerDeath에 여섯 개의 다른 시스템이 응답합니다. 하지만 질문 하나 — 이게 어디에 문서화되어 있나요?

코드에 없습니다. 프로젝트 관리 도구에도 없습니다. 어떤 다이어그램에도 없습니다. 딱 한 곳에 존재합니다: 원래 설정한 사람의 머릿속. 그 사람이 6개월 전에 팀을 떠났다면, 아무 데도 존재하지 않습니다.

이것이 이벤트 기반 아키텍처의 더러운 비밀입니다. 시스템을 디커플링하기 위해 채택합니다. AudioManagerUIManager에 대한 참조가 필요 없다는 걸 축하합니다. 하지만 비용에 대해서는 절대 이야기하지 않습니다: 실행 흐름이 보이지 않게 됩니다. 그리고 보이지 않는 것은, 정의상, 시각적으로 디버깅할 수 없습니다.

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

TinyGiants
GES Creator & Unity Games & Tools Developer

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

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

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

기획자가 코드 없이 이벤트를 설정한다: 디자이너와 프로그래머의 협업 문제

TinyGiants
GES Creator & Unity Games & Tools Developer

화요일 오후 3시. 디자이너가 다가와서 말합니다. "저기, 플레이어가 50 이상 데미지를 받을 때 화면 흔들림을 좀 더 강하게 할 수 있어요? 그리고 히트 사운드 전에 0.5초 딜레이 넣어주세요. 아, 독 효과 틱도 2초 대신 1.5초로 바꿔주세요."

세 가지 변경. 디자이너 입장에서는 15초면 결정할 수 있는 내용입니다. 하지만 실제로 벌어지는 일은 이렇습니다: Scene 뷰를 닫고, IDE를 열고, 로딩을 기다리고, 데미지 핸들러를 찾고, 메서드 안에 묻혀있는 화면 흔들림 강도 값을 찾아 변경합니다. 그다음 오디오 딜레이를 찾는데 — 그건 다른 클래스에 있습니다. 변경합니다. 그리고 독 코루틴을 찾는데 — 또 다른 클래스에 있고, 틱 간격은 WaitForSeconds 호출 안에 있습니다. 변경합니다. 세 파일 모두 저장하고, Unity로 돌아가서 리컴파일을 기다리고, 테스트합니다.

8분 후, 디자이너가 말합니다. "아, 흔들림은 원래가 나았어요. 그리고 독은 1.8초로 해볼 수 있을까요?"