DxRuby::Spriteの思想を信じよ
Ruby Game Developing Advent Calendar 2016 6日目の記事です。
昨日はvivit_jcさんの「DXRubyで作ったシューティングゲームを交えて高校生向けプログラミング講座をしてきた話」でした。
実際あのプログラムを一晩で書いたって聞かされたときは「は?なんだよ天才かよ」って思いました。さすがvivitさんやね!
あと1~2日目の記事を見て「司エンジンむっちゃ面白そうやん……」ってなったので、来年あたり使ってみたい。
本日は拙作「パズルっぽいゲーム」を制作した時の、DxRubyのSpriteの設計に大変助けられたよ、というお話をします。
7年越しの落ちゲー2度目の挑戦
実は「落ちゲー*1を作ろう!」と思ったのは今回が初めてではありません。
思い返せば7年ほど前。自分のPCとVisualStudio、そしてDXライブラリを手に入れて無敵感に舞い上がった私がまず作ろうと思ったのが
「ポーカーゲーム*2」と「落ちゲー」でした。
そして旧版と「パズルっぽいゲーム」を比較した時、より短期間でよりグレードの高いものになっています。
一体どうしてでしょう?
Spriteの仕様にゲームを“合わせる”
それは私のプログラミング能力が向上したから!!
――で済ませてもいいのですが、実際はそうでもないです。
今回「パズルっぽいゲーム」の制作では、(結果論ではありますが)DxRuby::Spriteの以下の3つの特徴にトコトン乗っかる形で設計していきました。
- Spriteは継承してナンボ
- 更新したいものはすべてSprite.update()に突っ込む
- 描写したいものはすべてSprite.draw()に突っ込む
これが功を奏したのでは、と思うんですよね。
しかしこれ、言葉にすればチョー簡単ですよね?
実際に見てみましょう。下記がPuzzleField.rb(一部抜粋)。ゲームの肝となるクラスです。
# パズルフィールドクラス class PuzzleField extend Forwardable def_delegators :@fight, :attack # パズル中のメインループ処理 def process # アニメーション実行中なら消去/移動は行わない unless animating? processes = [Proc.new{ fall }, Proc.new{ erase }, Proc.new{ raising }, Proc.new{ move_player }] processes.each do |pr| # trueが返ってきた(何らかの処理が実行された)ら、そこで打ち止め break if pr.call end end update end # 描写のみを行い、内部の更新を一切行わない # ポーズ時などに使用 def draw Sprite.draw([puzzle_objects, @field_images, @chain_counter, @score, @level, @damage_counter, @back_image]) end # 各種更新処理&描写処理 def update Sprite.update([puzzle_objects, @sounds.values, @erase_sounds, @score, @level, @damage_counter, @cube_factory]) Sprite.clean(@cubes) draw end # キューブとプレイヤーの合算 def puzzle_objects [@cubes, @player].flatten end
注目すべきはupdateメソッドとdrawメソッド。
ここに突っ込んでるのはすべてSpriteを継承した各種画像オブジェクトです。
updateで座標やらを更新して、drawで書き出す。もうチョー簡単! めんどくさいこと一切なし!
めんどくさいことはprocessメソッドの中にある fall(), erase(), raising(), move_player() の四天王が一括して管理してます。
旧版ではどうやってた?
ちなみに旧版でどうやってたかというと、例えば描写が以下の通り*3。
void cField::view(){ ClearDrawScreen(); int color; for( int i=1; i<FIELD_WIDTH-1; i++ ){ for( int j=1; j<FIELD_HEIGHT; j++ ){ switch( field[i][j] ){ case FLARE: color = GetColor( 255, 80, 80 ); break; case AQUA: color = GetColor( 40, 200, 255 ); break; case GAIA: color = GetColor( 40, 160, 40 ); break; case GUST: color = GetColor( 220, 220, 220 ); break; case STELLAR: color = GetColor( 255, 250, 100 ); break; case METAL: color = GetColor( 40, 40, 60 ); break; case NEU: color = GetColor( 40, 0, 40 ); break; default: color = GetColor( 0, 0, 0 ); } DrawString( i*20,j*20,"●",color); } } DrawString( cursor.x*20,cursor.y*20,"★",GetColor( 255, 255, 255 )); DrawLine( 15, 276, 160, 276, GetColor( 255, 255, 255 ) ); DrawString( 300, 20, "ESCで終了・SHIFTで段上げ", 0xffffff ); ScreenFlip(); }
わぁすごい! 神クラスによる一括描写だ!!
ちなみに新版ではupdateまでですべての作業を終わらせて、drawはSpriteのメソッドを呼んでいるだけなので、ある意味0行です。
座標情報はオブジェクトの中に持った方がいいんじゃない?
あとrubyに関係ない設計の話ですが、旧版と新版で大きく違うなぁと思う場所がもう一点。
旧版は、ブロックや自機がどこにいるかという座標情報をfield二次元配列の順序という形で持っている。
新版は、座標情報をブロックや自機の基底クラスPuzzleObject内にPosオブジェクトとして持っている。
どちらがいいかと聞かれたら、そりゃ後者じゃないのかなって。
「配列の位置関係」という形で座標を持つと、ブロックが移動するたび配列の要素の交換が発生します。
この手の落ちゲーは何につけてもブロックや自機が動くし落下するし競り上がるしで、交換の手間が馬鹿になりません。
それより何より、添字として持つことで「Posをオブジェクトとして扱えない」という欠点もあります*4。
Posに雑にメソッドを突っ込んでいくのはいいぞー、楽しいぞー。
# 座標を表すクラス class Pos attr_accessor :x, :y def ==(o) @x == o.x && @y == o.y end def eql?(o) self == o end def +(o) Pos.new(@x+o.x, @y+o.y) end # 座標が指定する範囲内にあれば真を返す def between?(x_range, y_range) x_range.include?(@x) && y_range.include?(@y) end # 座標を指定する範囲内に収める def clamp(x_range, y_range) @x = @x.clamp(x_range) @y = @y.clamp(y_range) end # チェックすべき範囲のPos群を得る # ただしcheckedに含まれるものは除く def check_positions(checked = Set.new) result = Set.new(Pos.directions.map{|p| self + p}) result.select! {|pos| pos.between?(0..FIELD_WIDTH-1, 0..FIELD_HEIGHT-1)} result - checked end # 1歩分進むための配列を返す def self.directions [Pos.new(0,1), Pos.new(-1,0), Pos.new(0,-1), Pos.new(1,0)] end end
その他こまごまとしたこと
Sprite継承推奨法則を応用して、
Ayameを継承したMusicクラスを作り、
さらにMusicが同時に鳴らないようにJukeboxクラスを作って制御してたりします。
とまぁ、こんな感じです。
「パズルっぽいゲーム」はゲームをDLすれば雑にコードが確認できるから*5、
皆もDLして教師にするなり反面教師にするなりしてくれよな!!
明日は……あれ?! 明日誰もいないぞ!?
まぁでもきっと誰かが颯爽と現れて良記事書いてくれるでしょう。
そう期待してますんで! 頼むよ! お楽しみに!!