◆PROCESSING 逆引きリファレンス
カテゴリー:動画・アニメーション
アニメーションを行うには
【解説】
ゲームでは弾と敵が衝突した時や、イベントが発生した時(宝箱をGETした、ボスが出現したなど)にアニメーションを表示したい場合があります。
アニメーションの種類にもいろいろありますが、ここではパラパラ漫画風のアニメーションを表示することを考えてみます。
パラパラ漫画はご存知ですよね?。あらかじめ用意された少しづつ絵柄が異なる画像を高速で連続表示して、動いているように見せかけるテクニックです。
教科書の端に漫画を書いて、パラパラめくって遊んだ記憶がある方もみえるのではないでしょうか?。あれです(笑)。
これをPROCESSINGで実現する方法を考えてみましょう。
といっても難しいことはなく、PROCESSINGの描画処理関数(draw)が回るごとに、異なる絵をどんどんと表示させればOKです。
(画像URL:illust-AC 様:ターシィ教授さん)
PROCESSINGの描画処理関数(draw)は、デフォルトで 1/60秒=約16.6ミリセカンド毎にグルグルと繰り返し実行されます。
このタイミングで、表示する絵柄(アニメーションの1フレーム)を切り替えて表示するのです。
処理の理屈は単純です。ちょっと工夫がいるとすれば、なるべく滑らかに表示を行う事でしょうか。
というのも描画処理関数(draw)は、かならず一定速度でループするわけではないからです。
描画処理関数(draw)の内部で重たい処理を実行すると(例えば、たくさんの画像を表示するなど)、呼び出されるタイミングに遅延が発生します。遅延が発生すると、本来期待した画像が表示できず、アニメーションがガタガタする事になります。
(画像URL:illust-AC 様:ターシィ教授さん)
遅延が発生しても(なるべく)スムーズに表示する方法はいくつか考えられます。
1つの方法として遅延が発生した「次の」描画処理で、前回描画できなかった画像をまとめて表示してしまう方法があります。
(画像URL:illust-AC 様:ターシィ教授さん)
遅延が発生したか否かを知るには、前回の処理から今回の処理までの経過時間を測定すればOKです。前回の処理から今回の処理までの時間が、1フレーム(16ミリセカンド)よりも長ければ、処理が遅延したことになります。
今回は経過時間を測定するために、PROCESSINGがもつミリセカンド取得関数(millis)を利用してみたいと思います。
【構文】
int mil = millis( ) ;
【パラメータ】
mil : PROCESSINGが実行開始されてからのミリセカンド
【注意】
int 値の範囲(+2147483647)を超えるミリセカンド=約596時間=24.8日は扱えません。まぁ、PROCESSINGのプログラムを連続で25日以上も動作させる人はいないとは思いますが・・・。
上記で紹介した工夫を行っても、あまりにも遅延が酷いとアニメーションの表示に違和感が出ます。
シューティングゲームでは遅延が発生すると、敵や弾の動きがガクガクする事になります。なめらかな動作をさせる為には、描画処理関数(draw)内部の処理は、なるべく早く処理完了するようにしないといけないですね。
【関連記事】
サンプルプログラム
アニメーション例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
//-------------------------------------------------------- //アニメーションのシーン(一連の表示したい画像の集まり)を //管理するクラス //-------------------------------------------------------- class aniScene{ ArrayList<PImage> aniFrame; //個々の画像フレーム int frameIndex; //表示フレーム番号 //コンストラクタ // cx : 1つの画像の幅 cy : 1つの画像の高さ // fx : 横に並んでいる数 fy : 縦に並んでいる数 // fileName : 集合画像ファイル名 aniScene( int cx, int cy, int fx, int fy, String fileName ){ aniFrame = new ArrayList<PImage>(); frameIndex = 0; //集合画像を読み取り、縦横サイズと数をもとに個々の画像 //に分割する PImage base; base = loadImage( fileName ); for( int ycount = 0; ycount < fy; ycount++ ){ for( int xcount = 0; xcount < fx; xcount++ ){ PImage work; int x = xcount * cx; int y = ycount * cy; work = base.get( x, y, cx, cy ); aniFrame.add( work ); //分割したものをフレームに押し込む } } } boolean disp( int x, int y ){ PImage work; //先頭から順番に取り出して表示する work = aniFrame.get( frameIndex ); image( work, x, y ); frameIndex++; boolean ret = false; if( frameIndex > aniFrame.size() - 1 ){ //最後まで取り出したら、表示フレーム番号を元に戻す frameIndex = 0; ret = true; //[全部表示したよ]と返す } return( ret ); } } //-------------------------------------------------------- //アニメーション管理クラス //-------------------------------------------------------- class anime { //シーンと表示座標をセットで管理するためのクラス class sceneMng { aniScene ds; //シーン int dx; //表示横座標 int dy; //表示縦座標 sceneMng( aniScene s, int x, int y ){ ds = s; dx = x; dy = y; } } //複数のシーンを、まとめて管理するリスト ArrayList<sceneMng> sceneList; anime(){ sceneList = new ArrayList<sceneMng>(); } //シーンを追加する //sn : 追加するシーン x / y : 表示位置 void addScene( aniScene sn, int x, int y ){ sceneMng sm = new sceneMng( sn, x, y ); sceneList.add( sm ); } void disp(){ boolean dispEnd = false; //管理リストの先頭から、すべてのシーンを取り出して //順番に表示する for( int index = 0; index < sceneList.size(); index++ ){ sceneMng dispSheen = sceneList.get( index ); dispEnd = dispSheen.ds.disp( dispSheen.dx, dispSheen.dy ); if( dispEnd == true ){ //該当シーンの画像がすべて表示されていたら //そのシーンを表示候補から除外する sceneList.remove( index ); } } } } anime animeList; //アニメ管理変数 aniScene bombScene; //爆発アニメ final float TARGET_FPS = 60.0f; // 目標フレームレート final float FRAME_TIME = 1000.0f / TARGET_FPS; // 1フレームの処理時間 int lastUpdateTime = 0; // 更新時刻 float passageTime = 0.0f; // 前フレームからの経過時間 void setup(){ size( 400,400 ); animeList = new anime(); frameRate( TARGET_FPS ); // 1/60秒で描画 } void draw(){ background( 255 ); //前回から今回までの経過時間を計測する int curTime = millis(); //現在のミリセカンド passageTime += curTime - lastUpdateTime; //前回からの経過時間 lastUpdateTime = curTime; //今回の更新時刻を記録 //処理遅延があった回数だけ、まとめて実行する for( ; passageTime >= FRAME_TIME; passageTime -= FRAME_TIME) { animeList.disp(); } } void keyPressed(){ //キーがおされたら、ランダムな位置に爆発アニメを描画する bombScene = new aniScene( 80, 80, 8, 2, "bomb.png" ); animeList.addScene( bombScene, (int)random(80,320), (int)random(80,320) ); } |
※上記処理の一部に、kitao’s blog 様の記事を参考にさせて頂きました。ありがとうございます。
爆発画像(bomb.png)は縦横が80ピクセルの爆発シーンが、横に8個、縦に2個集まったものです。これを aniScene クラスのコンストラクタ内部で切り出しています。
(画像URL:HSP3オフィシャル2D素材 様)
anime クラスは、アニメーションさせたいシーンを管理する為のクラスです。animeクラス内部にある sceneList で、表示したいシーンの一覧を管理しています。
上記サンプルを実行すると、キーボードが押されるたびに画面に謎の爆発が発生します(笑)。
<出力サンプル>
(画像URL:HSP3オフィシャル2D素材 様)
下記はサンプルプログラムをP5.jsで書き直したものです。
キーボードの押下ではなく、マウスクリックで表示するように変更してあります。また爆発アニメをランダムな位置に描くのではなく、マウス座標に描くようにしました。
本ページで利用しているアイコン画像は、下記サイト様より拝借しております。各画像の著作権は、それぞれのサイト様および作者にあります。