├── 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 |
--------------------------------------------------------------------------------