일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 상계9동
- 주식단테
- 리팩터링
- unity
- 448일선
- 스즈메의 문단속
- 224일선
- shader
- 산토리 하이볼
- JavaScript
- 112일선
- 2023 Gaming
- 리팩터링 4장
- 이득우의 언리얼 프로그래밍1
- 스포일러 주의
- URP
- 2023 구글 클라우드
- 2023 게이밍 인 구글 클라우드
- 2023 게이밍
- 공부
- 리팩터링 3장
- 이득우의 언리얼 프로그래밍 1
- GenAI
- 주식
- 전주비빔 라이스 버거
- 1일차
- 작계훈련
- 언리얼5
- 구글 컨퍼런스
- 언리얼 5
- Today
- Total
개발 이야기 안하는 개발자
언리얼 5_4_1 게임플레이 어빌리티 시스템 캐릭터 제작 기초 본문
게임 플레이 어빌리티 시스템(GAS)
액터가 소유하고 발동할 수 있는 어빌리티 및 액터간의 인터랙션 기능을 제공하는 프레임웍.
큰 규모의 RPG및 네트워크 멀티 게임에 효율적으로 적합함.
장점
- 유연성과 확장성
- 모듈러 시스템
- 네트워크 지원
- 데이터 기반 설계
- 완성도
단점
- 배우는 학습 비용
- 오버헤드
ASC(어빌리티 시스템 컴포넌트) 와 GA(게임플레이 어빌리티)
ASC에는 호출될때 발동할 수 있는 액션들을 저장한다.
GA 발동 과정.
1. ASC의 GiveAbility()에 발동할 GA의 타입을 전달한다. (GA의 타입정보를 게임 플레이 어빌리티 스펙이라 함)
2. ASC에게 어빌리티를 발동하라고 명령함.
3. 발동된 GA에는 발동한 액터와 실행 정보가 기록됨 (SpecHandle_발동된 어빌 핸들, ActorInfo_어빌소유자와 아바타 정보, ActivationInfo_발동 방식 정보)
GA의 함수
CanActivateAbility , ActivateAbility , CancelAbility , EndAbility
Spec(게임 플레이 어빌리티 스펙)
게임 플레이 어빌리티에 대한 정보를 담고 있는 구조체
ASC는 직접 어빌리티를 참조하지 않고 스펙 정보만 가지고 있음.
ASC로부터 어빌리티를 다루고자 할 경우 Spec의 Handle을 사용해야함(ASC->handle)
게임플레이 어빌리티의 인스턴싱 옵션
NonInstanced : 인스턴싱 없이 CDO에서 일괄처리
InstancedPerActor : 액터마다 하나의 어빌리티 인스턴스를 만들어서 처리 (네트워크 리플리케이션까지 고려하면 무난함)
InstancedPerExecution : 발동시 인스턴스를 생성함.
게임 플레이 태그
경량의 표식 데이터이다.
계층 구조로 구성되어 있어 체계적인 관리가 가능하다.
GameplayTagContainer라는 저장소에 저장되어 관리가 된다.
코드에선 태그를 다음과 같이 정의해서 사용한다.
#define ABTAG_ACTOR_ROTATE FGameplayTag::RequestGameplayTag(FName("Actor.Action.Rotate"))
#define ABTAG_ACTOR_ISROTATING FGameplayTag::RequestGameplayTag(FName("Actor.State.IsRotating"))
이를 활용해서 ASC와 같이 사용한다.
UPROPERTY(EditAnywhere, Category=GAS)
TObjectPtr<class UAbilitySystemComponent> ASC;
UPROPERTY(EditAnywhere, Category=GAS)
TArray<TSubclassOf<class UGameplayAbility>> StartAbilities;
ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC")); //생성자에서 생성
...
void AABGASFountain::PostInitializeComponents()
{
...
ASC->InitAbilityActorInfo(this, this);
for (const auto& StartAbility : StartAbilities)
{
FGameplayAbilitySpec StartSpec(StartAbility); //어빌을 토대로한 스펙 제작
ASC->GiveAbility(StartSpec); //ASC에게 스펙을 전달함.
}
}
...
void AABGASFountain::TimerAction()
{
FGameplayTagContainer TargetTag(ABTAG_ACTOR_ROTATE);
if(!ASC->HasMatchingGameplayTag(ABTAG_ACTOR_ISROTATING))
{
ASC->TryActivateAbilitiesByTag(TargetTag);
}
else
{
ASC->CancelAbilities(&TargetTag);
}
}
TimerAction 메소드에 보면 ABTAG_ACTOR_ROTATE와 ABTAG_ACTOR_ISROTATING 2개의 태그를 볼 수 있다.
이 둘은 제작할 GameAbility에 등록될 이름들이다.
제작한 GameplayAbility를 StartAbilities에 담고, 이를 ASC에 등록했다.
ASC내부에서 ABTAG_ACTOR_ISROTATING을 찾아보는데, 이때 이 태그는 현재 어빌이 활성화 되었는지를 체크하는 OwnerTag로 사용할 예정이다.
즉, 이미 어빌이 활성화 되었다면 해당 태그를 찾을 수 있다.
활성화 되었다면 비활성화, 비활성화라면 활성화 하도록 원하는 어빌을 찾은 태그로 호출해서 활성화 한다.
StartAbilities는 TArray로 만들었고, 이제 어빌을 제작한다.
제작은 UGameplayAbility를 상속받아 작성하면 된다.
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;
UABGA_Rotate::UABGA_Rotate()
{
AbilityTags.AddTag(ABTAG_ACTOR_ROTATE); //어빌을 대표하는 태그
ActivationOwnedTags.AddTag(ABTAG_ACTOR_ISROTATING); //어빌이 활성화 될때 활성화 되는 태그
}
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* 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);
AActor* AvatarActor = ActorInfo->AvatarActor.Get();
if (AvatarActor)
{
URotatingMovementComponent* RotatingMovement = Cast<URotatingMovementComponent>(AvatarActor->GetComponentByClass(URotatingMovementComponent::StaticClass()));
if (RotatingMovement)
{
RotatingMovement->Deactivate();
}
}
}
플레이어 캐릭터 ASC 설정
네트워크 멀티 플레이를 감안할때 서버에서 클라이언트로 배포되는 액터가 보다 적합함.
많이 사용하는 액터가 주기적으로 플레이어 정보를 배포하는 PlayerState 액터이다.
Owner를 Player State로 설정하고, Avatar를 Character로 설정하는 것이 일반적인 방법이다.
즉, Gas를 보유하는 것이 OwnerActor이고 비주얼적인 부분을 담당하는 것이 AvatarActor 이다.
ASC를 Owner가 Avatar에게 데이터를 넘겨주는 방식으로 구현한다.
OwnerActor 내용 구현
class ARENABATTLEGAS_API AABGASPlayerState : public APlayerState, public IAbilitySystemInterface
{
GENERATED_BODY()
public:
AABGASPlayerState();
virtual class UAbilitySystemComponent* GetAbilitySystemComponent() const override;
protected:
UPROPERTY(EditAnywhere, Category = GAS)
TObjectPtr<class UAbilitySystemComponent> ASC;
};
#include "Player/ABGASPlayerState.h"
#include "AbilitySystemComponent.h"
AABGASPlayerState::AABGASPlayerState()
{
ASC = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("ASC"));
//ASC->SetIsReplicated(true); //네트워크에서 받는거라면 해당 내용이 필요하다.
}
UAbilitySystemComponent* AABGASPlayerState::GetAbilitySystemComponent() const
{
return ASC;
}
AvatarActor에서도 ASC를 사용하는데, 이는 PlayerState(Owner)에게 받아야 함으로 생성하지 않고 그냥 둔다.
아래는 Avatar를 만드는 내용인데 이 코드는 기존에 제작했던 CharacterPlayer를 상속받아 제작되었다.
//생성자
ASC = nullptr;
virtual void PossessedBy(AController* NewController) override; //만약 멀티게임이라면 이 내용은 서버에서 받아야한다.
virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override; //부모가 EnhancedInput을 등록했던 메소드
void AABGASCharacterPlayer::PossessedBy(AController* NewController)
{
Super::PossessedBy(NewController);
//해당 폰(캐릭터)가 가진 PlayerState에서 가져온다 (월드 셋팅에서 셋팅 해야 한다)
AABGASPlayerState* GASPS = GetPlayerState<AABGASPlayerState>();
if (GASPS)
{
ASC = GASPS->GetAbilitySystemComponent();
ASC->InitAbilityActorInfo(GASPS, this);
for (const auto& StartAbility : StartAbilities) //비어있는 TArray
{
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"));
}
}
해당 객체가 가지고 있는 어빌을 모두 ASC에 등록하는 로직이다.
이렇게 함으로써 입력이 들어온 것에 맞춰 어빌을 호출할 것이다.
들어올 입력을 구현하는 내용은 SetupGasInputComponent()에서 다시 구현할 것이다.
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);
}
}
부모가 제작한 인풋을 그대로 사용하고, 들어온 입력에 맞춰서 등록된 어빌을 호출할 것이다.
제일 뒤에 붙은 0번, 1번이 등록된 메소드(GASInputPressed ...) 의 매개변수 이다. 각각 ID를 뜻한다.
점프 액션이 호출되면(Pressed 되면) GasInputPressed가 호출된다.
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);
}
}
}
코드에서 처럼 들어온 매개변수에 맞춰 호출될 것이고, 메소드에서는 ASC에서 등록된 어빌을 찾는다.
어빌들은 스펙에 ID를 저장했기 때문에 거기서 매칭되는 어빌을 찾는다.
찾은경우 어빌이 활성화 되어있는지 확인하고(Spec->IsActive())
활성화 하게 된다. (제작한 GameAbility의 ActivateAbility())
활성화 되어있는 경우 비활성화 한다. (제작한 GameAbility의 CancelAbility())
어빌리티 태스크(AT)
게임 플레이 어빌리티의 실행은 한 프레임에서 이루어 진다.
어빌은 EndAbility() 가 호출되기 전엔 끝나지 않는다.
비 동기적으로 작업을 수행하고 끝나면 결과를 통보받는 형태로 애니메이션 재생 같이시간이 소요되고 상태를 관리해야 하는 어빌리티의 구현 방법이다.
1. 어빌리티 태스크의 작업이 끝나면 브로드캐스팅되는 종료 델리게이트 선언.
2. GA는 AT를 생성한 후 바로 종료 델리게이트를 구독함.
3. GA의 구독 설정이 완료되면 AT를 구동 : AT의 ReadyForActivation 함수 호출
4. AT의 작업이 끝나면 델리게이트를 구독한 GA의 콜백 함수가 호출됨.
5. GA의 콜백함수가 호출되면 GA의 EndAbility 함수를 호출해 GA 종료
이제 AT를 활용해서 어빌을 호출해본다.
점프와 Attack을 할건데
점프는 이미 Chacater 클래스가 만들어놓은 언리얼 Jump 가 있기 때문에 둔다.
Attack을 구현할거고 GameAbility를 상속받아 제작한다.
UABGA_Attack::UABGA_Attack()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
액터마다 공격이 각각 진행되어야 함으로 Instanced 는 PerActor로 지정한다.
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();
}
어빌이 활성화 되면 AT를 생성하고, 제작한 OnCompleteCallBack과 OnInterruptedCallBack을 추가한다.
Complete는 끝나면 호출되고, Interrupted는 외부에 의해 Cancel이 호출되면 호출된다.
void UABGA_Attack::OnCompleteCallback()
{
bool bReplicatedEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}
void UABGA_Attack::OnInterruptedCallback()
{
bool bReplicatedEndAbility = true;
bool bWasCancelled = true;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}
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::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);
}
그 외에 제작한 코드.
제작한 GA를 GasCharacterPlayer에서 만든 StartInputAbilities에 넣으면 공격할때 어빌리티가 호출되는 것을 볼 수 있다.
여기서 만약 지금 캐릭터의 상태를 보고 싶다면 이 GA들을 블루프린트로 만들고 Tag를 추가하면 된다.
IsAttacking과 IsJumping을 추가한다.
점프와 어택을 블루프린트로 만들고 Owned 태그를 각각 알맞게 수정해준다.
이후 만든 블루프린트를 StartedInputAbilities에 넣는다.
실행하고 나서 ` 키를 누르고 Showdebug abilitysystem이라고 치면 화면에 지금 상태값을 태그로 보여준다.
화면 좌측에 Owned Tags의 값이 바뀌는것을 확인할 수 있다.
만약 이때 다른 객체가 ASC를 사용해서 그 객체것을 보고 싶다면 PageDown이나 PageUp키를 사용해서 볼 수 있다.
왼쪽에 뜨는 태그가 호출되지 않았는데 다른 객체의 것을 띄우는 경우가 있다.
이때는 INI로 들어가서 설정을 변경해 주면 된다.
config/DefaultGame.ini
[/Script/GameplayAbilities.AbilitySystemGlobals]
bUseDebugTargetFromHud=True
아래 2줄을 추가해주면 이제 변경되는 객체를 선택할때 해당 객체의 태그가 뜨게 된다.
공격이후에 콤보 동작을 호출하도록 타이머를 제작한다.
전에 제작했던 콤보 공격(몽타주)과 Data를 가져와서 같은 방식으로 타이머를 제작해서 콤보를 구현한다.
이번엔 Attack GA에서 제작을 한다.
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());
CurrentComboData = ABCharacter->GetComboActionData();
ABCharacter->GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);
UAbilityTask_PlayMontageAndWait* PlayAttackTask = UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, TEXT("PlayAttack"), ABCharacter->GetComboActionMontage(), 1.0f, GetNextSection());
PlayAttackTask->OnCompleted.AddDynamic(this, &UABGA_Attack::OnCompleteCallback);
PlayAttackTask->OnInterrupted.AddDynamic(this, &UABGA_Attack::OnInterruptedCallback);
PlayAttackTask->ReadyForActivation();
StartComboTimer();
}
Attack GA에서 Actor의 정보를 가져오고, 이를 토대로 데이터를 가져온다.
가져온 Avatar를 부모Character로 캐스팅하고, 데이터를 가져온다.
최초로 호출되었을때 공격 몽타주를 제작하고 타이머를 시작한다.
void UABGA_Attack::StartComboTimer()
{
int32 ComboIndex = CurrentCombo - 1;
ensure(CurrentComboData->EffectiveFrameCount.IsValidIndex(ComboIndex));
const float ComboEffectiveTime = CurrentComboData->EffectiveFrameCount[ComboIndex] / CurrentComboData->FrameRate;
if (ComboEffectiveTime > 0.0f)
{
GetWorld()->GetTimerManager().SetTimer(ComboTimerHandle, this, &UABGA_Attack::CheckComboInput, ComboEffectiveTime, false);
}
}
데이터를 가져와서 타이머를 돌린다.
void UABGA_Attack::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
if (!ComboTimerHandle.IsValid())
{
HasNextComboInput = false;
}
else
{
HasNextComboInput = true;
}
}
타이머가 활성화 되어있는 상태에서 입력이 들어오면 HasNextComboInput 이 true가 된다.
void UABGA_Attack::CheckComboInput()
{
ComboTimerHandle.Invalidate();
if (HasNextComboInput)
{
MontageJumpToSection(GetNextSection());
StartComboTimer();
HasNextComboInput = false;
}
}
타이머가 끝나면 등록되어 있던 CHeckComboInput이 호출되고, 입력값이 있따면 다음 섹션으로 이동해서 애니메이션이 시작된다.
다음과 같은 방식으로 공격 콤보가 이어지게 된다.
언리얼이 제공하는 점프가 아닌 직접 점프를 구현한다.
우선 점프 GA를 제작한다.
UABGA_Jump::UABGA_Jump()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
bool UABGA_Jump::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, OUT FGameplayTagContainer* OptionalRelevantTags) const
{
bool bResult = Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags);
if (!bResult)
{
return false;
}
const ACharacter* Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
return (Character && Character->CanJump());
}
void UABGA_Jump::InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
ACharacter* Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
Character->StopJumping();
}
void UABGA_Jump::OnLandedCallback()
{
bool bReplicatedEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}
ActivateAbility에 어빌리티태스크를 제작해야 하는데 이 AT또한 코드로 제작해본다.
AT로 제작한 JumpAndWaitForLanding 이다.
Jump를 수행하고 랜딩까지 기다렸다가 랜딩에 성공하면 OnComplete 델리게이트를 호출하는 AT를 제작해본다.
UABAT_JumpAndWaitForLanding* UABAT_JumpAndWaitForLanding::CreateTask(UGameplayAbility* OwningAbility)
{
UABAT_JumpAndWaitForLanding* NewTask = NewAbilityTask<UABAT_JumpAndWaitForLanding>(OwningAbility);
return NewTask;
}
void UABAT_JumpAndWaitForLanding::Activate()
{
Super::Activate();
ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
Character->LandedDelegate.AddDynamic(this, &UABAT_JumpAndWaitForLanding::OnLandedCallback);
Character->Jump();
SetWaitingOnAvatar(); //아바타가 준비가 될때까지 기다린다.
}
void UABAT_JumpAndWaitForLanding::OnDestroy(bool AbilityEnded)
{
ACharacter* Character = CastChecked<ACharacter>(GetAvatarActor());
Character->LandedDelegate.RemoveDynamic(this, &UABAT_JumpAndWaitForLanding::OnLandedCallback);
Super::OnDestroy(AbilityEnded);
}
Activate와 OnDestroy는 상속받은 내용이고 꼭 정의가 되어야 한다.
Character가 사용하는 LandedDelegate는 기본 이벤트이다.
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FJumpAndWaitForLandingDelegate);
...
UPROPERTY(BlueprintAssignable)
FJumpAndWaitForLandingDelegate OnComplete;
해당 AT가 끝났을때 호출되는 이벤트인 OnComplete를 제작한다.
void UABAT_JumpAndWaitForLanding::OnLandedCallback(const FHitResult& Hit)
{
if (ShouldBroadcastAbilityTaskDelegates()) //브로드캐스트가 활성화 되기전에 어빌이 활성화 되어있는지 체크한다.
{
OnComplete.Broadcast();
}
}
땅에 도착하는 그순간에 완료되었다고 브로드캐스트를 한다.
수정한 내용에 맞춰 점프도 이제 제작한 GA의 점프로 변경한다.
태그도 추가한다.
void UABGA_Jump::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
UABAT_JumpAndWaitForLanding* JumpAndWaitingForLandingTask = UABAT_JumpAndWaitForLanding::CreateTask(this);
JumpAndWaitingForLandingTask->OnComplete.AddDynamic(this, &UABGA_Jump::OnLandedCallback);
JumpAndWaitingForLandingTask->ReadyForActivation();
}
점프에서 Activate를 하면 다음과 같이 태스크를 제작하고 호출한다.
그렇게 함으로써 전에 했던 문제중 Spacebar를 때면 Tag가 바로 사라졌던 문제가 해결되었다.
이제 랜딩이 되어야지 Tag가 초기화되는것을 확인할 수 있다.
위 내용은 블루프린트에서도 TA를 제작할 수 있다.
스크립트에서 TA부분중 Create 부분이 BlueprintCallable로 제작했기 때문에 블루프린트에서도 호출이 가능하다.
JumpGA에서 ActivateAbility 에서 작동하던 내용을 주석처리하면 완전히 동일한 기능을 수행하는 것을 볼 수 있다.
지금 점프에서도 공격이 중간에 가능한데, 이를 태그로 막을 수 있다.
Activateion Blocked Tags를 통해서 해당 태그상태인 경우엔 작동을 못하도록 막을 수 있다.
위를 통해서 GA의 작동을 제한 할 수 있다.
공격을 주는 충돌체크를 제작한다.
애니메이션에서 사용할 AnimNotify를 상속받아 새로운 클래스를 제작한다.
...
UPROPERTY(EditAnywhere)
FGameplayTag TriggetGameplayTag;
...
태그를 활용해서 블루프린트에 이벤트를 사용할 거라서 태그를 먼저 정의한다.
FString UAnimNotify_GASAttackHitCheck::GetNotifyName_Implementation() const
{
return TEXT("GASAttackHitCheck");
}
void UAnimNotify_GASAttackHitCheck::Notify(USkeletalMeshComponent* MeshComp, UAnimSequenceBase* Animation, const FAnimNotifyEventReference& EventReference)
{
Super::Notify(MeshComp, Animation, EventReference);
if (MeshComp)
{
AActor* OwnerActor = MeshComp->GetOwner();
if (OwnerActor)
{
FGameplayEventData PayloadData;
UAbilitySystemBlueprintLibrary::SendGameplayEventToActor(OwnerActor, TriggetGameplayTag, PayloadData);
}
}
}
노티파이에 도달하면 이때 UAbilitySystemBlueprintLibrary에 포함된 이벤트를 호출한다. OwnerActor에게 Tag를 포함해서 데이터를 전송한다.
기존에 몽타주를 불러올땐 다른 모듈에서 몽타주를 로드했다.
하지만 해당 몽타주에 위와같은 노티파이가 포함이 되어있다면 당연하게도 노티파이는 로드가 안될 것이다.
왜냐하면 최초 모듈이 모두 컴파일되면 다음 모듈이 읽어지기 때문이다.
ArenaBattle 읽고, 다음에 ArenaBattleGas를 읽는다.
그래서 ArenaBattle에 몽타주를 읽어오는 부분을 주석처리하고 GAS로 새로만든 캐릭터플레이어에 넣어준다.
AABGASCharacterPlayer::AABGASCharacterPlayer()
{
ASC = nullptr;
static ConstructorHelpers::FObjectFinder<UAnimMontage> ComboActionMontageRef(TEXT("/Script/Engine.AnimMontage'/Game/ArenaBattleGAS/Animation/AM_ComboAttack.AM_ComboAttack'"));
if (ComboActionMontageRef.Object)
{
ComboActionMontage = ComboActionMontageRef.Object;
}
}
다시 돌아와 몽타주에 노티파이를 모두 변경해주고
태그도 추가해서 넣어준다.
히트를 판단하는 새로운 GA를 제작한다.
해당 GA는 피해를 받는 부분을 판단할 거라서 ASC에 Start Abilites에 추가한다.
제작한 GA에 Triggers를 추가하는데 이때 Trigger Tag는 Notify에 추가했떤 Tag를 넣는다.
Notify에서 지정된 태그는 ASC에 등록된 모든 Trigger에서 자동으로 응답받는다.
즉, Notify에 지정된 모든 태그는 Trigger로 등록된 모든 GA가 응답을 받는다. (물론 지정된 Actor내)
호출받은 대상 GA는 ActivateAbility가 호출이 된다.
게임플레이 어빌리티 타겟 엑터 (GameplayAbilityTargetActor)
게임플레이 어빌리티에서 대상에 대한 판정(주로 물리 판정)을 구현할 때 사용하는 특수한 액터
줄여서 TA라고 함.
확장성을 고려했을때 TA를 사용하는 것이 좋다.
- 타겟을 설정하는 다양한 방법이 있다.
- Trace를 사용해 즉각적인 타겟을 검출할 수 있다.
- 사용자의 최종 확인을 한번 더 거치는 방법이 있다
- 공격 범위 확인을 위한 추가 시각화가 가능하다(시각화를 수행하는 액터를 월드레티클이라 한다)
- StartTargeting , ConfirmTargetingAndContinue, ConfirmTargeting, CancelTargeting
게임플레이 어빌리티 타겟 데이터
타겟 액터에서 판정한 결과를 담은 데이터이다.
타겟 데이터를 여러개 묶어 전송하는것이 일반적인데 이를 타겟 데이터 핸들이라고 한다.
-trace 히트 결과(HitResult), 판정된 다수의 액터 포인터, 시작지점, 끝지점
타겟이 닿았는지 체크를 위해 GameplayAbilityTargetActor를 구현한다.
UCLASS()
class ARENABATTLEGAS_API AABTA_Trace : public AGameplayAbilityTargetActor
{
GENERATED_BODY()
public:
AABTA_Trace();
virtual void StartTargeting(UGameplayAbility* Ability) override;
virtual void ConfirmTargetingAndContinue() override;
void SetShowDebug(bool InShowDebug) { bShowDebug = InShowDebug; }
protected:
virtual FGameplayAbilityTargetDataHandle MakeTargetData() const;
bool bShowDebug = false;
};
void AABTA_Trace::StartTargeting(UGameplayAbility* Ability)
{
Super::StartTargeting(Ability);
SourceActor = Ability->GetCurrentActorInfo()->AvatarActor.Get();
}
GA로 부터 현재 Actor의 정보를 가져와서 SourceActor에 넣는다.
void AABTA_Trace::ConfirmTargetingAndContinue()
{
if (SourceActor)
{
FGameplayAbilityTargetDataHandle DataHandle = MakeTargetData();
TargetDataReadyDelegate.Broadcast(DataHandle);
}
}
다음 프로세스로 타겟의 데이터를 가져와서 브로드캐스트로 이벤트를 호출한다.
FGameplayAbilityTargetDataHandle AABTA_Trace::MakeTargetData() const
{
ACharacter* Character = CastChecked<ACharacter>(SourceActor);
FHitResult OutHitResult;
const float AttackRange = 100.0f;
const float AttackRadius = 50.f;
FCollisionQueryParams Params(SCENE_QUERY_STAT(UABTA_Trace), 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()->SweepSingleByChannel(OutHitResult, Start, End, FQuat::Identity, CCHANNEL_ABACTION, FCollisionShape::MakeSphere(AttackRadius), Params);
FGameplayAbilityTargetDataHandle DataHandle;
if (HitDetected)
{
FGameplayAbilityTargetData_SingleTargetHit* TargetData = new FGameplayAbilityTargetData_SingleTargetHit(OutHitResult);
DataHandle.Add(TargetData);
}
#if ENABLE_DRAW_DEBUG
if (bShowDebug)
{
FVector CapsuleOrigin = Start + (End - Start) * 0.5f;
float CapsuleHalfHeight = AttackRange * 0.5f;
FColor DrawColor = HitDetected ? FColor::Green : FColor::Red;
DrawDebugCapsule(GetWorld(), CapsuleOrigin, CapsuleHalfHeight, AttackRadius, FRotationMatrix::MakeFromZ(Forward).ToQuat(), DrawColor, false, 5.0f);
}
#endif
return DataHandle;
}
SweepSingleByChannel을 통해서 대상이 맞았는지를 체크하게 된다.
만약 맞은 대상이 있다면 TargetData를 제작하고 DataHandle에 넣어준다.
즉, MakeTargetData가 충돌판정을 진행하고 충돌이라면 맞은 대상을 브로드케스트로 데이터를 보내는 것이다.
다시 처음으로 돌아간다.
GA는 활성화 되면서 AT를 제작해서 이벤트를 대기하고, AT는 내부에서 Target을 판단하는 TA를 제작한다.
UABGA_AttackHitCheck::UABGA_AttackHitCheck()
{
InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}
void UABGA_AttackHitCheck::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
UABAT_Trace* AttackTraceTask = UABAT_Trace::CreateTask(this, AABTA_Trace::StaticClass());
AttackTraceTask->OnComplete.AddDynamic(this, &UABGA_AttackHitCheck::OnTraceResultCallback);
AttackTraceTask->ReadyForActivation();
}
GA는 Activate가 되는 순간 AT_Trace를 CreateTask로 제작한다.
이때, Create Task는 다음과 같다.
UABAT_Trace* UABAT_Trace::CreateTask(UGameplayAbility* OwningAbility, TSubclassOf<AABTA_Trace> TargetActorClass)
{
UABAT_Trace* NewTask = NewAbilityTask<UABAT_Trace>(OwningAbility);
NewTask->TargetActorClass = TargetActorClass;
return NewTask;
}
TargetActorClass를 변경 가능하도록 다음과 같이 제작한다.
공격 방식이 지금은 한가지 이지만, 만약 공격 방향이나 크기나 형태가 변경된다면 TargetActor의 클래스 형태도 다양해 질 것이다.
따라서 필요에 맞게 변경이 가능하고 분리하기 위해 모듈형식으로 제작한다.
Create할때 제작해야하는 TargetActorClass의 형태도 같이 받는다.
GA에서 제작한 AT에서 동작이 끝나 OnComplete를 호출할때 OnTraceResultCallBack을 호출받게 된다.
void UABGA_AttackHitCheck::OnTraceResultCallback(const FGameplayAbilityTargetDataHandle& TargetDataHandle)
{
if (UAbilitySystemBlueprintLibrary::TargetDataHasHitResult(TargetDataHandle, 0)) //Hit target이 있는 경우
{
FHitResult HitResult = UAbilitySystemBlueprintLibrary::GetHitResultFromTargetData(TargetDataHandle, 0); //결과 Data를 가져온다.
ABGAS_LOG(LogABGAS, Log, TEXT("Target %s Detected"), *(HitResult.GetActor()->GetName()));
}
bool bReplicatedEndAbility = true;
bool bWasCancelled = false;
EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicatedEndAbility, bWasCancelled);
}
데미지를 받은 대상, 즉 충돌체 데이터를 가져와 로그로 찍고 데이터를 초기화 한다.
이제 AT를 둘러본다.
void UABAT_Trace::Activate()
{
Super::Activate();
SpawnAndInitializeTargetActor();
FinalizeTargetActor();
SetWaitingOnAvatar();
}
AT가 활성화 되면 다음과 같은 메소드를 호출하게 된다.
void UABAT_Trace::SpawnAndInitializeTargetActor()
{
SpawnedTargetActor = Cast<AABTA_Trace>(Ability->GetWorld()->SpawnActorDeferred<AGameplayAbilityTargetActor>(TargetActorClass, FTransform::Identity, nullptr, nullptr, ESpawnActorCollisionHandlingMethod::AlwaysSpawn));
if (SpawnedTargetActor)
{
SpawnedTargetActor->SetShowDebug(true);
SpawnedTargetActor->TargetDataReadyDelegate.AddUObject(this, &UABAT_Trace::OnTargetDataReadyCallback);
}
}
void UABAT_Trace::OnTargetDataReadyCallback(const FGameplayAbilityTargetDataHandle& DataHandle)
{
if (ShouldBroadcastAbilityTaskDelegates())
{
OnComplete.Broadcast(DataHandle);
}
EndTask();
}
TA는 SpawnActorDeferred를 통해 제작이 된다.
CreateTask할때 받았떤 TargetActorClass를 통해 제작하게 되고 해당 Task에서 TargetData가 존재하고 이제 보내줄 준비가 되면 호출하는 델리게이트인 TargetDataReadyDelegate에 CallBack을 등록한다.
void UABAT_Trace::FinalizeTargetActor()
{
UAbilitySystemComponent* ASC = AbilitySystemComponent.Get();
if (ASC)
{
const FTransform SpawnTransform = ASC->GetAvatarActor()->GetTransform();
SpawnedTargetActor->FinishSpawning(SpawnTransform);
ASC->SpawnedTargetActors.Push(SpawnedTargetActor);
SpawnedTargetActor->StartTargeting(Ability);
SpawnedTargetActor->ConfirmTargeting();
}
}
정상적으로 생성된 다음 FinishSpawing을 통해 생성진행을 마무리한다.
ASC엔 SpawnedTargetActors라는 배열이 존재하는데 여기에 넣어주게 된다.
넣어두었던 TA를 바로 Start로 진행시키게 된다.
Start이후 준비가 되면 ConfirmTargeting을 진행하지만 해당 코드에선 다르게 할 게 없기 때문에 바로 실행하게 된다.
실행하면 데미지를 주지 못했을땐 빨간색을, 맞췄을땐 초록색을 띄는것을 볼 수 있다.
'Unreal > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
언리얼 5_4_2 어트리뷰트와 게임플레이 이펙트의 이해 (1) | 2024.03.27 |
---|---|
언리얼5_2_2 : 캐릭터의 애니메이션 설정 (0) | 2024.03.25 |
언리얼5_2_1 : 게임 컨텐츠의 기본 구조 (3) | 2024.03.06 |
언리얼5_1_4 : 언리얼 프로젝트의 에셋 (1) | 2024.03.05 |
언리얼5_1_3 : 언리얼 엔진의 자료구조와 메모리 관리 (1) | 2024.02.16 |