기억해 둘 사항들

- std::async의 기본 launch policy는 태스크의 비동기적 실행과 동기적 실행을 모두 허용

- 이러한 유연성 때문에 thread_local 접근의 불확실성이 발생하고, 태스크가 절대로 실행되지 않을 수 있고, wait 호출에 대한 프로그램 논리에도 영향을 미침

- 태스크를 반드시 비동기적으로 실행해야 한다면 std::launch::async를 지정

std::async의 launch policy

std::launch::async

반드시 비동기적으로(다른 스레드에서) 실행

 

std::launch::deferred

std::async가 돌려준 future 객체에 대해 get이나 wait이 호출될 때까지 함수 호출이 지연(deferred)됨

get이나 wait이 호출되면 동기적으로 실행됨

 

Default launch policy는 async | deferred

함수가 비동기로 실행될 수 있고 동기로 실행될 수 있음

따라서 스레드 라이브러리에서 과다구독 회피, 부하 균형화 등을 해결하는 과정에서 비동기 혹은 동기로 유연하게 실행될 수 있음

auto fut1 = std::async(f);

auto fut2 = std::async(std::launch::async | std::launch::deferred, f);

 

함수가 지연 실행될 수 있으므로 함수가 언제 실행될지 예측이 불가능

어떤 스레드에서 호출될 지 예측이 불가능

get이나 wait이 호출이 일어나는 보장이 없을 수도 있으므로, 함수가 실행될 것인지 예측하는 것이 불가능할 수도 있음

 

따라서 어떤 스레드를 사용할지 예측이 불가능하므로 thread_local 지역 변수와 같은 스레드 지역 저장소를 읽거나 쓸 수 없음

만약 f가 지연된다면 fut.wait_for는 항상 std::future_status::deferred를 반환하므로 무한루프가 발생할 가능성이 있음

using namespace std::literals;

void f(){
    std::this_thread::sleep_for(1s);
}

std::future<void> fut = std::async(f);

while(fut.wait_for(100ms) != std::future_status::ready)
{
    ...
}

 

위에 말한 버그는 부하가 많이 걸리지 않으면 발생하지 않을 수 있으므로 개발과 단위 테스트에서 간과하기 쉬움

부하가 많이 걸리면 과다구독이나 스레드 고갈 현상이 발생하여 태스크가 지연될 가능성이 있음

따라서 지연되지 않았을 때에만 timeout 기반 루프에 진입해야 함

if (fut.wait_for(0s) == std::future_status::deferred)
{
    ...
}
else
{
    while(fut.wait_for(100ms) != std::future_status::ready)
    {
        ...
    }
}

 

기본 launch policy를 사용하는 것은 다음 조건이 모두 성립할 때 적합

하나라도 성립하지 않는다면 비동기로 실행할 필요가 있음

  • get이나 wait을 호출하는 스레드와 반드시 동시적으로 실행되지 않아도 됨
  • 여러 스레드 중 어떤 스레드의 thread_local 변수를 읽고 쓰는지 중요하지 않음
  • get이나 wait이 반드시 호출되는 보장이 있거나 태스크가 전혀 실행되지 않아도 됨
  • 태스크가 지연될 수 있음을 고려한 구현

 

일일이 명시적으로 std::launch::sync를 지정하지 않아도 되는 함수 구현

// C+11
template<typename F, typename... Ts>
inline std::future<std::result_of_t<F(Ts...)>> reallyAsync(F&& f, Ts&&... params)
{
    return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}

// C++14
template<typename F, typename... Ts>
inline auto reallyAsync(F&& f, Ts&&... params)
{
    return std::async(std::launch::async, std::forward<F>(f), std::forward<Ts>(params)...);
}

콤보 공격 구현

강의에서는 Prefix와 Index로 다음 Section Montage를 설정했지만, 어빌리티에 ComboActionData들을 저장하는 것이 좋아보임

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);

	CurrentComboData = ABCharacter->GetComboActionData();
	CurrentCombo = 0;
	HasNextComboInput = false;

	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();
}

void UABGA_Attack::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
	HasNextComboInput = true;
}

FName UABGA_Attack::GetNextSection()
{
	CurrentCombo = FMath::Clamp(CurrentCombo + 1, 1, CurrentComboData->MaxComboCount);

	return *FString::Printf(TEXT("%s%d"), *CurrentComboData->MontageSectionNamePrefix, CurrentCombo);
}

void UABGA_Attack::StartComboTimer()
{
	const uint8 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, &ThisClass::CheckComboInput, ComboEffectiveTime, false);
	}
}

void UABGA_Attack::CheckComboInput()
{
	ComboTimerHandle.Invalidate();

	if (HasNextComboInput)
	{
		MontageJumpToSection(GetNextSection());
		StartComboTimer();
		HasNextComboInput = false;
	}
}

 

어빌리티 태스크(AT)의 제작 팁

AT는 UAbilityTask 클래스를 상속 받아 제작

AT 인스턴스를 생성해 반환하는 static 함수를 선언해 구현

AT가 종료되면 GA에 알려줄 델리게이트를 선언하고 종료될 때 델리게이트를 브로드캐스팅

시작과 종료 처리를 위해 Activate와 OnDestroy 함수를 Override

일정 시간이 지난 후 AT를 종료하고자 한다면 활성화 시 SetWaitingOnAvatar 함수를 호출해 Waiting 상태로 설정

만약 Tick을 활성화하고 싶다면 bTickingTask 값을 true로 설정

 

GameplayAbility와 AbilityTask의 실행 흐름

아래 흐름 외에 어빌리티가 종료돼서 AT가 종료되기도 함

 

블루프린트에서 호출을 위한 제작 규칙

static 함수에 UFUNCTION(BlueprintCallable)을 지정

콜백을 위한 델리게이트는 Dynamic Delegate로 선언

AT의 델리게이트에 UPROPERTY(BlueprintAssingable)을 지정

 

UABGA_Jump::UABGA_Jump()
{
	InstancingPolicy = EGameplayAbilityInstancingPolicy::InstancedPerActor;
}

void UABGA_Jump::ActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo, const FGameplayEventData* TriggerEventData)
{
	Super::ActivateAbility(Handle, ActorInfo, ActivationInfo, TriggerEventData);
	
	// UABAT_JumpAndWaitForLanding* JumpAndWaitForLandingTask = UABAT_JumpAndWaitForLanding::CreateTask(this);
	// JumpAndWaitForLandingTask->OnComplete.AddDynamic(this, &ThisClass::OnLandedCallback);
	// JumpAndWaitForLandingTask->ReadyForActivation();
}

void UABGA_Jump::InputReleased(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{
	Super::InputReleased(Handle, ActorInfo, ActivationInfo);

	ACharacter* Character = CastChecked<ACharacter>(ActorInfo->AvatarActor.Get());
	Character->StopJumping();
}

bool UABGA_Jump::CanActivateAbility(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayTagContainer* SourceTags, const FGameplayTagContainer* TargetTags, FGameplayTagContainer* OptionalRelevantTags) const
{
	if (Super::CanActivateAbility(Handle, ActorInfo, SourceTags, TargetTags, OptionalRelevantTags) == false)
	{
		return false;
	}

	const ACharacter* Character = Cast<ACharacter>(ActorInfo->AvatarActor.Get());
	return Character && Character->CanJump();
}

void UABGA_Jump::OnLandedCallback() 
{
	bool bReplicateEndAbility = false;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}

 

UABAT_JumpAndWaitForLanding::UABAT_JumpAndWaitForLanding()
{}

UABAT_JumpAndWaitForLanding* UABAT_JumpAndWaitForLanding::CreateTask(UGameplayAbility* OwningAbility)
{
	UABAT_JumpAndWaitForLanding* NewTask = NewAbilityTask<UABAT_JumpAndWaitForLanding>(OwningAbility);
	return NewTask;
}

void UABAT_JumpAndWaitForLanding::Activate()
{
	Super::Activate();

	ACharacter* Avatar = CastChecked<ACharacter>(GetAvatarActor());
	Avatar->LandedDelegate.AddDynamic(this, &ThisClass::OnLandedCallback);
	Avatar->Jump();

	SetWaitingOnAvatar();
}

void UABAT_JumpAndWaitForLanding::OnDestroy(bool bInOwnerFinished)
{
	ACharacter* Avatar = CastChecked<ACharacter>(GetAvatarActor());
	Avatar->LandedDelegate.RemoveDynamic(this, &ThisClass::OnLandedCallback);

	Super::OnDestroy(bInOwnerFinished);
}

void UABAT_JumpAndWaitForLanding::OnLandedCallback(const FHitResult& Hit)
{
	if (ShouldBroadcastAbilityTaskDelegates())
	{
		OnComplete.Broadcast();
	}
}

플레이어 캐릭터의 ASC 설정

플레이어 캐릭터에 설정하는 것이 가능

하지만 네트워크 멀티플레이를 감안했을 때, 서버에서 클라이언트로 배포되는 액터가 적합

주기적으로 플레이어 정보를 배포하는 PlayerState 액터를 많이 사용하므로 Onwer를 PlayerState로 설정하고, Avatar를 Character로 설정하는 것이 일반적인 방법

 

게임플레이 어빌리티 스펙

게임플레이 어빌리티에 대한 정보(어빌리티의 현재 상태 등)를 담고 있는 구조체

ASC는 직접 어빌리티를 참조하지 않고 스펙 정보만 가지고 있어 어빌리티를 다룰 때 스펙에 있는 Handle을 사용

핸들 값은 전역으로 설정되어 있고, 스펙 생성시 자동으로 1씩 증가함, 기본값 -1

어빌리티 정보 : 스펙

어빌리티 인스턴스에 대한 레퍼런스 : 스펙 핸들

 

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

게임 어빌리티 스펙에는 입력 값을 설정하는 필드 InputID가 제공됨

ASC에 등록된 스펙을 검사해 입력에 매핑된 GA를 찾을 수 있음 : FindAbilitySpecFromInputID

사용자 입력이 들어오면 ASC에서 입력에 관련된 GA를 검색함

 

해당 GA를 발견하면 현재 발동 중인지를 판별

GA가 발동 중이면 입력이 왔다는 신호를 전달 : AbilitySpecInputPressed

GA가 발동하지 않았으면 새롭게 발동시킴 : TryActivateAbility

 

입력이 떨어지면 GA에게 입력이 떨어졌다는 신호 전달 : AbilitySpecInputReleased

 

EnhancedInputComponent의 BindAction 함수를 활용하면 범용적인 입력 처리가 가능해짐

 

AABGASCharacterPlayer::AABGASCharacterPlayer()
	: AABCharacterPlayer()
{
	AbilitySystemComponent = nullptr;
}

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

	if (AABGASPlayerState* CharacterPlayerState = GetPlayerState<AABGASPlayerState>())
	{
		AbilitySystemComponent = CharacterPlayerState->GetAbilitySystemComponent();
		AbilitySystemComponent->InitAbilityActorInfo(CharacterPlayerState, this);

		int32 InputID = 0;

		for (const TSubclassOf<UGameplayAbility>& Ability : Abilities)
		{
			FGameplayAbilitySpec AbilitySpec(Ability);
			AbilitySpec.InputID = InputID;
			++InputID;

			AbilitySystemComponent->GiveAbility(AbilitySpec);
		}
	}
}

void AABGASCharacterPlayer::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	
	SetupGASInputCompnent();
}

void AABGASCharacterPlayer::SetupGASInputCompnent()
{
	if (IsValid(AbilitySystemComponent) == false || InputComponent == nullptr)
	{
		return;
	}

	UEnhancedInputComponent* EnhancedInputComponent = CastChecked<UEnhancedInputComponent>(InputComponent);

	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 0);
	EnhancedInputComponent->BindAction(JumpAction, ETriggerEvent::Completed, this, &AABGASCharacterPlayer::GASInputReleased, 0);
}

void AABGASCharacterPlayer::GASInputPressed(int32 InInputID)
{
	if (FGameplayAbilitySpec* AbilitySpec = AbilitySystemComponent->FindAbilitySpecFromInputID(InInputID))
	{
		AbilitySpec->InputPressed = true;

		if (AbilitySpec->IsActive())
		{
			AbilitySystemComponent->AbilitySpecInputPressed(*AbilitySpec);
		}
		else
		{
			AbilitySystemComponent->TryActivateAbility(AbilitySpec->Handle);
		}
	}
}

void AABGASCharacterPlayer::GASInputReleased(int32 InInputID)
{
	if (FGameplayAbilitySpec* AbilitySpec = AbilitySystemComponent->FindAbilitySpecFromInputID(InInputID))
	{
		AbilitySpec->InputPressed = false;

		if (AbilitySpec->IsActive())
		{
			AbilitySystemComponent->AbilitySpecInputReleased(*AbilitySpec);
		}
	}
}

 

AABGASPlayerState::AABGASPlayerState()
{
	AbilitySystemComponent = CreateDefaultSubobject<UAbilitySystemComponent>(TEXT("AbilitySystemComponet"));
	//AbilitySystemComponent->SetIsReplicated(true); // 네트워크 멀티플레이를 지원한다면 서버에서 클라이언트로 객체가 전송돼야 하므로 컴포넌트가 리플리케이션이 되도록 설정
}

 

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

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

 

NonInstanced

인스턴싱 없이 CDO에서 일괄 처리

가장 가볍지만 상태가 부여되는 게임플레이 어빌리티에 적합하지 않은 옵션

 

InstancedPerActor : 액터마나 하나의 어빌리티 인스턴스를 만들어 처리(Primary Instance)

InstancedPerExecution : 발동시 인스턴스를 생산함

 

네트워크 리플리케이션까지 고려했을 때 InstancedPerActor가 무난한 선택지

 

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

GA의 실행은 한 프레임에서 이루어짐

GA가 시작되면 EndAbility 함수가 호출되기까지는 끝나지 않음

 

애니메이션 재생 같이 시간이 소요되고 상태를 관리해야 하는 어빌리티의 구현 방법
비동기적으로 작업을 수행하고 끝나면 결과를 통보받는 형태로 AT를 제공

 

AT의 활용 패턴

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

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

 

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::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::InputPressed(const FGameplayAbilitySpecHandle Handle, const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo)
{}

void UABGA_Attack::OnCompleteCallback()
{
	bool bReplicateEndAbility = false;
	bool bWasCancelled = false;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}

void UABGA_Attack::OnInterruptedCallback()
{
	bool bReplicateEndAbility = false;
	bool bWasCancelled = true;
	EndAbility(CurrentSpecHandle, CurrentActorInfo, CurrentActivationInfo, bReplicateEndAbility, bWasCancelled);
}

 

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

	if (AABGASPlayerState* CharacterPlayerState = GetPlayerState<AABGASPlayerState>())
	{
		for (const auto& InputAbility : InputAbilities)
		{
			FGameplayAbilitySpec AbilitySpec(InputAbility.Value);
			AbilitySpec.InputID = InputAbility.Key;
			AbilitySystemComponent->GiveAbility(AbilitySpec);
		}
		
		APlayerController* PlayerController = CastChecked<APlayerController>(NewController);
		PlayerController->ConsoleCommand(TEXT("showdebug abilitysystem"));
	}
}

void AABGASCharacterPlayer::SetupGASInputComponent()
{
	EnhancedInputComponent->BindAction(AttackAction, ETriggerEvent::Triggered, this, &AABGASCharacterPlayer::GASInputPressed, 1);
}

 

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

꼭 필요한 상황이 아니라면 GA와 TA는 가급적 자기 역할만 충실하게 구현하는 것이 좋음

게임플레이 태그를 C++에서 설정하는 경우 기획 변경때마다 소스코드 컴파일을 수행해야 하므로 블루프린트에서 설정하는 것이 의존성 분리에 도움이 됨

공식 문서

시스템이 복잡하지만 아직 문서화가 되지 않은 상태

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

 

게임플레이 어빌리티 시스템(GAS) 프레임워크

액터가 소유하고 발동할 수 있는 어빌리티 및 액터 간의 인터렉션 기능을 제공

RPG, 액션 어드벤처, MOBA 장르의 제작을 쉽게하기 위한 도구, 대부분의 게임 제작에 활용 가능

 

장점

유연성과 확장성 : 다양하고 복잡한 게임 제작에 대응할 수 있도록 범용적으로 설계

모듈러 시스템 : 각 기능에 대한 의존성이 최소화되도록 설계

네트워크 지원 : 네트워크 멀티플레이어 게임에서도 활용 가능하도록 설계

데이터 기반 설계 : 데이터를 기반으로 동작하도록 설계

완성도 : 포트나이트를 통해 실효성 검증

 

단점

구성 요소가 많아서 학습하는 비용이 꽤 큼

작은 규모의 프로젝트에는 복잡한 구조로 오버헤드가 부담될 수 있음

 

큰 규모의 RPG 및 네트워크 멀티플레이 게임을 효율적으로 만드는데 적합

 

게임플레이 어빌리티 시스템 핵심 구성 요소

어빌리티 시스템 컴포넌트

프레임워크를 관리하고 처리하는 액터의 중앙 처리 장치

보통 컴포넌트부터 시작이 됨

 

게임플레이 태그 + 게임플레이 태그 컨테이너

현재 프로젝트 레벨에서 액터들의 상태나 행동을 관리

 

게임플레이 어빌리티 + 어빌리티 태스크, 게임플레이 이벤트

모든 스킬, 액션들을 관리

 

게임플레이 이펙트 + 이펙트 실행 계산, 게임플레이 큐

이펙트는 특수효과가 아닌 영향

 

게임플레이 어빌리티에 대한 결과를 시스템에 알려주는 형태

캐릭터가 가진 스탯 등 데이터에 영향을 미침

 

게임플레이 큐가 일반적으로 이펙트라고 부르는 특수 효과

 

어트리뷰트(게임플레이 어트리뷰트 데이터, 어트리뷰트 세트)

스탯 같은 데이터

 

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

 

C++ vs 블루프린트

게임플레이 어빌리티 시스템을 사용할 때는 C++과 블루프린트를 적절하게 혼용하는 것이 가장 좋음

GAS의 기본 설정과 세밀한 제어는 C++에서 진행되도록 구성됨

게임플레이 어빌리티, 게임플레이 이펙트 및 게임플레이 큐는 블루프린트에서 작업이 용이함

 

전체적인 GAS 시스템 설정을 C++로 작업하고, 블루프린트를 활용해 게임 콘텐츠를 제작

 

기억해 둘 사항들

- std::bind를 사용하는 것보다 람다가 더 읽고 쉽고 표현력이 좋음, 더 효율적일 수 있음

- C++14가 아닌 C++11에서는 이동 캡처를 구현하거나 객체를 템플릿화된 함수 호출 연산자에 바인딩할 때 std::bind가 유용할 수 있음

std::bind가 반환한 함수 객체를 바인드 객체라고 부름

 

std::bind보다 람다를 선호하는 이유

C++11에서 람다가 대부분 std::bind보다 적합하고 C++14에서는 람다가 항상 std::bind보다 적합

 

람다가 가독성이 더 좋음

using Time = std::chrono::steady_clock::time_point;
using Duration = std::chrono::steady_clock::duration;

enum class Sound
{
    Beep,
    Siren,
    Whistle
};

void setAlarm(Time t, Sound s, Duration d);

// L은 람다를 뜻함
auto SetSoundL = [](Sound s)
{
    using namespace std::chrono;
    using namespace std::literals;
    setAlarm(steady_clock::now() + 1h, s, 30s);
};

// bind
using namespace std::chrono;
using namespace std::literals;

auto setSoundB = std::bind(setAlarm, std::chrono::steady_clock::now() + 1h, std::placeholders::_1, 30s);

std::placeholders::_1을 사용하여 setSoundB의 첫 매개변수가 setAlram의 두번째 매개변수로 연결하는 과정이 필요함

std::bind 호출만 보면 매개변수의 타입을 알 수 없으므로 setAlarm의 선언을 봐야함

 

위 코드의 bind는 std::chrono::steady_clock::now()가 setAlaram이 호출되는 시점이 아닌 bind에서 바인드 객체를 생성할 때 저장되므로 잘못된 시간

위 문제를 해결한 코드는 람다에 비해 가독성이 떨어짐

// C++14
// 표준 연산자 템플릿에 대한 템플릿 타입 매개변수를 생략할 수 있음
auto setSoundB = std::bind(setAlarm,
                           std::bind(std::plus<>(),
                                     std::bind(std::chrono::steady_clock::now),
                                     1h),
                           std::placeholders::_1, 30s);

// C++11
auto setSoundB = std::bind(setAlarm,
                           std::bind(std::plus<std::chrono::steady_clock::time_point>(),
                                     std::bind(std::chrono::steady_clock::now),
                                     hours(1)),
                           std::placeholders::_1,
                           seconds(30));

 

setAlarm을 오버로딩하면 새로운 문제가 발생하는데, 람다는 기존의 함수를 호출하지만 std::bind는 컴파일되지 않음

컴파일러가 알고 있는 것은 함수 이름 뿐이며 이름만으로 중의성을 해소할 수 없으므로 적절한 함수 포인터 타입으로 캐스팅해야 함

enum class Volume { Normal, Loud, LoudPlusPlus};
void setAlarm(Time t, Sound s, Duration d, Volume v);

using SetAlarm3ParamType = void(*)(Time t, Sound s, Duration d);

auto setSoundB = std::bind(static_cast<SetAlarm3ParamType>(setAlarm),
                           std::bind(std::plus<>(),
                                     std::bind(std::chrono::steady_clock::now),
                                     1h),
                           std::placeholders::_1, 30s);

 

std::bind에 함수 포인터를 전달할 경우 인라인화 가능성이 낮음

람다의 클로저 클래스의 함수 호출 연산자 안에서 setAlarm 호출은 컴파일러가 일반적인 방식으로 인라인화할 수 있는 보통의 함수 호출임

그러나 위의 경우 setAlarm을 가리키는 함수 포인터를 전달하므로 바인드 객체에 대한 함수 호출 연산자 안에서 함수 포인터를 통해 setAlarm이 호출되므로(간접 호출) 컴파일러가 함수 포인터는 런타임에 결정되므로 인라인화할 가능성이 더 낮음

그러므로 bind를 사용할 때보다 람다를 사용할 때 더 빠른 코드가 산출될 수 있음

 

좀 더 복잡한 코드에서 람다의 장점이 두드러짐

람다가 더 짧고, 이해하기 쉽고, 유지보수에도 쉬움

// C++ 14
auto betweenL = [lowVal, highVal](const auto& val) { return lowVal <= val && val <= highVal; };

auto betweenB = std::bind(std::logical_and<>(), std::bind(std::less_equal<>(), lowVal, std::placeholders::_1), std::bind(std::less_equal<>(), std::placeholders::_1, highVal));

// C++ 11
// 비교할 타입을 명시적으로 지정해야 하고 람다에 auto 매개변수를 받지 못함
auto betweenL = [lowVal, highVal](int val) { return lowVal <= val && val <= highVal; };

auto betweenB = std::bind(std::logical_and<bool>(), std::bind(std::less_equal<int>(), lowVal, std::placeholders::_1), std::bind(std::less_equal<int>(), std::placeholders::_1, highVal));

 

bind에 전달된 w는 compressRateB 안에 값으로 전달되어 저장됨

문제는 값으로 전달되는지 알려면 bind의 작동 방식을 알고 있어야 하고 호출 구문 자체로는 추론할 수 없음

반면 람다는 w가 값으로 캡처되는지 참조로 캡처되는지 명백히 드러남

 

람다는 호출 매개변수들이 전달되는 방식이 명백히 드러나지만 바인드 객체는 명확하지 않음

매개변수의 전달 방식을 알려면 bind의 작동 방식을 기억해야 하는데, 함수 호출 연산자가 완벽 전달을 사용하기 때문에 참조로 전달됨

Widget w;

auto compressRateB = std::bind(compress, w, std::placeholders::_1);

auto compressRateL = [w](CompLevel lev) { return compress(w, lev); };

 

결론

따라서 std::bind를 사용하는 코드는 람다에 비해 읽기 힘들고 표현력이 낮고 효율성이 떨어질 가능성이 있음

C++14는 bind를 사용하는 적합한 경우가 없고 C++11에서는 두 경우가 적합할 수 있음

  • 이동 캡처 :  C++11은 이동 캡처를 지원하지 않으므로 람다와 std::bind의 조합을 통해서 이동 캡처를 흉내내는 것이 가능
  • 다형적(polymorphic) 함수 객체 : 바인드 객체에 대한 함수 호출 연산자는 완벽 전달을 사용하기 때문에 완벽 전달의 제약 안에서 어떤 타입의 매개변수도 받을 수 있기 때문에 객체를 템플릿화된 함수 호출 연산자와 바인딩할 때 유용
class PolyWidget
{
public:
    template <typename T>
    void opeartor()(const T& param) const;
};

PolyWidget pw;
auto boundPW = std::bind(pw, std::placeholders::_1);

// 서로 다른 타입의 매개변수들로 호출할 수 있음
// C++11 람다로는 불가능, C++14 람다는 auto 매개변수로 간단히 구현 가능
boundPW(1930);
boundPW(nullptr);
boundPW("RouseBud");

엑셀 데이터의 임포트

DataAsset과 유사하게 FTableRowBase를 상속받은 구조체 선언

엑셀의 Name 컬럼을 제외한 컬럼과 동일하게 UPROPERTY 속성을 선언

엑셀 데이터를 csv로 익스포트한 후 언리얼 엔진에 임포트

#pragma once

#include "CoreMinimal.h"
#include "Engine/DataTable.h"
#include "GameFramework/Actor.h"
#include "ABCharacterStat.generated.h"

USTRUCT()
struct FABCharacterStat : public FTableRowBase
{
	GENERATED_BODY()

public:
	FABCharacterStat() : MaxHp(0.0f),
	                     Attack(0.0f),
	                     AttackRange(0.0f),
	                     AttackSpeed(0.0f),
	                     MovementSpeed(0.0f) {}

protected:
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stat")
	float MaxHp;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stat")
	float Attack;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stat")
	float AttackRange;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stat")
	float AttackSpeed;
	
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Stat")
	float MovementSpeed;

	// 속성들이 추가되거나 제거됐을 때 유연하게 작업하게 위해 float으로 통일
	// 속성 집합이 모두 float이므로 operator+를 수정할 필요가 없음
	FABCharacterStat operator+(const FABCharacterStat& Other) const
	{
		const float* const ThisPtr = reinterpret_cast<const float* const>(this);
		const float* const OtherPtr = reinterpret_cast<const float* const>(&Other);

		FABCharacterStat Result;
		float* ResultPtr = reinterpret_cast<float*>(&Result);
		int32 StatNum = sizeof(FABCharacterStat) / sizeof(float);

		for (int32 i = 0; i < StatNum; ++i)
		{
			ResultPtr[i] = ThisPtr[i] + OtherPtr[i];
		}
		
		return Result;
	}
};

 

데이터를 관리할 싱글톤 클래스의 설정

프로젝트 셋팅에서 싱글톤으로 등록한 언리얼 오브젝트 사용

 

DEFINE_LOG_CATEGORY(LogABGameSingleton);

UABGameSingleton::UABGameSingleton()
{
	static ConstructorHelpers::FObjectFinder<UDataTable> DataTableRef(TEXT("/Script/Engine.DataTable'/Game/ArenaBattle/GameData/ABCharacterStatTable.ABCharacterStatTable'"));
	if (DataTableRef.Object)
	{
		const UDataTable* DataTable = DataTableRef.Object;
		check(DataTable->GetRowMap().Num() > 0);

		TArray<uint8*> ValueArray;
		DataTable->GetRowMap().GenerateValueArray(ValueArray);
		Algo::Transform(ValueArray, CharacterStatTable,
		                [](uint8* Value)
		                {
			                return *reinterpret_cast<FABCharacterStat*>(Value);
		                });
	}
	
	CharacterMaxLevel = CharacterStatTable.Num();
	ensure(CharacterMaxLevel > 0);
}

const UABGameSingleton& UABGameSingleton::Get()
{
	if (UABGameSingleton* Singleton = CastChecked<UABGameSingleton>(GEngine->GameSingleton))
	{
		return *Singleton;
	}

	UE_LOG(LogABGameSingleton, Error, TEXT("Invalid Game Singleton"));
	return *NewObject<UABGameSingleton>();
}

 

프로젝트 주요 레이어

게임 레이어 : 기믹과 NPC

미들웨어 레이어 : 캐릭터의 스탯 컴포넌트, 아이템 박스

데이터 레이어 : 스탯 데이터, 데이터 관리를 위한 싱글톤 클래스

 

캐릭터 스텟 시스템

레벨에 따른 스텟과 무기의 스텟을 더한 값으로 공격 판정을 진행하도록 설계

UCLASS( ClassGroup=(Custom), meta=(BlueprintSpawnableComponent) )
class ARENABATTLE_API UABCharacterStatComponent : public UActorComponent
{
	GENERATED_BODY()

public:
	FORCEINLINE float GetCurrentHp() const { return CurrentHp; }
	FORCEINLINE int32 GetCurrentLevel() const { return CurrentLevel; }
	void SetCurrentLevel(int32 InNewLevel);
	FORCEINLINE void SetModifierStat(const FABCharacterStat& InModifierStat) { ModifierStat = InModifierStat; }
	FORCEINLINE FABCharacterStat GetTotalStat() const { return BaseStat + ModifierStat; }
};

 

액터 생성과 지연 생성의 프로세스

BeginPlay의 실행을 지연시키기 위해 SpawnActorDeferred로 스폰하면 FinishSpawning을 호출하기 전까지 BeginPlay가 실행되지 않음

PostInitalizedComponents 함수도 FinishSpawning 함수 호출 이후에 호출됨

 

void AABStageGimmick::OnOpponentSpawn()
{
	const FTransform SpawnTransform(GetActorLocation() + FVector::UpVector * 88.0f);

	if (AABCharacterNonPlayer* ABOpponentCharacter = GetWorld()->SpawnActorDeferred<AABCharacterNonPlayer>(OpponentClass, SpawnTransform))
	{
		ABOpponentCharacter->OnDestroyed.AddDynamic(this, &AABStageGimmick::OnOpponentDestroyed);
		ABOpponentCharacter->SetCurrentLevel(CurrentStageNum);
		ABOpponentCharacter->FinishSpawning(SpawnTransform);
	}
}

 

INI 파일을 활용한 게임 데이터 관리

+는 배열을 의미

TArray NPCMeshes 변수가 ABCharacterNonPlayer에 선언되어 있다면 이것의 값을 지정

[/Script/ArenaBattle.ABCharacterNonPlayer]
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Barbarous.SK_CharM_Barbarous 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/sk_CharM_Base.sk_CharM_Base 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Bladed.SK_CharM_Bladed 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Cardboard.SK_CharM_Cardboard 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Forge.SK_CharM_Forge 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_FrostGiant.SK_CharM_FrostGiant 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Golden.SK_CharM_Golden 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Natural.SK_CharM_Natural 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Pit.SK_CharM_Pit 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Ragged0.SK_CharM_Ragged0 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_RaggedElite.SK_CharM_RaggedElite 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Ram.SK_CharM_Ram 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Robo.SK_CharM_Robo 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Shell.SK_CharM_Shell 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_solid.SK_CharM_solid 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Standard.SK_CharM_Standard 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Tusk.SK_CharM_Tusk 
+NPCMeshes=/Game/InfinityBladeWarriors/Character/CompleteCharacters/SK_CharM_Warrior.SK_CharM_Warrior

 

NPC 캐릭터를 랜덤하게 스폰

AABCharacterNonPlayer::AABCharacterNonPlayer()
{
	GetMesh()->SetHiddenInGame(true);
}

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

	ensure(NPCMeshes.Num() > 0);

	const int32 RandomIndex = FMath::RandRange(0, NPCMeshes.Num() - 1);
	NPCMeshHandle = UAssetManager::Get().GetStreamableManager().RequestAsyncLoad(NPCMeshes[RandomIndex], FStreamableDelegate::CreateUObject(this, &AABCharacterNonPlayer::NPCMeshLoadCompleted));
}

void AABCharacterNonPlayer::NPCMeshLoadCompleted() const
{
	if (NPCMeshHandle.IsValid())
	{
		if (USkeletalMesh* NPCMesh = Cast<USkeletalMesh>(NPCMeshHandle->GetLoadedAsset()))
		{
			GetMesh()->SetSkeletalMesh(NPCMesh);
			GetMesh()->SetHiddenInGame(false);
		}
	}

	NPCMeshHandle->ReleaseHandle();
}

'언리얼 > 언리얼 구현 예제' 카테고리의 다른 글

언리얼5 HUD UI 구현  (0) 2024.12.18
언리얼5 행동트리(Behavior Tree)  (2) 2024.12.17
언리얼5 무한 맵 제작  (3) 2024.11.18
언리얼5 아이템 시스템  (2) 2024.11.13
언리얼5 캐릭터 스탯과 위젯  (0) 2024.11.05

+ Recent posts

목차