오늘의 코드카타
오늘의 공부
팀 프로젝트 진행 내용
1. 기존에 ABP에 작성했던 캐릭터로부터 데이터를 받아오는 로직을 c++로 작성
- 가장 주요한 내용을 보자면,
UAnimInstance의 NativeThreadSafeUpdateAnimation() 함수를 통해 실시간으로 궤적 정보, 이동 정보(속도, 가속도 등), 상태를 업데이트함
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 캐시에 데이터가 없을 확률이 커져서 캐시 미스 발생