Sceneクラスがよくわらんという話
いや、理屈はわかる。使い方もわかる。んでも、なんだかいまいちピンとこないと言うか、なじまないと言うか。
DXRubyではWindow.loopを基本に作ることになっているが、これはちょっとしたものを極限まで簡単に作れるようにという意図でそうしている。逆に言えば、ちょっと規模の大きなものを作ろうとしたときにはこの仕掛けが逆に負担になってくる。つまり、すべてをWindow.loopの中に書かねばならないということだ。
こういう状況を打開する方法の一つとして、Sceneクラスを使うというのがある。Wikiにもサンプルを置いてはあるのだが、どうもいまひとつこれの使い方がしっくり来ないのだな。
例を挙げるとすれば、フェードイン/アウトをどう実装するか、とか。Sceneの一つとして分離してしまうか、そうじゃなければカウントとフラグでSceneのメイン処理の中に入れ込んでしまうことになる。どっちも正直なところイマイチな気がしてならない。
他には初代ドラクエみたいなメニューをどう実装するか。Sceneの中に入れ込んでしまうか。Sceneを分離した場合、バックに存在するマップの描画とかキャラのアニメーションはどうするのか。
そんな感じで、どうしてもなんか微妙というか、頑張ればそれで作れるんだろうとは思うんだけれども、そんなところで頑張ってもあんまり意味が無いというか、努力すべき部分はもっと別のところにあるんじゃないかとか、そんなふうに思うわけだ。
ぶっちゃけ、Sceneクラスの仕掛けに収まるというのは、とてもとても小さい枠に制限されてしまうことになるんじゃないか、それに従うのは非常に窮屈なんじゃないか。もっと自由に気楽にコードが書けてもいいんじゃないか。なんて思ったわけだ。
小さいサンプルとかを作るレベルから、もうひとつ上の、きちんとした体裁のゲームを作ってみようと思ったときに、それを簡単に作るためにはどのような技術が必要なんだろうな、ということを考えていたのだ。
とこで、そういうときのためにWindow.loopを分解したWindow.create/Input.update/Window.sync/Window.updateも用意してあるし、それを使ったどこでもゲームループをWikiに置いてあったりもする。んでもこれをどのように活用すればいいのか、という話になると、実例が無い。今回はそういう話。
ようはSceneクラスにメインループを好きなだけ作れれば、非常に気楽になるんじゃなかろうか。っていうかWindow.loopがそのような仕様になってればよかったのだ。一番初めの設計が悪いということか。
とりあえずWindow.loopを再定義しよう。いきなりむちゃくちゃである。
require 'dxruby' class ExitException < StandardError end module Window @@created = false def self.loop unless @@created Window.create @@created = true end while(true) do raise ExitException if Input.update yield Window.sync Window.update end end end
ゲームループがあちこちにあるとウィンドウを閉じたときに困る。ウィンドウを閉じたら例外が出してそれで大域脱出することにする。終了しますか、みたいなメッセージを出すにはこれを捕まえればよい。
適当にSceneを2つほど作る。
class Scene end class FirstScene < Scene def initialize @objects = [] @screen_collision = Sprite.new @screen_collision.collision = [0, 0, 639, 479] end def run Window.loop do @objects.push Hoge.new Sprite.update @objects @objects = @screen_collision.check(@objects) Sprite.draw @objects if Input.key_push?(K_SPACE) return SecondScene.new end end end end class SecondScene < Scene def initialize @objects = [] @screen_collision = Sprite.new @screen_collision.collision = [0, 0, 639, 479] end def run Window.loop do @objects.push Fuga.new Sprite.update @objects @objects = @screen_collision.check(@objects) Sprite.draw @objects if Input.key_push?(K_SPACE) return nil end end end end
HogeとFuga(Spriteを継承したものとする)を生成しまくって、画面から出た判定などを適当にして、SpaceキーでFirstScene→SecondScene→終了という感じに遷移させる。
メインのロジックはこう。
scene = FirstScene.new begin loop do result = scene.run if Scene === result scene = result else break end end rescue ExitException end
戻ってきた値がSceneオブジェクトだったらそれのrubを呼んで、違ったら終了。
あとはHogeとFugaを定義する。なんでもいいんだけど適当にオブジェクトを作って動かす。
class SpriteBase < Sprite def initialize(angle) self.x = 320 - self.image.width / 2 self.y = 240 - self.image.height / 2 speed = rand() * 5 + 2 @dx = Math.cos(angle / 180.0 * Math::PI) * speed @dy = Math.sin(angle / 180.0 * Math::PI) * speed end def update self.x += @dx self.y += @dy end end class Hoge < SpriteBase @@image = Image.new(16,16).circle_fill(8,8,8,C_YELLOW) def initialize self.image = @@image super(rand(360)) end end class Fuga < SpriteBase @@image = Image.new(16,16).circle_fill(8,8,8,C_GREEN) def initialize self.image = @@image super(rand(180) + 180) end def update super @dy += 0.2 end end
Sceneの中にメインループがあると、その場の処理がそこに閉じる。設計として美しいかといわれるとちょっと疑問もあるが、メインループをコア部分に置くよりもずっと気楽に書ける気がする。
フェードイン/アウトがしたければWindow.loopを並べればいいし、ドラクエのメニューを表現するにはボタンを押したときにメニュー用Sceneをnewしてrunを呼んでしまえばいい。引数にselfでも渡して、メニューSceneからdrawを呼べば描画もできるわけで、移動はしないでアニメーションだけさせたい、となれば、updateとanimationを別メソッドで実装すればいい。
うん、まあ、初めからこうしとけばよかったかな。