XNA Frameworkブログ
 

February, 2008

  • ひにけにXNA

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

    • 1 Comments

    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のどちらでも使えるようになっています。

  • ひにけにXNA

    Xbox LIVE community games

    • 2 Comments

    と、いうわけでXbox LIVE community gamesが発表されました。

    http://blogs.msdn.com/xna/archive/2008/02/20/announcing-xbox-live-community-games.aspx

     

    Xbox Live コミュニティゲームを一言で表せばYouTubeやニコニコ動画のゲーム版といった感じです。

    そう、今まではXNA Game Studioを使って作ったゲームを開発者以外の人に遊んで貰うのは色々と面倒でしたが、コミュニティーゲームの登場によって簡単に色んな人達があなたの作ったゲームを遊ぶことができるようになります。なぜなら、コミュニティゲームはクリエータークラブ会員でなくとも遊ぶことができるからです。

    ゲームの登録手順の概略は以下のようなっています。

    • ゲームを作る(大事なことなので忘れないように)
    • ゲーム登録
      • コミュニティゲーム登録サイトで作ったゲームを登録する(クリエータークラブ会員であることが必要)
      • ゲーム内の暴力表現等の有無は自己申告
    • ピアレビュー
      • 他のクリエータークラブ会員が登録されたゲームをダウンロードする
      • ゲームの評価をする
      • 一定のレビュー数に達した時点でコミュニティゲームとして登録される
    • コミュニティゲームを遊ぶ
      • 登録されたゲームはXbox LIVEアーケードのゲームと同じようにXbox LIVE マーケットプレースからダウンロード、遊ぶことができる

     

    ピアレビュー(Peer Review)はクリエータークラブ会員によるゲーム評価プロセスのことです。動画と違ってゲームの場合は、プログラムの���グといった問題があるので、ある程度プログラム知識のある人達によってレビューされる必要があります。

    ここで重要なのはマイクロソフトはシステムを提供するだけでピアレビュー自体には関与せず、ピアレビューはクリエータークラブ会員のみによって行われるので、より自由な発想のゲームが登場する可能性があるということです。

    また、詳細は決まっていませんがゲーム登録するときに、無料、有料を選択することができ、有料にすることで登録者が収入を得ることもできます。

     

    Xbox LIVE コミュニティゲームのサービス開始時期は今年のホリデーシーズンで、ベータ版は春頃開始を予定しています。

     

    正式運用されるまでの10ヵ月はゲームを遊びたい人達にとっては長いかもしれません。

    でも、ゲーム開発者の皆さんにとっては後10ヶ月しかありません。コミュニティゲームのローンチタイトルとして面白いゲームを作ってみてはどうでしょうか?

  • ひにけにXNA

    XNA Game Studio 3.0の新機能

    • 1 Comments

    GDC2008で、XNA Game Studio 3.0の新機能の1つが発表されました。それはWindows、Xbox 360に加えて新たにZuneがサポートされるようになったことです。

    日本では未発売のものですが、Zuneは携帯メディアプレイヤーですが他のプラットフォームと同じようにXNA Game Studioを使ってゲーム開発をすることができます。Xbox 360のように贅沢なGPUとかは搭載していませんし、スクリーンの解像度は違いますが、2Dベースのゲームであれば他のプラットフォームで書いたコードがほぼそのまま使えるようになっています。

    そして、Zune独自の機能として自分のお気に入りの音楽をゲーム中にBGMとして流したり、映像データをテクスチャとして使うことができるメディアAPIが追加されました。

     

    XNA Game Studio 3.0は春頃にレビューリリース予定で、最終リリースは今年のホリデーシーズンを予定しています。

  • ひにけにXNA

    ネットワーク その10 オブジェクト所有権

    • 1 Comments

    クライアント/サーバーピア・ツー・ピア型のどちらのネットワーク形態が優れているのかを議論するのが好きな人たちがいます。しかし、個人的にこの議論は間違ったものだと思います。誰が「どちらか一方のネットワーク形態を選ばなくてはいけない」と言ったんですが?私は両方の長所を活かしたハイブリット形式の大ファンです。

    ネットワークプログラミングは妥協との戦いです。100%の正確さと、ラグがまったく無いという2つの状態を両立することは不可能で、トレードオフをしなければいけません。

    時にはラグが多少大きくっなっても正確さをとる場合もあれば、逆に正確さを犠牲にして高いレスポンスを必要とする場合もあります。

    100万ドルの問題:  どのマシンがどのデータを管理するか?

    ゲーム内のそれぞれのデータについてのコスト/実益の分析を以下の質問に答えることでできます。

    1. そのデータは独立したものですか、もしくはそれ以外のデータと状態を一致させる必要がありますか?
    2. 全プレイヤーがそのデータのラグに対して同等に気にする必要がありますか、それともひとりのプレイヤーが他のプレイヤーよりも気にしなければいけませんか?
    3. そのデータのラグはどれだけ重要なものですか?ゲームデザインを変更することによって、その重要度を低くはできませんか?
    4. データ変化を予測することはできますか?間違った予測をした場合、問題になりますか、それともエラーをスムースに訂正することができますか?

    この質問の答えによって、どのようにデータを管理すればいいのかが判ります。

    1. 沢山のデータの整合性が必要な場合、それらは1台のマシンによって管理されるべき
    2. もしひとりのプレイヤーがデータのラグに対して他のプレイヤーより気にしなくてはいけない場合、そのデータはそのプレイヤーのローカルマシンによって管理されるべき
    3. ラグが大きな問題となる場合は、予測アルゴリズムを適用するべき
    4. 予測ミスが大きな問題となる場合は、予測アルゴリズムは適用すべきではない

    1と2の質問はどのマシンがデータを管理するかの指標になり、3と4の予測アルゴリズムの適用についての質問は別々のものだということに注意

     

    例えば:

    スペースシップの移動

    • スペースシップを操作しているプレイヤーにとってレイテンシは重要な問題。
    • それぞれのマシンでスペースシップの位置が多少ずれていても、大体の位置があっているのなら問題はない。最悪ケースとしては2つのスペースシップがぶつかったときに、それぞれのマシンでは多少違った方向に跳ね返るが、ゲーム的には問題はない。
    • 多少の予測ミスは問題にならない。間違った位置から正しい位置へと、誰にも気づかれること無く補正することができる。

    結論:それぞれのスペースシップはそれぞれのローカルマシンによって管理されるべき。予測アルゴリズムを適用する。

     

    レースの勝敗

    • レイテンシは特に重要ではない。あなたがゴールラインを通過してから0.5秒後に結果が表示されても大丈夫(カッコイイアニメーションやカメラワークで、このレイテンシを隠すことができる)。
    • それぞれのマシンでレース結果が違うのはダメ。勝者は常にひとり。
    • 間違った予測結果は大問題。「優勝おめでとう!!」と表示されて少し経ってから「やっぱり2位でした~」と表示されるのはひどすぎる。

    結論:1台のマシンがレース結果を決めるべき。予測アルゴリズムは適用しない

     

    死亡判定

    • 殺されたプレイヤーにとってレイテンシは重要だが、他のプレイヤーにとっては特に重要ではない。
    • マシン毎に違った結果になるのは問題。もし私が死んでいるのなら、他のマシンでも死んでいなくてはならない。
    • 予測ミスは許されない。「ぐはぁ、やられたー」と叫びながら派手なアニメーションが再生され、血飛沫を撒き散らしながら地面を転がった挙句に「ぐふぅ」と事切れた数秒後に何事もなかったのように生き返るのは問題。

    結論:それぞれのマシンがローカルプレイヤーの死亡判定をするべき。他のマシンは死亡判定の予測をしてはいけない。

    追記:ここでは視覚的な予測を適用することができる。例えば、私のマシンがヘッドショットだと判定するが、ヘッドショットされたプレイヤーの位置が100%確実ではないので死亡アニメーションを再生することはできない。その代わりに「ダメージを受けた」というアニメーションを再生することができ、視覚的なフィードバックを即座に得ることができる。もし、ヘッドショットを決めたプレイヤーから死亡か確定したという連絡が届いた場合は死亡アニメーションへスムースに変化させることができる。仮にこの予測が外れた場合でもダメージアニメーションをキャンセルして通常のアニメーションに戻すことができる。

     

    ビークル(乗り物)に乗る

    私は一度、キャラクターが徒歩で歩き回り、いろんなビークルに乗り降りすることができるゲームのプロトタイプ製作をしたことがありました。ビークルに乗っている間は「スペースシップの移動」の例で説明したのとまったく一緒です。ただビークルに乗ると言うのは違います。一度に複数のプレイヤーが運転することはできませんから!

    この問題を解決するために、まずどのマシンがどのオブジェクトに対して所有権を持っているのかを表すデータをつくりました。これで動的にビークルの所有権の変更ができます。次にどのマシンが所有権を持っているのかを決めなければいけません。

    ビークルのステート(操作、物理シム、衝突判定等)は、そのビークルを運転しているプレイヤーがいるマシンによって管理されます。それ以外に、この所有権を誰が持つべきを決める管理マシンを決めました。もしビークルに誰も乗っていない場合は、管理しているビークルの数が最も少ないマシンに所有権があります。

    ビークルのとなりに立ち「乗る」ボタンを押した時に以下の処理をします。

    • クライアントマシンは「乗せてください」というメッセージをビークル所有権管理マシンに送ります
    • クライアントマシンではキャラクターがビークルに乗り込むアニメーションが再生されますが、まだ所有権がないので運転することはできません。
    • 通常
      • ビークル所有権管理マシンは要求があったクライアントマシンに所有権を渡します
      • ビークル所有権管理マシンからの「乗って良いよ」というメッセージはキャラクターんがビークルに乗り込むアニメーションが終わるまでに届き、ビークルを問題なく運転することができます。この乗り込むアニメーションはラグを隠蔽するわけです。
    • 二人同時に乗り込もうとしたとき
      • ビークル所有権管理マシンは「すまん、君は乗れん」というメッセージを一方に送ります
      • 乗り込み要求を断られたキャラクターは乗り込みアニメーションをキャンセルしてビークルに乗り込む前の位置にもどされます。
      • 先に乗り込み要求を申請したキャラクターはビークルを運転することができます。

     

    ビークルを運転しているときはピア・ツー・ピア形式ですが、大事な決定をするのはクライアント/サーバー形式になっています。

    アイディアとしては大事な決定を下す単一の管理マシンが全てのデータを管理する必要がないということです。マシンAはビークルの乗り降りを管理して、マシンBはパワーアップアイテムの管理、そしてマシンCはセッション終了時に誰が勝敗を決定するといった感じに管理するものを割り振ることができます。

     

     

    これで私がネットワークに関して言わなければならないことの全てです。

    THE END

     

    原文:
    http://blogs.msdn.com/shawnhar/archive/2008/01/03/network-object-ownership.aspx

  • ひにけにXNA

    ネットワーク その9 究極の圧縮方法

    • 2 Comments

    今まで紹介してきた狡猾な圧縮方法より効果的なデータ圧縮方法があります。それはデータ自体を送らないということです。

    もちろん、まったくデータを送らないのでは相手側との同期ができません。でも、時には同期すること自体が重要ではない場合があります。

    2つのルール

    • ゲームプレイに関連するものは同期しなければならない
    • 殆どの物はゲームプレイに関与しない

     

    効果音やアニメーションは殆どの場合は同期する必要がありません。もし、ネットワークを介してキャラクターが前に走って移動する場合、それぞれりマシンではその情報を元に、走るアニメーションを再生させ、そのアニメーションに合わせて靴音を鳴らすことができます。靴音が鳴るタイミングが多少ずれていてもゲームプレイには関係ないのでネットワークを介して足音を同期させる必要はありません。

    ケーススタディ:MotoGPでは5%の確立で相手を抜き去った場合、抜いた相手に向かって手を振り上げるアニメーションを再生します。クラッシュした場合、ライダーが吹き飛ぶ複数のアニメーションの中からランダムで再生され、ネットワークにはどのアニメーションを再生したかという情報は送られません。

    あるプレイヤーから見ると手を振り上げたアニメーションをしているように見えますが、他のプレイヤー視点からはアニメーションが再生されていないという場合があります。クラッシュシーンでライダーの転がるアニメーションはそれぞれのプレイヤーによって違うアニメーションが再生されます。

    これらをネットワークを介して同期するにはネットワーク帯域が足りませんでした。しかし、これらのアニメーションはゲームプレイには関係無いものなので、誰も同期していないということには気づきませんでした。

    同じように、FPS等のゲームでは撃った弾の位置、壁に当たったときにできる弾痕の位置、飛び散るガラス片や薬莢の位置を同期する必要はありません。ここでネットワークに送る情報は単に「私はゲーム内で銃を撃っている」というブーリアン(bool)の情報だけです。このシンプルな情報を元に、各マシンではそれぞれにシミュレーションを行います。多少の違いが起きても、それらの平行世界で起きている事柄がほぼ同じである限りは問題がありません。

     

    一度、この「データをネットワークに送らなくても良いんだ」という考え方が身につくと、いろんな事ができるようになります。

    ケーススタディ:Moto GPで広告看板やパイロンはコース脇に配置されています。もしバイクがこれらのものと衝突した場合、バイクと一緒に吹き飛びます。もし、ゆっくりとした速度でぶつかった場合は看板やパイロンを押し動かすことができます。

    殆どのプレイヤーはレースに勝つことが目的で真剣にレースに参加しますが、中にはレースの邪魔をしようとするグリファー(訳注:オンラインゲームで嫌がらせをするプレーヤーのこと、英語ではGriefer、griefには深い悲しみ、悲痛といった意味があります)がいます。グリファー達はコースを逆走したりして、どうやったらレースをメチャクチャにできるのかを競っています。

    グリファー達は広告看板やパイロンをコースの中央に動かしてバリケードを築けることに気付きました。これで真剣にレースをしているライダー達がバリケードに衝突して派手にほこりを撒き散らしながら吹き飛ぶ筈です。

    でも、実際にはグリファー達の思い通りにはいきませんでした。

    広告看板やパイロンの位置情報はネットワークを介して送られてはいませんでした。単に充分なネットワーク帯域が無かったからです。

    以下は実際にあったことです。

    • あるグリファーはバイクを前後に動かしながら、ゆっくりと障害物を押し進めます
    • ピア・ツー・ピアを使っていたので、そのグリファーは自分のバイクの動きを完全にコントロールできます。
    • 他のマシンでは予測アルゴリズムを使っているので、グリファーのバイクの座標は大体一緒ではありますが、完全に一緒ではありません。
    • グリファーのマシン上ではグリファーの思い通りに障害物はコースの真ん中へ移動します。
    • 他のマシンでは最初の押しが少しだけ左にずれました。この段階では障害物の位置はグリファーが移動させた位置に近いですが一致している訳ではありません。更にグリファーが障害物を押し進めるうちにグリファーが実際に移動させた位置とはズレが生じ、最終的に他のマシン上では障害物はグリファーと当たらない位置に移動して、そのまま動かなくなります。それらの障害物の位置は同期されていないので、この矛盾は修正されません。

    結果

    • グリファーのマシン上では立派なバリケードが完成、他のマシン上ではバリケードができていない
    • レースを真剣にしている人達のマシン上ではバリケードが無いので問題なくレースを楽しむことができます。
    • 一方、グリファーのマシン上では立派なバリケードがあるので他のプレイヤー達はグリファーの思惑通りにバリケードに衝突してライダーは派手に中に舞い、バイクは火花を飛ばしながら滑っていきます。そのしばらく後に新しい情報がネットワークから届きます。あれ?実際には衝突は起きていなかった?この時、吹き飛んだはずのプレイヤーはコース上に配置され、問題なくレースは続行されます。

    100万人ものプレイヤー達が何時間もプレイしたにも関わらず、誰もこの矛盾には気づきませんでした。

    グリファーは盛大なクラッシュシーンを見て喜び、

    真剣にレースをしている人達は無事にレースを終えることができ、

    みんないつまでも幸せにレースを楽しんだとさ、めでたしめでたし

    原文:
    http://blogs.msdn.com/shawnhar/archive/2008/01/01/network-compression-just-say-no.aspx

  • ひにけにXNA

    ネットワーク その8 算術符号化圧縮

    • 3 Comments

    算術符号化は解りづらく、めったに使われないツールのひとつですが、時々その威力を発揮します。例えるならネットワークデータ圧縮における変なサイズの六角レンチです。

    算術符号化はクールです。なぜなら、あまり知られていないし、一見すると動かないように思えるものだからです。パーティーで女の子に好印象を持たせるのに活躍します(訳注:そうか?)

    以下のデータがあったとします。

        enum Species    // 種類
        {
            Camel,          // ラクダ
            Cat,            // ねこ
            Caterpillar,    // いもむし
            Cheetah,        // チーター
            Chimpanzee,     // チンパンジー
            Cobra,          // コブラ
            Cormorant,      // 鵜(う)
            Cougar,         // クーガー
            Coyote,         // コヨーテ
            Crab,           // カニ
            Crocodile,      // ワニ
        }
    
        Species animalA;
        Species animalB;
        bool whoWon;

    (そう、私たちは世界中の男の子が一度は思う「クーガーとワニはどっちが強いか?」という疑問に答えるためのゲームを作るんだ(訳注:ねこが最強))

     

    ビットフィールド圧縮を使った場合、animalAとanimalBの変数には11種類の動物がいるので、その情報を格納するのにそれぞれ4ビットが必要になり、誰が勝ったのかという情報で1ビット必要になるので合計で8ビットを超えてしまいます。

    でも、ちょっと待って

    animalAに格納する値の組み合わせは11種類、animalBも11種類、そしてwhoWonには2種類の値です。全ての組み合わせを考えると11× 11× 2の合計242通りになります。これならbyteに収まりそうです。

    ここでは4ビットを動物の種類に割り当てていますが実際は11種類必要なところに16種類を表すだけのスペースを用意してしまうというのはビットフィールド圧縮の問題です。この中途半端な組み合わせをコンパクトにする方法があれば良いと思いませんか?

    前の記事のビットフィールド圧縮の部分を、乗算、除算、そして剰余算に変更することで解決することができます。

     

        void ArithmeticEncode( ref int encoded, int valueRange, int Value )
        {
            encoded *= valueRange;
            encoded += value;
        }
    
        int encoded = 0;
        ArithmeticEncode( ref encoded, 11, (int)animalA );
        ArithmeticEncode( ref encoded, 11, (int)animalB );
        ArithmeticEncode( ref encoded, 2, whoWon? 1: 0 );
        packetWriter.Write((byte)encoded);

     

    そして、読み込み側は、ビットフィールドの時と同じようにパラメーターを逆順に読み込みます。

        void ArithmeticDecode( ref int encoded, int valueRange )
        {
            int value = encoded % valueRange;
            encoded /= valueRange;
            return value;
        }
    
        int encoded = packetReader.ReadByte();
        whoWon = ArithmeticDecode( ref encoded, 2 ) != 0;
        animalB = (Species)ArithmeticDecode( ref encoded, 11 );
        animalA = (Species)ArithmeticDecode( ref encoded, 11 );

     

    どう?クールでしょ?

     

    原文:
    http://blogs.msdn.com/shawnhar/archive/2007/12/30/network-compression-arithmetic-encoding.aspx

  • ひにけにXNA

    ネットワーク その7 ビットフィールドで圧縮

    • 2 Comments

    ビットフィールドは古くから知られている素晴らしいデータパッキング手法です。C#プログラマーがビットフィールドを使う機会は非常に少ないですが、ネットワークパケットの圧縮にはもってこいなので、この機会に使ってみましょう。

    バイトは8ビット、intは32ビット。でも、送るべきデータが8ビットや32ビットの倍数にならないときはどうします?例えば以下のようなデータを送るとします。

        bool isAlive;       // 生きているか?
        bool isFiring;      // 撃っているか?
    
        enum Species    // 種類
        {
            Camel,          // ラクダ
            Cat,            // ねこ
            Caterpillar,    // いもむし
            Cheetah,        // チーター
            Chimpanzee,     // チンパンジー
            Cobra,          // コブラ
            Cormorant,      // 鵜(う)
            Cougar,         // クーガー
            Coyote,         // コヨーテ
            Crab,           // カニ
            Crocodile,      // ワニ
        }
    
        packetWriter.Write(isAlive);
        packetWriter.Write(isFiring);
        packetWriter.Write((byte)species)
    

     

    これで3バイトになりますが、実際にはそんなにいりません。ブーリアン型は1ビットしか必要としませんし、生物の種類も11種しか居ないので4ビットで足ります。

    それぞれのフィールドにどれだけのビット数が必要かが判れば、ビットシフトすることによって複数のビット群を1つbyteやintにまとめることができます。使いやすいように、指定されたビット群をbyteやintといった値にまとめるメソッドを作るといいでしょう。

    ここでは、先の例では3バイトを送っていたところを1バイトにまとめて送ることができます(まだ2ビット余ってる)

        void AddToBitfield( ref int bitfield, int bitCount, int value )
        {
            bitfield <<= bitCount;
            bitfield |= value;
        }
    
        int bitfield = 0;
        AddToBitfield( ref bitfield, 1, isAlive? 1: 0 );
        AddToBitfield( ref bitfield, 1, isFiring? 1: 0 );
        AddToBitfield( ref bitfield, 4, (int)species );
    
        packetWriter.Write((byte)bitfield);
    

     

    AddToBitfieldメソッドは2つのことをします

    • 現在のビットフィールドを左シフト(「<<」演算子を使う)して、新しいフィールドを確保
    • 新しいビット群を設定(「|」演算子を使う)

    読み込むためには、逆のプロセスを行います。

     

        int ReadFromBitfield( ref int bitfield, int bitCount )
        {
            int value = bitfield & ((1 << bitCount) - 1 );
            bitfield >>= bitCount;
            return value;
        }
    
        int bitfield = PacketReader.ReadByte();
        species = (Species)ReadFromBitfield( ref bitfield, 4 );
        isFiring = ReadFromBitfield( ref bitfield, 1 ) != 0;
        isAlive = ReadFromBitfield( ref bitfield, 1 ) != 0;
    
    

    書き込んだ順番とは逆に読み込んでいることに注意してください。

    ReadFromBtfieldメソッドはAddToBitfieldと逆のことをします。

    • ビットフィールドの下位ビットからビット群を取り出す(「&」演算子を使う)
    • 残りのビットフィールドを右にシフトする(「>>」演算子を使う)

    ビットフィールドはenumやブーリアンをまとめるのに有効ですが、他の数値データも前回紹介した量子化の手法と組み合わせることで格納することができます。

    原文:
    http://blogs.msdn.com/shawnhar/archive/2007/12/28/network-compression-bitfields.aspx

Page 1 of 1 (7 items)