물리 엔진은 게임 세계 내 액터들에게 영향을 준다.
캐릭터의 길을 막아주고, 중력이나 외부의 힘으로부터 작용한 힘을 받은 액터의 움직임을 표현해주고, 액터가 지정한 영역에 들어왔는지 감지도 하고,,
이러한 물리엔진을 활용하려면, 콜리전을 설정해야 한다
콜리전 설정
콜리전은 물리적 충돌 영역을 의미하는데, 언리얼에서 콜리전은 크게 세 가지 방법으로 제작할 수 있다.
- 스태틱메시 에셋 : 스태틱메시 에셋에 콜리전 영역을 심는 방법이다. 스태틱메시 컴포넌트에서 비주얼과 충돌이라는 두 가지 기능을 설정할 수 있어 관리가 편리하다.
- 기본 도형(Primitive) 컴포넌트 : 구체, 박스, 캡슐 등의 기본 도형을 사용해 스태틱메시와 별도로 충돌 영역을 지정한다. 스켈레탈 메시를 움직일 때 주로 사용한다.
- 피직스 애셋 : 일반적으로 캐릭터의 이동은 캡슐 컴포넌트를 사용해 처리한다. 하지만 특정 상황에서 캐릭터의 각 관절이 흐느적거리는 헝겊 인형(RagDoll) 효과를 구현할 때 이 피직스 애셋을 사용한다. 캐릭터의 각 부위에 기본 도형으로 충돌 영역을 설정하고 이를 연결해 캐릭터의 물리를 설정한다. 피직스 애셋은 스켈레탈 메시에만 사용할 수 있다.
스태틱메시 애셋의 충돌 영역은 기본적으로 BlockAll 이라는 설정이 있어 별도 설정을 하지 않으면 캐릭터의 이동을 방해하는 영역이 된다(길막). 하지만 언리얼에서는 단순히 길을 막는 것이 아닌, 트리거 등의 다양한 상호작용을 할 수 있는 기능(물리 설정)을 제공한다.
- 콜리전 채널과 기본 반응
- 콜리전 채널의 용도
- 다른 콜리전 채널과의 반응
충돌체에는 반드시 하나의 콜리전 채널을 설정해야 한다. 언리얼 엔진은 WorldStatic, WorldDynamic, Pawn, Visibility, Camera, PhysicsBody, Vehicle, Destructible 이라는 여덟 개의 기본 콜리전 채널을 제공한다.
- WorldStatic : 움직이지 않는 정적인 배경 액터에 사용하는 콜리전 채널이다. 주로 스태틱메시 액터에 있는 스태틱메시 컴포넌트에 사용한다.
- WorldDynamic : 움직이는 액터에 사용하는 콜리전 채널이다. 블루프린트에 속한 스태틱메시 컴포넌트에 사용한다.
- Pawn : 플레이어가 조종하는 물체에 주로 사용한다. 캐릭터의 충돌을 담당하는 캡슐 컴포넌트에 설정된다.
- Visibility : 배경 물체가 시각적으로 보이는지 탐지하는데 사용한다. 탐지에서 폰은 제외된다. 마우스로 물체를 선택하는 피킹(Picking) 기능을 구현할 때 사용한다.
- Camera : 카메라 설정을 위해 카메라와 목표물 간에 장애물이 있는지 탐지하는데 사용한다. 이전 GTA 방식으로 캐릭터를 조작할 때 장애물이 시야를 가리면 카메라를 장애물 앞으로 줌인하는 기능(SpringArm->bCollisionTest = true)이 있었다. 이때 사용하는 채널이 Camera 채널이다.
- PhysicsBody : 물리 시뮬레이션으로 움직이는 컴포넌트에 설정한다.
캐릭터의 루트 캡슐 컴포넌트에는 Pawn 이라는 콜리전 채널이 설정된다. 이를 확인해 보자.
CapsuleComponent 에 콜리전 프리셋이 Pawn으로 들어가 있다.
콜리전 프리셋의 값 Pawn 과 Object Type 값 Pawn 은 서로 다른 설정 값이다. 우리가 확인해야 할 콜리전 채널은 Object Type 값이다.
해당 컴포넌트에서 물리 기능을 어떻게 사용할지 지정하는 부분은 Object Type 에 위치한 Collision Enabled 항목이다. 설정값은 다음과 같다.
- Query : 두 물체의 충돌 영역이 서로 겹치는지 테스트하는 설정. 충돌 영역의 겹침을 감지하는 것을 오버랩(Overlap) 이라고 부르며, 충돌 영역이 겹치면 관련 컴포넌트에 BeginOverlap 이벤트가 발생한다. 지정한 영역에 물체가 충돌하는지 탐지하는 레이캐스트(Raycast) 나 스윕(Sweep) 기능도 Query 에 속한다.
- Physics : 물리적인 시뮬레이션을 사용할 때 설정한다.
- Query and Physics : 위의 두 기능을 모두 사용하는 설정이다.
레이캐스트(Raycast) 는 '보이지 않는 광선'을 쏘아 광선의 진행 방향에 충돌할 물체가 있는지를 검사하는 방식이다.
스윕(Sweep) 은 움직임에 제한을 줄때 사용된다. 예를 들어, 플레이어가 이동 중 벽을 만나면 중간에 멈추도록 만들 때 스윕을 이용한다.
레이캐스트 예시 그림
Query and Physics 설정을 사용하면 모든 기능이 잘 동작하겠지만, 계산량이 많아지므로 각 액터마다 필요한 설정만 지정하는 것이 효율적이다. 콜리전 프리셋 Pawn 에는 Collision Enabled 항목에 Query and Physics 가 설정되어 있다. 그리고 Query 기능을 사용하는 경우 관련 이벤트가 발생하도록 Generates Overlap Events 옵션이 체크돼 있다.
마지막으로는 해당 컴포넌트에 설정된 콜리전 채널이 상대방 컴포넌트의 콜리전 채널과 어떻게 반응할지 지정하는 작업이 필요하다.
- 무시 (Ignore) : 콜리전이 있어도 아무 충돌이 일어나지 않는다.
- 겹침 (Overlap) : 무시와 동일하게 물체가 뚫고 지나갈 수 있지만 이벤트를 발생시킨다.
- 블록 (Block) : 물체가 뚫고 지나가지 못하도록 막는다.
다른 콜리전 채널과의 반응들
충돌은 두 물체의 상호작용이기 때문에 두 물체가 가진 반응 값에 따라 결과가 달라진다. 언리얼 엔진에서 물리는 무시 반응을 최대화하고, 블록 반응을 최소화하도록 동작한다. 예를 들어, 충돌 컴포넌트 중 하나를 무시로 설정하면 겹침과 블록 반응을 발생하지 않는다. 마찬가지로 하나를 겹침으로 설정하면 블록 반응은 일어나지 않는다.
겹침 반응에는 BeginOverlap 이벤트가 발생하고 블록 반응에는 Hit 이벤트가 발생한다. 다만 블록 반응에서도 BeginOverlap 이벤트를 발동시킬 수 있는데, 위의 Generate Overlap Events 항목이 체크되어 있어야 한다.
게임 기획을 고도화하려면 그에 맞게 새로운 콜리전 채널을 추가해야 한다. 학습을 위해 프로젝트 설정의 Collision 에서 새로운 물리 설정을 추가해 보자.
콜리전 채널은 오브젝트 채널과 트레이스 채널로 나뉜다.
- 오브젝트 채널 : 콜리전 영역에 지정하는 콜리전 채널 (WorldStatic, WorldDynamic, Pawn, PhysicsBody, Vehicle, Destructible)
- 트레이스 채널 : 어떤 행동에 설정하는 콜리전 채널 (Visibility, Camera)
먼저, 새 오브젝트 채널을 만들어 보자. 기본 반응은 Block 이다.
그 후, 프리셋을 만들어 다른 오브젝트 채널 / 트레이스 채널들과의 반응 방식을 설정해야 한다. 우리가 만들 프리셋의 콜리젼은 Collision Enabled (Query and Physics) , 오브젝트 유형은 ABCharacter 로 한다.
ABCharacter 로 프리셋을 생성했다. 그런데 우리가 만든 프리셋이 Trigger 프리셋과 충돌을 일으켰을 때, 어떤 반응이 나와야 적절한 것일까? Trigger 는 해당 영역에 물체가 들어왔는지 체크하지 막아서도록 (블록) 만든 것이 아니므로, Trigger 에서 ABCharacter 와의 상호작용을 '겹침'으로 바꾸어 주어야 한다(기본 설정이 블록임). 귀찮겠지만, 다른 프리셋도 마찬가지로 바꾸어 주자.
- OverlapAll : 겹칩
- OverlapAllDynamic : 겹침
- IgnoreOnlyPawn : 무시
- OverlapOnlyPawn : 겹침
- Spectator : 무시 (외부 관중)
- CharacterMesh : 무시 (캐릭터 메시)
- RagDool : 무시 (스켈레탈 메시의 피직스 애셋 물리를 가동)
- Trigger : 겹침
- UI : 겹침
프리셋 설정까지 완료했으면, 이제 캡슐 컴포넌트가 해당 프리셋을 사용하도록 코드를 짠다.
ABCharacter.cpp
...
AABCharacter::AABCharacter()
{
...
GetCapsuleComponent()->SetCollisionProfileName(TEXT("ABCharacter"));
}
이제 새롭게 생성된 캐릭터 내 캡슐 컴포넌트의 콜리전 프리셋이 Pawn 에서 ABCharacter 로 바뀌었는지 체크하면 된다.
트레이스 채널의 활용
물리 엔진을 이용해 캐릭터의 공격 기능을 구현해 보자. Attack 이라는 트레이스 채널을 생성하고, 기본 반응은 무시로 설정한다. 그 후, 앞서 제작한 콜리전 프리셋 ABCharacter 에서 Attack 트레이스 채널과의 설정을 블록으로 지정한다. 이제 Attack 은 ABCharacter 콜리전 프리셋에만 반응하게 된다.
새이제 공격 판정을 내리는 로직을 추가하자. 트레이스 채널을 사용해 물리적 충돌 여부를 가리는 함수 중 하나로 SweepSingleByChannel 이 있다. 물리는 월드의 기능이므로 GetWorld( ) 함수를 사용해 월드에게 명령을 내려야 한다.
해당 함수는 기본 도형을 인자로 받은 후 시작 지점에서 끝 지점까지 쓸면서(Sweep) 해당 영역 내에 물리 판정이 일어났는지를 조사한다. 이 함수의 인자들은 다음과 같다.
- HitResult : 물리적 충돌이 탐지된 경우 관련된 정보를 담을 구조체
- Start : 탐색을 시작할 위치
- End : 탐색을 끝낼 위치
- Rot : 탐색에 사용할 도형의 회전
- TraceChannel : 물리 충돌 감지에 사용할 트레이스 채널 정보
- CollisionShape : 탐색에 사용할 기본 도형 정보. 구체, 캡슐, 박스를 사용한다.
- Params : 탐색 방법에 대한 설정 값을 모아둔 구조체
- ResponseParams : 탐색 반응을 설정하기 위한 구조체
여기서 Params 은 인자로 Name_None, false, this 를 줬는데, 각각
Name_None TraceTag 의 이름, Name_None은 TraceTag 가 없다고 명시하는 것
false 복잡한 검사가 아닌 간단한 검사를 실행 (true 시 복잡한 검사(complex collision test) 를 하게 되며
검출되는 첫번째 오브젝트, 충돌 지점 등 상세한 정보를 얻게 된다)
this 충돌검사를 하는 객체이며 검사시에 제외된다
공격의 범위를 설정하기 위해 반지름이 50cm 인 구를 만들고, 정면 방향으로 200cm 떨어진 곳까지 쓸어서 충돌하는 물체를 감지한다. TraceChannel 은 Attack 채널을 사용한다. Attack 채널의 값은 언리얼 엔진에서 정의한 ECollisionChannel 열거형으로 가져올 수 있다.
언리얼 엔진은 총 32개의 콜리전 채널을 제공한다. 그 중 8개는 언리얼 엔진이 기본으로 사용하고, 여섯 개는 다른 용도로 사용하도록 예약되어 있다. 따라서 우리는 나머지 18개만 사용 가능하다. ECollisionChannel 열거형에 대한 선언은 다음과 같다.
EngineTypes.h
...
/**
* Enum indicating different type of objects for rigid-body collision purposes.
*/
UENUM(BlueprintType)
enum ECollisionChannel
{
ECC_WorldStatic UMETA(DisplayName="WorldStatic"),
ECC_WorldDynamic UMETA(DisplayName="WorldDynamic"),
ECC_Pawn UMETA(DisplayName="Pawn"),
ECC_Visibility UMETA(DisplayName="Visibility" , TraceQuery="1"),
ECC_Camera UMETA(DisplayName="Camera" , TraceQuery="1"),
ECC_PhysicsBody UMETA(DisplayName="PhysicsBody"),
ECC_Vehicle UMETA(DisplayName="Vehicle"),
ECC_Destructible UMETA(DisplayName="Destructible"),
/** Reserved for gizmo collision */
ECC_EngineTraceChannel1 UMETA(Hidden),
ECC_EngineTraceChannel2 UMETA(Hidden),
ECC_EngineTraceChannel3 UMETA(Hidden),
ECC_EngineTraceChannel4 UMETA(Hidden),
ECC_EngineTraceChannel5 UMETA(Hidden),
ECC_EngineTraceChannel6 UMETA(Hidden),
ECC_GameTraceChannel1 UMETA(Hidden),
ECC_GameTraceChannel2 UMETA(Hidden),
ECC_GameTraceChannel3 UMETA(Hidden),
ECC_GameTraceChannel4 UMETA(Hidden),
ECC_GameTraceChannel5 UMETA(Hidden),
ECC_GameTraceChannel6 UMETA(Hidden),
ECC_GameTraceChannel7 UMETA(Hidden),
ECC_GameTraceChannel8 UMETA(Hidden),
ECC_GameTraceChannel9 UMETA(Hidden),
ECC_GameTraceChannel10 UMETA(Hidden),
ECC_GameTraceChannel11 UMETA(Hidden),
ECC_GameTraceChannel12 UMETA(Hidden),
ECC_GameTraceChannel13 UMETA(Hidden),
ECC_GameTraceChannel14 UMETA(Hidden),
ECC_GameTraceChannel15 UMETA(Hidden),
ECC_GameTraceChannel16 UMETA(Hidden),
ECC_GameTraceChannel17 UMETA(Hidden),
ECC_GameTraceChannel18 UMETA(Hidden),
/** Add new serializeable channels above here (i.e. entries that exist in FCollisionResponseContainer) */
/** Add only nonserialized/transient flags below */
// NOTE!!!! THESE ARE BEING DEPRECATED BUT STILL THERE FOR BLUEPRINT. PLEASE DO NOT USE THEM IN CODE
ECC_OverlapAll_Deprecated UMETA(Hidden),
ECC_MAX,
};
...
위에서+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") 부터 18 까지가 우리가 새로 생성하는 오브젝트 채널과 트레이스 채널이 배정받는 값이다. 어떤 값을 배정받았는지는 프로젝트의 Config 폴더에 있는 DefaultEngine.ini 에서 확인할 수 있다.
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")
위의 코드를 보면 Attack 이 ECC_GameTraceChannel2 로 설정되어 있는 것을 알 수 있다.
콜리전 채널을 지정한 후, FCollisionShape::MakeSphere 함수를 사용해 탐지에 사용할 도형을 제작한다. 도형은 50cm 반지름의 구를, 회전값은 기본값을 지정한다. 탐색 영역은 200cm로 설정한다.
탐색 영역을 지정한 후, 탐색 방법을 설정한다. 공격 명령을 내리는 자신은 이 탐색에 탐지되지 않도록 포인터 this 를 무시할 액터 목록에 넣어 준다. 그리고 마지막 인자인 탐색 반응 설정은 구조체의 기본값을 사용한다.
마지막으로 액터의 충돌이 탐지된 경우 충돌된 액터에 관련된 정보를 얻기 위해 구조체를 넘겨주어야 한다. FHitResult 구조체로 지역 변수를 하나 생성하고, 이를 첫번째 인자에 넣어주면 된다.
ABCharacter.h
...
UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
...
private:
...
void AttackCheck();
}
ABCharacter.cpp
FCollisionQueryParams, SweepSingleByChannel인식을 못해 다시 쳤는데 인식됨?
HitResult.GetActor.IsValid() -> HitResult.GetActor()->IsValidLowLevel()로 업그레이됨
//업그레이드 문법변경
HitResult.GetActor()->IsValidLowLevel()
...
void AABCharacter::PostInitializeComponents()
{
...
// OnAttackHitCheck 에 델리게이트를 설정한다.
// 구체적으로는...
// 1. 애니메이션 실행 도중 AttackHitCheck 노티파이를 만난다.
// 2. AnimNotify_AttackHitCheck 함수 내의 OnAttackHitCheck.BroadCast() 가 호출
// 3. 델리게이트로 묶인 AABCharacter::AttackCheck() 가 호출됨
ABAnim->OnAttackHitCheck.AddUObject(this, &AABCharacter::AttackCheck);
}
void AABCharacter::AttackCheck()
{
FHitResult HitResult;
FCollisionQueryParams params;
//FCollisionQeuryParams Params; // (NAME_None, false, this);
float AttackStart = 100.0f;
bool bResult = GetWorld()->SweepSingleByChannel(
HitResult,
GetActorLocation() + GetActorForwardVector() * AttackStart,
GetActorLocation() + GetActorForwardVector() * AttackRange,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel2,
FCollisionShape::MakeSphere(AttackRadius),
params);
//Params);
if (bResult)
{
if (HitResult.GetActor()->IsValidLowLevel())
{
ABLOG(Warning, TEXT("Hit Actor Name : %s"), *HitResult.GetActor()->GetName());
Center = HitResult.ImpactPoint;
DrawDebugString(GetWorld(), Center, FString::Printf(TEXT("%s"), *HitResult.GetActor()->GetName()), 0, FColor::White, 1);
}
}
}
AttackCheck 함수가 실행되면 ABLOG 를 통해 로그가 출력된다.
ABCharacter_10 을 Hit 했다는 로그 출력
디버그 드로잉
공격할 때마다 로그창을 열고 이를 체킹하는 것은 번거로운 작업이다. 대신, 언리얼에서 제공하는 디버그 드로잉(Debug Drawing) 기능을 사용해 공격 영역을 실제로 그려보자. 먼저 소스 상단에 DrawDebugHelpers.h 헤더를 추가한다.
DrawDebugHelpers.h
/** Draw a capsule using the LineBatcher */
ENGINE_API void DrawDebugCapsule(const UWorld* InWorld, FVector const& Center, float HalfHeight, float Radius, const FQuat& Rotation, FColor const& Color, bool bPersistentLines = false, float LifeTime = -1.f, uint8 DepthPriority = 0, float Thickness = 0);
우리는 그 중 DrawDebugCapsule 함수를 이용할 것이다. 캡슐의 반지름은 50, 길이는 200, 방향은 캐릭터 시선 방향으로 눕힌다.
캡슐이 캐릭터 시선 방향으로 눕는다는 것은 캡슐의 상단으로 향하는 벡터(Z 벡터)가 캐릭터의 시선 방향과 일치한다는 것을 의미한다. 따라서 FRotationMatrix 의 MakeFromZ 함수에 캐릭터의 시선 방향 벡터를 입력하면 필요한 회전 좌표축을 생성할 수 있다.
실제 해보니 자기 자신과의 충돌로 적과의 충돌이 앞에 가야만 일어났다. sweep의 위치를 자기 중심이 아닌 100 앞에서 부터로 수정한후 잘되었다.
시작 : GetActorLocation() + GetActorForwardVector() * AttackStart,
끝 : GetActorLocation() + GetActorForwardVector() * AttackRange,
콜리전 세팅에서 자신을 제외하는 방법도 있을것 같다.
ABCharacter.h
...
UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
/* ... */
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
float AttackRange;
UPROPERTY(VisibleInstanceOnly, BlueprintReadOnly, Category = Attack, Meta = (AllowPrivateAccess = true))
float AttackRadius;
};
AttackRange 와 AttackRadius 를 선언한다.
ABCharacter.cpp
FCollisionQueryParams, SweepSingleByChannel인식을 못해 다시 쳤는데 인식됨? 몇시간 고생함 ㅠㅠ
HitResult.GetActor.IsValid() -> HitResult.GetActor()->IsValidLowLevel()로 업그레이됨
//업그레이드 문법변경
HitResult.GetActor()->IsValidLowLevel()
...
#include "DrawDebugHelpers.h"
...
ABCharacter::ABCharacter()
{
...
AttackRadius = 50.0f;
AttackRange = 200.0f;
}
void AABCharacter::AttackCheck()
{
FHitResult HitResult;
FCollisionQueryParams params;
//FCollisionQeuryParams Params; // (NAME_None, false, this);
float AttackStart = 100.0f;
bool bResult = GetWorld()->SweepSingleByChannel(
HitResult,
GetActorLocation() + GetActorForwardVector() * AttackStart,
GetActorLocation() + GetActorForwardVector() * AttackRange,
FQuat::Identity,
ECollisionChannel::ECC_GameTraceChannel2,
FCollisionShape::MakeSphere(AttackRadius),
params);
//Params);
#if ENABLE_DRAW_DEBUG
FVector TraceVec = GetActorForwardVector() * (AttackRange);
FVector Center = GetActorLocation() + GetActorForwardVector() * (AttackRange+AttackStart) * 0.5f;
float HalfHeight = (AttackRange-AttackStart) * 0.5f + AttackRadius;
FQuat CapsuleRot = FRotationMatrix::MakeFromZ(TraceVec).ToQuat(); //ForwardVector만으로 충분할듯
FColor DrawColor = bResult ? FColor::Yellow : FColor::White;
float DebugLifeTime = 1.0f;
float Thickness = bResult ? 3 : 6;
DrawDebugCapsule(GetWorld(),
Center,
HalfHeight,
AttackRadius,
CapsuleRot,
DrawColor,
false,
DebugLifeTime,
Thickness);
#endif
if (bResult)
{
if (HitResult.GetActor()->IsValidLowLevel())
{
ABLOG(Warning, TEXT("Hit Actor Name : %s"), *HitResult.GetActor()->GetName());
Center = HitResult.ImpactPoint;
DrawDebugString(GetWorld(), Center, FString::Printf(TEXT("%s"), *HitResult.GetActor()->GetName()), 0, FColor::White, 1);
}
}
}
실제로 DraweDebug 가 잘 동작하는지 확인해 보자.
Hit 이 제대로 이뤄지지 않으면 빨간색 캡슐을, 제대로 이뤄졌을 경우 초록색 캡슐이 생성되는 것을 확인해 볼 수 있다.
대미지 프레임워크
언리얼 엔진의 액터 클래스 AActor 에는 4개의 인자를 갖고 있는 TakeDamage 라는 함수가 구현돼 있다.
- DamageAmount : 전달할 대미지의 세기
- DamageEvent : 대미지 종류
- EventInstigator : 공격 명령을 내린 가해자
- DamageCauser : 대미지 전달을 위해 사용한 도구
여기서 대미지를 가한 진정한 가해자는 폰이 아니라 폰에게 명령을 내린 플레이어 컨트롤러이므로, EventInstigator 에는 폰이 아닌 컨트롤러의 정보를 보내줘야 한다.
공격이 감지된 액터에 데미지를 전달하는 로직은 책대로는 잘 안된다. ApplyDamage로 Damage를 전달해야 한다.
책의 TakeDamage는 데미지를 전달받은 쪽에서 처리해야하는 것 같다.
...
void AABCharacter::AttackCheck()
{
...
if (bResult)
{
if (HitResult.Actor.IsValid())
{
ABLOG(Warning, TEXT("Hit Actor Name : %s"), *HitResult.Actor->GetName());
}
//FDamageEvent DamageEvent;
//HitResult.Actor->TakeDamage(50.0f, DamageEvent, GetController(), this);
UGameplayStatics::ApplyDamage(HitResult.GetActor(), 50.0f, GetController(),
this, UDamageType::StaticClass());
}
}
공격을 멀리서 하면 damage를 적게하기 위해 Impact포인트와 자신과의 거리를 계산해서 전달했다. 공격시작점100과 Capsule의 Radius 50의 거리 때문에 50정도 더해주면 되는것 같았다.
UGameplayStatics::ApplyDamage(HitResult.GetActor(), AttackRange+50-(GetActorLocation()-Center).Size2D(), GetController(),
this, UDamageType::StaticClass());
여기서 부터가 데미지를 전달받은 액터쪽에서 처리해야할TakeDamage함수이다.
ABCharacter.h
...
UCLASS()
class ARENABATTLE_API AABCharacter : public ACharacter
{
...
virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;
}
대미지는 피해를 입은 액터에 관련 로직을 설정해 주어야 한다.
원래 이함수는 부모인 액터에 이미 구현이 되어 있어 Super키워드를 사용해 부모클래스로직을 먼저 실행해줘야한다.
ABCharacter.cpp
...
float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const & DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float FinalDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
ABLOG(Warning, TEXT("Actor : %s took Damage : %f"), *GetName(), FinalDamage);
return FinalDamage;
}
실제로 데미지를 입는 로그가 출력된다.
모든 액터에는 Can be Damaged 속성이 있는데, 이를 해제하면 무적 (받는 데미지가 0) 이 된다.
이제 대미지를 입었을 때 캐릭터가 죽는 WarriorDead 애니메이션도 넣어 보자. 먼저 ABAnimInstance 에서 IsDead 라는 이름의 속성을 추가한다.
ABAnimInstance.h
...
UCLASS()
class ARENABATTLE_API UABAnimInstance : public UAnimInstance
{
...
public:
...
void SetDeadAnim() { IsDead = true; }
private:
...
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = Pawn, Meta = (AllowPrivateAccess = true))
bool IsDead;
};
ABAnimInstance.cpp
...
UABAnimInstance::UABAnimInstance()
{
...
IsDead = false;
...
}
void UABAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
...
if (!::IsValid(Pawn)) return;
if (!IsDead)
{
// CurrentPawnSpeed 설정
// IsInAir 체크
}
}
void UABAnimInstance::PlayAttackMontage()
{
ABCHECK(!IsDead);
Montage_Play(AttackMontage, 1.0f);
}
void UABAnimInstance::JumpToAttackMontageSection(int32 NewSection)
{
ABCHECK(!IsDead);
...
}
IsDead 가 true 일때는 어떤 동작도 기본 동작들을 수행하지 않도록 만든다. 이제 애님 그래프에 몽타주를 추가하자.
void UABAnimInstance::NativeUpdateAnimation(float DeltaSeconds)
{
Super::NativeUpdateAnimation(DeltaSeconds);
auto Pawn = TryGetPawnOwner(); //폰얻기
if (!::IsValid(Pawn)) return;
if(!IsDead)
{
CurrentPawnSpeed = Pawn->GetVelocity().Size();
auto Character = Cast<ACharacter>(Pawn);
if (Character)
{
IsInAir = Character->GetMovementComponent()->IsFalling();
}
}
}
IsDead 변수로 몽타주 재생 여부를 경정한다.
또한 WarriorDead 에서 Loop Animation 을 끈다. 죽는 애니메이션은 한 번만 재생할 것이기 때문이다.
마지막으로, ABCharacter.cpp 에서 TakeDamge 함수를 수정한다.
ABCharacter.cpp
...
float AABCharacter::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
...
if (FinalDamage > 0.0f)
{
ABAnim->SetDeadAnim();
SetActorEnableCollision(false);
}
return FinalDamage;
}
FinalDamage 가 0보다 크면, 즉 공격당하면 AnimInstance의 SetDeadAnim()을 호출하면 IsDead=true로 되고 죽는 애니메이션을 애님블루프린트에서 재생하고, SetActorEnableCollision 값을 false 로 설정해 죽은 이후 해당 액터의 충돌 설정을 끈다.
죽은 이후에는 이제 Hit 판정이 나지 않는다!
참고: 라인트레이스
FVector startPos = tpsCamComp->GetComponentLocation();
FVector endPos = tpsCamComp->GetComponentLocation() + tpsCamComp->GetForwardVector() * 5000.f;
FHitResult hitInfo;
FCollisionQueryParams params;
params.AddIgnoredActor(this);
bool bHit = GetWorld()->LineTraceSingleByChannel(hitInfo, startPos, endPos, ECC_Visibility, params);
if (true == bHit)
{
FTransform bulletTrans;
bulletTrans.SetLocation(hitInfo.ImpactPoint);
UGameplayStatics::SpawnEmitterAtLocation(GetWorld(), bulletEffectFactory, bulletTrans);
}
'언리얼C++게임개발 > 09.충돌 설정과 대미지 전달' 카테고리의 다른 글
언리얼 엔진에서의 충돌 설정 (0) | 2024.01.30 |
---|