이제 순찰 중에 플레이어를 만나면 쫓아가도록 구현해보자

이때 Pawn Sensing Component를 통해 플레이어를 감지할 수 있다.

 블루프린트 상에서 추가해보면 다양한 속성을 볼 수 있는데 이때 듣는것과 보는것 모두 감지거리를 조정해줄 수 있다.

LOS는 최대로 들을 수 있는 거리이다.

이를 코드로 구현해보자. 우선 UPawnSensingComponent 를 전방선언해주고 이를 DefaultSubObject로 생성해주고 시야각과 시야범위를 설정해주자. 그리고 플레이어를 감지했을 때의 콜백 함수를 지정해주자.

Enemy.h

private:
	UPROPERTY(VisibleAnywhere)
	class UPawnSensingComponent* PawnSensing;
    
protected:
    UFUNCTION()
	void PawnSeen(APawn* SeenPawn);

Enemy.cpp

AEnemy::AEnemy()
{
    PawnSensing = CreateDefaultSubobject<UPawnSensingComponent>(TEXT("PawnSensing"));
	PawnSensing->SightRadius = 4000.f;
	PawnSensing->SetPeripheralVisionAngle(45.f);
}

void AEnemy::BeginPlay()
{
	Super::BeginPlay();
	if (HealthBarWidget)
	{
		HealthBarWidget->SetVisibility(false);
	}

	EnemyController = Cast<AAIController>(GetController());
	
	MoveToTarget(PatrolTarget);

	if (PawnSensing)
	{
		PawnSensing->OnSeePawn.AddDynamic(this, &AEnemy::PawnSeen);
	}
}

void AEnemy::PawnSeen(APawn* SeenPawn)
{

}

 

이렇게 해준 다음 이제 적의 Patrol Chase Attack 상태를 나누면서  쫓아가는 로직 구현해보자

우선 enum class를 통해 각 state를 나눠주자.

UENUM(BlueprintType)
enum class EEnemyState : uint8
{
	EES_Patrolling UMETA(DisplayName = "Patrolling"),
	EES_Chasing UMETA(DisplayName = "Chasing"),
	EES_Attacking UMETA(DisplayName = "Attacking"),
};

 

이렇게 해준 다음 Enemy클래스에서 이 enum class 변수를 가지고 있게 하자.

Enemy.h

private:
	EEnemyState EnemyState = EEnemyState::EES_Patrolling;

 

그리고 Pawn을 발견했을 때 쫓아가도록 해야한다. 이때 Cast로 검사를 해주게 되면 이 함수의 연산 간격이 짧기 때문에  연산부하가 많이 지게 된다. 이때 특정 태그값을 가지고 있는지 검사해주면 된다. 

SlashCharacter.cpp

void ASlashCharacter::BeginPlay()
{
	Tags.Add(FName("SlashCharacter"));
}

Enemy.cpp

void AEnemy::PawnSeen(APawn* SeenPawn)
{
	if (EnemyState == EEnemyState::EES_Chasing) return;

	//Chase
	if (SeenPawn->ActorHasTag(FName("SlashCharacter")))
	{
		EnemyState = EEnemyState::EES_Chasing;
		GetWorldTimerManager().ClearTimer(PatrolTimer);
		GetCharacterMovement()->MaxWalkSpeed=300.f;
		CombatTarget=SeenPawn;
		MoveToTarget(CombatTarget);
	}
}

 

이렇게 해주면 잘 쫓아오는 것을 알 수 있다.

 

이렇게 해주고 반경 안에서 사리지면 다시 Patrol하도록 해주자.

void AEnemy::CheckCombatTarget()
{
	if (!InTargetRange(CombatTarget, CombatRadius))
	{
		//바깥이면 Patrol로
		CombatTarget = nullptr;
		if (HealthBarWidget)
		{
			HealthBarWidget->SetVisibility(false);
			Attributes->SetHealth(100.f);
		}
		EnemyState = EEnemyState::EES_Patrolling;
		GetCharacterMovement()->MaxWalkSpeed = 125.f;
		MoveToTarget(PatrolTarget);
	}
}

 

이제 여기서 Attack State로 바꾸고 Attack을 할 수 있게 Attack Radius를 지정해주어야 한다. 그리고 이제 CheckCombatTarget함수에서 플레이어와의 거리를 측정해서 Patrol Chase Attack상태가 나눠지도록 조건문을 구성해주자. 이때 PawnSeen함수에서 공격상태가 아닐때만 Chase하도록 해주자. 

Enemy.h

private:
	UPROPERTY(EditAnywhere)
	double AttackRadius = 150.f;

 

Enemy.cpp

void AEnemy::PawnSeen(APawn* SeenPawn)
{
	if (EnemyState == EEnemyState::EES_Chasing) return;

	//Chase
	if (SeenPawn->ActorHasTag(FName("SlashCharacter")))
	{
		GetWorldTimerManager().ClearTimer(PatrolTimer);
		GetCharacterMovement()->MaxWalkSpeed=300.f;
		CombatTarget=SeenPawn;

		if (EnemyState != EEnemyState::EES_Attacking)
		{
			EnemyState = EEnemyState::EES_Chasing;
			MoveToTarget(CombatTarget);
		}
	}
}

void AEnemy::CheckCombatTarget()
{
	if (!InTargetRange(CombatTarget, CombatRadius))
	{
		//바깥이면 Patrol로
		CombatTarget = nullptr;
		if (HealthBarWidget)
		{
			HealthBarWidget->SetVisibility(false);
			Attributes->SetHealth(100.f);
		}
		EnemyState = EEnemyState::EES_Patrolling;
		GetCharacterMovement()->MaxWalkSpeed = 125.f;
		MoveToTarget(PatrolTarget);
	}
	else if (!InTargetRange(CombatTarget, AttackRadius) && EnemyState != EEnemyState::EES_Chasing)
	{
		//공격반경 바깥
		EnemyState = EEnemyState::EES_Chasing;
		GetCharacterMovement()->MaxWalkSpeed = 300.f;
		MoveToTarget(CombatTarget);
	}
	else if (InTargetRange(CombatTarget, AttackRadius) && EnemyState != EEnemyState::EES_Attacking)
	{
		//공격 반경 내부
		EnemyState = EEnemyState::EES_Attacking;
		//공격
	}
}

 

하지만 지금만약 적의 뒤에서 공격을 하면 아무 반응이 없다. 이러한 점을 고쳐보도록 하자. 이는 TakeDamage함수에서 지정해줄 수 있다.

float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (Attributes)
	{
		Attributes->ReceiveDamage(DamageAmount);
		HealthBarWidget->SetHealthPercent(Attributes->GetHealthPercent());
	}

	CombatTarget = EventInstigator->GetPawn();
	EnemyState = EEnemyState::EES_Chasing;
	GetCharacterMovement()->MaxWalkSpeed = 300.f;
	MoveToTarget(CombatTarget);

	return DamageAmount;
}

 

마지막으로 적에게 무기를 장착하기 위해 다양한 무기들을 추가해보자. 이때 무기 에셋마다 각 Origin(Pivot)이 다르기 때문에 조심해야한다.

우선 무료 무기 에셋 중에 Free Fantasy Weapon Sample Pack이라는 에셋을 추가하여 사용해보자. 무기 중에 Axe를 보면 원점이 손잡이에 있는 것을 볼 수 있다.

https://www.fab.com/ko/listings/d5be0dc9-1a41-4be2-a63a-5ed436f3445d

 

이 Axe로 매쉬를 변경하고 실행해서 잡아보면 문제가 생기는 것을 알 수 있다. 손에서 떨어져 있으며 도끼의 방향도 잘 못된것을 알 수 있다.

 

이렇게 된 무기들의 원점을 동일하게 바꿔주어야하는데 이를 위해 Blender를 사용하자. Blendor에서 사용하려면 해당 에셋을 Export해주어야한다.

 

Blender에서는 fbx 파일을 가져와서 에디트 모드로 a키를 눌러 모두 선택한 다음들어간다음 R키를 누르고 Y키를 눌러 y축이 고정된 상태로 180도 회전시켜주면된다. 그리고 G키와 Z키를 통해 원점을 Z축이 고정된 상태로 손잡이로 바꾸주자.

마지막으로 크기는 S키를 누르면 변경할 수있다. 완성된 fbx파일은 이름을 기존 것과 다르게 해준 다음 다시 언리얼에서 임폴트 해주자.

 

임폴트한 에셋에 머티리얼은 이전것과 동일한 것으로 지정해주자. 이렇게 해주면 이전과 다르게 잘 잡고 있는 것을 볼 수 있다.

'게임공부 > Unreal Engine' 카테고리의 다른 글

[Unreal Engine][C++]25. Patrol  (0) 2025.02.07
[Unreal Engine][C++]24. Enemey Behavior2  (0) 2025.02.05
[Unreal Engine][C++]23. Enemey Behavior  (0) 2025.01.27
[Unreal Engine][C++]22. Death  (1) 2025.01.26
[Unreal Engine][C++]21. Damage  (1) 2025.01.21

 

이제 단일 지점으로의 Patrol은 가능한 것을 볼 수 있다. 이제 한 Patrol Point에서 다른 Patrol Point로 이동할 수 있도록 구현해보자.

이를 위해 타켓포인트를 여러개 만들어주고 배열에 할당해주자

 

그리고 Tick함수에서 Target과의 거리를 측정해주는 함수가 작동할 수 있도록 Actor와의 지정해준 거리에 따라 bool값을 반환해주는 새로운 함수를 만들어주자.

Enemy.cpp

bool AEnemy::InTargetRange(AActor* Target, double Radius)
{
	const double DistanceToTarget = (Target->GetActorLocation() - GetActorLocation()).Size();
	return DistanceToTarget <= Radius;
}

 

이를 바탕으로  Tick함수에서 HealthBar 조건문도 변경해주고 PatrolTarget조건문도 만들어주자.

이때 PatrolRadius(멈출 거리)를 지정해주어야하는데 이 값이 double인데 moveTo에서는 이 반경보다 조금 더 이동하기 때문에 조금 더 거리를 지정해주어야한다.

Enemy.h

	UPROPERTY(EditAnywhere)
	double PatrolRadius = 200.f;

Enemy.cpp

void AEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (CombatTarget)
	{
		if (!InTargetRange(CombatTarget,CombatRadius))
		{
			CombatTarget = nullptr;
			if (HealthBarWidget)
			{
				HealthBarWidget->SetVisibility(false);
				Attributes->SetHealth(100.f);
			}
		}
	}

	if (PatrolTarget && EnemyController)
	{
		TArray<AActor*> ValidTargets;
		for (AActor* Target : PatrolTargets)
		{
			if (Target != PatrolTarget)
			{
				ValidTargets.AddUnique(Target);
			}
		}


		if (InTargetRange(PatrolTarget, PatrolRadius))
		{
			const int32 TargetSelection = FMath::RandRange(0, ValidTargets.Num() - 1);
			PatrolTarget = ValidTargets[TargetSelection];
			
			FAIMoveRequest MoveRequest;
			MoveRequest.SetGoalActor(PatrolTarget);
			//도착이라고 생각할 거리
			MoveRequest.SetAcceptanceRadius(15.f);
			EnemyController->MoveTo(MoveRequest);
		}
	}
}

 

이렇게 해주고 실행해주면 랜덤한 순찰포인트로 순찰하는 것을 볼 수 있다.

 

하지만 지금은 순찰 포인트로 이동하고 바로 다른 포인트로 이동하는데 이것을 조금 기다렸다가 다른 순찰포인트로 이동하도록 구현해보자. 

이를 위해 FTimerHandle 구조체 변수를 선언해주자. 이 구조체 변수는 일정 시간이 지난후  호출될 콜백함수가 필요하기 때문에 함수도 같이 선언해주자.

Enemy.h

private:
	FTimerHandle PatrolTimer;
	void PatrolTimerFinished();

 

Enemy.cpp

void AEnemy::PatrolTimerFinished()
{
	MoveToTarget(PatrolTarget);
}

 

이렇게 콜백함수를 만들어준 다음 기능별로 나눌 수 있는것은 나눠주자.

Enemy.cpp

bool AEnemy::InTargetRange(AActor* Target, double Radius)
{
	if (Target == nullptr) return false;
	const double DistanceToTarget = (Target->GetActorLocation() - GetActorLocation()).Size();
	DRAW_SPHERE_SingleFrame(GetActorLocation());
	DRAW_SPHERE_SingleFrame(Target->GetActorLocation());
	return DistanceToTarget <= Radius;
}

void AEnemy::MoveToTarget(AActor* Target)
{
	if (EnemyController == nullptr || Target == nullptr) return;

	FAIMoveRequest MoveRequest;
	MoveRequest.SetGoalActor(Target);
	//도착이라고 생각할 거리
	MoveRequest.SetAcceptanceRadius(15.f);
	EnemyController->MoveTo(MoveRequest);
}

AActor* AEnemy::ChoosePatrolTarget()
{
	TArray<AActor*> ValidTargets;
	for (AActor* Target : PatrolTargets)
	{
		if (Target != PatrolTarget)
		{
			ValidTargets.AddUnique(Target);
		}
	}

	if (ValidTargets.Num() > 0)
	{
		const int32 TargetSelection = FMath::RandRange(0, ValidTargets.Num() - 1);
		return ValidTargets[TargetSelection];
	}
	return nullptr;
}

 

만약 타켓이 거리안에 있다면 5초를 대기한 다음 이동하게 해주자 이때 GetWorldTimerManager의 SetTimer 기능을 활용하면 된다.

Enemy.cpp

void AEnemy::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);
	
	if (InTargetRange(PatrolTarget, PatrolRadius))
	{
		PatrolTarget = ChoosePatrolTarget();
		GetWorldTimerManager().SetTimer(PatrolTimer, this, &AEnemy::PatrolTimerFinished, 5.f);
	}
	
}

 

이렇게 해주면 5초를 대기한 다음 다음 순찰포인트로 이동한다.

 



이전 시간에 애니메이션을 임포트해주었으니 이제 올바른 상황에 맞게 출력되도록 설정해주자 

애니메이션 블루프린트의 Main State에 Blend Space를  사용하는 것으로 걷고 뛰는 애니메이션이 재생되도록 해주자.

이때 Blend Space는 속도와 같은 변수 값의 상태에 따라 다른 애니메이션이 재생되도록 해준다.

이를 위해 애니메이션/블랜드스페이스1D를 생성해주자.

가로축의 이름을 GroundSpeed로 바꿔주고 최소를0 최대를 300으로 해준다음 Idle Walk Run을 배치해주자

 

이렇게 배치해준다음 Idle State에서 애니메이션 포즈가 이 Blend Space를 통해 출력되도록 수정해주자

 

이렇게 해주고 이제 적의 속도를 가져와야하기 때문에 이벤트그래프에서 적캐릭터 BP를 가져오고 BP에 있는 Character Movement도 가져오도록 수정해주자.

 

 

그리고 Float로 GroundSpeed라는 변수를 선언해주고 Thread Safe Update Animation에서 이 GroundSpeed를 설정해줄 수 있도록 해주자.

 

그 다음, 이 변수 값을 가져와서 Blend Space 입력에 넣어주자

이렇게 해주면 움직임이 한쪽으로 치우쳐보이는데 이는 Character에서 Orient 값을 체크해주고 BP_Enemy의 컨트롤러 요 사용을 꺼주면 된다.

 

이를 코드로 구현해보자

AEnemy::AEnemy()
{
	PrimaryActorTick.bCanEverTick = true;

	GetMesh()->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GetMesh()->SetGenerateOverlapEvents(true);
	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);

	Attributes = CreateDefaultSubobject<UAttributeComponent>(TEXT("Attributes"));
	HealthBarWidget = CreateDefaultSubobject<UHealthBarComponent>(TEXT("HealthBar"));
	HealthBarWidget->SetupAttachment(GetRootComponent());

	GetCharacterMovement()->bOrientRotationToMovement = true;
	bUseControllerRotationPitch = false;
	bUseControllerRotationRoll = false;
	bUseControllerRotationYaw = false;
}

 

걷는 모션이 생겼으니 이제 특정 포인트를 순찰하도록 만들어보자. 여러 포인트를 가진 다음 상황에 따라 특정 포인트를 선택하여 순찰하도록 구현해보자. 이를 위해 AActor값의 TArray를 만들어주자.

 

Enemy.h

private:
	UPROPERTY(EditInstanceOnly,Category="AI Navigation")
	AActor* PatrolTarget;

	UPROPERTY(EditInstanceOnly, Category = "AI Navigation")
	TArray<AActor*> PatrolTargets;

 

이를 활용하기 위해 AAIController의 MoveTo함수를 사용하자.

 

이를 사용하려면 모듈에 AIModule을 추가해줘야한다. 이렇게 해준 다음 순찰타켓을 지정해주고 옵션값을 지정해준 다음 OutPath를 가져와서 DebugSphere을 그려주게 하자.

Enemy.cpp

void AEnemy::BeginPlay()
{
	Super::BeginPlay();
	if (HealthBarWidget)
	{
		HealthBarWidget->SetVisibility(false);
	}

	EnemyController = Cast<AAIController>(GetController());
	if (EnemyController && PatrolTarget)
	{
		FAIMoveRequest MoveRequest;
		MoveRequest.SetGoalActor(PatrolTarget);
		//도착이라고 생각할 거리
		MoveRequest.SetAcceptanceRadius(15.f);
		FNavPathSharedPtr NavPath;
		EnemyController->MoveTo(MoveRequest, &NavPath);
		//& : 복사x -> 참조
		TArray<FNavPathPoint>& PathPoints = NavPath->GetPathPoints();
		for (auto& Point : PathPoints)
		{
			const FVector& Location = Point.Location;
			DrawDebugSphere(GetWorld(), Location, 12.f, FColor::Green, false, 10.f);
		}
	}
}

 

이렇게 해준다음 BP에서 Patrol Point를 지정해주면 된다.

 

이렇게 해주고 실행하면 직선이기 때문에 구가 2개만 등장하는 것을 볼 수 있다.

만약 여기서 장애물이나 벽을 추가하면 구가 늘어나게 된다.

'게임공부 > Unreal Engine' 카테고리의 다른 글

[Unreal Engine][C++]26. Patrol2  (0) 2025.02.10
[Unreal Engine][C++]25. Patrol  (0) 2025.02.07
[Unreal Engine][C++]23. Enemey Behavior  (0) 2025.01.27
[Unreal Engine][C++]22. Death  (1) 2025.01.26
[Unreal Engine][C++]21. Damage  (1) 2025.01.21


이제 적이 움직일 수 있도록 만들어보자 

이를 위해 Nav Mesh를 사용하자. 이를 위해 액터 배치 패널을 열어주고 navmeshBounds Volume을 선택해주자

선택해주고 맵에 배치해주고 P키를 눌러보면 nav 매쉬가 적용된 부분을 볼 수 있다.

 

이 볼륨이 맵 전체에 적용될 수 있도록 크기를 키워주자.

 

이렇게 해주면 이제 AI 기능을 사용하여 적을 움직이게 할 수 있다. 이 기능을 사용하기 위해 블루프린트 클래스로 이동해주자.

이때 AI 컨트롤러가 적이 Spawn했을 때도 잘 작동하게 하기위해 디테일창에서 AI자동 빙의를 수정해주자

일단 테스트를 위해 시작할 때 AI컨트롤러를 통해 움직이도록 해보자

 

이렇게 해주면 0,0,0으로 이동하는 것을 볼 수 있다.

 

이제 특정 액터로 이동하게 만들기 위해 Actor 타입의 변수를 하나 선언해주자. 이때 기본값을 설정해줄 수도 있는데 이때 변수의 인스턴스 편집가능을 체크해주어야한다.

또한 Target Poin 객체를 추가하는 것으로 움직일 위치를 정해줄 수 있다.

 

움직이는 사용자를 Chase하려면 플레이어가 공격할 때 이 변수를 조정해주고 움직이는 함수를 호출해주는 것을 반복하는것으로 구현할 수 있을 것이다. 

그리고 프로젝트 세팅에서 네비게이션 매쉬의 런타임 생성을 Dynamic으로 바꿔주면 항아리같은 부숴지는 물체나 문 같이 열릴 수 있는 물체의 상태에 따라 이 매쉬가 재구성 된다. 이는 고비용이 든다.

 

또한 셀 사이즈 및 높이를 조정해주는 것으로 매쉬가 어떻게 얼마나 적용될지 정해줄 수 있다.

 

적이 움직일 때 애니메이션을 추가해주자 애니메이션은 Mixamo에서 추가해주자. 이때 Blender를 통해 Root본을 추가해주는 작업을 해주어야한다. 이때 폴더에 하나의 with Skin Animation이 존재해야 다른 것에도 일괄적으로 자동 적용이 된다.

Blender에서 N키를 눌러 사이드 바를 열 수 있다.

이렇게 변환이 끝난 애니메이션을 매쉬없이 임폴트해준다.

'게임공부 > Unreal Engine' 카테고리의 다른 글

[Unreal Engine][C++]25. Patrol  (0) 2025.02.07
[Unreal Engine][C++]24. Enemey Behavior2  (0) 2025.02.05
[Unreal Engine][C++]22. Death  (1) 2025.01.26
[Unreal Engine][C++]21. Damage  (1) 2025.01.21
[Unreal Engine][C++]20. Actor Component  (0) 2025.01.16

 



 Die 몽타주의 섹션이 재생되지만 바로 Idle로 돌아오기 때문에 계속 누워있을수 있도록 수정해주어야 한다.

이를 위해 각 애니메이션에서 제일 마지막부분을 따로 애니메이션으로 만들기 위해 마지막 부분을 선택한 뒤 에셋생성/애니메이션 생성/현재포즈를 통해 만들어주자.

그 다음 애니메이 블루플린트에서 새로운 State Machine을 만들어주자.

 

이 Main States에는 Idle과 Dead가 있고 내부 에는 각각의 애니메이션이 재생되도록 한다. 

 

이때 Idle과 Dead의 전환 조건도 만들어줘야하고 어떤 Dead애니메이션이 재생될지도 정해줘야한다. 이를 코드로 구현배자 Death포즈 여러개와 Alive 포즈를 정의해줄 enum 클래스를 선언해주자.

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")
};

 

이렇게 정의해준 enum 클래스를 Enemy 클래스에서 사용하도록 하자. 이때 애니메이션 블루프린트에서 사용할 수 있게 UPROPERTY값을 BluePrintReadOnly로 설정해주었다.

Enemy.h

protected:
	UPROPERTY(BlueprintReadOnly)
	EDeathPose DeathPose = EDeathPose::EDP_Alive;

 

그리고 Die함수 내에서 각 Death 애니메이션 섹션에 맞게 enum으로 선언된 변수도 바뀌도록 static_cast를 통해 처리해주었다.

Enemy.cpp

void AEnemy::Die()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
		const int32 Selection = FMath::RandRange(1, 6);
		FString SectionString = FString::Printf(TEXT("Death%d"), Selection); // "Death1", "Death2" 등의 문자열 생성
		DeathPose = static_cast<EDeathPose>(RandomIndex);
		FName SectionName = FName(*SectionString); // FString을 FName으로 변환
		AnimInstance->Montage_JumpToSection(SectionName, DeathMontage);
	}
}

 

이렇게 해주고 이 Enum 변수값을 가져오기 위해 애니메이션이 Initialize 될때 Enemy값을 변수로 가져올 수 있도록 해주자.

 

이때 DeathPose의 값을 가져오는 것을 멀티스레딩를 활용하여 가져오자. 이것은 Tread Safe Update Animation을 활용하여 만들 수 있는데 이때 멀티 스레딩에서 값에 안전하게 접근하도록 Property Access를 활용한다.

 

이렇게 해주면 이제 Death Pose가 Alive가 아니라면 Death State로 넘어가면 되게 해주면 된다.

 

이렇게 해준 다음 Dead에서는 DeadPose 변수값을 가져와서 Blend Pose를 통해 각 애니메이션에 맞는 Dead 애니메이션을 마지막에 출력해주도록 하자

 

 

이렇게 해주고 블랜드 타임을 0으로 해주면 잘 작동하는 것을 볼 수 있다. 

 

이제 Enemy가 죽었을 때 더 이상 충돌하지 않게 해주고 몇초 뒤에 Destroy 되도록 하자.

Enemy.cpp

void AEnemy::Die()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
		const int32 Selection = FMath::RandRange(1, 6);
		FString SectionString = FString::Printf(TEXT("Death%d"), Selection); // "Death1", "Death2" 등의 문자열 생성
		DeathPose = static_cast<EDeathPose>(Selection);
		FName SectionName = FName(*SectionString); // FString을 FName으로 변환
		AnimInstance->Montage_JumpToSection(SectionName, DeathMontage);
	}

	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	SetLifeSpan(3.f);
}

 

이렇게 해주고 HP바가 공격했을 때 표시되고 멀리 갔을 때는 다시 없어지게 구현해보자. 멀리 갔을 때 없어지는 것을 구현하기 위해 Enemy에서 현재 공격하고 있는 Actor를 저장하고 이 Actor와의 거리에 따라 없어지게 해주면 된다.

Enemy.h

	UPROPERTY()
	AActor* CombatTarget;

	UPROPERTY()
	double CombatRadius = 500.f;

 

Enemy.cpp

void AEnemy::BeginPlay()
{
	Super::BeginPlay();
	if (HealthBarWidget)
	{
		HealthBarWidget->SetVisibility(false);
	}
}

void AEnemy::Die()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
		const int32 Selection = FMath::RandRange(1, 6);
		FString SectionString = FString::Printf(TEXT("Death%d"), Selection); // "Death1", "Death2" 등의 문자열 생성
		DeathPose = static_cast<EDeathPose>(Selection);
		FName SectionName = FName(*SectionString); // FString을 FName으로 변환
		AnimInstance->Montage_JumpToSection(SectionName, DeathMontage);
	}
	if (HealthBarWidget)
	{
		HealthBarWidget->SetVisibility(false);
	}
	GetCapsuleComponent()->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	SetLifeSpan(3.f);
}

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);
				Attributes->SetHealth(100.f);
			}
		}
	}
}

void AEnemy::GetHit_Implementation(const FVector& ImpactPoint)
{
	if (HealthBarWidget)
	{
		HealthBarWidget->SetVisibility(true);
	}
	if (Attributes && Attributes->IsAlive())
	{
		DirectionalHitReact(ImpactPoint);
	}
	else
	{
		Die();
	}
	
	

	if (HitSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			this,
			HitSound,
			ImpactPoint
		);
	}

	if (HitParticles&& GetWorld())
	{
		UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			HitParticles,
			ImpactPoint
		);
	}
}

float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (Attributes)
	{
		Attributes->ReceiveDamage(DamageAmount);
		HealthBarWidget->SetHealthPercent(Attributes->GetHealthPercent());
	}

	CombatTarget = EventInstigator->GetPawn();

	return DamageAmount;
}

 

 

이렇게 하면 잘 작동하는 것을 볼 수 있다.

 

https://www.udemy.com/share/107vg03@Xgb5iRXcpg26gJ-bUYComus3EEfNr2SumFm1Wlohh2QkeHxGAaMbERJi-mc4LTXQig==/

 


이제 체력바는 있기 때문에 이를 활용해보자

적에게 Damage를 주면 체력바의 퍼센트가 감소하도록 하자.

이때 AActor클래스에 있는 TakeDamage함수를 재정의하여 사용하자. 이 함수를 통해 데미지를 입었을 때 발생할 함수를 연결해줄 수 있다.

 

 

그리고 ApplyDamage를 통해 데미지를 줄 수 있다. 이때 EventInstigator은 데미지를 주는 객체를 소유하고 있는 객인데 이때 칼을 쥐고 있는 캐릭터가 여기에 해당된다. DamageCauser은 칼 그자체를 뜻한다.

 

 

전체적인 작동방식은 다음과 같다

 

이제 실제 코드에서 적용해보자. 우선 무기에서 ApplyDamage를 구현하자. 우선 장착할때 Owner와 Instigator를 설정하도록 변경해주자.

Weapon.cpp

void AWeapon::Equip(USceneComponent* Inparent, FName InSocketName, AActor* NewOwner, APawn* NewInstigator)
{
	SetOwner(NewOwner);
	SetInstigator(NewInstigator);
	AttachMeshToSocket(Inparent, InSocketName);
	ItemState = EItemState::EIS_Equipped;
	if (EquipSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			this,
			EquipSound,
			GetActorLocation()
		);
	}
	if (Sphere)
	{
		Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
	if (EmbersEffect)
	{
		EmbersEffect->Deactivate();
	}
}

void AWeapon::OnBoxOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	const FVector Start = BoxTraceStart->GetComponentLocation();
	const FVector End = BoxTraceEnd->GetComponentLocation();

	TArray<AActor*> ActorsToIgnore;
	ActorsToIgnore.Add(this);

	for (AActor* Actor : IgnoreActors)
	{
		ActorsToIgnore.AddUnique(Actor);
	}

	//충돌 결과
	FHitResult BoxHit;

	UKismetSystemLibrary::BoxTraceSingle(
		this,
		Start,
		End,
		FVector(5.f, 5.f, 5.f),
		BoxTraceStart->GetComponentRotation(),
		ETraceTypeQuery::TraceTypeQuery1,
		false,
		ActorsToIgnore,		//무시할거
		EDrawDebugTrace::ForDuration,
		BoxHit,
		true		//자신무시
	);

	if (BoxHit.GetActor())
	{
		IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
		if (HitInterface)
		{
			HitInterface->Execute_GetHit(BoxHit.GetActor(), BoxHit.ImpactPoint);
		}
		IgnoreActors.AddUnique(BoxHit.GetActor());

		CreateFields(BoxHit.ImpactPoint);

		UGameplayStatics::ApplyDamage(
			BoxHit.GetActor(),
			Damage,
			GetInstigator()->GetController(),
			this,
			UDamageType::StaticClass()
		);
	}
}

 

이렇게 해주고 Enemy 클래스에서 TakeDamae를 override해서 사용해주면 된다.

Enemy.cpp

float AEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	if (Attributes)
	{
		Attributes->ReceiveDamage(DamageAmount);
		HealthBarWidget->SetHealthPercent(Attributes->GetHealthPercent());
	}

	return DamageAmount;
}

 

이렇게 해서 테스트해보면 정상적으로 작동하는 것을 볼 수 있다.

 

여기서 체력바를 조금 커스텀해보자. 이를 위해 커스텀 체력바를 다운받아주고 각 이미지을 프로젝트에 import해준 다음 압축 설정을 UserInterface2D로 설정해주자

 

이렇게 해준다음 배경이미지와 필이미지를 커스텀 이미지로 변경해주는데 이때 기존에 색이 영향을 주지않도록 Fill Color의 RGBA값을 모두 1로 설정해주자

그리고 배경의 색이 투명하도록 배경의 색조에 A값을 0으로 설정해주자

그리고 이미지를 활용하여 윤곽선을 나타내도록 해주자

 

이렇게 해주면 커스텀 HP바가 잘 적용된 것을 볼 수 있다.

 

이제 적의 체력이 0이 됐을 때 Death 애니메이션이 출력되도록 만들어보자.

우선 Death 애니메이션을 Mixamo에서 다운받아주자. 이때 전에 해줬던 것처럼 Root본을 추가해주는 작업을 Blender의 플러그인을 통해 해주면 된다. 애니메이션은 Death 애니메이션 중에 마음에 드는 것을 4개정도 다운받아주자.

다운받아준 다음 Blender를 열고 mixamo 사이드탭을 열어준다. 설정값은 다음과 같다. Input Path에 다운받은 폴더를 넣어주고 Output Path는 Convert된 값이 들어갈 곳으로 원하는 폴더를 지정해주면 된다. 설정해준 다음 Convert해주자.

이렇게 변환된 것을 이제 임폴트 해주면 되는데 이때 Skeleton은 SK_Paladin으로 지정해주고 매시 임포트는 꺼주자.

이렇게 해준다음 애니메이션 몽타주를 만들어주자. 이때 각 몽타주 섹션이 독립적이도록 섹션을 만든다음 지우기를 해주어야한다.

 

이제 체력이 0이 될 때 이 몽타주중 하나를 재생하게 해주면된다. 이를 위해 우선 체력이 0보다 큰지 확인해줄 bool 함수를 만들어주자.

AttributeComponent.cpp

bool UAttributeComponent::IsAlive()
{
	return Health > 0.f;
}

 

이렇게 해준 다음 적이 죽었는지 GetHit에서 확인해서 그에 따라 다른 몽타주를 재생하도록 무기에서 충돌할 때 데미지를 먼저 준 다음 GetHit이 실행되도록 순서를 바꿔주자.

Weapon.cpp

if (BoxHit.GetActor())
{
	UGameplayStatics::ApplyDamage(
		BoxHit.GetActor(),
		Damage,
		GetInstigator()->GetController(),
		this,
		UDamageType::StaticClass()
	);

	IHitInterface* HitInterface = Cast<IHitInterface>(BoxHit.GetActor());
	if (HitInterface)
	{
		HitInterface->Execute_GetHit(BoxHit.GetActor(), BoxHit.ImpactPoint);
	}
	IgnoreActors.AddUnique(BoxHit.GetActor());

	CreateFields(BoxHit.ImpactPoint);

	
}

 

이렇게 해준 다음 GetHit에서 살았는지 확인하고 살았다면 HitReact를 죽었다면 Die함수를 실행하도록 하자.

Enemy.cpp

void AEnemy::Die()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
		const int32 Selection = FMath::RandRange(1, 6);
		FString SectionString = FString::Printf(TEXT("Death%d"), Selection); // "Death1", "Death2" 등의 문자열 생성
		FName SectionName = FName(*SectionString); // FString을 FName으로 변환
		AnimInstance->Montage_JumpToSection(SectionName, DeathMontage);
	}
}

void AEnemy::GetHit_Implementation(const FVector& ImpactPoint)
{
	if (Attributes && Attributes->IsAlive())
	{
		DirectionalHitReact(ImpactPoint);
	}
	else
	{
		Die();
	}
	
	

	if (HitSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			this,
			HitSound,
			ImpactPoint
		);
	}

	if (HitParticles&& GetWorld())
	{
		UGameplayStatics::SpawnEmitterAtLocation(
			GetWorld(),
			HitParticles,
			ImpactPoint
		);
	}
}

 

이렇게 해주면 Die 몽타주의 섹션이 재생되지만 바로 Idle로 돌아오기 때문에 계속 누워있을수 있도록 수정해주어야 한다.

이를 위해 각 애니메이션에서 제일 마지막부분을 따로 애니메이션으로 만들기 위해 마지막 부분을 선택한 뒤 에셋생성/애니메이션 생성/현재포즈를 통해 만들어주자.

그 다음 애니메이 블루플린트에서 새로운 State Machine을 만들어주자.

 

이 Main States에는 Idle과 Dead가 있고 내부 에는 각각의 애니메이션이 재생되도록 한다. 

 

이때 Idle과 Dead의 전환 조건도 만들어줘야하고 어떤 Dead애니메이션이 재생될지도 정해줘야한다. 이를 코드로 구현배자 Death포즈 여러개와 Alive 포즈를 정의해줄 enum 클래스를 선언해주자.

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")
};

 

이렇게 정의해준 enum 클래스를 Enemy 클래스에서 사용하도록 하자. 이때 애니메이션 블루프린트에서 사용할 수 있게 UPROPERTY값을 BluePrintReadOnly로 설정해주었다.

Enemy.h

protected:
	UPROPERTY(BlueprintReadOnly)
	EDeathPose DeathPose = EDeathPose::EDP_Alive;

 

그리고 Die함수 내에서 각 Death 애니메이션 섹션에 맞게 enum으로 선언된 변수도 바뀌도록 static_cast를 통해 처리해주었다.

Enemy.cpp

void AEnemy::Die()
{
	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	if (AnimInstance && DeathMontage)
	{
		AnimInstance->Montage_Play(DeathMontage);
		const int32 Selection = FMath::RandRange(1, 6);
		FString SectionString = FString::Printf(TEXT("Death%d"), Selection); // "Death1", "Death2" 등의 문자열 생성
		DeathPose = static_cast<EDeathPose>(RandomIndex);
		FName SectionName = FName(*SectionString); // FString을 FName으로 변환
		AnimInstance->Montage_JumpToSection(SectionName, DeathMontage);
	}
}

 

이렇게 해주고 이 Enum 변수값을 가져오기 위해 애니메이션이 Initialize 될때 Enemy값을 변수로 가져올 수 있도록 해주자.

 

이때 DeathPose의 값을 가져오는 것을 멀티스레딩를 활용하여 가져오자. 이것은 Tread Safe Update Animation을 활용하여 만들 수 있는데 이때 멀티 스레딩에서 값에 안전하게 접근하도록 Property Access를 활용한다.

 

이렇게 해주면 이제 Death Pose가 Alive가 아니라면 Death State로 넘어가면 되게 해주면 된다.

 

이렇게 해준 다음 Dead에서는 DeadPose 변수값을 가져와서 Blend Pose를 통해 각 애니메이션에 맞는 Dead 애니메이션을 마지막에 출력해주도록 하자

 

 

이렇게 해주고 블랜드 타임을 0으로 해주면 잘 작동하는 것을 볼 수 있다. 


이제 만들어둔 HP바의 부모클래스인 유저 위젯을 상속받는 C++클래스를 만들어주자.

 

이때 블루프린트에서 ProgressBar로 HP바를 정의해뒀는데 이 C++ 클래스에서 똑같은 이름으로 바인딩을 UPROPERTY로 해줄 수 있다.

HealthBar.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Blueprint/UserWidget.h"
#include "HealthBar.generated.h"

/**
 * 
 */
UCLASS()
class SLASH_API UHealthBar : public UUserWidget
{
	GENERATED_BODY()
public:
	UPROPERTY(meta = (BindWidget))
	class UProgressBar* HealthBar;
	
};

Enemy클래스에서 블루프린트 객체를 호출하기 위해 부모 객체의 GetUserWidgetObject함수를 사용할 수 있다.

 

이렇게 해주고 블루프린트 상에서 부모클래스를 HealthBar로 바꿔주자.

 

이제 코드상에서 이 위젯을 호출하고 ProgressBar의 속성을 변경할 수 있게 하자.

일단 퍼센트에 따라 체력바가 조정되게 해야한다. 이때 UWidgetComponent에서 자신의 유저 위젯을 반환해주는 함수를

사용해서 현재 사용자 위젯을 가져올 수 있다. 이때 이 함수는 UUserWidget을 반환해주데 우리가 필요한 것은 HealthBar이기 때문에 캐스팅을 해주어야한다.

 

이때 캐스팅이 여러번 이루어지지 않도록 멤버변수를 활용해주자. 그리고 UPROPERTY를 활용해서 기본값에 가비지 값이 할당되지 않도록 하자.

 

HealthBarComponent.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/WidgetComponent.h"
#include "HealthBarComponent.generated.h"

/**
 * 
 */
UCLASS()
class SLASH_API UHealthBarComponent : public UWidgetComponent
{
	GENERATED_BODY()

public:
	void SetHealthPercent(float Percent);

private:
	UPROPERTY()
	class UHealthBar* HealthBarWidget;
};

 

HealthBarComponent.cpp

void UHealthBarComponent::SetHealthPercent(float Percent)
{
	if (HealthBarWidget == nullptr)
	{
		HealthBarWidget = Cast<UHealthBar>(GetUserWidgetObject());
	}
	
	if (HealthBarWidget && HealthBarWidget->HealthBar)
	{
		HealthBarWidget->HealthBar->SetPercent(Percent);
	}
}

 

이렇게 유저위젯관련 코드를 작성해주었으니 Enemy 클래스에서 멤버변수의 타입도 바꿔주고 생성자에서 CreateDefaultSubObject의 클래스명을 바꿔주고 BeginPlay에서 이를 호출하여 테스트해보자.

Enemy.h

private:

	UPROPERTY(VisibleAnywhere)
	class UHealthBarComponent* HealthBarWidget;

 

Enemy.cpp

AEnemy::AEnemy()
{
	PrimaryActorTick.bCanEverTick = true;

	GetMesh()->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GetMesh()->SetGenerateOverlapEvents(true);
	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);

	Attributes = CreateDefaultSubobject<UAttributeComponent>(TEXT("Attributes"));
	HealthBarWidget = CreateDefaultSubobject<UHealthBarComponent>(TEXT("HealthBar"));
	HealthBarWidget->SetupAttachment(GetRootComponent());
}

void AEnemy::BeginPlay()
{
	Super::BeginPlay();
	
	if (HealthBarWidget)
	{
		HealthBarWidget->SetHealthPercent(.1f);
	}
}

 

정상적으로 작동하는 것을 볼 수 있다.

'게임공부 > Unreal Engine' 카테고리의 다른 글

[Unreal Engine][C++]22. Death  (1) 2025.01.26
[Unreal Engine][C++]21. Damage  (1) 2025.01.21
[Unreal Engine][C++]19. Actor Component  (0) 2025.01.14
[Unreal Engine][C++]18. Treasure2  (0) 2025.01.03
[Unreal Engine][C++]17. Treasure  (0) 2025.01.03

 

일반적인 RPG게임에서는 체력이나 경험치, 골드량이 존재한다. 이번에는 이러한 요소들을 생성하고 캐릭터 클래스에 추가해보자. 이때 각 요소들은 모아서 Actor Component로 만들어주자. 

이를 위해 새로운 C++클래스를 생성해주자. 이때 Actor Component를 베이스로 해서 생성해주자.

 

 

일단 우선은 체력만을 가지도록 만들어주자.

AttributeComponent.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/ActorComponent.h"
#include "AttributeComponent.generated.h"


UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class SLASH_API UAttributeComponent : public UActorComponent
{
	GENERATED_BODY()

public:	
	UAttributeComponent();
	virtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;
protected:
	virtual void BeginPlay() override;

private:	
	UPROPERTY(EditAnywhere,Category="Actor Attributes")
	float Health;

	UPROPERTY(EditAnywhere, Category = "Actor Attributes")
	float MaxHealth;
		
};

 

이렇게 만든 클래스를 일단 적 클래스에 적용시켜보자. 

헤더파일에 추가한 다음 생성될 때 컴포넌트로 생성되도록하자.

Enemy.h

private:

	UPROPERTY(VisibleAnywhere)
	class UAttributeComponent* Attributes;

 

Enemy.cpp

#include "Components/AttributeComponent.h"

AEnemy::AEnemy()
{
	PrimaryActorTick.bCanEverTick = true;

	GetMesh()->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GetMesh()->SetGenerateOverlapEvents(true);
	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);

	Attributes = CreateDefaultSubobject<UAttributeComponent>(TEXT("Attributes"));

}

 

이렇게 해주고 핫리로딩해주면 잘 적용되어있는 것을 볼 수 있다.

 

이때 VisibleAnyWhere로 설정되어있기 때문에 Detail패널을 보고 수정할 수 있다.

 

 

이제 적에 머리에 HP바가 달려있도록 구현해보자. 이것은 위젯 컴포넌으로 만들 수 있다.

 

이렇게 생성해주고 구성은 다음과 같이 해주자

 

 

이렇게 해주고 위젯 컴포넌트를 상속받는 C++클래스를 생성해주자.

 

 

이렇게 만들어준 다음 이것을 Enemy 클래스에 추가해보자. 이때 WidgetComponent를 사용하려면 빌드파일에 모듈을 추가해줘야한다.

Slash.Build.cs

PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore","EnhancedInput","HairStrandsCore", "Niagara", "GeometryCollectionEngine", "UMG" });

 

Enemy.h

	private:
    UPROPERTY(VisibleAnywhere)
	class UWidgetComponent* HealthBarWidget;

Enemy.cpp

AEnemy::AEnemy()
{
	PrimaryActorTick.bCanEverTick = true;

	GetMesh()->SetCollisionObjectType(ECollisionChannel::ECC_WorldDynamic);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Visibility, ECollisionResponse::ECR_Block);
	GetMesh()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GetMesh()->SetGenerateOverlapEvents(true);
	GetCapsuleComponent()->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);

	Attributes = CreateDefaultSubobject<UAttributeComponent>(TEXT("Attributes"));
	HealthBarWidget = CreateDefaultSubobject<UWidgetComponent>(TEXT("HealthBar"));
	HealthBarWidget->SetupAttachment(GetRootComponent());
}

 

이렇게 해주면 BP_Enemy에 체력바를 넣을 수 있는 위젯 컴포넌트가 생긴것을 볼 수 있다. 여기서 만들어둔 체력바를 넣어주고 스페이스를 Screen으로 바꿔주면 된다. 

 

이렇게 하면 적 머리위에 체력바가 잘 부착되어있는 것을 볼 수 있다.

 

이제 항아리를 부쉈을 때 다양한 종류의 보물이 나오도록 구현해보자.

이를 구현하기에 앞서 플레이어가 모은 보물의 양에 따라 골드량이 달라지는 것을 구현해보자. 

우선 Treasure 클래스에 Gold변수를 추가해서 보물이 가진 골드량을 나타내도록 하자

Treasure.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Items/Item.h"
#include "Treasure.generated.h"

/**
 * 
 */
UCLASS()
class SLASH_API ATreasure : public AItem
{
	GENERATED_BODY()
protected:
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;
private:
	UPROPERTY(EditAnywhere, Category = Sounds)
	USoundBase* PickupSound;

	UPROPERTY(EditAnywhere, Category = "Treasure Properties")
	int32 Gold;
};

 

이렇게 해주고 Treasure 클래스를 바탕으로 만들어둔 블루프린트 클래스가 기본으로 두 다른 파생 블루프린트 클래스를 만들어주자.

 

보면 부모의 이벤트를 자동으로 호출하고 있는것도 볼 수 있다.

 

이제 이 방법으로 다양한 아이템을 만들어주자.

 

이렇게 만들어준 보물들중에 랜덤하게 스폰하도록 코드로 구현해보자. 이를 위해 TArray를 사용할 것이다.

BreakableActor.h

private:	

	UPROPERTY(EditAnywhere, Category="Breakable Properties")
	TArray<TSubclassOf<class ATreasure>> TreasureClasses;

 

BreakableActor.cpp

void ABreakableActor::GetHit_Implementation(const FVector& ImpactPoint)
{
	UWorld* World = GetWorld();
	if (World && TreasureClasses.Num() > 0)
	{
		FVector Location = GetActorLocation();
		Location.Z += 75.f;

		int32 Selection = FMath::RandRange(0, TreasureClasses.Num() - 1);

		World->SpawnActor<ATreasure>(TreasureClasses[Selection], Location, GetActorRotation());
	}
}

 

이렇게 해주면 랜덤하게 보물이 드랍된다.

 

여기에 약간의 효과를 추가해보자 이번에는 이펙트에 나이아가라 시스템을 사용해볼 것이다.

이를 위해 나이아가라시스템을 만들어주자.

 

이렇게 생성해주면 기본적인 요소들이 만들어져있다.

이 요소들을 살펴보면 다양하게 있는데 이를 통해 이펙트가 만들어지게 되는 것이다. 이 하나하나가 규칙이라고 보면 된다.

이 중에 렌더는 말 그대로 화면에 보이도록하는 것인데 지금은 하얀 입자 하나하나가 스프라이더 이고 이를 보이게 해준다.

 

Emitter State는 생명주기에 관한것으로 Self는 자체 수명 주기를 사용하는 것이다. 만약 이것을 시스템으로 바궈주면 시스템에서 생명주기를 정해주게 되는 것이다. 무한정 반복되며 주기는 2초이다.

 

SpawnRate 초당 스폰되는 Emitter의 수이다.

파티클 스폰은 스폰되는 모양에 관한 것으로 각 입자에 대한 초기값이다. 이때 Color값을 1보다 높게하면 반짝이는 효과를 낼 수 있다.

 

그리고 Shape Location을 조절해주는 것으로 이펙트 산란구역을 설정해줄 수 있다.

 

이펙트의 속도도 느리게 하고 나오는 각도도 조절해주자

 

그리고 이 이펙트가 자동으로 종료되는 것이 아닌 우리가 종료시키고 싶기 때문에 Kill Particles를 해제해준다.

 

Drag는 커질수록 입자의 속도를 느리게 해준다.

 

여기에 입자들이 소용돌이처럼 돌도록 하기위해 Vortex Velocity를 추가해주자. 그리고 Kill Particles를 다시 체크해주자.

 

효과는 성공적으로 만들었으니 이를 모든 아이템에서 작동하도록 아이템 코드에 나이아가라 시스템 변수 추가해주자.

Item.h

	private:
    UPROPERTY(EditAnywhere)
	class UNiagaraComponent* EmbersEffect;

Item.cpp

#include "NiagaraComponent.h"

// Sets default values
AItem::AItem()
{
	// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
	PrimaryActorTick.bCanEverTick = true;

	ItemMesh = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("ItemMeshComponent"));
	RootComponent = ItemMesh;

	Sphere = CreateDefaultSubobject<USphereComponent>(TEXT("Sphere"));
	Sphere->SetupAttachment(GetRootComponent());

	EmbersEffect = CreateDefaultSubobject<UNiagaraComponent>(TEXT("Embers"));
	EmbersEffect->SetupAttachment(GetRootComponent());
}

 

컴파일해준다음 BaseTreasure에 만들어둔 나이아가라 시스템을 추가해주자.

 

잘작동하는 것을 볼 수 있다.

 

이를 무기에도 붙여주는데 무기는 장착했을 때 이 효과가 없어지게 해주자.

Weapon.cpp

#include "NiagaraComponent.h"

void AWeapon::Equip(USceneComponent* Inparent, FName InSocketName)
{
	AttachMeshToSocket(Inparent, InSocketName);
	ItemState = EItemState::EIS_Equipped;
	if (EquipSound)
	{
		UGameplayStatics::PlaySoundAtLocation(
			this,
			EquipSound,
			GetActorLocation()
		);
	}
	if (Sphere)
	{
		Sphere->SetCollisionEnabled(ECollisionEnabled::NoCollision);
	}
	if (EmbersEffect)
	{
		EmbersEffect->Deactivate();
	}
}


이제 부숴지는 항아리는 만들었으니 부숴졌을 때 보물같은 것이 나오도록 구현해보자

우선 fab에서 무료 보물 에셋을 다운받아주자.

https://www.fab.com/ko/listings/a4c1584f-e305-4562-ad37-4c1483e09a10

다운받은 다음 보물이 나타날때 소리효과를 추가하기 위해 다음 사이트에서 Coin sfx를 하나 다운받아주자.

https://soundimage.org/

 

Soundimage.org | Thousands of free music tracks, sounds and images for your projects by Eric Matyas

“Ancient Mars”_Looping  (You can freely download this music track in MP3 format from my Sci-Fi 12 page…enjoy!) ______________________________________________________________________________ The purpose of this site to make good-sounding music, sound

soundimage.org

 

mp3로 다운받아 질텐데 wav로 변환한 다음 임포트해주고 이를 바탕으로 meta sound를 만들어주자.

이렇게 해준 다음 아이템 클래스를 상속받는 Treasure 클래스를 생성해주자.

 

이때 OnSphereOverlap 함수를 가져와서 재정의 해줄것이다. 이때 파생클래스는 UFUNCTION을 사용할 수 없다.

플레이와 overlap될 때 보물이 없어지고 소리가 재생되도록 해주자.

Treasure.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Items/Item.h"
#include "Treasure.generated.h"

/**
 * 
 */
UCLASS()
class SLASH_API ATreasure : public AItem
{
	GENERATED_BODY()
protected:
	virtual void OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult) override;
private:
	UPROPERTY(EditAnywhere, Category = Sounds)
	USoundBase* PickupSound;
};

Treasure.cpp

// Fill out your copyright notice in the Description page of Project Settings.


#include "Items/Treasure.h"
#include "Characters/SlashCharacter.h"
#include "Kismet\GameplayStatics.h"

void ATreasure::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor, UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	ASlashCharacter* SlashCharacter = Cast<ASlashCharacter>(OtherActor);
	if (SlashCharacter)
	{
		if (PickupSound)
		{
			UGameplayStatics::PlaySoundAtLocation(
				this,
				PickupSound,
				GetActorLocation()
			);
		}
		Destroy();
	}
}

 

컴파일해준 다음 이 클래스를 기반으로 블루프린트 클래스를 만들어주자. 만들어준 다음 소리를 추가해주고 매쉬를 적당한 매쉬로 추가해주자.

 

 

 

항아리가 깨졌을 때 스폰되게 만들어야하지만 일단 함수가 올바르게 작동하는지 확인하기 위해 씬에 하나 배치해보자.

시작하고 가까이 다가가면 소리가 재생되면서 없어지는 것을 볼 수 있다.

 

이제 항아리가 부숴지면 항아리가 있던 자리 중앙에 보물이 생성되게 만들어야한다. 이를 코드로 구현해보자. SpawnActor 함수를 통해 소환해줄 수 있다.
이때 함수의 정의와 매개변수에 대해 찾아보면 UClass를 매개변수로 받고있는 것을 볼 수 있다.

 

우선 코드상에서 각 클래스를 지정해주는 변수에 대해 알아보면 다음과 같다. 

UClass 변수를 지정해주고 블루프린트상에서 할당해주면 블루프린트 클래스를 가져다가 쓸수 있다.

 

항아리가 부숴질때 Actor를 Spawn하기 위해 Breakable 클래스에 UClass변수를 추가해주자.

BreakableActor.h

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Interfaces/HitInterface.h"
#include "BreakableActor.generated.h"

class UGeometryCollectionComponent;

UCLASS()
class SLASH_API ABreakableActor : public AActor, public IHitInterface
{
	GENERATED_BODY()

private:	
	UPROPERTY(EditAnywhere)
	UClass* TreasureClass;
};

 

이렇게 해주고 블루프린트상에서도 할당을 해주자.

 

이렇게 해주고 SpawnActor에서 이 변수를 매개변수로 사용하면 C++클래스 대신 블루프린트 클래스가 소환된다.

BreakableActor.cpp

void ABreakableActor::GetHit_Implementation(const FVector& ImpactPoint)
{
	UWorld* World = GetWorld();
	if (World && TreasureClass)
	{
		FVector Location = GetActorLocation();
		Location.Z += 75.f;
		World->SpawnActor<ATreasure>(TreasureClass, Location, GetActorRotation());
	}
}

 

컴파일 해주면 보물이 잘 생성되는 것을 볼 수 있다.

 

이때 UClass 변수로 생성해줬기 때문에 할당할 수 있는 것에 제한이 없다. 이러한 점을 보완하기 위해서 TSubclassOf라는 변수를 사용한다.

 

BreakableActor.h

private:	
	UPROPERTY(VisibleAnywhere)
	UGeometryCollectionComponent* GeometryCollection;

	UPROPERTY(EditAnywhere)
	TSubclassOf<class ATreasure> TreasureClass;
};

 

이렇게 해주고 컴파일해주면 선택할 수 있는 Actor가 제한되어있는것을 볼 수 있다.

 

그리고 여기서 부수고 난 다음에 충돌을 끄기 위해 UCapsuleComponent를 사용하자. 이것을 통해 Pawn이 통과하지 못하지만 깨지고 난 다음에는 지나다닐 수 있도록하자.

BreakableActor.h

	protected:
    UPROPERTY(VisibleAnywhere, BlueprintReadWrite)
	class UCapsuleComponent* Capsule;

 

BreakableActor.cpp

#include "Breakable/BreakableActor.h"
#include "Components/CapsuleComponent.h"
#include "GeometryCollection/GeometryCollectionComponent.h"
#include "Items/Treasure.h"

ABreakableActor::ABreakableActor()
{

	PrimaryActorTick.bCanEverTick = false;

	GeometryCollection = CreateDefaultSubobject<UGeometryCollectionComponent>(TEXT("GeometryCollection"));
	SetRootComponent(GeometryCollection);
	GeometryCollection->SetGenerateOverlapEvents(true);
	GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Camera, ECollisionResponse::ECR_Ignore);
	GeometryCollection->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Ignore);

	Capsule = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
	Capsule->SetupAttachment(GetRootComponent());
	Capsule->SetCollisionResponseToAllChannels(ECollisionResponse::ECR_Ignore);
	Capsule->SetCollisionResponseToChannel(ECollisionChannel::ECC_Pawn, ECollisionResponse::ECR_Block);

}

 

이렇게 해주고 LifeSpan이 끝날때 이 캡슐과 Pawn의 충돌도 Ignore로 바꿔준다.

 

이렇게 해주면 잘 작동하는 것을 볼 수 있다.

+ Recent posts