[개발일지] Unity Multiplay #2 - Scene 관리 및 스폰

qweasfjbv·2025년 3월 4일

개발일지

목록 보기
2/8

개요


네트워크 환경에서의 안정적인 씬 관리 방식과 플레이어를 스폰해보도록 하겠습니다.

씬을 안정적으로 관리하기 위해서는 하나의 씬에 싱글톤 오브젝트들을 다 집어넣어서 다시 로드되거나 언로드 되지않도록 해야합니다.

특히 FacepunchTransport 컴포넌트의 경우 Singleton인 NetworkManager에 붙어있긴 하지만 동시에 두 오브젝트가 존재하는 경우 Destroy 되기 전에 Awake 가 호출되고 SteamClient.Init 이 중복 호출되어서 에러가 발생할 수 있습니다.

FacepunchTransport 스크립트를 수정하여 해당 에러를 해결해도 NetworkManagerSocketManager 에서 에러가 발생하게 되니, 이를 안정적으로 해결하기 위해서 새로운 씬을 Additive하게 로드하는 방식으로 관리해보겠습니다.

구현


우선 각 씬에 SceneBase를 상속받는 컴포넌트를 하나씩 배치해줍니다.

	public class LobbyScene : SceneBase
	{
		protected override void Init()
		{
			base.Init();
			SceneEnum = SceneEnum.Lobby;
		}

		public override void Clear()
		{

		}
	}

SceneManagerEx 에서는 해당 컴포넌트를 통해 현재 씬을 확인하고 관리할 수 있도록 합니다.

SceneManagerEx

		public SceneBase CurrentScene { get { return GameObject.FindFirstObjectByType<SceneBase>(); } }

		public void Init()
		{
			LoadScene(SceneEnum.Main);
		}
		public void ChangeScene(SceneEnum sceneEnum)
		{
			UnloadCurrentScene();
			LoadScene(sceneEnum);
		}

		private async void LoadScene(SceneEnum sceneEnum)
		{
			CurrentScene?.Clear();
			await SceneManager.LoadSceneAsync(sceneEnum.ToString() + "Scene", LoadSceneMode.Additive);
		}

		public async void UnloadCurrentScene()
		{
			if (CurrentScene.SceneEnum == SceneEnum.None) return;
			await SceneManager.UnloadSceneAsync(CurrentScene.SceneEnum.ToString() + "Scene");
		}

제일 기본적인 서버관련 싱글톤 오브젝트들이 모여있는 ServerScene 에서 게임을 시작해서, 시작하자마자 MainScene 을 로드하도록 했습니다.

씬을 변경할 때에는 현재 씬을 내리고 새로운 씬을 로드하도록 했습니다.

맨 위에서부터 게임 시작하기 전, 시작 후, 로비 접속 후 hierarchy 창입니다.
ServerScene 에만 싱글톤 오브젝트들이 있기 때문에, 다시 MainScene으로 돌아가도 충돌이 일어나지 않습니다.


PlayerSpawner

Unity의 Netcode for GameObjects 에서 제공하는 NetworkManager에는 PlayerPrefab을 지정하는 필드가 있습니다.
해당 필드에 PlayerPrefab을 지정하면 해당 프리팹이 서버 생성 혹은 서버와 연결될 때 자동으로 Spawn 및 Owner로 설정해줍니다.

하지만 스폰할 시간을 직접 설정하거나 생성/삭제를 반복해야 한다면 직접 Spawn을 관리하는게 더 좋은 방법일 수 있습니다.

		[SerializeField] private GameObject playerPrefab;
		private Dictionary<ulong, GameObject> players = new Dictionary<ulong, GameObject>();

		[ServerRpc(RequireOwnership = false)]
		public void SpawnPlayerServerRPC(ulong clientId)
		{
			GameObject go = Instantiate(playerPrefab, new Vector3(0, 1f, 0), Quaternion.identity);
			NetworkObject no = go.GetComponent<NetworkObject>();

			if (no != null)
			{
				no.SpawnAsPlayerObject(clientId);
			}

			players.Add(clientId, go);
		}

		[ServerRpc(RequireOwnership = false)]
		public void DespawnPlayerServerRPC(ulong clientId)
		{
			ulong key = 100;
			foreach (var entry in players)
			{
				if(entry.Key == clientId)
				{
					key = entry.Key;
					break;
				}
			}

			GameObject go = players[key];
			players.Remove(key);
			go.GetComponent<NetworkObject>().Despawn();
			Destroy(go);
		}

Player Prefab 아래에 있는 Network Prefabs Lists 에 등록해준 다음 위 함수들을 Client가 시작될때, host가 시작될 때 호출해주면 됩니다.

마무리


로비를 만들고 로비 상에서 캐릭터를 스폰해서 서로에게 보이도록 구현하였습니다.
이제 플레이어의 움직임을 동기화해 보겠습니다.

0개의 댓글