Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

게임 개발 메모장

UE5 게임 프로그래머 포트 폴리오 본문

언리얼 엔진/Portpolio

UE5 게임 프로그래머 포트 폴리오

Dev_Moses 2023. 12. 23. 18:15

적용 기술 : GAS ( Gameplay Ability System) 

 

목차

1. 초기화 ( Ability System Component 생성 )

2. 게임 어빌리티를 활용한 플레이어 캐릭터 공격 과정

3. Enemy 캐릭터 

 

※ 좌,우 슬라이드로 조정하지 않아도 해당 코드 블록을 마우스 왼쪽으로 클릭을 해서 키보드 ' ←'  '→' 키를 사용해서 좌, 우로 코드를 볼 수 있습니다. 


1. 초기화 ( Ability System Component 생성 )

Character 나 Actor에 클래스에 Component ( 구성 요소 ) 로 부착하는 방식입니다.

 

- AYjCharacterBase 클래스 코드 일부 ( OwnerActor )

void AYjCharacterBase::InitializeDefaultAttributes() const
{
	// 서버 Section : 캐릭터의 속성들을 가지고 있는 이펙트들을 카테고리 별로 3종류로 나눔.
	ApplyEffectToSelf(DefaultPrimaryAttributes, 1.f);
	ApplyEffectToSelf(DefaultSecondaryAttributes, 1.f);
	ApplyEffectToSelf(DefaultVitalAttributes, 1.f);
}

void AYjCharacterBase::ApplyEffectToSelf(TSubclassOf<UGameplayEffect> GameplayEffectClass, float Level) const
{
	// 게임 플레이 이펙트를 자신에게 적용 시킴. ( 일종의 바인딩 작업 )
	check(IsValid(GetAbilitySystemComponent()));
	check(GameplayEffectClass);

	FGameplayEffectContextHandle ContextHandle = GetAbilitySystemComponent()->MakeEffectContext();
	ContextHandle.AddSourceObject(this);

	const FGameplayEffectSpecHandle SpecHandle = GetAbilitySystemComponent()->MakeOutgoingSpec(GameplayEffectClass, Level, ContextHandle);
	GetAbilitySystemComponent()->ApplyGameplayEffectSpecToTarget(*SpecHandle.Data.Get(), GetAbilitySystemComponent());
}

void AYjCharacterBase::AddCharacterAbilities()
{
	UYjAbilitySystemComponent* YjASC = CastChecked<UAuraAbilitySystemComponent>(AbilitySystemComponent);
	if (false == HasAuthority())
        {
           return;
        }

        // 초기 두 종류의 어빌리티를 등록한다.
	YjASC->AddCharacterAbilities(StartupAbilities);
	YjASC->AddCharacterPassiveAbilities(StartupPassiveAbilities);
}

 

- AYjCharacter 클래스 코드 일부 ( AvatarActor )

/* 빙의 함수를 호출해 캐릭터의 오너로 설정됨 (서버에서만 호출) */
void AYjCharacter::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	InitAbilityActorInfo();
	LoadProgress();

	AYjGameModeBase* const YjGameModeBase = Cast<AYjGameModeBase>(UGameplayStatics::GetGameMode(this));
	if(YjGameModeBase == nullptr)
	{
		return;
	}

	YjGameModeBase->LoadWorldState(GetWorld());
}

/* 서버에서 저장 정보들을 플레이어 스테이트에 Load 하는 과정 */
void AYjCharacter::LoadProgress()
{
	AYjGameModeBase* YjGameModeBase = Cast<AYjGameModeBase>(UGameplayStatics::GetGameMode(this));
	if(YjGameModeBase == nullptr)
	{
		return;
	}

	ULoadScreenSaveGame* const SaveData = YjGameModeBase->RetrieveInGameSaveData();
	if (SaveData == nullptr)
	{
		return;
	}

	if (SaveData->bFirstTimeLoadIn)
	{
		InitializeDefaultAttributes();
		AddCharacterAbilities();
	}
	else
	{
		UYjAbilitySystemComponent* YjAbilitySystemComponent = Cast<UYjAbilitySystemComponent>(AbilitySystemComponent);
		if(YjAbilitySystemComponent == nullptr)
		{
			return;
		}

		YjAbilitySystemComponent->AddCharacterAbilitiesFromSaveData(SaveData);

		AYjPlayerState* YjPlayerState = Cast<AYjPlayerState>(GetPlayerState());
		if (YjPlayerState == nullptr)
		{
			return;
		}

		YjPlayerState->SetLevel(SaveData->PlayerLevel);
		YjPlayerState->SetXP(SaveData->XP);
		YjPlayerState->SetAttributePoints(SaveData->AttributePoints);
		YjPlayerState->SetSpellPoints(SaveData->SpellPoints);

		UYjAbilitySystemLibrary::InitializeDefaultAttributesFromSaveData(this, AbilitySystemComponent, SaveData);
	}
}

/* 클라이언트 쪽 어빌리티 액터 초기화 */
void AYjCharacter::InitAbilityActorInfo()
{
	/* 어빌리티 액터 정보 초기화 */
    AYjPlayerState* const YjPlayerState = GetPlayerState<AYjPlayerState>();
    if(nullptr == YjPlayerState)
    {
        return;
    }

    YjPlayerState->GetAbilitySystemComponent()->InitAbilityActorInfo(YjPlayerState, this);
    Cast<UYjAbilitySystemComponent>(YjPlayerState->GetAbilitySystemComponent())->AbilityActorInfoSet();
	
    /* 어빌리티 시스템 컴포넌트와 어트리뷰트 셋은 각각 클라이언트의 게임 스테이트가 가짐.*/
    AbilitySystemComponent = YjPlayerState->GetAbilitySystemComponent();
    AttributeSet = YjPlayerState->GetAttributeSet();
	
    OnAscRegistered.Broadcast(AbilitySystemComponent);

    AYjPlayerController* const YjPlayerController = Cast<AYjPlayerController>(GetController());
    if (nullptr == YjPlayerController)
    {
    	return;
    }

    AYjHUD* const YjHUD = Cast<AYjHUD>(YjPlayerController->GetHUD());
    if (nullptr == YjHUD)
    {
    	return;
    }
    
    /* HUD 초기화 */
    YjHUD->InitOverlay(YjPlayerController, YjPlayerState, AbilitySystemComponent, AttributeSet);
}

 

 


2. 게임 어빌리티를 활용한 플레이어 캐릭터 공격 과정

 

[ 플레이어 캐릭터의 파이어볼 발사 ]

 

- YjPlayerController 

* 키보드 입력 바인딩 세팅 ( 마우스 오른쪽 버튼 : 파이어볼 발사 ) ,

* 오른쪽 버튼 InputTag가 입력이 되면 YjPlayerController에서 

  GetASC()->AbilityInputTagHeld() 호출 해서 InputTag를 넘깁니다.

 

 

void AYjPlayerController::SetupInputComponent()
{
	Super::SetupInputComponent();

	UYjInputComponent* const YjInputComponent = CastChecked<UYjInputComponent>(InputComponent);

	YjInputComponent->BindAction(MoveAction, ETriggerEvent::Triggered, this, &AYjPlayerController::Move);
	YjInputComponent->BindAction(ShiftAction, ETriggerEvent::Started, this, &AYjPlayerController::ShiftPressed);
	YjInputComponent->BindAction(ShiftAction, ETriggerEvent::Completed, this, &AYjPlayerController::ShiftReleased);
	YjInputComponent->BindAbilityActions(InputConfig, this, &ThisClass::AbilityInputTagPressed, &ThisClass::AbilityInputTagReleased, &ThisClass::AbilityInputTagHeld);
}

void AYjPlayerController::AbilityInputTagHeld(FGameplayTag InputTag)
{
        if(nullptr == GetASC())
        {
                return;
        }

	if (true == GetASC()->HasMatchingGameplayTag(FYjGameplayTags::Get().Player_Block_InputHeld))
	{
		return;
	}

        // InputTag가 LMB(왼쪽 마우스 버튼 클릭)가 아니면 
	if (false == InputTag.MatchesTagExact(FYjGameplayTags::Get().InputTag_LMB)) 
	{
		GetASC()->AbilityInputTagHeld(InputTag);	
		return;
	}

	if (TargetingStatus == ETargetingStatus::TargetingEnemy || bShiftKeyDown)
	{
		GetASC()->AbilityInputTagHeld(InputTag);
	}
	else
	{
		FollowTime += GetWorld()->GetDeltaSeconds();
		if (true == CursorHit.bBlockingHit)
		{
			CachedDestination = CursorHit.ImpactPoint;
		}

		APawn* const ControlledPawn = GetPawn();
		if (nullptr == ControlledPawn)
		{
			return;
		}

		const FVector WorldDirection = (CachedDestination - ControlledPawn->GetActorLocation()).GetSafeNormal();
		ControlledPawn->AddMovementInput(WorldDirection);		
	}
}

 

- YjAbilitySystemComponent

* TryActivateAbility()를 호출하여 InputTag가 있는 Ability를 활성화 시킵니다.

void UYjAbilitySystemComponent::AbilityInputTagHeld(const FGameplayTag& InputTag)
{
	if (false == InputTag.IsValid())
        {
            return;
        }

	FScopedAbilityListLock ActiveScopeLoc(*this);
	
        for (FGameplayAbilitySpec& AbilitySpec : GetActivatableAbilities())
	{
		if (true == AbilitySpec.DynamicAbilityTags.HasTagExact(InputTag))
		{
			AbilitySpecInputPressed(AbilitySpec);
			
                        if (false == AbilitySpec.IsActive())
			{
				TryActivateAbility(AbilitySpec.Handle);
			}
		}
	}
}

 

 

- AN_MontageEvent ( 몽타쥬 노티파이 )

* 해당 메시 컴포넌트를 소유한 액터에게 Event Tag를 전달할 수 있게 세팅합니다.

 

- AM_Cast_FireBolt (애님 몽타쥬) 

: AN_MontageEvent ( 노티파이 ) 를 원하는 동작에 세팅을 하고 EventTag (Event.Montage.FireBolt) 설정 

 

 

- GA_FireBolt  ( 게임 어빌리티 )

발사체 클래스, 데미지 이펙트 클래스, 데미지 타입, 입력 태그 (마우스 오른쪽),

어빌리티 태그  세팅 

(Ability Tags : 해당 어빌리티가 가지는 태그, 어빌리티의 식별자 역할을한다. 이 태그를 통해 어빌리티를 활성, 취소, 금지할 수 있다.

Block Abilities with Tag : 이 어빌리티가 실행되는 동안 해당 태그를 가진 어빌리티는 금지됨)

 

 

마우스 오른쪽 버튼을 클릭을 하면 GA_FireBolt 의  ActivateAbility 이벤트 가 실행 됩니다.

오른쪽 버튼을 눌렀을 때 Avatar 캐릭터가 타겟 위치로 방향을 돌려서 타겟팅 될 액터의 위치 값을 가지고 발사체를 스폰하고, 스폰될 발사체 액터는 Input SocketTag 위치에서 스폰됩니다.

 


Enemy의 데미지 이펙트 적용 ]

 

발사체 콜리젼과 Enemy의 콜리젼이 오버랩되면 해당 발사체의 데미지에 따라서 Damage 이펙트가 적용이되고이펙트 적용이 끝나면 PostGameplayEffectExecute() 가 호출이되면서 Enemy의 HP가 차감이 되고,

Effects_HitReact 태그에 의해서 Hit 어빌리티 활성화 이벤트가 호출됩니다.

 

- 발사체 콜리젼 오버랩

void AYjProjectile::OnSphereOverlap(UPrimitiveComponent* OverlappedComponent, AActor* OtherActor,
                                      UPrimitiveComponent* OtherComp, int32 OtherBodyIndex, bool bFromSweep, const FHitResult& SweepResult)
{
	UGameplayStatics::PlaySoundAtLocation(this, ImpactSound, GetActorLocation(), FRotator::ZeroRotator);
	UNiagaraFunctionLibrary::SpawnSystemAtLocation(this, ImpactEffect, GetActorLocation());
	LoopingSoundComponent->Stop();

	if (true == HasAuthority())  // 서버
	{
		UAbilitySystemComponent* TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor)
		
		if (UAbilitySystemComponent* const TargetASC = UAbilitySystemBlueprintLibrary::GetAbilitySystemComponent(OtherActor))
		{
			TargetASC->ApplyGameplayEffectSpecToSelf(*DamageEffectSpecHandle.Data.Get());
		}
		
		Destroy();
	}
	else
	{
		bHit = true;
	}
}

 

 

-  데미지 이펙트 실행 후 조건에 따라 HitReact 어빌리티 활성화 과정 

void UYjAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	Super::PostGameplayEffectExecute(Data);
	
	FEffectProperties Props;
	SetEffectProperties(Data, Props);

	if (Data.EvaluatedData.Attribute == GetHealthAttribute())
	{
		SetHealth(FMath::Clamp(GetHealth(), 0.f, GetMaxHealth()));
	}
	if (Data.EvaluatedData.Attribute == GetManaAttribute())
	{
		SetMana(FMath::Clamp(GetMana(), 0.f, GetMaxMana()));
	}

	if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
	{
		const float LocalIncomingDamage = GetIncomingDamage();  // 데미지의 경우에는 MetaAttribute이기 때문에 복제 대상이 아님 (잠깐 사용)
		SetIncomingDamage(0.f);
		
		if (LocalIncomingDamage > 0.f)
		{
			const float NewHealth = GetHealth() - LocalIncomingDamage;
			SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));

			const bool bFatal = NewHealth <= 0.f;  // HP가 0 이하로 떨어지지 않았다면 HitReact 어빌리티 활성화 시도
			if (false == bFatal)
			{
				FGameplayTagContainer TagContainer;
				TagContainer.AddTag(FYjGameplayTags::Get().Effects_HitReact);
				Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
			}
		}
	}
}

 

 

-  Enemy 클래스에서 HitReact Effect가 활성화 될 때, WalkSpeed를 감소 시킴,  

void AYjEnemy::BeginPlay()
{
	Super::BeginPlay();
	GetCharacterMovement()->MaxWalkSpeed = BaseWalkSpeed;
	InitAbilityActorInfo();

    // ---- 생략 -----
	if (const UYjAttributeSet* AuraAS = Cast<UYjAttributeSet>(AttributeSet))
	{
  		// ---- 생략 -----
		
		AbilitySystemComponent->RegisterGameplayTagEvent(FYjGameplayTags::Get().Effects_HitReact, EGameplayTagEventType::NewOrRemoved).AddUObject(
			this,
			&AYjEnemy::HitReactTagChanged
		);
	}
	  // ---- 생략 -----
}

void AYjEnemy::HitReactTagChanged(const FGameplayTag CallbackTag, int32 NewCount)
{
	bHitReacting = NewCount > 0;
	GetCharacterMovement()->MaxWalkSpeed = bHitReacting ? 0.f : BaseWalkSpeed;
}

 

- GA_HitReact 의 ActivateAbility 이벤트가 호출되면서 HitReact 몽타쥬를 실행 시킴

 

- Damage Effect가 적용될 때 Enemy의 HP가 0이 되면 죽음

void UYjAttributeSet::PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data)
{
	Super::PostGameplayEffectExecute(Data);
	
        // --------------- 생략 -----------------
	if (Data.EvaluatedData.Attribute == GetIncomingDamageAttribute())
	{
		const float LocalIncomingDamage = GetIncomingDamage();
		SetIncomingDamage(0.f);
		if (LocalIncomingDamage > 0.f)
		{
			const float NewHealth = GetHealth() - LocalIncomingDamage;
			SetHealth(FMath::Clamp(NewHealth, 0.f, GetMaxHealth()));

			const bool bFatal = NewHealth <= 0.f;
			if (true == bFatal)
			{
				ICombatInterface* const CombatInterface = Cast<ICombatInterface>(Props.TargetAvatarActor);
                                if(nullptr == CombatInterface)
                                {
                                     return;
                                }
		
                                CombatInterface->Die();     // HP가 0 이하면 Die 함수를 호출 
			}
			else
			{
				FGameplayTagContainer TagContainer;
				TagContainer.AddTag(FYjGameplayTags::Get().Effects_HitReact);
				Props.TargetASC->TryActivateAbilitiesByTag(TagContainer);
			}
		}
	}
}

 


3. Enemy 캐릭터 

 

 

- YjEnemy 클래스에서 서버가 해당 액터를 소유할 때, AI 컨트롤러, 블랙보드 값들을 초기화 시키고

  비헤이비어 트리를 실행 시킴

void AYjEnemy::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

        // * 서버 Section
	if (false == HasAuthority()) 
        {
            return;                    
        }

	YjAIController = Cast<AYjAIController>(NewController);
	YjAIController->GetBlackboardComponent()->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
	YjAIController->RunBehaviorTree(BehaviorTree);
	YjAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), false);
	YjAIController->GetBlackboardComponent()->SetValueAsBool(FName("RangedAttacker"), CharacterClass != ECharacterClass::Warrior);
}

 

 

-  비헤이비어 트리에서 매 틱마다 타겟을 탐색하는 서비스 구현 

void UBTService_FindNearestPlayer::TickNode(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
	Super::TickNode(OwnerComp, NodeMemory, DeltaSeconds);

	APawn* const OwningPawn = AIOwner->GetPawn();

	//* 소유한 폰이 플레이어인지 Enemy인지 확인 "피아 식별"
	const FName TargetTag = OwningPawn->ActorHasTag(FName("Player")) ? FName("Enemy") : FName("Player");

	TArray<AActor*> ActorsWithTag;
	UGameplayStatics::GetAllActorsWithTag(OwningPawn, TargetTag, ActorsWithTag);

	//* 타겟 액터 중 가장 가까운 액터와, 가장 가까운 거리를 구함
	float ClosestDistance = TNumericLimits<float>::Max();
	AActor* ClosestActor = nullptr;

	for (AActor* Actor : ActorsWithTag)
	{
		if (IsValid(Actor) && IsValid(OwningPawn))
		{
			const float Distance = OwningPawn->GetDistanceTo(Actor);
			if (Distance < ClosestDistance)
			{
				ClosestDistance = Distance;
				ClosestActor = Actor;
			}
		}
	}
	
	//* 블랙 보드에 갱신
	UBTFunctionLibrary::SetBlackboardValueAsObject(this,TargetToFollowSelector, ClosestActor);
	UBTFunctionLibrary::SetBlackboardValueAsFloat(this, DistanceToTargetSelector, ClosestDistance);
}

 

 

-  Enemy BehaviorTree 구현 

 

RangeAttacker ( 범위 공격자 클래스 ), MeleeAttacker ( 근접 공격자 클래스 ) 두 가지로 분류를 했습니다.조건은 Enemy 클래스가 초기화 될 때, 종류에 따라서 ECharacterClass 변수를 초기화 시켜주는데 Warrior (근접)만 아니면 블랙보드의 bRangeAttacker 가 true가 되게 하였습니다.

void AYjEnemy::PossessedBy(AController* NewController)
{
	Super::PossessedBy(NewController);

	if (false == HasAuthority()
	{
		return;
	}

	YjAIController = Cast<AYjAIController>(NewController);
	YjAIController->GetBlackboardComponent()->InitializeBlackboard(*BehaviorTree->BlackboardAsset);
	YjAIController->RunBehaviorTree(BehaviorTree);
	YjAIController->GetBlackboardComponent()->SetValueAsBool(FName("HitReacting"), false);
	YjAIController->GetBlackboardComponent()->SetValueAsBool(FName("RangedAttacker"), CharacterClass != ECharacterClass::Warrior);
}

 

 

BT의 Selector가 RangeAttacker가 아니면  MeleeAttacker 쪽 브랜치를 실행시켜주는데 Player와 거리가 충분한지 계산을 한 뒤에 Sequence 노드가 실행이 됩니다. 타겟(Player)에 도달한 뒤에 BTT_Attack 태스크가 실행이 됩니다.

 

 

BTT_Attack 태스크가 실행이 되면 블랙보드의 TargetSelector를 현재 컨트롤되고 있는 Enemy 액터의 SetCombatTarget

함수를 호출해서 넘겨주고 현재 액터가 가지고 있는 AttackTag가 부여된 Ability를 실행 시켜줍니다.

 

 

 

Abilities.Attack 태그가 부여된 GA_MeleeAttack 어빌리티가 실행이됩니다.

GE_Damage 이펙트가 실행이되고 해당 객체의 Attribute의 HP에 - Damage 수치가 적용이 되어

리플리케이티드가 되게 하였습니다.

 

'언리얼 엔진 > Portpolio' 카테고리의 다른 글

.  (0) 2023.12.29