본문 바로가기

동적 환경 상호작용의 기본 구현

입력·충돌·데미지 이벤트를 환경 변화와 피드백에 연결해 피격 후 물리로 전환하는 상자와 반응형 오브젝트를 구현합니다.

총을 쏘면 상자가 날아가고, 스위치를 밟으면 문이 열리고, 용암에 발을 들이면 체력이 깎이는 월드는 플레이어의 행동에 대답하는 월드입니다. 반대로 아무리 쏴도 그대로인 상자나 다가가도 꿈쩍하지 않는 문은 배경 그림처럼 느껴집니다. 지금까지 배운 물리 시뮬레이션, 콜리전 이벤트, 트레이스, 시간과 날씨는 각각 따로 쓰일 때보다 이렇게 하나의 반응으로 묶일 때 쓸모가 커집니다.

이 글에서는 이 요소들을 합쳐, 플레이어의 행동이나 게임 이벤트에 따라 월드가 반응하는 동적 환경 상호작용을 만듭니다. 총에 맞아 체력이 다하면 물리로 움직이기 시작하는 상자를 처음부터 끝까지 만들고, 움직이는 문, 물리 레버, 용암 같은 다른 예도 같은 틀로 살펴봅니다.


월드가 반응하는 방식

동적 환경 상호작용(dynamic environment interaction)은 게임 월드의 오브젝트가 플레이어의 행동, 물리적 충격, 게임 로직에 따라 실시간으로 바뀌고 반응하는 것을 말합니다. 단순한 물리 시뮬레이션을 넘어 게임플레이와 직접 연결되어, 플레이어에게 행동의 결과를 보여 준다는 점이 다릅니다.

예는 다양합니다. 총격이나 폭발로 벽, 상자, 유리창이 부서지는 파괴 가능한 오브젝트가 있고, 스위치를 누르거나 특정 위치에 가면 열리는 문이나 움직이는 플랫폼이 있습니다. 물체를 밀거나 던져 레버를 작동시키거나 길을 막는 물리 퍼즐, 밟으면 무너지는 바닥이나 닿으면 피해를 입는 용암 같은 환경 위험 요소, 버튼을 누르면 작동하는 기계 같은 상호작용 오브젝트도 모두 여기에 속합니다.

이런 상호작용은 대부분 같은 네 단계를 따릅니다. 먼저 상호작용을 시작하는 트리거가 있습니다. 플레이어의 입력, 다른 오브젝트와의 충돌, 시간 경과, 게임 이벤트가 그 예입니다. 트리거를 감지하면 어떤 행동을 할지 정하는 판단 로직이 실행되고, 그 결과에 따라 환경 오브젝트의 물리 상태, 겉모습, 활성 상태가 바뀝니다. 마지막으로 이펙트, 소리, 진동 같은 피드백으로 플레이어에게 변화를 알려 줍니다. 그래프를 만들 때 이 네 단계를 나누어 두면, 반응이 없을 때 어느 단계에서 끊겼는지 찾기 쉬워집니다.


피격 후 물리로 전환하는 상자

체력이 0이 되면 고정되어 있던 상자가 물리 바디로 움직이기 시작하고 효과를 재생하도록 만들어 보겠습니다. 블루프린트 이름은 BP_DestructibleBox이지만, 큐브 하나에 Simulate Physics를 켠다고 메시가 여러 조각으로 쪼개지지는 않습니다. 실제로 조각나는 파괴는 Chaos의 지오메트리 컬렉션(Geometry Collection) 같은 별도 구성이 필요하며, 이 예제는 피해를 받아 물리로 전환하는 흐름에 집중합니다.

상자 액터 만들기

Actor를 부모 클래스로 하는 BP_DestructibleBox를 만듭니다. 큐브 메시를 쓰는 Static Mesh Component를 루트로 두고 Mobility를 Movable로 설정하며, 물리에 쓸 단순 충돌이 있는지도 확인합니다. 별도의 감지 영역이 필요할 때만 Box Collision Component를 추가하는데, 이 예제에서는 메시가 Block을 맡으므로 추가한 상자는 No Collision으로 두어 충돌이 겹치지 않게 합니다.

콜리전은 Static Mesh Component를 BlockAll로 설정하고, 총알의 Object Type과도 서로 Block하는지 확인합니다. 처음에는 고정되어 있다가 파괴될 때 물리가 적용되도록 Simulate Physics는 꺼 둡니다. 이 상자는 Apply Damage로 피해를 받으며, 물리 충돌 자체의 Hit 알림도 쓰고 싶다면 그 컴포넌트의 Simulation Generates Hit Events를 따로 켭니다.

피해를 받아 물리로 전환하기

BP_DestructibleBox의 이벤트 그래프에 Event AnyDamage를 추가합니다. 이 이벤트는 권한이 있는 쪽에서 실행됩니다. 싱글플레이어에서는 로컬 월드가 권한을 가지고, 멀티플레이어에서는 서버가 피해를 적용해야 합니다. 액터의 Can Be Damaged가 켜져 있는지도 확인합니다.

변수는 두 개를 만듭니다. Float Health의 기본값을 100.0 정도로 두고, Boolean bDestroyed를 만들어 기본값을 false로 둡니다. bDestroyed는 파괴 경로를 이미 탔는지 나타내는 상태 표시일 뿐, true가 되었다고 메시가 실제로 부서지는 것은 아닙니다.

Event AnyDamage에서는 먼저 bDestroyed가 이미 참인지 보고, 참이면 바로 끝냅니다. 이 예제는 양수 피해만 다루므로 Health = Max(0, Health - Damage)로 체력을 갱신하고, Branch로 Health가 0 이하인지 확인합니다.

Branch의 True 경로에서는 가장 먼저 bDestroyed를 true로 바꿉니다. 그래야 효과와 타이머가 두 번 실행되지 않습니다. 그다음 메시의 Set Collision Enabled가 Query and Physics인지 확인하고 Set Simulate Physics를 true로 호출해 물리로 전환합니다. 원한다면 메시에 Add Impulse를 적용해 피해를 준 방향으로 상자가 튕겨 나가게 할 수 있습니다. 이때 Event AnyDamage에는 맞은 지점의 Hit 결과가 없으므로, 유효한 Damage Causer의 위치에서 상자 쪽으로 향하는 방향을 계산하고, 그 벡터가 영벡터가 아닐 때만 정규화해 충격량을 만듭니다. 정확한 접촉점과 법선이 필요하다면 Apply Point Damage와 Event PointDamage로 따로 연결해야 하며, Instigated By로 받은 컨트롤러에서 총알의 Hit 결과를 읽을 수는 없습니다.

효과는 Play Sound at Location과 나이아가라의 Spawn System at Location으로 재생하며, 쓸 이펙트 에셋은 미리 준비해 둡니다. 이름이 비슷한 Spawn Emitter at Location은 이전 파티클 시스템인 캐스케이드용 노드이므로 나이아가라 에셋에는 쓰지 않습니다. 부서진 상자를 정리하고 싶다면 Set Timer by Event로 5초 뒤 Destroy Actor를 호출해 월드에서 제거합니다.

총알이 피해를 주게 하기

이제 플레이어의 총알이 상자에 닿으면 피해를 주도록 만듭니다. 총알 블루프린트가 없다면 Actor 기반의 BP_Bullet을 만듭니다. Sphere Collision을 루트로 두고, 이 컴포넌트를 Projectile Movement의 Updated Component로 쓰며, 스윕과 상자에 대한 Block 응답을 설정합니다. 총알을 스폰할 때는 쏜 폰을 Instigator로 넘기고, 총알 자신과 쏜 사람은 충돌 대상에서 빼 둡니다.

Sphere Collision의 On Component Hit 이벤트에서 Other Actor를 Cast To BP_DestructibleBox로 캐스트합니다. 캐스트가 성공하면 Apply Damage를 호출하는데, Damaged Actor에는 캐스트한 상자를, Base Damage에는 25.0을, Damage Causer에는 총알 자신(Self)을, Event Instigator에는 총알의 Get Instigator Controller로 얻은 쏜 사람의 컨트롤러를 연결합니다. Instigated By는 Event AnyDamage 쪽의 출력 핀 이름이고 Apply Damage의 입력 핀 이름은 Event Instigator이므로, 두 이름을 헷갈리지 않습니다.

피해를 준 뒤에는 총알을 파괴합니다. 벽에 맞아 캐스트가 실패한 경로에서도 총알이 사라지도록 연결해야 총알이 남아 쌓이지 않습니다. 멀티플레이어라면 이 판정을 서버가 하고, 필요한 상자 상태와 효과는 따로 복제합니다.

체력이 줄고 물리로 바뀌는 순서

기본값 100인 상자에 서로 다른 총알이 25의 피해를 한 번씩 주면 체력은 75, 50, 25, 0으로 줄어듭니다. 네 번째 피해에서 먼저 bDestroyed를 true로 저장한 뒤 물리 전환, 효과, 제거 타이머를 시작하므로, 그 뒤에 들어오는 피해는 처음의 검사에서 끝나 같은 파괴 경로를 다시 실행하지 않습니다. 이 결과는 각 총알이 한 번씩만 피해를 주고 서버 권한이 이미 확인된 조건에서의 이야기입니다. 이 순서만으로 알림이 중복되거나 여러 클라이언트가 각자 피해를 주는 문제까지 해결되지는 않습니다.

시험하고 다듬기

레벨에 BP_DestructibleBox를 여러 개 놓고, 플레이어가 총을 쏘아 체력이 줄어드는지, 0이 되었을 때 물리 이동, 효과, 지연 제거가 한 번씩만 실행되는지 확인합니다.

동작을 확인했다면 더 다듬을 수 있습니다. 폭발이나 근접 공격처럼 피해의 종류에 따라 다르게 반응하게 하려면 Damage Type Class를 씁니다. 한 번에 부서지지 않고 금이 가거나 일부가 떨어지는 여러 단계의 파괴는, 스태틱 메시 컴포넌트를 여러 개 쓰거나 머티리얼을 바꿔 만들 수 있습니다. 총알이 닿을 때마다 불꽃 이펙트를, 파괴될 때 연기와 파편 이펙트를 더할 수도 있습니다. 파편 액터가 너무 많아지지 않게 수명을 제한하거나, 오브젝트 풀(object pool) 방식으로 재활용하는 최적화도 고려합니다.

반응이 없을 때 확인할 것

증상원인고치는 방법
총알이 상자에 맞았는데 피해가 적용되지 않음On Component Hit가 일어나지 않거나 Cast To BP_DestructibleBox가 실패함Projectile Movement의 Updated Component와 스윕, 양쪽의 Block 응답을 확인합니다. 물리 시뮬레이션 충돌이라면 Simulation Generates Hit Events도 보고, 캐스트, 서버 권한, Can Be Damaged를 점검합니다.
체력이 0인데 상자가 움직이지 않음Branch(Health <= 0)의 경로가 연결되지 않았거나 Set Simulate Physics를 호출하지 않음Event AnyDamage, 체력 갱신, Branch, True로 이어지는 흐름이 끊기지 않았는지 보고, Set Simulate Physics(true)가 실행되는지 확인합니다.
파괴된 상자가 공중으로 지나치게 튀어 오름Add Impulse의 세기가 너무 큼충격량 벡터의 크기를 절반 이하로 줄여 시작하고(예: 1200에서 600), 실제 게임의 카메라 거리에서 다시 맞춥니다.

같은 틀로 만드는 다른 상호작용

움직이는 문은 BP_MovingDoor 액터에 문 역할의 Static Mesh Component를 넣고, 문 앞에 감지 영역으로 쓸 Box Collision을 둡니다. Box Collision의 On Component Begin Overlap에서 플레이어가 들어오면 Timeline을 재생해 문 메시의 Set Relative Location을 움직이고, On Component End Overlap에서 Timeline을 거꾸로 재생해 문을 닫습니다. 네 단계로 보면 겹침이 트리거, 플레이어인지 확인하는 것이 판단, 문의 이동이 환경 변화이며, 여기에 문 여는 소리를 더하면 피드백이 됩니다.

물리 레버는 BP_Lever 액터에 레버 역할의 Static Mesh Component를 넣고 Simulate Physics를 켠 뒤, Physics Constraint Component로 레버가 한 축으로만 회전하게 제한합니다. 플레이어가 레버를 밀거나 다른 물체가 닿았을 때 On Component Hit나 On Component Begin Overlap에서 제약의 현재 Swing이나 Twist 각도, 또는 기준 자세에 대한 상대 회전을 읽고, 정해 둔 각도에 이르면 문을 열거나 다른 이벤트를 일으킵니다. 이때 Get Physics Angular Velocity가 돌려주는 각속도는 회전 각도와 다른 값이므로, 각도 기준값과 직접 비교하지 않습니다.

용암 같은 환경 위험 요소는 BP_Lava 액터에 겉모습을 위한 Static Mesh Component와 감지용 Box Collision Component를 넣고, Box Collision의 Collision Presets를 OverlapAll로 둡니다. On Component Begin Overlap에서 Other Actor를 Cast To BP_PlayerCharacter로 캐스트하고, 성공하면 겹친 액터마다 타이머 핸들을 따로 관리하거나, 겹친 액터들을 집합에 모아 타이머 하나의 콜백에서 차례로 돌며 서버에서 Apply Damage를 호출합니다. 타이머 콜백에는 입력이 없으므로, 콜백 안에서 집합의 유효한 대상을 읽습니다. 액터의 마지막 관련 컴포넌트가 영역을 벗어나면 그 액터를 집합에서 빼고, 더 이상 대상이 없으면 타이머를 해제합니다. 액터가 사라질 때를 대비해 EndPlay에서도 타이머와 참조를 정리합니다.

세 예 모두 처음부터 큰 시스템을 만들기보다 트리거 하나와 반응 하나를 먼저 연결해 확인하고, 그 위에 판단과 피드백을 더해 넓혀 가는 편이 안정적입니다.


정리

  • 동적 환경 상호작용은 트리거, 판단, 환경 변화, 피드백의 네 단계로 나누어 만들고 디버깅합니다.
  • 큐브 하나에 Simulate Physics를 켜는 것은 물리로 전환하는 것이지 조각나는 파괴가 아니며, 실제 파쇄에는 지오메트리 컬렉션 같은 구성이 필요합니다.
  • Event AnyDamage는 권한이 있는 쪽에서 실행되며, 상태 표시 bDestroyed를 가장 먼저 바꿔 파괴 경로가 한 번만 실행되게 합니다.
  • Apply Damage의 입력은 Damaged Actor, Base Damage, Event Instigator, Damage Causer, Damage Type Class이고, Instigated By는 받는 쪽 이벤트의 출력입니다.
  • 문, 레버, 용암도 같은 틀로 만들며, 여러 대상에 반복 피해를 주는 타이머는 대상별로 관리하고 EndPlay에서 정리합니다.

연습 문제

1. 체력 60인 상자에 들어온 네 번의 피해

새 상자의 초기 체력을 60으로 바꾸고, 25의 피해가 네 번 차례로 승인됐습니다. 매번 bDestroyed 검사가 먼저 실행되고, 전환 경로에 지연은 없습니다. 호출마다 체력과 bDestroyed의 값, 그리고 물리 전환, 효과, 5초 제거 타이머가 각각 몇 번 시작되는지 예측해 보세요. 타이머가 끝나기 전의 네 호출만 판단합니다.

해설 보기

첫 호출 뒤에는 60 - 25 = 35, false이고, 두 번째 호출 뒤에는 10, false입니다. 세 번째 호출에서는 Max(0, 10 - 25)로 체력이 0이 되고, bDestroyed를 true로 바꾼 뒤 물리 전환, 효과, 타이머를 한 번씩 시작합니다. 네 번째 호출은 처음의 bDestroyed 검사에서 끝나므로 체력은 0 그대로이고, 아무것도 추가로 시작하지 않습니다.

따라서 세 가지 후속 동작은 각각 한 번씩만 시작됩니다. 만약 bDestroyed를 바꾸는 노드를 Delay 뒤로 옮긴다면, 그 사이에 다른 호출이 들어와 파괴 경로를 다시 탈 수 있으므로 그 경우를 따로 설계해야 합니다.

2. 클라이언트에서 호출한 Apply Damage

멀티플레이어에서 총알의 On Component Hit가 클라이언트에서만 실행되도록 만들어 두었습니다. 그 클라이언트의 화면에서는 총알이 상자에 맞는 것이 보이는데, 상자의 체력이 줄지 않습니다. 왜 그럴까요?

해설 보기

Event AnyDamage는 권한이 있는 쪽에서 실행되므로, 클라이언트에서 호출한 Apply Damage로는 서버의 상자에 피해가 들어가지 않습니다. 클라이언트 화면에 맞는 장면이 보인 것은 표현일 뿐, 서버의 상태는 바뀌지 않았습니다.

총알의 충돌 판정을 서버에서 실행하도록 바꾸고, 서버에서 Apply Damage를 호출합니다. 상자의 체력이나 파괴 상태처럼 모든 플레이어가 봐야 하는 값은 복제 변수로 두고, 파괴 효과는 서버가 확정한 뒤 클라이언트에 전달합니다.

3. 반쯤 열린 문에서 돌아서기

BP_MovingDoor의 문은 Timeline으로 2초 동안 열립니다. 플레이어가 영역에 들어갔다가 1초 만에 나가서 On Component End Overlap이 실행됐습니다. Timeline의 Reverse 입력에 연결했을 때와 Reverse from End 입력에 연결했을 때, 문은 각각 어떻게 움직일까요?

해설 보기

Reverse는 지금 재생 위치인 1초 지점에서 거꾸로 재생하므로, 반쯤 열린 문이 그 자리에서 부드럽게 닫히기 시작해 1초 뒤에 완전히 닫힙니다.

Reverse from End는 재생 위치를 끝인 2초로 옮긴 뒤 거꾸로 재생하므로, 문이 순간적으로 완전히 열린 위치로 튀었다가 2초에 걸쳐 닫힙니다. 문처럼 도중에 방향이 바뀔 수 있는 움직임에는 Reverse를, 열 때도 마찬가지로 처음부터 다시 재생하는 Play from Start보다 현재 위치에서 이어 가는 Play를 쓰는 편이 자연스럽습니다.

Apply Damage의 입력 이름과 설명은 언리얼 엔진 공식 문서의 Apply Damage 노드에서 확인할 수 있습니다.

다음 글 블루프린트 성능 최적화 기법에서는 지금까지 만든 그래프가 느려질 때 어디서 비용이 생기는지 측정하고, Tick과 탐색, 생성 비용을 줄이는 방법을 살펴봅니다.