├── LICENSE ├── README.md ├── SteamClient.cs └── SteamP2PRelayTransport.cs /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 sagering 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityNetcodeSteamP2PRelayTransport 2 | This is a sample implementation of the com.unity.netcode.gameobjects Transport interface for Steam's Steamworks peer to peer relay service. 3 | 4 | # How to use this? 5 | 6 | The implementation is based on the [Facepunch](https://github.com/Facepunch/Facepunch.Steamworks) library, a C# wrapper for [Steamworks](https://partner.steamgames.com/doc/home). Install Facepunch 2.3.2. 7 | 8 | 1. Add the SteamClient.cs and SteamP2PRelayTransport.cs to your Unity project. 9 | 1. Add the the SteamClient and SteamP2PRelayTransport Component to your Netcode for GameObjects NetworkManager. 10 | 1. Set the NetworkManager's Transport to SteamP2PRelayTransport Component you just added. 11 | 1. Enter a Steam app id (or use the default 480 for testing) on the SteamClient Component. 12 | 1. Enter a Steam id of the hosting side. 13 | 14 | For testing, you will need two Steam clients running and logged in on two different Steam accounts and machines. 15 | -------------------------------------------------------------------------------- /SteamClient.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | using System.Collections.Generic; 3 | using UnityEngine; 4 | 5 | public class SteamClient : MonoBehaviour 6 | { 7 | /// 8 | /// The steam app id. The default value of 480 is a public app id for development. 9 | /// 10 | public uint steamAppId = 480; 11 | 12 | void Start() 13 | { 14 | Steamworks.SteamClient.Init(steamAppId); 15 | } 16 | 17 | void OnApplicationExit() 18 | { 19 | Steamworks.SteamClient.Shutdown(); 20 | } 21 | 22 | void Update() 23 | { 24 | Steamworks.SteamClient.RunCallbacks(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /SteamP2PRelayTransport.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Runtime.InteropServices; 5 | using System.Text; 6 | 7 | using Unity.Netcode; 8 | using UnityEngine; 9 | 10 | using Steamworks; 11 | using Steamworks.Data; 12 | 13 | public class SteamP2PRelayTransport : NetworkTransport 14 | { 15 | /// 16 | /// For clients, this is the steam id we want to connect to. 17 | /// 18 | public ulong serverId = 0; 19 | 20 | /// 21 | /// Enables additional debug logs. 22 | /// 23 | public bool debug = false; 24 | 25 | class ClientCallbacks : IConnectionManager 26 | { 27 | SteamP2PRelayTransport transport; 28 | 29 | // TODO: Increase buffer size. 30 | byte[] buffer = new byte[1024]; 31 | ArraySegment emptyPayload = new ArraySegment(); 32 | 33 | public ClientCallbacks(SteamP2PRelayTransport transport) 34 | { 35 | this.transport = transport; 36 | } 37 | 38 | /// 39 | /// We started connecting to this guy 40 | /// 41 | public void OnConnecting(ConnectionInfo info) 42 | { 43 | Debug.Log("ClientCallbacks: OnConnecting"); 44 | } 45 | 46 | /// 47 | /// Called when the connection is fully connected and can start being communicated with 48 | /// 49 | public void OnConnected(ConnectionInfo info) 50 | { 51 | Debug.Log("ClientCallbacks: OnConnected"); 52 | transport.InvokeOnTransportEvent(NetworkEvent.Connect, transport.ServerClientId, emptyPayload, Time.realtimeSinceStartup); 53 | } 54 | 55 | /// 56 | /// We got disconnected 57 | /// 58 | public void OnDisconnected(ConnectionInfo info) 59 | { 60 | Debug.Log("ClientCallbacks: OnDisconnected"); 61 | transport.InvokeOnTransportEvent(NetworkEvent.Disconnect, transport.ServerClientId, emptyPayload, Time.realtimeSinceStartup); 62 | } 63 | 64 | /// 65 | /// Received a message 66 | /// 67 | public unsafe void OnMessage(IntPtr data, int size, long messageNum, long recvTime, int channel) 68 | { 69 | Debug.Log("ClientCallbacks: OnMessage"); 70 | Debug.Assert(size <= buffer.Length, "Message size exceeds the max buffer length"); 71 | 72 | Marshal.Copy(data, buffer, 0, size); 73 | 74 | transport.InvokeOnTransportEvent(NetworkEvent.Data, transport.ServerClientId, new ArraySegment(buffer, 0, size), Time.realtimeSinceStartup); 75 | } 76 | } 77 | 78 | class ServerCallbacks : ISocketManager 79 | { 80 | SteamP2PRelayTransport transport; 81 | 82 | // TODO: Increase buffer size. 83 | byte[] buffer = new byte[1024]; 84 | ArraySegment emptyPayload = new ArraySegment(); 85 | 86 | public ServerCallbacks(SteamP2PRelayTransport transport) 87 | { 88 | Debug.Log("Instantiating ServerCallbacks"); 89 | this.transport = transport; 90 | } 91 | 92 | /// 93 | /// Must call Accept or Close on the connection within a second or so 94 | /// 95 | public void OnConnecting(Connection connection, ConnectionInfo info) 96 | { 97 | Debug.Log("ServerCallbacks: OnConnecting"); 98 | connection.Accept(); 99 | } 100 | 101 | /// 102 | /// Called when the connection is fully connected and can start being communicated with 103 | /// 104 | public void OnConnected(Connection connection, ConnectionInfo info) 105 | { 106 | Debug.Log("ServerCallbacks: OnConnected"); 107 | transport.InvokeOnTransportEvent(NetworkEvent.Connect, connection.Id, emptyPayload, Time.realtimeSinceStartup); 108 | } 109 | 110 | /// 111 | /// Called when the connection leaves. Must call Close on the connection 112 | /// 113 | public void OnDisconnected(Connection connection, ConnectionInfo info) 114 | { 115 | Debug.Log("ServerCallbacks: OnDisconnected"); 116 | connection.Close(); 117 | transport.InvokeOnTransportEvent(NetworkEvent.Connect, connection.Id, emptyPayload, Time.realtimeSinceStartup); 118 | } 119 | 120 | /// 121 | /// Received a message from a connection 122 | /// 123 | public void OnMessage(Connection connection, NetIdentity identity, IntPtr data, int size, long messageNum, long recvTime, int channel) 124 | { 125 | Debug.Log("ServerCallbacks: OnMessage"); 126 | 127 | // TODO: Assert that size <= buffer size 128 | Marshal.Copy(data, buffer, 0, size); 129 | 130 | transport.InvokeOnTransportEvent(NetworkEvent.Data, connection.Id, new ArraySegment(buffer, 0, size), Time.realtimeSinceStartup); 131 | } 132 | } 133 | 134 | private bool isClient = false; 135 | 136 | private SocketManager socketManager = null; 137 | private ConnectionManager clientConnection = null; 138 | 139 | /// 140 | /// A constant `clientId` that represents the server 141 | /// When this value is found in methods such as `Send`, it should be treated as a placeholder that means "the server" 142 | /// 143 | override public ulong ServerClientId { get => 0; } 144 | 145 | /// 146 | /// Send a payload to the specified clientId, data and channelName. 147 | /// 148 | /// The clientId to send to 149 | /// The data to send 150 | /// The delivery type (QoS) to send data with 151 | public override void Send(ulong clientId, ArraySegment payload, NetworkDelivery networkDelivery) 152 | { 153 | SendType sendType = CastToSendType(networkDelivery); 154 | 155 | if (isClient) 156 | { 157 | clientConnection?.Connection.SendMessage(payload.Array, payload.Offset, payload.Count, sendType); 158 | } 159 | else 160 | { 161 | if (socketManager == null) return; 162 | 163 | foreach (var connection in socketManager.Connected) 164 | { 165 | if (connection.Id != clientId) continue; 166 | 167 | connection.SendMessage(payload.Array, payload.Offset, payload.Count, sendType); 168 | } 169 | } 170 | } 171 | 172 | /// 173 | /// Polls for incoming events, with an extra output parameter to report the precise time the event was received. 174 | /// 175 | /// The clientId this event is for 176 | /// The incoming data payload 177 | /// The time the event was received, as reported by Time.realtimeSinceStartup. 178 | /// Returns the event type 179 | override public NetworkEvent PollEvent(out ulong clientId, out ArraySegment payload, out float receiveTime) 180 | { 181 | clientId = 0; 182 | receiveTime = Time.realtimeSinceStartup; 183 | payload = ArraySegment.Empty; 184 | 185 | return NetworkEvent.Nothing; 186 | } 187 | 188 | /// 189 | /// Connects client to the server 190 | /// 191 | override public bool StartClient() 192 | { 193 | try 194 | { 195 | clientConnection = 196 | Steamworks.SteamNetworkingSockets.ConnectRelay(serverId, 0); 197 | clientConnection.Interface = new ClientCallbacks(this); 198 | isClient = true; 199 | 200 | return true; 201 | } 202 | catch (Exception e) 203 | { 204 | Debug.LogError("StartClient failed with exception " + e); 205 | return false; 206 | } 207 | } 208 | 209 | /// 210 | /// Starts to listening for incoming clients 211 | /// 212 | override public bool StartServer() 213 | { 214 | try 215 | { 216 | socketManager = Steamworks.SteamNetworkingSockets.CreateRelaySocket(0); 217 | socketManager.Interface = new ServerCallbacks(this); 218 | return true; 219 | } 220 | catch (Exception e) 221 | { 222 | Debug.LogError("StartServer failed with exception " + e); 223 | return false; 224 | } 225 | } 226 | 227 | /// 228 | /// Disconnects a client from the server 229 | /// 230 | /// The clientId to disconnect 231 | override public void DisconnectRemoteClient(ulong clientId) 232 | { 233 | Debug.Log("DisconnectRemoteClient."); 234 | 235 | if (socketManager == null) return; 236 | 237 | foreach (var connection in socketManager.Connected) 238 | { 239 | if (connection.Id != clientId) continue; 240 | 241 | connection.Close(); 242 | } 243 | } 244 | 245 | /// 246 | /// Disconnects the local client from the server 247 | /// 248 | override public void DisconnectLocalClient() 249 | { 250 | Debug.Log("DisconnectLocalClient."); 251 | clientConnection?.Close(); 252 | } 253 | 254 | /// 255 | /// Gets the round trip time for a specific client. This method is optional 256 | /// 257 | /// The clientId to get the RTT from 258 | /// Returns the round trip time in milliseconds 259 | override public ulong GetCurrentRtt(ulong clientId) { return 0; } 260 | 261 | /// 262 | /// Shuts down the transport 263 | /// 264 | override public void Shutdown() 265 | { 266 | Debug.Log("Shutdown."); 267 | Steamworks.SteamClient.Shutdown(); 268 | } 269 | 270 | void LateUpdate() 271 | { 272 | socketManager?.Receive(); 273 | clientConnection?.Receive(); 274 | } 275 | 276 | static SendType CastToSendType(NetworkDelivery networkDelivery) 277 | { 278 | // TODO: This mapping might need to be revised. 279 | SendType sendType = SendType.Unreliable; 280 | 281 | switch (networkDelivery) 282 | { 283 | /// 284 | /// Unreliable message 285 | /// 286 | case NetworkDelivery.Unreliable: 287 | break; 288 | /// 289 | /// Unreliable with sequencing 290 | /// 291 | case NetworkDelivery.UnreliableSequenced: 292 | break; 293 | /// 294 | /// Reliable message 295 | /// 296 | case NetworkDelivery.Reliable: 297 | sendType |= SendType.Reliable; 298 | break; 299 | /// 300 | /// Reliable message where messages are guaranteed to be in the right order 301 | /// 302 | case NetworkDelivery.ReliableSequenced: 303 | sendType |= SendType.Reliable; 304 | break; 305 | /// 306 | /// A reliable message with guaranteed order with fragmentation support 307 | /// 308 | case NetworkDelivery.ReliableFragmentedSequenced: 309 | sendType |= SendType.Reliable; 310 | break; 311 | default: 312 | break; 313 | } 314 | 315 | return sendType; 316 | } 317 | 318 | void DebugOutput(NetDebugOutput type, string text) 319 | { 320 | Debug.Log(text); 321 | } 322 | 323 | public override void Initialize(NetworkManager networkManager = null) 324 | { 325 | Debug.Log("Initialize."); 326 | Steamworks.SteamNetworkingUtils.InitRelayNetworkAccess(); 327 | 328 | if (debug) 329 | { 330 | SteamNetworkingUtils.DebugLevel = NetDebugOutput.Debug; 331 | SteamNetworkingUtils.OnDebugOutput += DebugOutput; 332 | } 333 | } 334 | } 335 | --------------------------------------------------------------------------------