Programando Juegos Sociales en Línea (Parte 3) Tankster, Blob Storage y JSONP

Published on Author lopezLeave a comment

Anterior Post
Siguiente Post

En mi anterior post escribí sobre el Windows Azure Social Gaming Toolkit, y su ejemplo de juego en línea, para varios jugadores. Ver:

http://watgames.codeplex.com/
http://www.tankster.net/

(El lunes 1ro de Agosto, salió una nueva beta: http://watgames.codeplex.com/releases/view/70342)

Hay dos post interesantes de @ntotten acerca de los “internals” del juego y las decisiones que se tomaron:

Architecture of Tankster – Introduction to Game Play (Part 1)
Architecture of Tankster– Scale (Part 2)

Vale la pena mencionar acá el fragmento de Nathan:

Before we begin with an explanation of the implementation, it is important to know what our goals where for this game. We had three primary goals when building this game.

  1. The game must scale to handle several hundred thousand concurrent users.*
  2. The architecture must be cost effective.
  3. The architecture must be flexible enough for different types of games.

* This is the goal, but there are still some known limitations in the current version that would prevent this. I will be writing more about that later.

Ahora, en este post, quiero escribir sobre el uso e implementación de blob storage en Tankster.

Primero: ¿por qué usar blob storage? Si jugamos Tankster en modo de práctica, el cliente Javascript no necesita usar el backend de Azure. El código Javascript puede acceder al web role y al blob storage usando llamadas Ajax/JSON. Y en juego multi-jugador (en el modo de juego de Tankster llamado Skirmish), cada cliente “pollea” el blob storage para conseguir el estado del juego (los disparos de los tanques, mensajes de chat, otro estado). Pero de nuevo: ¿por qué blob storage? Hay razones monetarias (costo de acceder a la Web Role API vs el costo de leer un blob), pero no conozco los precios de Azure (ya saben, “el maestro no toca la plata” ;-). Pero hay otra razón: repartir el trabajo, dando algo de aire a las instacias de web role. En vez de hacer todo con la API alojada en el web role, se leen blobs desde Azure storage.

Hay una clase AzureBlobContainer en el proyecto de librería de clases Tankster.Common:

public class AzureBlobContainer<T> : IAzureBlobContainer<T>
{
    private readonly CloudBlobContainer container;
    private readonly bool jsonpSupport;
    private static JavaScriptSerializer serializer;
    public AzureBlobContainer(CloudStorageAccount account)
        : this(account, typeof(T).Name.ToLowerInvariant(), false)
    {
    }
    public AzureBlobContainer(CloudStorageAccount account, bool jsonpSupport)
        : this(account, typeof(T).Name.ToLowerInvariant(), false)
    {
    }
    public AzureBlobContainer(CloudStorageAccount account, string containerName)
        : this(account, containerName.ToLowerInvariant(), false)
    { 
    }
	//....

 

La clase usa generics. El tipo T puede ser un Game, GameQueue u otro tipo .NET. Pueden reusar esta clase en otros proyectos: es “game agnostic”, no está escrita para este proyectos, sino que se puede usar en otros. Los varios constructores le agregan flexibilidad para tests.

Este es el código para grabar un objeto tipado en un blob:

public void Save(string objId, T obj)
{
    CloudBlob blob = this.container.GetBlobReference(objId);
    blob.Properties.ContentType = "application/json";
    var serialized = string.Empty;
    serialized = serializer.Serialize(obj);
    if (this.jsonpSupport)
    {
		serialized = this.container.Name + "Callback(" + serialized + ")";
    }
    blob.UploadText(serialized);
}

El objeto (tipo T) es serializado a JSON. Y no sólo JSON: noten que hay un Callback (podríamos remover esto, si nuestro proyecto no lo necesita). Entonces, un objeto juego se graba como (he robado…. eh… tomado prestado 馃槈 este código del post de Nathan):

gamesCallback(
    {"Id":"620f6257-83e6-4fdc-99e3-3109718934a6"
    ,"CreationTime":"\/Date(1311617527935)\/"
    ,"Seed":1157059416
    ,"Status":0
    ,"Users":[
        {"UserId":"MxAb1iZtey732BGsWsoMcwx3JbklW1xSnsxJX9+KanI="
        ,"UserName":"MxAb1iZtey732BGsWsoMcwx3JbklW1xSnsxJX9+KanI="
        ,"Weapons":[]
        },
        {"UserId":"ZXjeyzvw7WTdP8/Uio4P6cDZ8jmKvCXCDp7JjWolAOY="
        ,"UserName":"ZXjeyzvw7WTdP8/Uio4P6cDZ8jmKvCXCDp7JjWolAOY="
        ,"Weapons":[]
        }]
    ,"ActiveUser":"MxAb1iZtey732BGsWsoMcwx3JbklW1xSnsxJX9+KanI="
    ,"GameRules":[]
    ,"GameActions":[]
    })

¿Por qué el callback? Para soportar JSONP:

http://en.wikipedia.org/wiki/JSONP

JSONP or “JSON with padding” is a complement to the base JSON data format, a pattern of usage that allows a page to request data from a server in a different domain. As a solution to this problem, JSONP is an alternative to a more recent method called Cross-Origin Resource Sharing.

Under the same origin policy, a web page served from server1.example.com cannot normally connect to or communicate with a server other than server1.example.com. An exception is the HTML <script> element. Taking advantage of the open policy for <script> elements, some pages use them to retrieve Javascript code that operates on dynamically-generated JSON-formatted data from other origins. This usage pattern is known as JSONP. Requests for JSONP retrieve not JSON, but arbitrary JavaScript code. They are evaluated by the JavaScript interpreter, not parsed by a JSON parser.

Si Ud. no conoce JSON y JSONP, acá hay un tutorial con ejemplo usando JQuery (la misma librería usada por el código de Tankster):

Cross-domain communications with JSONP, Part 1: Combine JSONP and jQuery to quickly build powerful mashup

Pero (ya saben, siempre hay un “pero” en todo proyecto de software) el blob storage de Azure no soporta el uso de JSONP URLs (que tienen un parámetro indicando el id de un callback generado al azar):

Query JSON data from Azure Blob Storage with jQuery

Hay una solución propuesta en un hilo de Stack Overflow. El agente Maxwell Smart diría: ah! el “viejo truco” de tener el callback ya especificado en el blob!

dataCallback({"Name":"Valeriano","Surname":"Tortola"})

Pueden leer una más detallada descripción del problema y solución con código de ejemplo en el post de @woloski’s post (Buen práctica Matías! escribir un post con código!):

Ajax, Cross Domain, jQuery, WCF Web API or MVC, Windows Azure

Un problema a tener en cuenta: vean que el nombre del callback está “hardcodeado” en el blob. El gamesCallback presente en el blob de Game Status debe ser el nombre de una función global. Pero pueden cambiar el texto del blob a cualquier Javascript válido.

¿Y del lado cliente? Pueden estudiar el código de gskinner en el proyecto Tankster.GamePlay, en src\ui\net\ServerDelegate.js:

(function (window) {
    goog.provide('net.ServerDelegate');
    goog.require('net.ServerRequest');
    
    var ServerDelegate = function () {
        throw new Error('ServerDelegate cannot be instantiated.');
    };
    
    var p = ServerDelegate;
    
    p.BASE_API_URL = "/";
    p.BASE_BLOB_URL = "http://tankster.blob.core.windows.net/";
    p.BASE_ASSETS_URL = "";
    p.load = function (type, data, loadNow) {        
        var req;
        if (p.CLIENT_URL == null) {
            p.CLIENT_URL = window.location.toString();
        }
//....

There is an interesting switch:

switch (type) {
    //Local / config calls
    case 'strings':
        req = new ServerRequest('locale/en_US/strings.js', 
		   null, 'jsonp', null, 'stringsCallback'); break;
    //Game calls
    case 'endTurn':
        req = new ServerRequest(apiURL+'Game/EndTurn/', 
		    null, 'xml'); break;
    case 'leaveGame':
        req = new ServerRequest(apiURL+'Game/Leave/'+data.id, 
		    {reason:data.reason}, 'xml', ServerRequest.POST); break;
    case 'playerDead':
        req = new ServerRequest(apiURL+'Game/PlayerDead/', 
		    null, 'json'); break;
    case 'gameCreate':
        req = new ServerRequest(apiURL+'Game/Queue', data, 
		    'xml', ServerRequest.POST); break;
    case 'usersGame':
        req = new ServerRequest(blobURL+'sessions/'+data, 
		    null, 'jsonp', null, 'sessionsCallback'); break;
    case 'gameStatus':
        req = new ServerRequest(blobURL+'games/'+data, 
		    null, 'jsonp', null, 'gamesCallback'); break;
    case 'gameQueueStatus':
        req = new ServerRequest(blobURL+'gamesqueues/'+data, 
		    null, 'jsonp', null, 'gamesqueuesCallback'); break;
    case 'notifications':
        req = new ServerRequest(blobURL+'notifications/'+data, 
		    null, 'jsonp'); break;
    
    //User calls
    case 'verifyUser':
//...

Vean el ‘gameStatus’: usa ‘jsonp’ como formato. Pienso que el  ‘gamesCallback’ como parámetro no es necesario: pueden usar cualquier otro nombre, la función a llamar reside en el contenido del blob.

Yo pienso que hay alternativas a esta forma de mantener el estado del juego. El blob refleja a la entidad Game (ver Tankster.Core\Entities\Game.cs). Pero podría ser implementado en dos blobs por juego:

– Uno de los blobs, conteniendo el estado inicial (jugadores, posiciones, armas…) y la lista de TODA la historia del juego (pienso que todo esto se puede reusar para juegos de tablero, como ajedrez o go, donde puede ser interesante tener la historia de las movidas del juego).

– Otro blob, conteniendo las “novedades” (los últimos 10 segundos de las acciones del juego). De esta forma, los clientes “pollean” este blob, que debería ser más corto que el actual.

El precio a pagar: el servicio en el Web Role que atiende las acciones de los juegos DEBE actualizar LOS DOS blobs. Pero esta actualización sólo ocurren cuando un jugador envía una movida (disparo, chat..). La consulta reiterada desde cada cliente se dispara, digamos, cada un segundo. Actualmente, el blob del juego mantiene los últimos 10 segundos de las acciones Y ADEMAS el estado inicial, jugadores, armas, etc… CADA CLIENTE lee reiteradamente esa información. En la implementación que sugiero, los datos a leer cada vez son menos. En el caso que un cliente se desconecte y reconecte, podría leer el blob que contiene el juego completo, para volver a sincronizar su estado.

La separación de esta información en dos blobs podría mejorar la escalabilidad de la solución. Pero ya saben: premature optimization is the root of all evil ;-). Si tomamos ese camino para almacenar y recuperar los juegos, deberíamos ejecutar algunos tests de carga para realmente conocer las consecuencias de esa decisión.

Tengo que seguir con más análisis de código de Tankster, patrones y desarrollo de juegos sociales en general.

Algunos enlaces:

WAT is to Social Gaming, as Windows Azure is to… | Coding4Fun Blog | Channel 9
Gracias! “Vieron a luz” y mencionan mi post 馃槈
Episode 52 – Tankster and the Windows Azure Toolkit for Social Games | Cloud Cover | Channel 9
Microsoft tailors Azure cloud services to social game developers | VentureBeat
Windows Azure Toolkit for Social Games Released to CodePlex – ‘Tankster’ Social Game Built for Windows Azure
Tankster, a Social Game Built for Windows Azure | Social Gaming on Windows Azure | Channel 9
Social Gaming on Windows Azure | Channel 9
Build Your next Game with the Windows Azure Toolkit for Social Games
Microsoft delivers early build of Windows Azure toolkit for social-game developers | ZDNet

http://www.delicious.com/ajlopez/tankster

Nos leemos!

Angel “Java” Lopez
http://www.ajlopez.com
http://twitter.com/ajlopez

Leave a Reply

Your email address will not be published. Required fields are marked *