Jan.27.2000
スプライト管理の実装について。
以前スプライトを表示するための機構を説明したが、その際上位で
管理するべき事として後回しにしていた「優先順位を誰がマネジメントするか」
という部分を今回実装する。
この段になるとプログラムの方法ではなく、実装形態というか概念的な
事になり明確な正解が存在はしない。ここでは私(れろれろ@ふみ)流の
やり方はこんな感じです、ということだけを提示し、皆様においては
「こういう風なやりかたもあるって感じね」と軽く流しておくのが
良いと思う。
スプライトというのは割と小さめの絵をセル画状に扱い、そのセル画が
何枚も重なって一枚の画面を構成するものです。一般的には一昔前の
ゲーム専用ハードウェアが専門に持っていた機構で、ソフトウェアで
実装されている場合あまりスプライトとは呼びません。言うなれば
そのハードウェアによるスプライトをエミュレートする機構といった
ところでしょうか。
スプライトの最も大きな特色に「優先順位」があります。個々のスプライト
は固有の優先順位を持っており、互いが重なった場合優先順位の高い
スプライトが手前に表示される仕組みになっています。
今回提示するスプライト管理ではこの優先順位の実装となります。
また併せて、変化があったところのみを書き換えて、処理時間を最小に抑える
技法を実装します。
rr_screen.c においてスプライトオブジェは RRSprite という構造体で
実装されています。もちろん RRSprite は構造体定義でしかありませんから
利用する際は変数定義などでメモリーをアロケートしてそれを RRSprite として
用いることになります。
今目標とするのは複数のスプライトオブジェ間の優先順位管理ですから、
それら RRSprite が勝手に散らばっていては困ります。そこで、ある程度
一箇所でスプライトオブジェのワーク(ポインタ)をまとめて記録しておく
必要があります。さらに優先順位をその中で持たなくてはなりません。
最も簡単な実装は RRSprite の配列を作ることです。その配列のインデックスが
小さければ手前、大きければ奥という風に取り決めてしまえば優先順位を
実現する事ができます。実際、ゲーム機のハードウェアスプライトは
この手法に近いものとなっています。しかし、これだと配列の大きさが
最大スプライト管理数を決めてしまいます。また、奥行きを表現するために
歯抜けのインデックスを使用したり、オブジェとオブジェの間にオブジェを
挿入とかいったことが困難で、結果無駄に大きめの配列を使う事になります。
さて、大量のポインタを扱い、順位の検索などが容易になる手法はなんで
しょう。答えの一つはリスト構造です。そこで glib の GList リスト管理
でスプライトオブジェクトを管理してみましょう(GSListでもいいけど)。
スプライトオブジェひとつひとつはメモリーアロケートで生成し、
そのポインタを GList 構造で管理します。GList を優先順位でソートし、
ソート結果の順番にそって表示すれば優先順位管理が行えるという
わけです。
rr_screen.c 内の RRSprite では奥行きや優先順位に関する情報を持って
いません。ある意味表示に必要な情報のみを持っているとも言えます。
そこでスプライト管理する為に優先順位情報を持たせる必要があるわけ
ですが、それは「RRSprite を内部に含め持つ構造体」を新たに定義して
実現します。
つまり RRSprite はあくまで表示のためのローレベル構造体で、優先順位は
その上のレベルの構造体にて定義される、といった概念を具体化します。
実際に定義したスプライト管理用構造体は RRSObj とし以下のように
定義します。
struct _RRSObj { gboolean sw; /* 表示スイッチ */ gboolean update; /* 再描画が必要かどうか */ RRSprite obj; /* 表示する Sprite */ RRSprite obj_old; /* 前回の Sprite データ */ gint z; /* 重ね合わせの順位 */ gint z_old; /* 順位前回のデータ */ };obj と z がスプライト管理に必要な要素です。update, obj_old, z_old は再描画が必要か否かの情報で、これらを用いて描画する箇所を積極的に 減らします。表示するかどうかのスイッチ sw は RRSprite の中にも 同等の指定があり二重化してしまいました、正直設計ミスです (^^;
ここでちょっと優先順位管理から離れていきなり話がでてきた再描画管理に
飛ばします。
画面を表示するに、毎フレームイメージを消去して一から描き直し、一画面分
描き上がったところで画面にぽんっと転送。これができるのならばなんら
問題はないのですけれども、実際は毎フレーム描き直しというのは処理が
重すぎます。特に X-Window でやっていると物凄くといっていいほど遅いので
なるべく描画領域を減らし、必要最低限のところを描き直すだけにする事により
ちょっとでも処理を軽くしようという作戦です。
スプライトオブジェが前のフレームとまったく同じ状態で存在していた場合
これは前フレームのままで良いわけですから描き直す必要はありませんね。
故に描画を省略できます。では、スプライトオブジェがどういった状態の
とき描き直さねばならないのでしょうか。もっともわかりやすいのが
「移動したとき」でしょう。それ以外だと大きさが変わったとき、形が
変わったとき、絵が変わったとき、…つまりスプライトオブジェのなんらかの
情報が変わったときです。これを検出するためには1フレーム前の
スプライトオブジェの状態と今の状態を比較し、変化があったら再描画の
必要ありとします。RRSObj の obj_old, z_old はこのために存在します。
再描画はどのように行うのでしょうか。それは前フレームのスプライトオブジェ
を消して、現在のスプライトオブジェの情報で描き直すことによって行われます。
この消すというアクションは前フレームのスプライトオブジェを背景、
もしくは背景色で塗りつぶすことによって行われます。このため、スプライト
管理ルーチンには背景管理も含まれます。
実際に再描画するかどうかは RRSObj の update フラグが立っているか否かで
判断します。アプリケーション側で RRSObj の情報を書き換えたら積極的に
update フラグを TRUE にすることで明示的に再描画させます。まあ、これ
でも良いのですが、いちいち書き換えた度に update = TRUE ってするのは
うっかり忘れてしまいそうです(笑)というか面倒ですね。そこで、RRSObj
内の obj, obj_old, z, z_old を比較して違っていたら(=更新されていたら)
update フラグを立てるというルーチンを用意します。処理は食いますけど
それくらいの計算時間はたやすいくらい最近のマシンは早いので問題は
ないでしょう。それより画面描画のほうが重たい処理なのですから。
さらにといってはなんですが rr_screen.c の中も高速化を目指します。
rr_screen.c ではフレーム毎に描き変わった範囲だけを表示し、処理を
軽減する機構が既にあったのですが、毎フレーム一枚の矩形領域を描く
だけでした。つまり描き変わった範囲を全包括する最小の矩形領域
一枚をアップデートするといった仕組みでした。これだと左上と右下に
それぞれ 1dot の点を打っただけで全画面が再描画エリアになってしまい
ます。実際に描き変わったのは 2dot だけなのに…。そこで描き変わった
範囲情報をもっと厳密し、リスト構造で複数持てるようにし、アップデート
はそのリスト構造の中の変化範囲のみとするようにします。単純に描画指示
があった範囲のみだと、スプライトが重なっているときなどの場合に同じ
領域(オブジェが重なっている部分)が複数回描かれることとなり場合に
よっては効率が落ちる可能性があります。そこで矩形領域が重なっていたら
合成して一枚の範囲としてしまい、重なっていなかったら新規範囲として
リストに追加といった技を使います。
スプライトオブジェの描画優先順位を実装します。
これは優先順位でソートして、その結果最も奥の方からぺたぺたと
塗り重ねるだけです。一番手前のスプライトオブジェは最も最後に
描画されるので他のオブジェに重なること無く一番上に表示される
といった仕組み。
glib の GList(またはGSList) でソートを行うには g_list_sort を
使うだけですが、「どういった基準で大小を判断し並べるか」といった
部分を関数で与えてやらねばなりません。今回は RRSObj 二つを
z で比較しますので、z を比較し、その大小の結果を返す関数を
用意します。
gint depth_sort_f(gconstpointer a, gconstpointer b) gint depth_sort_b(gconstpointer a, gconstpointer b)
リスト構造でスプライトオブジェを管理するわけですが、そのためには
スプライトオブジェのメモリ管理およびポインタ管理をしなければ
なりません。具体的には GList で保持されているスプライトオブジェの
情報はその RRSObj へのポインタです。
スプライトオブジェ管理の一貫として、そのメモリーアロケートと
リスト登録を行う関数を用意します。
RRSObj *RRSpritemanNew(void) RRSObj *RRSpritemanBGNew(void)戻り値は RRSObj* で、これ自体は rr_spriteman.c 内でも保持されて いますが、アプリケーション側でもどこかにひかえておきます。 で、スプライトオブジェへの変更等はそのアプリケーション側で 保持している RRSObj* に行うことで実現します。
ってこれだけじゃ皆様においてはつまんないと思いますので、もうすこし
うまみのあるサンプルを御用意しました :-)
即興で作ったものですのでかなり粗いし大したこと無いものですけれども。
ちなみに、こういったゲームを作っているわけでなくあくまでも スプライトルーチンの使用例です。
配布条件はソース/データ共に GPL に準じます。