본문 바로가기

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

[Study] Part 3 - 움직임 리플리케이션 (11/15)

728x90
반응형

 

 

  • 정리
    • 움직임 리플리케이션
      1. 네트웍 멀티플레이에서 안정적으로 캐릭터 움직임을 동기화하는 플로우의 이해
      2. 움직임 리플리케이션에 관련된 언리얼 소스코드 분석
      3. 움직임 리플리케이션을 디버깅하는 방법의 학습

 

 

  • 캐릭터 움직임의 리플리케이션 플로우
    • 클라이언트의 입력 정보를 서버로 보내고 서버에서 확인 후 수정을 거침

 

  • 네트웍 캐릭터 무브먼트에 관한 언리얼 공식 문서
  • 입력을 하면 입력을 원하는 가속도로 변환해서 PerformMovement를 호출해서 이동 계산을 진행한다는 것.
  • 매 틱마다 진행된다.
  • ACharacterAPawn에서 파생되었지만 단순히 캐릭터 이동 컴포넌트가 추가된 폰이 아니다. UCharacterMovementComponent네트워크 리플리케이션을 진행할 수는 있지만 폰에다가 다른 무브먼트 컴포넌트를 부착한다고 ACharacter처럼 네트워크 리플리케이션이 진행되는 건 아니다.
  • PerformMovement는 캐릭터를 물리적으로 이동시키는 역할을 담당. 네트워크가 연결되어 있지 않은 게임에서는 매 틱마다 직접 호출된다. 물리를 구현 (Phys~)
  • 네트웍 움직임 리플리케이션세 가지 역할에 대해서 각각 다르게 동작한다.
    • Autonomous Proxy
      • PerformMovement를 통해 실제로 이동을 구현
      • 이동에 대한 데이터를 SavedMoves라는 큐에 대기시킨다.
      • 유사한 움직임들은 함께 묶고, 서버 이동 RPC를 실행시켜 서버로 전송한다.
    • Authority
      • Authoritative Actor (Server)
        • 서버는 ServerMove를 수신하고 PerformMovement를 사용하여 클라이언트의 움직임을 재현
        • 서버의 캐릭터를 이동시킨 후에 클라이언트가 보고한 종료 위치와 일치하는 지 확인
        • 일치하면 유효하다는 신호를 클라이언트에게 보내고, 그렇지 않으면 ClientAdjustPositionRPC로 정정 신호를 보낸다.
        • 서버는 ReplicatedMovement 구조를 복제하여 연결된 다른 클라이언트의 시뮬레이션된 프록시에 움직임, 현재상태 등을 전송한다.
      • Autonomous Proxy (Owning Player’s Client)
        • ClientAdjustPositionRPC를 받으면 서버의 이동을 다시 체크한 후, 자신이 가지고 있는 SavedMoves 큐에서 일치하는 움직임을 바탕으로 다시 추적후 최종 위치를 찾는다.
    • Simulated Proxy
      • 복제된 움직임 정보를 부드럽게 시각적으로 표현해준다.
  • 매 틱마다 RPC를 호출해 움직임 정보를 동기화한다.

 

  • Autonomous Proxy, Authority, Simulated Proxy에 대한 내용이 기억나지 않는다면, 다음 글을 참고
 

[Study] Part 3 - 액터 리플리케이션 기초 (5/15)

정리액터 리플리케이션 기초액터 리플리케이션의 개요에 대한 학습C++을 활용한 프로퍼티 리플리케이션의 구현C++와 블루프린트간의 프로퍼티 리플리케이션 방식의 차이 비교 액터 리플리케이

j1y00h4.tistory.com

 

 

 


 

 

 

  • 주요 함수 정리
  • Autonomous Proxy 클라이언트의 진행
    • RepicatedMoveToServer
      • 클라이언트 캐릭터의 움직임을 보관하는 네트웍용 클라이언트 데이터 생성
      • 클라이언트의 데이터에 저장된 움직임 중에 참고할 중요한 움직임 기록 (OldMove)
      • 현재 틱의 움직임을 기록하는 신규 움직임 생성 (NewMove)
      • 입력을 처리하기 전의 각종 초기화 상태를 저장 (ex. StartLocation)
      • 필요시 최종 움직임과 현재 움직임을 병합 시도
      • 클라이언트 로컬에서의 움직임 진행 (PerformMovement)
      • 신규 움직임에 움직임 결과 상태를 저장 (ex. SavedLocation)
      • 신규 움직임을 클라이언트 데이터에 추가
      • ServerMove 함수를 호출해 OldMoveNewMove를 서버에 전송

 

  • 클라이언트가 호출하는 서버 RPC
    • ServerMove
      • 클라이언트의 최종 움직임 정보를 서버에 보내는 함수
        • 타임스탬프 : 움직임에 대한 시간 정보
        • 가속 정보 : 입력으로 발생된 최종 가속 정보를 작은 사이즈로 인코딩
        • 위치 정보 : 캐릭터의 최종 위치 정보. 캐릭터가 베이스(ex. 플랫폼) 위에 있는 경우는 상대 위치를 사용
        • 플래그 : 특수한 움직임(점프, 웅크리기)에 대한 정보
        • 회전 정보 : 압축된 회전 정보 (Yaw 회전 중심으로 저장)
        • 본 정보 : 스켈레탈 메시 컴포넌트의 경우, 기준이 되는 본 정보
        • 무브먼트 모드 정보 : 캐릭터 컴포넌트의 무브먼트 모드 정보
      • 함수의 선언은 Character 클래스에서 진행하고, 구현도 캐릭터에서 일단은 진행하지만 모든 로직의 수행은 CharacterMovementComponent에 있는 로직과 함수를 통해 진행. RPCCharacter에서 선언

 

  • 서버의 처리
    • ServerMove_Implementation
      • 서버 캐릭터의 움직임을 보관하는 네트웍용 서버 데이터 생성
      • 클라이언트로부터 받은 타임스탬프 값을 검증
        • 타임 스탬프 값을 다양한 방법으로 검증
        • 상당한 시간 차가 감지되면, 해킹 방지를 위해 서버 틱으로 제한함
        • 네트웍 매니저 설정의 보상 비율을 사용해 클라이언트와 서버 시간을 서서히 균등화시킴
      • 압축된 가속, 회전 데이터를 디코딩하고 클라이언트와 서버의 타임 스탬프 정보를 기록
      • MoveAutonomous 함수를 호출해 서버 캐릭터를 이동시킴
      • 클라이언트와의 차이를 비교하고 에러를 수정함
        • 떨어지는 상황, 착지할 때의 상황에 따라 허용 가능 범위 내에서 클라이언트 데이터를 신뢰함
        • 상당한 시간 차가 감지되면, 수정 정보를 기록함(PendingAdjustment)

 

  • 서버가 호출하는 클라이언트 RPC
    • ClientAdjustPosition
      • 클라이언트에게 수정할 위치 정보를 알려주는 함수
      • 중복 없이 서버 틱의 마지막에서 수정이 필요한 때만 전송
        • 타임 스탬프 : 클라이언트의 타임 스탬프 값
        • 델타 타임 : 서버의 델타 타임
        • 무브먼트 모드 정보 : 압축된 캐릭터 컴포넌트의 무브먼트 모드 정보
        • 새로운 속도 : 수정할 새로운 속도 정보
        • 새로운 위치 : 수정할 새로운 위치 정보
        • 새로운 회전 : 수정할 새로운 회전 정보
        • 새로운 베이스와 베이스 본 이름 : 수정할 베이스에 대한 정보
      • NetDriver.cpp에서 어떤 패킷을 보낼 때 우리가 필요하다면 클라이언트에게 위치를 수정해달라는 요청을 보낸다.

 

  • 클라이언트의 수정 처리
    • ClientAdjustPosition_Implementation
      • 타임 스탬프 값을 통해 서버로부터 확인받은 움직임 정보를 기록 (LastAckedMove)
      • 서버에서 전달받은 위치로 루트 컴포넌트(캐릭터)의 위치를 변경
      • 서버에서 전달받은 속도로 무브먼트 컴포넌트의 속도를 수정
      • 베이스 정보와 위치를 수정
      • 서버에 의해 클라이언트 위치가 업데이트되었다고 기록 (bUpdatePosition)
        • 서버의 수정 정보를 바탕으로 MoveAutonomous 함수를 호출해 클라이언트에서 남은 움직임을 재생

 

  • 움직임 리플리케이션의 디버깅
    • DefaultEngine.ini파일에서 LogNetPlayerMovement=VeryVerbose로 설정
    • 서버에서의 오차 발생시 드로우 디버그
      • 전달받은 클라이언트 위치를 붉은색으로 표시
      • 서버에서 움직인 위치를 녹색으로 표시
    • 오차를 전달받은 클라이언트에서의 드로우 디버그
      • 클라이언트가 지정했던 위치를 붉은색으로 표시
      • 서버가 수정해준 위치를 녹색으로 표시
      • 수정은 발생했지만 서버와 클라이언트 위치가 거의 동일한 경우에는 노란색으로 표시

 

 

~p.NetShowCorrections 1

 

  • 전달 받은 클라이언트 위치는 붉은색, 서버에서 움직인 위치는 녹색, 위치가 거의 동일한 경우에는 노란색으로 표시 되는 모습
  • 다만, 클라이언트에서 공격을 입력한 채로 이동을 하면 이동이 제한되지 않는다.
  • 이동한 마지막 위치는 동기화 되지만, 클라이언트는 이동하지 않는 반면에 서버는 이동이 있다.

 

void AABCharacterPlayer::Attack()
{
	//ProcessComboCommand();

	if (bCanAttack)
	{
		// RPC 함수를 바로 실행하는 것이 아니라 / 서버와 클라이언트 부분을 구분해서 애니메이션과 공격을 바로 실행할 수 있도록 구현
		// 기존에 MulticastRPC에 있던 애니메이션과 공격 로직을 가져와준다.
		// 클라이언트
		if (!HasAuthority())
		{
			// bCanAttack을 변수에서 프로퍼티로 승격시켜 서버에서 해당 값을 바꿀 때 자동으로 모든 클라이언트에 바뀐 값이 전송되도록
			bCanAttack = false;
			GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_None);

			FTimerHandle Handle;
			GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
				{
					bCanAttack = true;
					GetCharacterMovement()->SetMovementMode(EMovementMode::MOVE_Walking);
				}
			), AttackTime, false, -1.0f);

			PlayAttackAnimation();
		}
		
		// 현재 월드의 시간을 RPC를 통해 같이 보내주기
		// 주의할 점. 서버와 클라이언트의 월드는 서로 다른 월드이다. 클라이언트는 서버보다 늦게 생성하기 때문에 서버보다 늦게 흘러갈 수밖에 없다. 그렇기 때문에 서버의 시간을 가져와서 넘겨줘야 한다.
		ServerRPCAttack(GetWorld()->GetGameState()->GetServerWorldTimeSeconds());
	}
}

// ..

// 신호를 받은 서버는 ServerRPCAttack_Implementation를 실행하게 된다.
void AABCharacterPlayer::ServerRPCAttack_Implementation(float AttackStartTime)
{
	AB_LOG(LogABNetwork, Log, TEXT("%s"), TEXT("Begin"));

	// Multicast에서 진행 했던걸 해당 부분에서 진행
	// bCanAttack을 변수에서 프로퍼티로 승격시켜 서버에서 해당 값을 바꿀 때 자동으로 모든 클라이언트에 바뀐 값이 전송되도록
	bCanAttack = false;

	// OnRep 이벤트 함수의 경우 서버에서는 실행되지 않고, 클라이언트에서만 실행됨.
	// 그렇기 때문에 서버에서는 자동으로 호출되지않기 때문에 명시적으로 호출해줘야 함
	
	// 클라이언트에서는 모션을 재생했기 때문에
	// 서버 RPC에서는 서버에서 해야 될 일을 진행해주면 된다.
	OnRep_CanAttack();

	// 서버에서는 공격 시간이 클라이언트와 차이가 있기 때문에 클라이언트로부터 패킷을 받은 시간과 서버의 시간의 차이가 존재
	// 차이를 반영해준다. = 클라이언트가 보낸 시간
	AttackTimeDifference = GetWorld()->GetTimeSeconds() - AttackStartTime;
	AB_LOG(LogABNetwork, Log, TEXT("LagTime : %f"), AttackTimeDifference);
	AttackTimeDifference = FMath::Clamp(AttackTimeDifference, 0.0f, AttackTime - 0.01f);

	FTimerHandle Handle;
	GetWorld()->GetTimerManager().SetTimer(Handle, FTimerDelegate::CreateLambda([&]
		{
			bCanAttack = true;
			OnRep_CanAttack();
		}
	), AttackTime - AttackTimeDifference, false, -1.0f);

	// 마지막으로 공격한 시간 저장
	LastAttackStartTime = AttackStartTime;
	PlayAttackAnimation();

	// 서버는 서버와 모든 클라이언트에게 명령을 보내도록
	// MulticastRPC의 경우 게임에 중요한 내용보다는 무관한 어떤 효과들을 재생하는데 사용하는 것이 좋다.
	for (APlayerController* PlayerController : TActorRange<APlayerController>(GetWorld()))
	{
		// 시뮬레이션 프록시인가?
		if (PlayerController && GetController() != PlayerController)
		{
			// PlayerController가 Local이면 이미 재생했기 때문에 스킵
			// 여기서 걸러지는 녀석은 서버도 아니고 공격 명령을 내린 플레이어도 아닌 폰을 재생하는 플레이어 컨트롤러
			if (!PlayerController->IsLocalController())
			{
				AABCharacterPlayer* OtherPlayer = Cast<AABCharacterPlayer>(PlayerController->GetPawn());
				if (OtherPlayer)
				{
					OtherPlayer->ClientRPCPlayAnimation(this);
				}
			}
		}
	}
}
  • ABCharacterPlayer.cpp

 

  • Attack에서 보면 공격을 하고 나면, 클라이언트에서는 바로 EMovementModeMove_None으로 잠궈주고 ServerRPCAttack으로 RPC를 보내고 ServerRPCAttack_Implementation에서 처리를 하고 OnRep_CanAttack에서 EMovementModeMove_None으로 잠궈주는데 네트워크 상태가 좋지 않아 약 1초정도 이후에 받게 되는 경우 서버에서 이동을 잠그기 전에 입력이 전달되기 때문에 다음과 같은 현상이 일어나게 된다.

 

  • 이를 해결하기 위해 SholuderMoveQuaterMove에서 공격을 할 수 없는 경우 움직임을 제한시켜주는 코드를 추가해준다.

 

// ...
// 공격을 할 수 없는 경우 움직임 제한
if (!bCanAttack)
{
	return;
}
// ...
  • ABCharacterPlayer.cpp

 

 

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

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

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

www.inflearn.com

 

728x90