Today’s post is a brief example of how to implement a game using F# and SignalR. Creating a game for bots to play doesn’t have to be overly difficult. Since interesting emergent qualities can arise from simple rules, it makes for a fun way to show off SignalR, beyond the standard chat application. As this post will show, F# and SignalR work well together to create a nice communication framework without requiring a complex setup.
What is the game? It is a bot-played game of multi-player snakes. The rules are simple: eat food to grow, and run into opponents to slice off their tails. To give players a goal, they accrue points based on their length over time. It is a limited enough concept that a game engine and client can be built without overshadowing the SignalR aspects. A picture, or movie, is worth a thousand words. So below is a sample of the game player viewer. What is SignalR? If you’re not familiar, it is a library that provides real-time capabilities to web applications. Think websockets and other related technologies. In this particular case there is a web viewer and a console app leveraging the capability.
With definitions out of the way, time for the technical components. We’ll use .NET Core version 2.2. If you don’t have it installed, head out to the .NET Core Downloads page. Select SDK for your platform. Tangential, but you can also get here by going to dot.net, then navigating to Downloads
and .NET Core
.
The post will be broken up into 3 primary parts: SignalR server, SignalR client, SignalR webviewer. Discussing the specific game code will be out of scope, since it is the interactions that we really care about.
Server
For the server, Giraffe will be the base. It will host the SignalR services as well as the weS viewer. Creation is similiar to a typical dotnet app, but it’ll use the Giraffe template. If you need the templates you can get them by doing dotnet new -i "giraffe-template::*"
. The Giraffe template includes a reference to the Microsoft.AspNetCore.App
package, which includes SignalR, so no additional packages are necessary.
1 | dotnet new giraffe -lang F# -n GameServer -o GameServer |
The Giraffe templates thankfully generate all the necessary boilerplate code for a webapp on top of Kestrel. To simplify, we’ll focus on the components that need to be added to the server code. Add the necessary namespaces, this is not only for SignalR, but to support the background game engine service.
1 | open System.Threading; |
The SignalR components must be added to the pipeline. This is done in two places. Modify configureApp
to include .UseSignalR(...)
. Modify configureServices
to include services.AddSignalR()
. In addition, the game runs as a hosted service. To support this, modify configureServices
to also includ services.AddHostedService<GameService>()
.
1 | let configureApp (app : IApplicationBuilder) = |
Now that the components have been injected into the pipeline, they need to be created. For this we’ll need to create a SignalR hub as well as a GameService. Starting with the SignalR hub. We can send messages to the SignalR clients by supplying a function name and payload: this.Clients.All.SendAsync("Message", "foo")
. But, we can do better by defining the interface and making the calls type-safe, so let’s do that. Below is defined the client api interface. This ensures that calls from server to client match the required types. For simplicity, the server only has 3 messages it can send to clients.
LoginResponse
Reports success or failure, and their PlayerId if login was successful.Message
Sends general notifications to clients.GameState
Provides a serialized gamestate that clients act on.
1 | type IClientApi = |
Now, to define the SignalR hub. This effectively is the listener that all clients connect to. It leverages the IClientApi
that was just created. Here we need to write the handlers for messages accepted from clients. Players have four different actions they can signal to the server.
Login
For brevity, there is no authentication; provide a PlayerName and they get a PlayerId. It also adds a player to the game. The below code demonstrates how the server can send messages to all connected clients or just specific ones.Logout
Removes a player from the game.Turn
Players have one action they can perform, turn. They move in a specified direction until they turn, then they proceed in that direction.Send
Players can blast messages to all clients. Perhaps when the bots become self-aware they can taunt each other.
1 | type GameHub () = |
Now that the SignalR hub is done, it’s time to make the GameService that performs the server-side game logic as well as sending updated gamestate to players. For this a background service is used. At a set interval it processes current game state updateState
and sends it out to all clients. One note here: because I’ve choosen to use a client interface, the hub context is defined as IHubContext<GameHub, IClientApi>)
. If this wasn’t the case, it would be defined as IHubContext<GameHub>
and messages would be sent using this.HubContext.Clients.All.SendAsync("GameState", stateSerialized)
.
1 | type GameService (hubContext :IHubContext<GameHub, IClientApi>) = |
Beyond the specific game logic implementation, that’s all there is to the SignalR server. It now will send out gamestate updates as well as handle client messages.
Client
The next step is building the client. To do this, a dotnet console app will be created, and then the SignalR package is added.
1 | dotnet new console -lang f# -n ClientFs |
Once that is done, it needs the SignalR namespace.
1 | open Microsoft.AspNetCore.SignalR.Client |
The client needs to make a connection to the SignalR hub. Similar to the server, the client needs some event handlers for server generated messages.
LoginResponse
A successful login gives the client a playerId.Message
- Handle general message notifications.GameState
- When the server sends the current gamestate, the client evaluates and then sends an action message back.Closed
- When the connection closes, what does the client do? In this case attempts to reconnect.
Once the event handlers are setup, the client connects and performs a login. The handlers take care of the rest. As can be seen below, the client uses InvokeAsync
to send messages to the server (as seen in the login).
1 |
|
The handler logic is uninteresting, but it is useful to see the definitions that match with the handlers. In addition, I’ve included the client’s response back to the server in the gameState handler. Again, it uses InvokeAsync when contacting the server.
1 | let loginResponseHandler (connection :HubConnection) (success :bool) (id :string) = |
Game Viewer
The final piece to address is the game viewer. This comes in two parts: the layout and the code. For the layout, we leverage Giraffe’s view engine. It’s a simple view that contains an html canvas map, player list, messages display, and a state print (for debugging purposes). This is also where supporting js libraries: signalr, jquery, as well as the viewer game-server.js are included. For this project, the files reside in the WebRoot
directory.
1 | module Views = |
This may bring up a question, where did signalr.js
come from? Well, there is one more thing we need to add to the project. In a real project I’d package this differently, but a quick and dirty way will do for now.
1 | npm install @aspnet/signalr |
The code part of the game viewer is in javascript. A similar process is required as was performed with the F# client. A connection is created to the SignalR hub. Then event handlers are wired up. The viewer is read-only, to show messages and draw the map and player score list.
1 | /// SignalR connection |
At this point, we have all the necessary parts to support a SignalR F# server, F# client, and javascript client. That closes the loop on the communication framework. From here the game logic can be added to the server and client, and drawing can be added to the viewer. Those components are outside of the scope for this post. I hope you’ve found this to be a useful guide to leveraging a SignalR implementation with F#. Until next time…