Jasontreks Blog

DM 보내기


Send

리슨 서버(Listen Server) 멀티 플레이 구현

게임의 멀티플레이에 대해서는 기본적인 개념 정도만 알고 있었지 구현해본적은 없어 많이 해맸던 부분이다.

멀티플레이의 원리

게임에서 멀티플레이가 이루어지는 과정은 다음과 같다.

  1. 서버: 오브젝트를 생성하고 씬에 추가함.
  2. 클라: 서버가 생성한 오브젝트를 같은 위치에 똑같은 모습으로 생성함.
  3. 서버: 오브젝트의 위치를 실시간으로 업데이트 하고, 모든 클라이언트들에게 업데이트한 좌표값을 전송함.
  4. 클라: 서버로부터 받은 좌표값로 오브젝트의 위치를 업데이트해 동기화함.

서버가 실행하는 게임과 클라가 실행하는 게임은 물리적으로 분리된 개별의 프로세스이다. 즉 멀티플레이의 핵심은 서버의 게임 세상을 기준으로 하여 다른 모든 클라들이 매 프레임마다 서버에서 돌아가는 세상과 똑같이 맟추는것이다. 이것을 멀티플레이 동기화할고 하며, 이 작업을 위한 기본 툴들이 Godot에서 기본으로 제공이 된다.

동기화 항목에는 오브젝트의 위치 뿐만 아니라 회전, 애니메이션, 상태(체력, 시간 등)과 같은 것들도 포함될 수 있다.

Godot에서는 멀티플레이 동기화를 지원하는 두가지 노드가 기본으로 존재한다.

  • MultiplayerSpawner: 원하는 Scene을 등록해놓고, 그 Scene이 한쪽에서 생성되면 다른 피어들에게도 생성되도록 한다.
  • MultiplayerSynchronizer: Scene의 자식 노드로 추가하여, 해당 Scene의 속성이 변경되면 다른 피어들에게도 동기화되도록 한다.

RPC(Remote Procedure Call)

RPC란 원격으로 연결된 상태인 피어들끼리 전달하는 원격 함수 호출 명령이다. 네트워크 기술을 사용하는 거의 모든 프로그래밍에서 필요로 하는 범용적인 개념이며 게임에서는 이벤트 동기화때문에 이 RPC가 중요하다.

대포로부터 발사된 탄환 하나가 서버에서 업데이트하다가 땅에 떨어져 피격 이벤트를 실행하는 상황을 떠올려보자. 서버 혼자서만 단순히 피격당한 플레이어의 체력 수치를 깎고, 폭발 이펙트를 생성하는것에서 그친다면 동기화 작업만 이루어지는 것이다.

하지만 동기화 이외에도 피격당한 플레이어의 클라이언트에서만 실행할 또다른 로직이 필요할 수 있다. 예를 들면 화면 흔들림 효과를 실행한다거나, 체력바가 줄어드는 UI 애니메이션, 캐릭터가 공격을 맞고나서 내는 효과음같은 것들이 있다.

이런 것들은 동기화가 아닌, 지목한 피어의 프로세스에서만 실행시킬 전용 함수이기 떄문에 RPC가 필요하다.

c Godot에서 지원하는 RPC 함수 선언 문법은 아래와 같다.

# Player에 선언된 카메라 흔들림 함수
# 데코레이터와 함께 선언
@rpc("any_peer", "call_local")
func shake_camera(from_x: float, range: float) -> void:
	if not is_multiplayer_authority():
		return
		
	var distance: float = abs(from_x - cmc.camera.global_position.x)
	var t: float = inverse_lerp(range, 0, distance)
	var amplitude = lerp(0, 100, clamp(t, 0, 1))
	cmc.shake(amplitude)

RPC 호출은 다음과 같다.

# Shell에서 호출하는 RPC: Player 객체에게 "shake_camera" 함수 호출 요청
## 카메라 흔들림
game.players[1 - launcher].rpc("shake_camera", global_position.x, 1000)

처음으로 Listen Server 멀티플레이를 구현한 모습. 각 플레이어의 캐릭터와 대포의 위치가 실시간으로 동기화되고 있음을 확인할 수 있다.

두 플레이어가 각 진영에서 전투를 벌이는 형태로 구현한 모습. 게임의 프로토타입이 완성됨 시점이다.