메인 콘텐츠로 건너뛰기

"Scripting API" 태그의 게시물 4개 게시물건

C# runtime API

모든 태그 보기

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

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