書いた人:yhara
前編では、Ruby/SDL を利用して簡単なアクションゲームを作成しました。 後編では効果音や点数表示の実装など、よりゲームとしての完成度を高めていきたいと思います。
まだ Ruby/SDL をインストールしていない場合は、前編を参考に開発環境を整えてください。
それができたら、サンプルパック 2 をダウンロードし解凍してください。音声やフォントなどゲームに必要なファイルが入っています。(Ruby/SDLスターターキットにはほぼ同じデータが入っているのですが、title.png と game_over.png だけ入っていないのでダウンロードして image/ 以下にコピーしてください。)
前編の内容を実際に試してみた方なら、手元に main.rb、player.rb、items.rb という 3 つのファイルができているはずです。サンプルパック 2 にはこれらのファイルも同梱してあります。以下ではこれらのスクリプトを少しずつ改造して機能を付け加えていきます。
前編では、爆弾に当たったらすぐにゲームを終了するようになっていました。 これではゲームオーバーになるたびにゲームを起動し直さなくてはならず、面倒ですよね。 ゲームにタイトル画面を付け、ゲームオーバーになったらタイトル画面に戻るようにしてみましょう。
さて、「タイトル画面」「ゲーム画面」「ゲームオーバー画面」のように複数の場面 (シーン) を用意するにはどうすれば良いでしょうか?「メインループを複数用意する」というのが最も簡単なやり方ですが、この方法は似たようなコードをあちこちに書くはめになるのであまりお薦めしません。代わりに、ここでは別のやり方を考えてみます。
「タイトル画面」「ゲーム画面」「ゲームオーバー画面」のいずれのシーンでも、1 秒に 60 回画面を更新することは変わりません。違うのは 1 フレームごとに行う処理、act と render の内容だけです。となれば、これらをあらわすオブジェクトを用意してやれば良さそうですね。
ここでは TitleScene、GameScene、GameOverScene という 3 つのクラスを作り、それぞれに act と render というメソッドを定義することにします。act にはそのシーンでやりたいことを書きます。render にはそのシーンの画面を表示するコードを書きます。
ついでにもう一つ、シーンが始まったときに呼び出される start というメソッドも定義しています。
場面の切り替えを行うために、act の戻り値を以下のようにしています。
言葉で説明するより、実際にコードを見てもらった方が早いかも知れません。main.rb の class Input 以下を、以下のコードに置き換えてください。
# キー定義 (Enter キーを追加)
class Input
define_key SDL::Key::ESCAPE, :exit
define_key SDL::Key::RETURN, :ok # Enter キー (タイトル画面で使う)
define_key SDL::Key::LEFT, :left
define_key SDL::Key::RIGHT, :right
end
# タイトル画面
class TitleScene
def initialize
@title_image = SDL::Surface.load("image/title.png")
end
def start
end
# Enter が押されたらゲーム画面へ
def act(input)
if input.ok
return :game
else
return nil
end
end
# タイトル画像を表示するだけ
def render(screen)
screen.put(@title_image, 0, 0)
end
end
# ゲーム画面
class GameScene
def initialize
@player = Player.new(240)
@items = Items.new
end
def start
@items.clear
end
# 爆弾に当たったらゲームオーバー画面へ
def act(input)
@player.act(input)
is_crashed = @items.act(@player)
if is_crashed
return :game_over
else
return nil
end
end
def render(screen)
screen.fill_rect(0, 0, SCREEN_W, HOLIZON, [128, 255, 255])
screen.fill_rect(0, HOLIZON, SCREEN_W, SCREEN_H-HOLIZON, [0, 128, 0])
@player.render(screen)
@items.render(screen)
end
end
# ゲームオーバー画面
class GameOverScene
def initialize
@game_over_image = SDL::Surface.load("image/game_over.png")
end
def start
@time = 0
end
# 一定時間 (120 フレーム、約 2 秒) 経ったらタイトル画面へ
def act(input)
@time += 1
if @time > 120
return :title
else
return nil
end
end
# ゲームオーバー画像を表示するだけ
def render(screen)
screen.put(@game_over_image, 0, 0)
end
end
SDL.init(SDL::INIT_EVERYTHING)
screen = SDL.set_video_mode(SCREEN_W, SCREEN_H, 16, SDL::SWSURFACE)
# 各シーンのインスタンスを最初に作っておく
Scenes = {
:title => TitleScene.new,
:game => GameScene.new,
:game_over => GameOverScene.new,
}
input = Input.new
timer = FPSTimerLight.new
timer.reset
scene = Scenes[:title] # 最初はタイトル画面から
loop do
input.poll
break if input.exit
next_scene = scene.act(input)
if next_scene # next_scene が nil (か false) でなければ
scene = Scenes[next_scene] # 新しいシーンに移動する
scene.start # シーンが開始したときの処理
end
scene.render(screen)
timer.wait_frame{ screen.update_rect(0, 0, 0, 0) }
end
次に、items.rb の class Items のところを以下のように書き換えてください。
class Items
def clear
@items = []
end
(後略)
ここで一度 main.rb を実行してみてください。タイトル画面で Enter を押すとゲームが始まります。爆弾に当たるとゲームオーバーなのは前回と同じです。ゲームオーバーになったら画像が表示され、一定時間後にタイトル画面に戻るはずです。
main.rb の一番最後にメインループがあります。前編ではここで player や items の act と render を呼んでいましたが、改造後は scene.act と scene.render だけを呼び出すようになっています。プレイヤーやアイテムを動かす処理は、GameScene の act と render に移動しています。
Items クラスにも一つだけ clear というメソッドを増やしています。何回ゲームをプレイしても GameScene や Items のインスタンスは最初に作った一つを使いまわすので、ゲームが開始するたびに Items も初期化してやらないといけないわけです (これをしないと、2 回目のゲームが始まった瞬間に爆弾に当たってゲームオーバーになってしまいます)。
次は BGM を鳴らしてみましょう。音楽があるだけで、雰囲気が全然違いますよ。
BGM として利用可能なフォーマットは以下のいずれかです。
サンプルパック 2 には MOD 形式の音楽ファイル (famipop3.it) が入っているので、これを鳴らしてみましょう。main.rb を変更して、
という風にします。
Ruby/SDL で音を鳴らすには、最初に初期化を行う必要があります。 まず、SDL.init の引数で SDL::INIT_AUDIO (または SDL::INIT_EVERYTHING) を指定します。 次に、SDL::Mixer.open で音声の初期化を行います。
main.rb の初期化部分を以下のように変更してください。
SDL.init(SDL::INIT_EVERYTHING)
SDL::Mixer.open # 音声の初期化
screen = SDL.set_video_mode(SCREEN_W, SCREEN_H, 16, SDL::SWSURFACE)
次に、SDL::Mixer::Music.load で音楽ファイルを読み込み、 SDL::Mixer.play_music で再生します。 引数には読み込んだ BGM と再生する回数を指定します。回数に -1 を指定すると無限ループになります。 停止には SDL::Mixer.halt_music を使います。 halt_music には引数がありません (一度に 2 つ以上の BGM を鳴らすことはできないからです)。
では、main.rb を BGM を鳴らすように書き換えてみましょう。音楽を鳴らすのはゲーム中だけなので、GameScene を書き換えます。
class GameScene
def initialize
@player = Player.new(240)
@items = Items.new
# BGM の読み込み
@bgm = SDL::Mixer::Music.load("sound/famipop3.it")
end
def start
# ゲームが始まったら BGM を再生
SDL::Mixer.play_music(@bgm, -1)
end
def act(input)
@player.act(input)
is_crashed = @items.act(@player)
if is_crashed
# ゲームオーバーになったら BGM を停止
SDL::Mixer.halt_music
return :game_over
else
return nil
end
end
(後略)
書き換えたら、main.rb を実行してみてください。正しく音は鳴りましたか?
さて、肝心の音楽をどこから手に入れるかですが、以下の 2 通りが考えられます。
自分で作る場合は DTM の知識が必要になりますが、作りたいゲームに合った曲を用意できるという利点があります。 MIDI なら MIDI シーケンサ (google 検索)、 MOD ならトラッカー (modplug tracker など) を使うことになるでしょう。MP3 や Ogg Vorbis は単に音声データを圧縮したものなので、wav 形式で保存できる音楽ソフトなら何でも使えます。
自分で作るのが難しければ、フリー素材を利用するという手もあります。 「音楽 素材」で検索すればいろいろなサイトが見つかると思います。作者の方に感謝しつつゲームに組み込みましょう。
ただ、フリー素材は利用に条件が課せられていることもあります (商用利用は不可、など)。事前によく確認しておきましょう。
BGM が付いたところで、もう一種類の音、効果音を付けてみましょう。
効果音として利用可能なフォーマットには Wave, AIFF, RIFF, Ogg, VOC がありますが、Wave 形式を使うのが一般的です。 効果音を非常にたくさん使う場合は、Ogg 形式に圧縮すればゲーム配布時のファイルサイズを抑えることができます。 (ただし、効果音をロードすれば元の Wave ファイルと同じだけのメモリが消費されるので、メモリ不足には注意してください。)
効果音を鳴らすには、最初に SDL::Mixer の初期化が必要です。これは BGM のときと同じです。
SDL.init(SDL::INIT_EVERYTHING)
SDL::Mixer.open # 音声の初期化
効果音を読み込むには SDL::Mixer::Wave.load を使います。
wave = SDL::Mixer::Wave.load("sound/get.wav")
効果音を再生するには SDL::Mixer.play_channel を使います。 SDL::Mixer ではチャンネルという仕組みを利用して、複数の効果音を同時に鳴らせるようになっています。 play_channel の引数には再生に使うチャンネル番号、読み込んだ効果音、(再生回数 - 1)を指定します。
SDL::Mixer.play_channel(0, wave, 0) # チャンネル0で wave を 1 回だけ再生する
チャンネル番号を考えるのが面倒なときは、-1 を指定すると空いているチャンネルを適当に選んでくれます。 ただしチャンネル数には限りがあるので、1 秒に何十回も効果音の再生を始めるとチャンネルが足らなくなり、例外 SDL::Error が発生してしまいます。 これを避けるには、
など、何らかの工夫が必要になります。
では実際に効果音を鳴らしてみましょう。 サンプルパック 2 ではリンゴを取ったとき (get.wav) と爆弾に当たったとき (bom08.wav) の 2 種類の音を用意しています。どちらも「プレイヤーと物が接触したとき」に鳴らす音なので、当たり判定のところを書き換えるのが良さそうですね。
items.rb を以下のように書き換えてください。
class Items
def initialize
@items = []
# 音声の読み込み
@sound_get = SDL::Mixer::Wave.load("sound/get.wav")
@sound_bomb = SDL::Mixer::Wave.load("sound/bom08.wav")
end
def act(player)
(中略)
@items.each do |item|
case item
when Apple
# リンゴに当たったら
if item.collides?(player)
item.is_dead = true
SDL::Mixer.play_channel(-1, @sound_get, 0) # 効果音を鳴らす
end
when Bomb
# 爆弾に当たったら
if item.collides?(player)
crash = true
SDL::Mixer.play_channel(-1, @sound_bomb, 0) # 効果音を鳴らす
end
end
end
(後略)
書き換えたら、main.rb を実行してみてください。正しく効果音が鳴りましたか?
環境によっては、効果音が遅れて再生される場合があります。こんなときは、SDL::Mixer.open の引数でバッファサイズを小さくすると直ることがあります。
例えばバッファサイズをデフォルト (4096 バイト) の半分にするには以下のようにします。
SDL::Mixer.open(SDL::Mixer::DEFAULT_FREQUENCY, SDL::Mixer::DEFAULT_FORMAT,
SDL::Mixer::DEFAULT_CHANNELS, 2048)
効果音は単なる Wave ファイルなので、コンピュータにマイクを繋いで録音したり、音楽ソフトを使って自作することができます。
が、爆発音など自分で作るのが難しい音もあります。そういうものに関してはフリー素材を利用するのが良いでしょう。 「効果音」で検索すればいろいろなサイトが見つかると思います。作者の方に感謝しつつゲームに組み込みましょう。
ただ、フリー素材は利用に条件が課せられていることもあります (商用利用は不可、など)。事前によく確認しておきましょう。
サンプルパック 2 の爆発音は以下のサイトのものを使わせていただいています。ありがとうございます。
だいぶゲームらしくなってきましたが、ただ一つゲームと呼ぶには決定的に足りないものがあります。そう、得点 (スコア) です。次はスコア表示を付けてみましょう。
スコアを表示するには Ruby/SDL のフォント機能を使います。
フォントファイルには以下の形式が利用可能です。
それぞれどういうものか……の説明はあと回しにして、とりあえずスコア表示を実装してみましょう。 サンプルパック2 には boxfont2.ttf という TTF フォントが入っているので、これを使います。
TTF フォントの表示には、最初に初期化が必要です。
SDL::TTF.init
次に、SDL::TTF.open でフォントファイルを読み込みます。引数にはファイル名と文字のサイズを指定します。
font = SDL::TTF.open('boxfont2.ttf', 24)
フォントを描画するためには 3 種類のメソッドが用意されています。それぞれの特徴は、ごく大雑把に言えば以下のようになります。
基本的には、ゲーム中は高速に描画できる方がいいので draw_solid_utf8 を、タイトル画面は動きが少ないので draw_blended_utf8 を……といった感じで使い分けるのが良いでしょう。
SDL::TTF#draw_solid_utf8 の使い方は以下のような感じです。
font.draw_solid_utf8(screen, 'Hello, world!', x, y, 255, 255, 255)
スコアの計算方法にはいろいろなものが考えられますが、ここでは簡単に「リンゴを一つ取ったら 10 点」ということにしましょう。(別に 1 点でも良いのですが、10 点のほうがスコアの桁が大きくなってなんとなく気分がいいですよね。) また、ゲーム中の最高得点を「ハイスコア」として表示したいと思います。
必要な処理は
といったところでしょうか。
基本的には main.rb を書き換えていきますが、 (3) のところだけ items.rb を書き換える必要があります。 「リンゴを取る」処理が Items#act に書かれているからです。
Items#act を以下のように書き換えてください。配列を返り値にすることで、「爆弾に当たったかどうか」と「取ったリンゴの数」という 2 種類の値を返しています。
class Items
(中略)
def act(player)
# 取ったリンゴの数を数える
apples = 0
crash = false
(中略)
@items.each do |item|
case item
when Apple
# リンゴを取ったら
if item.collides?(player)
item.is_dead = true
SDL::Mixer.play_channel(-1, @sound_get, 0)
apples += 1 # apples を 1 増やす
end
when Bomb
if item.collides?(player)
crash = true
SDL::Mixer.play_channel(-1, @sound_bomb, 0)
end
end
end
(中略)
# 爆弾に当たったかどうかと、取ったリンゴの数を返す
[crash, apples]
end
(後略)
さて、ではこれを使って main.rb を書き換えてみましょう。まず、初期化のところに SDL::TTF.init を追加します。
SDL.init(SDL::INIT_EVERYTHING)
SDL::Mixer.open
SDL::TTF.init # フォント機能の初期化
screen = SDL.set_video_mode(SCREEN_W, SCREEN_H, 16, SDL::SWSURFACE)
次に、GameScene を以下のように書き換えます。initialize でフォントを読み込んで、render で文字を描画しています。
class GameScene
def initialize
@high_score = 0 # (1) プログラム起動時にはハイスコアは 0 にする
@player = Player.new(240)
@items = Items.new
@bgm = SDL::Mixer::Music.load("sound/famipop3.it")
# フォントの読み込み
@font = SDL::TTF.open('image/boxfont2.ttf', 50)
end
def start
@items.clear
@score = 0 # (2) ゲームが始まったらスコアを 0 にする
SDL::Mixer.play_music(@bgm, -1)
end
def act(input)
@player.act(input)
is_crashed, apples = @items.act(@player)
if is_crashed
SDL::Mixer.halt_music
# (4) ゲームオーバーになったらハイスコアを更新する
@high_score = @score if @high_score < @score
return :game_over
else
@score += apples * 10 # (3) リンゴを 1 個取るごとに 10 点加算する
return nil
end
end
def render(screen)
screen.fill_rect(0, 0, SCREEN_W, HOLIZON, [128, 255, 255])
screen.fill_rect(0, HOLIZON, SCREEN_W, SCREEN_H-HOLIZON, [0, 128, 0])
@player.render(screen)
@items.render(screen)
# スコアを描画する
@font.draw_solid_utf8(screen, "score: #{@score} hi-score: #{@high_score}", 0, 0, 255, 0, 0)
end
end
ここまでできたら、main.rb を起動してみてください。最初はスコアもハイスコアも 0 であること、リンゴを 1 個取るごとにスコアが 10 点増えること、最高得点を出すとハイスコアが更新されることを確認してください。
さて、今回はサンプルパック2 付属のフォントを使いましたが、ゲームの雰囲気によってはもっと丸い感じのフォントが欲しい……と思うこともあるでしょう。そういう時は「フリーフォント」で検索すれば、フリーで公開されている TTF フォントがいくつも見つかります。
が、ちょっと待ってください! TTF フォントの場合、「画像の一部に使うのは自由だが、.ttf ファイルそのものを再配布するのは不可」というライセンスになっているものが多くみられます。これでは、いい感じのフォントが見つかったとしてもゲームに組み込んで公開することができません。
そんな時に役に立つのがビットマップフォントです。
ビットマップフォントは、ASCII コード 0 から 255 までの全ての文字を横に並べた画像をフォントとして使用するものです。一度画像に変換してしまえば .ttf ファイルを再配布する必要はないので、多くのフリーフォントが使用可能になります。
TTF フォントからビットマップフォントを自動生成するツールを以下の URL で公開しています。どうぞご利用ください。
まず SDL::BMFont.open でフォントを読み込みます。フラグに SDL::BMFont::TRANSPARENT を指定すると、(画像のカラーキーのように) 文字の背景を透化することができます。
font = SDL::BMFont.open("filename.bmp", SDL::BMFont::TRANSPARENT)
描画は SDL::BMFont#textout で行います。
font.textout(screen, "Hello, world!", x, y)
単色のフォントの場合は、SDL::BMFont#set_color で文字の色を変えることができます。ただし open のときに SDL::BMFont::PALETTE を指定しておいてください。
font = SDL::BMFont.open("filename.bmp", SDL::BMFont::TRANSPARENT|SDL::BMFont::PALETTE)
font.set_color(0,255,0)
その他のフォント形式についても少しだけ解説しておきます。
SFont はビットマップフォントと似ていますが、文字ごとに幅を変えられたり、半透明が使えるなどより高機能になっています。
SFont に関するより詳しい情報については Linux-games.com を参照してください。Linux-games.com では SFont のサンプル もいくつか公開されています。
また、TTF フォントから SFont を自動生成するツールを現在製作中です。以下の URL にて公開予定です。
SFont の読み込みは、ビットマップフォントと同じく SDL::BMFont.open を使います (フラグに SDL::BMFont::SFONT を指定すると SFont の読み込みになります)。SFont の描画には SDL::BMFont#textout を使います。
font = SDL::BMFont.open("filename.bmp", SDL::BMFont::TRANSPARENT|SDL::BMFont::SFONT)
font.textout(screen, "Hello, world!", x, y)
BDF フォントは Unix 系 OS を中心に利用されているフォーマットで、漢字・ひらがな・カタカナなど英数記号以外の文字を表示できるのが特徴です。 ノベルゲーム・アドベンチャーゲームなど、日本語の文章を表示するなら事実上唯一の選択肢でしょう。 1
BDF フォントの入手先については (X11 を中心とした) フリーの日本語ビットマップフォント一覧等が参考になります。
BDF フォントの表示には SDL::Kanji が必要です (Windows 版バイナリには最初から組み込まれています)。
BDF フォントを使用するには、まず SDL::Kanji.open にファイル名と文字のサイズを指定して読み込みを行います。次に、SDL::Kanji#set_coding_system で表示する文字列の文字コードを指定します。.rb の文字コードに合わせて、SJIS, EUC, JIS のいずれかを指定してください。
font = SDL::Kanji.open("fileame.bdf", 12)
font.set_coding_system(SDL::Kanji::SJIS) #Rubyスクリプトの文字コードがshift_jisのとき
文字の表示には SDL::Kanji#put を使います。
font.put(screen, "こんにちは世界", x, y, r, g, b)
現在の仕様では、一度ゲームを終了するとハイスコアは消去されてしまいます。 最後の仕上げとして、ゲーム終了時にハイスコアをファイルに保存し、次の起動時にも引き継げるようにしてみましょう。
セーブデータを簡単に保存する方法としてはテキストファイルや YAML 形式 などが考えられますが、ここでは Ruby の標準ライブラリである Marshal モジュールを使ってみます。Marshal は Ruby のオブジェクトをバイナリデータに変換するモジュールで、Ruby のほとんどのオブジェクトをファイルに保存することができます (保存できないものは IO オブジェクトなど。詳細は Marshal#dump の項を参照してください)。
Marshal は Ruby の標準ライブラリなので、特にインストールや require などは必要ありません。
あるデータをファイルに Marshal 形式で保存するには以下のようにします。
File.open("savefile.dat", "wb"){|f| Marshal.dump(data,f) }
ここで、ファイルモードに「wb」を指定することに注意してください。Marshal 形式のデータはバイナリデータなので、ファイルモードに b を付けてバイナリモードにしないと Windows 上でデータがうまく読み書きできません。
逆にファイルから Marshal 形式のデータを読み込むには以下のようにします。
data = File.open("savefile.dat","rb"){|f| Marshal.load(f) }
ここでもファイルモードに b を付けています。
では、実際にハイスコアの保存処理を書いてみましょう。 ハイスコアの読み込み・保存はゲームの開始時と終了時に行うので、main.rb のトップレベルに書くのが良さそうです。しかしハイスコアのデータは GameScene が持っているので、これを外からアクセスできるようにしないといけませんね。
ということで、まず GameScene の定義を以下のように書き換えます。
class GameScene
attr_reader :high_score # @high_score へのアクセサを定義
def initialize(high_score)
@high_score = high_score # 前回のハイスコアを引数から受け取る
@player = Player.new(240)
@items = Items.new
@bgm = SDL::Mixer::Music.load("sound/famipop3.it")
@font = SDL::TTF.open('image/boxfont2.ttf', 50)
end
(後略)
次に、セーブデータを表すクラスを定義します。 今回は保存するデータが単純なのでありがたみが薄いですが、「ランキングも保存したい」「プレイ時間も記録したい」など保存するデータが複雑になってくれば、クラスに分けておいた方が便利です。
class SaveData
def initialize(file_name)
load(file_name)
end
attr_accessor :high_score
# 読み込み
def load(file_name)
# セーブファイルが存在すれば読み込む
if File.exist?(file_name)
data = File.open(file_name, "rb"){|f| Marshal.load(f) }
@high_score = data
else
# セーブファイルが存在しなければ、ハイスコアは 0
@high_score = 0
end
end
# 書き込み
def save
data = @high_score
File.open("savefile.dat", "wb"){|f| Marshal.dump(data, f) }
end
end
これを使うと、ハイスコアの読み込みは以下のように書けます。main.rb の Scenes を定義しているところを以下のように書き換えてください。
save_data = SaveData.new("savefile.dat") #セーブデータの読み込み
Scenes = {
:title => TitleScene.new,
:game => GameScene.new(save_data.high_score), #前回のハイスコアを渡す
:game_over => GameOverScene.new,
}
また、ハイスコアの保存は以下のように書けます。main.rb の一番最後に以下を付け足してください。
save_data.high_score = Scenes[:game].high_score
save_data.save
ここまでできたら main.rb を起動し、ハイスコアがちゃんと保存されるか確認してみてください。
本稿では 2 回にわたり、Ruby/SDL でのゲーム製作について解説してきました。
ごく簡単なゲームだったので、ゲーマーな方には物足りなかったかも知れません。しかし本稿を読み終えたあなたなら、どこをいじればゲームを難しくできるかはご存知のはずです。:-) 爆弾の数を多くする? 落下速度を速くする? それとも、落ちてくる物の種類を増やす? 空を飛べるようにしてみるとか、色違いのキャラクターを用意して 2 人対戦なんてのも良いかも知れませんね。
何にせよソースコードがあなたの手にある以上、このゲームの行く末はあなたのアイデア次第です。お好きなように改造してみてください。
「ゲームを作ってみたい」と思っている方にとって、本稿が少しでも後押しになれば幸いです。
Windows の「MS ゴシック」など OS 固有の日本語フォントを使うという選択肢もありますが、他の OS で動かせなくなるのでおすすめしません。 ↩