六回目の投稿です。よろしくお願いします。
目次
概要
解説内容 :
- CSVファイルの行ごとの読み込み方法
- Idを用いた文言データの検索方法
- ローカライズ対応 (多言語対応)
利点 :
- コードと文言データが分離され、データの参照性と改変容易性が向上する。
- データを編集ソフト(例 : グーグルスプレッドシート)を用いて管理することで、大人数での管理が容易となる。
- 最終的には、Unity上での変更点はCSVファイルの更新のみとなり、変更点が少ないことから、Gitでの競合リスクを低減できる。
今回使用するCSVファイル :
(例 : 言語がenのときに、idに1を指定すると、"number"が表示される。)
サンプル :
実装概要
1. CSVファイルの読み込み方法の概要
- CSVファイルの内容を読み込むためのスクリプトファイル(CsvReader.cs)を作成する。
- CSVファイル指定時に、StringReaderクラスを用いて上から順に行を読み込み、string配列に保存していく。
- ローカライズ処理用の、CsvReaderクラスを継承したクラス(WordingReader.cs)を作成する。
- 上記動作をエディタ上で完結させることにより、アプリ側での負担を減らす。
2. Idを用いたデータの検索方法の概要
- CsvReader.csで、FindDataWithId(string)というidから文言を取得する処理を記述する。
- テキストUIを設置し、文言を適用するスクリプトファイル(LocalizeText.cs)を作成する。
- 動作確認用に、Idの更新に関するエディタ拡張を実装する。
3. ローカライズ対応方法の概要
- WordingReader.csで、言語タイプを設定し、言語変更時に指定したイベントが呼ばれるように、言語変更処理を記述する。
- 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
{
}
}
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を入力・更新する :
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/