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を別メソッドで実装すればいい。
うん、まあ、初めからこうしとけばよかったかな。