簡単(かもしれない)日本語表示

2010/09/17 XNA Game Studio 4.0用サンプルをhttp://higeneko.net/hinikeni/sample/xna40/SimpleMessage.zipにアップしました。詳細は「サンプルコードをXNA 4.0向けに更新」を見てください。

2009/06/25 XNA GS 3.1用のサンプルをhttp://higeneko.net/hinikeni/sample/xna31/SimpleMessage.zipにアップしました。

以前紹介した英数字以外の文字列表示方法はコンテントパイプラインの全体の流れを説明するという目的もあったので複雑なつくりになっていました。RPGやテキストベースのアドベンチャーゲームなどの大量のテキストデータを扱う場合は、こういったものは必須になってきますが、今回はXNA 2.0から導入されたプロセッサパラメーターを使ってのシンプルな日本語を含む多言語文字列表示方法を紹介します。

とりあえず、前回のサンプルを2.0用に書き直しておきました。

MessageTextSample 2.0

なぜSpriteFontなのか?

Windowsアプリケーションプログラミングを経験した人達にとってXNAに同様の文字列描画APIがないというのは疑問に思うかも知れません。なぜ、普通のWindowsのアプリケーションのようにOSにインストールされたフォントを指定して文字列を簡単にかけないのでしょうか?それには主に以下の理由があります。

  1. 容量: 日本語のフォントファイルのサイズは大きくて再配布が難しい
  2. 速度: テクスチャ形式のフォントに比べて描画処理に時間が掛かる
  3. 法律: 面倒だから放っておくと、後でさらに面倒なことになる罠のこと
  4. 娯楽: OSにインストールされいるフォントを使うと事務的な印象を受けてしまう

1については、日本語フォントの殆どは約7,000文字近くのデータを持っています。私のマシンにインストールされているMSゴシックのファイルサイズは8MB近くあります。殆どのゲームでは500~1,500文字程度の文字しか使わないので7,000文字のデータを持つのは効率的ではありません。更に、キャラクターや状況によって複数のフォントを使い分けるのでファイルサイズが大きいというの問題になります。

2は、通常のフォントはTrueTypeと呼ばれるデータ形式で、これは一文字一文字のポリゴンデータを持っているようなものでテクスチャにあらかじめ描画された文字をひとつの四角形ポリゴンで表示するより処理するのに時間が掛かります。また、フォントデータは必要になったときにHDDから読み込むのでリアルタイム性の高いゲームを作ってるときにはその遅延時間を考慮しなければいけないという問題もあります。

3は面倒な問題で、フォントには著作権があり、その使用許諾の形式もフォントを作っている会社によってさまざまなものがありますが、その多くはテクスチャとして文字を使うのは良くてもフォントファイル自体の再配布を禁じているものがあります。それ以外にもいろんなライセンス契約形式があることに注意してください。

4については、フォントと言うのはゲームの雰囲気を伝えるために重要なもので、キャラクターやその場の雰囲気によって複数のフォントを使うことが多いです。例えばおどろどろしい雰囲気を出すために古印体というフォントを使ったりしますが、そういった特徴的なフォントがOSにインストールされていることは殆どありません。また、テクスチャにすることで

xboxControllerButtonA

のように普通の文字列の間にビットマップで描いた絵を文字として組み合わせることもできます。

以上の理由からXNAではSpriteFontを採用しています。

簡単メッセージプロセッサ

今回のサンプルの基本アイディアは

  1. FontDescriptionProcessorから派生したMessageProcessorを作る
  2. MessageProcessor.Processメソッド内で任意の文字列をFontDescriptionに追加する
  3. 追加する文字はMessageFilenameプロパティに指定されたファイルから読み込む

と、シンプルなものです。カスタムプロセッサのプロセスメソッドは以下のようになっています。

 public override SpriteFontContent Process(FontDescription input,
                                            ContentProcessorContext context)
{
    // MessageFilenameで指定されたファイル内の文字を追加する
    AppendCharacters(input, context);

    // 文字列を追加した後は単純にFontDescriptionProcessorのプロセスを呼ぶだけ
    return base.Process(input, context);
}

  MessageFilenameをプロセッサ・パラメーターとして以下のように宣言しています。詳細はこの記事が参考になると思います。ここではプロパティ画面上で読みやすいようにDisplayNameとDescriptionアトリビュートを使っています。

 /// <summary>
/// プロセッサーパラメーター
/// ここに読み込むメッセージファイル名を指定する
/// </summary>
// プロパティ画面で表示される文字列の指定
[DisplayName("メッセージファイル名")]
// プロパティ画面でのパラメーターの説明
[Description("メッセージテキストが含まれているテキストファイル名")]
public string MessageFilename
{
    get { return messageFilename; }
    set { messageFilename = value; }
}

string messageFilename;

Processメソッドから呼んでいるAppendCharactersメソッド内では単にファイルから文字列を読み込み、FontDescription.Characters.Addメソッドを使って使用する文字を追加しています。

 /// <summary>
/// FontDescriptionにMessageFilenameで指定されたファイル内の文字を追加する
/// </summary>
void AppendCharacters(FontDescription input, ContentProcessorContext context)
{
    // MessageFilenameは有効な文字列か?
    if (String.IsNullOrEmpty(MessageFilename))
        return;

    if (!File.Exists(MessageFilename))
    {
        throw new FileNotFoundException(
            String.Format( "MessageFilenameで指定されたファイル[{0}]が存在しません",
                            Path.GetFullPath(MessageFilename)));
    }

    // 指定されたファイルから文字列を読み込み、
    // FontDescription.Charctarsに追加する
    try
    {
        int totalCharacterCount = 0;

        using (StreamReader sr = File.OpenText(MessageFilename))
        {
            string line;
            while ( ( line = sr.ReadLine() ) != null )
            {
                totalCharacterCount += line.Length;

                foreach( char c in line )
                    input.Characters.Add( c );
            }
        }

        context.Logger.LogImportantMessage("使用文字数{0}, 総文字数:{1}",
            input.Characters.Count, totalCharacterCount);

        // CPにファイル依存していることを教える
        context.AddDependency(Path.GetFullPath(MessageFilename));
    }
    catch (Exception e)
    {
        // 予期しない例外が発生
        context.Logger.LogImportantMessage("例外発生!! {0}", e.Message);
        throw e;
    }
}

このカスタムプロセッサをコンパイルすると、以下のようにプロパティ画面上でメッセージプロセッサという名前のプロセッサが追加され、そこにメッセージファイル名というプロセッサ・パラメーターが表示されます。

SimpleMessage

ここにはテキストファイルであればどんなファイル名でも指定できますが、使っている環境に依存しないように相対パスを指定するようにしましょう。通常、コンテントパイプライン実行時のルートパスはContentフォルダになっています。また、多言語が指定できるようにファイルのエンコーディングはUTF-8を指定します。

さて、どんなファイルでも指定できるわけですが、前回の時にも書いたように私がプログラムするときには 「エラーが起きにくく、起きたとしても直ぐにエラーの原因が特定、修正ができる」 ということを心がけています。ですから、メッセージファイルと実際にランタイム時に使う文字列を別のファイルであるソースコードと別々にすると問題が起きやすく、見つけづらいという状態になってしまいます。

じゃあどうすればいいのか?また前回のように面倒なコーディングをしないといけないのか?と思ってしまいますが、今回はシンプルな解決方法をとります。それはソースコードをメッセージファイルとして指定するというものです。

ソースコードも立派なテキストファイル。そこに使われる文字は殆どが英数字なので日本語のメッセージに使われる文字数に比べると少ないし、既に英数字を使用する文字に指定しているのであれば無駄はありません。気をつけるとすれば、メッセージファイルが更新する度にコンテントビルドが行われるので、頻繁に書き換えるソースコードをメッセージファイルとして指定しないということです。

そこで、今回はサンプル内で使う文字列をまとめたMessageTable.csというソースコードを指定しています。

 namespace SimpleMessage
{
    static class MessageTable
    {
        public static string[] Messages = {
            "日本語メッセージテスト\n"+
            "キーボードのEnterキー、コントローラーのAボ���ンで\n"+
            "次のメッセージを表示します。",

            ...

            "最初のメッセージに戻ります。",
        };
    }
}

カスタムプロセッサは100行程度のプログラム。ランタイム側は特別なデータタイプを持つことなく、SpriteFontを追加し、そのプロセッサを変更してパラメーターを設定するだけなので、簡単に日本語メッセージを表示できるのではないでしょうか?

簡単(かもしれない)日本語表示サンプル

 

冒頭で紹介したサンプルも、このサンプルもXNA 2.0用です。XNA 2.0のプロジェクトファイルはC# Express 2005、Visual Studio 2005のどちらでも使えるようになっています。