Replicated

[ChronoSpace] Behavior Tree + Ability & Effect + Camera Shake 본문

언리얼 엔진/ChronoSpace

[ChronoSpace] Behavior Tree + Ability & Effect + Camera Shake

라구넹 2025. 2. 17. 02:48

계획: 저번 게시글에서 만들었던 Patrol이 캐릭터에 어느 정도 가까워지면 어빌리티를 발동해서 Trace 하고, Effect를 적용해서 에너지를 감소시킬 거임

예전에 중력 반전 만들 때 Trace로 처리하려다가 말았던 적이 있는데 해당 코드 재활용할 거임

 

(어빌리티 특성 상 코드가 좀 길어 헤더 파일은 안 넣을 것인데, 참고하실 분들은 깃허브 봐주세요)

 

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


#include "GA/CSGA_GiveDamage.h"
#include "GA/AT/CSAT_MultiTrace.h"
#include "GA/TA/CSTA_MultiTrace.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "ChronoSpace.h"

UCSGA_GiveDamage::UCSGA_GiveDamage()
{
	NetExecutionPolicy = EGameplayAbilityNetExecutionPolicy::ServerOnly;
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;

	static ConstructorHelpers::FClassFinder<UGameplayEffect> DamageEffectRef(TEXT("/Game/Blueprint/GA/GE/BPGE_PatrolDamage.BPGE_PatrolDamage_C"));
	if ( DamageEffectRef.Succeeded() )
	{
		DamageEffect = DamageEffectRef.Class;
	}
}

void UCSGA_GiveDamage::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
	//UE_LOG(LogCS, Log, TEXT("ActivateAbility - GiveDamage"));

	UCSAT_MultiTrace* DamageTraceTask = UCSAT_MultiTrace::CreateTask(this, ACSTA_MultiTrace::StaticClass());
	DamageTraceTask->OnComplete.AddDynamic(this, &UCSGA_GiveDamage::OnTraceResultCallback);
	DamageTraceTask->ReadyForActivation();
}

void UCSGA_GiveDamage::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
	int32 idx = 0;
	FGameplayEffectSpecHandle EffectSpecHandle = MakeOutgoingGameplayEffectSpec(DamageEffect);
	//UE_LOG(LogCS, Log, TEXT("OnTraceResultCallback"));
	if (EffectSpecHandle.IsValid())
	{
		//UE_LOG(LogCS, Log, TEXT("OnTraceResultCallback - EffectSpecHandle Is Valid"));
		ApplyGameplayEffectSpecToTarget(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, EffectSpecHandle, TargetDataHandle);
	}

	/*while (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, idx))
	{
		FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, idx);
		
		UE_LOG(LogCS, Log, TEXT("HitResult : %s"), *HitResult.GetActor()->GetName());

		++idx;
	}*/

	bool bReplicatedEndAbility = true;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}

게임플레이 어빌리티이다

주석 처리한 부분 제외하면 Trace로 얻어낸 액터들 확인 가능하다

 

이펙트는 30의 대미지를 주도록 설정한다

저기 Add라 되어 있는데 Override로 바꿔야 한다

 

기억해둘 부분은, TargetDataHandle에 여러 데이터가 들어 있는 경우

이펙트 적용 시 알아서 각각 적용해 준다

 

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


#include "GA/AT/CSAT_MultiTrace.h"
#include "GA/TA/CSTA_MultiTrace.h"
#include "AbilitySystemComponent.h"

UCSAT_MultiTrace::UCSAT_MultiTrace()
{
}

UCSAT_MultiTrace* UCSAT_MultiTrace::CreateTask(UGameplayAbility* OwningAbility, TSubclassOf<class ACSTA_MultiTrace> TargetActorClass)
{
	UCSAT_MultiTrace* NewTask = NewAbilityTask<UCSAT_MultiTrace>(OwningAbility);
	NewTask->TargetActorClass = TargetActorClass;

	return NewTask;
}

void UCSAT_MultiTrace::Activate()
{
	Super::Activate();
	SpawnAndInitializeTargetActor();
	FinalizeTargetActor();

	SetWaitingOnAvatar();
}

void UCSAT_MultiTrace::OnDestroy(bool AbilityEnded)
{
	if (SpawnedTargetActor)
	{
		SpawnedTargetActor->Destroy();
	}

	Super::OnDestroy(AbilityEnded);
}

void UCSAT_MultiTrace::SpawnAndInitializeTargetActor()
{
	SpawnedTargetActor = Cast<ACSTA_MultiTrace>(GetWorld()->SpawnActorDeferred<ACSTA_MultiTrace>(TargetActorClass, FTransform::Identity, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn));
	if (SpawnedTargetActor)
	{
		SpawnedTargetActor->SetOwner(GetOwnerActor());
		SpawnedTargetActor->TargetDataReadyDelegate.AddUObject(this, &UCSAT_MultiTrace::OnTargetDataReadyCallback);
	}
}

void UCSAT_MultiTrace::FinalizeTargetActor()
{
	UAbilitySystemComponent* ASC = AbilitySystemComponent.Get();

	if (ASC)
	{
		const FTransform SpawnTransform = ASC->GetAvatarActor()->GetTransform();
		SpawnedTargetActor->FinishSpawning(SpawnTransform);

		ASC->SpawnedTargetActors.Add(SpawnedTargetActor);
		SpawnedTargetActor->StartTargeting(Ability);
		SpawnedTargetActor->ConfirmTargeting();
	}
}

void UCSAT_MultiTrace::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& DataHandle)
{
	if (ShouldBroadcastAbilityTaskDelegates())
	{
		OnComplete.Broadcast(DataHandle);
	}

	EndTask();
}

어빌리티 태스크이다

 

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


#include "GA/TA/CSTA_MultiTrace.h"
#include "Abilities/GameplayAbility.h"
#include "AbilitySystemBlueprintLibrary.h"
#include "GameFramework/Character.h"
#include "Components/CapsuleComponent.h"
#include "DrawDebugHelpers.h"
#include "Physics/CSCollision.h"

ACSTA_MultiTrace::ACSTA_MultiTrace()
{
	bShowDebug = true;
}

void ACSTA_MultiTrace::StartTargeting(UGameplayAbility* Ability)
{
	Super::StartTargeting(Ability);
	SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();
}

void ACSTA_MultiTrace::ConfirmTargetingAndContinue()
{
	if (SourceActor)
	{
		FGameplayAbilityTargetDataHandle DataHandle = MakeTargetData();
		TargetDataReadyDelegate.Broadcast(DataHandle);
	}
}

FGameplayAbilityTargetDataHandle ACSTA_MultiTrace::MakeTargetData() const
{
	ACharacter* Character = CastChecked<ACharacter>(SourceActor);

	UAbilitySystemComponent* ASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(SourceActor);
	if (ASC == nullptr) return FGameplayAbilityTargetDataHandle();
	
	TArray< FHitResult > OutHitResults;
	const float AttackRange = 50.0f;
	const float AttackRaduis = 100.0f;

	FCollisionQueryParams Params(SCENE_QUERY_STAT(UCSAT_ReverseGravityTrace), false, Character);
	const FVector Forward = Character->GetActorForwardVector();
	const FVector Start = Character->GetActorLocation() + Forward * Character->GetCapsuleComponent()->GetScaledCapsuleRadius();
	const FVector End = Start + Forward * AttackRange;

	bool HitDetected = GetWorld()->SweepMultiByChannel(OutHitResults, Start, End, FQuat::Identity, CCHANNEL_CSACTION, FCollisionShape::MakeSphere(AttackRaduis), Params);

	FGameplayAbilityTargetDataHandle DataHandle;
	if (HitDetected)
	{
		for (auto OutHitResult : OutHitResults)
		{
			FGameplayAbilityTargetData_SingleTargetHit* TargetData = new FGameplayAbilityTargetData_SingleTargetHit(OutHitResult);
			DataHandle.Add(TargetData);
		}
	}

#if ENABLE_DRAW_DEBUG
	if (bShowDebug)
	{
		FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
		float CapsultHalfHeight = AttackRange * 0.5f;
		FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;
		DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsultHalfHeight, AttackRaduis, FRotationMatrix::MakeFromZ(Forward).ToQuat(), DrawColor, false, 5.0f);
	}
#endif

	return DataHandle;
}

타겟 액터이다

MakeTargetData에서 SeepMultiByChannel을 이용해 여러 액터들을 얻어낸다

 

이렇게 만들어진 어빌리티를

 

Patrol에서 부여해주고

 

외부(정확히는 행동 트리)에서 Active 시킬 수 있도록 public 함수로 하나 만들어준다

 

그리고 행동 트리를 수정해준다

MoveTo의 Acceptable Radius에 도달하면 MoveTo는 성공 판전이 된다

왼쪽 시퀀스에서 MoveTo성공 시 플레이어를 어느 정도 따라 잡았다는 이야기이다

그렇다면 이제 어빌리티를 발동해서 Energy를 깎아주면 된다

(그 후 잠시 Wait해서 난이도를 조정했다. 없으면 그대로 플레이어가 죽을 것이다)

 

Acceptable Radius는 MoveTo 노드에서 조정 가능

 

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


#include "BT/BTTask_ActivateGiveDamage.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "Player/CSAIController.h"
#include "Character/CSCharacterPatrol.h"
#include "ChronoSpace.h"

UBTTask_ActivateGiveDamage::UBTTask_ActivateGiveDamage()
{
	NodeName = TEXT("ActivateGiveDamage");
}

EBTNodeResult::Type UBTTask_ActivateGiveDamage::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
	EBTNodeResult::Type Result = Super::ExecuteTask(OwnerComp, NodeMemory);

	ACSCharacterPatrol* Patrol = Cast<ACSCharacterPatrol>(OwnerComp.GetAIOwner()->GetPawn());

	if (Patrol)
	{
		Patrol->ActivateGiveDamage();
	}

	return EBTNodeResult::Succeeded;
}

ActivateAGiveDamage 태스크 노드이다

대미지를 주었든 안주었든 일단 공격은 한 거니까 성공 처리를 해준다

 

 

작동 확인 완료

 

그런데 멀티 테스트를 하려니까 크래시난다

트리거 오버랩 콜백에 권한 체크 넣어줘야 한다

행동 트리 모델은 서버에서만 실행되니 관련 코드에는 안해줘도 된다

 

 

그런데, 에너지가 깎일 때 플레이어의 카메라를 흔들고 싶다

CameraShake 라는데 언리얼 엔진에 있다고 해서 그걸 써볼 것이다

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/camera-shakes-in-unreal-engine

찾아보면 뭔가 레거시 쓰는게 많은데 굳이 싶어서 그냥 공식 문서 보고 만들자

 

CameraShakeBase를 상속받아 블루프린트를 만들어준다

Pattern은 Perlin Noise를 써보자

 

옵션 조절도 해주고

 

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


#include "Player/CSPlayerController.h"
#include "ChronoSpace.h"

ACSPlayerController::ACSPlayerController()
{
	static ConstructorHelpers::FClassFinder<UCameraShakeBase> CameraShakeRef(TEXT("/Game/Blueprint/Camera/BP_CameraShake.BP_CameraShake_C"));
	if ( CameraShakeRef.Succeeded() )
	{
		CameraShake = CameraShakeRef.Class;
	}
}

void ACSPlayerController::ShakeCamera()
{
	ClientStartCameraShake(CameraShake);
}

플레이어 컨트롤러를 만들어준다

 

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


#include "Attribute/CSAttributeSet.h"
#include "ChronoSpace.h"
#include "GameplayEffectExtension.h"
#include "Player/CSPlayerController.h"
#include "CoreMinimal.h"

UCSAttributeSet::UCSAttributeSet() : MaxEnergy(100.0f), Damage(0.0f)
{
	InitEnergy(GetMaxEnergy());
}

void UCSAttributeSet::PreAttributeChange(const FGameplayAttribute& Attribute, float& NewValue)
{
	
	if (Attribute == GetDamageAttribute())
	{
		NewValue = NewValue < 0.0f ? 0.0f : NewValue;
	}
	
	// -> 최소 데미지 0.0f 으로 설정. 
}

bool UCSAttributeSet::PreGameplayEffectExecute(FGameplayEffectModCallbackData& Data)
{
	return true;
}

void UCSAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	Super::PostGameplayEffectExecute(Data);

	float MinimumEnergy = 0.0f;

	if (Data.EvaluatedData.Attribute == GetEnergyAttribute())
	{
		UE_LOG(LogTemp, Warning, TEXT("Direct Health Access : %f"), GetEnergy());
		SetEnergy(FMath::Clamp(GetEnergy(), MinimumEnergy, GetMaxEnergy()));
	}

	if ( Data.EvaluatedData.Attribute == GetDamageAttribute() )
	{
		UE_LOG(LogCS, Log, TEXT("Damage Detected : %f"), GetDamage());
		SetEnergy(FMath::Clamp(GetEnergy() - GetDamage(), MinimumEnergy, GetMaxEnergy()));
		SetDamage(0.0f);
        
		AActor* TargetActor = Data.Target.GetAvatarActor();
		if (TargetActor == nullptr) return;

		if (APawn* Pawn = Cast<APawn>(TargetActor))
		{
			ACSPlayerController* PC = Cast<ACSPlayerController>(Pawn->GetController());

			if (PC)
			{
				PC->ShakeCamera();
			}
		}
	}
}

그리고 어트리뷰트 셋에 PostGameplatEffectExecute에서 카메라를 컨트롤러를 가져와서 카메라를 흔든다

 

 

정상 작동 확인 가능하다

영상으로 찍진 않았는데 멀티플레이에서도 카메라 멀쩡히 흔들린다