일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스포일러 주의
- 2023 게이밍 인 구글 클라우드
- 주식
- 스즈메의 문단속
- 리팩터링 4장
- 1일차
- shader
- 언리얼 5
- GenAI
- 이득우의 언리얼 프로그래밍 1
- 구글 컨퍼런스
- 112일선
- 224일선
- 리팩터링
- 언리얼5
- 2023 게이밍
- 작계훈련
- 공부
- 2023 구글 클라우드
- JavaScript
- 2023 Gaming
- 리팩터링 3장
- 전주비빔 라이스 버거
- 448일선
- URP
- 주식단테
- 상계9동
- 산토리 하이볼
- 이득우의 언리얼 프로그래밍1
- unity
- Today
- Total
개발 이야기 안하는 개발자
언리얼5_3_4 : 움직임 리플리케이션 본문
클라이언트의 입력 정보를 서버로 보내고 서버에서 확인 후 수정을 거친다.
Autonomous Proxy 클라이언트의 진행 (ReplicateMoveToServer())
클라이언트 캐릭터의 움직임을 보관하는 네트워크 클라이언트 데이터 생성
클라이언트의 데이터에 저장된 움직임 중에 참고할 중요한 움직임 기록(OldMove)
현재 틱의 움직임을 기록하는 신규 움직임 생성(NewMove)
입력을 처리하기 전의 각종 초기화 상태를 저장(StartLocation)
필요시 최종 움직임과 현재 움직임을 병합 시도
클라이언트 로컬에서의 움직임 진행(PerformMovement)
신규 움직임에 움직임 결과 상태를 저장(SavedLocation)
신규 움직임을 클라이언트 데이터에 추가
ServerMove함수를 호출해 OldMove와 NewMove를 서버에 전송
클라이언트가 호출하는 서버 RPC (ServerMove())
클라이언트의 최종 움직임 정보를 서버에 보내는 함수
- 타임 스탬프 : 움직임에 대한 시간 정보
- 가속 정보 : 캐릭터의 최종 위치 정보. 캐릭터가 베이스 위에 있는 경우는 상대 위치를 사용
- 플래그 : 특수한 움직임에 대한 정보(점프, 웅크리기)
- 회전 정보 : 압축된 회전 정보 (Yaw 회전 중심으로 저장)
- 본 정보 : 스켈레탈 메시 컴포넌트인 경우, 기준이 되는 본 정보
- 무브먼트 모드 정보 : 캐릭터 컴포넌트의 무브먼트 모드 정
서버의 처리 (ServerMove_Implementation())
서버 캐릭터의 움직임을 보관하는 네트워크용 서버 데이터 생성
클라이언트로부터 받은 타임스탬프 값을 검증
- 타임 스탬프 값을 다양한 방법으로 검증
- 상당한 시간 차가 감지되면, 해킹 방지를 위해 서버 틱으로 제한함
- 네트워크 매니저 설정의 보상 비율을 사용해 클라이언트와 서버 시간을 서서히 균등화시킴
압축된 가속, 회전 데이터를 디코딩하고 클라이언트와 서버의 타임 스탬프 정보를 기록
MoveAutonomous함수를 호출해 서버 캐릭터를 이동시킴
클라이언트와의 차이를 비교하고 에러를 수정함
- 떨어지는 상황, 착지할 때의 상황에 따라 허용 가능 범위 내에서 클라이언트 데이터를 신뢰함
- 상당한 시간 차가 감지되면, 수정 정보를 기록함(PendinAdjustment)
서버가 호출하는 클라이언트 RPC (ClientAdjustPosition())
클라이언트에게 수정할 위치 정보를 알려주는 함수
중복없이 서버 틱의 마지막에서 수정이 필요한 때만 전송함
- 타임 스탬프 : 클라이언트의 타임 스탬프 값
- 델타 타임 : 서버의 델타 타임
- 무브먼트 모드 정보 : 압축된 캐릭터 컴포넌트의 무브먼트 모드 정보
- 새로운 속도 : 수정할 새로운 속도 정보
- 새로운 위치 : 수정할 새로운 위치 정보
- 새로운 회전 : 수정할 새로운 회전 정보
- 새로운 베이스와 베이스 본 이름 : 수정할 베이스에 대한 정
클라이언트의 수정 처리 (ClientAdjustPosition_Implementation())
타임 스탬프 값을 통해 서버로부터 확인받은 움직임 정보를 기록 (LastAckedMove)
서버에서 전달받은 위치로 루트 컴포넌트(캐릭터)의 위치를 변경
서버에서 전달받은 속도로 무브먼트 컴포넌트의 속도를 수정
베이스 정보와 위치를 수정
서버에 의해 클라이언트 위치가 업데이트되었다고 기록(bUpdatePositon)
- 서버의 수정정보를 바탕으로 MoveAutonomous 함수를 호출해 클라이언트에서 남은 움직임을 재생함.
움직임 리플리케이션의 디버깅
서버에서의 오차 발생시 드로우 디버그
- 전달받은 클라이언트 위치를 붉은색으로 표시
- 서버에서 움직인 위치를 녹색으로 표시
오차를 전달받은 클라이언트에서의 드로우 디버그
- 클라이언트가 지정했던 위치를 붉은색으로 표시
- 서버가 수정해준 위치를 녹색으로 표시
- 수정은 발생했지만 서버와 클라이언트 위치가 거의 동일한 경우에는 노란색으로 표시
디버깅을 찍어보기 위해서는 DefaultEngine.ini에서 로직을 추가해야한다.
[Core.Log]
LogNetPlayerMovement=VeryVerbose
위 코드를 추가하면 서버와 클라이언트의 모든 움직임 정보를 로그로 출력할 수 있게 된다.
코드를 추가하고 실행하고 난 뒤에 `를 누른후 p.NetShowCorrections 1을 입력하면 클라이언트의 위치가 녹색으로 표시가 된다.
PktLag=500 도 추가하고 난뒤 공격을 하고 이동을 하면 초록색 표시가 많이 뜨는 것을 볼 수 있다.
공격하고 난뒤 바로 이동하면 클라이언트 입장에선 정상 작동하고, 서버에서는 공격을 진행하면서 이동하는 것을 볼 수 있게 된다.
해당 원인은 클라이언트는 공격중에 못움직이지만 서버입장에선 이 오차때문에 이동중이라 판단하고 공격중에 이동하는 것이다. 이동하는 메소드에서 이동을 못하는 상태일땐 반환하도록 한다.
QuaterMove()
...
if (!bCanAttack)
{
return;
}
이렇게 함으로써 움직임을 막기 때문에 딜레이는 있지만 정상 작동하는 것을 볼 수 있다.
액터의 리플리케이션 플로우
프로퍼티 리플리케이션을 사용해 움직임 정보를 전송하고, 클라이언트는 받을 때마다 이를 적용한다.
움직임 리플리케이션을 위한 서버의 준비 (ReplicatedMovement())
서버에서 SimulatedProxy로 보내는 움직임 정보를 기록한 멤버 변수
OnRep_ReplicatedMovement로 이벤트 함수 호출함.
일반 움직임과 물리 움직임의 리플리케이션을 모두 처리하는 용도로 활용됨.
FRepMovement 구조체의 주요 멤버 변수
- 위치와 회전 : 컴포넌트의 현재 위치와 회전
- 데이터 정밀도 설정 : 위치, 회전, 속도의 데이터 정밀도. 끊김 현상이 보이지 않을 만큼 최소로 설정
- 물리 시뮬레이션 여부 플래그 : 물리 시물레이션으로 복제할지를 지정
- 이동 속도 : 컴포넌트의 이동 속도
- 각 속도 : 컴포넌트의 각 속도. 물리 시물레이션 진행시에만 사용
- 서버 프레임 : 서버에서의 물리 프레임
물리 움직임의 기록 (FRigidBodyState())
액터의 물리 상태를 기록하는 구조체
다음과 같은 멤버 변수로 구성되어 있음
- 위치 : 소수점 두 자리 정밀도로 기록됨
- 회전 : 사원수 정보로 기록됨
- 속도 : 소수점 두 자리 정밀도로 기록됨
- 각속도 : 소수점 두 자리 정밀도로 기록됨
- 플래그 : 휴면 상태와 같은 특정 물리 상태를 기록하는데 사용
액터 리플리케이션의 준비 (GatherCurrentMovement())
현재 액터의 움직임을 ReplicatedMovement 속성으로 변환해 설정하는 함수
액터의 PreReplication 함수에서 호출됨
액터의 물리 움직임과 일반 움직임을 구분해 각각처리함.
일반 움직임은 단순히 액터의현재 위치, 회전, 속도값을 ReplicatedMovement에 저장함.
물리 시뮬레이션의 경우 현재 월드에 설정된 물리 씬에서 해당 컴포넌트의 물리 상태를 저장함
- 현재 컴포넌트의 물리 상태 정보를 ReplicatedMovement로 옮김 (FRigidBodyState::FillFrom)
이를 통해 최종 ReplicatedMovement가 설정되어 클라이언트에 보내짐
액터의 움직임 리플리케이션 옵션을 활성화해주어야 올바로 동작함
액터 움직임 정보의 수신 (OnRep_ReplicatedMovement())
ReplicatedMovement의 물리 시뮬레이션 속성(bRepPhysics) 여부에 따라 두 가지로 실행됨.
일반적인 움직임에 대한 처리
- Simulated Proxy에 대해서만 처리함.
- 컴포넌트의 위치와 회전 정보를 갱신함.
- 속도 처리는 별도로 진행하지 않음.
물리 움직임에 대한 처리
- ReplicatedMovement의 정보를 현재 컴포넌트의 물리 상태로 옮김(FRigidBodyState::CopyTo)
- 물리 리플리케이션 씬에서 컴포넌트와 일치하는 타겟을 찾아서 업데이트(FReplicatedPhyscisTarget)
물리 움직임의 동기화 (ApplyRigidBodyState())
물리 리플리케이션의 틱에서 호출되는 함수
클라이언트의 물리 상태가 서버의 물리 상태의 오차 내에 있을 때까지 계속 호출됨
다음과 같은 로직으로 진행됨
1. 서버에서 받은 최종 속도와 핑을 기반으로 클라이언트의 물리 상태를 외삽(Extrapolation)으로 예측
2. 예측한 위치와 방향이 서버와 비교해 올바른지 체크하고 문제가 있다고 판단되면 에러 시간을 누적함.
3. 누적된 에러 시간 설정값을 넘으면 강제 조정(하드 스냅)을 진행함.
4. 차이가 크지 않다면 내삽(Interpolation)을 수행해 현재 위치, 회전, 속도, 각속도를 조정함
움직임 리플리케이션 옵션을 활성화해주어야 올바로 동작함
움직임을 동기화 해주기 위해선 Replicate Movement와 Replicates를 활성화 해주어야 한다.
움직임 동기화를 위해 발판을 만들어 본다.
빈 액터 BP에 StaticMesh를 Root로 지정하고 큐브를 넣는다.
간단한게 권한을 확인하고, MoveTimeline으로 시간값을 가져와 AddActorLocalOffset을 통해 이동시키는 로직을 구현한다.
MoveTimeLine은 아래와 같고, 부드럽게 보간을 위해 모두 선택하고 Auto로 변경한다.
이상태로 진행하면 서버에서는 큐브가 움직이지만 클라이언트에선 움직이지 않는다.
그후에 Details에서 Replicate Movement 와 Replicates의 값을 넣어주면 클라이언트에서도 움직이는 것을 확인할 수 있다.(C++에선 액터에 있으며 bReplicates와 bReplicatedMovement로 변경이 가능하다)
똑같은 방식으로 물리값에도 테스트를 해본다.
이렇게 물리값을 적용 시켜놓은 공을 클라이언트에서는 이 내용이 보이지 않는다.
이후 Replicates와 ReplicateMovement를 활성화 하면 클라이언트에도 이가 적용되는 것을 볼 수 있다.
하지만 이렇게 하면 문제가 되는게 서버와 클라이언트 모두 물리값이 적용되는 것은 맞지만 두개의 물리 충돌 연산값이 달라서 결과값이 바뀐다. 즉, 클라이언트와 서버의 공 위치가 바뀌게 된다.
이를 해결하기 위해선 이를 연산 받아주는 타 오브젝트들에게 StaticMeshReplicateMovement를 활성화 해주어야 한다.
결과를 보면 모두 정상작동하는 것을 볼 수 있다.
시뮬레이티드 프록시 클라이언트의 움직임 진행 (SimulatedTick())
SimulatedProxy의 캐릭터가 처리하는 캐릭터의 움직임.
캐릭터가 Simulated Proxy인 경우 캐릭터만의 추가적인 작업을 수행한다(SimulateMovement)
시뮬레이션으로 캡슐을 이동시킨 후 메시의 움직임을 부드럽게 보간한다 (SmoothClientPosition)
네트워크 멀티플레이 텔레포트 기능을 구현하는 방법
1. 캐릭터 무브먼트 컴포넌트에 텔레포트 명령을 위한 RPC 혹은 프로퍼티 리플리케이션을 새롭게 설계해 추가 구현.
2. 캐릭터 무브먼트 컴포넌트가 사용하는 RPC 기능에 텔레포트 기능을 추가 선언하고, 몇몇 중요한 가상 함수를 override하여 구현
캐릭터 무브먼트 컴포넌트가 관리하는 움직임 정보
캐릭터 무브먼트 구현을 위한 움직임 클래스
- FNetworkPredictionData_Client_Character 클래스 (클라이언트 캐릭터 데이터)
- FSavedMove_Character 클래스 (캐릭터 움직임)
캐릭터 무브먼트 컴포넌트의 구현 특징
- 캐릭터 무브먼트 컴포넌트는 클라이언트 캐릭터 데이터의 타입을 확정하지 않음.
- 클라이언트 캐릭터 데이터 역시 캐릭터 움직임의 타입을 확정하지 않음.
- 따라서 두 클래스를 상속받아 움직임을 추가하는 것이 가능함.
움직임 관련 클래스를 바꾸는데 사용되는 가상 함수
- 캐릭터 무브먼트 컴포넌트 클래스의 GetPredictionData_Client 함수
- 클라이언트 캐릭터 데이터 클래스의 AllocateNewMove 와 FreeMove 함
언리얼 오브젝트의 인자가 있는 생성자 선언
모든 언리얼 오브젝트는 초기화 오브젝트 인자가 있는 생성자를 사용할 수 있음.
초기화 오브젝트 인자를 사용해 서브 오브젝트 클래스를 변경할 수 있음.
이를 사용해 새로운 캐릭터 무브먼트 클래스를 생성하지 않고, 우리가 만든 컴포넌트로 교체 가능
- 언리얼 엔진이 지정한 캐릭터 무브먼트 컴포넌트의 이름을 사용해 해당 클래스를 찾을 수 있음.
FABNetworkPredictionData_Client_Character는 틱마나 호출되는데, 이 함수를 구현하면 언리얼 엔진에서 제공하는 FNetworkPredictionData_Client_Character를 제작한 클래스로 바꿔치기가 가능하다.
같은 방식으로 FABSavedMove_Character를 구현하면 제작한 움직임 클래스로 교체가 가능하다.
신규 움직임 능력을 추가하는 방법
캐릭터 움직임 클래스에서 관리하는 특별한 움직임에 대한 플래그 정보
플래그 값이 변경되면 움직임 시스템에서는 이를 중요한 움직임으로 간주함
기능 확장을 위해 예비 플래그를 4개 준비해두었음.
우리가 추가할 새로운 움직임 기능에 대한 플래그를 할당
위 4개를 통해 서버에게 움직임을 알려주는데, 아래 4개를 가지고 제작하면 된다.
텔레포트 능력의 추가
3초마다 재생 가능한 텔레포트 움직임의 구현함.
구현 편의를 위해 텔레포트 입력과 텔레포트 가능 여부를 플래그에 설정
- 텔레포트 명령을 내렸는가? : FLAG_Custom_0
- 현재 쿨타임을 포함해 텔레포트가 진행중인가? : FLAG_Custom_1
두 개의 플래그 정보를 통해 서버는 클라이언트의 상황을 수시로 전달 받을 수 있음.
텔레포트 능력의 구현 플로우
텔레포트 입력을 시작으로 다음과 같은 플로우로 클라이언트-서버 움직임을 동기화
텔레포트 기능을 구현해 본다
EnhancedInputComponent->BindAction(TeleportAction, ETriggerEvent::Triggered, this, &AABCharacterPlayer::Teleport);
SetupPlayerInputComponent에 추가해서 해당 입력 이벤트를 등록한다.
void AABCharacterPlayer::Teleport()
{
AB_LOG(LogABTeleport, Log, TEXT("%s"), TEXT("Begin"));
UABCharacterMovementComponent* ABMovement = Cast<UABCharacterMovementComponent>(GetCharacterMovement());
if (ABMovement)
{
ABMovement->SetTeleportCommand();
}
}
캐릭터의 무브먼트를 가져와서 제작할 무브먼트로 캐스트를 진행한다.
CharacterMovementComponent를 상속받는 ABCharacterMovement 클래스를 제작한다.
AABCharacterPlayer::AABCharacterPlayer(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer.SetDefaultSubobjectClass<UABCharacterMovementComponent>(ACharacter::CharacterMovementComponentName))
{
생성자를 const FObjectInitializer& ObjectInitializer 로 매개변수를 받게 한다.
이렇게 ObjectInitiaizer를 받은 상태에서 Super를 통해 캐릭터의 무브먼트를 가져와 형변환을 진행하게 한다.
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; //CoolDown
protected:
UPROPERTY()
float TeleportOffset;
UPROPERTY()
float TeleportCooltime;
};
버튼을 누르면 SetTeleportCommand() 가 호출된다.
void UABCharacterMovementComponent::SetTeleportCommand()
{
bPressedTeleport = true;
}
이후, CharacterMovementComponent가 지원하는 메소드인 OnMovementUpdated에서 캐릭터가 움직였는 지를 체크한다.
void UABCharacterMovementComponent::OnMovementUpdated(float DeltaSeconds, const FVector& OldLocation, const FVector& OldVelocity)
{
if (bPressedTeleport && !bDidTeleport)
{
ABTeleport();
}
if (bPressedTeleport)
{
bPressedTeleport = false;
}
}
체크하는 내용을 가져와서 텔레포트 버튼을 눌렀는지, 쿨타임중인지를 체크해서, 참이라면 텔레포트를 진행한다.
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);
}
}
시선 방향으로 거리만큼 텔레포트 한다.
이때, TeleportTo 는 언리얼이 지원하는 순간이동 함수이다.
서버와 통신하려고 사용하는 클래스이다.
class FABNetworkPredictionData_Client_Character : public FNetworkPredictionData_Client_Character
{
typedef FNetworkPredictionData_Client_Character Super;
public:
FABNetworkPredictionData_Client_Character(const UCharacterMovementComponent& ClientMovement);
virtual FSavedMovePtr AllocateNewMove() override;
};
인자가 있는 생성자를 제작한다.
FABNetworkPredictionData_Client_Character::FABNetworkPredictionData_Client_Character(const UCharacterMovementComponent& ClientMovement)
: Super(ClientMovement)
{
}
FNetworkPredictionData_Client* UABCharacterMovementComponent::GetPredictionData_Client() const
{
if (ClientPredictionData == nullptr)
{
UABCharacterMovementComponent* MutableThis = const_cast<UABCharacterMovementComponent*>(this);
MutableThis->ClientPredictionData = new FABNetworkPredictionData_Client_Character(*this);
}
return ClientPredictionData;
}
멤버변수 ClientPredictionData가 널일경우 자신을 AB MovementComponent로 변경하고 Mutable로 제작한다.
그리고 이를 새로운 클래스로 제작하는데, 이 내용은 원래 기존에 있던 코드이다.
그대로 가져와 붙여넣은 내용이다.
서버에서 받은 움직임 데이터를 저장하는 클래스를 상속받아 다시 제작한다.
class FABSavedMove_Character : public FSavedMove_Character
{
typedef FSavedMove_Character Super;
public:
virtual void Clear() override;
virtual void SetInitialPosition(ACharacter* Character) override;
virtual uint8 GetCompressedFlags() const override;
uint8 bPressedTeleport : 1;
uint8 bDidTeleport : 1;
};
Super로 정의해놓음으로써 부모의 메소드를 바로 호출하도록 한다.
이후, 메소드에서 부모의 메소드를 호출하기 위해 Super로 호출하게 된다.
Clear()로 움직임을 초기화 한다.
void FABSavedMove_Character::SetInitialPosition(ACharacter* Character)
{
Super::SetInitialPosition(Character);
UABCharacterMovementComponent* ABMovement = Cast<UABCharacterMovementComponent>(Character->GetCharacterMovement());
if (ABMovement)
{
bPressedTeleport = ABMovement->bPressedTeleport;
bDidTeleport = ABMovement->bDidTeleport;
}
}
데이터를 위와 같이 정의해서 가져온다.
이후, 해당 데이터를 서버로 보내는게 아니고 압축해서 보내주어야 한다.
uint8 FABSavedMove_Character::GetCompressedFlags() const
{
uint8 Result = Super::GetCompressedFlags();
if (bPressedTeleport)
{
Result |= FLAG_Custom_0;
}
if (bDidTeleport)
{
Result |= FLAG_Custom_1;
}
return Result;
}
이렇게 각 움직임 데이터를 FLAG에 싣어서 데이터를 보내게 된다.
이렇게 서버로 보내게 되면 서버에서는 이를 해석하고 적용해야 한다.
그렇기 위해서 제작했던 MovementComponent에 새로운 메소드를 정의한다.
void UABCharacterMovementComponent::UpdateFromCompressedFlags(uint8 Flags)
{
Super::UpdateFromCompressedFlags(Flags);
bPressedTeleport = (Flags & FSavedMove_Character::FLAG_Custom_0) != 0;
bDidTeleport = (Flags & FSavedMove_Character::FLAG_Custom_1) != 0;
if (CharacterOwner && CharacterOwner->GetLocalRole() == ROLE_Authority)
{
if (bPressedTeleport && !bDidTeleport)
{
AB_SUBLOG(LogABTeleport, Log, TEXT("%s"), TEXT("Teleport Begin"));
ABTeleport();
}
}
}
서버의 역할인 경우에만 계산을 진행한다.
텔레포트를 아직 안했다면 하도록 로직을 구현한다.
'Unreal > 이득우의 언리얼 프로그래밍' 카테고리의 다른 글
언리얼5_3_5 : PVP 게임 제작 (0) | 2024.04.03 |
---|---|
언리얼5_3_3 : RPC의 이해와 활용 (0) | 2024.03.31 |
언리얼5_3_2 : 액터 리플리케이 (1) | 2024.03.29 |
언리얼5_3_1 : 언리얼 네트워크 멀티플레이어 프레임워크 기초 (2) | 2024.03.29 |
언리얼5_2_5 : 언리얼 게임의 완성 (0) | 2024.03.28 |