LoginSignup
1
0

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-01-03

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

とりあえずまずは簡単なゲームを作ってみる。私は dxruby が好きなので、dxruby を用いて説明。ひたすら敵を避け続けるなんの面白みもない簡単なゲームを作成してみよう。

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」の説明を見たい人はこちらをクリック

方法1:ゲームループ内での case による振り分け

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

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

方法2:クラス等を使う

「シーン管理」と Google で検索するとわらわらと出てくるのは殆どこの方法。たとえば 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
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
game_main.rb
module GameMain
  module_function
  def exec
    # 中略
  end
end
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 の利点を失いたくはない。

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

dxruby 1.4.1 以降では、ゲームのメインループである Window.loop を複数記述できる。これを利用し、dxruby 作者のブログの例をもとに、load を用いることでシーンを別ファイルに分離させる。

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
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
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
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.loopbreak する必要がある。そのため each などのループ内から切り替えたい時に面倒。
  • 切り替え時に load が行われるので、スムーズに切り替えが行われず、一瞬引っかかる。

等の欠点がある。

SceneSwitcher

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

game.rb
require "dxruby"
require "scene_switcher"

switch_to "game_title.rb"
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
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
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

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

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

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

補足

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


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

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0