게임개발일지/리그오브워치

[리그오브워치개발일지] #12 유저 인터페이스 : 캐릭터 선택 (2) with 데디케이트 서버

김진우 개발일지 2023. 11. 17. 20:53

Git

레포지토리 주소 : https://github.com/kjinwoo12/UE5Game_LeagueOfWatch
기준 태그 : 12_teamSelection_withDedicatedServer

서론

데디케이트 서버는 개발할 때 뿐만 아니라 실제 서비스 중인 게임에도 사용하기 적합하다. 데디케이트 서버를 사용한 가장 유명한 게임은 포트나이트로, 100명의 유저가 한 맵에서 성능에 큰 문제없이 게임을 플레이 할 수 있는 서버의 성능을 실제로 서비스하며 증명한 게임이다. 이번 글에서는 언리얼 엔진 데디케이트 서버를 빌드해보고 필요한 기능을 만들어볼 것이다.

요구사항 확인

이전글에서부터 서버가 없으면 만들 수 없는 기능들이 나오는 것을 확인했다. 여러 플레이어가 캐릭터를 선택하고 확정하는 모습을 위젯에 보여줘야 하며, 캐릭터 선택 화면이 나오기 전에 플레이어를 모으는 매치메이킹 서버도 필요하다. 자세히 정리해보면 아래와 같다.

  1. 매치메이킹 기능
    1. 클라이언트에서 서버로 매치메이킹 요청
    2. 서버는 클라이언트를 매치메이킹 대기열에 추가
    3. 대기열에 추가된 클라이언트들을 5vs5 매치로 만들고, 매칭이 완료되면 클라이언트에게 매칭 정보 송신
    4. 클라이언트가 서버에서 매칭 정보를 받으면 캐릭터 선택 UI로 이동한다.
  2. 매칭 완료 후 캐릭터 선택 서버 접속
    1. 클라이언트에서 서버로 접속
    2. 서버에서 클라이언트 유저 식별
  3. 캐릭터 선택 기능
    1. 서버에서 클라이언트로 유저 식별정보 전달
    2. 클라이언트에서 유저 식별정보를 토대로 UI에 출력
    3. 클라이언트에서 유저가 캐릭터를 선택하면 서버로 유저 정보와 선택한 캐릭터 송신
    4. 서버에서 유저 정보와 해당 유저가 선택한 캐릭터 정보를 클라이언트로부터 수신 후 모든 클라이언트에 해당 유저가 캐릭터를 선택한 정보 송신
    5. 클라이언트에서 다른 유저가 선택한 캐릭터를 서버로부터 수신 후 위젯에 정보 표시

사전 준비

환경 설정

서버를 빌드할 필요 없이 에디터 상에서 게임을 실행하는 것에 만족한다면 에디터 개인 설정에 있는 "개별서버 실행"에 체크해두면 된다. 게임이 시작하면 자동으로 서버를 실행하고 클라이언트를 서버에 접속시킨다.

서버를 빌드할 필요가 있다면 미리 준비할 것이 많다. 언리얼 엔진 공식 문서를 살펴보면 언리얼 엔진 소스 빌드를 사용해야 한다는 내용이 있다. 언리얼 엔진 소스 빌드 설치 문서를 확인해서 환경 설정을 할 필요가 있다.

나는 UE5.1.1 버전을 사용중이기 때문에 Github에서 5.1.1-release 태그에 해당하는 커밋의 엔진을 빌드해 사용하겠다.

해당 커밋의 Setup.bat 실행에 문제가 있어서 231103 기준 최신 버전인 5.3.1-release 를 시용하겠다.

RPC와 Replication 이해

RPC는 호출과 실행이 각각 다른 머신에서 행해지는 함수를 말한다. 예를 들어, 클라이언트에서 총을 쏘는 함수를 호출하지만 총알을 만들어내고 총알의 궤적, 피격 판정 등은 서버에서 처리해야 하는데 이 때 사용하는 것이 RPC이다. 액터 소유권과 호출된 머신의 종류(서버, 클라이언트)에 따라서 실행이 안되는 경우도 있으니 주의해서 사용해야 한다.

RPC가 함수라면 Replication은 변수다. Replicated 지정자로 지정된 변수는 서버에서 값이 변경되면 모든 클라이언트로 업데이트된다. 단, 클라이언트에서 바뀐 값은 서버로 업데이트 되지 않는다.

기능 구현

요구사항1. 매치메이킹

매치메이킹은 상용 서비스도 많이 있고, 게임 개발에 있어서 우선순위가 낮기 때문에 잠시 보류한다.

요구사항2. 매칭 완료 후 캐릭터 선택 서버 접속

클라이언트에서 서버로 접속

클라이언트에서 서버로 접속하는 부분은 매치메이킹을 어떻게 만드느냐에 따라 코드가 매우 많이 달라질 수 있기 때문에 테스트 용도로 접속 기능을 만들 것이다. 개별 서버 실행을 체크하고 에디터에서 CharacterSelectionLevel을 실행하면 자동으로 클라이언트들을 서버에 접속시키기 때문에 하지 않아도 되는 작업이다.

우선 EntryLevelWBP_EntryLevel_Main 위젯을 만들고 접속 버튼 하나를 배치한다.

데디케이트 서버에 접속하려면 Open Level 노드를 사용해 레벨 이름 대신에 IP를 적어 넣으면 된다. 관련 문서에 따르면 Online Session Subsystem은 Dedicated Server용이 아니라고 한다.

서버 기본 맵을 CharacterSelectionLevel로 만든 다음에 서버를 패키징하고 실행하면 클라이언트의 EntryLevel에서 버튼을 눌렀을 때 서버가 스트리밍하고 있는 CharacterSelectionLevel에 접속할 수 있다.

플레이어 컨트롤러와 폰이 마우스 입력을 가져가서 위젯을 누를 수 없기 때문에 추가 처리가 필요하다.

서버에서 클라이언트 유저 식별

게임 모드에서는 플레이어가 서버에 로그인을 성공해 플레이어 컨트롤러가 할당되었을 때 실행되는 이벤트 OnPostLogin가 있다. 이곳에서 접속한 플레이어의 배열을 만들어 필요한 정보를 가져올 수 있도록 하자.

10명의 플레이어가 접속을 완료하면 모든 플레이어에게 AllPlayersConnected 이벤트를 호출한다.

유저 식별? 필요한 정보?

플레이어의 닉네임, 소속된 팀, 몇 번째 팀원인지에 대한 정보를 수집해야 플레이어에게 적절한 UI를 띄울 수 있을 것이다. 해당 정보가 없다면 WBP_CharacterSelection_Main에서 WBP_SelectedCharacterViewer 위젯에 띄울 정보가 없기 때문에 정상적인 화면을 띄울 수 없을 것이다. 플레이어의 정보를 다루는 PlayerState를 만들어야 한다.

블루프린트 BP_PlayerState_CharacterSelection를 만들고 아래와 같이 변수를 추가한다.

유저 식별 정보 업데이트

서버의 모든 플레이어에게 세 변수의 값을 업데이트 해야한다. 클라이언트에서 정보를 가지고 있어서 변수를 아무리 바꿔준다 한들 서버에는 적용이 되지 않기 때문에 3자 인증 방식을 통해 업데이트를 해주려고 한다.

3자 인증 방식 요약

주로 본인 인증에 사용되는 방식인 3자 인증은 유저와 실제 데이터 대신에 무작위로 설정된 고유번호를 주고 받으면서 데이터의 위변조를 막기 위해 사용된다. 실제 데이터가 필요한 서버는 인증 서버와 직접 통신해서 신뢰할 수 있는 데이터를 제공받는다. 지금 만들고 있는 리그오브워치에 상황을 대입해보자면 유저 이름과 소속 팀 데이터를 가지고 있는 매치메이킹 서버가 위 그림에 인증 서버에 해당한다. 지금은 요구사항1. 매치메이킹에서 말했듯이 매치메이킹 서버가 없기 테스트용 코드를 추가하는 것으로 마무리하겠다. 매치메이킹 서버를 다루게 되면 이 부분도 같이 다루도록 하겠다. 테스트 코드는 BP_GameMode_CharacterSelection에 추가했다.

요구사항3. 캐릭터 선택 구현

건드린 코드가 매우 많기 때문에 간략한 구현 아이디어 설명과 완성된 모습을 영상으로 보여주는 선에서 마무리 하겠다. 자세한 구현 방법은 Git에서 프로젝트를 받아 확인할 수 있다.

구현 아이디어

PlayerController는 컨트롤러 소유의 클라이언트 머신과 서버에만 존재하지만 PlayerState는 모든 머신에 서버와 동기화된 상태로 존재한다. 즉 각각의 캐릭터 선택 위젯에 PlayerState를 연결하고 Replication와 RPC를 활용한다면 모든 클라이언트에서 동일하게 유저의 이름과 선택된 캐릭터를 표시할 수 있다.

완성된 모습

어려웠던 점

RPC가 호출되는 시점은 반드시 위젯이 생성되고 화면에 표시된 이후여야만 한다. 그렇지 않다면 위젯은 RPC가 호출되기 전에 데이터가 서버와 동기화되지 않은 상태에서 위젯을 초기화하게 되고 결과적으로 화면에 정상적인 플레이어의 이름과 캐릭터를 띄울 수 없게 된다. 때문에 왜 정상적으로 작동하지 않은 것인지 조금 헤매었다. 처음엔 서버에 접속한 시점에 PlayerState를 받아와서 위젯을 초기화 할 수 없었지만 원인을 찾고 난 다음에는 위젯이 초기화된 시점에 PlayerController에게 PlayerState 값을 요청하도록 만들어 문제를 해결했다.