본문 바로가기

언리얼5/공부기록

UE 어빌리티 시스템 1_1 (정리) ASC , GA TAG

GAS란?

언리얼 게임 어빌리티 시스템은 전투스탯, 스킬 , 피격 이벤트 발생 시 처리 등을 개발자가 가독성을 가지고

개발에 임할 수 있도록 언리얼에서 지원하는 플러그인을 의미한다.

 

https://dev.epicgames.com/documentation/ko-kr/unreal-engine/gameplay-ability-system-for-unreal-engine?application_version=5.2

 

언리얼 엔진의 게임플레이 어빌리티 시스템

게임플레이 어빌리티 시스템 개요

dev.epicgames.com

 

GAS 프레임워크의 기능과 사용시 장단점

  • 액터가 소유하고 발동할 수 있는 어빌리티 및 액터 간의 인터랙션 기능을 제공하는 프레임워크이다.
  • RPG, 액션 어드벤처, MOBA 장르의 제작을 쉽게하기 위한 도구, 대부분의 게임 제작 활용 가능
  • 장점
    유연성과 확장성 : 다양하고 복잡한 게임 제작에 대응할 수 있도록 범용 적으로 설계
    모듈러 시스템 : 각 기능에 대한 의존성이 최소화되도록 설계
    네트워크 지원 : 네트웍 멀티플레이어 게임에서도 활용 가능하도록 설계
    데이터 기반 설계 : 데이터를 기반으로 동작하도록 설계
    완성도 : 배포된 대규모 게임사에서도 실제 사용하고 있는 검증된 실효성
     
  • 단점
    매우 어렵다.. : GAS를 사용하기 위해 반드시 알아야할 구성요소 단위가 많다.

어빌리티 시스템 구성 요소

 

  • 어빌리티 시스템 컴포넌트 : GAS 프레임 워크를 관리하고 처리하는 액터의 중앙 처리장치와 같다. 처음 시작 단위가 된다.
  • 게임플레이 태그 : 프로젝트의 레벨 레이어에서 액터들의 상태나 행동을 관리하는데 사용된다.
  • 게임플레이 어빌리티 : 모든 스킬 액션 어빌리티가 발동해서 시작되는 곳이다
  • 게임플레이 이펙트 :  전투에 의해 발생되는 이벤트들의 효과 단위를 의미한다 연출단위가 아니다
  • 어트리뷰트 : 스탯 데이터들의 단위를 의미한다

 


GAS System Flow

 

위 자료사진은 정해진 프레임이 아니라 정리된 사용방법이다

단위별 서로 다른 접근이 가능하지만 별다른 규칙없이 서로 다른 단위를 접근하는 플로우를 사용하면 매우 위험한 사용법이 될수있다.

 

  1. 처음은 중앙처리장치와 같은 어빌리티 시스템 컴포넌트를 액터에서 생성한다.
    액터가 취할 액션(Ability)를 부여한다.
  2. 부여된 조건의 Ability가 발동 조건이 성립되면 Action을 수행한다.
    해당 액션이 주변 또는 다른 대상에게 영향을 입히면 해당 정보는 게임 이펙트로 취급된다.
  3. 게임 이펙트가 발동되면 Attribute(스탯 데이터)에 영향을 준다.

AbilitTask  : 실제게임에서는 특정 공격 또는 스킬의 어빌리티가 발동되면 게임 로직외 연출이 같이 필요로한다
이러한 게임 연출에는 시간의 지연이 필요로하는데 해당 시연되는 정보를 Ability Task 단위로 나눈다.
AbilityTask의 조합을 가지고 어빌리티를 만들면 특정 애니메이션 연출을 합친 공격단위를 만들수있다.

Gameplay Cue: 게임이펙트가 발생되면 해당 단위에 맞춰서 시각적, 또는 청각적인 연출이 필요로 하는경우

GameplayCue를 조합하여 우리가 흔히 하는 이펙트를 만들수가 있다.


환경설정

 

1. GAS 플러그인

 

Setting-> Plugin-> GAS 추가

 

 

 

 

 

 

 

 

 

2. Build.cs 
PublicDependencyModuleNames에 GameplyAbilities GameTags, GameplayTasks 추가

      PublicDependencyModuleNames.AddRange(new string[]
        {
	        "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "UMG", "NavigationSystem", "AIModule", "GameplayTasks",
	        "GameplayAbilities", "GameplayTags" , "GameplayTasks"
        });

GAS의 기초적인 사용법 이해하기

 

기획내용
3초마다 회전과 정지를 반복하는 오브젝트 회전기능은 RotatingMovement 컴포넌트와

GAS의 게임어빌리티를 사용해서 구현

에픽
1. 액터에 해당 기능을 구현

2. 게임플레이 어빌리티 시스템으로 구현

3. 게임플레이 어빌리티 시스템에 게임 플레이 태그를 부여해 구현


어빌리티 시스템 컴포넌트 ASC

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

게임 플레이 어빌리티 GA

  • ASC에 등록되어 발동시킬 수 있는 액션 명령
    공격 마법 특수 공격 등
    간단한 액션 뿐만 아니라 상황에 따른 복잡한 액션 수행 가능
  • GA의 발동 과정
    ASC에 어빌리티를 등록 : ASC의 GiveAbility 함수에 발동할 GA의 타입을 전달
     - 발동한 GA 타입 정보를 게임플레이 어빌리티 스펙(GameplayAbiliySpec)이라고함.

    ASC에게 어빌리티를 발동하라고 명령 : ASC의  TryActivateAbility함수에 발동할 GA의 타입을 전달 (타입은 클래스타입으로 전달)
     - ASC에 등록된 타입이면 GA의 인스턴스가 생성됨

    발동된 GA에는 발동한 액터와 실행 정보가 기록됨.
     - SpecHandle : 발동된 어빌리티에 대한 핸들
     - ActorInfo : 어빌리티의 소유자와 아바타 정보
     - ActivationInfo : 발동 방식에 대한 정보
    •  GA의 주요 함수
       - CanActivateAbility : 어빌리티가 발동될 수 있는지 파악
       - ActivateAbility : 어빌리티가 발동될 때 함수
       - CancelAbility : 어빌리티가 취소될 때 호출
       - EndAbility : 스스로 어빌리티를 마무리할 때 호출

어빌리티 컴포넌트 사용법

//어빌리티를 사용하기 위해 필요한 헤더
//#include "AbilitySystemInterface.h"

class ARENABATTLE_API AABGASFountain : public AABFountain ,public  IAbilitySystemInterface
{
	GENERATED_BODY()
public:
	AABGASFountain();
	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;

	UPROPERTY(EditAnywhere, Category = GAS)
	TObjectPtr<class UAbilitySystemComponent> ASC;

	FTimerHandle ActionTimer;
	
	
};

 

  1. 헤더 AbilitySystemInterface를 추가한다.
  2. IAbilitySystemInterface를 상속받는다
  3. 기능구현을 위한 GAS 인터페이스 가상 함수와 PostInitalizeCompoenent 2개를 오버라이딩한다
    변수
     - UAbilitySystemCompoenent(ASC) :게임 어빌리티 컴포넌트 포인터 변수
    함수
    - GetAbilitySystemComponent : 게임 어벌리티 컴포넌트 Getter
    - PostInitializeComponents :
    엑터의 컴포넌트 초기화가 완료된 이후 호출됩니다
    엑터의 생명주기중 가장 마지막단에 호출됩니다.
  4. 현제는 기능 구현을 위한 세션을 만들었고 실제 엑터가 돌아가게끔 동작을하는 단위는
    GameplayAbility(GA)에서 처리됩니다
  5. GA를 상속받는 클래스를 생성합니다.

 

4  기능구현을 위한 세션만들기

void AABGASFountain::BeginPlay()
{
	Super::BeginPlay();
	GetWorld()->GetTimerManager().SetTimer(ActionTimer,this,&AABGASFountain::TimerAction,ActionPeriod,true,0.0f);
}

타이머로 3초마다 ActionPeriod를 호출한다.

 

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

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

	
	//초기화될때 어빌리티 시스템도 초기화 해줘야함
	//인자값1 Owner : ASC가 구동되고 작업이 실제 일어나는 존재
	//인자값2 Avatar : 데이터를 처리하지않지만 비주얼만 수행해주는 존재
    //지금은 Ower Avatar가 동일하다.
	ASC->InitAbilityActorInfo(this,this);
	
	const FGameplayAbilitySpec RotateSkillSpec(UABGA_Rotate::StaticClass());
	//발동할 정보만 기본적인 타입반환을위해 가지고있는다.
	
	ASC->GiveAbility(RotateSkillSpec);
}

 

1. 로테이션의 이동을 비활성화 한다

2.ASC가 이후 GA에게 토스할 엑터정보들을 정의하기위해 InitAbilityActorInfo를 초기화한다.
InitAbilityActorInfo 인자값(Actor , Actor)
Owner - ASC가 구동되고 작업이 실제 일어나는 대상의 엑터를 의미한다.
Avatar - 데이터를 처리하지않지만 비주얼만 수행하는 존재의 액터를 의미한다.
3.GA가 처리될때 어떤 이벤트를 처리해야할지 알수없기에
해당 정보를 FGameplayAbilitySpec 구조체로 발생될 대상클래스를 StaticClass로 타입을 알려준다.
4.GiveAbility 타입을 가지고있는 GASpec를 타입키값으로 저장한다?

 

void AABGASFountain::TimerAction()
{

	FGameplayAbilitySpec* RotateGASpec = ASC->FindAbilitySpecFromClass(UABGA_Rotate::StaticClass());
	if(!RotateGASpec)
	{
		return;
	}

	if(!RotateGASpec->IsActive())
	{
		ASC->TryActivateAbility(RotateGASpec->Handle);
	}
	else
	{
		ASC->CancelAbilityHandle(RotateGASpec->Handle);
	}
}

1. 저장된 GA Spec중 인자값으로 지정된 클래스 타입의 GA Spec를 가져온다.

2. 조건이 성립되면 해당 GA에 ActiveAbility 함수로 구현된 로직을 호출한다.
Active가 호출되면 isActive도 참을 반환한다

3. 조건이 성립되지않는다면 CancleAbility로 로테이션이 종료된다.
Cancle이 호출되면 isActive는 거짓을 반환한다.

 


GA 생성방법

GameplayAbility를 상속받는 파생클래스를 해당 동작할 기능의 이름으로 생성합니다 Ex) GARotate

 

게임어빌리티 사용법

새로 생성된 GA 클래스에 밑 2개의 함수를 오버라이딩합니다.

	/** Actually activate ability, do not call this directly */
	virtual auto ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
	                             const FGameplayAbilityActivationInfo ActivationInfo,
	                             const FGameplayEventData* TriggerEventData) -> void override;
	

	/** Destroys instanced-per-execution abilities. Instance-per-actor abilities should 'reset'. Any active ability state tasks receive the 'OnAbilityStateInterrupted' event. Non instance abilities - what can we do? */
	virtual auto CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
	                           const FGameplayAbilityActivationInfo ActivationInfo,
	                           bool bReplicateCancelAbility) -> void override;

 

생성된 부모 클래스안으로 들어가면 다양한 가상함수들을 확인할수있습니다.

이중에서 지금은 ActiveAbility ,CancelAbility를 오버라이딩해서 사용합니다.

 

파라미터 설명

  • Handle - 현재 발동한 어빌리티를 직접 처리할 핸들 정보입니다.
  • ActorInfo - 액터들의 정보를 가지고있습니다
    Owner - GA를 발동한 엑터
    Avater - 로직이외 비주얼을 담당하는 엑터
  • ActivationInfo - 어떻게 발동했는지를 담은 정보
  • TriggerEventData - 외부에서 발동했을시 어떻게 발동했는지 이벤트를 담은 정보
void UABGA_Rotate::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
	const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
	//현제 발동한 어빌리티를 직접 처리할 핸들 정보
	//엑터들의 정보를 담은 담은 엑터정보
	//어떻게 발동했는지 정보
	//만약 외부에서 발동했을시 어떻게 발동했는지 이벤트 정보
	
	AActor* AvatarActor = ActorInfo->AvatarActor.Get();
	if(AvatarActor)
	{
		URotatingMovementComponent* RotatingMovementComponent = Cast<URotatingMovementComponent>(AvatarActor->GetComponentByClass(URotatingMovementComponent::StaticClass()));

		if(RotatingMovementComponent)
		{
			RotatingMovementComponent->Activate(true);
		}
	}	
	
	
}

void UABGA_Rotate::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo,
                                 const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)
{
	Super::CancelAbility(Handle, ActorInfo, ActivationInfo, bReplicateCancelAbility);
	AActor* AvatarActor = ActorInfo->AvatarActor.Get();
	if(AvatarActor)
	{
		URotatingMovementComponent* RotatingMovementComponent = Cast<URotatingMovementComponent>(AvatarActor->GetComponentByClass(URotatingMovementComponent::StaticClass()));

		if(RotatingMovementComponent)
		{
			RotatingMovementComponent->Deactivate();
		}
	}	
}

 

ActiveAbility 함수 기능 설명

1. ActorInfo 인자값으로 아바타 엑터의 정보를 가져옵니다 현제에서는 owner avtar가 동일합니다.

2.액터가 가지고있는 로테이션컴포넌트로 회전을 활성화합니다.

 

ActiveCancle 함수

1. ActorInfo 인자값으로 아바타 엑터의 정보를 가져옵니다 현제에서는 owner avtar가 동일합니다.

2.액터가 가지고있는 로테이션컴포넌트로 회전을 비활성화합니다.


GamePlayTag

FName으로 관리되는 경량의 표식 데이터
 - 액터나 컴포넌트에 지정했던 태그와 다른 데이터

프로젝트 설정에서 별도로 게임플레이 태그를 생성하고 관리할 수 있음
 - 결과는 DfaultGameplayTags.ini 파일에 저장됨.

계층 구조로 구성되어 있어 체계적인 관리 가능
 - Actor.Action.Rotate: 행동에 대한 태그
 - Actor.State.IsRotating : 상태에 대한 태그

게임플레이 태그들의 저장소  GameplayTagContainer
 - 계층구조를 지원하는 검색 기능 제공

 - HasTagExact : 컨테이너에 A.1태그가 있는 상황에서 A로 찾으면 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

게임플레이 어빌리티 시스템과 독립적으로 사용 가능

 

태그 생성방법

프로젝트 셋팅 -> GameplayTags

 

 

태그의 원활한 사용법 헤더 별칭 지정방법

 

별도의 헤더파일을 생성하여 생성한 태그들의 별칭을 지정한다.

#pragma once
#include "GameplayTagContainer.h"

#define TAG_ACTOR_ROTATE FGameplayTag::RequestGameplayTag(FName("Actor.Action.Rotate"))
#define TAG_ACTOR_ISROTATING FGameplayTag::RequestGameplayTag(FName("Actor.State.IsRotating"))

 

 

태그가 중요한 이유

UABGA_Rotate::UABGA_Rotate()
{
	//어빌리티 태그 지정
	AbilityTags.AddTag(TAG_ACTOR_ROTATE);
	//어빌리티가 발돌되면 이태그가 심어진다 상태태그
	ActivationOwnedTags.AddTag(TAG_ACTOR_ISROTATING);
}

 

AbilityTags : 가져올 대상의 GA를 태그로 지정하여 컨테이닝이 가능하다
ActivationOwnedTags : 발동이 확인되면 현제 태그를 ISROTATING으로 변환된다.

 

해당 기능들을 사용하게되면 이전코드의 직접적인 GA 클래스의 접근이 필요로 하지않는다

따라서 클래스 의존성이 줄어들고 확장성이 커진다.



태그를 활용한 GA 컨테이닝 방법

 

정의부

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

 

실행부

	// const FGameplayAbilitySpec RotateSkillSpec(UABGA_Rotate::StaticClass());
	// //발동할 정보만 기존적인 타입반황을위해 가지고있는다.
	//
	// ASC->GiveAbility(RotateSkillSpec);

	for(const auto& StartAbility : StartAbilities)
	{
		FGameplayAbilitySpec StartSpec(StartAbility);
		ASC->GiveAbility(StartSpec);
	}

 

 

최초 GA 태그의 접근을 위한 컨테이너를 생성합니다.

위 컨테이너의 요소값의 정의는 C++가 아닌 블루프린트로 지정합니다 
배열에 담은 태그는 TAC_ACTOR_ROTATE 하나만 추가합니다.

만약 이글을 보고 실습을 하신다면 주의하기시 바랍니다.

 

 

 

 

 

태그를 활용한 타이머 함수

void AABGASFountain::TimerAction()
{
	UE_LOG(LogTemp,Log,TEXT("출력"));
	//인자값에 등록된 태그대상들을 컨테이너로 가져온다.
	const FGameplayTagContainer TargetTag(TAG_ACTOR_ROTATE);
	//현제 태그가 동작하고있는지 조건판단
	if(!ASC->HasMatchingGameplayTag(TAG_ACTOR_ISROTATING))
	{
		UE_LOG(LogTemp,Log,TEXT("1"));
		//인자값의 태그가 부여된 어빌리티의 Active를 실행한다.
		ASC->TryActivateAbilitiesByTag(TargetTag);
	}
	else
	{
		UE_LOG(LogTemp,Log,TEXT("2"));
		ASC->CancelAbilities(&TargetTag);
	}
}

 1. 3초마다 해당 함수를 호출한다.

 2. FGameplayTagContainer의 인자값으로 검색한 태그를 가져온다

 3. ASC의 태그 상태가 활성화된 상태인지 체크합니다.

  - RotateGA의 발동이 일어나면 ASC의 태그상태는 ISROTATING이 되도록 이미 정의되어있습니다.

 4. 조건이 성립되면 부여된 어빌리티의 Active를 실행합니다.

 

 

알게된 사실

CDO 수정된 정보는 이후 생성될 인스턴스에만 반영되고 월드에 배치된 대상 CDO 그대로 남아있어서

언리얼 재생시 크래시가 발생합니다 저는 런타임 이전의 CDO를 수정하면 당연히 월드에 이미 배치된 대상들도

수정된 정보를 따를줄 알았는데 아니여서 당황했습니다.

 

예시

BallActor를 레벨에 배치하고

이후 BallActorClass의 생성자 오버로딩을 수정한뒤 빌드합니다

수정된 이후의 BallActor는 수정된 정보를 적용받지만 이미 레벨에 배치된 대상의 인스턴스 정보는 수정되지 않고 정적인 상태로 

남게됩니다 이러한 언리얼 구조로 크래쉬가 나서 알게됬는데 언리얼 엔진을 이해하는데 많은 도움을 주게되는 감사한 버그였습니다

HappyEnding