📜 ⬆️ ⬇️

The simplest implementation of cross-platform multiplayer on the example of the evolution of a single .NET game. Part two, more technical

So, the promised continuation of my first article from the sandbox , in which there will be some technical details on the implementation of a simple multiplayer game with the ability to play with clients on different platforms.
I ended the previous part by the fact that in the latest version of my game “Magic Yatzy” I use WebSockets as a tool for client-server interaction. Now for some technical details.


1. General

In general, everything looks as shown in this diagram:

image
')
Above is a diagram of the interaction between the three clients on different platforms and the server. Consider each part in more detail.

2. Server

My server is based on MVC4, working as a “Cloud service” in Windows Azure. Why such a choice. It's simple:
1) I don't know anything except .NET.
2) I only have WebSocket for interactions related to the game, everything else, such as checking server status, getting / saving points and so on - through WebApi - therefore MVC.
3) I have a subscription to Azure services.

According to the scheme above - the server consists of three parts:
1) ServerGame - the implementation of all the logic of the game;
2) ServerClient - a kind of intermediary between the game and the network part;
3) WSCommunicator - the part responsible for network interaction with the client - receiving / sending commands.

The specific implementation of ServerGame and ServerClient depends on the particular game you are developing. In the general case, the ServerClient receives a command from the client, processes it and notifies the game about the client's action. At the same time, it monitors the change of the game state ( ServerGame ) and notifies (sends information via WSCommunicator ) his client about any changes.
For example, regarding my dice game: a user on a Windows 8 client secured several bones on his turn (made sure that their value did not change on the next roll). This information was passed to the server and ServerClient was notified of this by the ServerGame class, which made the necessary changes in the state of the game. All other ServerClients connected to this game (in this case, WP and Android) were notified about this change, and they, in turn, sent information to devices to alert users via the UI.
It should be said that there is nothing “server” in the ServerGame class itself . This is a common .NET class that has a common interface with ClientGame . Thus, we can substitute it instead of ClientGame in the client program and thus get a local game. This is exactly how a local game works in my “knuffle” - when both local and online games are possible from one UI page.
WSCommunicator is, as I said, the class responsible for networking. Specifically, this implements this interaction through WebSocket'ov. In .NET 4.5 there appeared its own implementation of web sockets. The core of this implementation is the WebSocket class, WSCommunicator is essentially a wrapper over it, which opens and closes the connection, attempts to reconnect, send / receive data in a specific format.
Now for some code. For the initial connection is used Http Handler. It is not necessary to add a physical page. It is enough to set the parameters in WebConfig:

… <system.webServer> <handlers> <remove name="ExtensionlessUrlHandler-Integrated-4.0" /> <add name="app" path="app.ashx" verb="*" type="Sanet.Kniffel.Server.ClientRequestHandler" /> <add name="ExtensionlessUrlHandler-Integrated-4.0" path="*." verb="*" type="System.Web.Handlers.TransferRequestHandler" resourceType="Unspecified" requireAccess="Script" preCondition="integratedMode,runtimeVersionv4.0" /> </handlers> </system.webServer> … 


Thus, when accessing the (virtual) “app.ashx” page on the server, the code from the “Sanet.Kniffel.Server.ClientRequestHandler” class will be called. Here is the code:

 public class ClientRequestHandler : IHttpHandler { public void ProcessRequest(HttpContext context) { if (context.IsWebSocketRequest) //  WebSocket context.AcceptWebSocketRequest(new Func<AspNetWebSocketContext, Task>(MyWebSocket)); else //  Http context.Response.Output.Write("  ..."); } public async Task MyWebSocket(AspNetWebSocketContext context) { string playerId = context.QueryString["playerId"]; if (playerId == null) playerId = string.Empty; try { WebSocket socket = context.WebSocket; // ,   WSCommunicator'          ServerClientLobby clientLobby = null; if (!string.IsNullOrEmpty(playerId)) { //         if ( !ServerClientLobby.playerToServerClientLobbyMapping.TryGetValue(playerId, out clientLobby)) { //  -   clientLobby = new ServerClientLobby(ServerLobby, playerId); ServerClientLobby.playerToServerClientLobbyMapping.TryAdd(playerId, clientLobby); } } else { //       return; } //     clientLobby.WebSocket = socket; await clientLobby.Start(); } catch (Exception ex) { //-   ... } } } 


I think, taking into account the comments, everything should be clear. The WSCommunicator.Start () method starts the "standby" command from the client. Here’s what it looks like ():

  public async Task Start() { if (Interlocked.CompareExchange(ref isRunning, 1, 0) == 0) { await Run(); } } protected virtual async Task Run() { while (WebSocket != null && WebSocket.State == WebSocketState.Open) { try { string result = await Receive(); if (result == null) { return; } } catch (OperationCanceledException) //     { } catch (Exception e) { //-  //  CloseConnections(); // ,       OnReceiveCrashed(e); } } } 


This is the general part, I will omit the further description of the server, since it will depend to a greater extent on the game you are doing. I can only say that commands are transmitted via WebSocket (including) in text format. The specific implementation of these commands again mainly depends on the game. When receiving a command from a client, it will be processed by the WSCommunicator.Receive () method, for sending to the client - WSCommunicator.Send (). Everything between - again depends on the logic of the game.

3. Customer

3.1 WinRT.

If the client were on a full-fledged .NET 4.5, then it would be possible to use the same WSCommunicator class as on the server with small additions - instead of the WebSocket class, you would need the ClientWebSocket class, plus add logic on the server connection request. But WinRT uses its own implementation of webboxes with the StreamWebSocket and MessageWebSocket classes. For sending text messages, use the second. Here is the code for establishing a connection with the server using it:

 public async Task<bool> ConnectAsync(string id, bool isreconnect = false) { try { //    ,            //(,  ) MessageWebSocket webSocket = ClientWebSocket; //     if (!IsConnected) { //   (ws://myserver/app.ashx") var uri = ServerUri(); webSocket = new MessageWebSocket(); webSocket.Control.MessageType = SocketMessageType.Utf8; //  webSocket.MessageReceived += Receive; webSocket.Closed += webSocket_Closed; await webSocket.ConnectAsync(uri); ClientWebSocket = webSocket; //        if (Connected != null) Connected(); //,    return true; } return false; } catch (Exception e) { //-   return false; } } 


Then everything is like on the server: WSCommunicator.Receive () receives messages from the server, WSCommunicator.Send () sends. GameClient works in accordance with the data received from the server and from the user.

3.2 Windows Phone, Xamarin and Silverlight (as well as .NET 2.0)

All of these platforms do not have out-of-the-box web socket support. Fortunately, there is an excellent open source WebSocket4Net library, which I mentioned in a previous article. Replacing the class of the web socket in WSCommunicatar e with the one implemented in this library, we will be able to connect to the server from the specified platforms. Here's how to change the connection code:

 public async Task<bool> ConnectAsync(string id, bool isreconnect = false) { try { //    ,            //(,  ) WebSocket webSocket = ClientWebSocket; //     if (!IsConnected) { //   (ws://myserver/app.ashx") var uri = ServerUri(); webSocket = new WebSocket(uri.ToString()); //  webSocket.Error += webSocket_Error; webSocket.MessageReceived += Receive; webSocket.Closed += webSocket_Closed; //  ,  ""   var tcs = new TaskCompletionSource<bool>(); webSocket.Opened += (s, e) => { //        ClientWebSocket = webSocket; if (Connected != null) Connected(); //,    else tcs.SetResult(true); }; webSocket.Open(); return await tcs.Task; } return false; } catch (Exception ex) { //-   return false; } } 


As you can see, there are differences, but there are not so many of them, the main thing is not asynchronous opening of the connection to the server, but this is easy to fix (although to support async await in older versions of .NET, you need to install the Microsoft.Bcl package from nuget).

Instead of conclusion

I read what I wrote and understand that there are more questions than answers. Unfortunately, it’s not physically possible to describe everything in one article, and it’s already not the shortest one ... but I will continue to train.

Source: https://habr.com/ru/post/187282/


All Articles