Highscore avec Firebase

J’ai eu besoin il y a peu d’ajouter un highscore à un jeu, mais j’ai trouvé assez peu de tutos expliquant comment faire.
Cet article va donc vous montrer comment le faire gratuitement avec Firebase. À noter que cet article vous présente comment utiliser une base de données NoSql. Vous pourrez donc l’adapter pour ce que vous voulez ayant besoin de stocker des données.

 

Pourquoi Firebase ?

Firebase permet de gérer une base de données NoSql avec authentification, et est poussé par Google. La limitation est à 100 personnes connectées simultanément, 1 GB de donnée, mais 10GB/mois. Bien sûr il existe des options payantes pour aller plus loin si votre application fonctionne bien ( https://firebase.google.com/pricing/ ).
Dans le cas d’un highscore on n’a pas besoin d’être connecté en continu, on peut donc gérer bien plus de personnes.

Créer un projet Firebase

Pour créer un compte Firebase, il faut avoir un compte google et vous rendre sur la console https://console.firebase.google.com. Une fois connecté, il suffit de créer un projet

L’id du projet est important pour toutes les applications qui devront communiquer avec votre Firebase.

Il est rappeler un peu partout mais si vous en avez besoin, une fois le projet ouvert vous pouvez récupérer aller le chercher dans les paramètres du projet.

Mais pour Unity on va surtout utiliser l’URL de connections qui reprend l’id du projet

Configurer Firebase

Pour se simplifier la vie, je vous propose d’ouvrir tous les droits pour moins de complexité. Activer le mode Anonyme dans les authentifications possibles. Vous pouvez activer plusieurs authentifications différentes pour votre application. Mais il faudra souvent gérer les règles de vos collections plus finement.

En parlant de collection, pour ceux qui ne connaissent pas le NoSql. On ne parle pas de tables, mais de collections, la raison étant simple, on ne doit pas ou avoir peu de relation entre deux collections. Une collection est un ensemble d’éléments, chacun d’entre eux ayant du sens un à un sous la forme d’un json. Par exemple si on créer une collection d’utilisateurs. On peut imaginer d’y mettre toutes ces données personnelles, mais aussi ses amis dans un tableau renvoyant des ids. Ses choix de configuration de l’application … ou tout données liées directement à l’utilisateur. En Sql on pourrait utiliser une table d’association pour chaque information. Si vous voulez plus d’information https://nosql.developpez.com/cours/

 

On va ajouter quelques règles pour gérer les données entrantes et sortantes. Pour ça on va aller dans Database.

Si une page vous propose de faire les premiers pas pour le Realtime Database, cliquez sur l’option pour passer à la suite

Puis dirigez-vous vers les règles de la base de données.

Afin d’être clair pour la suite, je vais me baser sur le fait que le score est fonction du niveau maximal atteint par le joueur ainsi que le temps qu’il a mis pour les faire. Ces règles sont pour valider la structure des données entrante. Pensez bien qu’une base de données NoSql n’implique pas la présence de toutes les données, mais on peut s’assurer d’avoir au moins le minimum.

{
"rules": {
    ".read": true,
    ".write": true,
    "highscores": {
      ".read": true,
      ".indexOn": ["level", "time"],
      ".write": true,
      "$highscore":{
        ".validate": "newData.hasChildren(['name', 'level', 'time', 'date'])",
        "level": {
          ".validate": "newData.isNumber() && (!data.exists() || data.val() >= newData.val())"
        },
        "name": {
          ".validate": "newData.isString() &&
                         newData.val().length <= 30"
        },
        "time": {
          ".validate": "newData.isNumber()"
        },
        "date": {
          ".validate": "newData.isNumber() &&
                          newData.val() <= now"
        }
      }
    }
}
}

Explication rapide, ici on spécifie qu’on donne à tout le monde le droit en lecture et en écriture. Pour pouvoir faire des tests selon les besoins. Bien sûr il faudra passer les premières lignes à « false » si vous passez votre projet en Release.
Ensuite on définit une collection highscores, accessible à tous en lecture. « .indexOn » met un trie par défaut lors de la récupération, trier par la propriété « level » puis « time » on reviendra dessus quand on ajoutera des scores. La collection est accessible en écriture à tout le monde, sauf qu’on va définir des limites lors de l’ajout et modification d’une donnée.

      "$highscore":{
        ".validate": "newData.hasChildren(['name', 'level', 'time', 'date'])",
        "level": {
          ".validate": "newData.isNumber() && (!data.exists() || data.val() >= newData.val())"
        },
        "name": {
          ".validate": "newData.isString() &&
                         newData.val().length <= 30"
        },
        "time": {
          ".validate": "newData.isNumber()"
        },
        "date": {
          ".validate": "newData.isNumber() &&
                          newData.val() <= now"
        }
      }

$**** est utilisé pour vérifier le contenu et « .validate » pour valider l’objet courant, newData la donnée essayant d’être ajouté et data les données existantes dans le cas d’une mise à jour. Donc sur la ligne complète, newData.hasChildren() définit qu’il faut obligatoirement avoir les propriétés name, level, time et date.

Ensuite on peut spécifier des validations pour chaque propriété.

Level doit être un nombre et s’il y avait déjà une valeur, elle doit être supérieure à l’ancienne.

Name doit être une chaine de caractère et inférieure à 30 caractères.

Time doit être un nombre

Date doit être un nombre, les dates étant géré en timestamp, ce qu’y nous permet de vérifier qu’on a pas une date dans le futur en étant inférieur à maintenant.

 

Pour aller plus loin, n’hésitez pas à lire l’aide. Avant de partir sur Unity, ajoutons quelques données.

Ajouter des données sur Firebase manuellement

L’interface n’est pas la plus simple à maitriser. Notre base de données gérant du Json, le plus simple reste d’importer ou exporter des jsons directement. Pour alimenter quelques données, téléchargez le JSON joint et allez donc dans l’onglet Données.

Les données que je vous propose sont tiré en bonne partie d’une de mes bases de données avec des ids factices mais valides 😉

{
  "highscores": {
    "id1": {
      "date": 1507904241.37306,
      "level": 10,
      "name": "Bart",
      "time": 85.5
    },
    "id2": {
      "date": 1492161216.19226,
      "level": 37,
      "name": "Ben",
      "time": 205.5
    },
    "id3": {
      "date": 1493085711.26346,
      "level": 9,
      "name": "Bepbop",
      "time": 73.5
    },
    "id4": {
      "date": 1490340104.77313,
      "level": 68,
      "name": "Shinriel",
      "time": 18.7
    }
  }
}

Vous devriez avoir quelque chose comme :

Il y a un code couleur, en rouge ce qui vient d’être supprimé, en vert ce qu’y vient d’être ajouté. Vous pourrez suivre en temps réèl vos données pendant que vos applications tournent.

Maintenant que nous avons une collection avec des données et des règles de validation, nous somme prêt à commencer la partie Unity !

Ajouter Firebase à Unity

Ayant eut plusieurs problèmes pour gérer les données, je préfère utiliser un petit asset qui fait bien le travail et simplifie grandement la gestion.

using SimpleFirebaseUnity;
using SimpleFirebaseUnity.MiniJSON;

Tout ce jouera à partir de la ligne suivante :

firebase = Firebase.CreateNew("VOTRE PROJET URL");

Vous pourrez retrouver l’url du projet dans les configurations ou directement dans la section database

Envoyer des données à Firebase

Pour pouvoir s’adapter à votre projet facilement, je vous propose quelques lignes de code. Mais le plus important est de les comprendre. De la même manière que votre score peut ne pas être basé sur le niveau maximal atteint par le joueur, vous aurez sans doute à le retravailler un peu.

Je me base sur cette structure de score

using System;

[Serializable]
public struct Score {
    public string Name;
    public int LevelCompleted;
    public int Time;
    public DateTime Date;
}
using System;
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using SimpleFirebaseUnity;
using SimpleFirebaseUnity.MiniJSON;
using UnityEngine.EventSystems;

public class ScoreActions : MonoBehaviour {

    protected Firebase firebase;

    void Start () {
        // On se connecte à notre Firebase
        firebase = Firebase.CreateNew("VOTRE PROJET URL");
        // On ajoute un score pour tester
        SubmitScore();
    }

    public void SubmitScore()
    {
        // Json de démo, pour expliquer comment on peut construire un JSON avec des valeurs, 
        //  et surtout les cas particuliers du double du timestamp
        // On génère à la main un timestamp standart pour JSON, qui malheureusement 
        //  n'existe pas en C# mono.
        var epochStart = new System.DateTime(1970, 1, 1, 8, 0, 0, System.DateTimeKind.Utc);
        var timestamp = (System.DateTime.UtcNow - epochStart).TotalSeconds;

        // Pour convertir un double en forçant le point et non la virgule vous 
        //  pouvez utiliser double.ToString("F1")
        string jsonToSend = string.Format(
            "{{ \"name\": \"{0}\", \"level\": {1}, \"time\": {2}, \"date\": {3} }}", 
            "Nom", 123, "999999.9999", timestamp);

        // On se place au niveau de la collection "highscores" qui sera créé 
        //  si elle n'existe pas
        Firebase scoresFirebase = firebase.Child("highscores");

        // On ajoute quelques callbacks savoir si tout ce passe bien ou pas.
        // Il est possible de mettre les callback sur le parent "firebase" mais 
        //  il aurait fallut demander à ce que la collection enfant hérite des 
        //  callbacks via firebase.Child("highscores", true);
        scoresFirebase.OnPushSuccess += PushOKHandler;
        scoresFirebase.OnPushFailed += PushFailHandler;

        // Enfin on push le tout, en spéficiant qu'on passe un json
        // Push(string json, bool isJson, string param = "")
        scoresFirebase.Push(jsonToSend, true);
    }
	
    void PushOKHandler(Firebase sender, DataSnapshot snapshot)
    {
        // Dans le cas où la requête a fonctionné
        Debug.Log("[OK] Push from key: " + sender.FullKey);
    }

    void PushFailHandler(Firebase sender, FirebaseError err)
    {
        // S'il y a eut un problème de connexion ou de validation
        Debug.Log("[ERR] Push from key: " + sender.FullKey + ", " + err.Message 
            + " (" + (int)err.Status + ")");
    }
}

Récupérer des données depuis Firebase

De la même manière voilà une manière pour récupérer les données depuis Firebase

using System;
using UnityEngine;
using UnityEngine.UI;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using SimpleFirebaseUnity;
using SimpleFirebaseUnity.MiniJSON;
using UnityEngine.EventSystems;

public class ScoreActions : MonoBehaviour {

    protected Firebase firebase;
    protected List<Score> scores;

    void Start () {
        // On se connecte à notre Firebase
        firebase = Firebase.CreateNew("VOTRE PROJET URL");
        // On récupère les scores pour tester
        GetScores();
    }

    public void GetScores()
    {
        // On se place au niveau de la collection "highscores"
        Firebase scoresFirebase = firebase.Child("highscores");

        // On ajoute quelques callbacks savoir si tout ce passe bien ou pas.
        scoresFirebase.OnGetSuccess += GetOKHandler;
        scoresFirebase.OnGetFailed += GetFailHandler;

        // On récupère tous les highscores, 
        // On trie par "level" mais on pourrait ne pas le faire étant spécifié dans les règles, 
        //  puis on recupère que les 10 derniers étant la seule manière de faire un trie décroisant.
        scoresFirebase.GetValue(FirebaseParam.Empty.OrderByChild("level").LimitToLast(10));
    }
	
    void GetOKHandler(Firebase sender, DataSnapshot snapshot)
    {
        // La requête a fonctionnée et on a les données demandées
        Debug.Log("[OK] Get from key: " + sender.FullKey);
        Debug.Log("[OK] Raw Json: " + snapshot.RawJson);

        // On pourrait déserialiser directement dans un objet qui matcherait.. 
        //  mais ce ne serait pas drôle, et autant montrer un petit peu plus compliqué !
        var scorelist = (Dictionary<string, object>)Json.Deserialize(snapshot.RawJson);
        scores = new List<Score>();

        foreach (KeyValuePair<string, object> json in scorelist)
        {
            var score = (Dictionary<string, object>) json.Value;

            // Venant d'un Json, tout est sous forme de string
            string timeAsString = score["time"].ToString();
            var time = Convert.ToDouble(timeAsString);
            var date = (double)score["date"];
            var level = (long)score["level"];
            string name = score["name"].ToString();

            // On transforme depuis le timestamp vers un objet date à l'inverse 
            //  de tout à l'heure
            System.DateTime dtDateTime = new DateTime(1970, 1, 1, 0, 0, 0, 0);
            dtDateTime = dtDateTime.AddSeconds(date).ToUniversalTime();

            // On rassemble le tout dans un même object
            // La méthode unchecked() étant utilisé pour ignore les dépassements, 
            //  si un double est trop grand pour le int.
            // voir https://docs.microsoft.com/fr-fr/dotnet/csharp/language-reference/keywords/unchecked
            scores.Add(
                new Score()
                {
                    Time = unchecked((int)time),
                    Date = dtDateTime,
                    LevelCompleted = unchecked((int)level),
                    Name = name
                });
        }
    }

    void GetFailHandler(Firebase sender, FirebaseError err)
    {
        // S'il y a eut un problème de connexion ou de validation
        Debug.Log("[ERR] Get from key: " + sender.FullKey + ",  " + err.Message 
            + " (" + (int)err.Status + ")");
    }
}

N’hésitez pas à copier le code dans votre IDE pour mieux le lire.
Voilà à vous d’adapter le code. Vous avez la gestion d’un INT, d’une Date et d’un String. Je pense qu’on a fait le tour de ce qu’y pouvait être utilisé dans un Json. Si vous avez besoin d’un objet plus complexe, n’hésitez pas à sérialiser votre objet.

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *