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

2

3

open System.Threading;

open System.Threading.Tasks;

open Microsoft.AspNetCore.SignalR



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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

let configureApp (app : IApplicationBuilder) =

let env = app.ApplicationServices.GetService<IHostingEnvironment>()

( match env.IsDevelopment() with

| true -> app.UseDeveloperExceptionPage()

| false -> app

.UseGiraffeErrorHandler errorHandler)

.UseCors(configureCors)

.UseStaticFiles()

.UseSignalR( fun routes -> routes.MapHub<GameHub>(PathString "/gameHub" ))

.UseGiraffe(webApp)



let configureServices (services : IServiceCollection) =

services.AddCors() |> ignore

services.AddSignalR() |> ignore

services.AddGiraffe() |> ignore

services.AddHostedService<GameService>() |> ignore



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

2

3

4

type IClientApi =

abstract member LoginResponse :bool * string -> System.Threading.Tasks.Task

abstract member Message :string -> System.Threading.Tasks.Task

abstract member GameState :string -> System.Threading.Tasks.Task



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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

type GameHub () =

inherit Hub<IClientApi> ()





member this.Login (name :string) =

let connectionId = this.Context.ConnectionId

let success, playerId = addPlayer name

if success then



this.Clients.Client(connectionId).LoginResponse( true , playerId)



this.Clients.All.Message(sprintf "New Player: %s (%s)" name playerId)

else



this.Clients.Client(connectionId).LoginResponse( false , "" )





member this.Logout (playerId :string) =

removePlayer playerId



this.Clients.All.Message(sprintf "Player left: %s" playerId)





member this.Turn (playerId :string, direction :string) =

updatePlayerDirection playerId direction





member this.Send (message: string) =

this.Clients.All.Message(message)



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

2

3

4

5

6

7

8

9

10

11

12

13

14

type GameService (hubContext :IHubContext<GameHub, IClientApi>) =

inherit BackgroundService ()



member this.HubContext :IHubContext<GameHub, IClientApi> = hubContext



override this.ExecuteAsync (stoppingToken :CancellationToken) =

let pingTimer = new System.Timers.Timer(TurnFrequency)

pingTimer.Elapsed.Add( fun _ ->

updateState ()

let stateSerialized = serializeGameState gState

this.HubContext.Clients.All.GameState(stateSerialized) |> ignore)



pingTimer.Start()

Task.CompletedTask



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

2

3

dotnet new console -lang f# -n ClientFs

cd ClientFs

dotnet add package Microsoft.AspNetCore.SignalR.Client



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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26



let main argv =



let connection =

(HubConnectionBuilder())

.WithUrl( "http://localhost:5000/gameHub" )

.Build()





connection.On<bool, string>( "LoginResponse" , fun success id -> loginResponseHandler connection success id) |> ignore

connection.On<string>( "Message" , fun message -> messageHandler message) |> ignore

connection.On<string>( "GameState" , fun gameState -> gameStateHandler connection gameState) |> ignore

connection.add_Closed( fun error -> reconnect connection error)





try

connection.StartAsync().Wait()

connection.InvokeAsync( "login" , myName).Wait()

with

| ex -> printfn "Connection error %s" (ex.ToString())

Environment.Exit( 1 )





getCommand connection



0



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

2

3

4

5

6

7

8

9

10

11

12

let loginResponseHandler (connection :HubConnection) (success :bool) (id :string) =

...



let messageHandler (message :string) =

...



let gameStateHandler (connection :HubConnection) (gameState :string) =

...

connection.InvokeAsync( "Turn" , playerId, move.ToString()) |> ignore



let rec reconnect (connection :HubConnection) (error : 'a ) =

...



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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

module Views =

open GiraffeViewEngine



let layout (content: XmlNode list) =

html [] [

head [] [

title [] [ encodedText "SnakeWorld" ]

link [ _rel "stylesheet"

_type "text/css"

_href "/main.css" ]

]

body [] content

]



let index (model : Message) =

[

div [ _class "container" ] [

div [ _class "row" ] [

div [ _id "mapWrapper" ; _class "col-6" ] [

canvas [ _id "worldMap" ; _class "world-map" ; _width "200" ; _height "200" ] [];

div [ _id "playerList" ; _class "player-list" ] []

]

]

div [ _class "row" ] [

div [ _id "message" ; _class "col-6" ] []

]

div [ _class "row" ] [

div [ _id "currentState" ; _class "col-6" ] []

]

]

script [ _src "signalr.js" ] []

script [ _src "jquery-3.3.1.min.js" ] []

script [ _src "game-viewer.js" ] []

] |> layout



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

2

npm install @aspnet/signalr

cp ./node_modules/@aspnet/signalr/dist/browser/signalr.js ./WebRoot



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

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17



const connection = new signalR.HubConnectionBuilder().withUrl( "/gameHub" ).build();





connection.start().catch( function (err) {

return console.error(err.toString());

});





connection.on( "Message" , function (message) {

$( "#message" ).text(message);

});





connection.on( "GameState" , function (gameState) {

handleGameState(JSON.parse(gameState))

});



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…