본문 바로가기

이벤트 시스템의 기초 이해

BeginPlay와 Tick, 입력·충돌 이벤트의 발생 조건을 구분하고 이벤트 중심으로 실행 흐름을 구성합니다.

1장에서 만든 BP_WelcomeMessage는 Event BeginPlay에서 출발해 메시지를 출력했습니다. 그때 BeginPlay를 직접 호출한 노드는 어디에도 없었습니다. 액터가 플레이를 시작하자 엔진이 알아서 BeginPlay를 실행했고, 그래프는 그 신호에 반응했을 뿐입니다. 이렇게 무언가 일어났을 때 그래프의 실행을 시작하는 노드가 이벤트(event)입니다.

앞 글의 함수와 비교하면 차이가 분명합니다. 함수는 내가 호출 노드를 놓아 실행하는 작업 단위이고, 이벤트는 엔진의 알림이나 다른 그래프의 호출로 실행이 시작되는 진입점입니다. 친구에게 내가 전화를 거는 것이 함수 호출이라면, 친구에게서 전화가 걸려 오는 것이 이벤트에 해당합니다. 다만 이 비유는 엔진이 보내는 이벤트에 맞는 것이고, 직접 만든 커스텀 이벤트(Custom Event)는 함수처럼 내가 호출할 수도 있습니다. 이 글에서는 자주 쓰는 이벤트의 종류와 각각이 언제 실행되는지 살펴보고, 플레이어가 상자 영역에 들어오면 메시지를 출력하는 그래프를 만들어 봅니다.


실행을 시작하는 진입점

게임에서는 실시간으로 수많은 사건이 일어납니다. 액터가 생성되고, 플레이어가 키를 누르고, 두 물체가 부딪히고, 시간이 흐릅니다. 이벤트는 이런 사건을 받아서 미리 정해 둔 로직을 실행하게 하는 장치입니다. 블루프린트에서 이벤트 노드는 주로 이벤트 그래프(Event Graph)에 놓이며, 다른 노드와 달리 스스로 실행 흐름을 시작합니다.

그래서 이벤트 노드에는 왼쪽의 실행 입력 핀이 없고 오른쪽의 실행 출력 핀만 있습니다. 실행이 들어오는 곳이 아니라 실행이 시작되는 곳이기 때문입니다. 이벤트가 사건에 대한 정보를 함께 넘겨야 할 때는 데이터 출력 핀이 붙습니다. 예를 들어 겹침 이벤트는 겹친 상대 액터를, Tick 이벤트는 지난 프레임 이후 흐른 시간을 출력 핀으로 내보냅니다.

엔진이 보내는 이벤트는 한 블루프린트에 하나씩만 둘 수 있습니다. BeginPlay에서 할 일이 여러 가지라면 BeginPlay 노드를 여러 개 놓는 것이 아니라, 하나의 BeginPlay 뒤에 실행 선으로 차례대로 잇습니다.


자주 쓰는 이벤트

언리얼 엔진에서 자주 쓰는 이벤트를 언제 실행되는지에 따라 묶어 보면 다음과 같습니다.

생명 주기 이벤트는 액터가 월드에 들어오고 나가는 순간에 실행됩니다. Event BeginPlay는 액터 인스턴스가 플레이를 시작할 때 실행되며, 레벨에 미리 배치한 액터는 게임이 시작될 때, 게임 도중 생성(spawn)한 액터는 생성된 직후에 받습니다. 초기 설정이나 준비 작업을 여기서 합니다. 반대로 Event End Play는 액터가 월드를 떠날 때 실행되고, 왜 떠나는지를 End Play Reason 핀으로 알려 줍니다. 두 이벤트는 액터 초기화와 종료 이벤트에서 자세히 비교합니다.

충돌 이벤트는 물체끼리 닿거나 겹칠 때 실행됩니다. Event ActorBeginOverlap은 이 액터가 다른 액터와 겹치기 시작할 때, Event ActorEndOverlap은 겹침이 끝나거나 한쪽이 파괴될 때 실행됩니다. 플레이어가 아이템 위를 지나갈 때 아이템을 줍게 하는 로직이 대표적인 예입니다. 공식 문서에 따르면 두 액터 모두 겹침을 허용하는 콜리전 설정이어야 하고 Generate Overlap Events가 켜져 있어야 합니다. 물체가 겹치지 않고 부딪히는 경우에는 Event Hit을 씁니다. 콜리전 설정은 콜리전 시스템 기본 개념에서 다룹니다.

Event Tick은 Tick이 켜져 있는 동안 정해진 간격마다 실행됩니다. 기본 간격에서는 프레임마다 실행되지만, 프레임률은 기기와 장면에 따라 달라지므로 1초에 몇 번 실행된다고 가정할 수 없습니다. 그래서 Tick은 지난 프레임 이후 흐른 시간을 Delta Seconds 핀으로 함께 넘겨 주며, 이동이나 타이머처럼 시간에 따라 변하는 계산에는 이 값을 곱해 프레임률과 상관없이 같은 속도가 나오게 합니다. Tick은 계속 변해야 하는 로직에 쓰지만, 많은 액터가 Tick마다 무거운 일을 하면 성능에 부담이 되므로 특정 사건이 있을 때만 실행해도 되는 일은 다른 이벤트로 옮깁니다.

입력 이벤트는 플레이어의 조작에 반응합니다. 언리얼 엔진 5.8에서는 향상된 입력(Enhanced Input) 시스템이 기본입니다. 이 방식에서는 "점프"처럼 플레이어가 하려는 동작을 입력 액션(Input Action) 에셋으로 만들고, 어떤 키가 그 동작을 일으키는지를 입력 매핑 컨텍스트(Input Mapping Context)에 정한 뒤, 그래프에서는 입력 액션 이벤트 노드에 반응합니다. 이 노드에는 입력이 시작될 때의 Started, 조건이 만족되는 동안의 Triggered, 끝날 때의 Completed 같은 실행 핀이 있습니다. 예전 입력 시스템의 액션 매핑과 축 매핑, 그리고 E나 Left Mouse Button처럼 키 이름이 붙은 키 이벤트 노드도 남아 있지만, 새로 만드는 프로젝트에서는 향상된 입력을 쓰는 편이 좋습니다. 입력 설정은 플레이어 컨트롤러, 폰 블루프린트에서 이어서 다룹니다.

커스텀 이벤트는 개발자가 직접 이름을 붙여 만드는 이벤트입니다. 그래프를 오른쪽 클릭해 Add Custom Event를 고르고 이름을 정하면 되며, 함수처럼 입력 매개변수를 둘 수 있습니다. 같은 블루프린트의 다른 곳에서 그 이름의 호출 노드로 실행하거나, 참조가 있다면 다른 블루프린트에서도 호출할 수 있습니다. 함수와 달리 결과를 돌려주는 출력은 없지만, 함수 안에 넣을 수 없는 Delay 같은 잠재 노드를 커스텀 이벤트 뒤에는 이을 수 있습니다.

더 깊이: 이벤트 그래프가 컴파일되는 방식과 Delay를 다시 부를 때

블루프린트 컴파일러 개요에 따르면, 컴파일러는 한 블루프린트의 이벤트 그래프를 모두 복사해 하나의 큰 그래프로 합치고, 그 안의 이벤트 노드마다 이 그래프로 들어가는 작은 함수를 따로 만듭니다. 반면 함수 그래프는 하나씩 따로 복제되어 각자의 함수로 컴파일됩니다. 그래서 BeginPlay와 겹침 이벤트, 직접 만든 커스텀 이벤트는 엔진 안에서 같은 큰 그래프로 들어가는 서로 다른 입구에 해당합니다.

이벤트 뒤에 Delay를 이었다면, 기다리는 동안 같은 이벤트가 다시 실행될 때의 동작도 알아 둬야 합니다. API 레퍼런스에 따르면 Delay는 카운트다운하는 동안 다시 호출되면 그 호출을 무시합니다. 예를 들어 문을 열고 3초 뒤에 닫는 커스텀 이벤트를 3초 안에 두 번 호출하면, 두 번째 호출에서도 Delay 앞의 노드는 다시 실행되지만 새로 기다리지는 않으므로 문은 첫 호출에서 정해진 시각에 한 번만 닫힙니다. 마지막 호출을 기준으로 다시 3초를 기다리게 하려면, 카운트다운 중에 다시 호출되면 남은 시간을 Duration으로 되돌리는 Retriggerable Delay를 씁니다.


이벤트를 그래프에 놓기

이벤트 노드를 추가하는 방법은 다른 노드와 같습니다. 이벤트 그래프의 빈 곳을 오른쪽 클릭하고 검색창에 이벤트 이름을 입력해 고릅니다. 예를 들어 ActorBeginOverlap을 입력하면 Event ActorBeginOverlap이 나타납니다. 그다음 이벤트 노드 오른쪽의 실행 핀을 끌어, 이벤트가 일어났을 때 처음 실행할 노드에 잇습니다. 1장에서 Event BeginPlay 뒤에 Print String을 이었던 것과 같은 방식입니다.

컴포넌트에서 일어나는 이벤트는 컴포넌트의 디테일 패널에서 만드는 편이 빠릅니다. 컴포넌트를 선택하면 디테일 패널 아래쪽 Events 항목에 그 컴포넌트가 보낼 수 있는 이벤트가 나열되고, 이름 옆의 + 버튼을 누르면 해당 컴포넌트에 연결된 이벤트 노드가 이벤트 그래프에 만들어집니다.


상자에 들어오면 메시지 출력하기

이제 플레이어가 보이지 않는 상자 영역에 들어오면 메시지를 출력해 봅니다. 이 실습은 액터 전체의 ActorBeginOverlap 대신, 액터에 추가한 Box Collision 컴포넌트의 On Component Begin Overlap 이벤트를 씁니다. 액터의 여러 컴포넌트 가운데 이 상자에 들어온 경우만 반응하게 하려는 것입니다.

이 실습에는 이미 움직일 수 있는 플레이어 Character가 있는 레벨이 필요합니다. 1장의 빈 Actor 실습만으로는 플레이어가 만들어지지 않으므로, 템플릿 프로젝트처럼 플레이를 시작하면 Character가 생성되고 조종되는 레벨에서 진행합니다. 플레이어가 아직 없다면 게임 모드, 스테이트 블루프린트와 플레이어 컨트롤러, 폰 블루프린트를 먼저 읽고 돌아옵니다.

먼저 BP_WelcomeMessage의 블루프린트 에디터를 열고, 컴포넌트 패널의 Add 버튼으로 Box Collision 컴포넌트를 추가합니다. Box Collision은 화면에는 보이지 않지만 겹침을 감지하는 상자 영역입니다. 뷰포트에서 상자의 위치와 크기를 캐릭터가 지나갈 수 있는 정도로 맞춥니다.

다음으로 겹침이 실제로 일어날 수 있는지 콜리전 설정을 확인합니다. Box Collision을 선택하고 디테일 패널에서 Generate Overlap Events가 켜져 있는지, 콜리전 프리셋이 겹침을 허용하는지 봅니다. 플레이어 Character의 콜리전 컴포넌트에서도 Generate Overlap Events가 켜져 있어야 합니다. 또 두 컴포넌트 가운데 어느 쪽도 상대의 오브젝트 채널을 무시(Ignore)하지 않고 적어도 한쪽이 겹침(Overlap)으로 응답해야 하며, 한쪽이 Overlap이고 다른 쪽이 Block이어도 서로 막지 않고 겹침이 일어납니다.

설정이 맞으면 Box Collision의 디테일 패널 Events 항목에서 On Component Begin Overlap 옆의 + 버튼을 누릅니다. 이벤트 그래프에 On Component Begin Overlap (Box) 노드가 만들어지는데, 이 노드는 이 상자가 다른 무언가와 겹치기 시작할 때 실행됩니다. 노드의 실행 핀에서 끌어 Print String을 만들고 In String에 누군가 겹쳤습니다!를 입력한 뒤 컴파일하고 저장합니다.

메인 에디터로 돌아와 BP_WelcomeMessage를 레벨에 배치하고 레벨을 저장한 뒤 Play를 누릅니다. 플레이어 캐릭터를 움직여 액터 근처의 보이지 않는 상자 안으로 들어가면, 화면에 누군가 겹쳤습니다!가 출력됩니다.


이벤트를 고를 때 따져 볼 것

이벤트를 고를 때는 두 가지를 먼저 봅니다. 하나는 그 이벤트가 언제 일어나는가이고, 다른 하나는 누가 실행을 시작시키는가입니다. 게임이 시작될 때 한 번 하면 되는 일은 BeginPlay에, 플레이어가 영역에 들어올 때 할 일은 겹침 이벤트에, 매 프레임 조금씩 변해야 하는 일만 Tick에 둡니다. 엔진이 실행시키는 이벤트인지, 다른 블루프린트가 호출하는 커스텀 이벤트인지에 따라 확인할 곳도 달라집니다.

겹침 이벤트를 쓸 때는 실행된 횟수와 상대를 함께 생각해야 합니다. 겹침 이벤트의 Other Actor는 상자에 들어온 액터이지 항상 플레이어인 것은 아닙니다. 굴러 온 물체나 적이 들어와도 같은 이벤트가 실행되므로, 플레이어만 처리하려면 상대가 플레이어인지 확인하는 노드를 덧붙입니다. 또 상대 액터에 겹침을 일으키는 컴포넌트가 여럿이면 한 번 들어올 때 이벤트가 여러 번 실행될 수 있고, 영역을 나갔다가 다시 들어오면 그때마다 다시 실행됩니다.

입력 이벤트는 받는 쪽의 설정도 필요합니다. 향상된 입력을 쓰려면 입력 액션과 입력 매핑 컨텍스트를 만들고 매핑 컨텍스트를 플레이어에 등록해야 합니다. 플레이어가 조종하는 폰이 아닌 일반 Actor가 입력을 받으려면, 그 액터가 입력을 받도록 켜 주는 설정이 따로 있어야 합니다.


정리

  • 이벤트는 사건이 일어났을 때 그래프의 실행을 시작하는 진입점이며, 실행 입력 핀 없이 실행 출력 핀만 있습니다.
  • 엔진이 보내는 이벤트는 한 블루프린트에 하나씩만 두고, 할 일이 여럿이면 그 뒤에 실행 선으로 잇습니다.
  • BeginPlay와 End Play는 액터가 월드에 들어오고 나갈 때, 겹침 이벤트와 Hit은 충돌할 때, Tick은 정해진 간격마다 실행됩니다. Tick에서 시간에 따른 계산에는 Delta Seconds를 씁니다.
  • 언리얼 엔진 5.8의 입력은 입력 액션과 입력 매핑 컨텍스트로 구성하는 향상된 입력이 기본입니다.
  • 커스텀 이벤트는 직접 만들어 호출하는 이벤트로, 입력은 있지만 반환 출력은 없고 Delay 같은 잠재 노드를 이을 수 있습니다.
  • 겹침 이벤트의 상대는 플레이어가 아닐 수 있고, 한 번 들어올 때 여러 번 실행될 수도 있습니다.

연습 문제

1. Tick에 둘까, 이벤트에 둘까

다음 세 기능을 각각 어떤 이벤트에서 시작할지 고르고 이유를 적어 보세요. (가) 플레이어가 문 앞 영역에 들어오면 문을 연다. (나) 회전하는 동전이 초당 90도씩 계속 돈다. (다) 레벨이 시작될 때 남은 시간을 60초로 맞춘다.

해설 보기

(가)는 플레이어가 영역에 들어오는 사건이 출발점이므로 문 앞에 둔 Box Collision의 겹침 이벤트가 알맞습니다. Tick에서 매 프레임 플레이어와의 거리를 재는 것보다 필요할 때만 실행됩니다.

(나)는 시간이 흐르는 동안 계속 변해야 하므로 Tick에서 처리할 수 있습니다. 이때 회전량은 90에 Delta Seconds를 곱해 구해야 프레임률과 상관없이 초당 90도가 됩니다. 단순한 회전이라면 Rotating Movement 컴포넌트를 써서 그래프 없이 처리할 수도 있습니다. (다)는 시작할 때 한 번만 하면 되므로 BeginPlay에서 합니다.

2. 메시지가 두 번 나온다

Box Collision의 On Component Begin Overlap에 연결한 메시지가, 플레이어가 상자에 한 번 들어갔을 뿐인데 두 번 출력되었습니다. 어떤 원인을 생각해 볼 수 있을까요?

해설 보기

플레이어 액터에 겹침을 일으키는 컴포넌트가 둘 이상일 수 있습니다. 예를 들어 캡슐 컴포넌트와 메시 컴포넌트 모두 Generate Overlap Events가 켜져 있으면, 컴포넌트마다 겹침이 시작되므로 이벤트가 두 번 실행될 수 있습니다.

Print String에 Other Comp 핀의 이름을 함께 출력해 보면 어느 컴포넌트가 들어왔는지 확인할 수 있습니다. 또 영역 가장자리에서 들락날락했다면 들어올 때마다 다시 실행되므로, 한 번만 처리해야 하는 기능이라면 처리했는지를 기억하는 Boolean 변수를 두는 방법도 있습니다.

3. 함수 대신 커스텀 이벤트를 쓰는 경우

문을 열고 3초 뒤에 자동으로 닫는 OpenThenClose 기능을 만들려고 합니다. 이것을 함수로 만들 수 있을까요? 커스텀 이벤트로 만들면 무엇이 달라질까요?

해설 보기

3초를 기다리려면 Delay 같은 잠재 노드가 필요한데, 함수는 호출된 자리에서 끝까지 실행되어야 하므로 함수 안에는 Delay를 넣을 수 없습니다. 그래서 이 기능은 함수로 만들기 어렵습니다.

커스텀 이벤트로 만들면 이벤트 뒤에 문을 여는 노드, Delay(3초), 문을 닫는 노드를 차례로 이을 수 있습니다. 대신 커스텀 이벤트는 결과를 돌려주는 출력이 없으므로, 호출한 쪽이 결과값을 받아야 하는 계산이라면 함수를 씁니다.

이벤트의 종류와 실행 조건은 언리얼 엔진 공식 문서의 이벤트 설명에서 확인할 수 있습니다.

다음 글 흐름 제어 노드 (분기, 루프) 사용법에서는 이벤트에서 시작된 실행을 조건에 따라 나누고, 같은 작업을 여러 번 되풀이하는 노드를 다룹니다.