본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 4 - 게임플레이 어빌리티 시스템의 시작 (2/10)

728x90
반응형

 

 

  • 정리
    • 언리얼 게임플레이 어빌리티 시스템의 시작
      1. 회전하는 분수대 기획을 세 가지 방법으로 구현
        1. 액터를 확장해 구현 : 익숙한 방법으로 빠르게 구현. 새로운 액터 기능이 추가됨 (액터의 규모가 커지면서 굉장히 관리하기 어려워질 수 있다.)
        2. GAS를 사용해 구현 : C++만 사용해 모든 기능 구현 가능. 액터로부터 기능 분리
        3. 게임플레이 태그를 추가 활용해 구현 : 의존성 없는 설계가 가능. 어빌리티 교체 가능
      2. 액터의 역할을 최소화시키는데 주력
      3. 게임플레이 태그를 활용해 의존성을 분리
      4. C++ 프로그래밍 없이도 게임플레이 태그 규약만 잘 지키면 시스템을 유연하게 확장할 수 있음

 

  • 게임플레이 어빌리티 시스템의 기본 익히기
  • 분수대 액터의 기획
    • 3초마다 회전과 정지를 무한 반복하면서 동작하는 분수대
    • 회전기능은 RotatingMovement 컴포넌트를 사용함
    • 이를 어떻게 구현?
      1. 액터에 해당 기능을 구현
      2. 게임플레이 어빌리티 시스템으로 구현
      3. 게임플레이 어빌리티 시스템에 게임플레이 태그를 부여해 구현
    • 3가지 구현 방법의 장단점 비교
  • GAS 시스템을 적용할 액터에다가 ASC를 부착한 후에 ASC로 하여금 지정한 게임플레이 어빌리티를 발동시키도록 지정할 예정
  • 게임플레이 태그를 활용해 보다 효과적으로 해당 기능들을 구현

기본 흐름

 

  • 어빌리티 시스템 컴포넌트
    • 줄여서 ASC(Ability System Component)로 불림
    • 게임플레이 어빌리티 시스템을 관리하는 핵심 컴포넌트
    • 게임플레이 어빌리티 및 다양한 작업을 관리하는 처리하는 중앙 처리 장치
    • 액터에 단 하나만 부착할 수 있음
    • 액터는 부착된 ASC를 통해 게임플레이 어빌리티를 발동시킬 수 있음
    • ASC를 부착한 액터 사이에 GAS 시스템의 상호 작용이 가능해짐

 

 

Timer에 맞춰 3초마다 분수대가 멈추는 모습

 

  • 게임플레이 어빌리티
    • 줄여서 GA(Gameplay Ability)로 불림
    • ASC에 등록되어 발동시킬 수 있는 액션 명령
      • 공격, 마법, 특수 공격 등등..
      • 간단한 액션 뿐만 아니라 상황에 따른 복잡한 액션 수행 가능
    • GA의 발동 과정
      • ASC에 어빌리티를 등록 : ASC의 GiveAbility 함수에 발동할 GA의 타입을 전달
        • 발동할 GA 타입 정보를 게임플레이 어빌리티 스펙(GameplayAbilitySpec)이라고 함
      • ASC에게 어빌리티를 발동하라고 명령 : ASC의 TryActivateAbility함수에 발동할 GA의 타입을 전달
        • ASC에 등록된 타입이면 GA의 인스턴스가 생성됨
      • 발동된 GA에는 발동한 액터와 실행 정보가 기록됨
        • SpecHandle : 발동된 어빌리티에 대한 핸들
        • ActorInfo : 어빌리티의 소유자와 아바타 정보
        • ActivationInfo : 발동 방식에 대한 정보
    • GA의 주요 함수
      • CanActivateAbility : 어빌리티가 발동될 수 있는지 파악
      • ActivateAbility : 어빌리티가 발동될 때 호출
      • CancelAbility : 어빌리티가 취소될 때 호출
      • EndAbility : 스스로 어빌리티를 마무리할 때 호출

 

 

	// ----------------------------------------------------------------------------------------------------------------
	//
	//	The important functions:
	//	
	//		CanActivateAbility()	- const function to see if ability is activatable. Callable by UI etc
	//
	//		TryActivateAbility()	- Attempts to activate the ability. Calls CanActivateAbility(). Input events can call this directly.
	//								- Also handles instancing-per-execution logic and replication/prediction calls.
	//		
	//		CallActivateAbility()	- Protected, non virtual function. Does some boilerplate 'pre activate' stuff, then calls ActivateAbility()
	//
	//		ActivateAbility()		- What the abilities *does*. This is what child classes want to override.
	//	
	//		CommitAbility()			- Commits reources/cooldowns etc. ActivateAbility() must call this!
	//		
	//		CancelAbility()			- Interrupts the ability (from an outside source).
	//
	//		EndAbility()			- The ability has ended. This is intended to be called by the ability to end itself.
	//	
	// ----------------------------------------------------------------------------------------------------------------
  • GameplayAbility.h

 

  • GA의 주요 함수들
#include "CoreMinimal.h"
#include "Abilities/GameplayAbility.h"
#include "ABGA_Rotate.generated.h"

UCLASS()
class ARENABATTLEGAS_API UABGA_Rotate : public UGameplayAbility
{
	GENERATED_BODY()
	
public:
	virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
	virtual void CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility) override;
};
  • ABGA_Rotate.h
#include "GA/ABGA_Rotate.h"
#include "GameFramework/RotatingMovementComponent.h"

void UABGA_Rotate::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);

	// AvatarActor - 주로 보여지는 액터!
	AActor* AvatarActor = ActorInfo->AvatarActor.Get();
	if (AvatarActor)
	{
		// 컴포넌트 가져오기
		URotatingMovementComponent* RotatingMovement = Cast<URotatingMovementComponent>(AvatarActor->GetComponentByClass(URotatingMovementComponent::StaticClass()));
		if (RotatingMovement)
		{
			RotatingMovement->Activate(true);
		}
	}
}

void UABGA_Rotate::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)
{
	Super::CancelAbility(Handle, ActorInfo, ActivationInfo, bReplicateCancelAbility);

	// AvatarActor - 주로 보여지는 액터!
	AActor* AvatarActor = ActorInfo->AvatarActor.Get();
	if (AvatarActor)
	{
		// 컴포넌트 가져오기
		URotatingMovementComponent* RotatingMovement = Cast<URotatingMovementComponent>(AvatarActor->GetComponentByClass(URotatingMovementComponent::StaticClass()));
		if (RotatingMovement)
		{
			RotatingMovement->Deactivate();
		}
	}
}
  • ABGA_Rotate.cpp

 

  • GameplayAbility를 상속받은 클래스를 생성해주고, 주요 함수중 ActivateAbilityCancelAbilityoverride하여 구현해준다.
#pragma once

#include "CoreMinimal.h"
#include "Prop/ABFountain.h"
#include "AbilitySystemInterface.h"		// GAS를 추가하기 위해서 이것을 소유하고 있는 액터는 AbilitySystemInterface라고 하는 인터페이스를 구현해주는 것이 좋다.
#include "ABGASFountain.generated.h"

UCLASS()
class ARENABATTLEGAS_API AABGASFountain : public AABFountain, public IAbilitySystemInterface
{
	GENERATED_BODY()
	
public :
	AABGASFountain();

	// IAbilitySystemInterface 구현부
	virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;

protected:
	virtual void PostInitializeComponents() override;
	virtual void BeginPlay() override;

	// 주기적으로 반복할 타이머 함수
	virtual void TimerAction();

protected:
	UPROPERTY(VisibleAnywhere, Category = Movement)
	TObjectPtr<class URotatingMovementComponent> RotatingMovement;

	UPROPERTY(EditAnywhere, Category = Timer)
	float ActionPeriod;

	// 실제 액터가 ASC를 가지고 있어야 함
	UPROPERTY(EditAnywhere, Category = GAS)
	TObjectPtr<class UAbilitySystemComponent> ASC;

	FTimerHandle ActionTimer;
};
  • ABGASFountain.h
#include "Prop/ABGASFountain.h"
#include "GameFramework/RotatingMovementComponent.h"
#include "ArenaBattleGAS.h"
#include "AbilitySystemComponent.h"
#include "GameplayAbilitySpec.h"
#include "GA/ABGA_Rotate.h"

AABGASFountain::AABGASFountain()
{
	ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));

	RotatingMovement = CreateDefaultSubobject<URotatingMovementComponent>(TEXT("RotateMovement"));
	ActionPeriod = 3.0f;
}

UAbilitySystemComponent* AABGASFountain::GetAbilitySystemComponent() const
{
	return ASC;
}

void AABGASFountain::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	RotatingMovement->bAutoActivate = false;
	RotatingMovement->Deactivate();

	// 초기화할때 ASC도 같이 초기화해줘야 함
	// 인자 - InOwnerActor, InAvatarActor 정보 두가지가 들어감.
	// InOwnerActor - 실제 ASC를 구동하고 데이터를 관리하는 실제 작업이 일어나고 있는 액터 정보를 의미
	// InAvatarActor - 실제로 데이터를 처리하지 않지만 비주얼만 수행해주는 액터를 지정
	// InOwnerActor == InAvatarActor의 경우 데이터도 관리, 실제 동작도 관리하는 형태로 작업을 초기화한다라는 의미
	// ASC 초기화
	ASC->InitAbilityActorInfo(this, this);
	
	//ASC에 GameplayAbilitySpec 넘겨주기
	FGameplayAbilitySpec RotateSkillSpec(UABGA_Rotate::StaticClass());
	ASC->GiveAbility(RotateSkillSpec);

}

void AABGASFountain::BeginPlay()
{
	Super::BeginPlay();

	// SetTimer, 핸들은 ActionTimer, 클래스의 TimerAction을 바인드, 루프 주기는 ActionPeriod, 반복유무 true, 바로 시작하도록 0초
	GetWorld()->GetTimerManager().SetTimer(ActionTimer, this, &AABGASFountain::TimerAction, ActionPeriod, true, 0.0f);
}

void AABGASFountain::TimerAction()
{
	ABGAS_LOG(LogABGAS, Log, TEXT("Begin"));

#pragma region GA 사용 

	FGameplayAbilitySpec* RotateGASpec = ASC->FindAbilitySpecFromClass(UABGA_Rotate::StaticClass());
	if (!RotateGASpec)
	{
		ABGAS_LOG(LogABGAS, Error, TEXT("No Rotate Spec Found!"));

		return;
	}

	// FGameplayAbilitySpec이 활성화 되있는지 확인하고, 활성화 안되어 있으면 Active 되어 있으면 Cancel
	if (!RotateGASpec->IsActive())
	{
		ASC->TryActivateAbility(RotateGASpec->Handle);
	}
	else
	{
		ASC->CancelAbilityHandle(RotateGASpec->Handle);
	}

#pragma endregion
}
  • ABGASFountain.cpp

 

  • 동일한 기능을 GA를 통해 구현.

 

  • IAbilitySystemInterface를 상속받아 구현부(GetAbilitySystemComponent)를 작성해주고, 실제 액터가 ASC를 가지고 있어야하기 때문에 UAbilitySystemComponentASC를 선언해주고, PostInitializeComponents에서 초기화 해주고 GameplayAbilitySpecGiveAbility를 통해 넘겨준다.

 

 

  • 게임플레이 태그
    • FName으로 관리되는 경량의 표식 데이터
      • 액터나 컴포넌트에 지정했던 태그와 다른 데이터
    • 프로젝트 설정에서 별도로 게임플레이 태그를 생성하고 관리할 수 있음
      • 결과는 DefaultGameplayTags.ini 파일에 저장됨
    • 계층 구조로 구성되어 있어 체계적인 관리 가능
      • Actor.Action.Rotate : 행동에 대한 태그
      • Actor.State.IsRotating : 상태에 대한 태그
    • 게임플레이 태그들의 저장소 : GameplayTagContainer
      • 계층 구조를 지원하는 검색 기능 제공
      • HasTagExact : 컨테이너에 A.1태그가 있는 상황에서 A와 B로 찾으면 false
      • HasAny : 컨테이너에 A.1태그가 있는 상황에서 A와 B로 찾으면 true
      • HasAnyExact : 컨테이너에 A.1태그가 있는 상황에서 A와 B로 찾으면 false
      • HasAll : 컨테이너에 A.1태그와 B.1태그가 있는 상황에서 A와 B로 찾으면 true
      • HasAllExact : 컨테이너에 A.1태그와 B.1태그가 있는 상황에서 A와 B로 찾으면 false
    • 게임플레이 어빌리티 시스템과 독립적으로 사용 가능
  • 게임플레이 어빌리티와 게임플레이 태그
    • 게임플레이 어빌리티에 부착한 태그
      • 어빌리티에 지정한 태그 (AbilityTags 태그 컨테이너)
    • 게임플레이 어빌리티에 대해 다양한 실행 조건의 설정
      • 태그로 어빌리티 취소 (CancelAbilitiesWithTag 태그 컨테이너)
      • 태그로 어빌리티 차단 (BlockAbilitesWithTag 태그 컨테이너)
      • 어빌리티 실행시 태그 설정 (ActivationOwnedTags 태그 컨테이너)
      • 태그가 있어야만 어빌리티 실행 (ActivationRequiredTags 태그 컨테이너)
      • 태그가 있으면 어빌리티 실행 차단 (ActivationBlockedTags 태그 컨테이너)
      • 시전자가 태그가 있어야 어빌리티 실행 (SourceRequiredTags 태그 컨테이너)
      • 시전자에 태그가 있으면 어빌리티 차단 (SourceBlockedTags 태그 컨테이너)
      • 시전 대상에 태그가 있어야 어빌리티 실행 (TargetRequiredTags 태그 컨테이너)
      • 시전 대상에 태그가 있으면 어빌리티 차단 (TargetBlockedTags 트개 컨테이너)
  • 블루프린트와 조합하여 특정 게임 플레이 어빌리티에 대한 의존성을 없애고 게임플레이 태그를 중심으로 게임 로직을 전개 가능

 

Project Settings>GameplayTags 게임플레이태그 생성

 

[/Script/GameplayTags.GameplayTagsSettings]
ImportTagsFromConfig=True
WarnOnInvalidTags=True
ClearInvalidTags=False
AllowEditorTagUnloading=True
AllowGameTagUnloading=False
FastReplication=False
InvalidTagCharacters="\"\',"
NumBitsForContainerSize=6
NetIndexFirstBitSegment=16
+GameplayTagList=(Tag="Actor.Action.Rotate",DevComment="")
+GameplayTagList=(Tag="Actor.State.IsRotating",DevComment="")
  • DefaultGameplayTags.ini

 

  • 게임플레이 태그를 생성하고, Refesh visual studio를 하면 DefaultGameplayTags.ini에 생성한 태그가 나오는걸 확인할 수 있다.

 

#pragma once

#include "GameplayTagContainer.h"		// GameplayTag를 활용하기 위한

// 실제 태그 넣기
// 명령 하나가 하나의 변수처럼 사용됨
#define ABTAG_ACTOR_ROTATE FGameplayTag::RequestGameplayTag(FName("Actor.Action.Rotate"))

#define ABTAG_ACTOR_ISROTATING FGameplayTag::RequestGameplayTag(FName("Actor.State.IsRotating"))
  • ABGameplayTag.h
  • txt파일을 만들어 다음과 같이 파일 내용을 채워준다.

 

#pragma once

#include "CoreMinimal.h"
#include "Prop/ABFountain.h"
#include "AbilitySystemInterface.h"		// GAS를 추가하기 위해서 이것을 소유하고 있는 액터는 AbilitySystemInterface라고 하는 인터페이스를 구현해주는 것이 좋다.
#include "ABGASFountain.generated.h"

UCLASS()
class ARENABATTLEGAS_API AABGASFountain : public AABFountain, public IAbilitySystemInterface
{
	GENERATED_BODY()
	
	// ...

	UPROPERTY(EditAnywhere, Category = GAS)
	TArray<TSubclassOf<class UGameplayAbility>> StartAbilities;

	FTimerHandle ActionTimer;
};
  • ABGASFountain.h
#include "Prop/ABGASFountain.h"
#include "GameFramework/RotatingMovementComponent.h"
#include "ArenaBattleGAS.h"
#include "AbilitySystemComponent.h"
#include "GameplayAbilitySpec.h"
#include "Tag/ABGameplayTag.h"
#include "Abilities/GameplayAbility.h"
//#include "GA/ABGA_Rotate.h"

// ...

void AABGASFountain::PostInitializeComponents()
{
	Super::PostInitializeComponents();

	RotatingMovement->bAutoActivate = false;
	RotatingMovement->Deactivate();

	// 초기화할때 ASC도 같이 초기화해줘야 함
	// 인자 - InOwnerActor, InAvatarActor 정보 두가지가 들어감.
	// InOwnerActor - 실제 ASC를 구동하고 데이터를 관리하는 실제 작업이 일어나고 있는 액터 정보를 의미
	// InAvatarActor - 실제로 데이터를 처리하지 않지만 비주얼만 수행해주는 액터를 지정
	// InOwnerActor == InAvatarActor의 경우 데이터도 관리, 실제 동작도 관리하는 형태로 작업을 초기화한다라는 의미
	// ASC 초기화
	ASC->InitAbilityActorInfo(this, this);
	
	for (const auto& StartAbility : StartAbilities)
	{
		// ASC에 GameplayAbilitySpec 넘겨주기
		FGameplayAbilitySpec StartSpec(StartAbility);
		ASC->GiveAbility(StartSpec);
	}
}

void AABGASFountain::BeginPlay()
{
	Super::BeginPlay();

	// SetTimer, 핸들은 ActionTimer, 클래스의 TimerAction을 바인드, 루프 주기는 ActionPeriod, 반복유무 true, 바로 시작하도록 0초
	GetWorld()->GetTimerManager().SetTimer(ActionTimer, this, &AABGASFountain::TimerAction, ActionPeriod, true, 0.0f);
}

void AABGASFountain::TimerAction()
{
	ABGAS_LOG(LogABGAS, Log, TEXT("Begin"));

	// 게임플레이 태그 사용
	FGameplayTagContainer TargetTag(ABTAG_ACTOR_ROTATE);

	if (!ASC->HasMatchingGameplayTag(ABTAG_ACTOR_ISROTATING))
	{
		ASC->TryActivateAbilitiesByTag(TargetTag);
	}
	else
	{
		ASC->CancelAbilities(&TargetTag);
	}
}
  • ABGASFountain.cpp

 

  • TArray를 통해 GameplayAbility를 담아주고, PostInitializeComponents에서 GameplayAbilitySpec을 넘겨주도록 한다.
  • TimerAction에서 게임플레이 태그를 사용하여 동일한 동작을 하도록 구현해준다.

 

 

 

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part4 - 게임플레이 어빌리티 시스템>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part4 - 게임플레이 어빌리티 시스템 강의 | 이득우 - 인프런

이득우 | 언리얼 엔진5의 게임플레이 어빌리티 시스템을 활용해 RPG게임의 각종 스킬 시스템을 효과적으로 구현하는 방법을 학습합니다., 게임 제작의 혁신, 언리얼 게임플레이 어빌리티 시스템

www.inflearn.com

 

728x90