ComplexSpriteと相対表現

ACの記事で複素数ネタを書いたが、そこにオマケとして計算を保持するクラスをつけておいた。これを使って階層Spriteを作ってみた。
https://gist.github.com/mirichi/2b25b0a58e93bf780df6

昔作ったEntityクラスの焼き直しで、あっちは行列を使っていたのだが、今回は複素数とCalcクラスを使っているので、DXRuby1.4.2でも動く。
Entityとの違いはもうひとつあって、親子関係の作り方をComplexSprite#targetで指定するように変えた。通常、Sprite#targetは描画先を指定するのだが、ここにComplexSpriteGroupオブジェクトを指定することで子Spriteが相対表現化する。相対表現化した場合、親のx/yが描画基点になり、angleやscaleを引き継いで一緒に変形する。この仕掛けは将来的にDXRubyに実装しようとしている階層Sprite機能の先取りである。が、先取りできているのはtarget指定と親のimageにRenderTargetオブジェクトを指定したときの動作ぐらいで、衝突判定周りはどうにもならないので放置中。
ともあれ、親子関係と相対表現化の機能について考えてみた。

Sprite相対表現化

もともと昔からそのような表現をしたくて考えてはいたのだ。Spriteそのものに子Spriteを持つ機能を追加するだとか。ただ、どういう感じに作れば多種多様な用途に対応できてシンプルになりかつ互換性が保てるのかという点で悩んだ。
色々と実験したり、仕様を考えたりしてきてはいて、その1つがWSのLightContainerであるし、また別の1つがEntityクラスであったわけだ。今回新たに構築したComplexSpriteは複素数を使う実験も兼ねて、相対表現化の1つの案として今まで考えてきた機能をある程度盛り込んである。

相対表現化仕様案

とりあえず、相対表現の親となるクラスはSpriteGroupとして新たに作る。Spriteにまとめることも何度か考えたが、相対表現化したSpriteの衝突判定が無理ゲーになるので諦めた。具体的には円形の判定を持つ場合に、縦横別スケーリングで更に回転していたものが、縦横別スケーリングで回転した親の下にいると、変形&回転した楕円同士の衝突判定が発生する。これが計算できない。
なので、親になるクラスはscale_x/yではなく、縦横兼用のscaleだけを持つ。こうすることで計算できない問題を仕様的に回避する。また、子を持たないSpriteでも子を持つ機能があると余計な計算が発生するのでそれを避ける意味もある。
親と子を繋ぐには、親のadd_childを呼んで子に追加するか、子のtargetに親を指定する。親のimageにRenderTargetが設定されていた場合、子の描画先はそのRenderTargetになる。nilもしくはImageの場合は親のtargetとなり、そこに親が設定されていれば更に親を見に行き、最終的にはWindowもしくはRenderTargetに行き着く。
update/drawを呼ぶと子のメソッドも再帰的に呼ばれる。SpriteGroup自身は配列ではないのでそのままではSprite.updateなどで処理されない。ここはSpriteGroupのupdate内で処理する感じ。drawについては親が保持するimageがRenderTargetかどうかで親子のどっちを先に処理するかが変わる。

衝突判定について

DXRubyのRenderTarget関連で手痛いことになっているのが衝突判定である。
RenderTargetは描画先であり、ここに描画する場合の座標指定はRenderTargetローカル座標系となる。これはWindowとは分離されていて、座標系の断絶が発生する。
Sprite#targetをRenderTargetオブジェクトにした場合、同様にWindowにした別のSpriteとは衝突判定ができない、ということになる。そのため、DXRuby製フレームワークのマウス判定は面倒なことになっている。
RenderTargetは描画先であるが、同時に描画ネタでもあるので、扱いとしてはImageと同様で、オブジェクト自身は描画座標を持たない。Sprite#targetに指定した場合に、それを参照してもスクリーン上の描画位置を取得することは理論上不可能なのである。描画ネタなんだから複数描画しちゃうこともあるし、そうなったらどうやって判定するんだよ、と。
これはGUIを扱い始めた頃からの課題で、Spriteの相対表現化をすることで、親オブジェクトがRenderTargetを保持すれば、同時に解決可能ではないかと考えている。衝突判定時に親オブジェクトを参照することで描画座標を取得できれば補正が可能である。複数の親が同じRenderTargetをimageに持つ場合の動作は保証されない。
親のtargetが最終的にRenderTargetに行き着いてしまった場合はどうにもならない。前提条件として、親子ツリーの最上位のtargetが同じものである必要がある。

ツリーの構成案

親オブジェクトのtargetをRenderTargetオブジェクトにできないようにする、というのもひとつの案である。となると親のtargetはWindowか、もしくは他の親オブジェクトに限定される。こうすればツリー内のSpriteはすべて判定できることが保証できる。とはいえ、単体のSpriteのtargetをRenderTargetにするのは今までどおりできないといけないので、こいつの判定は困る。
単体のSpriteのtargetも親もしくはWindowに限定すると、それなりにきれいにまとまるのかもしれないが、DXRuby的ポリシーとしての互換性問題と、同様にポリシーとしての自由さが失われてしまうのであまりよい案ではない。結局のところ、描画先の座標系が違うと判定はできない、ということなのだが、これを明示して意識させるのはちょいと難しい。
また、WindowやRenderTargetが直接子Spriteを保持できる、という案もある。こうすると基本的な扱い方はWindowに対するadd_childでのSprite登録、という感じになり、登録されたSpriteのupdate/drawが毎フレーム自動的に呼ばれるようになり、JSでよく見られるフレームワークの雰囲気に近くなる。案としてはアリだが今のところそこまでやる気は無い。そのようにしたければ各自でWindowやRenderTargetにメソッドを追加すればいいんじゃないかな。

想定用途

Spriteの相対表現化で何ができるようになるのか、というと、だいたい次のような感じのものが作りやすくなることが考えられる。
・ザコを伴っていたり、複数パーツからなるキャラ
GUIの多層ウィンドウ
・多関節キャラ
相対表現化の機能を追加すると、こういった感じのものを作る場合に、座標計算と、RenderTargetを挟んだ場合の衝突判定がシステム的に解決される。WSも簡素化できるかもしれないし、サンプルのlazer2.rbももっと簡単に作れるかもしれない。BaseballKnucleとかもシンプルになるかもしれない。
また、副作用的にSpriteのグルーピング機能が追加されることになる。親を変形・回転せず、座標を(0,0)にしておけば、単純にグルーピングする機能になる。この場合、親のtargetを変更することで子Spriteの描画先を一気に変更することができるようになり、こういう機能は現状のDXRubyに無いので場合によっては嬉しいかもしれない。

実装関連

DXRubyに機能追加するなら、なんにせよ計算量が増えるので、基本的にはCで実装することになる。ComplexSpriteのようにProcでどうこう、ということにはしないし、計算はコードで表現するのではなく、内部的に行列を扱うことになる。行列といっても2D専用の処理だから3*3だろうし、一番右の列は0,0,1固定なので値を持つ必要も計算する必要も無い。このへんを加味して最適化することになる。行列を使うやり方では各段に1個ずつ親オブジェクトがいるような多関節キャラは効率が下がる。このへんは実際に動かしてみてから考えることになる。
ComplexSpriteではupdateを呼ばないと座標が更新できないが、これを解決する必要がある。描画・衝突判定時に必要であれば再計算する。行列の値をキャッシュし、クリアする処理を作らなければならない。
最も厄介なのは衝突判定で、場合によっては現状のコードを捨てる必要がある。が、パラメータさえ調整すれば今でも判定できる(できてる)ので、それでどうにかなるかもしれない。

問題点

使うのが難しい。回転操作をする多関節キャラの場合だと、offset_syncやcenter指定で座標がどのように扱われるかを正確に理解する必要があり、このあたりはややこしい話である。俺自身たまに悩む。想定どおりには動いているのだが。まあ、多間接キャラを作るのは親子関係と連結位置などでどうしても難しいものではある。
また、どっかの親の子を親から外して他の親の下に移す操作を考えていない。これは実際にありえる。ローカル座標系からグローバル座標系への変換は描画時に必要なので作るが、逆にグローバル座標系からローカル座標系への変換はそれ専用の特殊な処理が必要だ。例えばグローバル座標系のSpriteを2倍描画する親の下に移動した場合、Spriteは1/2スケールに設定されることになる。でもそれでいいのか?
それから、Window.draw系メソッドとの兼ね合い。Sprite#drawをオーバーライドして自前で描画しようとした場合、親の影響をどう計算すればいいのか。Window.drawならdraw_exでどうにかできるかもしれないが、draw_tileだとかはどうにもならない。これをどうするか。諦めるしかないのか。
最後に、描画座標の問題。DXRubyの描画座標はピクセルに合わせて整数化されるが、回転やスケーリングがされた画像はピクセルを跨ぐように描画される。この場合、画像のテクセルと画面のピクセルが一致しない。親が回転などしていた場合、親の絵に対して、子の描画位置が整数化されて1ピクセル未満の位置ズレが発生する。これはComplexSpriteでも既に発生していて、Ruby側での対策は不可能である。

おしまい

つらつらと今まで考えていたことを書いてみたが、だいたい仕様と実装のイメージはできてきた。が、これは大変そうである。Spriteまわりに大掛かりな変更が発生する。んでもインターフェイスとしてはSpriteGroupが増えてSpriteにメソッドがいくつか追加される、だけになりそう。過去のコードに対する互換性は(ほぼ)保てる。はず。
実行速度は低下するだろうが、現状既に過剰な気がするので気にしない。まあ、程度による。親子関係をRubyで作る場合と比較すると速くなるし、楽にもなるだろう。
とりあえずそのうちやってはみると思う。でも完成するかどうかは別問題である。