11.13 - TIL

김혁·2025년 11월 13일

TIL

목록 보기
56/84

오늘의 코드카타

오늘의 공부

팀 프로젝트 진행 내용

1. 기존에 ABP에 작성했던 캐릭터로부터 데이터를 받아오는 로직을 c++로 작성

  • 가장 주요한 내용을 보자면, UAnimInstanceNativeThreadSafeUpdateAnimation() 함수를 통해 실시간으로 궤적 정보, 이동 정보(속도, 가속도 등), 상태를 업데이트함
void UAO_PlayerCharacter_AnimInstance::NativeThreadSafeUpdateAnimation(float DeltaSeconds)
{
	Super::NativeThreadSafeUpdateAnimation(DeltaSeconds);
	if (!Proxy.Owner || !Proxy.Character || !Proxy.MovementComponent) return;

	UpdateTrajectory(DeltaSeconds);
	UpdateEssentialValues();
	UpdateStates();
}
  • 궤적 정보의 경우, UPoseSearchTrajectoryLibrary.h에 정의되어 있는 함수를 통해 생성하게 되고, 이후에 중력과 충돌 정보를 통해 수정을 함
void UAO_PlayerCharacter_AnimInstance::UpdateTrajectory(float DeltaSeconds)
{
	UAnimInstance* AnimInstance = GetOwningComponent()->GetAnimInstance();

	FPoseSearchTrajectoryData TrajectoryData;
	if (Speed2D > 0.f)
	{
		TrajectoryData = TrajectoryData_Moving;
	}
	else
	{
		TrajectoryData = TrajectoryData_Idle;
	}

	// Generates a prediction trajectory based of the current character intent
	UPoseSearchTrajectoryLibrary::PoseSearchGenerateTransformTrajectory(
		AnimInstance,
		TrajectoryData,
		DeltaSeconds,
		Trajectory,
		PreviousDesiredControllerYaw,
		Trajectory
	);

	// Process InTrajectory to apply gravity and handle collisions
	UPoseSearchTrajectoryLibrary::HandleTransformTrajectoryWorldCollisions(
		this,
		this,
		Trajectory,
		true,
		0.01f,
		Trajectory,
		Trajectory_Collision,
		TraceTypeQuery1,
		false,
		TArray<AActor*>(),
		EDrawDebugTrace::None,
		true,
		150.f
	);

	UPoseSearchTrajectoryLibrary::GetTransformTrajectoryVelocity(Trajectory, -0.3f, -0.2f, TrjPastVelocity, false);
	UPoseSearchTrajectoryLibrary::GetTransformTrajectoryVelocity(Trajectory, 0.f, 0.2f, TrjCurrentVelocity, false);
	UPoseSearchTrajectoryLibrary::GetTransformTrajectoryVelocity(Trajectory, 0.4f, 0.5f, TrjFutureVelocity, false);
}
  • 이동 정보는 크게 캐릭터의 위치 정보와 가속도, 속력 등을 업데이트함
void UAO_PlayerCharacter_AnimInstance::UpdateEssentialValues()
{
	// Update Character Transform
	CharacterTransformLastFrame = CharacterTransform;
	CharacterTransform = Proxy.Character->GetActorTransform();

	// Update Acceleration
	AccelerationLastFrame = Acceleration;
	Acceleration = Proxy.MovementComponent->GetCurrentAcceleration();
	AccelerationAmount = Acceleration.Length() / Proxy.MovementComponent->GetMaxAcceleration();
	bHasAcceleration = (AccelerationAmount > 0.f);

	// Update Velocity
	VelocityLastFrame = Velocity;
	Velocity = Proxy.MovementComponent->Velocity;
	Speed2D = Velocity.Size2D();
	bHasVelocity = (Speed2D > 5.f);
	VelocityAcceleration = (Velocity - VelocityLastFrame) / GetDeltaSeconds();
}
  • 플레이어의 상태는 Chooser Table에서 PSD를 선택하는 기준으로, AO_PlayerCharacter_MovementEnums.h 파일에서 Enum 값으로 다양한 상태들을 실시간으로 업데이트함
// AO_PlayerCharacter.cpp

void UAO_PlayerCharacter_AnimInstance::UpdateStates()
{
	// Update MovementMode (OnGround, InAir)
	MovementModeLastFrame = MovementMode;
	if (Proxy.MovementComponent->MovementMode == MOVE_None
		|| Proxy.MovementComponent->MovementMode == MOVE_Walking
		|| Proxy.MovementComponent->MovementMode == MOVE_Flying)
	{
		MovementMode = ECustomMovementMode::OnGround;
	}
	else if (Proxy.MovementComponent->MovementMode == MOVE_Falling)
	{
		MovementMode = ECustomMovementMode::InAir;
	}

	// Update MovementState (Moving, Idle)
	MovementStateLastFrame = MovementState;
	if (IsMoving())
	{
		MovementState = EMovementState::Moving;
	}
	else
	{
		MovementState = EMovementState::Idle;
	}

	// Update Gait (Run, Walk, Sprint)
	GaitLastFrame = Gait;
	if (Proxy.Character->Gait == EGait::Run)
	{
		Gait = EGait::Run;
	}
	else if (Proxy.Character->Gait == EGait::Walk)
	{
		Gait = EGait::Walk;
	}
	else if (Proxy.Character->Gait == EGait::Sprint)
	{
		Gait = EGait::Sprint;
	}

	// Update Stance (Stand, Crouch)
	StanceLastFrame = Stance;
	if (Proxy.MovementComponent->IsCrouching())
	{
		Stance = EStance::Crouch;
	}
	else
	{
		Stance = EStance::Stand;
	}
}
  • 추가적으로 캐릭터의 이동 상태를 확인하기 위해서 다양한 함수를 선언해서 구현
  • 모두 Chooser Table에서 Row 값으로 활용하고 있고, ABP에서 모션 매칭 노드를 사용할 때 추가적으로 설정하기 위해 사용하게 되는 함수도 있음
// Movement Analysis
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
bool IsMoving();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
bool IsStarting();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
bool IsPivoting();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
bool ShouldTurnInPlace();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
bool ShouldSpinTransition();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
bool JustTraversal();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
bool JustLanded_Heavy();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
bool JustLanded_Light();
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "PlayerCharacter|MovementAnalysis", meta = (BlueprintThreadSafe))
float GetTrajectoryTurnAngle();

2. 플레이어 걷기 기능 추가 및 상태(Gait) 관리 로직 개선

  • 걷기 기능을 추가했고, Sprint 및 Walk 동작 실행 시에 CharacterInputState 구조체를 변경해서 구조체의 bool 값을 활용해서 상태를 관리하는 것으로 수정
// AO_PlayerCharacter.h

UPROPERTY(EditAnywhere, Category = "PlayerCharacter|Input")
FCharacterInputState CharacterInputState;
UPROPERTY(EditAnywhere, Category = "PlayerCharacter|Movement")
EGait Gait = EGait::Run;
// AO_PlayerCharacter.cpp

void AAO_PlayerCharacter::StartSprint()
{
	CharacterInputState.bWantsToSprint = true;
	CharacterInputState.bWantsToWalk = false;
	SetCurrentGait();
}

void AAO_PlayerCharacter::HandleWalk()
{
	if (CharacterInputState.bWantsToSprint)
	{
		return;
	}

	CharacterInputState.bWantsToWalk = !CharacterInputState.bWantsToWalk;
	SetCurrentGait();
}

void AAO_PlayerCharacter::SetCurrentGait()
{
	if (CharacterInputState.bWantsToSprint)
	{
		Gait = EGait::Sprint;
        GetCharacterMovement()->MaxWalkSpeed = 800.f;
	}
	else if (CharacterInputState.bWantsToWalk)
	{
		Gait = EGait::Walk;
        GetCharacterMovement()->MaxWalkSpeed = 200.f;
	}
	else
	{
		Gait = EGait::Run;
        GetCharacterMovement()->MaxWalkSpeed = 500.f;
	}
}

3. 캐릭터의 상태를 멀티플레이 환경에서 Replicate 되게 수정

  • Gait 상태와 착륙했을 시에 속력과 착륙 여부를 Replicated 설정
  • 해당 정보는 AnimInstance에서 활용하고 있음
// AO_PlayerCharacter.h

UPROPERTY(EditAnywhere, ReplicatedUsing = OnRep_Gait, Category = "PlayerCharacter|Movement")
EGait Gait = EGait::Run;
UPROPERTY(BlueprintReadOnly, Replicated, Category = "PlayerCharacter|Movement")
FVector LandVelocity;
UPROPERTY(BlueprintReadOnly, Replicated, Category = "PlayerCharacter|Movement")
bool bJustLanded = false;

UFUNCTION(Server, Reliable)
void ServerRPC_SetInputState(bool bWantsToSprint, bool bWantsToWalk);
UFUNCTION()
void OnRep_Gait();
  • 상태의 경우, 플레이어의 UX를 위해서 클라이언트에서 먼저 실행시키고, 서버에서 값이 변경됐다는 것을 알림
  • 또한 상태와 캐릭터의 이동속도를 분리를 해서, 클라이언트의 과한 권한을 방지함
// AO_PlayerCharacter.cpp

void AAO_PlayerCharacter::StartSprint()
{
	CharacterInputState.bWantsToSprint = true;
	CharacterInputState.bWantsToWalk = false;
	SetCurrentGait();

	if (!HasAuthority())
	{
		ServerRPC_SetInputState(CharacterInputState.bWantsToSprint, CharacterInputState.bWantsToWalk);
	}
}

void AAO_PlayerCharacter::SetCurrentGait()
{
	if (CharacterInputState.bWantsToSprint)
	{
		Gait = EGait::Sprint;
	}
	else if (CharacterInputState.bWantsToWalk)
	{
		Gait = EGait::Walk;
	}
	else
	{
		Gait = EGait::Run;
	}

	OnRep_Gait();
}

void AAO_PlayerCharacter::OnRep_Gait()
{
	switch (Gait)
	{
	case EGait::Walk:
		GetCharacterMovement()->MaxWalkSpeed = 200.f;
		break;
	case EGait::Run:
		GetCharacterMovement()->MaxWalkSpeed = 500.f;
		break;
	case EGait::Sprint:
		GetCharacterMovement()->MaxWalkSpeed = 800.f;
		break;
	}
}

알게 된 내용

1. 애님 인스턴스를 c++로 사용할 때 주의할 점

  • 멀티스레드 환경에서 초기 세팅이 매우 중요한데, 초기화 시에 Pawn 정보를 바로 가져올 시에는 오류가 발생함
  • 따라서 보통의 경우, 별도의 Proxy 객체를 사용해서 Game Thread -> Animation Thread 간 데이터 동기화를 안전하게 이루어지도록 함
USTRUCT()
struct FAOAnimInstanceProxy : public FAnimInstanceProxy
{
	GENERATED_BODY()

protected:
	virtual void InitializeObjects(UAnimInstance* InAnimInstance) override;
	virtual void PreUpdate(UAnimInstance* InAnimInstance, float DeltaSeconds) override;
	virtual void Update(float DeltaSeconds) override;

public:
	UPROPERTY(Transient)
	APawn* Owner = nullptr;
	UPROPERTY(Transient)
	AAO_PlayerCharacter* Character = nullptr;
	UPROPERTY(Transient)
	UCharacterMovementComponent* MovementComponent = nullptr;
};

UCLASS()
class AO_API UAO_PlayerCharacter_AnimInstance : public UAnimInstance
{
	GENERATED_BODY()

protected:
	UPROPERTY(Transient)
	FAOAnimInstanceProxy Proxy;
	virtual FAnimInstanceProxy* CreateAnimInstanceProxy() override { return &Proxy; }
	virtual void DestroyAnimInstanceProxy(FAnimInstanceProxy* InProxy) override {}
    
    ...
}
  • 추가적으로 InitializeObjects() 함수에서 Proxy에 선언된 변수들을 초기화하는데, 에디터에서 애님 프리뷰할 때는 TryGetPawnOwner()가 항상 nullptr을 반환한다고 함
  • nullptr을 반환해서 팅길 수도 있기 때문에 이를 위한 방지코드도 필요함
void FAOAnimInstanceProxy::InitializeObjects(UAnimInstance* InAnimInstance)
{
	FAnimInstanceProxy::InitializeObjects(InAnimInstance);

	if (!InAnimInstance) return;

	// 프리뷰 월드 방지
	if (!InAnimInstance->GetWorld() || InAnimInstance->GetWorld()->IsPreviewWorld()) return;
	
	Owner = InAnimInstance->TryGetPawnOwner();
	if (!Owner) return;

	Character = Cast<AAO_PlayerCharacter>(Owner);
	if (!Character) return;
	
	MovementComponent = Character->GetCharacterMovement();
}

2. 명시적으로 함수에 ThreadSafe를 선언해주는 방법

  • Chooser Table에서 Row 값을 함수로 사용하기 위해서는 ThreadSafe한 함수를 사용하는 것이 필요함
  • 이를 c++에서 명시적으로 설정하기 위해서는 아래와 같이 UPROPERTY에서 meta를 통해 설정이 가능함
UFUNCTION(BlueprintCallable, BlueprintPure, meta = (BlueprintThreadSafe))
bool IsMoving();

오늘의 CS

포인터 vs 레퍼런스

  • 포인터
    • 어떤 객체의 주소를 저장하는 변수
    • nullptr 저장 가능
    • 가리키는 대상을 바꿀 수 있음 -> 연산 가능
  • 레퍼런스
    • 기존 객체의 또 다른 이름
    • 선언 시 반드시 유효한 객체를 가리켜야 함
    • 한 번 바인딩하면 다른 대상을 가리킬 수 없음 -> 산술 연산 불가능
    • 주로 함수 파라미터로 const 키워드와 함께 사용해서 변경하지 못하지만 복사 비용이 발생하지 않게 사용 가능

포인터 vs 레퍼런스의 성능 차이

  • 단순히 언어 차원에서의 성능보다는, CPU 메모리 계층 구조와 메모리 접근 패턴에서 차이가 나게 됨
  • 레퍼런스
    • 이미 존재하는 객체의 별칭으로, 가리키는 대상이 확정적 정보이기 때문에 주소가 이미 확정적이라서, 컴파일러는 보통 레지스터로 최적화해서 접근
  • 포인터
    • 런타임에 가리키는 대상이 달라질 수 있기 때문에 최적화를 제한적으로 적용하게 되고, 불필요한 메모리 접근이 발생할 수 있음
    • 포인터가 랜덤한 위치를 가리킨다면, CPU 캐시에 데이터가 없을 확률이 커져서 캐시 미스 발생
profile
게임 개발자를 향해..

0개의 댓글