Thursday, October 13, 2011

WebSockets in .NET 4.5: a simple game

 

Websockets are one of the new great web standards, which applied in a right way, will enable development of completely new web applications. Microsoft introduces native support for them in .NET 4.5 and IE 10. Other browsers (Firefox 7, Chrome 14) already support the standard. To play with the new technology I created a simple game of pong  (get code here):

image

Basically when two players open the website, the game begins. If further players connect, new games are created for them. If one of the players in a game disconnects, the game ends.

There are some interesting aspects of this sample:

  • connections from players are grouped in games and the messages are broadcasted among game participants
  • there is a thread on the server responsible for moving the ball and broadcasting its position
  • different kinds of messages are sent

In order to run the sample you’ll need:

  • Windows 8 developer preview
  • IIS 8 with websockets module installed
  • Visual Studio 11 developer preview + NuGet
  • ASP.NET MVC 4 for Visual Studio 11

In this post I’m only going to show most interesting parts related to websockets usage, if you want to see more, grab the source.

The client

The client is the browser so we’ll be coding javascript. I prefer to use OO javascript so this is the application’s structure:

function Pong(..some initialization parameters..) {
    ...initialization code and fields here...
}

Pong.prototype = {
    ...methods here...
};

Following code shows how to open a WebSocket on client side:

    // start connecting websockets
    // Firefox 6 and 7 requires Moz prefix...
    if(typeof(MozWebSocket) == "function") {
        this.socket = new MozWebSocket(applicationUrl);
        this.openStateConst = MozWebSocket.OPEN;
    } else {
        this.socket = new WebSocket(applicationUrl);
        this.openStateConst = WebSocket.OPEN;
    }

    // register to socket events
    this.socket.onopen = $.proxy(function () {
        this.info("You are connected...");
    }, this);
    this.socket.onclose = $.proxy(function () {
        this.info("Other player disconnected! Refresh page to restart game");
    }, this);
    this.socket.onerror = $.proxy(function () {
        this.info("You have been disconnected! Sorry for that, refresh page to restart game");
    }, this);
    this.socket.onmessage = $.proxy(function (msg) {
        this.processMessage(msg);
    }, this);

Unfortunately as of Firefox 7, websocket API is still treated as a vendor extension, so you have to use Moz prefix. Otherwise it works great. Next code fragment shows how the incoming messages from server are handled:

    // processes message from server basing on its type
    processMessage: function (msg) {
        var data = JSON.parse(msg.data);

        switch (data.Type) {
            case "PlayerNumberMessage":
                this.myPlayer = data.PlayerNumber;
                if (this.myPlayer == 0) {
                    this.info("<span style='color:red'>You are red.</span>");
                } else {
                    this.info("<span style='color:blue'>You are blue.</span>");
                }
                break;

            case "PlayerPositionMessage":
                this.otherPlayer().yPos = data.YPos;
                this.draw();
                break;

            case "BallPositionMessage":
                this.ball.xPos = data.XPos;
                this.ball.yPos = data.YPos;
                this.draw();
                break;

            case "ScoreMessage":
                this.score = data.Score;
                this.displayScore();
                break;
        }
    }

Data is sent in JSON, so first thing to do is to deserialize it. Messages carry a property named Type that allows to differentiate between different kinds of data being sent. Message types include other player’s position, ball’s position, score change and a message sent when player is assigned his side (red or blue). I think this is quite self-explanatory.

One more thing on client’s side to mention is sending the player’s position to server:

    // register on mousemove over canvas
    $(canvasSelector).mousemove($.proxy(function (e) {
        // has player been initialized?
        if (this.myPlayer == -1)
            return;

        // calulate mouse vertical position relative to canvas
        var offset = $(this.canvas).offset();
        var mouseY = e.pageY - offset.top;

        // constraint movement
        if (mouseY < this.playerHeight / 2)
            mouseY = this.playerHeight / 2;
        if (mouseY > this.canvas.height - this.playerHeight / 2)
            mouseY = this.canvas.height - this.playerHeight / 2;

        this.players[this.myPlayer].yPos = mouseY;

        // send message to server with new position
        this.sendMessage({ YPos: mouseY });

        this.draw();
    }, this));

In reaction to mousemove over the canvas, a message with new player position is sent to the server. Sending the message:

    // serializes and sends message to server
    sendMessage: function (msg) {
        if (this.socket != "undefined" && this.socket.readyState == this.openStateConst) {
            var msgText = JSON.stringify(msg);
            this.socket.send(msgText);
        }
    },

Quite easy and strightforward.

The server

Entry point for server code is an implementation of IHttpHandler that accepts websocket connections:

    /// <summary>
    /// Http handler accepting web socket requests
    /// </summary>
    public class PongHttpHandler : IHttpHandler
    {
        public bool IsReusable
        {
            get { return false; }
        }

        public void ProcessRequest(HttpContext context)
        {
            if (context.IsWebSocketRequest)
            {
                // create a player
                var player = new PongPlayer();
                PongApp.JoinPlayer(player);

                // start receiving from socket
                context.AcceptWebSocketRequest(player.Receiver);
            }
        }
    }

HttpContext.IsWebSocketRequest is a new property returning true if incoming connection is a websocket request. Note, that as of now, you have to be using IIS 8 with websockets module installed. Otherwise this property always returns false.

In this case we accept the connection using AcceptWebSocketRequest by providing it with a delegate, that will asynchronously take care of receiving messages. This a little confusing, because there are two different ways of doing this that you can find by browsing different blogs. The other way is to provide an instance of WebSocketHandler class. The thing is that MSDN knows nothing about such class, and neither does it know about WebSocketCollection. Strange.

Anyway, the PongPlayer class representing a connection to a player looks like this:

    /// <summary>
    /// Represents a player and wraps his websocket operations
    /// </summary>
    public class PongPlayer
    {
        private PongGame _game;
        private object _syncRoot = new object();
        private AspNetWebSocketContext _context;

        public event Action<PongPlayer, PlayerPositionMessage> PlayerMoved;
        public event Action<PongPlayer> PlayerDisconnected;

        // player vertical positon
        public int YPos { get; private set; }

        public void SetGame(PongGame game)
        {
            if (_game != null)
                throw new InvalidOperationException();
            _game = game;
        }

        /// <summary>
        /// This method is used as delegate to accept WebSocket connection
        /// </summary>
        public async Task Receiver(AspNetWebSocketContext context)
        {
            _context = context;
            var socket = _context.WebSocket as AspNetWebSocket;
            // prepare buffer for reading messages
            var inputBuffer = new ArraySegment<byte>(new byte[1024]);

            // send player number to player
            SendMessage(new PlayerNumberMessage { PlayerNumber = _game.GetPlayerIndex(this) });

            try
            {
                while (true)
                {
                    // read from socket
                    var result = await socket.ReceiveAsync(inputBuffer, CancellationToken.None);
                    if (socket.State != WebSocketState.Open)
                    {
                        if (PlayerDisconnected != null)
                            PlayerDisconnected(this);
                        break;
                    }

                    // convert bytes to text
                    var messageString = Encoding.UTF8.GetString(inputBuffer.Array, 0, result.Count);
                    // only PlayerPositionMessage is expected, deserialize
                    var positionMessage = JsonConvert.DeserializeObject<PlayerPositionMessage>(messageString);

                    // save new position and notify game
                    YPos = positionMessage.YPos;
                    if (PlayerMoved != null)
                        PlayerMoved(this, positionMessage);

                }
            }
            catch (Exception ex)
            {
                if (PlayerDisconnected != null)
                    PlayerDisconnected(this);
            }
        }

        /// <summary>
        /// Sends message through web socket to player's rowser
        /// </summary>
        public async Task SendMessage(object message)
        {
            // serialize and send
            var messageString = JsonConvert.SerializeObject(message);
            if (_context != null && _context.WebSocket.State == WebSocketState.Open)
            {
                var outputBuffer = new ArraySegment<byte>(Encoding.UTF8.GetBytes(messageString));
                await _context.WebSocket.SendAsync(outputBuffer, WebSocketMessageType.Text, 
                    true, CancellationToken.None);
            }
        }

        /// <summary>
        /// Closes player's web socket
        /// </summary>
        public void Close()
        {
            if (_context != null && _context.WebSocket.State == WebSocketState.Open)
            {
                _context.WebSocket.CloseOutputAsync(WebSocketCloseStatus.NormalClosure, 
                    "Closing...", CancellationToken.None).Wait();
            }
        }
    }

Basically we’re waiting here in asynchronous manner for messages from a player in a infinite loop. When message arrives, it is deserialized and processed on the same thread. Sending messages is initiated externally by PongGame class and is performed on different thread.

The PongGame represents a game between two players and groups connections:

    public enum GameState
    {
        NotInitiated,
        WaitingForPlayer,
        InProgress
    }

    /// <summary>
    /// Represents pong game between two players
    /// </summary>
    public class PongGame
    {
        public const int LeftPlayer = 0;
        public const int RightPlayer = 1;

        ... more boring constants here ...

        private object _syncRoot = new object();
        private PongPlayer[] _players = new PongPlayer[2];
        private Vector _ballPosition = new Vector(FieldWidth / 2, FieldHeight / 2);
        private Vector _ballDirection = Vector.SW;
        private double _ballSpeed = BallStartingSpeedPixPerSecond;
        private int[] _score = new int[2];

        /// <summary>
        /// Token used to cances ball moving task
        /// </summary>
        private CancellationTokenSource _ballCancellationTokenSource;

        public GameState State { get; private set; }
        
        ... code here...
    }

When new player joins the game, JoinPlayer is called:

        /// <summary>
        /// Joins new player to this game
        /// </summary>
        /// <param name="player"></param>
        public void JoinPlayer(PongPlayer player)
        {
            lock (_syncRoot)
            {
                if (_players[LeftPlayer] != null && _players[RightPlayer] != null)
                    throw new InvalidOperationException();

                player.SetGame(this);
                player.PlayerDisconnected += OnPlayerDisconnected;
                player.PlayerMoved += OnPlayerMoved;

                if (_players[LeftPlayer] == null)
                {
                    _players[LeftPlayer] = player;
                    State = GameState.WaitingForPlayer;
                }
                else
                {
                    _players[RightPlayer] = player;
                    // we have two players, so start the game
                    State = GameState.InProgress; 
                    StartBall();
                }
            }
        }

We just store players in private field. When two players join, the game starts. There is a thread that is responsible for moving the ball and broadcasting ball’s position to both players:

        private void StartBall()
        {
            // this delegete is responsible for ball moving, bouncing and counting score
            Action<CancellationToken> ballMover = (cancellationToken) =>
            {
                var lastTime = DateTime.Now;

                while (true)
                {
                    var thisTime = DateTime.Now;
                    // how many seconds elapsed since last pass
                    var secondsElapsed = (thisTime - lastTime).TotalMilliseconds / 1000.0;

                    MoveBall(secondsElapsed);
                    lastTime = thisTime;

                    Thread.Sleep(50);

                    // finish task if cancel requested
                    if (cancellationToken.IsCancellationRequested)
                        break;
                }
            };

            // prepare cancellation token and run the task
            _ballCancellationTokenSource = new CancellationTokenSource();
            Task.Factory.StartNew(() => ballMover(_ballCancellationTokenSource.Token), 
                _ballCancellationTokenSource.Token, TaskCreationOptions.LongRunning, 
                TaskScheduler.Default);
        }

        private void MoveBall(double secondsElapsed)
        {
            lock (_syncRoot)
            {
                // calculate new position
                _ballPosition += _ballDirection * (_ballSpeed * secondsElapsed);

                ...checking for collision and bouncing the ball..

                // check for scores
                if (_ballPosition.X < 0 || _ballPosition.X > FieldWidth)
                {
                    _score[_ballPosition.X < 0 ? RightPlayer : LeftPlayer]++;
                    // broadcast score message
                    BroadcastMessage(new ScoreMessage { Score = _score });

                    ...reseting ball position...
                }

                // broadcast ball position message
                BroadcastMessage(new BallPositionMessage { XPos = (int)_ballPosition.X, 
                    YPos = (int)_ballPosition.Y });
            }
        }

        /// <summary>
        /// Sends a message to both players
        /// </summary>
        /// <param name="message"></param>
        private void BroadcastMessage(object message)
        {
            foreach (var player in _players)
            {
                player.SendMessage(message);
            }
        }

One more interesing thing about PongGame is the way it reacts to player events:

        private void OnPlayerMoved(PongPlayer player, PlayerPositionMessage position)
        {
            var otherPlayer = OtherPlayer(player);

            if (otherPlayer != null)
            {
                // send new player position to the other player
                otherPlayer.SendMessage(new PlayerPositionMessage { YPos = player.YPos });
            }
        }

        private void OnPlayerDisconnected(PongPlayer player)
        {
            lock (_syncRoot)
            {
                // stop the ball moving task
                _ballCancellationTokenSource.Cancel();
                var otherPlayer = OtherPlayer(player);

                if (otherPlayer != null)
                {
                    // close connection to other player, which means game is over
                    otherPlayer.Close();
                }
                if (GameOver != null)
                    GameOver(this);
            }
        }

The last bit here is the PongApp class which basically is a collection of ongoing games.

    /// <summary>
    /// Aggregates all games
    /// </summary>
    public static class PongApp
    {
        private static object _syncRoot = new object();

        private static List<PongGame> _games;

        static PongApp()
        {
            _games = new List<PongGame>();
        }
        
        /// <summary>
        /// Joins new player to existing game or creates new one 
        /// </summary>
        /// <param name="player"></param>
        public static void JoinPlayer(PongPlayer player)
        {
            lock (_syncRoot)
            {
                var game = _games.Where(g => g.State == GameState.WaitingForPlayer)
                    .FirstOrDefault();
                if (game == null)
                {
                    game = new PongGame();
                    _games.Add(game);
                    game.GameOver += OnGameOver;
                }
                game.JoinPlayer(player);
            }
        }

        private static void OnGameOver(PongGame game)
        {
            lock (_syncRoot)
            {
                _games.Remove(game);
            }
        }
    }

And that would be pretty much it. Again, the code is here.

2 comments:

Unknown said...

WebSocketCollection is provided as part of Microsoft.WebSockets package distributed by NuGet. See http://channel9.msdn.com/Events/BUILD/BUILD2011/SAC-807T for explanation.

Shiva Manjunath said...

Hi Marcin,

Are you available for doing contract work using .Net for a US client? Work can be done remotely from Poland. Please get in touch with me at theblogdoctor @ gmail . com for details. thank you -shiva

Share