본문 바로가기

공부/이득우의 언리얼 프로그래밍

[Study] Part 3 - 캐릭터 무브먼트의 확장 (13/15)

728x90
반응형

 

  • 정리
    • 캐릭터 무브먼트의 확장
      1. 네트웍 멀티플레이 게임을 구현하는 여러 방법의 이해
      2. 캐릭터 클래스에 설정된 캐릭터 무브먼트 컴포넌트를 확장하는 방법의 학습
      3. 캐릭터 무브먼트 클래스에 관련된 클래스를 확장해 새로운 움직임 기능을 추가하는 방법의 학습

 

캐릭터 무브먼트 컴포넌트 | 언리얼 엔진 5.1 문서 | Epic Developer Community

 

  • 고급 토픽 참고
  • 네트웍 멀티플레이 텔레포트 기능을 구현하는 방법
    • 두 가지 시나리오
      1. CharacterMovementComponent에 텔레포트 명령을 위한 RPC 혹은 프로퍼티 리플리케이션을 새롭게 설계해 추가 구현
      2. CharacterMovementComponent가 사용하는 RPC 기능에 텔레포트 기능을 추가 선언하고, 몇몇 중요한 가상 함수를 override하여 구현
    • 1번의 경우 전체적인 CharacterMovementComponent의 설계를 바꿔야 하지만 2번은 클래스만 교체하는 것으로 간편하게 구현 가능

 

  • 캐릭터 무브먼트 컴포넌트가 관리하는 움직임 정보
    • 캐릭터 무브먼트 구현을 위한 움직임 클래스
      • FNetworkPredictionData_Client_Character 클래스 (클라이언트 캐릭터 데이터)
      • FSavedMove_Character 클래스 (캐릭터 움직임)
    • 캐릭터 무브먼트 컴포넌트의 구현 특징
      • 캐릭터 무브먼트 컴포넌트는 런타임에서 클라이언트 캐릭터 데이터의 타입을 확정하지 않음
      • 클라이언트 캐릭터 데이터 역시 캐릭터 움직임의 타입을 확정하지 않음
      • 따라서 두 클래스를 상속받아 움직임을 추가하는 것이 가능함
    • 움직임 관련 클래스를 바꾸는데 사용되는 가상 함수
      • 캐릭터 무브먼트 컴포넌트 클래스의 GetPredictionData_Client 함수
      • 클라이언트 캐릭터 데이터 클래스의 AllocateNewMoveFreeMove 함수

 

  • 언리얼 오브젝트의 인자가 있는 생성자 선언
    • 모든 언리얼 오브젝트는 초기화 오브젝트 인자가 있는 생성자를 사용할 수 있음
    • 초기화 오브젝트 인자를 사용해 서브 오브젝트 클래스를 변경할 수 있음
    • 이를 사용해 새로운 캐릭터 무브먼트 클래스를 생성하지 않고, 우리가 만든 컴포넌트로 교체 가능
      • 언리얼 엔진이 지정한 캐릭터 무브먼트 컴포넌트의 이름을 사용해 해당 클래스를 찾을 수 있음
    AABCharacterPlayer::AABCharacterPlayer(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer.SetDefaultSubobjectClass<UABCharacterMovementComponent>(AChacter::CharacterMovementComponentName))
    {
    	// ...
    }
    
  • 클라이언트 캐릭터 데이터의 변경

 

클라이언트 캐릭터 데이터의 변경

  • GetPredictionData_Client를 오버라이드해서 변경. 이 함수는 매 틱마다 호출하며, 해당 함수를 오버라이드해줌으로써 FABNetworkPredictionData_Client_Character 함수를 임의로 만든 함수로 교체해줄 수 있다.

 

  • 캐릭터 움직임의 변경

캐릭터 움직임의 변경

  • AllocateNewMove함수를 오버라이드하여 기존의 캐릭터 움직임 클래스를 교체할 수 있다.

 

  • 신규 움직임 능력을 추가하는 방법
    • 캐릭터 움직임 클래스에서 관리하는 특별한 움직임에 대한 플래그 정보
    • 플래그 값이 변경되면 움직임 시스템에서는 이를 중요한 움직임으로 간주함
    • 기능 확장을 위해 예비 플래그를 4개 준비
    • 추가할 새로운 움직임 기능에 대한 플래그를 할당
  • 텔레포트 능력의 추가
    • 3초마다 재생 가능한 텔레포트 움직임을 구현
    • 구현 편의를 위해 텔레포트 입력과 텔레포트 가능 여부를 플래그에 설정
      • 텔레포트 명령을 내렸는가? : FLAG_Custom_0
      • 현재 쿨타임을 포함해 텔레포트가 진행 중인가? : FLAG_Custom_1
    • 두 개의 플래그 정보를 통해 서버는 클라이언트의 상황을 수시로 전달받을 수 있음
  • 텔레포트 능력의 구현 플로우
    • 텔레포트 입력을 시작으로 다음과 같은 플로우로 클라이언트-서버 움직임을 동기화 함

텔레포트 능력의 구현 플로우

 

class ENGINE_API ACharacter : public APawn
{
	GENERATED_BODY()
public:
	/** Default UObject constructor. */
	ACharacter(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());
	// ...
}
  • ACharacter.h

 

  • ACharacter에 있는 생성자가 인자가 있는 생성자인데 기본값이 지정되어 있었기 때문에 그대로 호출할 수 있었다. 이 인자를 받는 생성자를 만들 수 있게 ACharacter를 상속받고 있는 베이스부터 하나씩 일괄적으로 변경해준다.

 

AABCharacterPlayer::AABCharacterPlayer(const FObjectInitializer& ObjectInitializer) 
	: Super(ObjectInitializer.SetDefaultSubobjectClass<UABCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
	{
		// ...
			static ConstructorHelpers::FObjectFinder<UInputAction> InputActionTeleportRef(TEXT("/Script/EnhancedInput.InputAction'/Game/ArenaBattle/Input/Actions/IA_Teleport.IA_Teleport'"));
		if (nullptr != InputActionTeleportRef.Object)
		{
			TeleportAction = InputActionTeleportRef.Object;
		}
	}
	
void AABCharacterPlayer::SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	
	// ...
	
	EnhancedInputComponent->BindAction(TeleportAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::Teleport);
}

void AABCharacterPlayer::Teleport()
{
	AB_LOG(LogABTeleport, Log, TEXT("%s"), TEXT("Begin")); 

	UABCharacterMovementComponent* ABMovement = Cast<UABCharacterMovementComponent>(GetCharacterMovement());
	if (ABMovement)
	{
		ABMovement->SetTeleportCommand();
	}
}
  • AABCharacterPlayer.cpp

 

  • ACharacter를 상속받는 ABCharacterBase, ABCharacterPlayer, ABCharacterNonPlayer의 생성자를 인자로 받는 생성자로 변경시켜주고, ABCharacterPlayer의 경우 CharacterMovementComponent를 바꿔치기 해준다.
  • 생성자에서 인풋으로 텔레포트를 등록해주고, SetupPlayerInputComponent에서 Teleport함수를 바인딩해준다.
  • 텔레포트를 구현하는 함수도 추가해준다.

 

LogABTeleport와 CharacterMovement가 ABCharacter Movement Component로 변경된 모습

  • 정상적으로 우리가 만들어 준 ABCharacterMovementComponent 클래스로 동작하는걸 확인했으니, 구체적인 코드를 작성해준다.

 

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "ABCharacterMovementComponent.generated.h"

// 움직임 클래스
class FABSavedMove_Character : public FSavedMove_Character
{
	// 상위 클래스인 FSavedMove_Character의 Super를 사용하기 위해 (언리얼 오브젝트가 아니라 Super가 없다.)
	typedef FSavedMove_Character Super;

public:
	// 움직임 초기화
	virtual void Clear() override;
	// 최초 위치 지정
	virtual void SetInitialPosition(ACharacter* Character) override;
	// Flag를 판단하고 행동하기 위한 함수
	virtual uint8 GetCompressedFlags() const override;

	uint8 bPressedTeleport : 1;
	uint8 bDidTeleport : 1;
};

class FABNetworkPredictionData_Client_Character : public FNetworkPredictionData_Client_Character
{
	// 상위 클래스인 FSavedMove_Character의 Super를 사용하기 위해 (언리얼 오브젝트가 아니라 Super가 없다.)
	typedef FNetworkPredictionData_Client_Character Super;

public:
	FABNetworkPredictionData_Client_Character(const UCharacterMovementComponent& ClientMovement);

	// 움직임을 저장하는
	virtual FSavedMovePtr AllocateNewMove() override;
};

UCLASS()
class ARENABATTLE_API UABCharacterMovementComponent : public UCharacterMovementComponent
{
	GENERATED_BODY()
	
public:
	// 생성자
	UABCharacterMovementComponent();

	// 입력에서 받은 명령을 수용
	void SetTeleportCommand();

protected:

	// 클라이언트 > 서버
	virtual class FNetworkPredictionData_Client* GetPredictionData_Client() const override;

	// 텔레포트를 하는 함수
	virtual void ABTeleport();


	virtual void OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity) override;
	virtual void UpdateFromCompressedFlags(uint8 Flags) override;

public:
	// 현재 텔레포트 상태를 확인하기 위한
	uint8 bPressedTeleport : 1;
	uint8 bDidTeleport : 1;

protected:
	// 텔레포트 관련 변수들
	UPROPERTY() 
	float TeleportOffset;

	UPROPERTY()
	float TeleportCooltime;
};
  • ABCharacterMovementComponent.h
#include "Character/ABCharacterMovementComponent.h"
#include "GameFramework/Character.h"
#include "ArenaBattle.h"

// 생성자
UABCharacterMovementComponent::UABCharacterMovementComponent()
{
	// 변수들 초기화
	bPressedTeleport = false;
	bDidTeleport = false;

	TeleportOffset = 600.0f;
	TeleportCooltime = 3.0f;
}

// 텔레포트 명령이 들어오면
void UABCharacterMovementComponent::SetTeleportCommand()
{
	bPressedTeleport = true;
}

FNetworkPredictionData_Client* UABCharacterMovementComponent::GetPredictionData_Client() const
{
	if (ClientPredictionData == nullptr)
	{
		UABCharacterMovementComponent* MutableThis = const_cast<UABCharacterMovementComponent*>(this);
		MutableThis->ClientPredictionData = new FABNetworkPredictionData_Client_Character(*this);
	}

	return ClientPredictionData;
}

// 텔레포트를 수행하는 함수
void UABCharacterMovementComponent::ABTeleport()
{
	if (CharacterOwner)
	{
		AB_SUBLOG(LogABTeleport, Log, TEXT("%s"), TEXT("Teleport Begin"));

		// 텔레포트할 위치
		FVector TargetLocation = CharacterOwner->GetActorLocation() + CharacterOwner->GetActorForwardVector() * TeleportOffset;
		CharacterOwner->TeleportTo(TargetLocation, CharacterOwner->GetActorRotation(), false, true);
		bDidTeleport = true;

		// 타이머 (쿨타임)
		FTimerHandle Handle;
		GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
			{
				bDidTeleport = false;
				AB_SUBLOG(LogABTeleport, Log, TEXT("%s"), TEXT("Teleport End")); 
			}
		), TeleportCooltime, false, -1.0f);
	}
}

void UABCharacterMovementComponent::OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity)
{
	// 발동조건
	if (bPressedTeleport && !bDidTeleport)
	{
		ABTeleport();
	}

	if (bPressedTeleport)
	{
		bPressedTeleport = false;
	}
}

// 압축된 CompressedFlag를 풀어주는 함수
void UABCharacterMovementComponent::UpdateFromCompressedFlags(uint8 Flags)
{
	Super::UpdateFromCompressedFlags(Flags);

	// 클라이언트의 명령을 풀어준다.
	bPressedTeleport = (Flags & FSavedMove_Character::FLAG_Custom_0) != 0;
	bDidTeleport = (Flags & FSavedMove_Character::FLAG_Custom_1) != 0;

	// Role이 서버라면..
	if (CharacterOwner && CharacterOwner->GetLocalRole() == ROLE_Authority)
	{
		if (bPressedTeleport && !bDidTeleport)
		{
			AB_SUBLOG(LogABTeleport, Log, TEXT("%s"), TEXT("Teleport Begin"));

			// 텔레포트 실행
			ABTeleport();
		}
	}
}

FABNetworkPredictionData_Client_Character::FABNetworkPredictionData_Client_Character(const UCharacterMovementComponent& ClientMovement) : Super(ClientMovement)
{
	
}

FSavedMovePtr FABNetworkPredictionData_Client_Character::AllocateNewMove()
{
	return FSavedMovePtr(new FABSavedMove_Character());
}

void FABSavedMove_Character::Clear()
{
	Super::Clear();

	bPressedTeleport = false;
	bDidTeleport = false;
}

void FABSavedMove_Character::SetInitialPosition(ACharacter* Character)
{
	Super::SetInitialPosition(Character);

	// 텔레포트 하기전에 
	UABCharacterMovementComponent* ABMovement = Cast<UABCharacterMovementComponent>(Character->GetCharacterMovement());
	if (ABMovement)
	{
		bPressedTeleport = ABMovement->bPressedTeleport;
		bDidTeleport = ABMovement->bDidTeleport;
	}
}

// Flag들을 GetCompressedFlags에 압축해서 보내줘야 한다.
uint8 FABSavedMove_Character::GetCompressedFlags() const
{
	uint8 Result = Super::GetCompressedFlags();

	if (bPressedTeleport)
	{
		// FLAG_Custom_0 : 0x10
		Result |= FLAG_Custom_0;
	}

	if (bDidTeleport)
	{
		// FLAG_Custom_1 : 0x20
		Result |= FLAG_Custom_1;
	}

	return Result;
}
  • ABCharacterMovementComponent.cpp

 

  • ABCharacterMovementComponent
    • 생성자에서는 변수들을 초기화 시켜준다.
    • 텔레포트 명령이 들어오면 SetTeleportCommand로 플래그를 설정해주고, 오버라이드한 CharacterMovementComponentOnMovementUpdated에 의해 발동조건을 체크하고 텔레포트를 실행한다.
    • ABTeleport에서는 텔레포트할 위치를 설정하고, 쿨타임을 돌려준다.
    • 클라이언트에서 서버로 텔레포트에 대한 신호를 전달해주기 위해서는 GetPredictionData_Client함수를 상속받아 사용해준다.
    • GetPredictionData_Client 구현을 위해 새로운 클래스 FNetworkPredictionData_Client_Character를 상속받은 FABNetworkPredictionData_Client_Character를 만들어준다. 생성자와 함께 AllocateNewMove함수를 상속받아 구현해준다. 기존의 로직에서 우리가 만든 코드로 바꿔치기해주고 있다고 생각하면 된다.
    • 이후, 움직임을 저장하는 클래스인 FSavedMove_Character를 상속받은 FABSavedMove_Character를 만들어주고 필요한 함수들을 오버라이드 해주며 기능을 바꿔치기 해준다.
      • Clear - 움직임 초기화
      • SetInitialPosition - 최초 위치 지정
      • GetCompressedFlags - Flag를 판단하고 행동하기 위한 함수
    • SetInitialPosition을 통해 CharacterMovementComponent를 가지고 와서 값을 이식시켜준다.
    • 그러면 ServerMoveRPC를 통해 클라이언트에서 서버로 캐릭터 움직임 플래그를 전달시켜줄 수 있다.
    • 서버는 RPC를 받아 실행해주는데, UpdateFromCompressedFlags함수에서 움직임 플래그를 받아 클라이언트의 명령을 분석하고 그에 맞는 기능을 수행하게 된다.

 

 

 

서버와 클라이언트가 고의적인 렉이 있더라도 서로 텔레포트에 대한 상태 변화가 적용되는 모습

 

 

 

해당 포스트는 인프런의 <이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이어 프레임웍의 이해>
강의를 수강하고 정리한 내용입니다.
 

이득우의 언리얼 프로그래밍 Part3 - 네트웍 멀티플레이 프레임웍의 이해 강의 | 이득우 - 인프런

이득우 | 또 하나의 언리얼 엔진이라고도 불리는 네트웍 멀티플레이어 프레임웍을 학습합니다. 네트웍 멀티플레이어 게임을 제작할 때 반드시 알아야 하는 주요 개념, 내부 동작 원리, 최적화

www.inflearn.com

 

 

 

 

728x90