[UE4] Hit Color, Hit Stop (HitData, TakeDamage, FDamageEvent, SetTimer, FTimerHandle. IsNearlyZero, CustomTimeDilation)
🔥 Hit 처리
이전에는 플레이어가 공격했을 때 Sword의 콜리전에 Enemy가 충돌되어 OnComponentBeginOverlap 함수를 호출시키는 과정까지 구현하였다.
이번에는 Enemy가 Hit 됐을때의 기능들을 하나씩 구현해보았다.
🔎 HitData 구조체 생성
HitData는 WeaponStructures 클래스에 생성하였다. (WeaponStructure는 공격에 필요한 구조체, 함수를 모아둔 UObject Class)
Hit 됐을때 출력될 몽타주와 데미지 수치(Power), 밀려나는 정도(Launch), Stop 모션을 구현할 시간(StopTime), Sound, Effect 등이 있다.
추가로 WeaponStructures 클래스에서 데미지를 전달하는 SendDamage 함수와 PlayMontage 함수, Stop모션을 구현하는 PlayHitStop 함수를 선언하였다.
🔎 Send Damage
블루프린트에서는 데미지를 구현할 때 Apply Damage 노드와 Any Damage 노드를 함께 사용했었다.
이제 C++로 구현할 때는 두 개의 함수를 둘 다 호출해야 하는 것이 아닌, 하나의 함수만 호출하면 된다.
BP 상에서의 AnyDamage 노드는 ReceiveAnyDamage라는 함수로 작성되어 있다.
ReceiveAnyDamage 함수가 호출되는 곳은 OnTakeAnyDamage 라는 델리게이트의 함수들을 모두 호출하며
이렇게 ReceiveAnyDamage 함수를 호출하는 함수는 TakeDamage라는 함수이다.
TakeDamage 함수의 원형을 보면, float을 반환하는 virtual 함수로 정의되어 있다.
즉, 이것들을 정리해보면, Apply Damage를 호출하면 → 결국 TakeDamage를 호출하게 된다.
(Apply Damage == ReceiveAnyDamage → TakeDamage에서 호출)
따라서 C++ 클래스에서 데미지 기능을 구현할 때는 TakeDamage 함수를 호출하여 사용하면 된다.
데미지를 입을 InOther 캐릭터에 TakeDamage 함수를 호출하여 매개변수로 데미지 값(Power), FDamageEvent, 컨트롤러, Causer를 넘겨주면 된다.
두번째 FDamageEvent 대신에 FActionDamageEvent가 들어갔는데, 이는 바로 아래에서 설명하였다.
📄 FDamageEvent
두번째 매개변수인 FDamageEvent 구조체를 살펴보면 소멸자가 가상함수로 작성되어 있는것을 확인할 수 있다.
또한 같은 클래스 내의 다른 구조체를 보면 FDamageEvent를 상속받아 PointDamageEvent 구조체를 새로 생성하여 Point Damage를 구현하였다.
이는 곧 FDamageEvent를 상속받아 새로운 클래스를 작성하여 사용할 수 있고, 필요한 기능이나 정보가 있다면 FDamageEvent를 재정의하여 사용하라는 의미이다. (블루프린트에서는 구조체 상속이 불가능하기 때문에 이런 구조의 기능 구현이 불가능하다)
이처럼, 나도 현재 캐릭터에 입히는 Damage 기능에 맞춰 DamageEvent를 생성하여 사용하였다.
FDamageEvent를 상속받는 FActionDamageEvent 구조체를 정의하였고, 그 안에 Hit Data를 선언해두었다.
따라서 TakeDamage의 매개변수로 FDamageEvent가 아닌 FActionDamageEvent를 전달한 것이고, FActionDamageEvent의 HitData를 this로 초기화하여, 자기 자신의 HitData 정보를 전달하였다.
🔎 Enemy의 데미지 처리 (Damage를 받는 쪽)
이러한 데미지처리를 받는 쪽(InOther)인 Enemy에서도 Hit 기능을 구현해주었다. (물론 Player가 공격받을때도 동일하다)
Enemy에서는 TakeDamage 함수를 재정의하여 사용한다.
데미지를 입힐 때 Enemy의 내부적으로만 필요한 정보를 저장하여 사용할 수 있는 FDamageData 구조체를 생성하였다.
TakeDamage는 위에서 확인했다시피 float을 Return하는데, 부모에서 데미지를 가감하거나 연산했던 결과를 받아서 처리할 수 있게 하기 위함이다.
따라서 float damage 변수에 TakeDamage 결과를 먼저 저장하고, DamageData구조체의 정보들을 초기화한 후 다시 damage를 float형으로 Return해주었다.
그밖에 TakeDamage 함수에서는 데미지를 받는 객체의 상태(State)를 Hitted Mode로 설정하고, 매개변수로 받아온 const& 이벤트를 형변환(FActionDamageEvent*)하여 Damage의 Event로 설정해주었다.
Enemy의 상태가 변할때마다 호출되도록 델리게이트에 등록해놓은 OnStateTypeChanged 함수에서는 Hitted 상태가 됐을 때 Hitted 함수를 호출하도록 하였다.
💥 Hit Color 기능
Hitted 함수에서는 HP 관리와, 사망처리, Hit Color 처리, Hit Stop 처리 등을 해야하는데, 우선적으로 구현할 기능들을 먼저 구현해놓고, 추후에 구현할 것들은 TODO 명령어로 지정해두었다.
Hit Color 기능은 데미지를 입었을 때 일시적으로 Mesh를 빨간색으로 변경한 뒤, 일정시간이 지나면 원상복구되는 기능이다.
IICharacter 인터페이스에서 만들어두었던 Change_Color 함수로 Mesh의 Material들을 Red로 변경해준 뒤, 0.2초 뒤 원래의 색상으로 돌려놓는 SetTimer 함수를 사용하였다.
SetTimer를 사용하기 위해선 Timer 핸들이 필요하기 때문에 FTimerHandle을 하나 변수로 생성하였고, TimerDelegate로 RestoreColor라는 색상 원상복구 함수를 작성하고, 등록해주었다.
RestoreColor 함수에서는 Mesh의 색상을 다시 OriginColor로 변경한 뒤, 사용한 Timer Handle을 제거해주었다.
💡 Timer 사용 시 주의사항
Timer가 설치되어있는 상황(SetTimer된 상황)에서 Destroy가 진행되면 TimerManager에 Timer는 주소가 NULL이 된 채 남아있는다.
이때 Set Timer가 Loop였다면 다시 Timer에 접근하게 되면서 프로그램이 터지게 된다.
따라서 반드시 사용이 완료된 Timer Handle은 Clear Timer로 제거시켜주어야 한다.
Hit Color 처리가 끝났다면, Hit Data에 있는 정보를 기반으로 나머지 기능을 수행한다.
💥 PlayMontage
HitData의 PlayMontage함수를 이용하여 Owner의 PlayAnimMontage를 실행시켰다.
💥 PlayHitStop
Hit Stop 기능은 공격으로 데미지를 입혔을 때 타격감을 주기 위해 잠시 공격이 일정시간 멈췄다가 다시 시간이 흘러가는것을 의미한다.
우선, Stop Time이 거의 0에 가깝다면 FMath 클래스의 IsNearlyZero 함수를 통해 기능을 수행하지 않도록 중단시켰다.
이후, 시간을 멈추고 다시 흐르도록 만들때, 게임 전체, 혹은 World상에 있는 액터 전체의 TimeDilation값을 조정하면 안된다.
게임은 게임대로 흘러가야하기 때문에, 현재 World상에 배치되어 있는 Actor들의 시간만 제어해야 하는데, GetCurrentLevel 함수를 통해 액터들을 불러오고, 이 액터들을 Pawn으로 형변환 해주었다.
형변환에 성공한 캐릭터들(Pawn들)의 CustomTimeDilation을 1 * 10^-3(1e-3f)로 설정하여 0.001초의 시간이 흐르도록 변경하였다.
이후, 시간을 변경한 Pawn 객체는 다시 시간을 돌려놔야 하기 때문에 따로 Pawns 배열에 저장하였고,
StopTime이 지난 후 원상복구시키기 위해 TimerDelegate에 람다함수식으로 함수를 등록해주었다.
🔎 Player의 DoAction_Combo → Enemy 로 Send Damage
이후 DoAction_Combo 클래스의 OnAttachmentBeginOverlap 함수에서 충돌이 시작됐을 때 HitDatas의 SendDamage 함수를 호출하여 Hit 처리가 되도록 하였다.
실제 HitData를 추가하기 위해 OneHand DataAsset의 HitDatas 엘리먼트를 추가해주었다.
컴파일 후 프로그램을 실행시켜보면 성공적으로 Hit 처리가 되는것을 볼 수 있었다.