[UE5] Character Movement Component Series : Prone Mechanic

연하·2024년 9월 26일
0

Character Movement Component In-Depth 강의 시리즈를 공부하면서 한글로 정리한 포스트입니다. 의역과 오역이 난무하니 주의해주세요! https://youtu.be/j45CUV9lWTA?si=nsePEHZSnSmg8hMK

Prone Implementation

UENUM(BlueprintType)
enum ECustomMovementMode
{
	CMOVE_None		UMETA(Hidden),
	CMOVE_Slide		UMETA(DisplayName = "Slide"),
	CMOVE_Prone		UMETA(DisplayName = "Prone"),
	CMOVE_MAX		UMETA(Hidden),
};

새로운 Custom Movement Mode를 추가해주고,

	/// Prone
	UPROPERTY(EditDefaultsOnly) float ProneEnterHoldDuration = .2f;
	UPROPERTY(EditDefaultsOnly) float ProneSlideEnterImpulse = 300.f;
	UPROPERTY(EditDefaultsOnly) float MaxProneSpeed = 300.f;
	UPROPERTY(EditDefaultsOnly) float BrakingDecelerationProning = 2500.f;

	// Prone
private:
	void EnterProne(EMovementMode PrevMode, ECustomMovementMode PrevCustomMode);
	void ExitProne();
	bool CanProne() const;
	void PhysProne(float deltaTime, int32 Iterations);

Prone과 관련된 변수와 함수를 선언해주자.

PhysProne()

전체 구현부는 저자의 깃허브에서 확인하실 수 있습니다!
https://github.com/delgoodie/Zippy

PhysProne 의 경우, PhysWalking 의 코드와 굉장히 유사하게 구현했다. Walking과 Prone은 기본적으로 이동 속도의 변화일 뿐이고, 동작 자체가 매우 유사하다. 이번 튜토리얼에서는 서브 스테핑이 어떻게 작동하는지에 중점을 둘 예정이다.

우리는 서브 스테핑을 할 것이며, 서브 스테핑이 의미하는 것은 한 프레임에서 여러번 반복하는 것이다. 따라서 프레임이 주어지고, 프레임은 특정 델타 타임으로 구성되어 있다. 한 프레임 안에서 여러번 반복하며, 더 정확하고 움직임에 대해 더 높은 해상도의 시뮬레이션을 할 수 있다.

float remainingTime = deltaTime;

while문에 들어가기 전, remainingTime에 deltaTime을 넣어준다.

while ((remainingTime >= MIN_TICK_TIME) && 
	  (Iterations < MaxSimulationIterations) &&
      CharacterOwner && (CharacterOwner->Controller || bRunPhysicsWithNoController || (CharacterOwner->GetLocalRole() == ROLE_SimulatedProxy)))

서브 스테핑에 들어가기 위한 조건문이다. 최소 틱 시간보다 남은 시간이 더 많은지 확인하고, 반복이 최대 시뮬레이션 반복보다 적을때(무한 루프에 들어가는 것을 방지하기 위함), 그리고 밑의 조건들을 만족할 때 서브 스테핑을 실행한다.

Iterations++;
bJustTeleported = false;
const float timeTick = GetSimulationTimeStep(remainingTime, Iterations);
remainingTime -= timeTick;

텔레포트를 사용하지 않기 때문에 bJustTeleported 를 false로 설정하고, GetSimulationTimeStep 함수를 이용해 틱 시간을 얻는다.

// Save current values
UPrimitiveComponent * const OldBase = GetMovementBase();
const FVector PreviousBaseLocation = (OldBase != NULL) ? OldBase->GetComponentLocation() : FVector::ZeroVector;
const FVector OldLocation = UpdatedComponent->GetComponentLocation();
const FFindFloorResult OldFloor = CurrentFloor;

모든 현재 값에 대해 저장된 값을 만들어준다. 이렇게 하는 이유는, 방금 계산한 이동을 되돌려야 할 수도 있기 때문이다.

// Ensure velocity is horizontal.
MaintainHorizontalGroundVelocity();
const FVector OldVelocity = Velocity;
Acceleration.Z = 0.f;

// Apply acceleration
CalcVelocity(timeTick, GroundFriction, false, GetMaxBrakingDeceleration());

MaintainHorizontalGroundVelocity 는 속도를 수평으로 유지하는 데 매우 유용한 함수이다. 우리는 지면에 있을 수도 있고, 경사면에 있을 수도, 평평하지 않은 평면에 있을 수도 있지만 속도는 평면에 수평으로 유지된다. 주로 지상 이동 모드에서 도움이 되는 함수이다. Acceleration 은 입력 벡터라는 것을 잊지 말자. 아무튼, Z값을 0으로 변경하여 위 또는 아래로 가속할 수 없게 한다.

마찰은 항상 캡슐에 적용되므로, 캡슐이 움직일 때 마찰이 가해지며 속도가 느려진다. 그렇기 때문에 무한히 가속할 수 없다. 우리는 캐릭터가 걸어다니다가 키를 놓았을 때 곧바로 멈추기를 원한다. 하지만, 이것을 위해 마찰을 높이게 되면 캐릭터의 이동속도가 정말 느려질 것이고, 슬라이드와 같은 이동 모드가 제대로 구현되지 않을 수 있다. 따라서 존재하는 것이 BrakingDeceleration 인데, 이것은 키를 놓았을때와 같이 어떤 움직임도 적용하지 않을 때 적용되는 값이다. 이 값을 이용해 빠르게 멈출 수 있다.

커스텀 무브먼트 모드의 경우 GetMaxBrakingDeceleration() 함수를 사용하면 0을 반환하므로, 함수를 오버라이드 하여 해당 이동 모드의 감속도를 반환해주어야 한다.

// Compute move parameters
const FVector MoveVelocity = Velocity;
const FVector Delta = timeTick * MoveVelocity; // dx = v * dt
const bool bZeroDelta = Delta.IsNearlyZero();
FStepDownResult StepDownResult;

if ( bZeroDelta )
{
	remainingTime = 0.f;
}
else
{
	MoveAlongFloor(MoveVelocity, timeTick, &StepDownResult);

	if ( IsFalling() )
	{
		// pawn decided to jump up
		const float DesiredDist = Delta.Size();
		if (DesiredDist > KINDA_SMALL_NUMBER)
		{
			const float ActualDist = (UpdatedComponent->GetComponentLocation() - OldLocation).Size2D();
			remainingTime += timeTick * (1.f - FMath::Min(1.f,ActualDist/DesiredDist));
		}
		StartNewPhysics(remainingTime,Iterations);
		return;
	}
	else if ( IsSwimming() ) //just entered water
	{
		StartSwimming(OldLocation, OldVelocity, timeTick, remainingTime, Iterations);
		return;
	}
}

만약 속도가 0이면 캡슐이 어디로도 움직이지 않으므로 서브스테핑이 필요하지 않게된다. 따라서 시뮬레이션을 서두르기 위해 남은 시간을 즉시 0으로 설정한다.

속도가 0이 아니라면, MoveAlongFloor 함수를 이용해 바닥을 따라 이동한다. 이 함수 또한 매우 유용한 헬퍼 함수인데, 이 함수는 컴포넌트를 안전하게 움직여준다. 이름에서 알 수 있듯 바닥을 따라 움직이며, 이것은 정말 도움이 된다. 다양한 각도의 경사면에서 움직일 수 있고, 각각의 속도와 시간 틱을 가질 수 있으며, StepDownResult 이라는 결과값이 반환되기 때문이다. 현재 있는 표면과 이동중인 표면을 아는 것은 정말 중요하다. 캡슐이 계단과 같은 표면에서 움직인다고 상상해보자. 그 표면은 항상 변하기 때문에, 우리는 그에 따라 처리해주어야 한다. StepDownResult를 통해 변화된 표면을 알 수 있다.

이 줄 자체가 실제로 움직임이 일어나는 곳이며, 나머지는 움직임을 정리하는 코드들이다. 첫번째는 움직이다가 떨어졌을 때의 경우이다. 움직이고 있다가 바닥이 없는 곳으로 떨어지면, 우리는 기본적으로 falling 모드에 들어가게 될 것이다. 여기서 볼 수 있는 멋진 것 중 하나는, 남은 시간과 반복을 사용하여 StartNewPhysics 를 실행한다는 것이다. 새로운 물리 시뮬레이션을 전체 델타 타임이 아니라 우리가 가지고 있는 남은 델타 타임을 사용하여 시뮬레이션 중간에 이동 모드를 전환할 수 있게 된다. StartSwimming 의 경우에도, 같은 경우를 설정한다. 이러한 코드들을 사용해, 걷다가 다른 이동 모드로 전환되는 경우를 처리할 수 있게 된다.

// Update floor.
// StepUp might have already done it for us.
if (StepDownResult.bComputedFloor)
{
	CurrentFloor = StepDownResult.FloorResult;
}
else
{
	FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, bZeroDelta, NULL);
}

StepDownResult.bComputedFloor 가 true라면, 캐릭터가 아래로 이동할 때 이미 바닥 정보를 계산했다는 뜻이므로 CurrentFloor 에 현재 바닥 정보를 대입해주고,
false 라면 새로운 바닥 정보를 계산한다.

// check for ledges here
const bool bCheckLedges = !CanWalkOffLedges();
if ( bCheckLedges && !CurrentFloor.IsWalkableFloor() )
{
	// calculate possible alternate movement
	const FVector GravDir = FVector(0.f,0.f,-1.f);
	const FVector NewDelta = bTriedLedgeMove ? FVector::ZeroVector : GetLedgeMove(OldLocation, Delta, GravDir);
	if ( !NewDelta.IsZero() )
	{
		// first revert this move
		RevertMove(OldLocation, OldBase, PreviousBaseLocation, OldFloor, false);

		// avoid repeated ledge moves if the first one fails
		bTriedLedgeMove = true;

		// Try new movement direction
		Velocity = NewDelta/timeTick; // v = dx/dt
		remainingTime += timeTick;
		continue;
	}
	else
	{
		// see if it is OK to jump
		// @todo collision : only thing that can be problem is that oldbase has world collision on
		bool bMustJump = bZeroDelta || (OldBase == NULL || (!OldBase->IsQueryCollisionEnabled() && MovementBaseUtility::IsDynamicBase(OldBase)));
		if ( (bMustJump || !bCheckedFall) && CheckFall(OldFloor, CurrentFloor.HitResult, Delta, OldLocation, remainingTime, timeTick, Iterations, bMustJump) )
		{
			return;
		}
		bCheckedFall = true;

		// revert this move
		RevertMove(OldLocation, OldBase, PreviousBaseLocation, OldFloor, true);
		remainingTime = 0.f;
		break;
	}
}
else
{
	// Validate the floor check
	if (CurrentFloor.IsWalkableFloor())
	{
		AdjustFloorHeight();
		SetBase(CurrentFloor.HitResult.Component.Get(), CurrentFloor.HitResult.BoneName);
	}
	else if (CurrentFloor.HitResult.bStartPenetrating && remainingTime <= 0.f)
	{
		// The floor check failed because it started in penetration
		// We do not want to try to move downward because the downward sweep failed, rather we'd like to try to pop out of the floor.
		FHitResult Hit(CurrentFloor.HitResult);
		Hit.TraceEnd = Hit.TraceStart + FVector(0.f, 0.f, MAX_FLOOR_DIST);
		const FVector RequestedAdjustment = GetPenetrationAdjustment(Hit);
		ResolvePenetration(RequestedAdjustment, Hit, UpdatedComponent->GetComponentQuat());
		bForceNextFloorCheck = true;
	}

	// check if just entered water
	if ( IsSwimming() )
	{
		StartSwimming(OldLocation, Velocity, timeTick, remainingTime, Iterations);
		return;
	}

	// See if we need to start falling.
	if (!CurrentFloor.IsWalkableFloor() && !CurrentFloor.HitResult.bStartPenetrating)
	{
		const bool bMustJump = bJustTeleported || bZeroDelta || (OldBase == NULL || (!OldBase->IsQueryCollisionEnabled() && MovementBaseUtility::IsDynamicBase(OldBase)));
		if ((bMustJump || !bCheckedFall) && CheckFall(OldFloor, CurrentFloor.HitResult, Delta, OldLocation, remainingTime, timeTick, Iterations, bMustJump) )
		{
			return;
		}
		bCheckedFall = true;
	}
}

[*TODO] 32:41 - 42:02 Ledges와 관련된 구현 로직은 추후에 정리하겠습니다 :)

여기서 가장 중요한 부분은 서브 스테핑이다. 더 작은 델타 타임으로 반복을 수행하면 더 정확한 시뮬레이션을 얻을 수 있다는 것을 기억하자.

CanProne()

bool UNyongMovementComponent::CanProne() const
{
	return IsCustomMovementMode(CMOVE_Slide) || IsMovementMode(MOVE_Walking) && IsCrouching();
}

슬라이드하고 있거나, 웅크리고 있을 때 Prone 모드에 들어갈 수 있다.

EnterProne()

void UNyongMovementComponent::EnterProne(EMovementMode PrevMode, ECustomMovementMode PrevCustomMode)
{
	bWantsToCrouch = true;

	if (PrevMode == MOVE_Custom && PrevCustomMode == CMOVE_Slide)
	{
		Velocity += Velocity.GetSafeNormal2D() * ProneSlideEnterImpulse;
	}

	FindFloor(UpdatedComponent->GetComponentLocation(), CurrentFloor, true, NULL);
}

여기서 FindFloor 를 호출하는 이유는 Enter 함수 이후에 MoveAlongFloor 함수가 바로 호출되기 때문이다. MoveAlongFloor 의 가장 첫 줄을 보면, 현재 바닥이 걸을 수 있는지의 여부를 확인하고 그렇지 않다면 return 해버린다. 만약 갱신하지 않고 무브먼트 모드를 변경한다면, 첫번째 서브 스테핑 단계에서 이동이 실패하고 바닥이 갱신되므로, 첫번째 서브스테핑을 성공시키기 위해 함수를 호출한 것이다.

bool UNyongMovementComponent::IsMovingOnGround() const
{
	return Super::IsMovingOnGround() || IsCustomMovementMode(CMOVE_Slide) || IsCustomMovementMode(CMOVE_Prone);
}

Prone도 지상에서 움직이는 이동 모드이므로, IsMovingOnGround() 함수에 추가해준다.

void UNyongMovementComponent::PhysCustom(float deltaTime, int32 Iterations)
{
	Super::PhysCustom(deltaTime, deltaTime);

	switch (CustomMovementMode)
	{
	case CMOVE_Slide:
		PhysSlide(deltaTime, Iterations);
		break;
	case CMOVE_Prone:
		PhysProne(deltaTime, Iterations);
		break;
	default:
		UE_LOG(LogTemp, Fatal, TEXT("Invalid Movement Mode"));
	}
}

PhysCustom 함수에 PhysProne 을 호출해주는 케이스문을 추가해준다.

virtual void OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode) override;
void UNyongMovementComponent::OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode)
{
	Super::OnMovementModeChanged(PreviousMovementMode, PreviousCustomMode);

	if (PreviousMovementMode == MOVE_Custom && PreviousCustomMode == CMOVE_Slide) ExitSlide();
	if (PreviousMovementMode == MOVE_Custom && PreviousCustomMode == CMOVE_Prone) ExitProne();

	if (IsCustomMovementMode(CMOVE_Slide)) EnterSlide(PreviousMovementMode, (ECustomMovementMode)PreviousCustomMode);
	if (IsCustomMovementMode(CMOVE_Prone)) EnterProne(PreviousMovementMode, (ECustomMovementMode)PreviousCustomMode);
}

OnMovementModeChanged 함수를 오버라이드하고 구현해준다. 이부분은 강의 이후 수정된 내용이므로, 뒷부분에서 설명한다.

virtual float GetMaxSpeed() const override;
virtual float GetMaxBrakingDeceleration() const override;
float UNyongMovementComponent::GetMaxSpeed() const
{
	if (IsMovementMode(MOVE_Walking) && Safe_bWantsToSprint && !IsCrouching()) return MaxSprintSpeed;

	if (MovementMode != MOVE_Custom) return Super::GetMaxSpeed();

	switch (CustomMovementMode)
	{
	case CMOVE_Slide:
		return MaxSlideSpeed;
	case CMOVE_Prone:
		return MaxProneSpeed;
	default:
		UE_LOG(LogTemp, Fatal, TEXT("Invalid Movement Mode"))
		return -1.f;
	}
}

float UNyongMovementComponent::GetMaxBrakingDeceleration() const
{
	if (MovementMode != MOVE_Custom) return Super::GetMaxBrakingDeceleration();

	switch (CustomMovementMode)
	{
	case CMOVE_Slide:
		return BrakingDecelerationSliding;
	case CMOVE_Prone:
		return BrakingDecelerationProning;
	default:
		UE_LOG(LogTemp, Fatal, TEXT("Invalid Movement Mode"))
			return -1.f;
	}
}

두 개의 함수를 오버라이드 해주자.

Trigger

이전 강의에서는 compressed flag를 사용했지만, prone의 경우는 sprint와 다르게 일회성 이벤트이기 때문에 prone을 하고 있는지 여부를 서버에 매 프레임마다 알릴 필요가 없기 때문에 사용할 필요가 없다. 이번 강의에서는 RPC를 이용해 트리거할 예정이다.

uint8 Saved_bWantsToProne : 1;
bool Safe_bWantsToProne;

커스텀 SavedMove 구조체에 Saved 변수를 선언해주고, 무브먼트 컴포넌트 안에 Safe 변수를 선언해주자.

void UNyongMovementComponent::FSavedMove_Nyong::SetMoveFor(ACharacter* C, float InDeltaTime, FVector const& NewAccel, FNetworkPredictionData_Client_Character& ClientData)
{
	FSavedMove_Character::SetMoveFor(C, InDeltaTime, NewAccel, ClientData);

	UNyongMovementComponent* CharacterMovement = Cast<UNyongMovementComponent>(C->GetCharacterMovement());
	Saved_bWantsToSprints = CharacterMovement->Safe_bWantsToSprint;
	Saved_bPrevWantsToCrouch = CharacterMovement->Safe_bPrevWantsToCrouch;
	Saved_bWantsToProne = CharacterMovement->Safe_bWantsToProne;
}

void UNyongMovementComponent::FSavedMove_Nyong::PrepMoveFor(ACharacter* C)
{
	Super::PrepMoveFor(C);
	UNyongMovementComponent* CharacterMovement = Cast<UNyongMovementComponent>(C->GetCharacterMovement());
	CharacterMovement->Safe_bWantsToSprint = Saved_bWantsToSprints;
	CharacterMovement->Safe_bPrevWantsToCrouch = Saved_bPrevWantsToCrouch;
	CharacterMovement->Safe_bWantsToProne = Saved_bWantsToProne;
}

SetMoveFor, PrepMoveFor 함수를 구현해주고,

void TryEnterProne() { Safe_bWantsToProne = true; }
UFUNCTION(Server, Reliable) void Server_EnterProne();

void UNyongMovementComponent::Server_EnterProne()
{
	Safe_bWantsToProne = true;
}

두 함수를 선언 및 구현해준다. 두 함수 모두 Safe_bWantsToProne 변수를 true 처리하는 것 뿐이라는걸 알 수 있다.

Server RPC의 경우, 호출될 때 항상 현재 프레임 movement RPC가 도착하기 전에 도착한다. 따라서 이런 RPC가 호출될 때, 서버의 프레임이 클라이언트의 프레임보다 항상 먼저 실행될 수 있다.

클라이언트가 프레임 100에서 움직임 관련 작업을 수행하고 RPC를 서버에 보내면, 서버는 클라이언트가 보낸 RPC를 처리한 후 프레임 100을 실행한다. 따라서 서버는 프레임을 실행하기 전에 클라이언트가 보낸 움직임 정보로 상태 변수를 미리 업데이트하는 것이 중요하다.

FTimerHandle TimerHandle_EnterProne;
void UNyongMovementComponent::CrouchPressed()
{
	bWantsToCrouch = ~bWantsToCrouch;
	GetWorld()->GetTimerManager().SetTimer(TimerHandle_EnterProne, this, &UNyongMovementComponent::TryEnterProne, ProneEnterHoldDuration);
}

void UNyongMovementComponent::CrouchReleased()
{
	GetWorld()->GetTimerManager().ClearTimer(TimerHandle_EnterProne);
}

특정 시간 동안 C키를 누른 후 prone을 시작하므로, 타이머 핸들이 필요하다. 타이머 핸들은 서버와 클라이언트에서 다른 속도로 실행되기 때문에 movement safe하지 않다. 하지만 클라이언트만이 Enter Prone을 호출할 것이기 때문에 괜찮다. 클라이언트만이 C키를 실제로 누르고 뗀걸 알 수 있기 때문이다.

void UNyongMovementComponent::UpdateCharacterStateBeforeMovement(float DeltaSeconds)
{
	// 생략

	if (Safe_bWantsToProne)
	{
		if (CanProne())
		{
			SetMovementMode(MOVE_Custom, CMOVE_Prone);
			if(!CharacterOwner->HasAuthority()) Server_EnterProne();
		}
		Safe_bWantsToProne = false;
	}

	if (IsCustomMovementMode(CMOVE_Prone) && !bWantsToCrouch)
	{
		SetMovementMode(MOVE_Walking);
	}

	Super::UpdateCharacterStateBeforeMovement(DeltaSeconds);
}

Prone으로 들어갈 때, 서버가 아닌 경우 Server RPC를 호출한다.

Amendments

Sprint

void UNyongMovementComponent::OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity)
{
	Super::OnMovementUpdated(DeltaSeconds, OldLocation, OldVelocity);

	if (MovementMode == MOVE_Walking)
	{
		if (Safe_bWantsToSprint)
		{
			MaxWalkSpeed = Sprint_MaxWalkSpeed;
		}
		else
		{
			MaxWalkSpeed = Walk_MaxWalkSpeed;
		}
	}

	Safe_bPrevWantsToCrouch = bWantsToCrouch;
}

원래는 OnMovementUpdated 함수 내에서 Max Walk Speed를 변경하고 있었다. 이부분을 지우고,

float UNyongMovementComponent::GetMaxSpeed() const
{
	if (IsMovementMode(MOVE_Walking) && Safe_bWantsToSprint && !IsCrouching()) return MaxSprintSpeed;

	// 생략
}

이 코드를 작성해준다. 무브먼트 모드가 Walking이고, Sprint를 원하고, Crouch 상태가 아니라면 MaxSprintSpeed 를 반환해주자. 만약 Safe_bWantsToSprint 상태가 아니라면 Super 함수에서 MaxWalkSpeed를 반환할 것이다. 동일한 이동 모드이지만 다른 최대 속도를 갖을 뿐이기 때문에, 좀 더 깔끔하게 구현할 수 있다.

Slide

먼저 슬라이드 구현 로직이 일부 변경 및 추가되었다. 자세한 코드는 깃허브를 참고하자.

void UNyongMovementComponent::EnterSlide()
{
	UE_LOG(LogTemp, Warning, TEXT("Enter Slide"));

	bWantsToCrouch = true;
	Velocity += Velocity.GetSafeNormal2D() * Slide_EnterImpulse;
	SetMovementMode(MOVE_Custom, CMOVE_Slide);
}

원래 EnterSlide() 함수 내에서 무브먼트 모드가 변경됐지만, 이제는 OnMovementModeChanged() 함수에서 변경된다.

void UNyongMovementComponent::OnMovementModeChanged(EMovementMode PreviousMovementMode, uint8 PreviousCustomMode)
{
	Super::OnMovementModeChanged(PreviousMovementMode, PreviousCustomMode);

	if (PreviousMovementMode == MOVE_Custom && PreviousCustomMode == CMOVE_Slide) ExitSlide();
	if (PreviousMovementMode == MOVE_Custom && PreviousCustomMode == CMOVE_Prone) ExitProne();

	if (IsCustomMovementMode(CMOVE_Slide)) EnterSlide(PreviousMovementMode, (ECustomMovementMode)PreviousCustomMode);
	if (IsCustomMovementMode(CMOVE_Prone)) EnterProne(PreviousMovementMode, (ECustomMovementMode)PreviousCustomMode);
}

이 방법으로 구현하면, 이미 기본 함수인 Set Movement Mode 함수만 호출하면 된다. 따로 EnterSlide 함수를 호출할 필요가 없다. 이유는 두 가지가 있는데, 하나는 우선 깔끔하다. 블루프린트에서 Set Movement mode를 호출할 때 Enter 과 Exit 함수가 자동으로 호출되므로, 두 함수를 블루프린트에 노출하지 않아도 자동으로 불러질 것이다. 두번째로, 슬라이드를 종료하고 falling 모드나 Walking, Swimming 모드에 들어갈 수도 있으므로 Exit Slide 함수가 Movement Mode를 설정하는 것을 원하지 않기 때문이다.

enum CompressedFlags
{
	FLAG_Sprint		= 0x10,
	FLAG_Custom_1	= 0x20,
	FLAG_Custom_2	= 0x40,
	FLAG_Custom_3	= 0x80,
};

커스텀 Saved Move 구조체 안에 CompressedFlags 만들어 커스텀 플래그를 만들어주었다. 기존 커스텀 플래그와 값이 같다는 것에 주의하자.

void UNyongMovementComponent::UpdateFromCompressedFlags(uint8 Flags)
{
	Super::UpdateFromCompressedFlags(Flags);

	Safe_bWantsToSprint = (Flags & FSavedMove_Nyong::FLAG_Sprint) != 0;
}

이런식으로 사용할 수 있다. 가독성 말고는 크게 좋은 점은 없지만, 완성도를 높이기 위해 이렇게 했다.

0개의 댓글