학습목표

  • 플레이어 캐릭터의 게임플레이 어빌리티 시스템 설정 방법의 학습
  • 플레이어 캐릭터의 입력에 따른 게임플레이 어빌리티 발동 구현
  • 어빌리티 태스크의 활용 방법의 학습
  • 게임플레이 어빌리티 시스템의 디버깅 방법의 학습

 

 

 

목차

     

     


     

     

    목표

     


     

    플레이어 캐릭터의 기획

     

    플레이어 캐릭터의 기획

    • 기존 플레이어 캐릭터(ABCharacterPlayer) 클래스를 상속
    • 입력에 따라 정해진 게임플레이 어빌리티가 발동되도록 설정
    • 점프 GA: 스페이스 바를 누르면 점프 어빌리티가 발동
    • 공격 GA: 마우스 왼쪽 클릭시 공격 어빌리티가 발동

     

    게임플레이 어빌리티 시스템의 기본 흐름

     


     

     

     

     

    ASC + GA Spec + GA를 사용하여 공격 구현하기

     


     

     

     

    플레이어 캐릭터의 AbilitySystemComponent(ASC) 설정

     

     플레이어 캐릭터의 AbilitySystemComponent(ASC) 설정 

    • 분수대 액터와 같이 플레이어 캐릭터에 설정하는 것이 가능
    • 하지만 네트웍 멀티플레이를 감안했을때, 서버에서 클라이언트로 배포되는 액터가 보다 적합
    • 이 때 많이 사용하는 액터가 주기적으로 플레이어 정보를 배포하는 PlayerState 액터임
    • 따라서 OwnerPlayerState로 설정하고, AvatarCharacter로 설정하는 것이 일반적인 방법  

     

     

     프레임웍의 데이터를 누가 관리하는게 좋을까??  (= PlayerState에서ASC를 설정하는 이유는?)

    • 네트워크 멀티플레이를 감안하면 PlayerState가 가장 적합하다.
      • PlayerState은 주기적으로 서버에서 클라이언트로 배포되는 액터이기 때문이다.
    • 하지만 실제 게임에서의 상호작용은 Pawn이 담당하기 때문에 PlayerState의 역할과 Pawn의 역할을 구분해주는 것이 필요하다.
    • GAS에서 데이터를 담당하는 액터를 OwnerActor라고 부른다. 
      • OwnerActor를 PlayerState 액터로 지정하여 설계
      • 우리가 플레이어 캐릭터를 생성할 때는 GAS Framework는 PlayerState에서 AbilitySystemComponent(ASC)를 생성하고 이 생성된 ASC 포인터를 캐릭터에 넘겨준다.
      • 이후에 발동한 캐릭터의 어빌리티들은 대부분 모두 캐릭터의 중심으로 진행된다.
      • 이 어빌리티로 인한 결과는 ASC가 데이터로 보관하고 이것들은 실시간으로 서버에서 클라이언트로 전송한다.
    • 실제 게임에서 물체와 상호작용하는 비주얼적인 부분을 담당하는 액터를 Avatar Actor라고 부른다.
      Avatar Actor는 우리가 조종할 캐릭터로 설계

     


     

     

    게임플레이 어빌리티 스펙 Gameplay Ability Spec (GA Spec) 

     

    Gameplay Ability Spec (GA Spec) 

    • 게임플레이 어빌리티에 대한 정보를 담고 있는 구조체
    • ASC는 직접 어빌리티를 참조하지 않고 Spec 정보만 가지고 있음
    • Spec어빌리티의 현재 상태와 같은 다양한 정보를 가지고 있음
    • ASC로부터 어빌리티를 다루고자 할 경우 Spec에 있는 Handle을 사용해 컨트롤함
    • 핸들 값은  전역으로 설정되어 있으며 Spec 생성시자동으로 1씩 증가함. 기본값 -1
    • 어빌리티 정보: Spec
    • 어빌리티 인스턴스에 대한 레퍼런스: Spec Handle    

     

     

    어빌리티에 대한 정보는 Spec 데이터를 통해 확인할 수 있다.

    어빌리티를 컨트롤하기 위해서는 Spec에 포함된 Spec Handle 정보를 사용해서 어빌리티를 직접 제어한다.

     


     

     

     

    어빌리티 시스템 컴포넌트의 입력 처리

     

    • 게임 어빌리티 스펙에는 입력 값을 설정하는 필드 InputID가 제공됨
    • ASC에 등록된 Spec을 검사해 입력에 매핑된 GA를 찾을 수 있음 FindAbilitySpecFromInputID
    • 사용자 입력이 들어오면 ASC에서 입력에 관련된 GA를 검색함
    • 해당 GA를 발견하면, 현재 발동 중인지를 판별
      • GA가 발동 중이면 입력이 왔다는 신호를 전달:  AbiltiySpecInputPressed
      • GA가 발동하지 않았다면 새롭게 발동시킴: TryActivateAbility
    • 입력이 떨어지면 동일하게 처리
      • GA에게 입력이 떨어졌다는 신호를 전달:  AbiltiySpecInputReleased

     

    EnhancedInputComponent의 BindAction 함수를 활용하면

    범용적인 입력 처리가 가능해짐

     


     

     

     

    게임플레이 어빌리티의 인스턴싱 옵션 

     

    상황에 따라 다양한 인스턴스 정책을 지정할 수 있음

       
      NonInstanced   인스턴싱 없이 CDO에서 일괄 처리   
      InstancedPerActor -  액터마다 하나의 어빌리티 인스턴스를 만들어 처리.
    Primary Instance만 존재함
    -  네트워크 리플리케이션을 고려했을때 이 정보들도 넘어가는데 InstancedPerActor가 리플리케이션을 하기에 용이하기 때문에 많이 사용된다. 예를들어, 캐릭터 액션이 여러 개가 동시에 발동될 필요가 없기 때문에 InstancedPerActor를 사용해서 하나의 인스턴스에서시작과 끝이 명확하게 지정하여 사용하면 된다. 
      InstancedPerExecution -  어빌리티를 발동할 때마다 인스턴스를 (계속해서) 생산함
    -  인스턴스가 여러개 생길 수도 있음. 가장 먼저 생성된 인스턴스를 Primary Instance라고 부름
    InstancedPerExecution는 비용이 비싸다.  

     

     

    네트웍 리플리케이션까지 고려했을때

    InstancedPerActor가 무난한 선택지다

     


     

     

    어빌리티 태스크 Ability Task(AT)의 활용

     

    Ability Task (AT)

    • GA의 실행(Activation)은 한 프레임에서 이루어짐
    • GA가 시작되면 EndAbility함수가 호출되기까지는 끝나지 않음
    • 애니메이션 재생 같이 시간이 소요되고 상태를 관리해야 하는 어빌리티의 구현 방법
      • 비동기적으로 작업을 수행하고 끝나면 결과를 통보받는 형태로 구현
      • 이를 위해 GAS는 Ability Task(AT)를 제공하고 있음     

     

    Ability Task (AT)의 활용 패턴

    1.  Ability Task(AT)에 작업이 끝나면 브로드캐스팅되는 종료 델리게이트를 선언함
    2.  GA는 AT를 생성한 후 바로 종료 델리게이트를 구독함
    3.  GA의 구독 설정이 완료되면 AT를 구동:  AT의 ReadyForActivation 함수 호출
    4.  AT의 작업이 끝나면 델리게이트를 구독한 GA의 콜백 함수가 호출됨
    5.  GA의 콜백함수가 호출되면 GA의 EndAbility 함수를 호출해 GA를 종료   

     

    GA는 필요에 따라 다수의 Ability Task(AT)를 사용해 복잡한 액션 로직을 설계할 수 있음

     


     

     

    코드 구현

     


     

    ABCharacterPlayer

     

    ABCharacterPlayer.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "Character/ABCharacterPlayer.h"
    #include "AbilitySystemInterface.h"
    #include "ABGASCharacterPlayer.generated.h"
    
    UCLASS()
    class ARENABATTLEGAS_API AABGASCharacterPlayer : public AABCharacterPlayer, public IAbilitySystemInterface
    {
    	GENERATED_BODY()
    	
    public:
    	AABGASCharacterPlayer();
    
    	virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
    	virtual void PossessedBy(AController* NewController) override;
    	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
    
    protected:
    	void SetupGASInputComponent();
    	void GASInputPressed(int32 InputId);
    	void GASInputReleased(int32 InputId);
    
    protected:
    	UPROPERTY(EditAnywhere, Category = GAS)
    	TObjectPtr<class UAbilitySystemComponent> ASC;
    
    	UPROPERTY(EditAnywhere, Category = GAS)
    	TArray<TSubclassOf<class UGameplayAbility>> StartAbilities; // 플레이어에게 지정할 어빌리티 목록들
    	
    	UPROPERTY(EditAnywhere, Category = GAS)
    	TMap<int32, TSubclassOf<class UGameplayAbility>> StartInputAbilities;
    };

     

     

     

    ABCharacterPlayer.cpp

    더보기
    #include "Character/ABGASCharacterPlayer.h"
    #include "AbilitySystemComponent.h"
    #include "Player/ABGASPlayerState.h"
    #include "EnhancedInputComponent.h"
    
    AABGASCharacterPlayer::AABGASCharacterPlayer()
    {
    	ASC = nullptr;
    
    	// Attack 몽타주
    	static ConstructorHelpers::FObjectFinder<UAnimMontage> ComboActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_ComboAttack.AM_ComboAttack'"));
    	if (ComboActionMontageRef.Object)
    	{
    		ComboActionMontage = ComboActionMontageRef.Object;
    	}
    }
    
    UAbilitySystemComponent* AABGASCharacterPlayer::GetAbilitySystemComponent() const
    {
    	return ASC;
    }
    
    void AABGASCharacterPlayer::PossessedBy(AController* NewController)
    {
    	// PossessedBy(AController* NewController)는 네크워크 멀티프레임웍에서는 서버에서만 호출된다.
    	// 네크워크 멀티프레이어에서도 이것을 전달 받으려면 OnLapPlayerStateEvent함수에서 구현을 해줘야 한다.
    	// 이 예제는 싱글 플레이어 기준이므로 위의 사항을 고려하지 않고 진행하였다.
    
    	Super::PossessedBy(NewController);
    
    	AABGASPlayerState* GASPS = GetPlayerState<AABGASPlayerState>();
    	if (GASPS)
    	{
    		ASC = GASPS->GetAbilitySystemComponent();
    		ASC->InitAbilityActorInfo(GASPS, this);
    
    		for (const auto& StartAbility : StartAbilities)
    		{
    			FGameplayAbilitySpec StartSpec(StartAbility);
    			ASC->GiveAbility(StartSpec);
    		}
    
    		for (const auto& StartInputAbility : StartInputAbilities)
    		{
    			FGameplayAbilitySpec StartSpec(StartInputAbility.Value);
    			StartSpec.InputID = StartInputAbility.Key;
    			ASC->GiveAbility(StartSpec);
    		}
    
    		SetupGASInputComponent();
    
    		APlayerController* PlayerController = CastChecked<APlayerController>(NewController);
    		PlayerController->ConsoleCommand(TEXT("showdebug abilitysystem"));
    	}
    }
    
    void AABGASCharacterPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
    {
    	Super::SetupPlayerInputComponent(PlayerInputComponent);
    
    	SetupGASInputComponent();
    }
    
    void AABGASCharacterPlayer::SetupGASInputComponent()
    {
    	if (IsValid(ASC) && IsValid(InputComponent))
    	{
    		UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent);
    
    		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 0);
    		EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AABGASCharacterPlayer::GASInputReleased, 0);
    		EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 1);
    	}
    }
    
    void AABGASCharacterPlayer::GASInputPressed(int32 InputId)
    {
    	FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
    	if (Spec)
    	{
    		Spec->InputPressed = true;
    		if (Spec->IsActive())
    		{
    			ASC->AbilitySpecInputPressed(*Spec);
    		}
    		else
    		{
    			ASC->TryActivateAbility(Spec->Handle);
    		}
    	}
    }
    
    void AABGASCharacterPlayer::GASInputReleased(int32 InputId)
    {
    	FGameplayAbilitySpec* Spec = ASC->FindAbilitySpecFromInputID(InputId);
    	if (Spec)
    	{
    		Spec->InputPressed = false;
    		if (Spec->IsActive())
    		{
    			ASC->AbilitySpecInputReleased(*Spec);
    		}
    	}
    }

     

     

     

    BP_ABGASCharacterPlayer

     


     

    ABGA_Attack

     

    ABGA_Attack.h

    더보기
    #pragma once
    #include "CoreMinimal.h"
    #include "Abilities/GameplayAbility.h"
    #include "ABGA_Attack.generated.h"
    
    UCLASS()
    class ARENABATTLEGAS_API UABGA_Attack : public UGameplayAbility
    {
    	GENERATED_BODY()
    	
    public:
    	UABGA_Attack();
    
    public:
    	virtual void ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData) override;
    	virtual void InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo) override;
    	virtual void CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility) override;
    	virtual void EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled) override;
    
    protected:
    	UFUNCTION()
    	void OnCompleteCallback();
    
    	UFUNCTION()
    	void OnInterruptedCallback();
    };

     

     

     

    ABGA_Attack.cpp

    더보기
    #include "GA/ABGA_Attack.h"
    #include "Character/ABCharacterBase.h"
    #include "Abilities/Tasks/AbilityTask_PlayMontageAndWait.h" // AbilitySystem 내장클래스
    #include "ArenaBattleGAS.h"
    #include "GameFramework/CharacterMovementComponent.h"
    
    UABGA_Attack::UABGA_Attack()
    {
    	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
    }
    
    void UABGA_Attack::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
    {
    	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
    
    	AABCharacterBase* ABCharacter = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
    	ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
    
    	UAbilityTask_PlayMontageAndWait* PlayAttackTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, TEXT("PlayAttack"), ABCharacter->GetComboActionMontage());
    	// 몽타주 재생이 끝나면 아래의 콜백 함수들이 호출된다.
    	PlayAttackTask->OnCompleted.AddDynamic(this, &UABGA_Attack::OnCompleteCallback);
    	PlayAttackTask->OnInterrupted.AddDynamic(this, &UABGA_Attack::OnInterruptedCallback);
    	PlayAttackTask->ReadyForActivation();
    }
    
    void UABGA_Attack::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
    {
    	ABGAS_LOG(LogABGAS, Log, TEXT("Begin"));
    }
    
    void UABGA_Attack::CancelAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateCancelAbility)
    {
    	Super::CancelAbility(Handle, ActorInfo, ActivationInfo, bReplicateCancelAbility);
    
    }
    
    void UABGA_Attack::EndAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, bool bReplicateEndAbility, bool bWasCancelled)
    {
    	Super::EndAbility(Handle, ActorInfo, ActivationInfo, bReplicateEndAbility, bWasCancelled);
    
    	AABCharacterBase* ABCharacter = CastChecked<AABCharacterBase>(ActorInfo->AvatarActor.Get());
    	ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
    }
    
    void UABGA_Attack::OnCompleteCallback()
    {
    	bool bReplicatedEndAbility = true; // 네트워크 리플리케이션 여부
    	bool bWasCancelled = false;		   // 취소됬는지 여부. false면 취소되지 않았다는 의미
    	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled); // 명시적으로 어빌리티가 끝났다고 지정 
    }
    
    void UABGA_Attack::OnInterruptedCallback()
    {
    	bool bReplicatedEndAbility = true; // 네트워크 리플리케이션 여부
    	bool bWasCancelled = true;		   // Interrupted된 경우니까 true로 설정하여 취소시킴
    	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
    }

     

     


     

     

     

     

    실행화면

     


     

     

     

     

    Game Tag 사용하기

     


     

     

    GA의 블루프린트 상속 및 게임플레이 태그 설정 

     

    • 꼭 필요한 상황이 아니라면 GamplayAbility(GA)와 AbilityTask(AT)는 가급적 자기 역할만 충실하게 구현하는 것이 좋음
    • Gameplay Tag를 C++에서 설정하는 경우 기획 변경때마다 소스코드 컴파일을 수행해야 함 
    • Gameplay Tag 설정은 블루프린트에서 설정하는 것이 의존성 분리에 도움이 됨
    • Gameplay Tag 설정 기획
    • 점프 GA의 ActivationOwnedTags에 ' Character.State.IsJumping ' Gameplay Tag 설정
    • 공격 GA의 ActivationOwnedTags에 ' Character.State.IsAttacking ' Gameplay Tag 설정

     

    GAS 디버깅을 사용해 현재 상황의 확인 가능

    ~ 누르고 showdebug abilitysystem 명령어를 입력하면 디버깅 화면 확인가능  


     

     

    Project Settings에서 Game Tag 추가 

     

    Project Settings - Gameplay Tags

    • Gameplay Tag List:  Add New Tag
      • Character.State.IsAttacking 추가
      • Character.State.IsJumping 추가


     

     

    Gameplay Tag를 상속받는 Blueprint 생성 - BPGA_Attack, BPGA_Character 

     

    BPGA_Attack, BPGA_Character 생성

     

     

     

    BPGA_Attack

    Gameplay Tag 설정

    • Details - Activation Owned Tags
      • Character.State.IsAttacking

     

    BP_ABGASCharacterPlayer

    플레이어 캐릭터의 입력을 GAS으로 사용

    •  Details - GAS - Start Input Abilities에 elements 2개 추가
      • BPGA_CharacterJump 할당
      • BPGA_Attack 할당