본문 바로가기

언리얼레퍼런스/콜리전

[언리얼] 콜리전

교재를 공부하면서 새로 배운 자료 구조, 함수 및 C++ 정보들을 정리하는 글입니다.

● 게임 세계에 속해있는 움직이는 액터는 가상의 물리 엔진으로부터 영향을 받는다. 캐릭터의 길을 막는 용도 외에도 중력과 외부로부터 작용한 힘을 받은 물체의 움직임을 사실적으로 표현하는 데 물리 엔진이 활용된다.

또한 물리 엔진은 액터가 지정한 영역에 들어왔는지 감지하는 기능을 사용해 게임에서 플레이어가 해결해야 하는 기믹 요소를 제작하는데도 중요한 역할을 한다.

● 이렇게 다양한 기능을 제공하는 물리 엔진을 활용하려면 콜리전(= 물리적 충돌 영역)을 설정해야 한다.

콜리전은 3가지 방법으로 제작이 가능하다.

1. 스태틱 메시 애셋 : 스태틱 메시 애셋에 콜리전 영역을 심는 방법. 스태틱 메시를 더블 클릭하면 열리는 스태틱 메시 애셋에 콜리전을 심으면 스태틱 메시 컴포넌트에서 비주얼과 충돌이라는 두 가지 기능을 설정할 수 있어 관리가 편하다.

2. 기본 도형 컴포넌트 : 구체 / 박스 / 캡슐의 기본 도형이 있으며, 이를 이용해서 충돌 영역을 지정하는 방법. 스태틱 메시와 별도로 충돌 영역을 제작하는 데 사용된다. 스켈레탈 메시를 움직일 때 주로 사용한다.

3. 피직스 애셋 : 일반적으로 캐릭터 이동은 캡슐 컴포넌트를 이용해 처리한다. 하지만 특정 상황 캐릭터의 각 관절이 흐느적거리는 헝겊 인형(Rag Doll) 효과를 구현할 때, 이 피직스 애셋을 사용한다. 캐릭터 각 부위에 기본 도형으로 충돌 영역을 설정하고 이를 연결해 캐릭터의 물리를 설정한다.

피직스 애셋은 스켈레탈 메시에만 사용할 수 있다.

● 충돌 영역을 설정하면 이를 어떻게 활용할지 지정해야 한다. 스태틱 메시 애셋에는 Block All이라는 기본 설정이 있으므로 별도의 설정을 하지 않아도 캐릭터의 이동을 방해하는 레벨 콘텐츠를 제작할 수 있다. 하지만, 단순히 길을 막는 게 아니라 다른 액터나 레벨과 상호작용하면서 문제를 풀어나가는 콘텐츠를 제작하려면 물리 엔진의 세부적인 설정을 해야 한다. 세부 설정은 3가지로 구분된다.

1. 콜리전 채널과 기본 반응

2. 콜리전 채널의 용도

3. 다른 콜리전 채널과의 반응

● 충돌체에는 반드시 하나의 콜리전 채널을 설정해야 한다. 언리얼 엔진은 WorldStatic / WorldDynamic / Pawn / Visibility / Camera / PhysicsBody / Vehicle / Destructible 총 8개의 기본 콜리전 채널을 제공한다.

각 콜리전 채널의 용도는 다음과 같다.

World Static
움직이지 않는 정적인 배경 액터에 사용하는 콜리전 채널.
주로 스태틱 메시 액터에 있는 스태틱 메시 컴포넌트에 사용한다.
World Dynamic
움직이는 액터에 사용하는 콜리전 채널이다.
블루 프린트에 속한 스태틱 메시 컴포넌트에 사용한다.
Pawn
플레이어가 조종하는 물체에 주로 사용한다.
캐릭터의 충돌을 담당하는 캡슐 컴포넌트에 설정된다.
Visibility
배경 물체가 시각적으로 보이는지 탐지하는 데 사용한다.
탐지에서 Pawn은 제외된다.
마우스로 물체를 선택하는 피킹 기능을 구현할 때 사용된다.
PhysicsBody
물리 시뮬레이션으로 움직이는 컴포넌트에 설정한다.
Camera
카메라 설정을 위해 카메라와 목표물 간 장애물이 있는지 탐지하는 데 사용.
이전에 GTA 방식으로 캐릭터 조작 시, 장애물이 시야를 가리면 카메라를 장애물 앞으로 줌인하는 기능이 있었는데, 이때 사용하는 채널이 Camera 채널이다.

캐릭터의 루프 컴포넌트인 캡슐 컴포넌트는 Pawn 콜리전 채널이 설정된다. 콜리전 프리셋의 값과 오브젝트 타입의 값은 서로 다른 설정값이고, 확인해야 할 것은 오브젝트 타입의 값이다.

● 그리고 해당 컴포넌트에서 물리 기능을 어떻게 사용할지 지정해야 하는데, 이는 콜리전 활성화됨(Collision Enable)에서 설정할 수 있다.

이 콜리전 활성화됨(Collision Enable)에서 설정할 수 있는 값은 다음과 같다.

Query(= Query Only)
두 물체의 충돌 영역이 서로 겹치는지 테스트하는 설정.
충돌 영역의 겹침을 감지하는 것을 오버랩이라고 하는데, 충돌 영역이 겹치면 관련 컴포넌트에 BeginOverlap 이벤트가 발생한다.
지정한 영역에 물체가 충돌하는지 탐지하는 레이크스트(Raycast)나 스윕(sweep) 기능도 Query에 속한다.
Physics(= Physics Only)
물리적인 시뮬레이션을 사용할 때 설정한다.
Query and Physics
(= Collision Enabled)
위의 두 기능을 모두 사용하는 설정.
이로 설정 시, 모든 기능이 잘 동작하나, 물리 엔진이 수행할 계산량이 많아진다.
따라서 액터마다 자신에게 필요한 기능을 파악하여 Query나 Physics 설정만 지정하는 것이 효과적이라고 할 수 있다.
콜리전 프리셋 Pawn에는 Collision Enable 항목에 Query and Physics가 설정되어 있다.
그리고 Query 기능을 사용하는 경우 관련 이벤트가 발생하므로 Generate Overlap Events 옵션이 체크되어 있다.

● 마지막으로 해당 컴포넌트에 설정된 콜리전 채널이 상대방 컴포넌트의 콜리전 채널과 어떻게 반응할지 지정하는 작업이 있다. 다른 콜리전 채널과 반응을 무시 / 겹침 / 블록 총 3가지로 지정할 수 있다.

무시 Ignore
콜리전이 있어도 아무 충돌이 일어나지 않음.
겹침 Overlap
무시와 동일하게 물체가 뚫고 지나갈 수 있지만 이벤트를 발생시킨다.
블록 Block
물체가 뚫고 지나가지 못하도록 막는다.

충돌은 두 물체의 상호작용이므로, 두 물체가 가진 반응 값에 따라 결과가 달라진다. 언리얼 엔진에서 물리는 무시 반응을 최대화하고, 블록 반응을 최소화하도록 동작한다.

예를 들어 충돌 컴포넌트 중 하나를 무시 반응으로 설정하면 겹침과 블록 반응은 발생하지 않게 되는 것이다. 그리고 충돌 컴포넌트 중 하나를 겹침 반응으로 설정하면 앞으로 블록 반응은 발생하지 않는다.

● 두 컴포넌트 간 물리적 반응이 일어날 때 각 컴포넌트에는 겹침 시, BeginOverlap 이벤트

블록 반응에는 Hit 이벤트가 발생한다. 이를 사용하여 게임 콘텐츠를 설계할 수 있다.

+ 블록 반응 설정 시, Hit 이벤트 말고, BeginOverlap 이벤트도 발동시킬 수 있다. 그러기 위해서 Generates Overlap Event 항목이 양쪽 컴포넌트에 모드 체크되어 있어야 한다.

● 콜리전 채널은 새로 만들 수도 있다. 프로젝트 세팅 > 콜리전에서 만들 수 있다.

이때, 콜리전 채널은 오브젝트 채널과 트레이스 채널로 나뉘는데, 각 용도는 다음과 같다.

- 오브젝트 채널 : 콜리전 영역에 지정하는 콜리전 채널

- 트레이스 채널 : 어떤 행동에 설정하는 콜리전 채널

기본적으로 제공되는 채널에서 WorldStatic / WorldDynamic / Pawn / PhysicBody / Vehicle / Destructible은 오브젝트 채널이고,

Visibility / Camera는 트레이스 채널이다.

● 다음과 같이 오브젝트 채널에서 콜리전 채널을 생성한다.

그리고 이미 만들어진 콜리전 프리셋과 문제없도록 설정해야 한다. 그러기 위해서 프리셋도 새로 추가해 준다.

추가로 Trigger라는 프리셋은 캐릭터의 진로를 방해하지 않고, 보이지 않으면서 영역 안에 들어왔는지 감지하는 프리셋이다.

따라서 이는 ABCharacter과 블록이 되면 안 된다. 이를 방지하기 위해 Trigger 프리셋에서 ABCharacter 과의 반응을 겹침으로 변경해 줘야 한다.

● 그 외에도 많은 프리셋이 있다. 각 프리셋의 역할은 다음과 같다.

- OverlapAll : 모든 액터와 겹치는 설정

- OverlapAllDynamic : 모든 다이내믹과 겹치는 설정으로, 기본적으로 모든 액터와 겹치는 설정

- IgnoreOnlyPawn : 폰만 충돌을 무시하는 설정

- OverlapOnlyPawn : 폰만 겹침 이벤트가 발동하도록 설정

- Spectator : 외부 관중과의 충돌을 설정

- CharacterMesh : 캐릭터 메시에 사용하는 물리 설정

- RagDoll : 스켈레탈 메시의 피직스 애셋 물리를 가동하기 위한 물리 설정

- Trigger : 지정한 영역에 물체가 들어오면 이벤트가 발동하는 용도로 사용

- UI : UI 요소에 사용하는 설정

 

그 외에도 언리얼 공식 자료를 보면 확인할 수 있다.

● 그리고 새로 만든 프리셋을 ABCharacter의 캡슐 콜리전으로 설정해 주면 다음과 같이 설정된다.

● 다음으로 공격 애니메이션이 일어나는 특정 타이밍에 공격 범위 내 위치한 액터가 있으면 감지된 액터에게 대미지를 주는 행위를 만들 것이다.

이렇게 어떤 행동에 대한 물리적 판정이 필요할 때 물리 엔진을 활용할 수 있다.

언리얼 엔진은 행동에 대한 판정을 위해 트레이스 채널이라는 카테고리로 콜리전 채널을 제공한다. 캐릭터의 공격 판정을 위해서 트레이스 채널을 추가해야 한다.

그리고 위에서 만들었던 오브젝트 채널 ABCharacter에서 추가된 트레이스 채널 Attack을 블록으로 지정한다.

트레이스 채널을 사용해서 물리적 충돌 여부를 가리는 함수 중 하나로 'SweepSingleByChannel'이 있다. 물리는 월드의 기능이므로 GetWorld() 함수를 사용해서 월드에게 명령을 내려야 한다.

해당 함수는 기본 도형을 인자로 받고, 시작 지점에서 끝 지점까지 쓸면서(= Sweep) 해당 영역 내 물리 판정이 일어났는지 조사한다. 이 함수에서 사용할 파라미터 설정은 꽤나 복잡하다. 인자로 넣어야 하는 요소는 다음과 같다.

- HitResult : 물리적 충돌이 탐지되는 경우 관련된 정보를 담을 구조체
- Start : 탐색을 시작할 위치
- End : 탐색을 끝낼 위치
- Rot : 탐색에 사용할 도형의 회전
- TraceChannel : 물리 충돌 감지에 사용할 트레이스 채널 정보
- CollisionShape : 탐색에 사용할 기본 도형 정보, 구체, 캡슐, 박스를 사용한다.
- Params : 탐색 방법에 대한 설정값을 모아둔 구조체
- ResponseParams : 탐색 반응을 설정하기 위한 구조체

● 이제 공격 범위를 설정하기 위해서 반지름 50cm의 구를 만들어야 하고, 캐릭터 위치에서 정면 방향으로 200cm 떨어진 곳까지 쓸어서(= Sweep) 충돌 물체가 있는지 감지해야 한다. 그러므로 TraceChannel은 Attack으로 설정해야 한다.

● 이때, Attack 채널의 값은 언리얼 엔진에서 정의한 ECollisionChannel 열거형으로 가져올 수 있는데, C++ 코드에서 이를 가져오는 법은 복잡하다. 언리얼 엔진에서 게임에서 사용할 수 있는 총 32개 채널에서 게임 프로젝트에서 약 18개 정도 사용이 가능하다.

Engine > Source > Runtime > Engine > Classes > Engine > EngineTypes.h에서 확인 가능

게임 프로젝트에서 새로 생성하는 오브젝트 채널과 트레이스 채널은 ECC_GameTraceChannel 1번부터 18번까지 중에서 하나를 배정받는 것인데, Attack은 위에서 확인할 수 있듯 'ECC_GameTraceChannel2'를 받은 것이다.

이를 가져오거나 어떤 값을 배정받았는지는 프로젝트 폴더 > Config 폴더 > DefaultEngine.ini에서 확인할 수 있다.

+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Block,bTraceType=False,bStaticObject=False,Name="ABCharacter") +DefaultChannelResponses=(Channel=ECC_GameTraceChannel2,DefaultResponse=ECR_Ignore,bTraceType=True,bStaticObject=False,Name="Attack")

● 콜리전 채널을 지정하면, FCollisionShape::MakeSphere 함수를 사용해 탐지에 사용할 도형을 제작한다. 탐색할 도형으로는 50cm 반지름을 가지는 구체를 사용하고 회전 값은 기본 값을 지정한다.

그리고 도형의 탐색 영역을 지정하고 탐색을 시작할 위치는 액터(= 캐릭터)가 있는 곳으로, 끝낼 위치는 액터 시선 방향으로 200cm 떨어진 곳으로 지정한다.

또한 탐색할 영역까지 지정되면 탐색 방법을 설정해야 한다. 공격 명령을 내리는 자신은 이 탐색에 감지돼서는 안되므로 this(= 자신 포인터)를 무시할 액터 목록에 넣어줘야 한다. 마지막 인자인 탐색 반응 설정은 구조체 기본값을 사용해야 한다.

마지막으로 액터의 충돌이 탐지되면, 충돌된 액터에 관련된 정보를 얻기 위해서 구조체를 넘겨줘야 한다. 이는 FHitResult 구조체로 지역 변수를 생성하고, 이를 첫 번째 인자에 넣어주면 탐색 기능을 만들 수 있다.

이를 코드로 작성하면 코드의 흐름은

ABCharacter.h에서 공격 범위에 있을 경우 실행할 함수

1. 각 공격 애니메이션마다 AttackHitCheck 노티파이를 생성했는데, 각 공격 애니메이션이 실행되어 AttackHitCheck 노티파이를 만나면 위에서 선언한 AttackCheck() 함수를 호출하도록 한다.

2. 이 AttackCheck() 함수는 SweepSingleByChannel() 함수를 사용하여, 현재 캐릭터 주변에 액터가 있는지 탐지하고,

탐지되면 그 액터를 HitResult.GetActor()로 사용하려는 액터가 유효한지 점검을 한다. 유효하다면 그 액터의 이름을 출력하도록 구현되었다.

+ 243번 줄은 기존에 HitResult.Actor.IsValid()였지만, Unreal Engine5에서는 HitResult.GetActor()로 변경하여 사용할 수 있다.

3. 빌드 후, 게임을 플레이하고, 씬에 배치된 캐릭터 근처에서 공격을 하면 다음과 같이 탐지된 액터의 이름이 로그에 찍힌다.

● 그런데 공격 범위가 시각적으로 보이지 않다 보니 공격에 성공했는지 로그를 보지 않으면 파악할 수 없다. 로그를 보지 않고 파악하려면 디버그 드로잉(Debug Drawing) 기능을 사용해야 한다.

● 디버그 드로잉 기능을 사용하기 위해서는 상단에 DrawDebugHelpers.h 헤더를 추가해야 한다.

DrawDebugHelpers.h

이 헤더에는 다양한 그리는 함수들이 선언되어 있고, 그중 캡슐 모양을 그리는 기능인 DrawDebugCapsule() 함수를 사용하여 탐색을 위해 원이 움직인 궤적을 표현해 본다.

캡슐의 반지름을 50으로 설정하고, 탐색 시작 위치에서 탐색 끝 위치로 향하는 벡터를 구한 후, 벡터의 중점 위치와 벡터 길이의 절반을 대입하면 우리가 원하는 크기의 캡슐 모양을 구할 수 있다.

캡슐은 상하가 서있는 모습을 가지므로, 회전 행렬을 적용해 캡슐 방향을 캐릭터 시선 방향으로 눕힌 후, 공격 범위에 맞게 길이를 설정하면 된다.

+ 여기서 캡슐을 캐릭터 시선 방향으로 눕히는 것은 캡슐 상단으로 향하는 벡터(Z 벡터)가 캐릭터의 시선 방향과 일치한다는 것을 의미한다. 즉, FRotationMatrix의 MaxkeFromZ() 함수에 캐릭터의 시선 방향 벡터를 입력하면 필요한 회전 좌표 축을 생성할 수 있다.

코드를 작성하면 다음과 같다.

// 공격했을 때, 액터의 충돌이 감지되면 void AABCharacter::AttackCheck() { FHitResult HitResult; FCollisionQueryParams Params(NAME_None, false, this); bool bResult = GetWorld()->SweepSingleByChannel( HitResult, GetActorLocation(), GetActorLocation() + GetActorForwardVector() * 200.0f, FQuat::Identity, ECollisionChannel::ECC_GameTraceChannel2, FCollisionShape::MakeSphere(50.0f), Params); #if ENABLE_DRAW_DEBUG // 반지름 50, 탐색 시작 위치에서 탐색 끝 위치로 향하는 벡터를 구한 후, 벡터의 중점위치와 벡터 길이의 절반을 대입하면 원하는 크기의 캡슐 모양을 구할 수 있음 FVector TraceVec = GetActorForwardVector() * AttackRange; // 앞으로 200cm거리 탐색 FVector Center = GetActorLocation() + TraceVec * 0.5f; // 벡터의 중점 위치 float HalfHeight = AttackRange * 0.5f + AttackRadius; // 벡터 길이의 절반 FQuat CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat(); // Z축 회전 좌표축 생성 FColor DrawColor = bResult ? FColor::Green : FColor::Red; // 공격 범위 내 적이 탐색되면 녹색, 아니면 빨간색으로 float DebugLifeTime = 5.0f; // 캡슐 5초간 생성 DrawDebugCapsule(GetWorld(), // 캡슐 디버그 생성 Center, HalfHeight, AttackRadius, CapsuleRot, DrawColor, false, DebugLifeTime ); #endif if (bResult) { if (HitResult.GetActor()) { ABLOG(Warning, TEXT("Hit Actor Name : %s"), *HitResult.GetActor()->GetName()) } } }

그리고 플레이해 보면 공격 범위 내에 들어올 경우, 초록색 캡슐이

공격 범위 외의 경우, 빨간색 캡슐이 생성되는 것을 확인할 수 있다.

● 언리얼 엔진이 제공하는 대미지 프레임 워크를 사용하면 대미지에 대한 여러 기능을 간편하게 처리할 수 있다.

언리얼 엔진의 액터 클래스 AACtor는 TakeDamage라는 함수가 구현되어 있다. 이 함수를 사용하면 손쉽게 액터에 대미지를 전달할 수 있다.

● TakeDamage() 함수에는 총 4개의 매개 변수가 있다. 각 매개 변수의 용도는

- DamageAmount : 전달한 대미지의 세기

- DamageEvent : 대미지 종류

- EventInstigator : 공격 명령을 내린 가해자

- DamageCauser : 대미지 전달을 위해 사용한 도구

● 대미지를 전달하는 행위에 항상 가해자와 피해자가 있다. 여기서 해자는 폰이 아니라 폰에게 명령을 내린 플레이어 컨트롤러이다. 따라서 EventInstigator에는 폰이 아닌 컨트롤러의 정보를 보내야 한다.

우리가 조종하는 폰은 플레이어가 대미지 전달을 위해 사용하는 도구라고 해석할 수 있으므로, 마지막 파라미터인 DamageCauser로 지정해야 한다.

그런데, 대미지는 피해자 액터에 관련 로직을 구성해 줘야 완성이 된다. 그래서 TakeDamage() 함수를 오버라이드 해 액터가 받은 대미지를 처리하는 로직을 추가해야 한다.

TakeDamage() 함수는 부모 클래스인 AActor에 기본적인 대미지 관련 로직이 구현되어 있기 때문에 Super 키워드를 사용해서 부모 클래스 로직을 먼저 실행해 줘야 한다.

ABCharacter.h에 TakeDamage() 함수 오버라이드 선언

ABCharacter.cpp에 TakeDamage() 함수 오버라이드 구현

그러면 공격 범위에 있을 때, 로그에 피해자 이름과, 대미지 값이 출력되는 것을 확인할 수 있다.

이때, *GetName() 함수에서 피해자 액터 이름이 나오는 이유는 AttackCheck() 함수에서 HitResult.GetActor()의 TakeDamage() 함수가 호출된 것으로 GetName() 사용 시 피해자 액터 이름이 출력되기 때문이다.

이렇게 대미지가 전달되기 위해서는 모든 액터의 디테일에서 '대미지 받기 가능 여부(Can be Damaged)' 속성이 체크가 되어 있어야 한다.

체크가 해제되어 있으면 대미지 결과가 항상 0으로 무적 상태가 된다.

● 이제 대미지를 받아서 캐릭터가 사망하는 애니메이션을 만들기 위해서 bool 타입의 IsDead 변수와 사망 상태로 변경해 주는 함수를 생성한다.

그리고 isDead = true(사망함) 상태가 되면, 공격 / 점프 등 애니메이션이 동작할 수 없도록 한다.

● 이렇게 isDead 변수를 생성했으면, 애니메이션 블루 프린트로 이동하여 '부울로 포즈 블렌딩' 노드를 새로 추가하고,

isDead가 true(사망 상태)이면 사망 애니메이션을 재생하도록 하고,

isDead가 false(생존 상태)이면 기본 애니메이션(BaseAction 스테이트 머신 + Default 슬롯(= 애님 몽타주의 Attack 애니메이션들))을 재생하도록 한다.

● 그리고, 대미지를 입었을 때, 사망하면 SetActorEnableCollision 함수를 사용하여 액터의 충돌 설정을 꺼서 앞으로 캐릭터에 충돌 이벤트가 발생하지 않도록 한다.

+ 대미지를 입었을 때, 사망으로 바꿔야 하므로 액터에 대미지를 전달하는 TakeDamage() 함수에 코드를 작성해야 한다.

이제 플레이하고 공격하여 캐릭터가 사망하면 충돌 설정이 꺼졌기 때문에 충돌이 발생하지 않아, 겹쳐질 수 있게 됐으며, 공격을 해도 충돌이 감지되지 않는 것을 확인할 수 있다.