적(Enemy)의 피격과 Behavior Tree가 연결되어 있지 않다.  적이 피격 되었을 때 Behavior Tree에 알리고 피격이 끝났을 때 Behavior를 WaitMode로 변경하는 코드를 추가하였다.

 

 

 

 


 

 

 

 
Plugins
 
  Weapon
 
    Resource
 
      Icon128.png
weapon_thumbnail_icon.png
 
    Source
 
      Weapon  
      SWeaponCheckBoxes.h .cpp
SWeaponDetailsView.h .cpp

SWeaponDoActionData.h .cpp
SWeaponEquipmentData.h .cpp
SWeaponHitData.h .cpp

SWeaponLeftArea.h .cpp
Weapon.Build.cs
WeaponAssetEditor.h .cpp
WeaponAssetFactory.h .cpp
WeaponCommand.h .cpp 
WeaponContextMenu.h .cpp
WeaponModule.h .cpp
WeaponStyle.h .cpp 
 
   

 

 
Source
    BehaviorTree
    CBTService_Melee.h .cpp
CBTTaskNode_Action.h .cpp
CBTTaskNode_Equip.h .cpp
CBTTaskNode_Hitted.h .cpp 생성

CBTTaskNode_Patrol.h .cpp
CBTTaskNode_Speed.h .cpp
CPatrol.h .cpp
    Characters
    CAIController.h .cpp
CAnimInstance.h .cpp
CEnemy.h .cpp 
CEnemy_AI.h.cpp
CPlayer.h.cpp
ICharacter.h .cpp
    Components
    CAIBehaviorComponent.h .cpp
CFeetComponent.h .cpp

CMontagesComponent.h .cpp 
CMovementComponent.h .cpp 
CStateComponent.h .cpp
CStatusComponent.h .cpp  
CWeaponComponent.h .cpp 
CZoomComponent.h .cpp
    Notifies
    CAnimNotifyState_BeginAction.h .cpp
CAnimNotifyState_BowString.h .cpp
CAnimNotify_CameraShake.h .cpp 
CAnimNotify_End_Parkour.h .cpp
CAnimNotifyState_EndAction.h .cpp
CAnimNotify_EndState.h .cpp
CAnimNotifyState.h .cpp
CAnimNotifyState_CameraAnim.h .cpp
CAnimNotifyState_Collision.h .cpp 
CAnimNotifyState_Combo.h .cpp
CAnimNotifyState_Equip.h .cpp
CAnimNotifyState_SubAction.h .cpp
    Parkour
    CParkourComponent.h .cpp
    Utilities
    CHelper.h
CLog.h .cpp
    Weapons
    CArrow.h .cpp
CAura.h .cpp
CCamerModifier.h .cpp
CGhostTrail.h .cpp
CRotate_Object.h .cpp
CThornObject.h .cpp
CAnimInstance_Bow.h .cpp
CAttachment_Bow.h .cpp
CDoAction_Around.h .cpp
CDoAction_Bow.h .cpp
CDoAction_Combo.h .cpp
CDoAction_Warp.h .cpp
CSubAction_Around.h .cpp
CSubAction_Bow.h .cpp
CSubAction_Fist.h .cpp
CSubAction_Hammer.h .cpp
CSubAction_Sword.h .cpp
CDoAction_Warp.h .cpp

CAttachment.h .cpp
CDoAction.h .cpp
CEquipment.h .cpp
CSubAction.h .cpp
CWeaponAsset.h .cpp
CWeaponData.h .cpp
CWeaponStructures.h .cpp
    Global.h
CGameMode.h .cpp
U2212_06.Build.cs
    U2212_06.uproject
 

 

 

 

 

Hitted 구현하기

 

 


 

 

CBTTaskNode_Hitted 생성

 

새 C++ 클래스 - BTTaskNode - CBTTaskNode_Hitted 생성

 

 

CBTTaskNode_Hitted.h

더보기
#pragma once
#include "CoreMinimal.h"
#include "BehaviorTree/BTTaskNode.h"
#include "CBTTaskNode_Hitted.generated.h"
UCLASS()
class U2212_06_API UCBTTaskNode_Hitted : public UBTTaskNode
{
GENERATED_BODY()
public:
UCBTTaskNode_Hitted();
protected:
virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
virtual void TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds) override;
};

 

 

 

CBTTaskNode_Hitted.cpp

더보기
#include "BehaviorTree/CBTTaskNode_Hitted.h"
#include "Global.h"
#include "Characters/CEnemy_AI.h"
#include "Characters/CAIController.h"
#include "Components/CStateComponent.h"
UCBTTaskNode_Hitted::UCBTTaskNode_Hitted()
{
bNotifyTick = true;
NodeName = "Hitted";
}
EBTNodeResult::Type UCBTTaskNode_Hitted::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
Super::ExecuteTask(OwnerComp, NodeMemory);
ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());//Enemy의 controller 캐스팅하여 가져온다.
controller->StopMovement();//맞은 순간 Enemy를 멈추게 해준다.
return EBTNodeResult::InProgress;//맞으면서 동작 플레이가 되므로 InProgress
}
void UCBTTaskNode_Hitted::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
ACAIController* controller = Cast<ACAIController>(OwnerComp.GetOwner());
ACEnemy_AI* ai = Cast<ACEnemy_AI>(controller->GetPawn());
UCStateComponent* state = CHelpers::GetComponent<UCStateComponent>(ai);
if (state->IsHittedMode() == false)//Hitted가 끝나면
{
//Task를 Succeeded로 만들어주고 리턴한다.
FinishLatentTask(OwnerComp, EBTNodeResult::Succeeded);
return;
}
}

 

 

 

 


 

 

 

CEnemy

 

CEnemy.h

더보기
#pragma once
#include "CoreMinimal.h"
#include "Characters/ICharacter.h"
#include "Components/CStateComponent.h"
#include "GameFramework/Character.h"
#include "CEnemy.generated.h"
UCLASS()
class U2212_06_API ACEnemy : public ACharacter, public IICharacter //다중상속
{
GENERATED_BODY()
private:
UPROPERTY(EditAnywhere, Category = "Color")
FLinearColor OriginColor = FLinearColor::White;//색상 설정.
protected:
UPROPERTY(VisibleAnywhere)
class UCMontagesComponent* Montages;
UPROPERTY(VisibleAnywhere)
class UCMovementComponent* Movement;
UPROPERTY(VisibleAnywhere)
class UCStateComponent* State;
UPROPERTY(VisibleAnywhere)
class UCStatusComponent* Status;
public:
ACEnemy();
public:
float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, class AController* EventInstigator, AActor* DamageCauser) override;//TakeDamage 오버라이드하여 재정의.
protected:
virtual void BeginPlay() override;
private:
UFUNCTION()
void OnStateTypeChanged(EStateType InPrevType, EStateType InNewType);
protected:
virtual void Hitted();//자식에서 재정의 할 수 있도록 virtual로 만든다.
public:
virtual void End_Hitted() override;//ICharacter의 가상함수 오버라이드. CEnemy_AI에서 재정의하도록 virtual.
private:
UFUNCTION()
void RestoreColor();
private:
void Dead();
public:
void End_Dead() override;//ICharacter의 가상함수 오버라이드.
private:
struct FDamageData
{
float Power;
class ACharacter* Character;
class AActor* Causer;
struct FActionDamageEvent* Event; //CWeaponStructure의 FActionDamageEvent
} Damage; //Damage란 이름으로 저장.
private:
FTimerHandle RestoreColor_TimerHandle;//변한 색이 일정시간 후에 되돌아오도록 시간을 기록하는 변수.
};

자식 클래스(CEnemy_AI)에서 재정의할 수 있도록 virtual 함수로 변경

  • 변경 전: 
    • void Hitted();
    • void End_Hitted() override;
  • 변경 후: 
    • virtual void Hitted();
    • virtual void End_Hitted() override;
      • ICharacter의 가상함수 오버라이드. CEnemy_AI에서 재정의하도록 virtual.

 

 

 

CEnemy.cpp

더보기
#include "Characters/CEnemy.h"
#include "Global.h"
#include "CAnimInstance.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "Components/SkeletalMeshComponent.h"
#include "Components/CapsuleComponent.h"
#include "Components/CMontagesComponent.h"
#include "Components/CMovementComponent.h"
#include "Components/CStatusComponent.h"
#include "Weapons/CWeaponStructures.h"
ACEnemy::ACEnemy()
{
CHelpers::CreateActorComponent<UCMontagesComponent>(this, &Montages, "Montage");
CHelpers::CreateActorComponent<UCMovementComponent>(this, &Movement, "Movement");
CHelpers::CreateActorComponent<UCStateComponent>(this, &State, "State");
CHelpers::CreateActorComponent<UCStatusComponent>(this, &Status, "Status");
GetMesh()->SetRelativeLocation(FVector(0, 0, -90));
GetMesh()->SetRelativeRotation(FRotator(0, -90, 0));
USkeletalMesh* mesh;
CHelpers::GetAsset<USkeletalMesh>(&mesh, "SkeletalMesh'/Game/Character/Mesh/SK_Mannequin.SK_Mannequin'");
GetMesh()->SetSkeletalMesh(mesh);
TSubclassOf<UCAnimInstance> animInstance;
CHelpers::GetClass<UCAnimInstance>(&animInstance, "AnimBlueprint'/Game/ABP_Character.ABP_Character_C'");
GetMesh()->SetAnimClass(animInstance);
GetCharacterMovement()->RotationRate = FRotator(0, 720, 0);
}
void ACEnemy::BeginPlay()
{
Super::BeginPlay();
Movement->OnWalk();//기본값 설정.
Create_DynamicMaterial(this);//IICharacter의 함수 사용. 색이 적용되는 객체를 this(여기서는 Enemy)로 설정.
Change_Color(this, OriginColor);//색상 할당.
State->OnStateTypeChanged.AddDynamic(this, &ACEnemy::OnStateTypeChanged);
}
void ACEnemy::OnStateTypeChanged(EStateType InPrevType, EStateType InNewType)
{
switch (InNewType)
{
case EStateType::Hitted: Hitted(); break;//실제처리는 아래의 Hitted()에서 일어난다.
case EStateType::Dead: Dead(); break;//실제처리는 아래의 Dead()에서 일어난다.
}
}
void ACEnemy::Hitted()
{
//Apply Damage 데미지 처리
{ //다음 타격 때 꼬이지 않게 하기위해 초기화 해준다.
Status->Damage(Damage.Power);
Damage.Power = 0;
}
//Change Color
{
Change_Color(this, FLinearColor::Red);
FTimerDelegate timerDelegate;
timerDelegate.BindUFunction(this, "RestoreColor");
GetWorld()->GetTimerManager().SetTimer(RestoreColor_TimerHandle, timerDelegate, 0.2f, false);//0.2초 동안 반복없이 실행.
}
//Damage는 CEnemy.h의 FDamageData이다.
if(!!Damage.Event && !!Damage.Event->HitData)
{
FHitData* data = Damage.Event->HitData;//FDamageData의 FActionDamageEvent* Event내의 HitData
data->PlayMontage(this); //몽타주 재생
data->PlayHitStop(GetWorld()); //HitStop 재생
data->PlaySoundWave(this);//소리 재생
data->PlayEffect(GetWorld(), GetActorLocation(), GetActorRotation());//Effect 재생
if (Status->IsDead() == false)
{
FVector start = GetActorLocation(); //start=현재 위치
FVector target = Damage.Character->GetActorLocation();//target=데미지를 전달하는 대상
FVector direction = target - start;//direction=target을 바라보는 방향
direction.Normalize();
LaunchCharacter(-direction * data->Launch, false, false);
SetActorRotation(UKismetMathLibrary::FindLookAtRotation(start, target));
}
}
//사망 처리
if (Status->IsDead())
{
State->SetDeadMode();//State을 DeadMode로 만들어준다.
return;
}
Damage.Character = nullptr;
Damage.Causer = nullptr;
Damage.Event = nullptr;
}
void ACEnemy::End_Hitted()
{
State->SetIdleMode();
}
void ACEnemy::RestoreColor()
{
Change_Color(this, OriginColor);//원래 색으로 돌려준다.
GetWorld()->GetTimerManager().ClearTimer(RestoreColor_TimerHandle);//RestoreColor_TimerHandle를 초기화.
}
void ACEnemy::Dead()
{
GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);//Collision 꺼줌.
Montages->PlayDeadMode();
}
void ACEnemy::End_Dead()
{
Destroy(); //죽으면 소멸시켜준다.
}
float ACEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
float damage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);
Damage.Power = damage;
Damage.Character = Cast<ACharacter>(EventInstigator->GetPawn());
Damage.Causer = DamageCauser;
Damage.Event = (FActionDamageEvent*)&DamageEvent;//레퍼런스로 캐스팅하면 캐스팅이 안 되거나 캐스팅 후 상속이 안 먹는 경우가 발생할 수 있다. //안전하게 C스타일 캐스팅하기 위해 포인터로 캐스팅한다.
State->SetHittedMode();//HittedMode로 변경.
return damage;
}

변경사항 없음.

 

 


 

 

CEnemy_AI

 

CEnemy_AI.h

더보기
#pragma once
#include "CoreMinimal.h"
#include "Characters/CEnemy.h"
#include "CEnemy_AI.generated.h"
UCLASS()
class U2212_06_API ACEnemy_AI : public ACEnemy
{
GENERATED_BODY()
private:
UPROPERTY(EditDefaultsOnly, Category = "AI")
class UBehaviorTree* BehaviorTree;
UPROPERTY(EditDefaultsOnly, Category = "AI")
uint8 TeamID = 2;//TeamID를 0~255번까지 지정가능하다. 255번은 중립이다. ID 같으면 아군이고 ID가 다르면 적이다.
private:
UPROPERTY(EditDefaultsOnly, Category = "Label")
float LabelViewAmount = 3000.0f;
private:
UPROPERTY(EditAnywhere, Category = "Patrol")
class ACPatrolPath* PatrolPath;//클래스 밖에서도 지정할 수 있어야 한다. 서로간 만들어진 객체를 참조할것이라서 softObjectPtr 사용
#if WITH_EDITOR
private:
UPROPERTY(VisibleDefaultsOnly)
class UWidgetComponent* LabelWidget;
#endif
private:
UPROPERTY(VisibleDefaultsOnly)
class UCWeaponComponent* Weapon;
UPROPERTY(VisibleDefaultsOnly)
class UCAIBehaviorComponent* Behavior;
public:
FORCEINLINE uint8 GetTeamID() { return TeamID; }
FORCEINLINE class UBehaviorTree* GetBehaviorTree() { return BehaviorTree; }
FORCEINLINE class ACPatrolPath* GetPatrolPath() { return PatrolPath; }
public:
ACEnemy_AI();
protected:
virtual void BeginPlay() override;
public:
virtual void Tick(float DeltaTime) override;
private:
void UpdateLabelRenderScale();
protected:
void Hitted() override;//CEnemy의 Hitted 재정의
public:
void End_Hitted() override;//CEnemy의 End_Hitted 재정의
};

부모 클래스인 CEnemy의 함수 재정의

  • void Hitted() override;
  • void End_Hitted() override;

 

 

 

CEnemy_AI.cpp

더보기
#include "Characters/CEnemy_AI.h"
#include "Global.h"
#include "Components/CWeaponComponent.h"
#include "Components/CAIBehaviorComponent.h"
#include "Components/WidgetComponent.h"
#include "Components/CStatusComponent.h"
#include "Widgets/CUserWidget_Label.h"
ACEnemy_AI::ACEnemy_AI()
{
PrimaryActorTick.bCanEverTick = true;
CHelpers::CreateComponent<UWidgetComponent>(this, &LabelWidget, "Label", GetMesh());
CHelpers::CreateActorComponent<UCWeaponComponent>(this, &Weapon, "Weapon");
CHelpers::CreateActorComponent<UCAIBehaviorComponent>(this, &Behavior, "Behavior");
TSubclassOf<UCUserWidget_Label> labelClass;
CHelpers::GetClass<UCUserWidget_Label>(&labelClass, "WidgetBlueprint'/Game/Widgets/WB_Label.WB_Label_C'");
LabelWidget->SetWidgetClass(labelClass);
LabelWidget->SetRelativeLocation(FVector(0, 0, 220));
LabelWidget->SetDrawSize(FVector2D(120, 0));
LabelWidget->SetWidgetSpace(EWidgetSpace::Screen);
}
void ACEnemy_AI::BeginPlay()
{
Super::BeginPlay();
LabelWidget->InitWidget();
UCUserWidget_Label* label = Cast<UCUserWidget_Label>(LabelWidget->GetUserWidgetObject());
label->UpdateHealth(Status->GetHealth(), Status->GetMaxHealth());
label->UpdateName(GetName());
label->UpdateControllerName(GetController()->GetName());
}
void ACEnemy_AI::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
UCUserWidget_Label* label = Cast<UCUserWidget_Label>(LabelWidget->GetUserWidgetObject());
if (!!label)
{
label->UpdateHealth(Status->GetHealth(), Status->GetMaxHealth());
UpdateLabelRenderScale();
}
}
void ACEnemy_AI::UpdateLabelRenderScale()
{
UCUserWidget_Label* label = Cast<UCUserWidget_Label>(LabelWidget->GetUserWidgetObject());
CheckNull(label);
APlayerCameraManager* cameraManager = UGameplayStatics::GetPlayerCameraManager(GetWorld(), 0);
FVector cameraLocation = cameraManager->GetCameraLocation();
FVector targetLocation = GetController()->GetTargetLocation();
float distance = FVector::Distance(cameraLocation, targetLocation);
float sizeRate = 1.0f - (distance / LabelViewAmount);
if (distance > LabelViewAmount)
{
label->SetVisibility(ESlateVisibility::Collapsed);
return;
}
label->SetVisibility(ESlateVisibility::Visible);
label->SetRenderScale(FVector2D(sizeRate, sizeRate));
}
void ACEnemy_AI::Hitted()
{
Super::Hitted();
CheckTrue(State->IsDeadMode());//이미 DeadMode면 Hitted 처리할 필요없다.
Behavior->SetHittedMode();
}
void ACEnemy_AI::End_Hitted()
{
Super::End_Hitted();
Behavior->SetWaitMode();//Hitted가 끝나면 Behavior를 WaitMode로 만들어준다.
}

함수 재정의

  • void ACEnemy_AI::Hitted()
    • Super::Hitted();
      CheckTrue(State->IsDeadMode()); //이미 DeadMode면 Hitted 처리할 필요없다.
      Behavior->SetHittedMode();
  • void ACEnemy_AI::End_Hitted()
    • Super::End_Hitted();
      Behavior->SetWaitMode(); //Hitted가 끝나면 Behavior를 WaitMode로 만들어준다.

 

 

 

※참고: BP 프로젝트의 BP_Enemy_AI 블루프린트 노드

Enemy가 피격되었을때 AI Behavior의 상태를 바꾸어주어야 한다.

 

 


 

 

 

 

CAnimNotify_EndState

 

 

CAnimNotify_EndState.h

더보기
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "Components/CStateComponent.h"
#include "CAnimNotify_EndState.generated.h"
UCLASS()
class U2212_06_API UCAnimNotify_EndState : public UAnimNotify
{
GENERATED_BODY()
private:
UPROPERTY(EditAnywhere, Category = "Type")
EStateType StateType;
public:
FString GetNotifyName_Implementation() const override;
void Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation) override;
};

변경사항 없음.

 

 

 

CAnimNotify_EndState.cpp

더보기
#include "CAnimNotify_EndState.h"
#include "Global.h"
#include "Characters/ICharacter.h"
FString UCAnimNotify_EndState::GetNotifyName_Implementation() const
{
return "EndState";
}
void UCAnimNotify_EndState::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)
{
Super::Notify(MeshComp, Animation);
CheckNull(MeshComp);
CheckNull(MeshComp->GetOwner());
IICharacter* character = Cast<IICharacter>(MeshComp->GetOwner());
CheckNull(character);
switch(StateType)
{
//Dead를 Hitted보다 먼저 검사할 수 있게 순서를 맞춰준다.
case EStateType::BackStep: character->End_BackStep(); break;
case EStateType::Dead: character->End_Dead(); break;
case EStateType::Hitted: character->End_Hitted(); break;
}
}

void UCAnimNotify_EndState::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation)

  • switch(StateType)
    {
    case EStateType::BackStep: character->End_BackStep(); break;
    case EStateType::Dead: character->End_Dead(); break;
    case EStateType::Hitted: character->End_Hitted(); break;
    }
    • DeadHitted보다 먼저 검사할 수 있게 순서를 맞춰준다.
    • End_Hitted();는 ICharacter, CEnemy, CEnemy_AI 순서로 재정의되며 실행될 것이다.

 


 

 

피격(Hitted) 몽타주들

 

피격이 되는 몽타주들의 맨 뒤에 CAnimNotify_EndState를 넣고 State TypeHitted로 설정한다.

 


 

 

BT_Melee

 

 

Wait을 맨 앞쪽에 넣은 이유는?

  • 처음 상태가 Wait으로 들어오기 때문에 처음 위치에 Wait이 없으면 Wait을 판단할 수 없어 제대로 동작하지 않는다.
  • 위의 경우 맨 앞의 Wait Task가 없으면 Enemy_AI가 움직이지 않는다. 
  • 시작부분이 되는 Task를 앞쪽에 넣으면 해결된다.
    • 해결방안
      • 방법1. 위의 방법처럼 맨 앞에 Wait Task를 넣는다.
      • 방법2. 코드에서 처음 상태를 Patrol로 만들어주면 Wait Task가 필요없다.    

 


 

 

실행화면