12.04 - TIL

김혁·2025년 12월 4일

TIL

목록 보기
70/84

오늘의 코드카타

  • 등대
    • 그래프에서 인접한 점 2개 중에 하나는 불을 켜야 할 때, 가장 적게 불을 켤 수 있는 수를 구하는 문제
    • edge의 개수는 n-1개이기 때문에 트리 구조라고 볼 수 있고, 1번을 기준으로 잡았을 때 리프 노드에서부터 불을 켠 경우와 켜지 않은 경우를 탐색하면 정답을 도출할 수 있을 것으로 보인다.
    • 리프 노드부터 탐색하기 위해 dfs를 활용했고, 불을 켜지 않은 경우와 켠 경우에 필요한 등대의 개수를 각각 0번, 1번 배열에 저장하는 방식으로 dp를 활용했다.
    • 자신의 노드가 불을 켠 경우는 연결된 노드의 필요한 등대가 가장 적은 경우를 더하게 되고, 불을 켜지 않은 경우는 연결된 노드가 반드시 불이 켜져있어야 한다는 점을 주의해야 했다.
    • https://school.programmers.co.kr/learn/courses/30/lessons/133500

팀 프로젝트 진행 내용

1. 피격 리액션이 클라에서 정상적으로 작동하지 않는 문제 해결

Client (리액션 동작 안 함)Server (리액션 정상 동작)
  • 서버에서는 피격 시 정상적으로 동작을 하지만, 클라에서는 피격 시 리액션이 동작하지 않는 문제가 있었다.
  • UAO_GameplayAbility_MeleeHitConfirm는 데미지를 처리하는 로직이기 때문에 서버에서만 실행되게끔 NetExecutionPolicy를 ServerOnly로 지정했었다. 근데 데미지는 정상적으로 적용이 되는 것을 보아서 해당 과정은 정상적으로 동작을 하지만, 리액션 애니메이션을 실행해주는 어빌리티가 정상적으로 동작하지 않은 것을 알 수 있었다.
  • 리액션 애니메이션은 NetExecutionPolicy를 LocalPredicted로 지정했었는데, 이에 대해 문제가 있어보였다. 데미지를 처리하는 Ability가 Server Only로 되어 있다보니 이벤트를 보내는 것도 서버 ASC에만 보내서 클라에서는 애니메이션이 정상적으로 동작하지 않는다고 추측했다.
  • 이를 해결하기 위해서 UAO_GameplayAbility_HitReact의 NetExecutionPolicy를 EGameplayAbilityNetExecutionPolicy::ServerInitiated로 지정해서 서버에서 몽타주를 재생시키고 이를 클라에게 재생시키도록 전파하게끔 했다.
  • 해당 Ability의 경우, 플레이어의 입력에 따른 즉각 반응이 필요하지 않은, 즉 예측 구조를 활용하지 않고, 서버에서 실행시킴으로써 안전성을 높였다고 볼 수 있다.

Gameplay Ability의 Replication

  1. Local Predicted : 반응성과 정확성 사이에 균형이 잘 잡힌 옵션. 클라이언트가 명령을 내리면 로컬 클라이언트에서 어빌리티가 즉시 실행되지만, 어빌리티의 실제 영향이 어땠는지에 대한 최종 결정은 서버가 내리고, 클라이언트를 오버라이드시킴.
  2. Local Only : 클라이언트가 단순히 로컬에서 어빌리티를 실행함. 서버에 리플리케이트하지 않음.
  3. Server Initiated : 서버에서 시작하는 어빌리티가 클라이언트에 전파됨. 어빌리티를 사용하는 클라이언트에서는 로컬 예측이 없어 딜레이가 관측될 수 있음.
  4. Server Only : 서버에서만 실행되며, 클라이언트로 리플리케이트되지 않음. 어빌리티가 변경하는 모든 변수는 평소처럼 리플리케이트됨. 따라서 어빌리티 자체는 서버에서만 실행되더라도 해당 어빌리티로 인해 변한 데이터는 클라에서 관측할 수 있음.


    참고 : (공식문서)게임플레이 어빌리티 - 리플리케이션

2. Tag 기반으로 다양한 몽타주 재생되게 구현

  • 원래는 HitReact Ability에 재생되는 몽타주를 하나로 설정해서 하나의 몽타주만 재생되게끔 구현했는데, 우리 게임의 기획에 있어서 맷돼지 AI 같은 돌진을 받았을 때는 플레이어가 날라가게끔 구현하고자 했다. 이를 위해 여러 가지의 태그에 맞게 여러 가지 몽타주를 재생시키는 방향으로 설계를 했다.
  • TMap<FGameplayTag, TObjectPtr<UAnimMontage>>를 통해 게임 플레이 태그와 피격 몽타주를 매핑해서 관리하고자 했다.
UCLASS()
class AO_API UAO_GameplayAbility_HitReact : public UGameplayAbility
{
	GENERATED_BODY()
    
    ...

protected:
	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "HitReact")
	TObjectPtr<UAnimMontage> DefaultHitReactMontage;

	UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "HitReact")
	TMap<FGameplayTag, TObjectPtr<UAnimMontage>> HitReactMontageMap;
}
  • TriggerEventData에서 받은 이벤트 태그가 클래스 내부의 맵에 Key로 존재하는지 확인하고 존재한다면 그에 맞는 몽타주를, 없다면 Default 몽타주를 실행시켜 안전성과 예외 처리를 해줬다.
void UAO_GameplayAbility_HitReact::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
	const FGameplayEventData* TriggerEventData)
{
	FGameplayTag EventTag;
	if (TriggerEventData)
	{
		EventTag = TriggerEventData->EventTag;
	}

	TObjectPtr<UAnimMontage> MontageToPlay = nullptr;
	if (EventTag.IsValid())
	{
		if (TObjectPtr<UAnimMontage>* FoundMontage = HitReactMontageMap.Find(EventTag))
		{
			MontageToPlay = FoundMontage->Get();
		}
	}
	
	if (!MontageToPlay)
	{
		MontageToPlay = DefaultHitReactMontage;
	}
	
	TObjectPtr<UAbilityTask_PlayMontageAndWait> MontageTask
		= UAbilityTask_PlayMontageAndWait::CreatePlayMontageAndWaitProxy(this, NAME_None, MontageToPlay);
	
	...
}

3. 방향별로 피격 리액션 작동하게 태그로 구현

  • 피격 리액션을 방향별로 다르게 재생시키고 싶었다.
  • 리액션 Ability 내부에서 피격 방향이 어딘지 계산한 후에 해당 동작을 태그로 구분을 해서 기존의 피격 리액션처럼 TMap에 태그 기반으로 매핑 시켜서 재생시키고자 했다.
  • 먼저 Event.Combat.HitReact.Heavy처럼 태그가 들어온다면, 피격 방향에 맞게 .Front/.Back/.Right/.Left 태그가 추가가 되는 함수를 작성했다.
  • 공격자와 피격자의 벡터를 계산하고, 피격자의 Forward Vector와 내적을 했을 때 특정 값 이상이거나 이하라면 정면 또는 뒷면이라고 판단을 했고, 그외의 경우에 Right Vector와 내적을 했을 때 양수면 우측, 음수면 좌측이라고 판단을 해서 값에 따라 태그를 추가했다.
FString UAO_GameplayAbility_HitReact::GetHitDirectionSuffix(const FGameplayEventData* TriggerEventData,
	const FGameplayAbilityActorInfo* ActorInfo) const
{
	const AActor* InstigatorActor = TriggerEventData ? const_cast<AActor*>(TriggerEventData->Instigator.Get()) : nullptr;
	const AActor* TargetActor = ActorInfo && ActorInfo->AvatarActor.IsValid() ? ActorInfo->AvatarActor.Get() : nullptr;

	if (!InstigatorActor || !TargetActor)
	{
		return TEXT("Front");
	}

	const FVector TargetLocation = TargetActor->GetActorLocation();
	const FVector InstigatorLocation = InstigatorActor->GetActorLocation();

	FVector ToInstigator = InstigatorLocation - TargetLocation;
	ToInstigator.Z = 0.f;
	if (!ToInstigator.Normalize())
	{
		return TEXT("Front");
	}

	FVector Forward = TargetActor->GetActorForwardVector();
	Forward.Z = 0.f;
	if (!Forward.Normalize())
	{
		return TEXT("Front");
	}

	const FVector Right = FVector::CrossProduct(FVector::UpVector, Forward);

	const float ForwardDot = FVector::DotProduct(Forward, ToInstigator);
	const float RightDot = FVector::DotProduct(Right, ToInstigator);

	const float FrontThreshold = 0.707f; // cos(45도)

	if (ForwardDot > FrontThreshold)
	{
		return TEXT("Front");
	}
	if (ForwardDot < -FrontThreshold)
	{
		return TEXT("Back");
	}
	if (RightDot >= 0.f)
	{
		return TEXT("Right");
	}
	return TEXT("Left");
}
  • 해당 Ability를 실행하는 부분에서 기존의 이벤트 데이터로 받은 태그는 베이스 태그로 하였고, 접미사를 추가로 붙여 방향까지 포함한 태그를 새로 생성해서 최종 태그로 하였다.
  • 최종 태그가 TMap에 존재하면 해당 몽타주를 가져오고, 없다면 먼저 베이스 태그를 검색하고 이도 없다면 Default Montage를 실행시킬 몽타주로 지정했다.
void UAO_GameplayAbility_HitReact::ActivateAbility(const FGameplayAbilitySpecHandle Handle,
	const FGameplayAbilityActorInfo* ActorInfo, const FGameplayAbilityActivationInfo ActivationInfo,
	const FGameplayEventData* TriggerEventData)
{
	// 베이스 이벤트 태그 (Event.Combat.HitReact.Heavy)
	FGameplayTag BaseTag;
	if (TriggerEventData)
	{
		BaseTag = TriggerEventData->EventTag;
	}

	const FString DirectionSuffix = GetHitDirectionSuffix(TriggerEventData, ActorInfo);

	// 베이스 태그에 .Front / .Back 등의 접미사를 붙여서 방향까지 포함한 태그 생성
	FGameplayTag FinalTag = BaseTag;
	if (BaseTag.IsValid())
	{
		const FString BaseTagString = BaseTag.GetTagName().ToString();
		const FString FinalTagString = FString::Printf(TEXT("%s.%s"), *BaseTagString, *DirectionSuffix);
		FinalTag = FGameplayTag::RequestGameplayTag(FName(*FinalTagString));
	}

	TObjectPtr<UAnimMontage> MontageToPlay = nullptr;
	
	// 최종 태그로 몽타주 맵에서 찾아보기
	if (FinalTag.IsValid())
	{
		if (TObjectPtr<UAnimMontage>* FoundMontage = HitReactMontageMap.Find(FinalTag))
		{
			MontageToPlay = FoundMontage->Get();
		}
	}

	// 방향까지 붙힌 태그로 못 찾는 경우, 베이스 태그로도 찾아보기
	if (!MontageToPlay && BaseTag.IsValid())
	{
		if (TObjectPtr<UAnimMontage>* FoundMontage = HitReactMontageMap.Find(BaseTag))
		{
			MontageToPlay = FoundMontage->Get();
		}
	}
    
    if (!MontageToPlay)
	{
		MontageToPlay = DefaultHitReactMontage;
	}
    
    ...
}
  • 이러한 로직에 따라 피격 모션을 3개로 분리를 했다. 약하게 공격받는 Event.Combat.HitReact.Light, 강하게 공격받는 Event.Combat.HitReact.Heavy, 플레이어가 쓰러지는 Event.Combat.HitReact.Knockdown으로 크게 3가지의 피격 모션을 가지고, 방향에 따라 4가지씩 더 추가해줬다.

Event.Combat.HitReact.Light

FrontBackRightLeft

Event.Combat.HitReact.Heavy

FrontBackRightLeft

Event.Combat.HitReact.Knockdown

FrontBackRightLeft
profile
게임 개발자를 향해..

0개의 댓글