Ajout du multilingue dans votre jeux avec Unity 3D

Il y a plusieurs déjà 4 ans, j’écrivais un article pour expliquer comment rendre son jeu multilingue avec Unity. Je reviens aujourd’hui avec une nouvelle méthode, vraiment plus simple ! Effectivement ma précédente méthode exploitait un fichier JSON avec le parser SimpleJSON. Aujourd’hui nous allons plutôt voir comment utiliser un simple fichier ini avec un peu de C# ! Ce fichier pourra ainsi être partagé et lu par n’importe quel être humain de cette planète !

Le fichier ini

Déjà qu’est ce qu’un fichier ini ? C’est un fichier qui sert à stocker des paramètres sous forme de clé/valeur avec comme symbole de délimitation, le signe égal. Vous voulez un exemple ?

Hello = Bonjour
OK = Valider
Cancel = Annuler

Il n’y a vraiment rien de plus simple, vous pouvez même ajouter des bloc de section pour structurer votre fichier.

[Main Menu]
Hello = Bonjour
OK = Valider
Cancel = Annuler

; Voici un commentaire !
[Level 1]
Level1Name = Une course sans limite
Level1Description = Une description...

Avantages

  • Très facile à parser
  • Très rapide à mettre en oeuvre
  • Facile à utiliser
  • Fichiers de traduction lisibles et facilement partageable (pour traducteur)
  • Convient à pas mal de cas

Désavantages

  • C’est un fichier texte et il faut le parser, à la différence d’une sérialisation binaire
  • Peut poser des soucis de mémoire si le contenu est trop important (à partir de plusieurs centaines de mégas)
  • Pas adapté à tous les types de jeux, par exemple tous les jeux où il y a beaucoup de textes (RPG)

Avec ces avantages et inconvénients en tête, nous pouvons désormais nous mettre à l’oeuvre pour écrire ce système de traduction !

Ecrire le gestionnaire

La première étape est de créer un fichier source nommé Translation.cs. Le manager sera purement statique. J’attire votre attention sur le code qui suis, il est en C# 6.0. Vous pouvez activer la prise en charge de C# 6.0 à partir de Unity 2017 en suivant ce lien. Si vous utilisez une très vielle version d’Unity il faudra remplacer les interpolations de chaînes par une fonction string.Format.

using UnityEngine;
using System.Collections.Generic;
using System;
using System.IO;

public sealed class Translation : MonoBehaviour
{
    public static readonly SystemLanguage[] Languages = { SystemLanguage.English, SystemLanguage.French };
    private static Dictionary<string, string> Translations = null;

#if UNITY_EDITOR
    private static bool d_OverrideLanguage = false;
    private static SystemLanguage d_Language = SystemLanguage.English;
#endif

    private static void CheckInstance()
    {
    // It's already initialized.
    if (Translations != null)
      return;

    Translations = new Dictionary<string, string>(); 

    // Get the current language.
    var lang = Application.systemLanguage;

#if UNITY_EDITOR
    // Override the current language for testing purpose.
    if (d_OverrideLanguage)
      lang = d_Language;
#endif

    // Check if the current language is supported.
    // Otherwise use the first language as default.
    if (Array.IndexOf<SystemLanguage>(Languages, lang) == -1)
      lang = Languages[0];

    // Load and parse the translation file from the Resources folder.
    var data = Resources.Load<TextAsset>($"Translations/{lang}");
    if (data != null)
      ParseFile(data.text);
    }

    // Returns the translation for this key.
    public static string Get(string key)
    {
    CheckInstance();

        if (Translations.ContainsKey(key))
            return Translations[key];

#if UNITY_EDITOR
        Debug.Log($"The key {key} is missing");
#endif

        return key;
    }

    public static void ParseFile(string data)
    {
        using (var stream = new StringReader(data))
        {
            var line = stream.ReadLine();
            var temp = new string[2];
            var key = string.Empty;
            var value = string.Empty;

            while (line != null)
            {
                if (line.StartsWith(";") || line.StartsWith("["))
                {
                    line = stream.ReadLine();
                    continue;
                }

                temp = line.Split('=');

                if (temp.Length == 2)
                {
                    key = temp[0].Trim();
                    value = temp[1].Trim();

                    if (value == string.Empty)
            continue;

          if (Translations.ContainsKey(key))
            Translations[key] = value;
          else
            Translations.Add(key, value);
                }

                line = stream.ReadLine();
            }
        }
    }
}

J’ai ajouté quelques commentaires mais comme vous pouvez le constater il n’y a vraiment rien de bien compliqué sur ce code.

En premier lieu vous remarquerez que j’ai volontairement bridé le manager à deux langues, à savoir l’Anglais et le Français. Vous êtes libre d’en ajouter d’autres rassurez vous. Le principe est simple, on va avoir un fichier par traduction qui sera stocké dans le dossier Assets/Resources/Translations/. Le nom du fichier devra être composé d’un nom de l’énumération SystemLanguage, avec comme extension .txt. Dans notre cas cela fait French.txt et English.txt. Si la langue système de l’utilisateur est le Chinois, alors on basculera sur la première langue du tableau Languages, à savoir l’Anglais ici. Encore une fois, vous êtes vraiment libre de choisir la langue par défaut. Vous remarquerez qu’il y a deux variables pour faire du debug, elles permettent de surcharger la langue utilisée afin que vous puissiez faire vos propre tests.

Ensuite nous avons une petite fonction qui parse le fichier ligne par ligne et qui ne prend pas en compte les commentaires (;) et les sections ([). Passons à l’utilisation de ce manager dans un script, ensuite nous verrons comment traduire des UI.

Utilisation du manager

Assurez vous d’avoir crée deux fichiers French.txt et English.txt dans Assets/Resources/Translations/. Puis ouvrez les pour y copier le contenu suivant pour respectivement les fichier Français et Anglais.

Hello = Bonjour
OK = Valider
Cancel = Annuler
Hello = 
OK = 
Cancel =

Oui le fichier Anglais est vide car les clés que nous utilisons sont suffisamment explicites comme ça 🙂 Si vous avez des clés plus compliqués, par exemple de longs textes, il est recommandé de créer une clé courte comme menu.value1 = My first value from the menu et y attribuer la valeur désirée.

Maintenant depuis n’importe quel script vous pouvez essayer le code suivant.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Test : MonoBehaviour 
{
  private void Start () 
  {
    var hello = Translation.Get("Hello");
    Debug.Log(hello);
  }
}

Ce code produira le contenu suivant :

Affichage de la traduction dans la console
Affichage de la traduction dans la console

Traduire vos UI

Nous allons voir comment traduire vos UI maintenant avec un script terriblement compliqué…

using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Text))]
public sealed class TranslateText : MonoBehaviour
{
  [SerializeField]
  private string m_Key = null;

  private void Start()
  {
    var text = GetComponent<Text>();
    text.text = Translation.Get(m_Key != string.Empty ? m_Key : text.text);
    Destroy(this);
  }
}

Sont fonctionnement est on ne peut plus simple, il suffit de l’ajouter sur tous les GameObjects de type UnityEngine.UI.Text et la traduction sera automatique. Si le champ de texte à déjà une valeur et que la clé de traduction est vide, alors le code essayera d’utiliser cette valeur comme clé de traduction, sinon la clé de traduction donnée sera utilisée. Vous pouvez modifier le code pour une prise en charge de TextMeshPro aussi 😉

Mise en place du script de traduction
Mise en place du script de traduction
Résultat d'interface graphique traduite
Résultat d’interface graphique traduite

Conclusion

Nous venons de voir comment internationaliser très rapidement un jeu avec Unity3D. Evidemment il existe des solutions super complètes qui vont utiliser un fichier csv ou autre, mais cette solution est à mon humble avis, celle qui a le meilleur ratio facilité/efficacité/rapidité/performance. Effectivement parser un fichier n’est pas très compliqué. Cela va cependant le devenir si vous avez beaucoup de textes. Là il faudra modifier ce système ou en utiliser un autre. Mais dans pas mal de cas ça fonctionnera très bien. D’ailleurs toutes mes productions perso (The Lost Maze, M.A.R.S. Extraction, GunSpinning VR) et pro (Daedalis) l’utilisent ! Sachez qu’il aurait été possible d’utiliser un ScriptableObject, nous aurions eu l’avantage d’un chargement un peu plus rapide (mais sérieusement, à moins de fonctionner sur un Pentium II…) mais l’impossibilité de partager le fichier de traduction à une autre personne (ou alors en livrant le projet).

Enfin sachez que vous pouvez modifier légèrement ce système pour y stocker des paramètres de configuration…

Vous pouvez télécharger les sources du projet à partir du dépôt github du blog !