Spriteにまつわるあれこれ

この記事はDXRuby Advent Calendar 2013の8日目です。
7日目の記事はしのかろさんのDXRubyのSpriteを継承して拡張する方法についてでした。あおたくさんとハイドさんがMarkdownで綺麗にしてくれました。https://gist.github.com/RPGP1/7833833

Spriteの挙動を詳細に調べて、Rubyの言語仕様とうまいこと融合させた、Sprite拡張の手法である。両方について高い理解度がないとこういうことはできない。すごい。8日目はしのかろさんに続いてSpriteの記事で行こうと考えていたのだが、直球勝負では分が悪いので変化球で逃げることにして、作者ならではのSpriteの裏話あれこれを。

Spriteの歴史的な話

DXRubyのSpriteは古くて、うちのブログで検索すると2009年7月あたりが初出となる。当時は外部ライブラリとして実装していて、DXRubyFrameworkという名前であった。Ruby1.8の遅さを克服するためにSpriteを作り、できる限りC側で処理してしまうというのが元の発想である。当時の記事はこのあたり(DXRubyFramework0.0.1α)。今どきのPCとRubyならRubyだけで書いても普通にこのぐらいのものは動くんじゃないか。
DXRubyFwは速度のみを目的としていたので、Spriteは非常に使いにくいものだった。自動移動用コマンド列とか言う変なものも実装していた。弾幕ゲーを作るためにあの手この手で限界にチャレンジしていたのだな。
また、DXRubyExtensionという外部ライブラリも存在していて、これもRuby1.8の遅さを克服するための衝突判定ライブラリで、衝撃的に使いにくいものだった。こっちはまだ同梱されているが、DXRuby1.6では消える予定となっている。
Rubyが1.9でVM化して速くなったので、DXRuby1.4でFwとExを融合したSpriteクラスを実装し、ややこしい機能はバッサリ削除して、基本Rubyで全部書く、という方針に切り替えて、今に至る。
DXRuby1.4のリリースは2012年8月末なので、Sprite機能は実に3年の時間をかけて試行錯誤を繰り返し、開発されたのである。

Spriteの概念的な話

Spriteは描画プリミティブであり、それ自身が描画する画像と、その他描画情報を持っている。自分自身を適切に描画する機能を一通り持つ。オブジェクトにパラメータを設定してdrawメソッド一発でそのように描画できる。DXRubyの描画の基本はWindow.draw_exなのでこのオプションに対応したクラス設計となっていて、Sprite#drawの内部実装はSprite構造体から値を取るか引数から取るかが違うだけで、Window.draw_exとほぼ同じである。ようするにSpriteはWindow.draw_exで描画する機能をまとめてクラス化したものだと言える。
ここだけ見るとRubyで書いたものと何が違うの?って話になるわけだが、実際のところ違いは特に無い。描画プリミティブ機能なんてRubyで書けばいいじゃないってずっと思っていた。しかし逆に考えると、こういった機能はみんながそれぞれ同じものを作ることになるし、それだったら組み込みで存在していたほうがいいだろう、という感じである。
ただ、それとは別に衝突判定の存在は非常に大きい。しかし使われ方を見ている限り、多少オーバースペックだったかもしれないとか思ったりもしないでもない。

Spriteのインスタンス変数的な話

RubyでSpriteクラスを作るとパラメータ類はインスタンス変数とそのアクセサになるだろう。しかしDXRuby1.4のSpriteにはインスタンス変数は無い。これは実行速度と使い勝手を考えた上での設計判断なわけだが、困る人もいるらしい。

インスタンス変数が存在しない理由は大きく2つ。
・内部からのアクセスが遅い。特に衝突判定時にまとめて大量アクセスが発生するのが憂慮される。当たり前だが構造体から取り出したほうが速い。
・Spriteを継承した場合にインスタンス変数名の衝突を考慮しなければならないのがイヤだ。ついうっかりでバグるとしょんぼりする。
と、いう感じである。さすがに他の人の手によって互換ライブラリが作られることまで考慮していなかった。

Spriteのオブジェクト保持的な話

しのかろさんの記事にもあるように、Spriteオブジェクトのパラメータ類はすべて渡されたRubyオブジェクトをそのまま保持し、使うときに使えない値だった場合にエラーとなる。使わないパラメータについては何を入れていてもエラーにならない。
これはDXRuby1.4用にSpriteを実装したときにそのようにしたもので、それまでのSpriteはそのようになっていなかった。DXRubyFwのSpriteは内部自動処理を高速化するためにCで扱える型に変換して保持していたのだが、DXRuby1.4では内部自動処理を削除したからその必要が無くなったのだ。
結果、何でも格納できてそのまま持っておいて、使うときにエラーにするという動きに(特に何も考えず感覚で)変更し、問題も無さそうなのでその形でリリースした。テキトーさがひどい。しかしRuby歴が長くなってきた頃の感覚というのは案外バカにできないようで、ダックタイピングの手引き? - Rubyでの静的型付けの心理学などを見ると俺の感覚に非常に近く、これはこれでいいんだろうと思っている。
こういう実装になっているのは現状Spriteだけだが、ユーザが継承するクラスはSpriteぐらいじゃないかと思うので特に問題はないだろう。

Spriteの衝突判定的な話

自分で言うのも何だが衝突判定には気合が入っている。
Spriteに衝突判定を乗せるためには描画と完全に一致させる必要があり、DXRubyEx時代にはサポートしていなかった回転や縦横別比率スケーリング、描画基準位置移動など、すべてのSpriteの機能をサポートしている。DXRubyExでサポートされていた形状である点、円、矩形、三角形の衝突を含み、複数の形状を組み合わせることもできる。作るのに3ヶ月ほどかかった力作である。しかし上でも書いたようにこの機能をフル活用されているところを見たことがない。っていうか俺もあんまり使わない。あの頑張りは何だったの。
処理速度的にはもっと改善できると思うが、とりあえず問題になりそうなレベルでもないようなので放置している。用途が2Dゲーなので描画数にはおのずと限界があり、徹底的に最適化する必要性が薄い。
ところでDXRubyのSpriteはSprite#collision=を指定しないとImageサイズの矩形で判定されるが、逆にSprite#image=を設定せずSprite#collision=だけ設定することで衝突判定のみを実行することができる。判定範囲を回転・スケーリングするにはcenter_x/yも設定しておく必要がある。デフォルトの原点はImageサイズから算出されるからである。
独自のクラスでキャラを作っているが衝突判定が面倒だという場合に、上記のようにそれ専用にSpriteを使う手がある。Spriteはメソッドを持った単純な構造体なので生成負荷もメモリ消費も非常に軽い。ぜひどうぞ。

Spriteのクラスメソッド的な話

Spriteオブジェクトの配列を渡してまとめて処理するクラスメソッドがいくつか用意されている。
よくある処理なので使うとラクだという話なのだが、渡す配列の中にSpriteじゃないものが入っていてもそれなりに動くようになっている。Sprite.updateならupdateメソッドがあるときに限り呼ばれるとか、Sprite.cleanだとvanished?を呼んでみてtrueなら削除してくれるとか。例えばるびま用サンプルのMapクラスはオブジェクトをSpriteの配列に突っ込んでいるが、MapクラスはSpriteを継承していない。
このへんはそれっぽく作ったつもりだったのだが、最新の1.5.7devでも地味にイマイチな挙動をしている場合があり現在検証&整備中である。

Spriteの今後的な話

大きく機能拡張するという予定はいまのところ無い。
今以上にどうにかするには、例えばぜんぜん別のもの(Chipmunkなど)を持ってきてくっつけるとか、何らかのフレームワーク的なものを追加してラクをする方向に行くか、まあ、そんな感じであり、基本機能としては現状で問題ないんじゃないかと考えている。
DXRubyでやることの予定としては、Soundまわりの拡張や、OpenGL連携、Input::IMEなどがあって、それらと比べるとSpriteをいじるのは優先度が落ちるかなーみたいな。

おしまい的な話

なんかだらだら書いていたら長くなってしまった。そんなに役に立つ情報でもないのでAdventCalendarに書くのもどうかと思ったが、こういうときでもないと書かないので、それはそれでよしとしよう。
DXRubyのSpriteはこんな方針、こんな感覚で作られたのだ。Spriteを使ってちょっと突っ込んだものを作る人や、似たようなものを作る人の役には立つかもしれない。
Spriteそのものもそれなりに頑張って作ったものなんで有効活用してあげてくださいな。

以上。次の記事はあおいたくさんによる「AnimeSpriteの話(仮)」の予定です。お楽しみに〜