【GameTips】多言語テキストシステム

どうもです、タドスケです。

ゲーム開発で使えそうな部品を提供するこのシリーズ。

今回は多言語テキストシステムを作ってみました。

目次

多言語テキストシステムとは?

ゲーム内で表示するテキストを管理する方法は色々あります。

いちばんシンプルなのは、プログラム内に直接テキストを埋め込んでしまうやり方です。

textbox.setText('テキスト')
button.setText('OK')

小規模なツールやミニゲームであれば問題ありません。

しかしプログラムの規模が大きくなるにつれ、同じボタンがたくさんの場所で使われるかもしれません。

もし後から全てのボタンの「OK」を「YES」に変更したくなったら……テキストを一箇所でまとめて管理したくなってきますね。

次に思いつくのは、テキストを辞書(dict)で管理するやり方です。

'TEXT_ID_01': 'こんにちは'
'TEXT_ID_02': 'OK'

組み込み側のコードは以下のようになります。

textbox.setText(text['TEXT_ID_01'])
button.setText(text['TEXT_ID_02'])

これなら、例えば全てのボタンのテキストを「OK」→「YES」に変えたくなったとしても、辞書の定義箇所を変えるだけで済みます。

日本語のみのプログラムであれば、これでも十分です。

しかし最近のゲームでは、ゲーム内で言語(日本語、英語など)を切り替えられることも珍しくありません。

TEXT_ID_01 が日本語では「こんにちは」になり、英語では「Hello」になってほしい場合、どのようにすればよいでしょうか?

その問題を解決するのが、今回の多言語テキストシステムです。

実行例

以下のファイルを用意します。

■samplefiles/test.csv

hello,こんにちは,Hello!
thanks,ありがとう,Thank you!

■samplefiles/test2.csv

weapon_1,銅の剣,Copper sword
weapon_2,鉄の剣,Iron sword

次に以下のようなコードを用意します。

system = System(LanguageId.Japanese)

# 辞書を読み込む
system.load_dictionary(CsvReader(Path('samplefiles/test.csv')))
system.load_dictionary(CsvReader(Path('samplefiles/test2.csv')))

print('[日本語]')
print(system.get_text('test', 'hello'))
print(system.get_text('test', 'thanks'))
print(system.get_text('test2', 'weapon_1'))
print(system.get_text('test2', 'weapon_2'))
print('')

# 英語に切り替える
system.change_language(LanguageId.English)

print('[英語]')
print(system.get_text('test', 'hello'))
print(system.get_text('test', 'thanks'))
print(system.get_text('test2', 'weapon_1'))
print(system.get_text('test2', 'weapon_2'))

実行すると、以下の出力が得られます。

[日本語]
こんにちは
ありがとう
銅の剣
鉄の剣

[英語]
Hello!
Thank you!
Copper sword
Iron sword

実装方法

実行例のような動作をするためにどのようなプログラムを書けばよいでしょうか。

クラス単位で解説します。

System クラス

class System:
    """テキストシステムクラス."""
    def __init__(
            self,
            language: LanguageId = LanguageId.Japanese) -> None:
        self._dictionaries: dict[str, TextDictionary] = {}
        self._language = language
    def load_dictionary(self, reader: AbstractReader) -> None:
        """辞書を読み込みます.
        既に同名の辞書を読み込んでいる場合は上書きされます.
        :param reader: 辞書の読み込みオブジェクト
        """
        d = TextDictionary(reader, self._language)
        self._dictionaries[reader.name] = d
    def remove_dictionary(self, dictionary_name: str) -> None:
        """辞書を削除します.
        :param dictionary_name: 辞書名
        """
        self._dictionaries.pop(dictionary_name, None)
    def change_language(self, language: LanguageId):
        """言語を変更します."""
        self._language = language
        self._reload_all_dictionaries()
    def get_text(
            self,
            dictionary_name: str,
            text_name: str) -> tp.Optional[str]:
        """テキストを得ます.
        :param dictionary_name: 辞書名
        :param text_name: テキスト名
        """
        d = self._dictionaries.get(dictionary_name)
        if d is None:
            return None
        return d.get_text(text_name)
    @property
    def language(self) -> LanguageId:
        """現在の言語."""
        return self._language
    def _reload_all_dictionaries(self) -> None:
        """全ての辞書を再読み込みします."""
        for dict_ in self._dictionaries.values():
            dict_.reload(self._language)

テキストシステムの本体クラスです。

テキスト辞書の登録、言語の切り替え、テキストの取得など、全ての操作はシステムクラス経由で行います。

get_dictionary 関数を敢えて用意しないことで、利用者に TextDictionary の存在を意識させず、簡単に利用できるようにしています。

言語が切り替わると、現在登録されている全てのテキスト辞書を再読み込みします。

辞書を複数登録できるようにすることで、「タイトル画面でしか使わないテキストはゲーム内では読み込まない」というようにしてメモリを節約することができます。

TextDictionary クラス

class TextDictionary:
    """テキスト辞書クラス.
    :param reader: 読み込みオブジェクト
    :param language: 言語
    """
    def __init__(
            self,
            reader: AbstractReader,
            language: LanguageId = LanguageId.Japanese) -> None:
        self._reader = reader
        self._data: TextDictionaryDataType = {}
        self.reload(language)
    @property
    def name(self) -> str:
        """辞書名."""
        return self._reader.name
    def is_empty(self) -> bool:
        """辞書が空か?"""
        return not self._data
    def get_text(self, key: str) -> tp.Optional[str]:
        """テキストを得ます.
        :param key: テキストのキー
        :return: テキスト。見つからない場合は None
        """
        return self._data.get(key)
    def reload(self, language: LanguageId) -> None:
        """指定した言語で辞書を再読み込みします.
        :param language: 言語
        """
        is_success, data = self._reader.read(language)
        if is_success:
            self._data = data
        else:
            self._data = {}

キーからテキストを取得できるテキスト辞書のクラスです。

辞書の読み込みに使う Reader クラスを外部から渡す(「依存性の注入」ともいいます)ことで、「読み込むファイルが csv なのか xlsx なのか」ということを辞書クラスが意識せずに読み込めるようにしています。

Reader クラス

class AbstractReader:
    """テキスト辞書を読み込む抽象クラス."""
    def __init__(self, path: Path) -> None:
        self._path = path
    def read(self, language: LanguageId) -> tuple[bool, TextDictionaryDataType]:
        """指定した言語で辞書を読み込みます.
        :param language: 言語
        :return: 読み込み結果(成功したら True)、辞書データ
        """
        raise NotImplementedError
    def _get_path(self) -> Path:
        return self._path
    @property
    def name(self) -> str:
        return self._path.stem
class CsvReader(AbstractReader):
    """CSV ファイルから辞書データを読み込むクラス.
    :param csv_path: csv ファイルのパス
    """
    # 言語に対応する csv の列
    _LANGUAGE_ROWS: tp.ClassVar[Mapping[LanguageId, int]] = {
        LanguageId.Japanese: 1,
        LanguageId.English: 2,
    }
    def __init__(self, csv_path: Path) -> None:
        super().__init__(csv_path)
    def read(self, language: LanguageId) -> tuple[bool, TextDictionaryDataType]:
        """(override)辞書を読み込みます.
        :return: 読み込み結果(成功したら True)、辞書データ
        """
        path = self._get_path()
        try:
            with path.open(encoding='utf8', newline='') as f:
                csvreader = csv.reader(f, delimiter=',')
                data = self._read_from_reader(csvreader, language)
            return True, data
        except Exception as e:
            print(e)
            return False, {}
    def _read_from_reader(self, csvreader, language) -> dict[str, str]:
        data: dict[str, str] = {}
        for row in csvreader:
            assert 2 <= len(row)
            id_ = row[0]
            text = row[self._LANGUAGE_ROWS[language]]
            data[id_] = text
        return data
class NullReader(AbstractReader):
    """空の読み込みクラス."""
    def read(self, language: LanguageId) -> TextDictionaryDataType:
        return {}

テキスト辞書(TextDictionary)用のデータを読み込むクラスです。

基底クラスの AbstractReader で、読み込み用の read 関数の引数と戻り値を定義しています。

AbstractReader を継承するクラスは、read 関数で TextDictionaryDataType 型のデータを返しさえすれば、中身はどんな実装でも構いません。

これにより、csv ファイルを読み込む CsvReader クラスや、何もしないダミーの NullReader クラスなどを自由に作ることができます。

コード

コード全文は以下の場所にアップしてあります。

実装

GitHub
gametips/src/language at master · tadosuke/gametips ゲームで使えそうな処理. Contribute to tadosuke/gametips development by creating an account on GitHub.

テスト

GitHub
gametips/tests/language at master · tadosuke/gametips ゲームで使えそうな処理. Contribute to tadosuke/gametips development by creating an account on GitHub.

拡張のアイデア

他のファイル形式をサポートする

今回はシンプルさのため csv ファイルを利用しましたが、チーム開発で利用する場合は Excel や スプレッドシートを使いたくなるでしょう。

その場合、以下の2つの方法があります。

読み込めるファイルを増やす

AbstractReader を継承する新たなReaderクラス(仮に XlsxReader とします)を作り、read 関数内部でファイルを読み込んで TextDictionaryDataType を返すようにします。

その後、System クラスの load_dictionary 関数に XlsxReader を渡してやれば、xlsx ファイルも読み込めるようになります。

system.load_dictionary(XlsxReader(Path('samplefiles/test.xlsx')))

ただし Excel や スプレッドシートの読み込み処理は csv と比べて複雑なため、処理も重くなります。

テキストの量が増えてくると、ロード時間が長くなってしまうかもしれません。

コンバーターを作る

Excel のファイルを読み込んで csv ファイルに変換(コンバート)するコンバーターを作ります。

そうすれば Excel の使いやすさと、csv の読み込みの速さを両立することができます。

コンバーターを毎回実行するのが面倒なら、簡単に実行できるバッチファイルを用意したり、Excel のファイルの変更を検知して自動でコンバートするプログラムを別で用意するのもよいでしょう。

他の言語を追加する

csv ファイルの列と LanguageId の定義を増やせば、他の言語にも対応できます。

テキスト以外の情報も含める

今回は各言語ごとのテキストのみを扱っていますが、テキスト以外の情報を含めることもできます。

アラインメント(左/右寄せ)、スタイル(太字)、文字色など、テキストと紐づく情報をまとめると、管理しやすくなるかもしれません。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

目次