728x90
반응형
- 정리
- 캐릭터 무브먼트의 확장
- 네트웍 멀티플레이 게임을 구현하는 여러 방법의 이해
- 캐릭터 클래스에 설정된 캐릭터 무브먼트 컴포넌트를 확장하는 방법의 학습
- 캐릭터 무브먼트 클래스에 관련된 클래스를 확장해 새로운 움직임 기능을 추가하는 방법의 학습
- 캐릭터 무브먼트의 확장
캐릭터 무브먼트 컴포넌트 | 언리얼 엔진 5.1 문서 | Epic Developer Community
- 고급 토픽 참고
- 네트웍 멀티플레이 텔레포트 기능을 구현하는 방법
- 두 가지 시나리오
- CharacterMovementComponent에 텔레포트 명령을 위한 RPC 혹은 프로퍼티 리플리케이션을 새롭게 설계해 추가 구현
- CharacterMovementComponent가 사용하는 RPC 기능에 텔레포트 기능을 추가 선언하고, 몇몇 중요한 가상 함수를 override하여 구현
- 1번의 경우 전체적인 CharacterMovementComponent의 설계를 바꿔야 하지만 2번은 클래스만 교체하는 것으로 간편하게 구현 가능
- 두 가지 시나리오
- 캐릭터 무브먼트 컴포넌트가 관리하는 움직임 정보
- 캐릭터 무브먼트 구현을 위한 움직임 클래스
- FNetworkPredictionData_Client_Character 클래스 (클라이언트 캐릭터 데이터)
- FSavedMove_Character 클래스 (캐릭터 움직임)
- 캐릭터 무브먼트 컴포넌트의 구현 특징
- 캐릭터 무브먼트 컴포넌트는 런타임에서 클라이언트 캐릭터 데이터의 타입을 확정하지 않음
- 클라이언트 캐릭터 데이터 역시 캐릭터 움직임의 타입을 확정하지 않음
- 따라서 두 클래스를 상속받아 움직임을 추가하는 것이 가능함
- 움직임 관련 클래스를 바꾸는데 사용되는 가상 함수
- 캐릭터 무브먼트 컴포넌트 클래스의 GetPredictionData_Client 함수
- 클라이언트 캐릭터 데이터 클래스의 AllocateNewMove와 FreeMove 함수
- 캐릭터 무브먼트 구현을 위한 움직임 클래스
- 언리얼 오브젝트의 인자가 있는 생성자 선언
- 모든 언리얼 오브젝트는 초기화 오브젝트 인자가 있는 생성자를 사용할 수 있음
- 초기화 오브젝트 인자를 사용해 서브 오브젝트 클래스를 변경할 수 있음
- 이를 사용해 새로운 캐릭터 무브먼트 클래스를 생성하지 않고, 우리가 만든 컴포넌트로 교체 가능
- 언리얼 엔진이 지정한 캐릭터 무브먼트 컴포넌트의 이름을 사용해 해당 클래스를 찾을 수 있음
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함수를 바인딩해준다.
- 텔레포트를 구현하는 함수도 추가해준다.
- 정상적으로 우리가 만들어 준 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로 플래그를 설정해주고, 오버라이드한 CharacterMovementComponent의 OnMovementUpdated에 의해 발동조건을 체크하고 텔레포트를 실행한다.
- 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
'공부 > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
[Study] Part 3 - 게임의 완성 (15/15) (3) | 2024.11.10 |
---|---|
[Study] Part 3 - 게임플로우 다듬기 (14/15) (0) | 2024.11.09 |
[Study] Part 3 - 물리 움직임 리플리케이션 (12/15) (0) | 2024.08.29 |
[Study] Part 3 - 움직임 리플리케이션 (11/15) (3) | 2024.08.28 |
[Study] Part 3 - 캐릭터 공격 구현 개선 (10/15) (0) | 2024.08.27 |