Friday, 3 May 2013

Backgammon with SignalR – Part Two – Server Side SignalR

This is the second of a three part series of posts on BackgammonR – an online Backgammon game built with SignalR. If you haven’t read it already, please see Part 1 – Game Engine.

SignalR actually provides two abstractions above the base technologies for maintaining persistent connections between browser and server. One is fairly low-level, called PersistentConnection that I didn’t look into. The other is Hubs.

I have a single SignalR hub called GameNotificationHub which inherits from Microsoft.AspNet.SignalR.Hub and thus obtains its base functionality. Within that class you can create public methods, which – once SignalR has worked its magic – become end-points that can be called from the client-side JavaScript.

They all return void though – which initially seems counter-intuitive and not what you would do were you wiring up an end-point for an AJAX request for example. However the point is of course that with this type of application we may need to push responses to this method call not just to the caller, but also to groups of other users connected to the application as well.

This is done via the Clients property – which has various dynamic fields such as Caller, All, Others, Group to allow you to send messages to just the appropriate sets of users. Strung on the end of that are further dynamic methods that will resolve to client-side code that is “called” from the server. Parameters can be passed to these messages – which can be simple strings or more complex types that will be accessible as JSON on the client.

For example the code sample that follows illustrates the Join method on the hub. It’s called when an authenticated user first access the game. There’s a check to ensure that the user hasn’t already joined, and if not, they are added to the list of players. Two notifications to the client are made – one to the caller to trigger the update of their screen, and one to all users to flash a message that a new player has joined.

        public void Join(string name)
        {
            if (!Manager.Instance.Players.Any(x => x.Name == name))
            {
                var user = new Player 
                { 
                    Name = name, 
                    ConnectionId = Context.ConnectionId,
                    Status = ConnectionStatus.ReadyToPlay 
                };
                Manager.Instance.Players.Add(user);

                Clients.All.joined(user, Context.ConnectionId);
                Clients.Caller.callerJoined(name);                
            }
        }

Other methods on the hub allow users to leave, challenge others to games, accept or reject those challenges and to take turns in the game.

The groups feature was particularly useful here. Within SignalR you can create groups as you see fit and assign users to them, and so push messages to just particular groups of them. The classic example of this is the rooms in a chat site. I used a group for each game, identifying it by the game Id, and thus only pushing updates to those users playing or viewing a game. For example in the Move method parameters are passed identifying the game and the move requested. Checks are made to ensure it’s the calling user’s turn and that the move they want to make is valid according to the rules of the game. If that’s all OK, the internal state of the game is updated and the whole Game object passed as a notification to the group associated with that game.

        public void Move(Guid gameId, int[] from, int[] to)
        {
            var game = GetGame(gameId);
            if (game != null)
            {
                if ((game.CurrentPlayer == 1 && game.Black.ConnectionId == Context.ConnectionId) ||
                    (game.CurrentPlayer == 2 && game.White.ConnectionId == Context.ConnectionId))
                {
                    if (game.Move(from, to))
                    {
                        // Notify all clients in group of move
                        Clients.Group(game.Id.ToString()).moved(game);
                    }
                    else
                    {
                        Clients.Caller.displayError("Invalid move.");
                    }
                }
                else
                {
                    Clients.Caller.displayError("Not your turn.");
                }
            }
            else
            {
                Clients.Caller.displayError("Game not found.");
            }
        }

Click for Part 2 – SignalR on the client

Play the game

View the code

No comments:

Post a Comment