ゲームプログラミングにおいては、「タイトル画面」「ゲームメイン画面」「ゲームオーバー画面」など、複数の『シーン』を切りかえる必要がある。では、それをどうやって実装するか?
とりあえずまずは簡単なゲームを作ってみる。私は dxruby が好きなので、dxruby を用いて説明。ひたすら敵を避け続けるなんの面白みもない簡単なゲームを作成してみよう。
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 による振り分け
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 ならモジュールを使ってこうできる。
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
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
module GameMain
module_function
def exec
# 中略
end
end
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
を用いることでシーンを別ファイルに分離させる。
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
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
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
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」。使い方はいたって簡単。
require "dxruby"
require "scene_switcher"
switch_to "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
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
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の方が良いのだろうが、このライブラリが想定しているのはプログラミング初心者にできるだけ簡単に複数のシーンを持つゲームを作らせることなので、そうした用途に使っていただければと。
-
load
同様、load "filename", true
とすると、内部的に生成される 無名モジュールをトップレベルとして行われ、 グローバルな名前空間を汚染しない。なおload
と違う点として、$LOAD_PATH
は探索せずカレントディレクトリしか見ない。 ↩