🔥 Connecting server and clients
Netcode for Entities 1.5.0v
https://docs.unity3d.com/Packages/com.unity.netcode@1.5/manual/creating-multiplayer-gameplay.html
주제 | 설명 |
---|---|
서버와 클라이언트 연결 Connecting server and clients | Netcode for Entities는 Unity Transport 패키지를 사용하여 연결을 관리합니다. 각 연결은 하나의 엔티티로 저장되며, 해당 엔티티는 NetworkStreamConnection 컴포넌트를 포함하고 있어, 연결에 사용되는 Transport 핸들을 참조합니다. 이 엔티티의 이름은 일반적으로 'NetworkConnection [nid]' 형식입니다. |
상태 및 입력 동기화 Synchronizing states and inputs | Netcode는 고스트(ghost) 상태와 입력/명령을 동기화합니다. 이 항목에서는 지원되는 타입 목록과, 어떤 필드와 컴포넌트를 eventual consistency 모델을 통해 복제할지 표시하는 방법(마크업)을 설명합니다. |
시간 동기화 Time synchronization | Netcode는 서버 권한 모델(server authoritative model) 을 사용합니다. 서버는 고정된 시간 간격으로 시뮬레이션을 실행하며, 이 간격은 마지막 업데이트 이후 경과한 시간에 따라 결정됩니다. 따라서 클라이언트는 항상 서버 시간과 동기화되어야 이 모델이 제대로 작동합니다. |
보간 및 예측보간 Interpolation and extrapolation | 게임에서 보간(interpolation) 및 예측보간(extrapolation) 을 사용하여, 불안정한 네트워크 환경에서도 부드러운 게임플레이 경험을 제공할 수 있습니다. |
예측 Prediction | 게임의 지연(latency)을 보완하기 위해 예측 로직을 사용할 수 있습니다. 예측은 클라이언트가 로컬에서 먼저 행동을 시뮬레이션하고, 이후 서버로부터 확정된 결과를 받는 방식입니다. |
물리 Physics | Netcode 패키지는 Unity Physics와의 일부 통합 기능을 제공합니다. 이를 통해 네트워크 환경에서도 물리 기반 오브젝트를 보다 쉽게 사용할 수 있습니다. 이 통합 기능은 predicted ghosts와 interpolated ghosts 양쪽 모두에 대한 물리 처리를 지원합니다. |
✅ 서버와 클라이언트 연결
https://docs.unity3d.com/Packages/com.unity.netcode@1.5/manual/network-connection.html
Netcode for Entities는 연결을 관리하기 위해 Unity Transport 패키지를 사용하며, 각 연결을 하나의 엔티티로 저장합니다.
이 엔티티는 NetworkStreamConnection
컴포넌트를 가지며, 연결에 사용된 Transport 핸들을 포함합니다.
연결이 종료되면(서버가 유저 연결을 끊거나 클라이언트가 요청한 경우) 이 엔티티는 삭제됩니다.
✏️ 명령 수신 대상 설정
[AutoCommandTarget feature]
를 사용하지 않거나 더 세밀한 제어가 필요한 경우, CommandTarget을 설정해야 합니다.
이 설정은 클라이언트에서 받은 명령을 저장할 엔티티를 지정합니다.
이 엔티티 참조를 최신 상태로 유지하는 것은 게임 개발자의 책임입니다.
✏️ 게임 상태 진입
게임은 NetworkStreamInGame
컴포넌트를 연결 엔티티에 수동으로 추가해야 합니다. 이 작업은 자동으로 이루어지지 않습니다.
이 컴포넌트가 추가되기 전에는 클라이언트가 명령을 전송하지 않으며, 서버도 스냅샷을 전송하지 않습니다.
✏️ 연결 해제 요청
연결을 해제하려면 해당 엔티티에 NetworkStreamRequestDisconnect
컴포넌트를 추가하세요.
Transport 드라이버를 통한 직접적인 연결 해제는 지원되지 않습니다.
✨ 수신 버퍼 (Incoming Buffers)
각 연결은 최대 3개의 수신 버퍼를 가질 수 있습니다:
- 명령:
IncomingRpcDataStreamBuffer
- RPC:
IncomingCommandDataStreamBuffer
- 스냅샷 (클라이언트 전용):
IncomingSnapshotDataStreamBuffer
서버에서 클라이언트로 스냅샷이 전송되면 버퍼에 저장되고 나중에 GhostReceiveSystem
에 의해 처리됩니다.
RPC 및 명령도 동일한 방식으로 NetworkStreamReceiveSystem
이 수집하고, 이후 각각의 시스템이 처리합니다.
⚠️ 서버는 IncomingSnapshotDataStreamBuffer
를 가지지 않습니다.
✨ 송신 버퍼 (Outgoing Buffers)
각 연결은 최대 2개의 송신 버퍼를 가질 수 있습니다:
- 명령 (클라이언트 전용):
OutgoingRpcDataStreamBuffer
- RPC:
OutgoingRpcDataStreamBuffer
생성된 명령은 먼저 송신 버퍼에 저장되고, 클라이언트가 매 틱마다 이 버퍼를 전송합니다.
RPC도 마찬가지로 해당 송신 시스템이 버퍼에 인코딩하고, RpcSystem
이 이를 묶어 MTU 단위로 전송합니다.
✅ 연결 흐름 (Connection flow)
Netcode는 게임 시작 시 자동으로 서버 또는 클라이언트 연결을 설정하지 않습니다.
기본적으로 ClientServerBootstrap
는 클라이언트와 서버 월드만 생성합니다.
연결 채널을 여는 방식은 개발자가 결정합니다.
선택 가능한 방법:
- 1️⃣
NetworkStreamDriver
를 사용하여 직접Connect
또는Listen
호출 - 2️⃣
AutoConnectPort
및DefaultConnectAddress
설정 NetworkStreamRequestConnect
및NetworkStreamRequestListen
요청 엔티티 생성
⚠️ 주의
연결 중에는 반드시 Application.runInBackground = true
로 설정해야 하며, 그렇지 않으면 포커스를 잃었을 때 연결이 끊길 수 있습니다.
1️⃣ 수동 연결 / 리스닝 (Manually listen or connect )
NetworkStreamDriver
싱글톤을 사용해 Connect
또는 Listen
메서드를 호출합니다.
코드 예시는 DOTS samples repository 참고
public void StartClientServer(string sceneName) { Debug.Log($"[StartClientServer] Called with '{sceneName}'."); if (ClientServerBootstrap.RequestedPlayType != ClientServerBootstrap.PlayType.ClientAndServer) { Debug.LogError($"Creating client/server worlds is not allowed if playmode is set to {ClientServerBootstrap.RequestedPlayType}"); return; } var server = ClientServerBootstrap.CreateServerWorld("ServerWorld"); var client = ClientServerBootstrap.CreateClientWorld("ClientWorld"); SceneManager.LoadScene("FrontendHUD"); //Destroy the local simulation world to avoid the game scene to be loaded into it //This prevent rendering (rendering from multiple world with presentation is not greatly supported) //and other issues. DestroyLocalSimulationWorld(); if (World.DefaultGameObjectInjectionWorld == null) World.DefaultGameObjectInjectionWorld = server; SceneManager.LoadSceneAsync(sceneName, LoadSceneMode.Additive); var port = ParsePortOrDefault(Port.text); NetworkEndpoint ep = NetworkEndpoint.AnyIpv4.WithPort(port); { using var drvQuery = server.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<NetworkStreamDriver>()); drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.RequireConnectionApproval = sceneName.Contains("ConnectionApproval", StringComparison.OrdinalIgnoreCase); drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.Listen(ep); } ep = NetworkEndpoint.LoopbackIpv4.WithPort(port); { using var drvQuery = client.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<NetworkStreamDriver>()); drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.Connect(client.EntityManager, ep); } }
2️⃣ AutoConnectPort 사용 (Using the AutoConnectPort)
ClientServerBootstrap
AutoConnectPort
필드에는 서버와 클라이언트가 처음 설정될 때 각각 자동으로 수신하고 연결하도록 지시하는 데 사용할 수 있는 두 가지 특수 속성이 포함되어 있습니다.
public class AutoConnectBootstrap : ClientServerBootstrap { public override bool Initialize(string defaultWorldName) { AutoConnectPort = 7979; CreateDefaultClientServerWorlds(); return true; } }
서버는 와일드카드 주소(DefaultListenAddress:AutoConnectPort
)에서 리슨을 시작합니다.
DefaultConnectAddress
는 기본적으로 NetworkEndpoint.AnyIpv4
로 설정되어 있습니다.
클라이언트는 서버 주소(DefaultConnectAddress:AutoConnectPort
)로 연결을 시작하며, 이 DefaultConnectAddress
는 기본적으로 NetworkEndpoint.Loopback
으로 설정되어 있습니다.
💡 참고:
에디터에서는 PlayMode 도구를 사용해 AutoConnectAddress
와 AutoConnectPort
값을 오버라이드할 수 있습니다.
그러나 AutoConnectPort
를 0으로 설정한 경우에는 PlayMode 도구의 오버라이드 기능이 작동하지 않으며, 이 경우 수동으로 연결을 트리거해야 합니다.
3️⃣ NetworkStreamRequest로 연결 흐름 제어
NetworkStreamDriver
에서 직접 메서드를 호출하는 대신, 다음과 같은 싱글톤을 생성하여 연결 흐름을 제어할 수 있습니다:
NetworkStreamRequestConnect
singleton : 원하는 서버 주소/포트로 연결을 요청할 때 사용합니다.NetworkStreamRequestListen
singleton : 서버가 원하는 주소/포트에서 리슨을 시작하도록 요청할 때 사용합니다.
// 클라이언트 월드에서, NetworkStreamRequestConnect가 있는 새 엔티티 생성 // 이 엔티티는 이후 NetworkStreamReceiveSystem에 의해 처리됩니다. var connectRequest = clientWorld.EntityManager.CreateEntity(typeof(NetworkStreamRequestConnect)); EntityManager.SetComponentData(connectRequest, new NetworkStreamRequestConnect { Endpoint = serverEndPoint }); // 서버 월드에서, NetworkStreamRequestListen이 있는 새 엔티티 생성 // 이 엔티티도 마찬가지로 NetworkStreamReceiveSystem에 의해 처리됩니다. var listenRequest = serverWorld.EntityManager.CreateEntity(typeof(NetworkStreamRequestListen)); EntityManager.SetComponentData(listenRequest, new NetworkStreamRequestListen { Endpoint = serverEndPoint });
이러한 요청은 런타임에 NetworkStreamReceiveSystem
에 의해 소비되어 실제 연결 동작을 수행합니다.
💡 참고 :
실행 중 오류가 발생하면 PlayMode Tools 창을 열고 다시 Play Mode로 진입해 보세요.
만약 월드가 이미 존재한다면, 부트스트랩 코드(위 설명 참조)에 의해 자동으로 월드가 생성되고 있을 가능성이 큽니다.
서버가 이미 리슨 중이거나 클라이언트가 이미 연결 중이라면, 자동 연결 기능이 이미 활성화되어 있는 것입니다.
이 경우 수동 연결 방식을 사용하려면 부트스트랩 코드에서 자동 연결을 비활성화해야 합니다.
4️⃣ Network simulator
Unity Transport는 SimulatorUtility
를 제공합니다.
이 도구는 Netcode for Entities 패키지에 포함되어 있으며, 에디터 내에서 다음 경로를 통해 접근하고 설정할 수 있습니다:
Multiplayer > PlayMode Tools
실제 네트워크 상황을 보다 현실적으로 반영하기 위해, 시뮬레이터를 활성화한 상태로 자주 게임플레이를 테스트해보는 것이 좋습니다.

✅ 클라이언트 연결 이벤트 수신
NetworkStreamDriver
싱글톤을 통해 제공되는 ConnectionEventsForTick
(형식: NativeArray<NetCodeConnectionEvent>.ReadOnly
) 컬렉션을 사용하여,
클라이언트 및 서버에서 클라이언트 연결 이벤트를 반복(iterate)하면서 반응할 수 있습니다.
이 이벤트는 오직 단일 SimulationSystemGroup 틱 동안만 존재하며, 각각 NetworkStreamConnectSystem
과 NetworkStreamListenSystem
에서 초기화됩니다.
시스템이 위 시스템들의 job 이후에 실행되면, 같은 틱에서 발생한 이벤트를 수신하게 됩니다.
반면, 그 이전에 컬렉션을 조회하면 이전 틱의 값들을 반복하게 됩니다.
// 예제 시스템 [UpdateAfter(typeof(NetworkReceiveSystemGroup))] [BurstCompile] public partial struct NetCodeConnectionEventListener : ISystem { [BurstCompile] public void OnUpdate(ref SystemState state) { var connectionEventsForClient = SystemAPI.GetSingleton<NetworkStreamDriver>().ConnectionEventsForTick; foreach (var evt in connectionEventsForClient) { UnityEngine.Debug.Log($"[{state.WorldUnmanaged.Name}] {evt.ToFixedString()}!"); } } }
💡 주의 :
서버는 고정된 델타 타임으로 실행되기 때문에, SimulationSystemGroup
은 각 렌더 프레임마다 0번 이상 실행될 수 있습니다.
따라서 ConnectionEventsForTick
은 반드시 SimulationSystemGroup
내에서 실행되는 시스템 안에서만 읽어야 합니다.
예를 들어 InitializationSystemGroup
, PresentationSystemGroup
, 또는 어떤 MonoBehaviour 메서드 내에서 접근하면 다음과 같은 문제가 발생할 수 있습니다:
b) 시뮬레이션이 이 프레임에서 실행되지 않을 경우, 이벤트를 중복 수신
a) 이전 틱 이벤트를 놓치고 현재 틱만 받음
1️⃣ NetCodeConnectionEvents on the client
상태 | 호출 규칙 |
---|---|
Unknown | 절대 발생하지 않음 |
Connecting | 자신의 클라이언트에서 한 번 발생. NetworkStreamReceiveSystem 이 Connect 호출을 인식한 시점 (호출 프레임 다음일 수 있음) |
Handshake | 내부 transport 드라이버가 Connected 상태에 진입하면 한 번 발생 |
Approval | 연결 승인 기능이 활성화된 경우에만 발생. Handshake 이후 |
Connected | 서버가 NetworkId 를 보내면 발생 |
Disconnected | 연결 종료 또는 타임아웃 또는 서버에 의해 연결 해제 시 발생. DisconnectReason 이 설정됨 |
💡 주의 :
클라이언트는 다른 클라이언트의 이벤트를 수신하지 않습니다. 클라이언트 월드에서 발생한 이벤트는 자신의 클라이언트에 대한 이벤트뿐입니다.
Handshake 및 Approval 단계는 실패할 수 있으며, ClientServerTickRate.HandshakeApprovalTimeoutMS
(기본값: 5000ms)의 타임아웃을 가집니다.
2️⃣ NetCodeConnectionEvents on the server
상태 | 호출 규칙 |
---|---|
Unknown | 절대 발생하지 않음 |
Connecting | 클라이언트가 연결을 시작하는 순간을 서버는 알 수 없으므로 발생하지 않음 |
Handshake | 모든 클라이언트에 대해 발생. transport 연결이 수락되면 진입 |
Approval | 승인 기능이 활성화된 경우에만 발생. Handshake 성공 후 |
Connected | 클라이언트가 NetworkId 를 부여받는 순간 발생 |
Disconnected | 클라이언트가 연결 해제되거나 타임아웃되면 발생. DisconnectReason 설정됨 |
💡 주의 :
서버는 바인딩 성공이나 리스닝 시작에 대한 이벤트는 발생시키지 않습니다. 기존 API를 통해 상태를 확인해야 합니다.
✅ Connection approval
서버에서 각 클라이언트 연결에 대해 승인을 요구하도록 설정할 수 있습니다.
연결 승인은 다음과 같은 목적으로 사용됩니다:
- 플레이어 편의성: 화이트리스트 / 블랙리스트, 비밀번호 보호된 서버 등
- 보안 검증: 매치메이킹 응답으로 받은 시크릿 토큰을 사용하여, 매치메이킹된 플레이어만 서버에 입장하도록 제한
연결 승인이 활성화되면 다음과 같은 변경이 발생 :
- 클라이언트는 Handshake 및 Approval 단계에서만
IApprovalRpcCommand
형식의 RPC를 서버에 전송할 수 있습니다. - 모든 클라이언트는 Handshake 상태에서 곧바로 Connected로 넘어가는 것이 아니라 Approval 상태로 이동합니다.
- 서버는 연결된 각 클라이언트의 엔티티에
ConnectionApproved
컴포넌트를 직접 추가해야만 승인됩니다. NetworkId
는 승인 성공 이후에만 할당됩니다. 승인에 실패하면 클라이언트는 연결이 끊어집니다.- 승인 과정에는 타임아웃이 존재합니다 (
ClientServerTickRate.HandshakeApprovalTimeoutMS
참조, 기본값: 5000ms)
다시 강조하면:
- Handshake 및 Approval 단계 동안, 클라이언트는 여러 개의 RPC 메시지를 보낼 수 있습니다.
- 단, 이들 모두
IApprovalRpcCommand
타입이어야 합니다. - 이 RPC의 payload에는 인증 토큰, 플레이어 정보 등 클라이언트의 유효성을 검증하기 위한 모든 정보를 담을 수 있습니다.
- 서버는
ConnectionApproved
컴포넌트를 클라이언트의 연결 엔티티에 추가함으로써 연결을 승인합니다. 이후 연결 흐름이 계속 진행됩니다.
NetworkStreamDriver
에는 RequireConnectionApproval
필드가 있으며, 클라이언트와 서버 모두에서 true로 설정해야 연결 승인 흐름이 올바르게 작동합니다.
if (isServer) { using var drvQuery = server.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<NetworkStreamDriver>()); drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.RequireConnectionApproval = true; drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.Listen(ep); } else { using var drvQuery = client.EntityManager.CreateEntityQuery(ComponentType.ReadWrite<NetworkStreamDriver>()); drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.RequireConnectionApproval = true; drvQuery.GetSingletonRW<NetworkStreamDriver>().ValueRW.Connect(client.EntityManager, ep); }
연결 승인 처리도 다음과 같이 설정할 수 있습니다 :
// The approval RPC, here it contains a hypothetical payload the server will validate public struct ApprovalFlow : IApprovalRpcCommand { public FixedString512Bytes Payload; } // This is used to indicate we've already sent an approval RPC and don't need to do so again public struct ApprovalStarted : IComponentData { } [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation | WorldSystemFilterFlags.ThinClientSimulation)] public partial struct ClientConnectionApprovalSystem : ISystem { public void OnCreate(ref SystemState state) { state.RequireForUpdate<RpcCollection>(); } public void OnUpdate(ref SystemState state) { var ecb = new EntityCommandBuffer(Allocator.Temp); // Check connections which have not yet fully connected and send connection approval message foreach (var (connection, entity) in SystemAPI.Query<RefRW<NetworkStreamConnection>>().WithNone<NetworkId>().WithNone<ApprovalStarted>().WithEntityAccess()) { var sendApprovalMsg = ecb.CreateEntity(); ecb.AddComponent(sendApprovalMsg, new ApprovalFlow { Payload = "ABC" }); ecb.AddComponent<SendRpcCommandRequest>(sendApprovalMsg); ecb.AddComponent<ApprovalStarted>(entity); } ecb.Playback(state.EntityManager); } } [WorldSystemFilter(WorldSystemFilterFlags.ServerSimulation)] public partial struct ServerConnectionApprovalSystem : ISystem { public void OnUpdate(ref SystemState state) { var ecb = new EntityCommandBuffer(Allocator.Temp); // Check connections which have not yet fully connected and send connection approval message foreach (var (receiveRpc, approvalMsg, entity) in SystemAPI.Query<RefRO<ReceiveRpcCommandRequest>,RefRW<ApprovalFlow>>().WithEntityAccess()) { var connectionEntity = receiveRpc.ValueRO.SourceConnection; if (approvalMsg.ValueRO.Payload.Equals("ABC")) { ecb.AddComponent<ConnectionApproved>(connectionEntity); // Destroy RPC message ecb.DestroyEntity(entity); } else { // Failed approval messages should be disconnected ecb.AddComponent<NetworkStreamRequestDisconnect>(connectionEntity); } } ecb.Playback(state.EntityManager); } }
🔥 연결 확인 Sample 코드
https://discussions.unity.com/t/lobby-relay-and-netcode-for-entities/1538817/8
WorldManager.cs
using System.Diagnostics.CodeAnalysis; using Unity.Entities; namespace Managers { public static class WorldManager { private static World _clientWorld, _serverWorld; [SuppressMessage("ReSharper", "ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator")] public static void DestroyLocalSimulationWorld() { foreach (var world in World.All) { if (world.Flags != WorldFlags.Game) continue; world.Dispose(); break; } } public static void RegisterServerWorld(World world) => _serverWorld = world; public static void RegisterClientWorld(World world) => _clientWorld = world; public static World GetServerWorld() => _serverWorld; public static World GetClientWorld() => _clientWorld; } }
RelayServerDataHelper.cs
using System; using System.Collections.Generic; using System.Linq; using Unity.Networking.Transport; using Unity.Networking.Transport.Relay; using Unity.Services.Relay.Models; namespace Extension { public static class RelayServerDataHelper { private static RelayServerData GetRelayData(List<RelayServerEndpoint> endpoints, byte[] allocationIdBytes, byte[] connectionDataBytes, byte[] hostConnectionDataBytes, byte[] keyBytes) { var endpoint = endpoints.FirstOrDefault(e => e.ConnectionType == "dtls") ?? throw new InvalidOperationException($"Endpoint for connectionType dtls not found"); var server = NetworkEndpoint.Parse(endpoint.Host, (ushort)endpoint.Port); var allocationId = RelayAllocationId.FromByteArray(allocationIdBytes); var connData = RelayConnectionData.FromByteArray(connectionDataBytes); var hostData = RelayConnectionData.FromByteArray(hostConnectionDataBytes); var key = RelayHMACKey.FromByteArray(keyBytes); return new RelayServerData(ref server, 0, ref allocationId, ref connData, ref hostData, ref key, true); } public static RelayServerData RelayData(JoinAllocation a) => GetRelayData(a.ServerEndpoints, a.AllocationIdBytes, a.ConnectionData, a.HostConnectionData, a.Key); public static RelayServerData RelayData(Allocation a) => GetRelayData(a.ServerEndpoints, a.AllocationIdBytes, a.ConnectionData, a.ConnectionData, a.Key); } }
RelayInitializer.cs
using System; using System.Collections; using System.Threading.Tasks; using Unity.Entities; using Unity.NetCode; using Unity.Networking.Transport; using Unity.Networking.Transport.Relay; using Unity.Services.Authentication; using Unity.Services.Core; using Unity.Services.Relay; using UnityEngine; using Extension; namespace Managers { public class RelayInitializer : MonoBehaviour { private static RelayServerData? _relayServerData, _relayClientData; private static Action OnConnectionComplete; public static string _joinCode; // Singleton (without duplicate destroy logic) for access to the class instance from within the static methods private static RelayInitializer Instance { get; set; } protected void Awake() => Instance = this; // HOSTING public static void StartHost() => Instance.StartCoroutine(InitializeHost()); private static IEnumerator InitializeHost() { var initializeTask = UnityServices.InitializeAsync(); while (!initializeTask.IsCompleted) yield return null; if (ProcessTaskFail(initializeTask, nameof(initializeTask))) yield break; var signInTask = Task.CompletedTask; if (!AuthenticationService.Instance.IsSignedIn) { signInTask = AuthenticationService.Instance.SignInAnonymouslyAsync(); while (!signInTask.IsCompleted) yield return null; } if (ProcessTaskFail(signInTask, nameof(signInTask))) yield break; var allocationTask = RelayService.Instance.CreateAllocationAsync(5); while (!allocationTask.IsCompleted) yield return null; if (ProcessTaskFail(allocationTask, nameof(allocationTask))) yield break; var joinCodeTask = RelayService.Instance.GetJoinCodeAsync(allocationTask.Result.AllocationId); while (!joinCodeTask.IsCompleted) yield return null; if (ProcessTaskFail(joinCodeTask, nameof(joinCodeTask))) yield break; _joinCode = joinCodeTask.Result; try { Debug.Log("Hosting relay data"); _relayServerData = RelayServerDataHelper.RelayData(allocationTask.Result); } catch (Exception e) { Debug.LogException(e); _relayServerData = null; yield break; } Debug.Log("Success, players may now connect"); while (_relayServerData == null || (!_relayServerData?.Endpoint.IsValid ?? false)) yield return null; yield return JoinUsingCode(_joinCode); yield return WaitRelayConnection(); SetupRelayHostedServerAndConnect(); } private static void SetupRelayHostedServerAndConnect() { if (ClientServerBootstrap.RequestedPlayType != ClientServerBootstrap.PlayType.ClientAndServer) { UnityEngine.Debug.LogError($"Creating client/server worlds is not allowed if playmode is set to {ClientServerBootstrap.RequestedPlayType}"); return; } var relayServerData = _relayServerData.GetValueOrDefault(); var relayClientData = _relayClientData.GetValueOrDefault(); var oldConstructor = NetworkStreamReceiveSystem.DriverConstructor; NetworkStreamReceiveSystem.DriverConstructor = new RelayDriverConstructor(relayServerData, relayClientData); var server = ClientServerBootstrap.CreateServerWorld("ServerWorld"); WorldManager.RegisterServerWorld(server); var client = ClientServerBootstrap.CreateClientWorld("ClientWorld"); WorldManager.RegisterClientWorld(client); NetworkStreamReceiveSystem.DriverConstructor = oldConstructor; WorldManager.DestroyLocalSimulationWorld(); World.DefaultGameObjectInjectionWorld ??= server; // Load scene here if you want to. Debug.Log(_joinCode); var networkStreamEntity = server.EntityManager.CreateEntity(ComponentType.ReadWrite<NetworkStreamRequestListen>()); server.EntityManager.SetName(networkStreamEntity, "NetworkStreamRequestListen"); server.EntityManager.SetComponentData(networkStreamEntity, new NetworkStreamRequestListen { Endpoint = NetworkEndpoint.AnyIpv4 }); networkStreamEntity = client.EntityManager.CreateEntity(ComponentType.ReadWrite<NetworkStreamRequestConnect>()); client.EntityManager.SetName(networkStreamEntity, "NetworkStreamRequestConnect"); client.EntityManager.SetComponentData(networkStreamEntity, new NetworkStreamRequestConnect { Endpoint = relayClientData.Endpoint }); ProcessConnectionComplete(); } // CONNECTING public static void ConnectByCode(string joinCode) => Instance.StartCoroutine(ProcessCodeConnection(joinCode)); private static IEnumerator ProcessCodeConnection(string joinCode) { Instance.StartCoroutine(JoinExternalServer(joinCode)); yield return WaitRelayConnection(); ConnectToRelayServer(); } private static IEnumerator WaitRelayConnection() { while (_relayClientData == null || (!_relayClientData?.Endpoint.IsValid ?? false)) yield return null; } private static IEnumerator JoinExternalServer(string joinCode) { Debug.Log("Waiting for relay response"); var setupTask = UnityServices.InitializeAsync(); while (!setupTask.IsCompleted) yield return null; var signInTask = Task.CompletedTask; if (!AuthenticationService.Instance.IsSignedIn) { signInTask = AuthenticationService.Instance.SignInAnonymouslyAsync(); while (!signInTask.IsCompleted) yield return null; } if (ProcessTaskFail(signInTask, nameof(signInTask))) yield break; yield return JoinUsingCode(joinCode); } private static IEnumerator JoinUsingCode(string joinCode) { // Send the join request to the Relay service var joinTask = RelayService.Instance.JoinAllocationAsync(joinCode); while (!joinTask.IsCompleted) yield return null; if (ProcessTaskFail(joinTask, nameof(joinTask))) yield break; // Format the server data, based on desired connectionType try { _relayClientData = RelayServerDataHelper.RelayData(joinTask.Result); } catch (Exception e) { Debug.LogException(e); _relayClientData = null; } _joinCode = joinCode; } private static void ConnectToRelayServer() { var relayClientData = _relayClientData.GetValueOrDefault(); var oldConstructor = NetworkStreamReceiveSystem.DriverConstructor; NetworkStreamReceiveSystem.DriverConstructor = new RelayDriverConstructor(new RelayServerData(), relayClientData); var client = ClientServerBootstrap.CreateClientWorld("ClientWorld"); WorldManager.RegisterClientWorld(client); NetworkStreamReceiveSystem.DriverConstructor = oldConstructor; WorldManager.DestroyLocalSimulationWorld(); World.DefaultGameObjectInjectionWorld ??= client; var networkStreamEntity = client.EntityManager.CreateEntity(ComponentType.ReadWrite<NetworkStreamRequestConnect>()); client.EntityManager.SetName(networkStreamEntity, "NetworkStreamRequestConnect"); client.EntityManager.SetComponentData(networkStreamEntity, new NetworkStreamRequestConnect { Endpoint = relayClientData.Endpoint }); ProcessConnectionComplete(); } // COMMON private static bool ProcessTaskFail(Task task, string taskName) { if (!task.IsFaulted) return false; Debug.LogError($"Task {taskName} failed."); Debug.LogException(task.Exception); return true; } public static void SubscribeToConnectionComplete(Action handler) => OnConnectionComplete += handler; public static void ProcessConnectionComplete() { if (OnConnectionComplete == null) return; OnConnectionComplete?.Invoke(); foreach (var handler in OnConnectionComplete?.GetInvocationList()!) OnConnectionComplete -= (Action)handler; } } public class RelayDriverConstructor : INetworkStreamDriverConstructor { private RelayServerData _relayServerData, _relayClientData; public RelayDriverConstructor(RelayServerData serverData, RelayServerData clientData) { _relayServerData = serverData; _relayClientData = clientData; } public void CreateClientDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug) { var settings = DefaultDriverBuilder.GetNetworkSettings(); settings.WithRelayParameters(ref _relayClientData); DefaultDriverBuilder.RegisterClientDriver(world, ref driverStore, netDebug, settings); } public void CreateServerDriver(World world, ref NetworkDriverStore driverStore, NetDebug netDebug) => DefaultDriverBuilder.RegisterServerDriver(world, ref driverStore, netDebug, ref _relayServerData); } }