Ajoutez des succès à votre univers

succes

Les succès (achievements en anglais) se sont installés petit à petit dans le paysage vidéo ludique. Mais qu’est-ce que c’est qu’un succès exactement ? Un succès, c’est un marqueur, badge ou autre élément visuel acquis par le joueur en effectuant des actions précises. Par exemple : vaincre 1000 ennemis, ramasser tous les objets cachés… ou plus ironique : mourir 100 fois.

Un succès est associé à une action précise. Ce n’est pas quelque chose de vague, les conditions d’obtention doivent être claires et mesurables, au même titre que le calcul du score, de la perte de points de vies, etc.

Ce tutoriel est d’une difficulté élevée et demande de bonnes connaissances en programmation orientée objet.

Voyons ensemble pourquoi il est intéressant de les utiliser et comment les intégrer dans votre jeu.

Disclaimer

Cet article a été originalement écrit le 13/06/2014 sur mon précédent blog. Certaines informations présentées ici peuvent donc ne plus fonctionner telles quelles.

Pourquoi utiliser des succès ?

C’est vrai ça, pourquoi j’irais m’embêter à mettre des succès, mon jeu est déjà génial ! Hum… Tout d’abord, parce que les joueurs en réclament. Eh oui, les joueurs veulent des succès. Certains pour se donner des objectifs supplémentaires, d’autre pour se la péter devant leurs potes… Quand à ceux qui s’en fichent un peu, ils sont quand même contents d’en débloquer un de temps en temps ! C’est gratifiant de débloquer un succès.

Attention tout de même. Les succès, c’est bien, mais avec modération. Il ne faut pas dénaturer le sens du terme. Un succès, c’est tout de même un coup d’éclat, une belle réussite. Il faut avoir fait quelque chose d’important pour débloquer un succès. Sinon, l’intérêt suscité par le déblocage d’un succès sera moindre, voire inexistant. Pire même, cela peut froisser le joueur. Si vous lui jetez un succès à la figure dès qu’il fait quoi que ce soit, il va finir par croire que vous le prenez pour une bille et va continuer de jouer avec cette sensation désagréable d’être sous estimé.

Un de mes amis, Roger Costecalde, a été confronté une fois à ce type de problème sur le jeu Worms Crazy Golf. Lors de sa première partie, il a réussi à mettre la balle dans le trou en un coup. Vlan, il a débloqué 5 succès d’un coup :

  1. Premier tour (Terminez un trou)
  2. Cui-cui (Faites un birdie)
  3. Maîtriser rapidement (Réalisez un par ou mieux)
  4. Vous frappez vous marquez (Réalisez tous les objectifs pour un trou)
  5. Coups faciles (Faites un coup roulé sur le green)

Tout ça pour une seule action. Je vous laisse imaginer sa réaction :

Si vous ne savez pas trop si une action mérite un succès ou non, demandez-vous simplement si cette action est difficile à réaliser. Ce qui est facile n’est pas un succès. Vaincre un ennemi est simple, en vaincre 1000 l’est beaucoup moins. Dans ce cas là, ce n’est pas le fait de vaincre un ennemi qui est récompensé, mais le fait d’avoir eu la ténacité de continuer jusqu’à arriver à 1000. La nuance peut se situer dans la quantité si l’action est facile.

Mais trêve de théorie. Passons à la préparation.

Préparation

Grosso modo, il va nous falloir un gestionnaire. Ce gestionnaire va centraliser la gestion des succès dans le jeu. Chaque succès aura un type, mais plusieurs succès pourront avoir le même type. Un type représente une action à réaliser pour déclencher la routine de détection des succès. La raison est simple : vous pouvez avoir plusieurs niveaux de succès basés sur une même action :

  • Tuer 100 monstres
  • Tuer 1.000 monstres
  • Tuer 10.000 monstres

Ces 3 succès sont basés sur la même information (le nombre de monstres tués), ils auront donc tous le même type que l’on pourrait nommer « tuer un monstre ». Par simplicité, ces types seront représentés par une énumération, mais nous verrons cela plus tard.

De par cette structure, il va falloir mettre en place plusieurs choses, une seule liste / dictionnaire ne sera pas suffisant. Notez également que toute la gestion des succès sera basée sur des compteurs, y compris pour des actions de type « one shot », par exemple vaincre un boss. Ce fonctionnement est générique et peut être adapté à tous les besoins.

Note : ces compteurs seront des entiers. Si vous manipulez des nombres flottants dans votre logique de jeu, vous ne pourrez pas les stocker tels quels (pour un score ou un nombre de points de dommage par exemple), vous devrez faire une petite conversion.

Cette précision étant faite, entrons dans le vif du sujet. Pour stocker les informations de succès, il va nous falloir un conteneur : Achievement.

public class Achievement
{
 public int CountToUnlock { get; set; }

 public bool IsUnlocked { get; set; }

 public string Name { get; set; }

 public string Description { get; set; }

 public bool Hidden { get; set; }
}

C’est un simple conteneur d’informations, je n’ai mis que des propriété pour aller plus vite. Nous n’auront pas besoin de plus de toute façon.

Plusieurs attributs :

  • CountToUnlock : le nombre de fois que le gestionnaire devra constater l’action pour débloquer le succès.
  • IsUnlocked : drapeau mis à vrai dès que le succès est débloqué
  • Name : le nom du succès (qui pourra être utilisé en affichage plus tard)
  • Description : la description du succès (idem, pourra être utilisée pour affichage plus tard)
  • Hidden : Indique si le succès est invisible ou non. Un succès caché ne sera pas visible par le joueur dans la liste des succès tant qu’il ne l’aura pas débloqué.

Certains succès peuvent être cachés pour différentes raison. Par exemple pour ne pas spoiler l’histoire en donnant le nom d’un boss avant l’heure, ou bien pour des actions cachée en jeu, forçant le joueur à explorer le contenu et à découvrir les succès « par hasard ». Voici un exemple de succès caché dans Deus-Ex Human Revolution (petit spoil) :

dxhr_hidden

Notre conteneur étant prêt, attaquons nous au plus gros : le manager.

Le gestionnaire

Attaquons la partie sous marine de l’iceberg. Il nous faut créer maintenant le gestionnaire. On va y aller petit à petit (parce qu’il y en a du code à écrire !). Le but est de créer un moteur de succès qui pourra être adapté selon vos besoins, quel que soit le projet. C’est plus complexe, mais plus facile à réutiliser par la suite.

Encore un tout petit peu de théorie et on pourra commencer à coder ! Promis ! Comme le but est de créer un moteur réutilisable facilement et (relativement) simplement, il faut prendre en compte l’affichage. Eh oui, quand le joueur débloque un succès, il faut le prévenir. Mais s’occuper de ça dans le moteur n’est pas une bonne chose. C’est une mauvaise chose car cela rend le moteur dépendant de l’affichage et des outils utilisés. Il nous faut donc externaliser la routine d’affichage.

L’externalisation de la routine d’affichage se fera via une interface que voilà :

public interface IAchievementDisplay
{
  void ShowAchievement(Achievement achievement);
}

Le moteur s’appuiera sur cette définition et ce sera ensuite à vous de créer votre afficheur personnalisé en utilisant cette interface. De cette manière, on conserve l’indépendance du moteur de succès vis à vis de l’affichage.

Aller on se lance. Créons notre gestionnaire avec les attributs dont nous auront besoin :

public abstract class AchievementEngine<T>
{
  protected Dictionary<T, int> _achievementCounter;

  protected Dictionary<T, List<Achievement>> _achievements;

  protected int _achievementCount;

  protected int _unlockedAchievements;

  protected IAchievementDisplay _display;
}

Vous remarquerez tout d’abord deux choses : le moteur est abstrait et n’étend pas un MonoBehaviour. Vous devrez étendre cette classe pour créer votre gestionnaire personnalisé, puis ensuite le stocker (comme bon vous semble) dans votre logique que jeu, toujours pour des raisons de flexibilité et de réutilisabilité. Ensuite vous verrez également que le gestionnaire est paramétré d’un T. Ce type est l’énumération personnalisée contenant vos types de succès. Si cette structuration vous semble tirée par les cheveux, laissez-vous porter jusqu’à la fin, vous comprendrez pourquoi on fait tout ça.

Expliquons les attributs maintenant :

  • _achievementCounter : Ce dictionnaire rassemble les compteurs par type d’action. Pour reprendre l’exemple précédent, « nombre de monstres tués ». Ces compteurs serviront de référence pour le déblocage des succès.
  • _achievements : Ce dictionnaire rassemble les succès par type d’action. Chaque type est associé à une liste de succès comme expliqué plus haut. Avec ce dictionnaire, nous allons pouvoir savoir quels succès sont associés à quelle action et déterminer s’ils sont débloqués lors d’une nouvelle action.
  • _achievementCount : Ce compteur sera en lecture seule et accessible via une propriété. Il donnera le nombre total de succès définis
  • _unlockedAchievements : Ce compteur sera en lecture seule également et accessible via une propriété. Il donnera le total de succès débloqués par le joueur.
  • _display : L’instance de IAchievementDisplay qui s’occupera de l’affichage des succès débloqués.

Encore une fois (je me répète beaucoup), pour conserver la généricité du moteur de succès, certaines méthodes spécifiques seront déclarées abstraites et devront être redéfinies dans votre implémentation :

protected abstract void InitializeAchievements();

protected abstract void LoadPlayerData();

protected abstract void SavePlayerData();

Le constructeur de notre gestionnaire prendra en paramètre l’instance de l’afficheur (ou null si vous ne voulez pas en utiliser pour le moment) :

public AchievementEngine(IAchievementDisplay display)
{
  _display = display;

  _achievementCounter = new Dictionary<T, int>();
  _achievements = new Dictionary<T, List<Achievement>>();

  InitializeAchievements();

  LoadPlayerAchievements();
}

On initialise les variables… on initialise les succès (voir ci dessus pour la déclaration) et ensuite on charge les succès du joueur. Cette dernière manipulation n’est pas nécessaire dans le constructeur. Vous pouvez très bien créer le gestionnaire avant d’avoir accès aux succès du joueur. Dans ce cas, commentez ou retirez cette ligne, vous n’aurez qu’à l’appeler vous même plus tard. LoadPlayerAchievements est un raccourcis pour effectuer 2 choses : charger les succès et recalculer le compteur de succès. Simple et efficace. Ainsi, dans votre implémentation, vous n’aurez qu’à vous soucier de la déclaration des succès.

public void LoadPlayerAchievements()
{
  LoadPlayerData();

  UpdateAchievementCounters();
}

Vous suivez toujours ? Il y en a encore à écrire… Passons maintenant à la mise à jour des compteurs de succès. Cette méthode sera utilisée uniquement pour initialiser les compteurs après le chargement des succès du joueur… mais vous pourriez en avoir besoin dans un autre cas de figure. C’est pour cela que le traitement est découpé dans une méthode à part entière. Le code !

protected void UpdateAchievementCounters()
{
  _achievementCount = 0;
  _unlockedAchievements = 0;
  foreach(var list in _achievements.Values)
  {
    foreach(var achievement in list)
    {
      _achievementCount++;
      if(achievement.IsUnlocked)
      {
        _unlockedAchievements++;
      }
    }
  }
}

C’est simple : on parcourt les entrées du dictionnaire de succès, et chaque liste de chaque entrée. A partir de là on met à jours les compteurs.

Bon… On a notre constructeur, le chargement des succès et l’initialisation des compteurs associés. On n’y est pas encore, mais on s’en approche. Maintenant que toute cette structure est en place, nous allons pouvoir attaquer le coeur du système : la détection des actions qui vont incrémenter les compteurs, et potentiellement débloquer des succès.

protected virtual void UpdateUnlockedAchievements(T type)
{
    List<achievement> achievementList = _achievements [type];
    foreach(var achievement in achievementList)
    {
        if(!achievement.IsUnlocked)
        {
            if(_achievementCounter[type] >= achievement.CountToUnlock)
            {
                achievement.IsUnlocked = true;
                ShowAchievement(achievement);
            }
        }
    }
}

Alors… Tout d’abord cette méthode ne prend qu’un seul paramètre : le type d’action qui vient d’être enregistré. C’est un parti pris afin de ne pas avoir à parcourir l’ensemble des succès. Mais dans certains cas, cela ne vous conviendra pas. Comme par exemple un succès qui se déclenche uniquement si vous avez réalisé un certain nombre… d’autres succès. C’est pour ça qu’elle est déclarée en virtual afin d’être surchargeable si besoin. Le contenu maintenant. On parcourt la liste des succès de ce type et si le succès n’est pas débloqué, on vérifie le compteur, tout simplement. Si le compteur actuel dépasse la valeur définie, c’est que le succès est débloqué. On appelle la méthode d’affichage du succès :

protected virtual void ShowAchievement(Achievement achievement)
{
    if(_display != null)
    {
        _display.ShowAchievement(achievement);
    }
    else
    {
        Debug.Log("Achievement unlocked : " + achievement.Name);
    }
}

Si un gestionnaire d’affichage est défini, on lui envoie l’info… sinon on balance un log dans la console. C’est une méthode par défaut qui peut être utilisée telle quelle simplement. Mais encore une fois, dans l’optique de pouvoir effectuer des traitements particuliers avant l’affichage, cette méthode est déclarée virtual.

Mise en pratique

Tout ce que nous avons fait jusqu’à présent n’est pas encore utilisable. Eh ouais c’est long… en revanche, ce qui a été fait plus haut est réutilisable tel quel dans n’importe lequel de vos projets. Et ça c’est cool. Vous n’aurez plus qu’à paramétrer une toute petite partie du moteur pour avoir votre gestion de succès. Voyons voir ce qu’il vous reste à faire :

  • Créer l’énumération qui contiendra les types d’actions
  • Créer votre moteur personnalisé en étendant AchievementEngine.
  • Créer votre gestionnaire de rendu

Pour la création de l’énumération, je vous laisse vous débrouiller. Pour le gestionnaire de rendu, comme c’est quelque chose de complètement lié à votre projet, je vous laisse vous débrouiller également. Le moteur notifiera votre gestionnaire d’affichage quand il devra faire quelque chose. A vous de jouer pour faire un rendu cool. Je vais par contre vous montrer un exemple de surcharge du moteur comme base de travail :

 
public class CustomAchievementEngine : AchievementEngine<AchievementType>
{
 public CustomAchievementEngine(IAchievementDisplay display) : base(display) { }

 protected override void InitializeAchievements()
 {
  var list = new List<Achievement> ();
  list.Add (new Achievement(){CountToUnlock = 10, IsUnlocked = false, Name = "Killer", Description="You killed 10 people", Hidden = false});
  list.Add (new Achievement(){CountToUnlock = 100, IsUnlocked = false, Name = "Insane", Description="You killed 100 people !", Hidden = false});
  _achievements [AchievementType.Kills] = list;
 }

 protected override void LoadPlayerData() { }
 protected override void SavePlayerData() { }
}

Notre moteur personnalisé étend AchievementEngine. Notez que l’énumération que j’utilise ici pour catégoriser mes actions s’appelle AchievementType. Le constructeur est vide, je me contente de faire passer au constructeur parent l’instance du gestionnaire d’affichage. Les méthodes de chargement et de sauvegarde ne nous intéresse pas ici. Là encore, c’est quelque chose de propre à vos projets. On aurait pu prévoir un stockage générique… mais je crois que le tutoriel commence à être suffisamment chargé comme ça. 🙂

La méthode InitializeAchievements créer une liste de succès pour le type Kills. Deux succès plus exactement, le premier débloqué à 10 occurrences, le second à 100. Et c’est tout. Le reste est géré génériquement par le moteur. Il notifiera automatiquement le gestionnaire d’affichage quand un pallier sera atteint.

Voilà. La structure est là, vous pouvez maintenant gérer des succès de manière relativement simple et générique. Comme c’est un sujet assez complexe, je vous ai préparé un projet d’exemple qui utilise tout ce qui est présenté ici.

Crédits : ce tutoriel est basé sur l’article de Michael Adaixo et adapté par moi même.


Merci à Yannick Comte pour la relecture.

Advertisements

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s