3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Unity] CSVファイルを用いたローカライズ対応

Last updated at Posted at 2024-11-02

六回目の投稿です。よろしくお願いします。

目次

  1. 概要
  2. 実装概要
  3. 実装方法
  4. 最後に

概要

解説内容 :
  1. CSVファイルの行ごとの読み込み方法
  2. Idを用いた文言データの検索方法
  3. ローカライズ対応 (多言語対応)
利点 :
  1. コードと文言データが分離され、データの参照性と改変容易性が向上する。
  2. データを編集ソフト(例 : グーグルスプレッドシート)を用いて管理することで、大人数での管理が容易となる。
  3. 最終的には、Unity上での変更点はCSVファイルの更新のみとなり、変更点が少ないことから、Gitでの競合リスクを低減できる。
今回使用するCSVファイル :

image.png
(例 : 言語がenのときに、idに1を指定すると、"number"が表示される。)

実装概要

処理の概略図
image.png

1. CSVファイルの読み込み方法の概要

  1. CSVファイルの内容を読み込むためのスクリプトファイル(CsvReader.cs)を作成する。
  2. CSVファイル指定時に、StringReaderクラスを用いて上から順に行を読み込み、string配列に保存していく。
  3. ローカライズ処理用の、CsvReaderクラスを継承したクラス(WordingReader.cs)を作成する。
  • 上記動作をエディタ上で完結させることにより、アプリ側での負担を減らす。

2. Idを用いたデータの検索方法の概要

  1. CsvReader.csで、FindDataWithId(string)というidから文言を取得する処理を記述する。
  2. テキストUIを設置し、文言を適用するスクリプトファイル(LocalizeText.cs)を作成する。
  3. 動作確認用に、Idの更新に関するエディタ拡張を実装する。

3. ローカライズ対応方法の概要

  1. WordingReader.csで、言語タイプを設定し、言語変更時に指定したイベントが呼ばれるように、言語変更処理を記述する。
  2. LocalizeText.csで、言語変更時に、言語に応じた文言が適用されるように処理を記述する。

実装方法

1. CSVファイルの読み込み方法の実装

1.1. CsvReader.csの作成

以下のように、CsvReader.csを作成する :

CsvReader.cs (tap)
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.IO;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace App.ReadCsv
{
    public class CsvReader : MonoBehaviour
    {
        [SerializeField]
        private TextAsset csv;
        [SerializeField, HideInInspector]
        private List<string[]> rawReadDatas = new List<string[]>(); // エディタ上で設定済み
        
#if UNITY_EDITOR
        private void OnValidate()
        {
            if (!csv) { return; }

            var path = AssetDatabase.GetAssetPath(csv);
            if (!path.Contains(".csv"))
            {
                csv = null;
                rawReadDatas.Clear();
                Debug.LogError("CSVファイルを指定してください。");
                return;
            }
            rawReadDatas = Read(csv.text);
        }

        /// <summary>
        /// データの読み込み
        /// </summary>
        private static List<string[]> Read(string text)
        {
            var readDatas = new List<string[]>();
            if (string.IsNullOrEmpty(text)) { return readDatas; }

            var reader = new StringReader(text);
            while (reader.Peek() != -1) // 読み込む文字がなくなるまで実行
            {
                var line = reader.ReadLine();   // 1行づつ読み込む
                readDatas.Add(line.Split(",")); // 行を分割して保存
            }
            Debug.Log($"Read CSV :\n {string.Join("\n", readDatas.Select(e => string.Join(", ", e)))}");

            return readDatas;
        }
#endif
    }
}
  • #if UNITY_EDITOR {処理} #endif : {処理}がUnityエディタでしか実行されないようにする。
  • [SerializeField, HideInInspector] : インスペクター上では表示されないように、Unity内部に保存されるようにしている。
  • OnValidate() : ファイルを指定した際などに呼ばれる。
  • ".csv"という文字列が含まれている場合にしか許容しない : CSVファイルしか受け付けないようにする。
  • StringReaderクラス : 行がなくなるまで、上から順に行を読み込み、保存していっている。
1.2. WordingReader.csの作成

以下のように、WordingReader.csを作成し、空のゲームオブジェクトに付与し、CSVファイルを適用させる :

WordingReader.cs (tap)
namespace App.ReadCsv.Localize
{
    public class WordingReader : CsvReader
    {
        
    }
}

image.png

2. Idを用いたデータの検索方法の実装

2.1. CsvReader.csに、Idから文言を取得する処理を記述

以下のように、CsvReader.csに追記する :

CsvReader.cs (tap)
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System.IO;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace App.ReadCsv
{
    public class CsvReader : MonoBehaviour
    {
        [SerializeField]
        private TextAsset csv;
        [SerializeField, HideInInspector]
        private List<string[]> rawReadDatas = new List<string[]>(); // エディタ上で設定済み

+       private static List<string[]> readDatas = new List<string[]>();
+      
+       private void Awake()
+       {
+           if (readDatas.Count > 0) { return; }
+       
+           readDatas = rawReadDatas;
+       }
+
+       /// <summary>
+       /// idを用いて、データを検索
+       /// </summary>
+       public static string[] FindDataWithId(string id)
+       {
+           var lineData = readDatas.FirstOrDefault(e => e[0] == id);
+           if (lineData == default) { return lineData; }
+           
+           var lineDataExceptId = new string[lineData.Length - 1];
+           for (var i = 0; i < lineData.Length - 1; i++) // 1列目を取り除く
+           {
+               lineDataExceptId[i] = lineData[i + 1];
+           }
+           return lineDataExceptId;
+       }
        
#if UNITY_EDITOR
        private void OnValidate()
        {
            if (!csv) { return; }

            var path = AssetDatabase.GetAssetPath(csv);
            if (!path.Contains(".csv"))
            {
                csv = null;
                rawReadDatas.Clear();
                Debug.LogError("CSVファイルを指定してください。");
                return;
            }
            rawReadDatas = Read(csv.text);
        }

        /// <summary>
        /// データの読み込み
        /// </summary>
        private static List<string[]> Read(string text)
        {
            var readDatas = new List<string[]>();
            if (string.IsNullOrEmpty(text)) { return readDatas; }

            var reader = new StringReader(text);
            while (reader.Peek() != -1) // 読み込む文字がなくなるまで実行
            {
                var line = reader.ReadLine();   // 1行づつ読み込む
                readDatas.Add(line.Split(",")); // 行を分割して保存
            }
            Debug.Log($"Read CSV :\n {string.Join("\n", readDatas.Select(e => string.Join(", ", e)))}");

            return readDatas;
        }
#endif
    }
}
  • readDatas : データをstaticとして運用することでシーン間で共有され、かつ、FindDataWithId()をstatic関数にでき、運用しやすくなる。
  • readDatas.FirstOrDefault(e => e[0] == id) : 読み込んだデータの1行目の1列目が"id"という文字列であるかどうかを確認している。(今回使用したCSVファイルの1行目は、"id,jp,en"なので、満たしている。)
  • FindDataWithId()では、1列目(id)が取り除かれた行(JpとEnの文言)のみが返される。
2.2. テキストUI(Legacy)を設置し、LocalizeText.csを作成

テキストUIを設置し、以下のように、LocalizeText.csを作成し、テキストに付与する :

LocalizeText.cs (tap)
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;

namespace App.ReadCsv.Localize
{
    [RequireComponent(typeof(Text))] // Textコンポーネントを自動で付与
    public class LocalizeText : MonoBehaviour
    {
        public string Id // idの取得・適用
        {
            get => id;
            set
            {
                id = value;
                onIdChanged?.Invoke();
            }
        }
        [SerializeField, Header("文言ID")]
        private string id;

        private Text text;

        private readonly UnityEvent onIdChanged = new UnityEvent();

        private void Awake()
        {
            text = GetComponent<Text>();
        }

        private void Start()
        {
            onIdChanged.AddListener(UpdateText);
        }

        private void OnEnable()
        {
            UpdateText();
        }

        private void UpdateText()
        {
            var wordings = CsvReader.FindDataWithId(id);
            if (wordings == default) 
            {
#if UNITY_EDITOR
                Debug.LogError("LocalizeText : 存在しないidです。");
#endif
                return; 
            }
            text.text = wordings[0]; // 0 : Jp, 1 : En
        }
    }
}
  • CsvReader.FindDataWithId(id) : インスペクター上で指定したIdに応じた文言を取得
  • UnityEvent onIdChanged : UpdateText()を登録しておくことで、Idが変更された際に、自動でテキストが更新されるように、Setter (string Id { set => ...} )を定義している。

この時点で、テキストに付与したLocalizeText.idにidを指定することで、日本語の文言が表示されるようになっている。(例 : idに1を指定すると、テキストに"番号"と表示される。)

2.3. LocalizeText.csに、Idの更新に関するエディタ拡張を実装

以下のように、LocalizeText.csに追記する :

LocalizeText.cs (tap)
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
+#if UNITY_EDITOR
+using UnityEditor;
+#endif

namespace App.ReadCsv.Localize
{
    [RequireComponent(typeof(Text))]
    public class LocalizeText : MonoBehaviour
    {
        public string Id
        {
            get => id;
            set
            {
                id = value;
                onIdChanged?.Invoke();
            }
        }
        [SerializeField, Header("文言ID")]
        private string id;

        private Text text;

        private readonly UnityEvent onIdChanged = new UnityEvent();

        private void Awake()
        {
            text = GetComponent<Text>();
        }

        private void Start()
        {
            onIdChanged.AddListener(UpdateText);
        }

        private void OnEnable()
        {
            UpdateText();
        }

        private void UpdateText()
        {
            var wordings = CsvReader.FindDataWithId(id);
            if (wordings == default) 
            {
#if UNITY_EDITOR
                Debug.LogError("LocalizeText : 存在しないidです。");
#endif
                return; 
            }
            text.text = wordings[0]; // 0 : Jp, 1 : En
        }
    }

+#if UNITY_EDITOR
+   [CustomEditor(typeof(LocalizeText))]
+   public class LocalizeTextEditor : Editor
+   {
+       private string id;
+
+       public override void OnInspectorGUI()
+       {
+           base.OnInspectorGUI(); // LocalizeTextクラスでのインスペクター上での記述を消さないようにする。
+
+           GUILayout.Space(15);
+
+           using (new EditorGUILayout.HorizontalScope())
+           {
+               id = EditorGUILayout.TextField(id); // id入力欄
+
+               GUILayout.Space(5);
+
+               if (GUILayout.Button("Idを更新"))
+               {
+                   var creator = (LocalizeText)target; // LocalizeTextクラスの取得
+                   creator.Id = id;
+               }
+           }
+       }
+   }
+#endif
}

すると、エディタ上にIdの更新ボタンが出現するので、ゲームを実行し、Idを入力・更新する :
image.png

3. ローカライズ対応方法の実装

3.1. WordingReader.csに、言語タイプを設定し、言語変更時に指定したイベントが呼ばれるように、言語変更処理を記述

以下のように、WordingReader.csに追記する :

WordingReader.cs (tap)
+using UnityEngine;
+using UnityEngine.Events;
+
namespace App.ReadCsv.Localize
{
    public class WordingReader : CsvReader
    {
+       public enum LanguageType
+       {
+           Jp = 0,
+           En,
+       }
+
+       public static LanguageType CurrentLanguageType
+       {
+           get => currentLanguageType;
+           set
+           {
+               currentLanguageType = value;
+               OnLanguageSwitched?.Invoke();
+#if UNITY_EDITOR
+               Debug.Log($"WordingReader : 言語が{value}に変更されました。");
+#endif
+           }
+       }
+       private static LanguageType currentLanguageType = LanguageType.Jp;
+
+       public static UnityEvent OnLanguageSwitched { get; set; } = new UnityEvent(); // 言語変更時に呼ばれるイベント
+
+       public void SwitchNextLanguage()
+       {
+           var length = System.Enum.GetValues(typeof(LanguageType)).Length;
+           CurrentLanguageType = (LanguageType)(((int)CurrentLanguageType + 1) % length);
+       }
+
+       private void Update()
+       {
+#if UNITY_EDITOR
+           if (Input.GetKeyDown(KeyCode.L))
+           {
+               SwitchNextLanguage();
+           }
+#endif
        }
    }
}
  • Jp = 0 : FindDataWithId()では、1列目が取り除かれたreadDataが取得されるので、Jpが0番目に来るため、Jpを0番目と番号付けておく。
  • SwitchNextLanguage() : デバッグ用に、Lキーで文言を変更できる機能を搭載しておく。
3.2. LocalizeText.csに、言語変更時に、言語に応じた文言が適用されるように処理を記述

以下のように、LocalizeText.csに追記する :

LocalizeText.cs (tap)
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.Events;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace App.ReadCsv.Localize
{
    [RequireComponent(typeof(Text))]
    public class LocalizeText : MonoBehaviour
    {
        public string Id
        {
            get => id;
            set
            {
                id = value;
                onIdChanged?.Invoke();
            }
        }
        [SerializeField, Header("文言ID")]
        private string id;

        private Text text;

        private readonly UnityEvent onIdChanged = new UnityEvent();

        private void Awake()
        {
            text = GetComponent<Text>();
        }

        private void Start()
        {
            onIdChanged.AddListener(UpdateText);
        }

        private void OnEnable()
        {
+           WordingReader.OnLanguageSwitched.AddListener(UpdateText);
            UpdateText();
        }

        private void UpdateText()
        {
            var wordings = CsvReader.FindDataWithId(id);
            if (wordings == default) 
            {
#if UNITY_EDITOR
                Debug.LogError("LocalizeText : 存在しないidです。");
#endif
                return; 
            }
            
+           var languageType = WordingReader.CurrentLanguageType;
+           if (wordings.Length <= (int)languageType)
+           {
+#if UNITY_EDITOR
+               Debug.LogError($"LocalizeText : 読み込んだデータに、言語タイプ\"{languageType}\"は存在しません。");
+#endif
+               return;
+           }
-           text.text = wordings[0]; // 0 : Jp, 1 : En
+           text.text = wordings[(int)languageType];
        }
        
+       private void OnDisable()
+       {
+           WordingReader.OnLanguageSwitched.RemoveListener(UpdateText);
+       }
    }

#if UNITY_EDITOR
   [CustomEditor(typeof(LocalizeText))]
   public class LocalizeTextEditor : Editor
   {
       private string id;

       public override void OnInspectorGUI()
       {
           base.OnInspectorGUI(); // LocalizeTextクラスでのインスペクター上での記述を消さないようにする。

           GUILayout.Space(15);

           using (new EditorGUILayout.HorizontalScope())
           {
               id = EditorGUILayout.TextField(id); // id入力欄

               GUILayout.Space(5);

               if (GUILayout.Button("Idを更新"))
               {
                   var creator = (LocalizeText)target; // LocalizeTextクラスの取得
                   creator.Id = id;
               }
           }
       }
   }
#endif
}
  • WordingReader.OnLanguageSwitched : イベントにUpdateText()を登録することで、言語変更時にテキストが更新されるようにする。

最後に

どうでしたか、わかりやすかったでしょうか。
CSVファイルを用いてデータを管理することで、大人数での管理が容易になり、アセットの変更も最小限になり、Gitでの競合のリスクも低減されます。個人的には、グーグルスプレッドシートの使用を推奨しており、対応言語を増やす際にも便利だと思われます。
また気が向いたら何か書きます。それでは。

宣伝 : CUPLEXという時間差アクションパズルゲームを作成しています。私は主にプログラムとプロジェクト管理を担当しています。よかったら、覗いてみてください↓
https://store.steampowered.com/app/2499100/CUPLEX/

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?