うにのトゲは刺さると痛い(´・ω・`)-14 : LLM

※2018/05/19追記 ファイルが存在しない場合にDefaultLanguageを開くようにすることを忘れていたので追加して修正した(ノ∀`) 多分これでいいはず…(´・ω・`)

ついでにGitHubにも上げてみたけど、久しぶり過ぎて、まともに上がってるかどうかは不明(ノ∀`)
https://github.com/elekingmole/LocalizedLanguageManagerForUnity

またしても寄り道し過ぎてて、3日ほど時間を費やした…_| ̄|○


国際化

完成はまだ先だけど、そろそろタイトル画面で使用している文字列もローカリゼーションを適用させるかと、ぐぐる。色々と読む……

 

 

 

なんでUnityは国際化の仕組みを提供してないんだよヽ(`Д´)ノ

誰も作ってないのが不思議……もしかしたらアセットで配布されてんのかな(´・ω・`)?


しょうがないので、Javaのローカリゼーションを真似て作る。と言っても、Key=value形式でテキストファイルに格納する形にしただけだがw

コンセプト等は、

  • Key=value形式("title=タイトル"や"start=スタート"みたいな)を羅列した各言語ファイルを作る。BOMなしにしないとファイル頭に"EFBBBF"がひっついてlls[key]でtitleが取得出来なくなる(ノ∀`)ハマッタ
  • 各言語ファイルの拡張子はllf(Localized Language file)としたが、SetFileExtension()で書き換えられるようにしたので、何でもいいw
  • splitしたセットをDictionaryに格納。実際に使う時はLLM.GetLocalizedString("title")のようにkeyを引数にしてローカライズされた言語要素を取得する。
  • SetLanguage()でApplication.systemLanguageに対応するtargetFilenameを取得して拡張子を追加してコルーチンを呼び出す。最初は動的変更が出来るようにしようと思って、こういうメソッドにしたが、Application.systemLanguageは起動時に環境変数(?)を取得して、アプリが再起動されるまで保持し続けるようだから意味がなかった(ノ∀`) この処理はStart()でやるべきかな?
  • 本当はResources以下にllfファイルを入れてResources.Loadする方が楽なのだが、これだと開発者だけしかllfファイルの追加が出来ない。 ユーザーが独自のllfファイルを追加して使用出来るようにする為に、wwwによるアクセス方法を選択する必要があったので、通常時も同じようにwwwによってアクセスするStreamingAssetsにllfファイルを置くようにコーディングした。
  • Androidのパスは正直良く解らないというかバージョンによって変わってしまう酷いものなので、無理に全対応をすることは諦めた(ノ∀`) 取り敢えずはStreamingAssetsとインストール後のフォルダ(内部ストレージのAndroid\data\com.Com.Pr\files(?))とユーザー自らがパスを調べて手入力してもらうという力技でSDカード上のファイルを読み込めるようにしたw
  • 凄い適当実装なのと、そんなにテストしてないから、本当にまともに動くかは不明w 一応、日本語と英語は試したけども。
  • llfファイル名は基本的に各言語の最初の三文字。例外は日本とスロバキアとスロベニアと中国語繁体字と中国語簡体字かな?
  • 本当は開始時に各言語ファイル名群(jpnやeng)を文字配列に代入しておいて、switch (Application.systemLanguage)の箇所でその文字配列をtargetFilenameに代入する形にしようかと思ったが面倒くさいのでやめた。こうしておいて、その文字配列への代入メソッドを作って任意のファイルを読み込めるようにしておけば、各システム言語に対して任意の言語ファイルを割り当てられると思ったが、そこまでする意味があるのかわからなくなったから(ノ∀`) unKnownとかdefaultで処理しても良さげな分岐が順番通りに残っているのはその時の迷いの痕跡w
  • LoadTextData()は最初はコールバックのある形にしていたが、呼び出し側にコールバックを用意するのが面倒になって削除した(ノ∀`) www.isDoneとかを使うべきなのだが、面倒くさくてやってないw GetLocalizedString()でエラーを出さない為だけにisReadFinishedなんてのを用意したが、いつか直さないといけない気がしないでもない(´・ω・`)

テストは同じCanvasに下の二つのスクリプトを貼り、各言語ファイルの内容をちょっとだけ書き換え、StreamingAssetsとインストール後のフォルダとSDカードのルートにコピーして実行。あとPlayerSettingsで[Minimum API Level]はAndroid5.0以上にし、[Write Permission]は"External"にした。

LocalizationTestのStart()の

LLM.SetPathMode(LocalizedLanguageManager.PathMode.persistentData)
.SetLanguage();

の箇所を、StreamingAssetsの時は

LLM.SetLanguage();

に変える。

インストール後のフォルダの時は変更なし。

手入力の時は

LLM.SetPathMode(LocalizedLanguageManager.PathMode.manuallyInput)
.SetManuallyInputPath("(SDカードのパス)").SetLanguage();

に変える。SDカードのパスはESエクスプローラーで調べた(ノ∀`)


LocalizedLanguageManager

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

public class LocalizedLanguageManager : MonoBehaviour
{
    Dictionary<string, string> lls = new Dictionary<string, string>();
    TextReader textReader;
    public bool isReadFinished;

    string defaultLanguage = "eng";
    string forcedLanguage = "";
    string fileExtension = ".llf";
    string manuallyInputPath = "";
    PathMode pathMode = PathMode.streamingAsset;

    public LocalizedLanguageManager SetFileExtension(string extension)
    {
        fileExtension = extension;
        return this;
    }

    public LocalizedLanguageManager SetPathMode(PathMode mode)
    {
        pathMode = mode;
        return this;
    }

    public LocalizedLanguageManager SetManuallyInputPath(string path)
    {
        manuallyInputPath = path;
        return this;
    }

    public LocalizedLanguageManager SetDefaultLanguage(string filename)
    {
        defaultLanguage = filename;
        return this;
    }

    public LocalizedLanguageManager SetForcedLanguage(string filename)
    {
        forcedLanguage = filename;
        return this;
    }

    public void SetLanguage()
    {
        string targetFilename = "";

        if (forcedLanguage.Length == 0)
        {
            switch (Application.systemLanguage)
            {
                case SystemLanguage.Afrikaans:
                    targetFilename = "afr";
                    break;
                case SystemLanguage.Arabic:
                    targetFilename = "ara";
                    break;
                case SystemLanguage.Basque:
                    targetFilename = "bas";
                    break;
                case SystemLanguage.Belarusian:
                    targetFilename = "bel";
                    break;
                case SystemLanguage.Bulgarian:
                    targetFilename = "bul";
                    break;
                case SystemLanguage.Catalan:
                    targetFilename = "cat";
                    break;
                case SystemLanguage.Chinese:
                    targetFilename = "chi";
                    break;
                case SystemLanguage.Czech:
                    targetFilename = "cze";
                    break;
                case SystemLanguage.Danish:
                    targetFilename = "dan";
                    break;
                case SystemLanguage.Dutch:
                    targetFilename = "dut";
                    break;
                case SystemLanguage.English:
                    targetFilename = "eng";
                    break;
                case SystemLanguage.Estonian:
                    targetFilename = "est";
                    break;
                case SystemLanguage.Faroese:
                    targetFilename = "far";
                    break;
                case SystemLanguage.Finnish:
                    targetFilename = "fin";
                    break;
                case SystemLanguage.French:
                    targetFilename = "fre";
                    break;
                case SystemLanguage.German:
                    targetFilename = "ger";
                    break;
                case SystemLanguage.Greek:
                    targetFilename = "gre";
                    break;
                case SystemLanguage.Hebrew:
                    targetFilename = "heb";
                    break;
                case SystemLanguage.Icelandic:
                    targetFilename = "ice";
                    break;
                case SystemLanguage.Indonesian:
                    targetFilename = "ind";
                    break;
                case SystemLanguage.Italian:
                    targetFilename = "ita";
                    break;
                case SystemLanguage.Japanese:
                    targetFilename = "jpn";
                    break;
                case SystemLanguage.Korean:
                    targetFilename = "kor";
                    break;
                case SystemLanguage.Latvian:
                    targetFilename = "lat";
                    break;
                case SystemLanguage.Lithuanian:
                    targetFilename = "lit";
                    break;
                case SystemLanguage.Norwegian:
                    targetFilename = "nor";
                    break;
                case SystemLanguage.Polish:
                    targetFilename = "pol";
                    break;
                case SystemLanguage.Portuguese:
                    targetFilename = "por";
                    break;
                case SystemLanguage.Romanian:
                    targetFilename = "rom";
                    break;
                case SystemLanguage.Russian:
                    targetFilename = "rus";
                    break;
                case SystemLanguage.SerboCroatian:
                    targetFilename = "ser";
                    break;
                case SystemLanguage.Slovak:
                    targetFilename = "svk";
                    break;
                case SystemLanguage.Slovenian:
                    targetFilename = "svn";
                    break;
                case SystemLanguage.Spanish:
                    targetFilename = "spa";
                    break;
                case SystemLanguage.Swedish:
                    targetFilename = "swe";
                    break;
                case SystemLanguage.Thai:
                    targetFilename = "tha";
                    break;
                case SystemLanguage.Turkish:
                    targetFilename = "tur";
                    break;
                case SystemLanguage.Ukrainian:
                    targetFilename = "ukr";
                    break;
                case SystemLanguage.Vietnamese:
                    targetFilename = "vie";
                    break;
                case SystemLanguage.ChineseSimplified:
                    targetFilename = "chs";
                    break;
                case SystemLanguage.ChineseTraditional:
                    targetFilename = "cht";
                    break;
                case SystemLanguage.Unknown:
                    break;
                case SystemLanguage.Hungarian:
                    targetFilename = "hun";
                    break;
                default:
                    break;
            }

            if (targetFilename.Length == 0)
            {
                targetFilename = defaultLanguage + fileExtension;
            }
            else
            {
                targetFilename += fileExtension;
            }
        }
        else
        {
            targetFilename = forcedLanguage + fileExtension;
        }

        StartCoroutine(LoadTextData(targetFilename));
    }

    public string GetLocalizedString(string key)
    {
        if (isReadFinished)
        {
            if (lls.ContainsKey(key))
            {
                return lls[key];
            }
            else
            {
                string temp = "[key] ";
                foreach (string s in lls.Keys)
                {
                    temp += s + "/";
                }
                temp += "\n[value] ";
                foreach (string s in lls.Values)
                {
                    temp += s + "/";
                }

                return temp;
            }
        }
        else
        {
            return "Not Ready";
        }
    }


    public IEnumerator LoadTextData(string targetFilename)
    {
        string path = "";
        string textBuffer = "";
#if UNITY_EDITOR
        path = Application.streamingAssetsPath + "\\" + targetFilename;
		FileStream file = new FileStream(path,FileMode.Open,FileAccess.Read);
		textReader = new StreamReader(file);
		yield return new WaitForSeconds(0f);
#elif UNITY_ANDROID
        WWW www = null;

        switch(pathMode){
            case PathMode.streamingAsset:
                path = "jar:file://" + Application.dataPath + "!/assets" + "/" + targetFilename; 
        
                www = new WWW(path);
                yield return www;
                if(www.text.Length == 0){
                    path = "jar:file://" + Application.dataPath + "!/assets" + "/" + defaultLanguage + fileExtension;
                }

                break;
            case PathMode.persistentData:
                if(!File.Exists(Application.persistentDataPath + "/" +targetFilename)){
                   targetFilename = defaultLanguage + fileExtension;
                }

                path = "file://"+ Application.persistentDataPath + "/" +targetFilename;
                break;
            case PathMode.manuallyInput:
                if(!File.Exists(manuallyInputPath + "/"+ targetFilename)){
                   targetFilename = defaultLanguage + fileExtension;
                }
                path = "file://"+ manuallyInputPath + "/"+ targetFilename;
                break;
        }

		www = new WWW(path);
		yield return www;
		textReader = new StringReader(www.text);
#endif

        string[] tempString = null;
        while ((textBuffer = textReader.ReadLine()) != null)
        {
            if (textBuffer.Contains("="))
            {
                tempString = textBuffer.Split('=');

                if (tempString.Length > 2)
                {
                    for (int i = 2; i < tempString.Length; i++)
                    {
                        tempString[1] += "=" + tempString[i];
                    }
                }

                lls.Add(tempString[0], tempString[1]);
            }
        }

        isReadFinished = true;
    }

    public enum PathMode
    {
        streamingAsset = 0,
        persistentData = 1,
        manuallyInput = 2,
    }
}

LocalizationTest

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

public class LocalizationTest : MonoBehaviour
{
    public static string resultTxt = "";
    private GUIStyle rectStyle = new GUIStyle();
    LocalizedLanguageManager LLM = null;

    void Start()
    {
        rectStyle.wordWrap = true;
        rectStyle.fontSize = 50;
        rectStyle.alignment = TextAnchor.MiddleLeft;
        LLM = GetComponentInParent<LocalizedLanguageManager>();
        LLM.SetPathMode(LocalizedLanguageManager.PathMode.persistentData).SetLanguage();
    }

    void Update()
    {
        if (LLM.isReadFinished && resultTxt.Length == 0)
        {
            resultTxt = "[title] " + LLM.GetLocalizedString("title") + "\n" + 
                        "[start] " + LLM.GetLocalizedString("start")+ "\n" +
                        "[splitTest] " + LLM.GetLocalizedString("splitTest");
        }
    }

    void OnGUI()
    {
        GUI.TextArea(new Rect(10, Screen.height / 2, Screen.width, 350), resultTxt, rectStyle);
    }
}

これらはインストール後のフォルダに入れたファイル。他の場所に置いたファイルはそれぞれ"per"の部分を"StreamingAssets"やら"SD"に変えて表示時に区別がつくようにした。
jpn.llf

title=日本語タイトル per
start=ゲームスタート
splitTest=分割=文字列=

eng.llf

title=English per
start=Game Start
splitTest=splited=strings=

我ながら自分が説明ベタなのは知っているが、このアイディアを具現化した後すぐの、頭のとっちらかっている状態で書くエントリは本当に酷い(ノ∀`) 自分でも後で読み直して理解出来るかどうか心配(´・ω・`) コードの方も直さなきゃって思うところがあるものの、もう面倒くさくてその気力が出て来ない(ノ∀`) ソノウチネ

元々そんなにガチでやる必要はなかった国際化であったが、ユーザーが追加したファイルを読み込んで使えるようにすることはいずれ実装しようと思っていたので、ついつい或る程度の形になるまで粘ってしまった…(ヽ'ω`)

まあ取り敢えずこれで通常の神経衰弱ゲーム作成に戻れるから良しとするか…

参考:
今回使ったのか前に使ったのか覚えていないが、GUIStyle.wordWrapのお話。
Unity拡張:GUILayout.Label で自動折り返し表示

C#でDictionaryを使用したので、メモ。
C# の Dictionary の使い方

シングルクォーテーションでやらなくてハマったんだったかな(´・ω・`)?
C# 文字列を分割 特定の文字で区切る Split エントリがなくなってる。
◇Unityでゲーム開発 -C#で文字列操作-

C#の言語仕様とかを理解していないのがいけないのかもしれないんだけども、Visual Studioが余計な自動変換とかしてエラーの原因になって鬱陶しいことがままある(´・ω・`)
Protection level error in dictionary [closed]

【Unity】テキストファイル読み込み?

パーミッションって内部が上位で内部を持っていたら外部もOKかと勘違いしてハマッた(ノ∀`)
逆だったかw
AndroidのpersistentDataPathがカオス
UnityでAndroid,Windowsにファイルを書き込む際の注意点
UnityにおけるAndroidアプリのパーミッション付与について