ゲーム化!tomo_manaのブログ

ゲーム化!tomo-manaのブログ

Unityでゲームを作る方法について紹介しています

Unity×会計#7 言語切替スイッチ (Unity 2019.4.4f1)

f:id:tomo_mana:20211220010553p:plain

今回は言語切り替えスイッチを実装します。

構想

言語ボタンを押したら切り替わるようにします。

f:id:tomo_mana:20210815214739p:plain
言語ボタン(2か国語)

複数の言語情報を持たせるには、少なくともstringの二次元配列が必要になります。C#で多次元配列を扱うのにまだ慣れていないので、とりあえず今できる方法で実装することにして、先にコアになるインターフェースを決めます。

// 欲しい文字列の指定
enum NAME_ID {
    NAME_ID_BS_CASH,
    :
};
// 言語ごとの名前を取得
public string GetStrLang(NAME_ID);  // 現在選択されている言語で取得
public string GetStrLang(NAME_ID, NAME_LANG);  // 言語指定で取得

●文字列だけのクラスを作成する
●列挙体 ID一覧
●string get文字(列挙体 id)

上記のインターフェースだけ決めておけば、とりあえず多重配列がうまくいかなくても、おそらく以下のような処理で言語切替ができます。

// 言語パッチのテーブル
struct LangNames {
    string jpn;
    string eng;
    
    public LangNames(string jp, string en)
    {
        this.jpn = jp;
        this.eng = en;
    }
};
LangNames[] langNames = {
    new LangNames( "現金", "Cash" ),
    :
};
// 言語ごとの名前を取得
string GetStrLang( NAME_ID id )
{
    if( selectedLang == LANG_ENG ){
        return (langNames[(int)id]).eng;
    } else {
        return (langNames[(int)id]).jpn;
    }
}

最終的に、言語ボタンを押されたらすべてのオブジェクトに表示更新を指示する必要があります。こういう時は ExecuteEvents を使用します。

// インターフェース
public interface ILangSwitchHandler : IEventHandler

// メッセージ
ExecuteEvents.Execute<ILangSwitchHandler>(
    target: gameobject,
    eventData: null,
    functor: ILangSwitchHandlerCallback
);

実装

こういうプロジェクト全体に影響する修正を、いきなり大掛かりにやると大概収拾がつかなくなる印象がありますので、まず一つのファイルで、スコープをクラス内に限定して作ります。

STEP1: 特定のコードに適用してみる(小さくテスト)

コード
public class BalanceSheet : MonoBehaviour
{
    // 追加(STEP1)
    public enum NAME_ID {
        NAME_ID_BS_CASH,
        NAME_ID_BS_FLA,
        NAME_ID_BS_FIA,
        NAME_ID_BS_NAN,
        NAME_ID_BS_FLL,
        NAME_ID_BS_FIL,
        NAME_ID_BS_NAP,
        NAME_ID_MAX
    };
    public struct LangNames {
        public string jpn;
        public string eng;
        
        public LangNames(string jp, string en)
        {
            this.jpn = jp;
            this.eng = en;
        }
    };
    LangNames[] langNames = {
        new LangNames( "現金", "Cash" ),
        new LangNames( "流動資産", "Flex.Assets" ),
        new LangNames( "固定資産", "FixedAssets" ),
        new LangNames( "純資産(負)", "NetAssets" ),
        new LangNames( "流動負債", "Flex.Liabilities" ),
        new LangNames( "固定負債", "FixedLiabilities" ),
        new LangNames( "純資産", "NetAssets" ),
    };
    string GetStrLang( NAME_ID id )
    {
//      return (langNames[(int)id]).jpn;
        return (langNames[(int)id]).eng;
    }
    // 修正
    private void RedrawBalanceSheet()
    {
        // 領域ごとの金額(表示)を更新
        for( i = 0; i < (int)(BS_AREA.BS_MAX); i++ )
        {
            /* 略 */
            bsArea[i].textArea.text = GetStrLang( bsArea[i].areaNameID ) + "\n" + bsArea[i].value;
        }
    }
    void Start()
    {
        int i;
        for( i = 0; i < (int)(BS_AREA.BS_MAX); i++ )
        {
            /* 略 */
            bsArea[i].areaNameID = (NAME_ID)i;
        }
    }
}
動作テスト

うまくいきました。(以下英語)

f:id:tomo_mana:20210815220730p:plain
貸借対照表(英語表示)

STEP2: 共有する領域をグローバルに持っていく

次に、他のファイルに適用する前に、領域の共有のさせ方を考えます。(ローカル→グローバル→ファイル分割)

独自クラスを作って、ダメもとで static にしてみたら、意外と動いてしまったので、そのまま行ってみます。

コード

GlobalString.cs

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

// STEP1
public enum NAME_ID {
    NAME_ID_BS_CASH,
    :
    NAME_ID_MAX
};

public class GlobalString
{

    public struct LangNames {
        public string jpn;
        public string eng;
        
        public LangNames(string jp, string en)
        {
            this.jpn = jp;
            this.eng = en;
        }
    };
    static LangNames[] langNames = {
        new LangNames( "現金", "Cash" ),
        :
    };
    public static string GetStrLang( NAME_ID id )
    {
//      return (langNames[(int)id]).jpn;
        return (langNames[(int)id]).eng;
    }
}

BalanceSheet.cs
こちらは、先ほど追加した処理をコメントアウトして、1行修正します。

public class BalanceSheet : MonoBehaviour
{
    /* 略 */
    private void RedrawBalanceSheet()
    {
        /* 略 */
        // 領域ごとの金額(表示)を更新
        for( i = 0; i < (int)(BS_AREA.BS_MAX); i++ )
        {
            bsArea[i].textArea.text = GlobalString.GetStrLang( bsArea[i].areaNameID ) + "\n" + bsArea[i].value;  // static参照
        }
    }
}

STEP3: スイッチで切り替わるようにする

言語切替ボタンを追加します。

Hierarchy
f:id:tomo_mana:20210816214952p:plain
Hierarchy - 言語切替ボタン

日本語と英語のボタンです。HorizontalLayoutGroupで横並びにしています。

f:id:tomo_mana:20210816215256p:plain
言語切替 - 追加されたボタン
コード

メッセージインターフェースを追加し、言語切替メッセージを送ります(冒頭に記したもの)。

// インターフェース
public interface ILangSwitchHandler : IEventHandler

// メッセージ
ExecuteEvents.Execute<ILangSwitchHandler>(
    target: gameobject,
    eventData: null,
    functor: ILangSwitchHandlerCallback
);

先程追加した、言語スイッチを取りまとめているゲームオブジェクト(LangSwitch)に、以下のコードを追加します。
LangSwitch.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public interface ILangSwitchHandler : IEventSystemHandler
{
    void LangSelected(LANG selectedLang);
}
public class LangSwitch : MonoBehaviour
{
    void ILangSwitchHandlerCallback(ILangSwitchHandler receiver, BaseEventData eventData)
    {
        receiver.LangSelected( GlobalString.GetSelectedLang() );
    }
    void ChangeLanguage( GameObject gameobject )
    {
        // 再帰:子から先
        foreach( Transform child in gameobject.transform ){
            ChangeLanguage( child.gameObject );
        }
        ExecuteEvents.Execute<ILangSwitchHandler>(
            target: gameobject,
            eventData: null,
            functor: ILangSwitchHandlerCallback
        );
    }
    
    void ButtonClicked(int buttonNo)
    {
        GlobalString.SetSelectedLang( (LANG)buttonNo );
        
        // ルート取得
        foreach( GameObject root in gameObject.scene.GetRootGameObjects() )
        {
            if( root.GetComponent<Canvas>() ){
                foreach( Transform child in root.transform ){
                    ChangeLanguage( child.gameObject );
                }
            }
        }
    }
    
    // Start is called before the first frame update
    void Start()
    {
        int i = 0;
        Button button;
        foreach( Transform child in gameObject.transform ){
            button = child.gameObject.GetComponent<Button>();
            if( !ReferenceEquals(button, null) ){
                int ii = i;
                button.onClick.AddListener(() => ButtonClicked(ii));
                i++;
            }
        }
        ButtonClicked(0);
    }
}

文字列を管理するクラスには、選択されている言語(LANG)を追加します。
GlobalString.cs

// 以下、STEP2からの追加分
public enum LANG {
    LANG_JPN = 0,
    LANG_ENG,
    LANG_MAX
};
public class GlobalString
{
    // 追加
    static LANG selectedLang = LANG.LANG_JPN;
    
    public static void SetSelectedLang( LANG Lang )
    {
        selectedLang = Lang;
    }
    public static LANG GetSelectedLang()
    {
        return selectedLang;
    }
    
    // 修正
    public static string GetStrLang( NAME_ID id )
    {
        if( selectedLang == LANG.LANG_ENG ){
            return (langNames[(int)id]).eng;
        } else {
            return (langNames[(int)id]).jpn;
        }
    }
}

受信するコードには、先ほど定義したインターフェースを追加します。この段階では、STEP1、2で改造を加えてきたBalanceSheetにだけ修正を加えます。
BalanceSheet.cs

public class BalanceSheet : MonoBehaviour, ILangSwitchHandler
{
    // 初期化リストに文字列IDを追加
    public struct BsAreaId {
        :
        public NAME_ID areaNameId;
        
        public BsAreaId(int id, string name, NAME_ID areaNameId)
        {
            :
            this.areaNameId = areaNameId;
        }
    }
    private BsAreaId[] bsAreaId = {
        new BsAreaId( (int)(BS_AREA.BS_CASH), "Cash",               NAME_ID.NAME_ID_BS_CASH ),
        :
    }
    public struct BsArea {
//        public string areaName;
        public NAME_ID areaNameId;
        :
    }

    // インターフェース追加
    public void LangSelected(LANG selectedLang)
    {
        RedrawBalanceSheet();
    }
    // 以下、文字列IDを参照するように修正
    private void RedrawBalanceSheet()
    {
        // 領域ごとの金額(表示)を更新
        for( i = 0; i < (int)(BS_AREA.BS_MAX); i++ )
        {
            bsArea[i].textArea.text = GlobalString.GetStrLang( bsArea[i].areaNameId ) + "\n" + bsArea[i].value;
        }
        /* 以下略:bsArea[].areaName ⇒ GlobalString.GetStrLang( bsArea[i].areaNameId )に置換 */
    }
}

LangSwitchは他のプログラムがStart()した後でないとエラーになるので、実行順を設定します。
Edit > Project Settings... > Script Execution Order

f:id:tomo_mana:20210817215549p:plain
実行順の設定
動作テスト

日本語とEnglishボタンを押すと貸借対照表の表示が切り替わることを確認できました。

STEP4: 全ファイルに適用する

それでは、最後に気合を入れて、STEP3で追加したインターフェースを対象の全コードに適用します。

損益計算書

ProfitAndLoss.cs
貸借対照表と同じ修正のため省略)

入力フィールド

InputField.csほか
ボタン、あるいはTextMeshProUGUI単体で言語変換できるコンポーネントを作ります。結局これが一番万能なので、別記事にもまとめておきます。

public class LangSwitch : MonoBehaviour
{
    void ChangeLanguage( GameObject gameobject )
    {
		foreach( Transform child in gameobject.transform ){
			ChangeLanguage( child.gameObject );
		}
		ExecuteEvents.Execute<ILangSwitchHandler>(
			target: gameobject,
			eventData: null,
			functor: ILangSwitchHandlerCallback
		);
    }
    void ButtonClicked(int buttonNo)
    {
        int i;
        // ルート取得
        foreach( GameObject root in gameObject.scene.GetRootGameObjects() )
        {
        	// キャンバス以下
        	if( root.GetComponent<Canvas>() ){
}

作成したコードを、TextMeshPro、またはButton - TextMeshProに追加します。
表示する文字(NAME_ID)を選択します。

f:id:tomo_mana:20210818223634p:plain
LangSwitchComponent - 表示文字の選択
現金仕訳の表示

テスト用に入れている部分は、文字列変換テーブルとは別に実装します。

public class InputWindow2 : MonoBehaviour, ILangSwitchHandler
{
    string[] msg_j = new string[]{
        "成功!",
        "現金不足!",
        "在庫不足!",
        "START!"
    };
    string[] msg_e = new string[]{
        "SUCCEED!",
        "LACK OF CASH!",
        "LACK OF REMAINS!",
        "START!"
    };
    
    LANG selected;
    
    int error_hold;
    void UpdateLog(int error)
    {
        if( error == -1 ){
            error = error_hold;
        } else {
            error_hold = error;
        }
        if( !ReferenceEquals(log, null) ){
            //現金:xxxx/仕入:xxxx/売上:xxxx/在庫:xx/xx(@100)
            if( selected == LANG.LANG_ENG ){
                log.text = msg_e[error] + "\nCash:" + tb.cash + "\nPurchases:" + tb.purchase + "\nSales:" + tb.sales + "\nRemain:" + tb.q_remain + "/" + tb.q_total + "(@1000)";
            } else {
                log.text = msg_j[error] + "\n現金:" + tb.cash + "\n仕入:" + tb.purchase + "\n売上:" + tb.sales + "\n在庫:" + tb.q_remain + "/" + tb.q_total + "(@1000)";
            }
        }
    }
    public void LangSelected(LANG selectedLang)
    {
        this.selected = selectedLang;
        UpdateLog( -1 );
    }
}
モード切替スイッチ

最後に、モード切替スイッチに少し細工をします。非表示の状態ではメッセージを受け取れないので、モードを切り替えた時に言語切替メッセージを出すようにします。
ModeSwitch.cs

public class ModeSwitch : MonoBehaviour
{
    [SerializeField]
    LangSwitch langSwitch = default;
    
    void ButtonClicked(int buttonNo)
    {
        // 追加
        if( !ReferenceEquals( langSwitch, null ) ){
            langSwitch.CheckActiveGameObjects();
        }
    }
}

LangSwitch.cs

public class LangSwitch : MonoBehaviour
{
    public void CheckActiveGameObjects()
    {
        ButtonClicked( -1 );
    }
    void ButtonClicked(int buttonNo)
    {
        // -1の場合は、設定言語を保持したままサーチ
        if( buttonNo != -1 ){
            GlobalString.SetSelectedLang( (LANG)buttonNo );
        }
        // 以下メッセージを飛ばす処理
    }
}

動作テスト

全部英語に変わりました。でも今後追加する全てのオブジェクトにこの修正を入れるのはちょっとめんどくさいかもしれません。

f:id:tomo_mana:20210818222123p:plain
動作テスト(英語表示)

ゲームのURL(前回までと同じURLです)
tomo-mana.hatenablog.com

次回

商品原価の変化

参考

純資産の英語は、Equity or NetAsset?⇒ 多分NetAssetで良さそう。Equityは資本を指す。NetAssetはAssetsとLiabilitiesの差を指す。
Net Asset(純資産) | 国際会計の用語集 - ビズインフォログ