シーン切り替えを簡単に行う rubygem「Scene Switcher」を作った

ゲームプログラミングにおいては、「タイトル画面」「ゲームメイン画面」「ゲームオーバー画面」など、複数の『シーン』を切りかえる必要がある。では、それをどうやって実装するか？

とりあえずまずは簡単なゲームを作ってみる。私は [dxruby](http://dxruby.osdn.jp/) が好きなので、dxruby を用いて説明。ひたすら敵を避け続ける<del>なんの面白みもない</del>簡単なゲームを作成してみよう。

```ruby:game.rb
require "dxruby"

x = 320 # 自機の座標
y = 240
enemy_x = 100 # 敵の座標
enemy_y = 100
enemy_vx = 10 # 敵の速度
enemy_vy = 10

Window.loop do
  # 自機の移動
  x += Input.x * 5
  x = 0   if x < 0
  x = 600 if x > 600
  y += Input.y * 5
  y = 0   if y < 0
  y = 440 if y > 440
  
  # 敵の移動
  enemy_x += enemy_vx
  enemy_y += enemy_vy
  enemy_vx = -enemy_vx if enemy_x < 0 || enemy_x > 600
  enemy_vy = -enemy_vy if enemy_y < 0 || enemy_y > 440
  
  # 衝突判定
  if x < enemy_x + 40 && x + 40 > enemy_x && y < enemy_y + 40 && y + 40 > enemy_y
    exit
  end

  # 表示
  Window.draw_box_fill(x, y, x + 40, y + 40, C_WHITE)
  Window.draw_box_fill(enemy_x, enemy_y, enemy_x + 40, enemy_y + 40, C_RED)
end
```

ではここに、タイトル画面やゲームオーバー画面といった『シーン』を追加してみよう。まずはタイトル画面から始まり、スペースキーでゲームが始まる。ぶつかるとゲームオーバー画面になり、スペースキーでリトライできる…というようにしたい。

いくつかの方法とそのデメリットを説明した上で、それを解決するべく作成した「Scene Switcher」を紹介する。

長いので、とっとと「Scene Switcher」の説明を見たい人は[こちらをクリック](#sceneswitcher)

# 方法１：ゲームループ内での case による振り分け

```ruby:game.rb
require "dxruby"

scene = "title"
font = Font.new(24)

# 中略

Window.loop do
  case scene
  when "title" # タイトル画面
    Window.draw_font(100, 100, "Push space to start.", font)
    scene = "main" if Input.key_push?(K_SPACE)
  when "main" # メイン画面
    # 中略
  when "gameover" # ゲームオーバー画面
    Window.draw_font(100, 100, "GAME OVER! Push space to retry.", font)
    if Input.key_push?(K_SPACE)
      x = 320
      y = 240
      enemy_x = 100
      enemy_y = 100
      enemy_vx = 10
      enemy_vy = 10
      scene = "main"
    end
  end
end
```

たぶん一番簡単な方法。しかし、このやり方では基本的にすべてを１ファイル内に記述する必要があるため、規模が大きくなってくると苦しい。「タイトル画面」「ゲームメイン画面」「ゲームオーバー画面」を、それぞれ別のスクリプトファイルに記述したい！

# 方法２：クラス等を使う

「シーン管理」と Google で検索するとわらわらと出てくるのは殆どこの方法。たとえば Ruby ならモジュールを使ってこうできる。

```ruby:game.rb
require "dxruby"

require_relative "game_title"
require_relative "game_main"
require_relative "game_end"

$scene = GameTitle
$font = Font.new(24)
$x = 320 # 自機の座標
$y = 240
$enemy_x = 100 # 敵の座標
$enemy_y = 100
$enemy_vx = 10 # 敵の速度
$enemy_vy = 10

Window.loop do
  $scene.exec()
end
```

```ruby:game_title.rb
module GameTitle
  module_function
  def exec
    Window.draw_font(100, 100, "Push space to start.", $font)
    $scene = GameMain if Input.key_push?(K_SPACE)
  end
end
```

```ruby:game_main.rb
module GameMain
  module_function
  def exec
    # 中略
  end
end
```

```ruby:game_end.rb
module GameOver
  module_function
  def exec
    Window.draw_font(100, 100, "GAME OVER! Push space to retry.", $font)
    if Input.key_push?(K_SPACE)
      $x = 320
      $y = 240
      $enemy_x = 100
      $enemy_y = 100
      $enemy_vx = 10
      $enemy_vy = 10
      $scene = GameMain
    end
  end
end
```

これでとりあえずはシーンごとに別ファイルに分離できた。グローバル変数がうざいが、もう少し工夫すればグローバル変数無しでもなんとかなる。

しかし、**この程度の簡単なゲーム作成にクラスとかモジュールとか使いたくないのだ！**

私が Ruby を好きな理由の一つは、さまざまな書き方ができるところである。Ruby はオブジェクト指向言語に分類されるが、簡単なスクリプトを作成する際にいちいちクラスを作る必要が無い。特にこれは初心者にプログラミングを教える際に都合が良いので、せっかくのこの Ruby の利点を失いたくはない。

# 方法３：ゲームループ外での case による振り分け + load

dxruby 1.4.1 以降では、ゲームのメインループである ``Window.loop`` を複数記述できる。これを利用し、[dxruby 作者のブログ](http://d.hatena.ne.jp/mirichi/20141124/p1)の例をもとに、``load`` を用いることでシーンを別ファイルに分離させる。

```ruby:game.rb
require "dxruby"

$scene = "title"
loop do
  case $scene
  when "title"
    load "game_title.rb"
  when "main"
    load "game_main.rb"
  when "gameover"
    load "game_end.rb"
  end
  break if Window.closed?
end
```

```ruby:game_title.rb
font = Font.new(24)

Window.loop do
  Window.draw_font(100, 100, "Push space to start.", font)
  if Input.key_push?(K_SPACE)
    $scene = "main"
    break
  end
end
```

```ruby:game_main.rb
x = 320 # 自機の座標
y = 240
enemy_x = 100 # 敵の座標
enemy_y = 100
enemy_vx = 10 # 敵の速度
enemy_vy = 10

Window.loop do
  # 中略
  if x < enemy_x + 40 && x + 40 > enemy_x && y < enemy_y + 40 && y + 40 > enemy_y
    $scene = "gameover"
    break
  end
end
```

```ruby:game_end.rb
font = Font.new(24)

Window.loop do
  Window.draw_font(100, 100, "GAME OVER! Push space to retry.", font)
  if Input.key_push?(K_SPACE)
    $scene = "main"
    break
  end
end
```

これはなかなか良いのだが、

- 切り替えるには ``Window.loop`` を ``break`` する必要がある。そのため ``each`` などのループ内から切り替えたい時に面倒。
- 切り替え時に ``load`` が行われるので、スムーズに切り替えが行われず、一瞬引っかかる。

等の欠点がある。

# SceneSwitcher

そこで作った gem がこの「Scene Switcher」。使い方はいたって簡単。

```ruby:game.rb
require "dxruby"
require "scene_switcher"

switch_to "game_title.rb"
```

```ruby:game_title.rb
font = Font.new(24)

Window.loop do
  Window.draw_font(100, 100, "Push space to start.", font)
  if Input.key_push?(K_SPACE)
    switch_to "game_main.rb"
  end
end
```

```ruby:game_main.rb
x = 320 # 自機の座標
y = 240
enemy_x = 100 # 敵の座標
enemy_y = 100
enemy_vx = 10 # 敵の速度
enemy_vy = 10

Window.loop do
  # 中略
  if x < enemy_x + 40 && x + 40 > enemy_x && y < enemy_y + 40 && y + 40 > enemy_y
    switch_to "game_end.rb"
  end
end
```

```ruby:game_end.rb
font = Font.new(24)

Window.loop do
  Window.draw_font(100, 100, "GAME OVER! Push space to retry.", font)
  if Input.key_push?(K_SPACE)
    switch_to "game_main.rb"
  end
end
```

という風に、``switch_to "filename"`` と書くだけで簡単に他のスクリプトに切り替えることができる。一言で言ってしまえば「戻ってこない ``load``」である。[^1]

欠点として上記の「方法３」同様、

- 切り替え時に「スクリプトを読み込んで、eval する」ということをしているので、スムーズに切り替えが行われず、一瞬引っかかる。

というものがあるが、スクリプトをキャッシュしているので、２回目以降の切り替えでは問題ない。また、事前に ``SceneSwitcher.cache("filename")`` としてキャッシュしておくこともできる。

# 補足

本格的なゲーム作成であれば方法２の方が良いのだろうが、このライブラリが想定しているのはプログラミング初心者にできるだけ簡単に複数のシーンを持つゲームを作らせることなので、そうした用途に使っていただければと。

[^1]: ``load`` 同様、``load "filename", true`` とすると、内部的に生成される 無名モジュールをトップレベルとして行われ、 グローバルな名前空間を汚染しない。なお ``load`` と違う点として、``$LOAD_PATH`` は探索せずカレントディレクトリしか見ない。