언리얼 멀티플레이 구현 개념

Lee Raccoon·2024년 6월 22일
0

멀티플레이 통신 방식

게임에서 멀티플레이를 구현하는 데에는 크게 두가지 방법으로 나뉜다.

  • P2P
  • 클라이언트-서버 모델

간단하게 설명하자면 P2P는 Peer to Peer의 약어로 모든 플레이어가 이어져있는 것을 뜻한다.

클라이언트 서버 모델은 하나의 시스템을 서버로 정의하고 다른 모든 시스템은 클라이언트로 정의 한 후, 모든 클라이언트가 서버에 연결되어 통신하는 방식이다.

모든 클라이언트는 오직 서버와만 대화할 수 있다.

P2P가 각각의 플레이어와 모두 통신을 해야했던 것과는 다르게 서버에게만 데이터를 보내면 서버가 다른 클라이언트에게 이 정보를 모두 공유하게 된다.

또한 서버가 클라이언트에게 받은 내용이 적절치 않다고 판단되면 다른 클라이언트에게 이를 레플리케이션하지 않을 수 있기 때문에 보안적인 측면에서도 더 좋다고 할 수 있다.

클라이언트 서버 모델에는 크게 두 가지가 있다.

  • Dedicated 서버
    • Player를 갖지 않음
    • 렌더링을 하지 않음
    • 그저 게임을 시뮬레이트 하고 있을 뿐
  • Listen 서버
    • Player가 호스팅 하는 서버임
    • 호스트는 지연 시간이 발생하지 않으므로 어드밴티지를 가짐

리슨 서버는 직접 게임을 플레이 하는 플레이어가 서버를 맡게 되는 것이다.
데디케이티드 서버는 플레이어가 아닌 전용 서버 시스템을 구축해두는 것이다.

규모가 큰 게임같은 경우 고사양의 서버가 필요할 수 있기 때문에 보통 전용 서버를 사용하는 것이 일반적이다.

리슨 서버는 본인이 곧 서버인 플레이어가 존재하기 때문에 이를 통해 이점을 얻을 수 있기도 하다.
그래서 경쟁이 주요 게임 콘텐츠인 경우 리슨 서버 사용을 피하기도 한다.

언리얼의 통식 방식

언리얼은 클라이언트-서버 방식을 사용한다.
싱글 플레이 게임의 경우도 여전히 클라이언트 서버 방식을 사용한다는 점을 처음 알았는데, 어짜피 본인이 서버이자 클라이언트이기 때문에 그냥 그대로 쓰는 것 같다.

멀티플레이어 게임의 특성

멀티플레이어 게임은 보통 서버가 존재한다.

서버는 다른 클라이언트들과는 독자적으로 존재하며 각 클라이언트들은 그들만의 게임을 갖고 있다.
언리얼에서는 서버가 Authority를 가진다.
각자가 각자의 게임을 갖고 있기 때문에 어떤 게임이 올바른 게임인지 딱 정해야하는게 그게 서버 측의 게임이라는 것.

위와 같은 특성으로 멀티플레이어에는 몇가지 특징이 있다.

  • Game Mode는 서버에 하나만 존재한다.
    • 클라이언트에서 GameMode를 얻으려하면 nullptr를 얻게 된다.
  • PlayerController는 서버에도, 클라이언트에도 존재한다.
    • 서버는 모든 플레이어의 컨트롤러가 존재, 클라이언트는 자기만의 컨트롤러가 존재.
  • PlayerState는 서버와 클라이언트 둘 다 모든 플레이어의 State가 존재한다.
    • 클라이언트가 플레이하는 Pawn 이외에도 Pawn이 존재하므로 그에 대한 State가 있는 것은 당연할 것
  • HUD는 클라이언트 각자의 것만 가짐
    -서버가 Dedicated인 경우 HUD가 없고 Listen 서버인 경우 해당 플레이어를 위한 HUD가 존재

레플리케이션 (Replication)

이제 서버와 클라이언트들은 레플리케이트를 통해
어떤 Value가 레플리케이트 설정이 되어 있으면 이제 NetUpdate 때 마다 서버의 값을 가져와서 동기화 시키게 된다.

서버의 값이 바뀐 경우는 다음 업데이트에서 클라이언트에게 보내질 것이다.
근데 클라이언트 값이 바뀐 경우는?
클라이언트에서 서버로 값이 이동하나?

결론을 말하자면 Replication은 서버 -> 클라이언트로 일방통행이다.

어? 근데 그러면 클라이언트들의 입력을 서버가 받아서 서버가 클라이언트들에게 "야, 얘 움직였다" 라고 말해줘야할텐데
어떻게 서버에게 전달하지? 싶다.

그건 RPC (Remote Procedure Control)이라는 방식을 통해서 서버에 전달되게 된다.

언리얼 멀티플레이 테스트

LAN 환경에서 간단하게 멀티플레이 환경을 테스트 해보자


프로젝트에 간단하게 로비를 만들었다.
캐릭터에 1번을 눌러 본인을 리슨서버로써 레벨을 여는 블루프린트를 작성하였다.
2번을 누르면 내 IP 주소에서 열린 서버에 참여하는 것이다.

하지만 코드로도 하는 법을 알아야겠지?
(그래봤자 언리얼 형님들이 만들어놓으신 함수를 가져다 쓰는 것 밖에 안되지만..)

void AMultiPlayTestCharacter::OpenLobby()
{
	UWorld* World = GetWorld();
	if (World)
	{
		World->ServerTravel("/Game/Levels/Lobby?listen");
	}
}

void AMultiPlayTestCharacter::CallOpenLevel(const FString& Address)
{
	UGameplayStatics::OpenLevel(this, *Address);
}

void AMultiPlayTestCharacter::CallClientTravel(const FString& Address)
{
	APlayerController* PlayerController = GetGameInstance()->GetFirstLocalPlayerController();
	if (PlayerController)
	{
		PlayerController->ClientTravel(Address, ETravelType::TRAVEL_Absolute);
	}
}

위와같은 함수들을 만들어서 BlueprintCallable로 만들어줬다.

World->ServerTravel("/Game/Levels/Lobby?listen");에서 레벨과 함께 뒤에 ?를 붙이고 옵션을 붙일 수 있다.
여기서는 listen을 붙임으로써 리슨 서버로 레벨을 열 수 있었다.

UGameplayStaticsOpenLevel에서는 WorldContext와 레벨을 넣어서 레벨을 열 수 있는데, 여기서 레벨이 아니라 IP 주소, URL을 넣음으로써 해당하는 곳으로 접속이 가능하다.

PlayerControllerClientTravel는 서버의 주소와 TravelType을 지정하여 새 서버로 이동하는 함수이다.
서버 측에서 이를 호출하면 클라이언트에게 다른 맵으로 이동시키는 데 쓰이고
클라이언트 측에서 호출하면 본인이 새 서버로 이동하는 데 쓰인다고 한다.

참고로 UWorldServerTravel이라는 함수가 있는데, 서버 전용 함수로써 접속된 모든 클라이언트들의 ClientTravel을 호출시켜서 서버가 이동하는 맵으로 모두 따라가게 된다고 한다.

로비에서 본 게임으로 넘어갈 때 월드에서 이를 호출시키면 되는거겠지.

이제 실행해서 순서대로 1, 2, 3을 눌러보면?

업로드중..

잘 된다! ServerTravel을 해본다는게 깜빡해서 한번 더 했는데
업로드중..

아니... 어째서?
다들 잘 넘어와지긴 했는데 스폰 위치가 이상하게 설정됐는지 스폰되자마자 머나먼곳으로 떠나버렸다..

다음 맵으로 넘어갈 때 스폰 되는 걸 신경쓰지 않으면 클라이언트가 끔찍한 경험을 하게 될지도..

profile
영차 영차

0개의 댓글

관련 채용 정보