동적 오브젝트 및 상호작용
타임라인·애니메이션·물리 제어로 문·스위치·플랫폼이 입력에 반응하게 만들고 동적 상태를 안정적으로 관리합니다.
UI로 상호작용의 신호를 전달했다면, 이제 그 결과를 월드 안에서 직접 느끼게 해야 합니다. 문이 열리고, 스위치가 눌려 들어가고, 발판이 올라가는 변화는 연출을 넘어, 플레이어에게 방금 한 행동이 실제로 반영되었다는 확신을 줍니다. 반대로 키를 눌렀는데 세계가 아무 반응도 없다면, 플레이어는 입력이 들어갔는지조차 알 수 없습니다.
이 글에서는 오브젝트를 동적으로 바꾸는 네 가지 기술인 타임라인, 재질 파라미터, 물리 제어, 생성과 가시성 제어를 정리하고, 플레이어가 작동시키는 움직이는 발판을 만들어 봅니다. 발판의 움직임은 매 프레임 남은 거리의 일정 비율을 줄이는 방식이라, 일정한 속도의 이동과 어떻게 다른지 계산으로 확인하고, 자주 생기는 문제와 고치는 방법도 함께 정리합니다.
오브젝트가 반응해야 하는 이유
플레이어의 행동에 환경이 바로 반응하면, 플레이어는 자기 행동에 대한 보상을 느끼고 만족합니다. 문이 열리거나 발판이 움직이는 것은 시각 효과에서 끝나지 않고, 플레이어가 다음 지역으로 가거나 새 퍼즐을 풀게 하는 게임 플레이의 흐름이 됩니다. 정적인 배경이 아니라 행동에 따라 상태가 바뀌는 환경으로 보이게 하고, 어떤 오브젝트의 변화가 이야기의 단서가 되거나 세계의 비밀을 드러내기도 합니다.
오브젝트를 바꾸는 네 가지 기술
타임라인
타임라인(Timeline)은 시간에 따른 값의 변화를 그래프로 정해 두고, 오브젝트의 위치, 회전, 크기나 재질 파라미터를 부드럽게 보간하는 데 쓰는 노드입니다. 복잡한 애니메이션 코드 없이 움직임을 직관적으로 만들 수 있습니다.
타임라인에는 여러 종류의 트랙이 있습니다. Float Track은 0.0에서 1.0처럼 하나의 실수 값이 시간에 따라 바뀌는 것을, Vector Track은 위치나 크기 같은 3D 벡터 값이 바뀌는 것을 정합니다. Event Track은 정해 둔 시간에 이벤트를 일으켜, 소리를 재생하거나 파티클을 만드는 데 씁니다.
재생은 입력 핀으로 다룹니다. Play는 지금 재생 위치에서 정방향으로 진행하며, 처음부터 다시 재생하려면 Play from Start를 씁니다. Reverse는 지금 위치에서 거꾸로 재생하고, Stop은 재생을 멈춥니다. 출력 핀인 Update는 재생하는 동안 매 프레임 지금의 값을 내보내 오브젝트의 속성에 적용하게 하고, Finished는 재생이 끝났을 때 실행됩니다. 문 열기와 닫기, 발판 이동, 조명 밝기의 변화, 깃발의 흔들림 같은 곳에 씁니다.
재질 파라미터
오브젝트에 적용된 재질의 값을 블루프린트에서 실행 중에 바꿔 시각적인 변화를 줄 수 있습니다. Set Scalar Parameter Value on Materials는 하나의 실수 값 파라미터를, Set Vector Parameter Value on Materials는 색이나 위치 같은 벡터 값 파라미터를 바꾸고, Set Material은 재질 자체를 다른 재질로 바꿉니다.
상호작용할 때 오브젝트의 색을 바꿔 강조하거나 켜짐과 꺼짐을 보여 주고, 스위치를 누르면 발광(Emissive) 파라미터를 올려 불이 켜지게 할 수 있습니다. 벽이 부서질 때 금이 가는 텍스처로 바꾸거나, 물에 젖었을 때 거칠기를 낮추는 것도 같은 방법이며, 날씨와의 연동은 동적 환경 변화와 시뮬레이션에서 다뤘습니다.
물리 제어
오브젝트에 힘을 가하거나 물리 시뮬레이션 상태를 바꿔 동적인 움직임을 만들 수도 있습니다. Add Impulse는 강체의 운동량을 순간적으로 바꾸는 충격을 주어, 총알에 맞거나 폭발로 날아가는 것을 표현합니다. Add Force는 힘이 작용하는 동안 매 프레임 호출해, 바람에 밀리는 물체처럼 지속되는 힘을 표현합니다. Set Simulate Physics는 물리 시뮬레이션을 켜고 끄고, Set Physics Linear Velocity와 Set Physics Angular Velocity in Degrees(라디안으로 정하려면 Set Physics Angular Velocity in Radians)는 직선 속도와 회전 속도를 직접 정합니다.
스위치를 누르면 어떤 물체가 밀려나거나 떨어지는 퍼즐, 문이 부서질 때 파편이 흩어지는 효과, 특정 조건에서 캐릭터를 래그돌로 바꾸는 효과에 씁니다.
생성, 제거, 가시성
조건에 따라 오브젝트를 만들거나 없애고, 보이거나 안 보이게 해서 환경을 바꿀 수도 있습니다. Spawn Actor From Class는 새 액터를 레벨에 만들어 부서질 때의 파편이나 정해진 시간의 몬스터를 생성하고, Destroy Actor는 주운 아이템이나 부서진 물체를 레벨에서 없앱니다.
Set Visibility는 씬 컴포넌트 하나의 보이기를 바꾸고, 액터 전체를 숨기려면 Set Actor Hidden In Game을 씁니다. 다만 숨기는 것과 충돌, Tick, 물리는 별개라서, 숨긴 물체도 여전히 부딪히거나 계산될 수 있습니다. 퍼즐을 풀면 숨겨진 길이 나타나게 하거나, 트리거에 들어가면 함정이 나타나게 하거나, 아이템을 주우면 그 메시를 없애는 데 씁니다.
움직이는 발판 만들기
이제 플레이어가 상호작용 키로 작동시키는 발판을 만들어 봅니다. 누르면 위로 올라가고, 다시 누르면 내려오는 발판입니다.
입력을 발판까지 전달하기
환경과 상호작용하는 오브젝트 설정의 문처럼, 입력은 준비된 캐릭터나 컨트롤러 한 곳에서만 받습니다. 플레이어에 CandidatePlatform 변수(BP_MovingPlatform의 Object Reference)를 만들고, 발판의 BeginOverlap에서 실제 플레이어와 캡슐인지 확인한 뒤 발판 자신(Self)을 저장합니다. EndOverlap에서는 지금의 후보가 이 발판일 때만 비웁니다.
발판에는 RequestMove라는 커스텀 이벤트를 만들고, 입력으로 RequestingPlayer(Character의 Object Reference)를 받게 합니다. 플레이어가 Interact를 받으면 유효한 CandidatePlatform을 Target으로 RequestMove를 호출하고, 플레이어 자신을 넘깁니다. RequestMove는 요청자가 유효한지, 요청자의 캡슐이 자기 InteractionTrigger에 겹쳐 있는지를 다시 확인한 뒤 아래의 이동 분기로 넘어갑니다. 플레이어의 그래프에서 다른 액터의 컴포넌트를 참조 없이 바로 쓸 수는 없으므로, 이렇게 범위 확인은 발판이 직접 합니다. 또 발판에 입력 노드를 두는 것만으로 이 호출 경로가 생기지는 않습니다.
컴포넌트와 변수
BP_MovingPlatform이라는 액터 블루프린트를 만들고 두 컴포넌트를 추가합니다. PlatformMesh는 발판의 스태틱 메시로 루트 컴포넌트로 두고, Movable로 정하고 Simulate Physics는 끄며, 필요한 대상에 Block하는 충돌을 줍니다. 뒤에서 쓸 Set Actor Location의 Sweep은 루트 컴포넌트만 검사하므로, 막혀야 할 충돌은 루트에 있어야 합니다. InteractionTrigger는 발판을 작동시킬 Box Collision으로, Custom에서 Pawn에만 Overlap, 나머지에는 Ignore로 응답하게 하고, 플레이어 캡슐과 양쪽 모두 Generate Overlap Events를 켭니다.
변수는 다섯 개를 만듭니다. StartLocation(Vector)은 BeginPlay에서 저장할 시작 월드 위치이고, EndLocation(Vector)은 시작 위치에 이동할 거리를 더한 목표 위치입니다. MoveSpeed(Float)는 양수인 보간 계수로, 단위는 초당(1/s)입니다. Instance Editable로 두어 레벨에서 조절하며, 일정한 cm/s 속도를 뜻하지 않습니다. bIsMoving(Boolean)은 발판이 지금 움직이는 중인지를, TargetLocation(Vector)은 지금 향하는 목표 위치를 저장합니다.
시작과 요청 처리
Event BeginPlay에서 Get Actor Location으로 지금 위치를 StartLocation에 저장하고, EndLocation을 StartLocation + (0, 0, 300)으로 정합니다. 그리고 TargetLocation은 StartLocation으로, bIsMoving은 False로 시작합니다.
RequestMove에서 요청이 유효하고 bIsMoving이 False라면, bIsMoving을 True로 바꾸고 TargetLocation을 정합니다. Distance (Vector) 노드로 지금 위치가 StartLocation과 EndLocation 가운데 어느 쪽에 더 가까운지 비교해, 더 먼 쪽을 새 목표로 고릅니다. 그러면 아래에 있을 때는 위로, 위에 있을 때는 아래로 움직입니다. 오버랩하는 동안에는 사용자 인터페이스와 환경 연동처럼 안내를 띄울 수 있습니다.
매 프레임 이동하기
Event Tick에서 Branch로 bIsMoving이 True일 때만 이동 로직을 실행합니다. 이동은 Lerp (Vector)로 계산하는데, A에는 Get Actor Location (Self)로 얻은 지금 위치를, B에는 TargetLocation을, Alpha에는 Clamp(Get World Delta Seconds * MoveSpeed, 0.0, 1.0)을 넣습니다. 그 결과를 Set Actor Location의 New Location에 연결하고 Sweep을 켭니다. Sweep 중에 막히는 Hit가 생기면 이동을 멈추고, 막혔다는 피드백을 줍니다.
도착은 Distance (Vector)로 지금 위치와 TargetLocation 사이의 거리를 재서 판단합니다. 거리가 예를 들어 10.0 cm보다 작아지면, 목표 위치로 마지막 Sweep 이동을 시도하고, 막히지 않았다면 위치를 확정한 뒤 bIsMoving을 False로 바꿉니다. 다음 입력이 들어오면 반대쪽 끝을 새 목표로 고릅니다. 발판이 움직이기 시작할 때 소리를 내고, 움직이는 동안 먼지 파티클을 만들면 반응이 더 분명해집니다.
이 이동은 일정한 속도가 아니다
A에 매번 지금 위치를 넣는 이 보간은, 매 프레임 남은 거리의 일정 비율만큼 움직입니다. Delta Seconds가 1/60초이고 MoveSpeed가 1.5라면 Alpha는 1/60 × 1.5 = 0.025입니다. 남은 거리가 300 cm일 때 첫 프레임의 이동은 300 × 0.025 = 7.5 cm이고, 다음 프레임에는 남은 292.5 cm의 0.025배인 7.3125 cm를 움직입니다. 이렇게 이동 거리가 매번 줄어들기 때문에, 일정한 속도의 이동과는 다르게 처음에는 빠르고 도착할수록 느려집니다.
이 방식에서는 남은 거리가 0에 점점 가까워질 뿐 정확히 0이 되지는 않습니다. 그래서 일정 거리 안에 들어오면 도착한 것으로 보는 허용 거리와, 마지막에 목표 위치로 확정하는 단계가 필요합니다.
같은 결과를 재현하는 기준
글만 보고 같은 결과를 재현하려면 다음 값부터 고정합니다. BP_MovingPlatform을 열고 Components에서 InteractionTrigger를 선택한 뒤 Details의 Collision에서, Collision Presets를 Custom으로 하고 Pawn은 Overlap, 나머지는 Ignore로 정하며, Generate Overlap Events를 켭니다. MoveSpeed는 1.5, EndLocation은 StartLocation 기준 Z + 300, 도착 판정 거리는 10.0 cm입니다.
이 조건에서 장애물이 없다면 발판은 남은 거리의 일정 비율을 줄여 가며 다가갑니다. 연속 시간으로 근사하면 남은 거리는 매초 e⁻¹·⁵배로 줄어드므로, 300 cm에서 10 cm 아래로 줄어드는 데 ln(300 / 10) ÷ 1.5 ≈ 2.27초쯤 걸립니다. 다만 실제로는 Tick의 간격과 충돌에 따라 달라지므로, 정해진 도착 시간을 보장하는 값은 아닙니다. 목표에 도착하면 bIsMoving이 False로 바뀌어 더 움직이지 않고, 다시 상호작용하면 시작 위치로 거꾸로 움직입니다.
발판이 이상하게 움직일 때
상호작용 키를 눌러도 발판이 움직이지 않는다면, 오버랩 감지에 실패했거나 bIsMoving 분기의 연결이 빠졌을 수 있습니다. 오버랩이 후보 참조를 제대로 저장하는지, 그리고 별도의 Interact 입력이 그 발판의 커스텀 이벤트를 실제로 호출하는지 확인합니다.
발판이 떨리면서 목표에 다가가지 못한다면, Alpha가 지나치게 크거나 도착 판정 거리가 0에 가까울 수 있습니다. Alpha를 0에서 1 사이로 제한했는지, MoveSpeed가 양수인지, 목표 위치가 고정되어 있는지, Sweep에 막히고 있지 않은지, 도착한 뒤 멈추는 처리가 있는지를 차례로 확인합니다.
발판이 너무 빨리 순간 이동한다면, Set Actor Location의 입력에 Lerp 결과가 아니라 TargetLocation이 바로 연결되었을 수 있습니다. New Location 핀이 반드시 Lerp (Vector)의 결과를 받도록 연결을 정리합니다.
성능과 멀티플레이어
타임라인은 너무 많은 것을 동시에 재생하지 않게 하고, 쓸데없이 길거나 키가 복잡한 타임라인은 피합니다. 물리 시뮬레이션이 꼭 필요하지 않은 오브젝트라면 Set Actor Location이나 회전, 크기를 직접 바꾸는 방식을 씁니다. 물리 시뮬레이션은 비용이 더 크기 때문입니다. 부서지는 물체의 파편은 시뮬레이션하는 시간을 제한하거나, 일정 시간이 지나면 Destroy Actor로 없애 부담을 줄입니다.
화면에 안 보이는 것과 게임 플레이 계산을 멈추는 것은 별개입니다. 플레이어나 네트워크 상태에 영향을 주지 않는 대상만 골라, 따로 로직을 만들어 애니메이션과 Tick, 물리를 줄입니다. 복잡한 상호작용 로직은 여러 컴포넌트나 자식 블루프린트로 나누면 읽기도 최적화하기도 쉽습니다.
멀티플레이어 게임에서는 움직이는 오브젝트의 위치와 움직임, 켜짐 여부가 모든 클라이언트에 정확히 맞아야 하므로 복제(replication) 설정을 신중하게 구성합니다. 이 발판은 로컬 플레이어 한 명을 위한 예이므로, 멀티플레이어에서는 서버가 이동 상태를 정하고 필요한 위치와 상태를 복제하도록 따로 만들어야 합니다. 이동 중의 입력을 막는 경로와, 도착하거나 막혔을 때 bIsMoving을 푸는 경로도 각각 시험합니다.
정리
- 오브젝트는 타임라인, 재질 파라미터, 물리 제어, 생성과 제거, 가시성 제어로 바꾸며, 숨기는 것과 충돌, Tick, 물리는 별개입니다.
- 타임라인의
Play는 지금 위치에서 정방향으로,Play from Start는 처음부터,Reverse는 거꾸로 재생하고,Update와Finished로 값을 적용하고 끝을 처리합니다. - 발판도 문처럼 입력을 한 곳에서 받아 후보 발판의 커스텀 이벤트를 호출하고, 발판이 요청자의 범위를 다시 확인합니다.
- A에 지금 위치를 넣는 Lerp는 매 프레임 남은 거리의 비율(Alpha = Delta Seconds × MoveSpeed)만큼 움직여, 일정한 속도가 아니며 정확히 0에 닿지 않습니다.
- 그래서 도착 허용 거리와 마지막 Sweep 확정이 필요하고, 루트 컴포넌트의 충돌로 Sweep 막힘을 처리합니다.
- 물리가 필요 없는 움직임은 트랜스폼을 직접 바꾸고, 멀티플레이어에서는 서버가 상태를 정해 복제합니다.
연습 문제
1. 이번 프레임의 이동 거리
남은 거리가 200 cm이고, Delta Seconds가 0.02초, MoveSpeed가 2입니다. 이번 프레임에 발판은 몇 cm 움직이고, 막힘이 없다면 남은 거리는 얼마일까요? MoveSpeed 2를 초당 2 cm로 읽어도 될까요?
해설 보기
Alpha는 0.02 × 2 = 0.04이므로 이번 이동은 200 × 0.04 = 8 cm이고, 막힘이 없다면 남은 거리는 192 cm입니다. 다음 프레임에는 같은 방식으로 지금 위치와 목표에서 다시 계산합니다.
MoveSpeed 2는 남은 거리를 줄이는 비율의 계수라서, 초당 2 cm의 일정한 속도로 읽으면 안 됩니다. 실제 이동 거리는 남은 거리에 따라 달라집니다.
2. 숨긴 벽에 부딪힌다
퍼즐을 풀면 길을 막던 벽이 사라지게 하려고 벽 액터에 Set Actor Hidden In Game을 True로 정했습니다. 벽은 안 보이는데 플레이어가 여전히 그 자리를 지나가지 못합니다. 왜 그럴까요?
해설 보기
Set Actor Hidden In Game은 액터를 그리지 않게 할 뿐, 충돌은 그대로 남겨 둡니다. 숨기는 것과 충돌은 별개이기 때문입니다.
길을 열려면 벽의 충돌도 함께 꺼야 합니다. 벽 컴포넌트의 충돌을 끄는 노드를 함께 쓰거나, 다시 나타날 일이 없다면 Destroy Actor로 벽을 없앱니다.
3. 일정한 속도로 움직이게 하려면
이 발판은 처음에 빠르고 도착할수록 느려집니다. 300 cm를 일정한 속도로 3초 동안 움직이게 하고 싶다면 무엇을 바꿔야 할까요?
해설 보기
A에 매번 지금 위치를 넣는 대신, 이동을 시작할 때의 위치와 목표 위치를 고정해 두고, Alpha를 시작부터 흐른 시간 ÷ 3초로 0에서 1까지 일정하게 늘립니다. 그러면 Alpha가 시간에 비례해 늘어나므로 초당 100 cm의 일정한 속도로 움직입니다.
같은 일을 하는 다른 방법은, 길이 3초에 0에서 1로 일정하게 늘어나는 Float 트랙을 가진 타임라인을 만들고, Update에서 고정한 두 위치를 그 값으로 Lerp하는 것입니다. 앞 글의 문과 같은 구조입니다.
다음 글 환경 변수와 플레이어 행동 반응에서는 광석을 캐면 인벤토리가 늘어나는 것처럼, 플레이어의 행동이 환경의 상태 값을 바꾸고 그 값이 다시 세계에 반영되는 구조를 다룹니다.