この記事は Ruby Advent Calendar 2014 の 23日目の記事です。
今日は、2、3年程前からコツコツと作っている Ruby 用の GUI ツールキットの紹介をしたいと思います。
はじめに
趣味の時間を使って GUI ツールキットをイチから作るとなるとなかなかの地道な開発となります。それが最近やっと多少使い物になるところまでやってきたので少しずつ公開してみたりしています。
ただ、ドキュメントの整備などが全くないのでライブラリとしてバーン!と公開することは出来ず、ニコニコ動画でチラ見せ程度に地味ーーにただただコーディングするだけの動画をアップしたりしています。再生数などほとんど増えませんが、公開出来るレベルに来た事自体がただ嬉しかったりします。
で、最近公開したRuby でブロック崩しゲームを書いてみたという動画に「解説が入るといいね」というコメントを頂いたので、今回ここで解説記事を書いてみる事にしました。
Hello, World!
とりあえず説明の代わりとして、このツールキットを使うとどのような感じで GUI が作れるのかを雰囲気だけでも伝えられればと思い、恒例のこんにちは世界!なコードを載せてみます。
Application.start do
win = Window.new title: "HelloApp", frame: [100, 100, 500, 500] do # ウィンドウの作成
on :draw do |e| # draw メソッドをオーバーライド
e.painter.push do # Window 内を描画
fill :red # 赤で
text "Hello, World!", 10, 10 # テキストを描画
end
end
end
win.show # ウィンドウを表示
end
Processing 風ラッパー
元々 Processing のような Creative Coding 的なものが好きなのもあって、ツールキット上の極薄のラッパーライブラリを書いて Processing 風にも書けるようにしてあります。
これは Processing スタイルで書いた簡単なペイントアプリもどきです。なかなかわかりやすいコードになってると思いませんか?
canvas = Image.new(512, 512).paint {background :white} # 画像を生成
setup do # 起動時に一度だけ呼ばれる
size canvas.size # ウィンドウの大きさを画像の大きさにする
end
draw do # 描画のたびに呼ばれる
image canvas # 画像を描画する
text "#{event.fps.to_i} FPS", 10, 10 # 1秒間に draw が呼ばれる回数を表示
end
pointer do # マウスイベントを処理
if down? || drag? # マウスボタンの押下またはドラッグ?
canvas.paint do # 画像に描画する
fill event.left? ? :red : event.right? ? :blue : :white # マウスのボタンで色を変える
ellipse *(event.pos - 10).to_a, 20, 20 # マウスの位置に円を描画
end
end
end
key do # キーイベントを処理
quit if chars =~ /q/i # 'Q' のキーが押されたら終了する
end
いかがでしょうか。Processing に近い記述になっているかと思います。
これらの書き方は小さいものを短いコードで書くには適しているので、これから書くブロック崩しゲームもこのスタイルで書いて行くことにします。
物理エンジンの組み込み
そもそも今回ブロック崩しを作ってみた経緯としては、ツールキットに Box2D という物理エンジンを組み込んでみたからというのがあります。
ではなぜ物理エンジンなのか、というとゲーム開発の事も想定していたからというのもありますが、単純に GUI のライブラリとして View 間の相互作用として物理法則を組み込んだら面白そうだなと思ったからです。まだ実装はしていませんが、Box2D の Joint 等も手軽に利用できたらゲームにとどまらず面白いことに使えそうです。
で実際に組み込んでみると、副産物として View 同士の衝突判定も出来るようになしました。それで、適当に配置した四角形(ブロック)と、円形状(ボール)に初速を与えて物理演算すると、もうブロック崩しが出来上がってしまったようなものでした。
ということでここからブロック崩しゲームの解説をしていきたいと思います。
ブロック崩しの原型
まずはブロック崩しの見た目だけ実装してみます。
def add_shape (klass: RectShape, frame: [0, 0, 100, 100], color: :white)
# 形状の追加関数
window.add klass.new { # RectShape や EllipseShape クラスのインスタンスを生成してウィンドウに追加
set frame: frame, fill: color # インスタンスの一度色を指定
}
end
setup do
set title: 'Breakout', size: [600, 400] # ウィンドウのタイトルと大きさを指定
5.times do |y| # 10x5 個のブロックを配置する
10.times do |x|
c = [:white, :red, :green, :blue, :yellow][y] # 段ごとに色を変える
shape = add_shape frame: [(x + 1) * 50, (y + 1) * 20, 30, 10], color: c # 画面上部にブロックを生成
end
end
$bar = add_shape frame: [0, 350, 100, 20], color: :blue # マウスで操作する青いバーを生成
add_shape frame: [0, window.h - 1, window.w, 1] # 画面下端に謎の高さ1ピクセルの矩形を追加
end
pointer do |e|
$bar.x = e.x - $bar.w / 2 # バーの位置をマウスの座標で更新
if e.down? # マウスボタンを押したら
ball = add_shape klass: EllipseShape, frame: [e.x, $bar.y - 20, 20, 20] # マウスの位置にボールを生成
end
end
こんな画面になります。
動きを付ける
次にゲームになるようにボールに動きを与えます。まずは各形状が物理法則にのっとって動作するようにします。add_shape() 関数の set 呼び出しの部分を以下のように修正します
# 密度1、摩擦係数0、反発係数1 で減衰なしでバウンドするように
set frame: frame, fill: color, density: 1, friction: 0, restitution: 1, type => type
View の density, friction, restitution を固定の値で設定するようにしています。こうすることで親 View に Box2D の b2World が生成され、各形状には内部で b2Body やら b2Fixture やらが生成され自動で物理演算がされるようになります。それでもこのままではまだボールは動きません。重力も設定していないのでそれぞれが受ける力がなにも無いためです。そこでボールに初速を与えてみます。
# ボールに初速を与える
ball.velocity = Point.new(rand(-1.0..1.0), -1).normal * 500
velocity が指定されたため、今度はボールが勢い良く上に向かって動き出します。
しかも特に衝突判定のコードも書いていないのにボールが壁やブロックにあたって反射するようになっています。物理演算エンジンサマサマですね。これですでに相当ブロック崩し感が出てきたのではないでしょうか。
ボールの衝突処理
そしていよいよボールがブロックにあたったらブロックが消滅するようにします。
# ブロック形状の on_contant メソッドを実装してボールがぶつかってきたら自分が消えるように
block_shape.on(:contact) {$garbages << shape}
# 謎の下辺の形状にあたったらボールが消えるように
bottom.on(:contact) {|e| $garbages << e.view}
これだけです。Box2D の ContactListener を Ruby の世界から記述出来るようになっているため、この2行でボールと他の形状との衝突処理が完結に書けています。
ブロック崩しゲームの完成
最終的にコードは以下の様になります。いかがでしょうか。これだけの短いコードで単純ながらブロック崩しゲームが記述出来ているはなかなか素敵だと思いませんか?
$garbages = []
def add_shape (klass: RectShape, frame: [0, 0, 100, 100], color: :white, type: :static)
window.add klass.new {
set frame: frame, fill: color, density: 1, friction: 0, restitution: 1, type => type
}
end
setup do
set size: [600, 400]
wall.friction = 0
5.times do |y|
10.times do |x|
shape = add_shape frame: [(x + 1) * 50, (y + 1) * 20, 30, 10], color: [:white, :red, :green, :blue, :yellow][y]
shape.on(:contact) {$garbages << shape}
end
end
$bar = add_shape frame: [0, 350, 100, 20], color: :blue
bottom = add_shape frame: [0, window.h - 1, window.w, 1]
bottom.on(:contact) {|e| $garbages << e.view}
end
pointer do |e|
$bar.x = e.x - $bar.w / 2
if e.down?
ball = add_shape klass: [EllipseShape, RectShape].sample, frame: [e.x, $bar.y - 20, 20, 20], type: :dynamic
ball.velocity = Point.new(rand(-1.0..1.0), -1).normal * 500
end
end
update do
$garbages.uniq.each {|o| o.parent.remove o}
$garbages.clear
end
$garbages 周りの記述が謎ですが、これは Box2D の世界では衝突処理中に要素の削除とかをしてはいけないようで、その制限を回避するための処理になります。ですがツールキット側で上手く回避する修正をする予定なので、今後はこれらの行は必要無くなる予定でいて、衝突処理も以下のように書けるようになります。
# 衝突相手をその所属する親から削除する
bottom.on(:contact) {|e| e.view.parent.remove e.view}
さいごに
いかがでしたでしょうか。ちょっと分かりにくいですが、作りはシンプルながら一応ブロック崩しと言えるゲームらしきモノがとても簡単に作れたのが伝わりましたでしょうか。ゲームとしての最終的な動き等は記事の最初の方でも紹介したこちらの動画をご覧ください。
ちなみに私、昨日の OS X Advent Calendar にもCRuby インタプリタの CocoaPod を作ったので紹介しますという記事を書かせてもらっています。CRuby のインタプリタを Mac のアプリに簡単に組み込める CocoaPod を作ったよ、いう記事なのですが、これそもそもこの GUI ツールキットを iOS で動かしたくて作った Pod だったりします。
まだツールキット自体が iOS 上で安定して動作しない問題があるのですが、それが解消できれば近い内にこのブロック崩しも iOS 上で動くようになる予定です。
そうなると iOS/Mac 用のゲーム用ライブラリとして使えなくもないなと思い始めてしまうのですが、音周りが全くないのが寂しかったりしてきます。Ruby 用のサウンド関係のライブラリも Gem として色々ありますが、iOS でも動くものとなるとなかなか無いと思いますので、また自分で作りたい気持ちが湧いてきてしまうのですよね。
元々は Ruby でテキストエディタを作りたくて作り始めたツールキットなのに、物理エンジンを載せた辺りから方向性が怪しくなってきてしまっているので、音関係にまで手を出すのはやめた方が良いとわかりつつ・・・。つい作り始めてしまう未来が見えて辛いですw