ActorComponent
ActorComponent
는 다양한Component
를 가질 수 있다.
MeshComponent
,CameraComponent
,SpringArmComponent
,GroomComponent
등을 가질 수 있다.
Attribute Component
는 게임 오브젝트의 속성을 관리하고 추척하는데 이용되는데, 게임 내에서 일반적으로 HP, MP 등의 것들을 의미한다.
우선ActorComponent
를 생성한다.
Components
하위 폴더에AttributeComponent
를 생성한다.
- AttributeComponent.h
// AttributeComponent.h private: // Actor 현재 체력 UPROPERTY(EditAnywhere, Category = "Actor Attributes") float CurrentHealth; // Actor 최대 체력 UPROPERTY(EditAnywhere, Category = "Actor Attributes") float MaxHealth;
이제 Hp가 필요한 곳에서 include하여 사용할 수 있다.
- Enemy.h
// Enemy.h ... class UAtributeComponent; ... private: ... // Attribute class UPROPERTY(VisibleAnywhere) UAttributeComponent* Attributes;
- Enemy.cpp
// Enemy.cpp ... #include "Components/AttributeComponent.h" ... AEnemy::AEnemy() { ... Attributes = CreateDefaultSubobject<UAttributeComponent>(TEXT("Attributes")); ... }
컴파일 후 실행시
BP_Enemy
의Components
탭에서Attributes
가 추가된 것을 확인할 수 있다.
우측의Details
패널에서 체력 설정도 가능하다.
Widget
Blueprint에서 구현
체력바를 추가해야 체력을 확인할 수 있다.
User Widget
을 클릭하고WBP_HealthBar
로 파일명을 정해준다.
파일을 열고 우측 상단의Palette
에서Canvas Panel
을 검색한 후 하단의Hierarchy
에 드래그 드랍 해준다.
Canvas Paenl
은 Canvas 의미 그대로 가장 밑바탕이 되는 판이라고 보면 된다.
다시Palette
에서Progress Bar
를 검색한 후,Canvas Panel
에 드래그 드랍 해준다. 이때 이름은HealthBar
로 해준다.(그림에 오타 있음)
우측의Details
패널에Anchors
가 있는데, 기준점을 의미한다.
Anchors
기준으로 x축과 y축으로 0.5만큼 이동시키고, size를 280과 30으로 한다.
Percent
쪽에서는Progress Bar
의 퍼센테이지를 조정할 수 있다. 왼쪽부터 오른쪽으로 퍼센테이지가 적용되도록 할수도 있고, 중앙에서 양쪽으로 퍼센테이지 조정도 가능하다.
Appeaarance
에서는 색을 조정할 수 있다.
먼저 빌드파일에UMG
를 추가해 주어야 한다.
- Study.Build.cs
... PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore" , "GeometryCollectionEngine", "Niagara", "UMG"}); ...
Enmey 클래스에 WidgetComponent 를 추가한다.
- Enemy.h
// Enemy.h ... class UWidgetComponent; ... private: ... // WidgetComponent class UPROPERTY(VisibleAnywhere) UWidgetComponent* HealthBarWidget; ...
- Enemy.cpp
// Enemy.cpp ... #include "Components/WidgetComponent.h" ... AEnemy::AEnemy() { ... // WidgetComponent class HelthBarWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("HealthBarWidget")); HealthBarWidget->SetupAttachment(GetRootComponent()); ... }
컴파일 후 실행시
BP_Enemy
에HealthBarWidget
이 추가된 것을 확인할 수 있다.
우측의Details
패널에서Space
를Screen
으로(World
로 설정할 시 체력바가 고정되어 보임),Widget Class
를SBP_HealthBar
로 설정한 후Viewport
에서 위치를 적절히 조정한다.
실행하면 정상적으로 실행된다.
체력바가 조금 크다 싶으면 사이즈를 조절해준다.c++에서 구현
HUD
폴더에user widget
클래스를 생성한다.
- HealthBar.h
// HealthBar.h ... class UProgressBar; ... public: // meta = (BindWidget) : 블루프린트에 동일한 이름을 가진 위젯이 있다면 // 해당 위젯포인터로 코드(C++)에서 런타임에 엑세스 할 수 있다. UPROPERTY(meta = (BindWidget)) UProgressBar* HealthBar;
이제
WBP_HealthBar
의 부모 클래스를 방금 생성한HealthBar
클래스로 변경하기 위해 우측 상단의Graph
를 클릭한 후 중앙 상단의Class Settings
를 클릭한다.
좌측 하단의Details
패널에서Parent Class
에서 생성한HealthBar
클래스를 선택해준다.
Parent Class
가 생성한HealthBar
클래스로 변경 된 것을 확인할 수 있다.
이제 c++ 클래스로 퍼센테이지를 구현해야 한다.
구현하기전 확인해야 할게 있는데
Enemy
class에서UWidgetComponent
타입의HealthBarWidget
변수를 선언했고,BP_Enemy
에서 위젯 컴포넌트를 사용할 수 있게 했다.
BP_Enemy
의Details
패널에 가서 보면HealthBarWidget
은Widget class
를WBP_HealthBar
로 선택했다.
WidgetComponent.h
에 들어가보면GetWidgetClass()
가 있고 이를 통해WBP_HealthBar
를 사용할 수 있다.
우리가 사용하려는건 c++ 클래스인UserWidget
이므로 그 아래에 있는GetUserWidgetObject()
가 필요하다.
우선BP_Enemy
의WidgetComponent
인HealthBarWidget
이 퍼센테이지를 얻는 함수를 만들어야 한다.
먼저WidgetComponent
를 만들어야 한다.
All Classes
에서WidgetComponent
를 검색하고 next를 클릭한다.
HUD
폴더에 생성되도록 설정해주고 생성한다.
- HealthBarComponent.h
// HealthBarComponent.h ... class UHealthBar; ... public: void SetHealthPercent(float Percent); ... private: // UserWidget 타입 클래스 UHealthBar* HealthBarWidget;
함수를 선언하고 사용할
UHealthBar
타입의HealthBarWidget
도 선언한다.
- HealthBarComponent.cpp
// HealthBarComponent.cpp ... #include "Components/ProgressBar.h" ... void UHealthBarComponent::SetHealthPercent(float Percent) { // UserWidget을 사용하기 위해 GetUserWidgetObject()를 통해 가져온 UserWidget을 UHealthBar 타입으로 캐스팅 if(HealthBar == nullptr) { HealthBarWidget = Cast<UHealthBar>(GetUserWidgetObject()); } if(HealthBarWidget && HealthBarWidget->HealthBar) { // UHealthBar 타입의 HealthBarWidget에서 HealthBar에 접근해 SetPercent 함수 호출 HealthBarWidget->HealthBar->SetPercent(Percent); } }
이제
Enemy
클래스에서HealthBarComponent
클래스의SetHealthPercent
함수에 접근하여 퍼센테이지를 조절할 수 있도록 구현한다.
기존에 사용하기 위해 선언했던UWidgetComponent
타입의 변수들을 전부HealthBarcomponet
타입으로 변경한다.
- Enemy.h
// Enemy.h ... // class UWidgetComponent; -> class UHealthBarComponent로 변경 class UHealthBarComponent; ... private: // UPROPERTY(VisibleAnywhere) // UWidgetComponent* HealthBarWidget -> UHealthBarComponent* HealthBarWidget으로 변경 UHealthBarComponent* HealthBarWidget;
- Enemy.cpp
// Enemy.cpp ... #include "HUD/HealthBarComponent.h" // "Components/WidgetComponent.h" ... AEnemy::AEnemy() { ... // <UWidgetComponent> -> <UHealthBarComponent> 변경 HealthBarWidget = CreateDefaultSubobject<UHealthBarComponent>(TEXT("AttributeComponent")); ... } ... void AEnemey::BeginPlay() { Super::BeginPlay(); if(HealthBarWidget) { HealthBarWidget->SetHealthPercent(.1f); } }
실행시
SetHealthPercent
함수의 파라미터값에 따라 퍼센테이지가 조절되는 것을 확인할 수 있다.
데미지 설정
언리얼에는
TakeDamage
와ApplyDamage
가 있다. 이를 이용하면 보다 편리하게 데미지 관련 구현을 할 수 있다.
TakeDamage 관련 API
ApplyDamage 관련 API
알법한 것들은 제외하고 나머지만 요약하자면
- TakeDamage
- FDamageEvent& DamageEvent : 추가적인 데미지 관련 데이터
- AController* Instigator : 공격을 가한 주체에 대한 정보
- AActor* DamageCauser : 검으로 공격당했다면 검이 DamageCauser가 됨
- ApplyDamage
- AActor* DamagedActor : 피해를 입는 액터
- AController* EventInstigator : 공격을 가한 주체에 대한 정보
- TSubclassOf\ DamageTypeClass : 언리얼엔진에서는 DamageType이 있고, 다양한 피해 유형을 정의하여 사용할 수 있음
ApplyDamage
와TakeDamage
는 서로 보완적으로 작동하여 데미지 처리 동작을 한다.
Weapon
클래스에서OnBoxBeginOverlap()
함수 실행시 Hit관련 판정이 발생하므로 여기에ApplyDamage()
함수 사용시 적에게 데미지를 줄 수 있다.
먼저Enemy
클래스에서 TakeDamage() 함수를 override한다.
AItem의 경우 AActor를 상속받으므로, AActor에서 f12를 누르고TakeDamage
를 검색하면 찾을 수 있다.
- Enemy.h
// Enemy.cpp ... public: ... virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override; ...
Enemy
클래스에서TakeDamage()
를 선언하였으니,Weapon
클래스에서ApplyDamage()
를 호출할 차례이다.
먼저 다양한 무기와 다양한 데미지가 필요할 수 있으므로Weapon
클래스에 Damage 관련 변수를 추가해주고,EquipWeapon()
실행시Owner
와Instigator
를 설정해주도록 변경한다.
- Weapon.h
// Weapon.h public: ... void EquipWeapon(USceneComponent* InParent, FName InSocketName, AActor* NewOwner, APawn* Instigator) ... private: ... // 무기 데미지 UPROPERTY(EditAnywhere, Category = "Weapon Properties") float Damage = 20.f; ...
- Weapon.cpp
// Weapon.cpp ... void AWeapon::EquipWeapon(USceneComponent* InParent, FName InSocketName, AActor* NewOwner, APawn* Instigator) { SetOwner(NewOwner); SetInstigator(NewInstigator); ... } ... void AWeapon::OnBoxBeginOverlap(...) { ... if(CollisionBoxHit.GetActor()) { ... // CollisionBoxHit 대상에게 데미지를 가함 UGameplayStatics::ApplyDamage(CollisionBoxHit.GetActor(), Damage, GetInstigator()->GetController(), this, UDamageType::StaticClass()); } }
EquipWeapon()
의 파라미터가 추가되었으니WraithCharacter.cpp
에서 파라미터를 추가해 주어야 한다.
- WraithCharacter.cpp
// WraithCharacter.cpp ... void AWraithCharacter::PickUp() { ... if(OverlappingWeapon) { OverlappingWeapon->Equip(GetMesh(), FName("RightHandSocket"), this, this); ... } }
이제 이를 처리할
TakeDamage()
를 구현해야 한다.
그전에 데미지를 받으면 체력의 변화가 생기므로AttributeComponent
에 접근해 체력에 관한 값을 받아오거나 건들 수 있어야 한다.
private에 있으므로getter
와setter
를 생성한다.
- AttributeComponent.h
// AttributeComponent.h public: // CurrentHealth Setter() void ReceiveDamage(float Damage); // 체력 퍼센트 Getter() float GetHealthPercent();
- AttributeComponent.cpp
... // CurrentHealth Setter() void UAttributeComponent::ReceiveComponent(float Damage) { // 체력이 0 미만으로 내려가지 않도록함 CurrentHealth = FMath::Clamp(CurrentHealth - Damage, 0.f, MaxHealth); } // 체력 퍼센트 Getter() void UAttributeComponent::GetHealthPercent() { return Health / MaxHealth; }
- Enemy.cpp
// Enemy.cpp float AEnemy::TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) { if(Attributes && HealthBarWidget) { Attributes->ReceiveDamage(DamageAmount); HealthBarWidget->SetHealthPercent(Attributes->GetHealthPercent()); } return DamageAmount; }
실행시 공격을 받으면 HP가 깎이는 것을 확인할 수 있다.
Custom HealthBar
필요에 따라 에셋을 이용한 체력바를 이용할 수도 있다.
사용할 에셋을import
한다.
HUD
에서 위젯으로 사용할 것이므로import
한 에셋 둘 다Compression Settings
를UserInterface2D(RGBA)
로 변경한다.
WBP_HealthBar
에서Fill Image
를 다운받은 에셋으로 변경한다.
기존에 적용되었던 색은opacity
를 조절하여 보이지 않게 한다.
BackGround
->Tint
에서Alpha
값을 0으로 하여 투명하게 만든다.
체력바의 윤곽선을 적용시키기 위해Palette
패널에서Image
를 드래그드랍하여 추가한다.
우측의Apperance
->Brush
->Image
에 에셋을 적용시킨다.
크기를 적당히 조절하여 맞춘다.
HealthBar의Slot
을 참조하면 쉽게 맞출 수 있다.
컴파일 후 실행하면 정상적으로 적용된 모습을 확인할 수 있다.
위치가 조금 애매하므로 머리 위에 표시되도록BP_Enemy
에서 조정해준다.
Enemy Death Animation
체력바도 추가되었고, 데미지도 받도록 추가되었으니 이제 체력이 0이 되었을 때 Death Animation이 재생되도록 해야 한다.
먼저 필요한 애니메이션을 다운받는다.
root
가 없을 경우 블렌더를 통해 생성한다.
SK_Paladin
을 선택하고,Mesh
의Import Mesh
를 비활성화시킨 후Import All
을 클릭한다.
AM_Death
애니메이션 몽타주를 생성하고
애니메이션을 집어넣고, 몽타주 섹션을 나눈 다음, 모든 애니메이션의 연결을Clear
를 통해 끊어준다.
이제 캐릭터가 죽으면 몽타주를 재생하도록 코드를 작성해야 한다.
먼저AttributeComponent
클래스에서 캐릭터의 사망 여부를 판정할 수 있는 bool 타입 함수를 하나 만든다.
- AttributeComponent.h
// AttributeComponent.h ... public: ... // 생존 여부 리턴함수 bool IsAlive();
- AttributeComponent.cpp
// AttributeComponent.cpp ... // 생존 여부 리턴함수 bool UAttributeComponent::IsAlive() { // 체력이 0 초과시 참 리턴 return CurrentHealth > 0.f; }
Enemy
클래스에서GetHit_Implementation
함수 호출시 피격관련 애니메이션과 사운드를 재생하는데, 해당 부분에서AttributeComponent
의IsAlive
함수를 호출하여 값에 따라 피격 애니메이션 또는 사망 애니메이션을 재생하도록 변경한다.
그전에 먼저Weapon
클래스에서ApplyDamage
의 위치를 변경해야한다.
(피격 애니메이션 재생(체력20)->ApplyDamage
로 인해 데미지20-> 체력0이지만 GetHit 함수 발동해야 체력 0 여부 확인가능하므로 한번더 피격되야지 사망 애니메이션 재생)
코드 위치를 변경시킨다.
Weapon
클래스에서GetHit
이 동작하게 되면Enemy
클래스에서GetHit_Implementation
이 실행되므로 해당 부분에서 체력을 확인하고 피격애니메이션 및 사운드 재생 or 사망애니메이션 재생을 하도록 코드를 수정한다.
- Enemy.h
// Enemy.h ... protected: ... void Die(); private: ... UPROPERTY(EditDefaultOnly, Category = "Montage") UAnimMontage* DeathMontage;
- Enemy.cpp
// Enemy.cpp ... void AEnemy::Die() { // DeathMontage 플레이 UanimInstance* AnimInstance = GetMesh()->GetAnimInstance(); if(AnimInstance && DeathMontage) { AnimInstance->Montage_Play(DeathMontage)); const int32 SectionNumber = FMath::RandRange(0, 5); FName SectionName = FName(); switch(SectionNumber) { case 0: SectionName = FName("Death1"); break; case 1: SectionName = FName("Death2"); break; case 2: SectionName = FName("Death3"); break; case 3: SectionName = FName("Death4"); break; case 4: SectionName = FName("Death5"); break; case 5: SectionName = FName("Death6"); break; default: break; } AnimInstance->Montage_JumpToSection(SectionName, DeathMontage); } } ... void AEnemy::GetHit_Implementation(const FVector& ImpactPoint) { // Attributes 유효성 검사 및 생존여부 확인 if(Attributes && Attributes->IsAlive) { DirectionalHitReact(ImpactPoint); } else { Die(); } }
BP_Enemy
에서DeathMontage
를 추가해주고 실행하면 애니메이션 몽타주가 재생되는 것을 확인할 수 있다.
Animation Bluepirnt
에서 실제 캐릭터가 죽었는지 확인할 방법이 없으므로DeathMontage
가 끝나자마자Idle
포즈로 돌아가는 문제가 발생한다.
해결하기 위해서 먼저 죽어 있는 상태의 단일 프레임을 이용한 애니메이션 에셋을 제작해아한다.
다운받은 애니메이션 모두 끝까지 재생시키고 마지막 쓰러져 있는 부분에서 정지시킨 뒤, 좌측 상단의Create Asset
->Create Animation
->Current Pose
를 클릭하는 과정을 반복한다..
경로를 지정해주고, 저장한다.
ABP_Paladin
에서State Machine
을 추가해준다.
Main State
를 더블클릭후,Idle
과Dead
State 를 추가해 준다.
각 Death 애니메이션에 맞는 Dead 애니메이션을 재생하기 위해 기존에 생성해둔CharacterTypes
클래스에ENUM
을 하나 생성한다.
- CharacterTypes.h
// CharacterTypes.h ... // Paladin DeathAnimation에 따른 DeadAnimation UENUM(BlueprintType) enum class EDeathPose : uint8 { EDP_Alive UMETA(DisplayName = "Alive"), EDP_Death1 UMETA(DisplayName = "Death1"), EDP_Death2 UMETA(DisplayName = "Death2"), EDP_Death3 UMETA(DisplayName = "Death3"), EDP_Death4 UMETA(DisplayName = "Death4"), EDP_Death5 UMETA(DisplayName = "Death5"), EDP_Death6 UMETA(DisplayName = "Death6") }; ...
Enemy
클래스도 수정한다.
- Enemy.h
// Enemy.h ... #include "Characters/CharacterTypes.h" ... private: ... // 캐릭터 사망모션 종류 열거형 UPROPERTY(BlueprintReadOnly, meta = (AllowPrivateAccess = "true")) EDeathPose DeathPose = EDeathPose::EDP_Alive; ...
- Enemy.cpp
// Enemy.cpp void AEnemy::Die() { ... switch(SectionNumber) { case 0: SectionName = FName("Death1"); DeathPose = EDeathPose::EDP_Death1; break; case 1: SectionName = FName("Death2"); DeathPose = EDeathPose::EDP_Death2; break; case 2: SectionName = FName("Death3"); DeathPose = EDeathPose::EDP_Death3; break; case 3: SectionName = FName("Death4"); DeathPose = EDeathPose::EDP_Death4; break; case 4: SectionName = FName("Death5"); DeathPose = EDeathPose::EDP_Death5; break; case 5: SectionName = FName("Death6"); DeathPose = EDeathPose::EDP_Death6; break; default: break; } ... } ...
이제
ABP_Enemy
는 해당 폰이 소유하고 있는 데이터가 필요하다.
Try Get Pawn Owner
을 통해Enemy
를 Cast하여 해당 변수를 이용해야 한다.
Event Blueprint Update Animation
은 매 프레임 실행되므로,Event Begin Play
와 같은 노드가 필요하다.
좌측 하단의FUNCTION
에서Blueprint Initialize Animation
을 선택하면Event Begin Play
와 동일한 역할을 하는 이벤트노드를 생성할 수 있다.
Cast To BP_Enemy
노드를 연결하고,As BP Enemy
핀을 우클릭하면 결과를 변수로 저장할 수 있다.
Enemy
로 이름을 바꿔준다.
Transition Rule
에서 캐스팅한Enemy
를 통해 바로DeathPose
에 접근하여 포즈에 따라 애니메이션을 재생시키는 방법도 있지만 그런 것들은Anim Graph
에서 하는것 보단Event Graph
에서 해당 과정을 통해 변수로 따로 만들고, 해당 변수를 통해 애니메이션을 재생시키는 편이 훨씬 깔끔하다.
두가지 방법이 있다.1. 일반적인 방법
Enemy
노드를 우클릭하고Convert to Validated Get
을 클릭하여Event Blueprint Update Animation
노드와 연결한다.
Death Pose
핀을 우클릭하여Promote to Variable
을 클릭하여 변수를 하나 생성한다.2. 언리얼5에 새로 도입된 멀티스레딩 활용
애니메이션 블루프린트에서 다른 애니메이션 블루프린트를 이용할 수 있다.
하지만 메인 애니메이션 블루프린트의Event Graph
에서 하나씩만 실행되기 때문에 병목현상이 발생할 수 있다고 한다.
이때 멀티스레딩을 이용해 여러 함수를 동시에 호출하는 방법을 사용하는 방법을 이용한다고 한다.
여러 애니메이션 블루프린트가 있고 각 블루프린트에 대해 동시에 변수 업데이트를 진행하면 하나씩 진행하는것보다 성능이 향상되는 원리이다.
여기서부터는 번역을 통한 서술이므로 문장이 매끄럽지 않을 수 있음.
좌측 하단의FUNCTION
에서Blueprint Thread Safe Update Animation
을 선택한다.
해당 함수는Event Blueprint Update Animation
노드와 같이 매 프레임마다 호출되는데, 다른 애니메이션 블루프린트 함수와 병렬로 호출되거나 동시에 호출될 수 있는 방식으로 변수값에 접근해서 안전하게 처리한다 라고 한다.
해당 함수에서 작업되는 것들은thread safe
해야 한다고 한다.
(구글링한 결과: 스레드 안전(thread 安全, 영어: thread safety)은 멀티 스레드 프로그래밍에서 일반적으로 어떤 함수나 변수, 혹은 객체가 여러 스레드로부터 동시에 접근이 이루어져도 프로그램의 실행에 문제가 없음을 뜻한다.)
즉, 기존의 방식대로Enemy
에 직접 접근하여DeathPose
를 호출하지 않는다.
동일한 캐릭터에 대해 동시에 호출되는 다른 애니메이션 블루프린트에 이 함수의 여러버전이 있는 경우, 병렬로 호출되는 해당 버전중 하나에서 이 버전에disagree
하는 버전이 있을 수 있기 때문이다.
그래서 모든 관련 데이터는Proxy Data Structure
에 cached 되는데, 우리는 프록시의 복사본을 통해서Enemy
관련된 값들에 접근할 수 있다.
Thread Safe
가 확인되면 업데이트 프레임에 대한 애니메이션 함수가 완료되고, 프록시가 업데이트된다. 그러면 멀티스레드 기능이 다른 기능과 충돌하는 데이터에 엑세스하지 않는다.
간단하게 정리하자면
1. 멀티스레드 기능을 이용해 여러 블루프린트에서 호출되는 변수값들에 안전하게 접근해 처리한다.
(예를 들어 공중부양 여부 관련 변수를 다양한 애니메이션 블루프린트에서 이용할 경우, 멀티스레드를 통해 안전하게 접근해 관련 데이터를 처리)
2. 이러한 것들은Thread Safe
해야 한다(여러 스레드로부터 동시 접근이 이뤄지더라도 프로그램의 실행에 문제가 없어야함).
3. 이러한 멀티스레드 특성때문에 기존 방식처럼 직접적으로 접근해 값을 변경하는 것은 불가능
4. 그러므로Proxy Data Structure
에 관련된 것들을 저장하고, 복사본을 통해 멀티스레드 기법을 이용하여 처리함.
먼저 프록시에 접근하기 위해Property Access
노드를 검색하여 추가한다.
해당 노드를 통해Enemy
의DeathPose
에 접근 가능하다.
이 값을 이용해ABP_Enemy
에 존재하는DeathPose
변수값을setter
함수를 이용해 할당한다.
Idle
->Dead
로 가는Transition Rule
은DeathPose
가Alive
가 아닐 경우로 지정해준다.
Idle
애니메이션을Idle State
에 추가해주고
Blend Poses(EDeathPose)
노드를 이용해 포즈를 블렌딩한다.
Idle
포즈에서 사망 애니메이션으로 넘어갈 때 블렌딩 타임을 0으로 하면 움찔거림 없이 부드럽게 애니메이션이 넘어간다.
Enemy 사망관련 추가 수정
- Enemy 사망시
Capsule Component
가 남아있어 충돌이 발생하는 문제
- Enemy.cpp
// Enemy.cpp ... void AEnemy::Die() { ... // 사망시 CapsuleComponent 충돌 삭제 GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision); }
CapsuleComponent
와 더이상 충돌하지 않는다.
2. Enemy 사망시 일정시간 후에 사라지도록 구현
- Enemy.cpp
// Enemy.cpp ... void AEnemy::Die() { ... // 사망시 CapsuleComponent 충돌 삭제 GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision); // 사망시 일정시간 후에 삭제 SetLifeSpan(3.f); }
- 체력바 표시 조건 추가
기본적으로 보이지 않지만, 공격시 표시되고 조건에 따라 체력바 다시 숨기기 구현
- Enemy.cpp
// Enemy.cpp ... void AEnemy::BeginPlay() { ... // 기본적으로 HealthBarWidget 숨김처리 if(HealthBarWidget) { HealthBarWidget->SetVisibility(false); } ... } ... void AEnemy::GetHit_Implementatioin(const FVector& ImpactPoint) { // 피격시 HealthBarWidget 표시 if(HealthBarWidget) { HealthBarWidget->SetVisibility(true); ... } } ...
조건을 정하기 전에, 피격시 전투중인 대상에 대한 정보를 가지도록 변수를 만들어야 한다.
- Enemy.h
// Enemy.h ... private: ... // 전투중인 대상 UPROPERTY() AActor* CombatTarget; // 전투 반경 UPROPERTY(EditAnywhere) double CombatRadius = 500.f;
데미지를 받을 때, 데미지를 주는
Instigator
와DamageCauser
를 알 수 있다.
- Enemy.cpp
// Enemy.cpp ... void AEnemy::Tick(float DeltaTime) { Super::Tick(DeltaTime); if(CombatTarget) { const double DistanceToTarget = (CombatTarget->GetActorLocation() - GetActorLocation()).Size(); if(DistanceToTarget > CombatRadius) { CombatTarget = nullptr; if(HealthBarWidget) { HealthBarWidget->SetVisibility(false); } } } } ... float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) { ... // 피격시 EventInstigator를 CombatTarget으로 지정 CombatTarget = EventInstigator->GetPawn(); return DamageAmount; }
거리가 멀어지면 체력바가 보이지 않게 된다.
마지막으로Enemy
사망시에도 체력바가 남아있다. 물론 위에서 설정한LifeSpan
에 따라 체력바도 사라지지만, 체력이 0이 되자마자 보이지 않게 구현하려고 한다.
- Enemy.cpp
// Enemy.cpp ... void AEnemy::Die() { ... if(HelathBarWidget) { HealthBarWidget->SetVisibiliry(false); } ... }
컴파일 후 실행시 더이상 사망후에 체력바가 보이지 않는다.