Forza quattro per Android, MVC + SurfaceView

Ho realizzato una nuova app per Android: Forza quattro (in inglese connect4), il mitico gioco a turno dove vince chi riesce ad allineare per primo 4 o più pedine dello stesso colore.

Realizzata per sperimentare il rendering SurfaceView+Canvas e il pattern MVC in ambiente Android.

device-2015-10-03-112816

device-2015-09-06-160459

 

E’ possibile giocare contro lo smartphone selezionando uno dei 32 livelli di difficoltà. L’intelligenza artificiale è basata sull’algoritmo minimax con alcune ottimizzazioni per renderlo più veloce:

Il gioco include:

  • Effetti sonori tramite SoundPool
  • Musica di sottofondo tramite classe MediaPlayer
  • Rendering software tramite SurfaceView e Canvas (su thread separato, come spiegato qui)
  • Effetto caduta dei gettoni

Lo trovate qui in google play!

googleplay

L’ambiente Android si sa, è fortemente event-driven. L’approccio tramite pattern classico MVC è comunque fattibile: ho mappato il pattern alle seguenti componenti Android (non necessariamente la soluzione migliore!):

  • Model: sostanzialmente lo stato della scacchiera, un array bidimensionale.
  • View: classe SurfaceView che legge i dati dal modello e fa il rendering su schermo
  • Controller: activity Android che legge l’input utente e coordina Model e View assieme (più una terza classe Sound responsabile della musica ed effetti sonori).

20150913_170934

L’applicazione nel suo complesso richiede 3 thread: quello principale dell’activity stessa (UI), quello dentro la classe View che legge lo stato di gioco e aggiorna lo schermo e l’ultimo quello che incorpora l’algoritmo minimax per la scelta della mossa da parte del computer.

La caduta dei gettoni è realizzata con la classica equazione del moto uniformemente accelerato: s = 1/2 * g * t^2 dove t è il tempo espresso in tick millisecondi e g è l’accelerazione espressa in pixel / ms /ms.

Serializzazione JSON in C#

JSON è un formato per l’interscambio dei dati tra applicazioni client-server,

in C# possiamo utilizzare:

Json.Encode()
Json.Decode()

che troviamo nel namespace System.Web.Helpers

Con queste due funzioni possiamo rispettivamente serializzare e deserializzare un qualsiasi oggetto C# da e verso JSON.
La serializzazione standard prevede la creazione di stringhe JSON in forma di coppie chiave-valore.

Ad esempio, immaginiamo di avere la seguente classe C# che memorizza lo stato di gioco corrente in un ambiente multiplayer (ad esempio da dover inviare a tutti i client connessi ogni tot millisecondi):

private class GameSnapshot
{
            public class EnemyPosition
            {
                public double x;
                public double y;
                public double direction;
                public int energy;
            }

            public string playerName;
            public double playerXpos;
            public double playerYpos;
            public int lives;
            public int level;
            public int points;
            public List<EnemyPosition> enemiesPositions;
            public DateTime timestamp;
}

La lista enemiesPositions contiene le posizioni degli avversari, gli altri parametri sono quelli relativi al giocatore (posizione, stato, vite rimanenti etc..).
Una plausible conversione JSON dell’oggetto potrebbe essere la seguente:

{
  "playerName": "unnamed player",
  "playerXpos": 12.124354328,
  "playerYpos": 3.8943532434,
  "lives": 4,
  "level": 12,
  "points": 81000,
  "enemiesPositions": [
    {
      "x": 0.17568866497636246,
      "y": 0.8758737812172034,
      "direction": 0.5097326890145115,
      "energy": 1102696000
    },
    {
      "x": 0.26358662371690694,
      "y": 0.8292919941382911,
      "direction": 0.26260223484719275,
      "energy": 2123401755
    },
    {
      "x": 0.7504166782602746,
      "y": 0.5961088224296034,
      "direction": 0.5173756976227163,
      "energy": 1782593816
    },
    {
      "x": 0.4766914893298836,
      "y": 0.8596706198806272,
      "direction": 0.1079285652879293,
      "energy": 1907324897
    },
    {
      "x": 0.5031359705622941,
      "y": 0.13449626934458328,
      "direction": 0.21105524069213086,
      "energy": 1125504690
    }
  ],
  "timestamp": "/Date(1437732572430)/"
}

Per un totale di 923 byte, quasi 1K di dati da trasmettere. Non molti, ma dobbiamo considerare che i dati potrebbero dover essere trasmessi molte volte al secondo e che i client connessi potrebbero essere molti.

Di default la serializzazione prevede la creazione di coppie chiavi-valore dove chiave è il nome della variabile (es. “playerName”) e valore il suo valore corrispondente (es. “unnamed player”). Per una codifica più efficiente possiamo eliminare tutte le chiavi e lasciare solo i valori: la posizione relativa dei valori nella struttura dati JSON è sufficiente a ricostruire completamente (deserializzare) l’oggetto originale.

A tal fine possiam utilizzare il tipo dati dynamic, introdotto in C# 4.0. Dynamic è un tipo dati appunto dinamico, il compilatore ignora il tipo reale della variabile o lo pospone all’esecuzione. Creando una lista di dynamic possiamo creare sequenze di oggetti non omogenei tra loro (liste o array di tipi diversi) ognuno dei quali è anche anomimo (non ha un nome esplicito, è solo un elemento della lista).

La classe di prima diventa semplicemente una lista di tipi dynamic:

var gameShapshot = new List<dynamic>();

Possiamo aggiungere a questa lista (metodo .Add()) qualsiasi oggetto: un int, una striga, un DateTime, liste di altri oggetti e cosi’ via. Ad esempio, possiamo creare un oggetto dynamic che contiene le stesse informazioni della classe GameSnapshot vista in precedenza.

var gs = new List<dynamic>();

gs.Add("unnamed player");
gs.Add(12);
gs.Add(4);
gs.Add(81000);
gs.Add(DateTime.Now);
gs.Add(12.124354328);
gs.Add(3.8943532434);
var enemiesPositions = new List<dynamic>();

for (var i = 0; i < 5; i++)
{
   var enemyPos = new List<dynamic>();
   enemyPos.Add(rnd.NextDouble());
   enemyPos.Add(rnd.NextDouble());
   enemyPos.Add(rnd.NextDouble());
   enemyPos.Add(rnd.Next());

   enemiesPositions.Add(enemyPos);
}
gs.Add(enemiesPositions);

String snapShot = Json.Encode(gs);
System.Console.WriteLine(snapShot);

Abbiamo ora un oggetto C# che contiene le stesse informazioni (sia in valore che in struttura) ma la serializzazione JSON risulta molto più snella:

[
  "unnamed player",
  12,
  4,
  81000,
  "/Date(1437732515952)/",
  12.124354328,
  3.8943532434,
  [
    [
      0.5849644861114046,
      0.9460975015284948,
      0.9270875434982998,
      1612467919
    ],
    [
      0.5084204606285414,
      0.6327498385835205,
      0.7234090821460863,
      784917789
    ],
    [
      0.8641532142014025,
      0.2696795273896677,
      0.21300438335770946,
      1166561007
    ],
    [
      0.6842819497381719,
      0.10087060700211237,
      0.3484976507483505,
      686316417
    ],
    [
      0.27328705707252354,
      0.2811392360744715,
      0.3885680466837101,
      1938705260
    ]
  ]
]

Occupa 650 byte, poco più della metà. Riducendo i decimali di default (spesso non serve tutta quella risoluzione, ovviamente dipende dal contesto) arriviamo a circa 400 byte. A parità di banda significa raddoppiare il numero di client connessi contemporaneamente, raddoppiare il rate di invio dei dati o una combinazione dei due.

Per lunghi array di valori numerici, ad esempio per dati relativi a grafici la serializzazione senza nomi risulta particolarmente efficiente e si adatta bene a linguaggi loosely typed come Javascript e PHP.

Ad esempio:

[
   [
      0.5849644861114046,
      0.9460975015284948,
      0.9270875434982998,
      0.8641532142014025
    ],
    [
      0.5084204606285414,
      0.6327498385835205,
      0.7234090821460863,
      0.2811392360744715
    ],
    [
      0.8641532142014025,
      0.2696795273896677,
      0.21300438335770946,
      0.2696795273896677
    ],
    [
      0.6842819497381719,
      0.10087060700211237,
      0.3484976507483505,
      0.9270875434982998
    ],
    [
      0.27328705707252354,
      0.2811392360744715,
      0.3885680466837101,
      0.6327498385835205
    ],
    [
      0.8641532142014025,
      0.2696795273896677,
      0.21300438335770946,
      0.2696795273896677
    ]
]

Ho trovato la soluzione con i dynamic abbastanza utile e veloce da implementare ma credo ne esistano altre. BSON per esempio? Oppure Message Pack.

Creare uno splash screen in Android

Gli splash screen sono utilizzati per mostrare all’utente del testo o delle immagini statiche prima ancora del completo caricamento dell’applicazione, soprattutto quando il caricamento richiede alcuni secondi. Senza un buon splash screen l’utente vede semplicemente uno schermo nero: più di 4-5 secondi di nero possono far desistere l’utente dal continuare ad aspettare!

Di solito uno splash screen è semplicemente una immagine a tutto schermo che mostra il nome dell’applicazione, il logo, del testo o, nelle versioni più evolute, una barra di caricamento.

popular_apps_splash_screens

Realizzare uno splash screen in Android è abbastanza semplice.

Creiamo una nuova blank activity, chiamiamola ad esempio “start”:

Screen shot 2015-07-19 at 1.59.12 PM

Possiamo cambiare il colore dello sfondo, aggiungere qualsiasi immagine e/o testo preferiamo.

Impostiamo ora l’activity come activity principale. Sarà la prima ad essere lanciata all’avvio dell’applicazione. A tal fine modifichiamo il manifest.xml in questo modo:


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="gianluca.connect4" >

    <application android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity android:name=".start"
            android:label="@string/app_name" >
            <intent -filter>
                <action android:name="android.intent.action.MAIN"></action>
                <category android:name="android.intent.category.LAUNCHER"></category>
            </intent>
        </activity>
    </application>
</manifest>

Ridefiniamo ora il metodo onCreate() nel sorgente start.java in questo modo:

public class start extends Activity
{
    int timeout = 2000;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_start);

        new Handler().postDelayed(new Runnable()
        {
            @Override
            public void run()
            {
                Intent i = new Intent(start.this, main.class);
                startActivity(i);
                finish();
            }
        }, timeout);
    }
}

Praticamente non facciamo altro che creare un nuovo oggetto Runnable (un nuovo thread) e ne posticipiamo l’esecuzione a 2 secondi dall’evento onCreate(). L’intent che andiamo a creare chiamerà l’activity main che nel nostro caso è la prossima schermata dell’applicazione.

Avviando l’applicazione vedremo lo splash screen per 2 secondi, poi verrà lanciata l’activity main.
Il tempo di attesa impostato per lo splash screen può essere sfruttato in vari modi:

  • Caricare oggetti locali particolarmente pesanti (immagini, file audio etc..)
  • Accedere a risorse in rete e scaricare dati da backend remoti
  • Avviare, riavviare, disabilitare device locali (wifi, bluetooth etc..)
  • Nessuna operazione particolare, solo mostrare il nome dell’app, i contatti etc..