Top > ComputerGraphics > XNA > Sprite > TextureAnimation1
Last-modified: Sat, 20 Oct 2012 06:35:24 JST
Counter:5736 Today:1 Yesterday:2 Online:9
このエントリーをはてなブックマークに追加

スプライト・テクスチャのアニメーション1

About

StarAnimation.gif

 2Dゲームを作ったりメニューを表示する場合に、SpriteBatchを使ってテクスチャを表示することがあります。ここでは左の画像のように、テクスチャをアニメーションさせる方法について解説します。一応MicrosoftのMSDNにも「サンプル」が掲載されていて、ここではそれを噛み砕いて改良した形になっています。サンプルはXNAで作られていますが、アルゴリズムは異なるプラットフォームでも有効に利用できると思います。2回続きの項目で、次は「スプライト・テクスチャのアニメーション2」です。

基本的な処理の概念

0.png 1.png

 周知の通り、”アニメーション”は複数の連続する絵から構成されます。図の上の様に、0~6まで通し番号が付けられた各絵を用意したとすると、この7枚を連続して切り替えることでアニメが完成します。ゲームではこれらの連続する絵を等間隔に並べて、1枚のテクスチャとして用意することが多く、ここでもそれに倣って同じようにします。これは2Dゲームが普及した時代から使われてきた伝統的な手法です。  ここでアニメーションを構成する1枚の”絵”は一般的に「フレーム」と呼ばれるので、ここでもフレームと呼ぶことにします。ちょうどFPS(Frame Per Second)のFrameです。

 すると「1枚にしてしまってどうするんだ!?どうやってアニメーションとして見せるんだ!?」と、なる気がしますが心配することなかれ、下の図のように、”そのテクスチャ(画像)のこの部分を表示する”といった処理を行うことでフレームを連続して見せることでアニメーションとします。赤い矩形領域を右側に一つずつずらしていけば、アニメーションが完成するといった算段です。最後のフレームまで行ったら、また最初のフレームに戻るなどします。

サンプルで用いるアニメーション用画像

「完成予想アニメーション」

StarAnimation.gif

「StarAnimation1.png」

StarAnimation1.png

「StarAnimation2.png」

StarAnimation2.png

 ここでは左の画像の様なアニメーションを実現することでテクスチャアニメーション実装を確認します。アニメーション用の素材を用意していれば、それを利用しても問題ありません。ただし各フレームのサイズは一律であることが必須条件なので注意する必要があります。MSDNで紹介されているサンプルでは、中央の「StarAnimation1」の様な横一列にフレームが並べられた画像を使っていて、アルゴリズム的に、"その様な画像"しか扱えないようになっていますが、ここでは右の「StarAnimation2」の様な、任意の行・列でフレームを並べた画像も扱えるようにします。

※ここに掲載した図は見やすさのため背景を黒にしています。サンプルプログラム中に同梱されるテクスチャは黒い部分が透過されていることに注意してください。

不適切なアニメーション用画像

 用いる画像素材はWeb上でフリーに配布されているものでも良いし、自作しても良いです。ここで用意した画像はMicrosoftのPowerPoint他を利用して作成しています。基本的には各フレームの幅が統一されていればどんな素材でも利用できます。ただし次の様な構造であるアニメーション用テクスチャは利用できないので注意してください。また一般にこれらの様な素材は扱われてないと思います。

1.「並び順が左上→右下になっていない」 3.png

フレームの順序は左上から右下へ向かう必要があります。 2.png

2.「行・列の数が揃っていない」 4.png

途中で抜けがあり、各行や列の数が揃えられていないのも問題です。図の場合は7が欠損していると見てください。この図の様な場合は行*列が1*7でしか設定できません。

実装

 基本的にはサンプルプログラムを見てもらうとして、ここではポイントだけ解説します。

必要な値とコンストラクタ

 まずはコンストラクタから確認します。初期化に必要な値を確認しておきましょう。まず全てのフレームが含まれるテクスチャ、そのテクスチャは何行(Row)、何列(Column)から構成されるか、そしてそのアニメーションのFPS、これらを引数にとって初期化します。texture、Row、Columnは説明するまでもありませんが、FPSについては改めて説明しておきます。
 FPS:FramePerSecondとは、1秒(Second)あたりに(Per)、何フレーム(Frame)表示するかを示す値です。つまり30FPSならば1秒あたりに30枚のフレームが表示されます。一般に24FPSあればリアルタイムと呼びます(しそう見える)が、XBOX360も然り、最近のゲームは60FPSで構成されていることが多いです。
 RowとColumn、Textureの幅と高さが分かれば、1フレームあたりの幅(Width)と高さ(Height)を求められるので保存しておきます。これは、テクスチャ全体の内、どの範囲を表示するか決定するために必要な値になります。またそのテクスチャに含まれる最大フレーム数(FrameCount)は、Row*Columnで求められます。最後までフレームが進んだかを判定するために利用するので、これも算出して保存しておきます。

 最後に現在のフレームを示す値(currentFrame)、合計進行時間(TotalElapsedSecond)、停止状態を示す値(Paused)を初期化しておきます。

 public AnimatedTexture2D(Texture2D texture, int frameRow,
                         int frameColumn, int framePerSecond)
        {
            //テクスチャを保持
            this.texture = texture;

            //1秒あたりに何フレーム表示するか、から1フレームあたり何秒表示するかを逆算する
            this.SecondPerFrame = (float)1 / framePerSecond;

            this.FrameColumn = frameColumn;
            this.FrameRow = frameRow;

            //テクスチャに占める1つのフレームの幅は、テクスチャの幅/フレーム数で求められる
            this.frameWidth = texture.Width / frameColumn;
            this.frameHeight = texture.Height / frameRow;
            this.FrameCount = frameColumn * frameRow;

            //初期化
            this.currentFrame = 0;
            this.TotalElapsedSecond = 0;
            this.Paused = false;
        }

フレームの描画

 アニメーションさせることよりも先に、フレームの描画について説明しておきます。先に紹介した処理概念の通り、1枚のテクスチャ画像の中から特定の領域を決定して描画することでアニメーションを再現します。

 元々、SpriteBatchにはテクスチャの一部を矩形領域(長方形、つまりはRectangle)として指定して、その部分だけを描画する機能が実装されています。そこで、今表示したいフレームを、その矩形領域として設定することで、上手くその領域だけを表示させることができます。

 矩形領域(Rectangle)は、左上の座標X・Yとその高さ・幅の4つのパラメータから構成されます。つまり先に保存したフレームの幅(Width)と高さ(Height)から求めることが出来るわけです。またフレームの左上の座標は次の式から求めることが出来ます。

  • プログラム式
    • 左上座標のX = frameWidth * (frame % FrameColumn),
    • 左上座標のY = frameHeight * (frame / FrameColumn),
  • 日本語式
    • 左上座標のX = フレーム幅 X (指定したフレーム番号 / フレームの列数)の余り
    • 左上座標のY = フレーム高 X (指定したフレーム番号 / フレームの列数)

 ここでフレーム番号を次から次へと順に変化させていけばアニメーションが成立します。指定したフレームをピンポイントで表示する需要があるため、このメソッドではこの様な実装となっていますが、実際には「currentFrame」を指定することになります。次の項目で、このcurrentFrameの値を順に変動する方法を説明します。

        public void DrawFrame(SpriteBatch spriteBatch, int frame, Vector2 position,
         Color color, float rotation, Vector2 origin, float scale,
          SpriteEffects spriteEffects, float layerDepth)
        {

            Rectangle sourceRect = new Rectangle(
                frameWidth * (frame % FrameColumn),//左上
                frameHeight * (frame / FrameColumn),//左上(ここが厄介)
                frameWidth,//幅
                frameHeight//高さ
                );

            spriteBatch.Draw(texture, position, sourceRect, color,
             rotation, origin, scale, spriteEffects, layerDepth);
        }

フレームを次々に変更する方法

 先の説明の通り、指定するフレームを次から次へと順に変更して描画することでアニメーションが成立します。ここでは次に描画するフレームを値「currentFrame」で設定します。この値はコンストラクタで「0」に初期化しているので、一番最初に描画されるのは0番目のフレーム、ということになります。

 まず現在の停止状態を示す値(Paused)がtrueだった場合は、全ての処理をreturnによってスキップして終了します。例えばもしcurrentFrameの値が0、Pausedの値がtrueであった場合、currentFrameの値は0から延々値が変わらないので、ずっと0番目のフレームが描画され続けることになります。

 TotalElapsedSecondは、アニメーションが開始されてから、現在までに経過した時間を示す値です。Gameが更新される度に、その更新にかかった時間を加算していくと求められる値です。例えば、Gameの更新に毎度0.16秒かかるとすると、Updateする度に0.16秒ずつ経過時間が加算されていくことになります。
 さて先の通りFPSから逆算して、1フレーム当たりの時間を求めることが出来ました(TimePerFrame)。つまりこの1フレームあたりの時間が経過していたら、次のフレームに移動する、という処理を実行すれば良いことになります。
 今回の処理のコアはここにあります。連続するフレームなので単純にcurrentFrameに「+1」してやれば良いのです。ただし、フレームが最後まで進んだ時は、最初のフレーム、つまりcurrentFrameの値を0に戻してやる必要があります。総フレーム数(FrameCount)が例えば5だったとすると、最後のフレームの番号は「5-1=4」で「4」になります。そこで総フレーム数で割った余りを利用してやると、自動的に「0~4(フレーム数は5)」になるのがお分かりいただけるでしょうか。

 最後に、フレームを1つ進めたときには、その分時間を戻してやる必要があります。これでまた所定の時間が経過すると、フレームが1つ先へ進みます。

        public virtual void Update(float totalElapsedSecond)
        {
            //一時停止中ならUpdateを実行しない
            if (Paused)
                return;

            //合計時間を加算
            TotalElapsedSecond += totalElapsedSecond;

            //合計時間が1フレームあたりの時間を超えたら
            if (TotalElapsedSecond > SecondPerFrame)
            {
                //フレームを次のフレームへ
                currentFrame++;
                //フレームの値は、0~フレーム数で調整する(ifはなるべく使わない)
                currentFrame = currentFrame % FrameCount;
                //合計時間から、フレームあたりの時間を引く
                TotalElapsedSecond -= SecondPerFrame;
            }

        }

サンプルのその他のメソッド

 サンプルプログラム中のその他のメソッドは、アニメーションを停止したり、関連する値を取得するためのものが主であるため、そのままソースコードを参照されたい。

Result

result.png

 実行結果は画像の通り、実際にはアニメーションしている点に注意されたい。表示した二つの星が同じ動きをしていることが確認できるハズです。動作速度を変更してみたり、停止して再開してみたり、サンプルプログラムを動かして確認してください。

この様な実装方法になった理由

 ゲームプログラミングの正攻法なぞ、ロクにゲームを作ったことが無い私には分かりませんが、大凡次の理由が考えられます。

なぜ全てのフレームを1枚のテクスチャ画像にまとめる必要があったのか


1. コストの削減

 最初はTexture2Dの配列を用意して、そこに各フレームを読み込んで、順に入れ替える様な実装方法を考えていました。こうすればいちいち繋がったテクスチャを用意する必要もありません。なぜこの実装方法がダメかというと、テクスチャ画像をメモリ上に展開して、その設定を複数回に渡って切り替える、という処理が比較的高コストであるためです。次はこの画像、次はこの画像とメモリ上に展開し、その値を入れ替えるのは非効率です。そうであれば、ここで紹介した実装方法の様に、必要な分は全て上げてしまって、どこからどこまでを利用する、という設定の方が断然効率が良いのです。
 現在のフレームがどのフレームであるか、を計算するのにコストがかかる様に見えますが、アニメーションの度に画像を変更するのも大概であるし、現在のゲームでは2Dの処理ごときでメモリもCPUもそこまで気にする必要の無いケースが殆どです。「ってアレ!?じゃあどっちの実装方法でも良いじゃん!?」と思いますが、以下の2つの理由もあって、やっぱり1枚のテクスチャ画像にまとめた方が良さそうです。


2. 手間の削減とミスの防止

 必要なテクスチャ画像はどれか、という選択を行うコストが減ります。例えばコンストラクタで、利用するテクスチャはコレとコレとコレとコレなんて設定するのは結構な手間になります(XNAのLoadContentでTexture2Dを作る様を見ていれば周知の通り)。また紹介した実装方法であれば、アニメーションに必要なテクスチャを選択し忘れることもなくなるでしょう。本当はテクスチャAとテクスチャBとCとDとEとFとを読み込まなければいけないのに、Cを誤って読み込み忘れた、もしくは誤ってCの領域を解放してしまった、など。そういった事態になり難い設計です。


3. リソースとしての管理の容易さ

 リソースの管理の観点から言及しても1枚であることにメリットがあります。例えばフレームが全て別のファイルであった場合、「StarAnimation_00.png」から「StarAnimation_32.png」まで合計33枚の画像を用意してプロジェクトに取り込む、何てことをしなければいけなくなります。先の通り、もしかしたらどれか取り込み忘れるかもしれないし、枚数が100枚やもっと大きくなると大変なことになります。専用のファイルフォーマットを作っても良いかもしれないですが、少なくとも個人で制作する限りじゃそれは大変な作業になるわけで、そんなことはしたくありません。1枚の画像に全てのフレームがまとまっているなら、「StarAnimation.png」「AttackAnimation.png」などと管理することが出来るようになります。
 もっと言うと、前後でどのようにアニメーションするのかも1枚の中で確認できた方が良いし、"誤って順番が前後している"場合や、"1ドット(Pixel)ずつ上下にずれている"場合なども気がつきやすくなります。作るのは多少面倒ですが、それなりにメリットも大きい気がしませんか。

なぜ横一列のテクスチャにしないのか

CharacterWalk.png

キャラクタ歩行のサンプル画像 出典「Made in GAPAN 歩~Ayumu~」
URL「http://freett.com/gepponkoku/mat_32/human32.htm|
基本改変・利用が許可されていたので利用しました。

 これも実装時には結構悩みました。実際MSDNで公開されているサンプルでは横一列しか対応していないわけですし。なぜ横一列が良さそうであるかというと、計算コストが下げられるためです。フレームの矩形領域を指定する場合に、Yは常に0であるため、除算が1回分減ります。更にはFrameHeightの値なども必要が無くなるわけです。たかがそれだけと見るべきか、そうでないかは判断しかねますが、極力、軽く高速なプログラミングを行いたいのは性です。
 で、実際には横一列のみを対象とすることを止めたのですが、結局現在のマシンパワーはそれくらいどうということは無いし、少しの計算増加は無視することにして、リソース生成や管理の観点から複数の行列に対応することに決定しました。単純に横一列に延々と並んだフレームは見難いのです。例えばメニュー用画像などで横長の文字が延々並んでいると最悪です。折り返してパッと一目で見せてもらった方が、それがどういうアニメーションか分かり易いです。その上、旧来のリソースのほとんどが横一列になっていません。複数行列に跨っていることが多く、それらをチマチマと加工するのが非常に面倒だ、というのもありました。リソースは財産です。
 次の項目に関わることに言及すると、例えば画像の様に、RPGツクールなどでキャラクターの歩行アニメーション素材があります。あれは一枚のテクスチャ中に、全方向分のアニメーションに関わるフレームが含まれているわけです。横一列に並んでいて、0~4番は下に移動するフレームのセット、5~9番は上に移動するフレームのセット、とフレームを延々横に並べられるよりは、1行目は下に移動するセット、2行目は上に移動するセット、と並んでいた方が見やすいし、管理しやすいし、実際そうなっています。

 というわけで次は特定のフレームを指定してアニメーションさせられるように、今回のプログラムを拡張します。

 実際にはメモリ上に展開できる画像の解像度制限の都合もあるようです。例えばipad2とOpenGLESの組み合わせでは、テクスチャの最大解像度は「2048 x 2048」と定められています。環境によって異なりますが、横に長い・縦に長い解像度だと入り切らなくなるので、やはり縦横にフレームを配置するテクスチャを用意するのが一番です。

References