Juegos sociales online con SignalR y Windows Azure (1 de 3)


EOD technicians play tic-tac-toe with a children at a family day picnic

Hoy os propongo una mini-serie de 3 capítulos sobre SignalR y Windows Azure con un ejemplo de juego sencillo: el tres en raya, en el que podremos jugar contra otro oponente online.

La tarea presenta algunas complicaciones que resolveremos gracias a SignalR y Azure de forma muy sencilla:

  • Notificaciones: al realizar cada jugada debemos notificar al oponente. Hay muchas maneras de recibir esta notificación, dependiendo del navegador que utilicemos podemos hacer polling, que consiste en ir pidiendo novedades al servidor de forma cíclica, long polling o utilizar websockets para recibir la jugada. SignalR nos simplificará la tarea detectando automáticamente las capacidades de nuestro navegador y utilizando la técnica más adecuada en cada uno.
  • Escalabilidad: ¿qué pasa si nuestro juego tiene éxito y tenemos millones de usuarios simultáneos? Necesitaremos que el servicio escale y para eso tenemos Windows Azure, utilizaremos un Web Role para el servicio y Azure Storage Tables para almacenar la información.
  • Comunicaciones: al escalar a lo ancho nos encontraremos con otro problema, los clientes de un servidor no reciben información de los otros servidores, deberemos establecer un canal de comunicaciones entre los servidores para que reciban qué están haciendo los demás. Para ello utilizaremos el Service Bus de Azure, que nos proporciona un modelo de eventos y suscripciones http://www.asp.net/signalr/overview/performance-and-scaling/scaleout-in-signalr

Atacaremos estos puntos clave uno por uno y en este primer capítulo habrá bastante código. Montaremos todo el sistema base para el juego. Hoy empezamos creando la aplicación básica con SignalR, guardando la información en listas en memoria como en los ejemplos básicos, en el siguiente capítulo le añadiremos Azure y en el último añadiremos otro cliente al sistema, además del cliente web que hacemos hoy.

Antes de empezar os dejo un par de “disclaimers”:

Nota para puristas: voy a hacer bastantes simplificaciones para que este artículo sea didáctico, aunque me duela, no voy a utilizar MVVM, MVC, TDD ni nada que me aparte de lo que pretendo mostrar hoy, el uso de SignalR en Azure.

Nota para pragmáticos: no os esperéis poder hacer copy/paste del código para vuestra aplicación en producción. Aparte de lo comentado en la advertencia anterior, el ejemplo no será completo, voy a dejar algunas cosas como ejercicios para que hagáis vosotros 🙂

Y ahora que ya no tengo ninguna responsabilidad puedo empezar a hackear tranquilo.

¿Qué necesito?

Para poder compilar y probar el código vas a necesitar:

SignalR

Para crear cualquier juego multijugador online en tiempo real tenemos que establecer canales de comunicación entre todas las partes implicadas. Normalmente no es algo trivial, más si añadimos diferentes plataformas de cliente, tales como web, aplicaciones móviles y de tableta. Para que funcione bien tendremos que gestionar los posibles problemas de conexión, establecer puntos de conexión entre clientes diferentes, crear un canal de difusión para enviar notificaciones a todos los clientes, confirmar que los mensajes llegan, crear librerías de cliente que mantengan la conexión abierta con el servidor, autorizar e identificar a los usuarios y un largo etcétera de funcionalidades.

Cuando se trata de un cliente Web, añadimos otra vuelta de tuerca, pues debemos tener en cuenta las capacidades de cada navegador para decidir en el momento qué tecnología de comunicaciones funcionará mejor.

SignalR es una librería para ASP.NET que nos liberará de gestionar todos estos problemas y nos permitirá concentrarnos en la tarea que realmente queremos hacer. Nos proporciona una librería en servidor para gestionar todas las conexiones y suscripciones, además de generar dinámicamente un script que nos permitirá realizar la conexión desde el cliente.

Un tres en raya social

Partimos de una aplicación sencilla como el tres en raya para no complicar demasiado el ejemplo, algo intermedio entre  el ejemplo básico de chat y el juego online ShootR.

Empezaremos con una aplicación ASP.NET vacía:

tictactoe_01_newproject

Una vez tengamos nuestra solución, necesitaremos un archivo Global.asax y una página HTML donde mostrar nuestro tablero:

3er.01a.Globalasax

En el mismo menú seleccionamos HTML Page y la llamamos default:

3er.01b.defaulthtmlDentro del body de la página colocamos un panel de tres en raya:

<div id="partida">
    <button id="empezar">Empezar nueva partida</button>
    <style>
        #tablero {
            height: 160px;
            width: 160px;
            padding: 5px;
        }

        .row {
            height: 50px;
            width: 150px;
        }

        .col {
            height: 48px;
            width: 48px;
            border: 1px solid black;
            float: left;
            text-align: center;
            line-height: 48px;
        }

        .row0, .row2 {
            background-color: rgba(0,0,0,0.2);
        }

        .col1 {
            background-color: rgba(0,0,0,0.2);
        }
    </style>

    <div id="tablero">
        <div class="row row0">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
        <div class="row row1">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
        <div class="row row2">
            <div class="col col0"></div>
            <div class="col col1"></div>
            <div class="col col2"></div>
        </div>
    </div>

    <div>
        <h2>Solicitud de partidas</h2>
        <div id="partidas">
            <div>Cargando...</div>
        </div>
    </div>
</div>
<div>
    <h2>Mensajes</h2>
    <div id="mensajes"></div>
</div>

En cuanto al código, empezaremos con la parte de servidor. Vamos a utilizar SignalR, necesitaremos añadir las referencias a las librerías en nuestro proyecto. Por suerte tenemos la herramienta Nuget que nos permitirá descargar e instalar en nuestro proyecto todo lo necesario con un click:
3er.02.alt.Nuget
Buscamos en la sección Online por SignalR:
3er.02.signalr
Nuget nos instalará todas las dependencias que necesita la librería y acto seguido nosotros cambiaremos el evento Application_Start en el Global.asax.cs, tal como nos indica el propio SignalR al instalarse:

protected void Application_Start(object sender, EventArgs e)
{
    // Register the default hubs route: ~/signalr
    RouteTable.Routes.MapHubs();
}

Esta pequeña línea realiza la magia que nos permitirá comunicarnos con los clientes desde el servidor. En nuestro proyecto ya podemos crear un Hub, el punto de conexión entre los clientes y el servidor, al que llamaremos PartidaHub:
3er.03.signalrhub

La plantilla nos creará a mínima expresión de un Hub:

public class PartidaHub : Hub
{
    public void Hello()
    {
        Clients.All.hello();
    }
}

Este código de servidor que se ha generado automáticamente, está llamando a una función definida en todos los clientes conectados a este Hub llamada “hello”; SignalR se encargará de que eso ocurra.
En el Hub vamos a crear métodos que podrán ser llamados por el cliente y el mismo hub, a su vez, podrá realizar llamadas a todos los clientes, al emisor del mensaje o a receptores concretos, mediante los siguientes métodos:

//como el anuncio aquel de refrescos...
//para todos
Clients.All.hello();
//para el que llama
Clients.Caller.hello();
//para todos los demás
Clients.Others.hello();
//para uno en concreto
Clients.Client(id).hello();

Si vamos a mirar la definición de estas propiedades y métodos veremos que son de tipo dynamic, lo que nos permite escribir nombres de funciones que no tenemos definidas en ningún sitio de nuestro código C#.

Datos de partida

Antes de ponernos con el Hub creamos unas cuantas estructuras que necesitamos para jugar: los jugadores, las partidas y las solicitudes de partida, para que un jugador pueda empezar una partida y así otro pueda apuntarse.
Creamos una carpeta Data y empezamos a crear unas cuantas clases, la primera una clase para almacenar la partida y la lógica de juego:

public class Partida
{
    public const int Dimension = 3;

    public Partida(string pais, string id)
    {
        Pais = pais;
        Id = id;
        _tablero = new char[Dimension * Dimension];
        for (int i = 0; i < _tablero.Length; i++)
        {
            _tablero[i] = ' ';
        }
    }

    public string Pais { get; set; }
    public string Id { get; set; }
    public Jugador Jugador1 { get; set; }
    public Jugador Jugador2 { get; set; }
    public int Turno { get; set; }

    char[] _tablero;
    public string Tablero
    {
        get
        {
            return new string(_tablero);
        }
        set
        {
            _tablero = value.ToCharArray();
        }
    }

    public bool Marcar(int x, int y, int jugador)
    {
       if (Estado == EstadoPartida.EnJuego)
       {
           var posicion = Dimension * x + y;
           if (jugador == Turno)
           {
               if (_tablero[posicion] == ' ')
               {
                   var valor = Turno == 0 ? 'O' : 'X';
                   _tablero[Dimension * x + y] = valor;
                   Turno = Turno == 0 ? 1 : 0;
                   return ComprobarFinal();
               }
               else
               {
                   throw new InvalidOperationException("La casilla está ocupada");
               }
           }
           else
           {
               throw new InvalidOperationException("No es tu turno");
           }
       }
       else
       {
           throw new InvalidCastException("El juego ha acabado");
       }
   }

   public char Valor(int x, int y)
   {
       return _tablero[Dimension * x + y];
   }

   bool ComprobarFinal()
   {
       int[] jugadas = new int[Partida.Dimension * 2 + 2];

       for (int i = 0; i < Partida.Dimension; i++)
       {
           for (int j = 0; j < Partida.Dimension; j++)
           {
               var v = Valor(i, j);
               int sum = 0;
               if (v == 'X')
               {
                   sum = -1;
               }
               else if (v == 'O')
               {
                   sum = 1;
               }
               jugadas[i] += sum;
               jugadas[Partida.Dimension + j] += sum;
               if (i == j)
               {
                   jugadas[Partida.Dimension * 2] += sum;
               }
               if (i + j == Partida.Dimension - 1)
               {
                   jugadas[Partida.Dimension * 2 + 1] += sum;
               }
           }
       }
       if (jugadas.Count((x) => x == 3) > 0)
       {
           Estado = EstadoPartida.Gana1;
       }
       else if (jugadas.Count((x) => x == -3) > 0)
       {
           Estado = EstadoPartida.Gana2;
       }
       else if (!Tablero.Contains(' '))
       {
           Estado = EstadoPartida.Empate;
       }

       return Estado != EstadoPartida.EnJuego;
   }
}

También necesitaremos una clase Jugador para identificarlos:

public class Jugador
{
    public Jugador(string pais, string nombre, string id)
    {
        Nombre = nombre;
        Pais = pais;
        Id = id;
    }

    public string Pais { get; set; }
    public string Nombre { get; set; }
    public string Id { get; set; }
}

Y una clase DatosPartida que nos permita ir añadiendo jugadores, solicitudes y partidas de forma ordenada. Esta clase nos vendrá bien a la hora de transformar nuestra aplicación a Azure:

public class DatosPartida
{
    List<Jugador> _jugadores = new List<Jugador>();
    List<Partida> _partidas = new List<Partida>();
    List<Jugador> _solicitudes = new List<Jugador>();

    public DatosPartida()
    {
    }

    public Jugador NuevoJugador(string pais, string nombre, string id)
    {
        var jugador=_jugadores.FirstOrDefault(j => j.Pais == pais && j.Nombre == nombre);
        if (jugador == null)
        {

            jugador = new Jugador(pais, nombre, id);
            _jugadores.Add(jugador);
        }
        else
        {
            //actualiza el id del jugador, imaginaremos, por ahora, que si se llaman igual es el mismo...
            jugador.Id = id;
        }
        return jugador;
    }

    public Jugador NuevaSolicitud(Jugador jugador)
    {
        _solicitudes.Add(jugador);
        return jugador;
    }

    public Jugador ObtenerSolicitud(string pais, string nombre)
    {
        return _solicitudes.FirstOrDefault(s => s.Pais == pais && s.Nombre == nombre);
    }

    public bool BorrarSolicitud(Jugador solicitud)
    {
        return _solicitudes.Remove(solicitud);
    }

    public Partida EmpezarPartida(string pais, Jugador jugador1, Jugador jugador2)
    {
        var partida = new Partida(pais, Guid.NewGuid().ToString())
        {
            Jugador1 = jugador1,
            Jugador2 = jugador2
        };
        _partidas.Add(partida);
        return partida;
    }

    public Jugador ObtenerJugador(string pais, string id)
    {
        return _jugadores.FirstOrDefault(jugador => jugador.Id == id);
    }

    public Partida ObtenerPartida(string pais, string id)
    {
        return _partidas.FirstOrDefault(p=>p.Pais==pais && p.Id==id);
    }

    public void GuardarMovimiento(Partida partida)
    {
        var old=ObtenerPartida(partida.Pais, partida.Id);
        _partidas.Remove(old);
        _partidas.Add(partida);
    }

    public IEnumerable<Jugador> ListaDisponibles(string pais)
    {
        return _solicitudes.Where((s)=>s.Pais==pais);
    }
}

Y finalmente escribiremos el Hub, donde cada vez que queramos notificar algo a un cliente o a muchos utilizaremos los métodos dinámicos de Clients.All , Clients.Caller, etc.:

public class PartidaHub : Hub
{
    private readonly static Lazy<DatosPartida> _datos = new Lazy<DatosPartida>(true);
    public DatosPartida Datos { get { return _datos.Value; } }

    public Jugador NuevaPartida(string pais, string nombre)
    {
        var jugador = Datos.NuevoJugador(pais, nombre, Context.ConnectionId);
        var solicitud=Datos.NuevaSolicitud(jugador);
        Clients.All.Nueva(solicitud);  //avisamos a todos que una nueva partida ha sido solicitada
        return jugador;
    }

    public IEnumerable<Jugador> PartidasDisponibles(string pais)
    {
        return Datos.ListaDisponibles(pais);
    }

    public bool Jugar(string pais, string jugador1, string jugador2)
    {
        var solicitud = Datos.ObtenerSolicitud(pais, jugador1);
        if (solicitud != null)
        {
            var jugadorB=Datos.ObtenerJugador(pais, Context.ConnectionId);
            if (jugadorB == null)
                jugadorB = Datos.NuevoJugador(pais, jugador2, Context.ConnectionId);
            Partida partida = Datos.EmpezarPartida(pais, solicitud, jugadorB);

            //eliminamos las solicitudes pendientes de los jugadores
            Datos.BorrarSolicitud(solicitud);
            var solicitud2 = Datos.ObtenerSolicitud(jugadorB.Pais, jugadorB.Nombre);
            if (solicitud2 != null)
            {
                Datos.BorrarSolicitud(solicitud2);
            }

            //notificacmos solicitudes eliminadas
            Clients.All.PartidaEliminada(solicitud);
            if (solicitud2 != null)
            {
                Clients.All.PartidaEliminada(solicitud2);
            }
            //notificamos al llamante sobre el comienzo de la partida
            Clients.Caller.Jugando(partida);
            //notificamos al jugador que solicitó una partida sobre el comienzo de la misma
            Clients.Client(solicitud.Id).Jugando(partida);
            return true;
        }
        else
        {
            error("Ya no existe la partida");
        }
        return false;
    }

    public void MarcaCasilla(string pais, string nombre, int x, int y)
    {
        var partida = Datos.ObtenerPartida(pais, nombre);

        if (partida != null)
        {
            int turno = 0;
            if (Context.ConnectionId == partida.Jugador2.Id)
                turno = 1;
            int turnoAnterior = partida.Turno;

            try
            {
                bool acabada = partida.Marcar(x, y, turno);
                Datos.GuardarMovimiento(partida);
                Clients.Client(partida.Jugador1.Id).Jugada(partida);
                Clients.Client(partida.Jugador2.Id).Jugada(partida);

                if (acabada)
                {
                    switch (partida.Estado)
                    {
                        case EstadoPartida.Empate:
                            Clients.Client(partida.Jugador1.Id).FinJuego("Esta vez hay empate");
                            Clients.Client(partida.Jugador2.Id).FinJuego("Esta vez hay empate");
                            break;
                        case EstadoPartida.Gana1:
                            Clients.Client(partida.Jugador1.Id).FinJuego("Ganaste la partida!!!");
                            Clients.Client(partida.Jugador2.Id).FinJuego("Esta vez te ganó " + partida.Jugador1.Nombre);
                            break;
                        case EstadoPartida.Gana2:
                            Clients.Client(partida.Jugador2.Id).FinJuego("Ganaste la partida!!!");
                            Clients.Client(partida.Jugador1.Id).FinJuego("Esta vez te ganó " + partida.Jugador2.Nombre);
                            break;
                    }
                }
            }
            catch (InvalidOperationException ex)
            {
                mensaje(ex.Message);
            }
        }
        else
        {
            error("no existe la partida");
        }
    }

    private void error(string error)
    {
        mensaje("Error: " + error);
    }

    private void mensaje(string msg)
    {
        Clients.Caller.Mensaje(msg);
    }
}

Código en cliente

En el lado del cliente vamos a definir unas funciones para comunicarnos con el servidor. Lo primero que necesitamos es obtener el Hub que hemos definido antes. SignalR crea para nosotros una librería JavaScript que tiene todo lo necesario para comunicarnos con este.
Creamos en la carpeta Scripts un script “tresenraya.js” y añadimos al final de la página html las referencias a los siguientes scripts, antes del cierre de la etiqueta body:

<script src="Scripts/jquery-2.0.1.min.js"></script>
<script src="Scripts/jquery.signalR-1.1.2.min.js"></script>
<!--Reference the autogenerated SignalR hub script. -->
<script src="/signalr/hubs"></script>
<script src="Scripts/tresenraya.js"></script>

El script /signalr/hubs es el que genera SignalR automáticamente con el código para acceder a nuestros hubs.
Una vez hemos añadido los scripts, podemos empezar a escribir nuestro tresenraya.js. En primer lugar necesitamos conectar al hub e inicializar, en nuestro versión sencilla pediremos el nombre al usuario nada más empezar, lo que vendría a ser el login de los pobres (y confiados). Para el código js usamos jQuery1.6.4 que es el que nos ha instalado SignalR:

$(function () {
    //obtenemos el hub
    var servicioPartidas = $.connection.partidaHub;
    //iniciamos la conexión y una vez iniciada (método done) llamamos a nuestra función de inicialización
    $.connection.hub.start().done(init);

    var pais = "Mallorca";  //para no pedir tantos datos al principio vamos a suponer 
                            //que estáis todos de vacaciones en mi isla ^^

    function init() {
        nombre = prompt("Escribe tu nombre");
        //llamamos a una función de servidor que nos devuelve una lista de partidas
        //que se realizan en Mallorca ^^
        servicioPartidas.server.partidasDisponibles(pais).done(function (list) {
            $partidas.empty();
            $.each(list, function () {
                crearEntrada(this);
            });

        });
        //botón nueva partida
        $btnEmpezar.bind("click", null, function (e) {
            servicioPartidas.server.nuevaPartida(pais, nombre);
            return false;
        });
        //inicializamos los recuadros del 3 en raya
        for (var row = 0; row < 3; row++) {
            for (var col = 0; col < 3; col++) {
                var $celda = $(".row" + row + " .col" + col);
                $celda.click(function () {
                    var x = row;
                    var y = col;
                    return function () {
                        if (partidaActual != null) {
                            servicioPartidas.server.marcaCasilla(partidaActual.Pais, partidaActual.Id, x, y);
                        }
                    }
                }());
            }
        }
    }

Fijaos que estamos llamando a los métodos que definimos en nuestro Hub: nuevaPartida, marcarCasilla, etc., y usamos los mismos parámetros que hemos definido en el código C#.

Para el código anterior necesitamos unas cuantas funciones más que nos permiten mostrar las solicitudes de partida que van llegando:

$btnEmpezar = $("#empezar");
$partidas = $("#partidas");
$mensajes = $("#mensajes");
var partidaActual;
var nombre;
var partidaListItem = "<div id='{Pais}_{Nombre}item'>Pais: {Pais} Usuario:{Nombre} <button id='{Pais}_{Nombre}'>Jugar</button></div>";

// A simple templating method for replacing placeholders enclosed in curly braces.
if (!String.prototype.supplant) {
    String.prototype.supplant = function (o) {
        return this.replace(/{([^{}]*)}/g,
            function (a, b) {
                var r = o[b];
                return typeof r === 'string' || typeof r === 'number' ? r : a;
            }
        );
    };
}

function crearId(entity) {
    return entity.Pais + "_" + entity.Nombre;
}

function crearEntrada(solicitud) {
    if (solicitud.Nombre != nombre) {
        $partidas.append(partidaListItem.supplant(solicitud));
        $("#" + crearId(solicitud)).click(function (e) {
            var valores = e.currentTarget.id.split("_");
            servicioPartidas.server.jugar(valores[0], valores[1],nombre);
            return false;
        });
    }
}

Recibir notificaciones del servidor

Hasta ahora hemos escrito código que envía información, pero también queremos recibir el push que realiza el servidor hacia los clientes. Para esto tenemos que definir unas funciones con los mismos nombres y parámetros que utilizamos en el lado del servidor.
Si recordáis el código anterior, al crear una nueva solicitud de partida llamábamos al método Clients.All.Nueva(solicitud);. Este es el método que tendremos que escribir en el cliente, por ejemplo, con el siguiente código recibimos el mensaje de partidas nuevas:

servicioPartidas.client.nueva = function (solicitud) {
    if (solicitud.Nombre == nombre && solicitud.Pais == pais) {
        //si es tu partida deshabilita el botón de empezar.. ya estás jugando
        $btnEmpezar.text("Esperando a que llegue un jugador");
        $btnEmpezar.prop("disabled", true);
        $partidas.prop("disabled", true);
    }
    else {
        //si no es tu partida saca la lista
        crearEntrada(solicitud);
    }

Aquí cuando se nos notifica el principio y final del juego:

servicioPartidas.client.jugando = function (partida) {
    partidaActual = partida;
    $btnEmpezar.text("Jugando partida " + partida.Jugador1.Nombre + " contra " + partida.Jugador2.Nombre);
    $btnEmpezar.prop("disabled", true);
    $partidas.prop("disabled", true);
    pintarTablero(partida.Tablero);
};

function pintarTablero(tablero) {
    for (var row = 0; row < 3; row++) {
        for (var col = 0; col < 3; col++) {
            var $celda = $(".row" + row + " .col" + col);
            $celda.text(tablero[3 * row + col]);
        }
    }
}

servicioPartidas.client.finJuego = function (mensaje) {
    $mensajes.append(mensaje + "<br/>");
    $btnEmpezar.text("Empezar nuevo juego");
    $btnEmpezar.prop("disabled", false);
    $partidas.prop("disabled", false);
}

Cada vez que hay una jugada desde el servidor se nos manda el tablero y lo volvemos a representar:

servicioPartidas.client.jugada = function (partida) {
    //cuando llega una jugada de nuestra partida en curso
    //pintamos el tablero
    pintarTablero(partida.Tablero);
}

Y dos funciones más que también necesitamos:

//los mensajes también irán apareciendo
servicioPartidas.client.mensaje = function (msg) {
    $mensajes.append(msg + "<br/>");
}

servicioPartidas.client.partidaEliminada = function (solicitud) {
    $("#" + crearId(solicitud) + "item").remove();
}

Tras escribir (o cortapegar) todo este código ya os debería funcionar la primera versión del tres en raya online. Mañana transformaremos el código para que funcione en un WebRole de Windows Azure.
3er.01.pacowins
Espero vuestros comentarios!!! Id pensando cómo haríais que esta aplicación funcione bien en Azure.

Si habéis llegado hasta aquí os merecéis este vídeo viejuno que os explicará por qué he elegido el tres en raya:

Anuncios

  1. Pingback: Juegos sociales online con SignalR y Windows Azure (3 de 3) | Mouseless Me
  2. Jose

    Buenas Juan Manuel, primero de todo decirte que me parece muy interesante la guia/tutorial de como crear aplicaciones con signalr + azure.
    El tema es que estoy empezando el desarrollo de un juego para Facebook y quiero utilizar SignalR y Table Storage.
    Mi duda es la siguiente:
    En mi aplicación podre consultar los datos del perfil de los usuarios. Es decir una simple llamada a Web Api para obtener los datos del perfil. Mi pregunta es: Hago una petición a un Web Api, o utilizo el signalR para obtener la info del perfil?
    Sin saber mucho sobre el tema entiendo que si utilizo una llamada ajax a un web api, se optimizará mejor los recursos que utilizar el signalR abierto.
    Que me aconsejos? Trabajo todo por el SignalR o para estas llamadas triviales utilizo un Web Api?

    Muchas gracias!

  3. Pingback: Migrando el tres en raya: Azure SDK 2.4, SignalR 2.1.1, Windows 8.1 | Mouseless Me
  4. Andres

    Como esta Juan Manuel, buscando en internet me encontre con este tema que me parece muy interesante de signalR, y quería probarle el ejemplo que ud colgo en esta pagina pero me encontre con un pequeño problema el cual me da en la clase partida y PartidaHub que no hay EstadoPartida y Estado, no se si me podría ayudar soy nuevo en esto gracias

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s