Ruby/SDL の使いかた
とりあえずRubyの使いかたは知っているものとして書いています。
第0章 SDLとはなにか Ruby/SDL のどこがよいのか
まず、SDLについて説明します。 これは、http://www.libsdl.org/ によると、「クロスプラットフォームな マルチメディアライブラリ」だ、ということです。 つまり、さまざまなハードやOS上で動く、高速な画像描画機能やwave/CDの演奏 機能、ジョイスティックの利用機能等をもったライブラリです。 今のところ、公式にはWindows,Linux,BeOS,MacOS,IRIX,Solaris,FreeBSDで 使えます。
そして、Ruby/SDLはその機能をRubyから使うための拡張ライブラリです。 今のところ、Linux,Windows,FreeBSDでの動作を確認しています。 BeOSで動いたという報告もあります。
つぎに、Ruby/SDL のどこがよいのかについて書きます。
まず、Rubyで使える、というのが大きいでしょう。 Rubyはオブジェクト指向言語の中でも非常に手軽に使えます。その上、非常に 高度な機能を持っています。 たとえば、例外機能です。 これのおかげで、Cでは面倒なエラーチェックを簡単にすることができます。
また、複数のプラットフォームで使えるということもあります。 DirectXのようにWindowsだけということはありません。 いまのところRuby/SDLで使えるのは、3つのプラットフォームだけですが、 RubyもSDLもさまざまなプラットフォームで使えるため、そのほかでも 使える可能性は大きいでしょう。
第1章 Ruby/SDL のインストール
Linuxについては、Ruby/SDLのREADME.jaを見てください。
FreeBSDにはportsが存在しますので、そちらを使うのがよいでしょう。 Rubyの処理系として「ruby_r」を使うということに注意してください。 これはスレッド用のライブラリをリンクしたRuby処理系です。 ruby_rもPortsからインストールできます。
Windowsでのインストールは、<URL:rubysdl_install.html>を参照してください。
第2章 初期化
まずは、まっ黒なウィンドウを表示するスクリプトです。
require 'sdl' # (1) SDL.init( SDL::INIT_VIDEO ) # (2) screen = SDL.setVideoMode( 640, 480, 16, SDL::SWSURFACE ) # (3) sleep(3)
まずは(1)でライブラリの読み込みをします。そして(2)で初期化、(3)で ウィンドウの生成です。あとはsleepで3秒待っています。
- の引数に SDL::INIT_AUDIO などを `|'(OR)で指定すれば音を出したり
ジョイスティックを利用したりできるようになります。
- の最後の引数に SDL::FULLSCREEN をORで指定するとフルスクリーンにでき、
SDL::SWSURFACEをSDL::SWSURFACEのかわりに指定するとハードウェアによる 高速化が(可能ならば)使えるようになります。
引数の詳しい意味などははリファレンスを参照してください。
第3章 描画その1
まずは線や円の描画です。
require 'sdl' SDL.init( SDL::INIT_VIDEO ) screen = SDL.setVideoMode( 640, 480, 16, SDL::SWSURFACE ) # (1) screen.drawLine( 100, 100, 400, 200, [ 0, 0, 255 ] ) # (2) screen.updateRect( 0, 0, 0, 0 ) # (3) loop do # (4) while event = SDL::Event2.poll case event when SDL::Event2::Quit, SDL::Event2::KeyDown exit end end end
初期化は2章と同じです。(1)の返値は「画面」を表すオブジェクトで、 これにたいして描画命令を与えればそれがウィンドウに表示されます。(2)で線を 描画しています。色の指定はRGBそれぞれ0から255までの数値の配列でします。 そして(3)でその内容を実際に画面に反映します。実際の表示はupdateRectが 呼出されるごとに行われます。(4)以降はあとで説明します。とりあえずこう いうものだと思っておいてください。
第4章 描画その2
次に画像を読みこんでそれを表示します。icon.bmpというビットマップファイルが 必要です。
require 'sdl' SDL.init( SDL::INIT_VIDEO ) screen = SDL.setVideoMode( 640, 480, 16, SDL::SWSURFACE ) image = SDL::Surface.load( 'icon.bmp' ) # (1) image = image.displayFormat # (2) SDL.blitSurface( image, 0, 0, 0, 0, screen, 100, 100 ) # (3) screen.updateRect( 0, 0, 0, 0 ) loop do while event = SDL::Event2.poll case event when SDL::Event2::Quit, SDL::Event2::KeyDown exit end end end
初期化やupdateRectは前と同様です。(1)でファイルを読みこみ、(2)で読み込んだ データを高速描画できる形式に変換し、(3)で描画です。
今度は透過色について説明します。画像データは普通長方形です。しかし描画したい ものは普通長方形ではないでしょう。つまり画像データのまわりの部分は描画しない ようにしたいわけです。そのために「描画しない部分」を同じ色で描いておいて その色をプログラム中で指定する、という方法をとります。
以下のスクリプトが透過色のサンプルです。
require 'sdl' SDL.init( SDL::INIT_VIDEO ) screen = SDL.setVideoMode( 640, 480, 16, SDL::SWSURFACE ) image = SDL::Surface.load( 'icon.bmp' ) image.setColorKey( SDL::SRCCOLORKEY, image.getPixel(0,0) ) # (1) image = image.displayFormat 100.times do SDL.blitSurface( image, 0, 0, 0, 0, screen, rand(640) , rand(480) ) # (2) end screen.updateRect( 0, 0, 0, 0 ) loop do while event = SDL::Event2.poll case event when SDL::Event2::Quit, SDL::Event2::KeyDown exit end end end
だいたい前のと同じです。透過色の効果がわかりやすいように描画(2)は 描画位置がランダムで 100回描画するようにしてあります。(1)が透過色の設定です。SDL::SRCCOLORKEYは 必ず指定します。また、透過色として画像の左上の端の色を指定しています。 この点はだいたい透過色になっているのでだいたいこれで十分でしょう。(1)を コメントアウトして実行してみれば透過色の作用がわかるでしょう。
第5章 イベントによる入力の取り扱い
プログラムは普通「入力」、「処理」、「出力」の3つの要素から成立しています。 その「入力」の部分です。
require 'sdl' SDL.init( SDL::INIT_VIDEO ) screen = SDL.setVideoMode( 640, 480, 16, SDL::SWSURFACE ) image = SDL::Surface.load( 'icon.bmp' ) image.setColorKey( SDL::SRCCOLORKEY, image.getPixel(0,0) ) image = image.displayFormat image_x = 20 loop do while event = SDL::Event2.poll # (1) case event # (2) when SDL::Event2::Quit exit # (3) when SDL::Event2::KeyDown if event.sym == SDL::Key::SPACE then # (4) image_x += 10 end end end screen.fillRect( 0, 0, 640, 480, [ 0, 0, 0 ] ) screen.put( image, image_x, 200 ) screen.updateRect( 0, 0, 0, 0 ) end
以上のプログラムではスペースを「押すごと」に画像が右方向に移動します。
SDLの入力処理の方法には2通りあります。ここではイベントによる処理を 説明します。ここでいうイベントによる処理とは、「何か」が起きたときに その情報をキューに溜めてそれを処理するというものです。(1)のwhileループの 中がその処理の部分です。上のスクリプトがだいたいの定型だと言って 良いでしょう。
まず、(1)でイベントの情報をキューからとりだします。もしキューが空なら nilが返ってくるのでその場合はループ終了です。次に、(2)でイベントの 種類をcaseで分岐します。SDL::Event2::Quitは、ウィンドウの閉じるボタンを押し たなど、プログラムが終了しようとしたときに生じるイベントです。ここでは(3)で exitを読んでスクリプトの実行を終了しています。SDL::Event2::KeyDownはキーボード のキーを押し下げたときに生じるイベントです。このイベントが生じたときは、 event.symの内容を見ることによってどのキーが押されたのかがわかります。 ここでは、(4)のようにスペースキーかどうかをチェックしています。キーボード のキーを表わす定数がSDL::Key以下に定義されていて、SDL::Key::SPACEの他に、 SDL::Key::Aで「A」キー、SDL::Key::UPで上キーなどがあります。
イベントの種類、そこから得られる情報などはリファレンスを参照してください。 ジョイスティックやマウスの情報もここから得ることができます。
第6章 イベントによらない入力の取り扱い
次はもう一方の入力処理についてです。こちらはイベントという仕組を介せすに 入力情報を得ます。
以下がキーボードから入力を得るためのサンプルです。スペースを 「押している間」、絵が右に移動します。
require 'sdl' SDL.init( SDL::INIT_VIDEO ) screen = SDL.setVideoMode( 640, 480, 16, SDL::SWSURFACE ) image = SDL::Surface.load( 'icon.bmp' ) image.setColorKey( SDL::SRCCOLORKEY, image.getPixel(0,0) ) image = image.displayFormat image_x = 20 loop do while event = SDL::Event2.poll case event when SDL::Event2::Quit exit end end SDL::Key.scan # (1) image_x += 1 if SDL::Key.press?( SDL::Key::SPACE ) # (2) screen.fillRect( 0, 0, 640, 480, [ 0, 0, 0 ] ) screen.put( image, image_x, 200 ) screen.updateRect( 0, 0, 0, 0 ) end
まず(1)で現在の状態を取得して(2)で押しているかどうか判別する、それだけ です。SDL::Key.scanを呼ばないとSDL::Key.press?で得られる情報が 取得できません。また、SDL::Key.press?に与える定数はイベントの所で説明し たキーボード定数と同じものを使います。
マウスから情報を得るのはSDL::Mouse.stateを呼ぶだけです。ジョイスティックの 場合はあらかじめデバイスのを開いておく必要があります。これも詳しくは Ruby/SDL附属のサンプルやリファレンスを参照してください。
第7章 時間の処理 その1
ここでは時間処理関連について説明します。
ここで利用するメソッドは、SDL.initが呼びだされてからの時間をミリ秒単位で返す SDL.getTicks、指定された秒数だけ停止するsleepです。
ここでは、ゲームなどで良く使われる、ループ1周の時間を一定にすることで プログラムの進行速度を保つという機能を実現します。
以下がそのサンプルです。
require 'sdl' SDL.init( SDL::INIT_VIDEO ) screen = SDL.setVideoMode( 640, 480, 16, SDL::SWSURFACE ) image = SDL::Surface.load( 'icon.bmp' ) image.setColorKey( SDL::SRCCOLORKEY, image.getPixel(0,0) ) image = image.displayFormat image_x = 320 image_dx = 3 interval = 1.0 / 50 # (1) loop do loop_start = SDL.getTicks/1000.0 # (2) while event = SDL::Event2.poll case event when SDL::Event2::Quit exit end end # なんかする (ここでは画像が往復する) screen.fillRect( 0, 0, 640, 480, [ 0, 0, 0 ] ) image_x += image_dx image_dx *= -1 if image_x < 0 || image_x > 640 screen.put( image, image_x, 200 ) screen.updateRect( 0, 0, 0, 0 ) loop_end = SDL.getTicks / 1000.0 # (3) if loop_end - loop_start < interval then # (4) sleep interval - ( loop_end - loop_start ) end end
まず、(1)で1ループの時間を設定しています。ここでは1秒に50回ループがまわる ようにしました。そして(2)でループが始まる時刻を記録、(3)でループ終了時の時刻を 記録、(4)でその差がintervalを越えていたら必要な時刻待つ、というように なっています。
SDL.getTicksはミリ秒単位で整数値を返し、sleepの引数は秒単位で少数を与えることに 注意してください。
第7章 時間の処理 その2
上のサンプルには以下の様な問題があります。
- 遅いハードを使っているなどの問題があって、ループ1周に想定以上の時間が かかった場合にそのまま遅れてしまう。
- sleepで待てる時間の精度はそれほど良くない(Linux i386で10msくらい)ので、 ループ1周の時間を正確にできない。
で、それに対応する対応策に以下の様なものがあります。
- 大きく遅れたときは重い処理(描画処理など)をとばす。
- 前のループで遅れた分を次のループでごまかす。
- sleepでの分解能より小さい待ちはビジーループで待つ。
これを実装したのが以下のクラスです。
class FPSTimerSample FPS_COUNT = 10 def initialize(fps = 60, accurary = 10, skip_limit = 15) @fps = fps @accurary = accurary / 1000.0 @skip_limit = skip_limit end # reset timer, you should call just before starting loop def reset @old = get_ticks @skip = 0 end # execute given block and wait def wait_frame now = get_ticks nxt = @old + (1.0/@fps) if nxt > now || @skip > @skip_limit yield @skip = 0 wait(nxt) @old = nxt else @skip += 1 @total_skip += 1 @old = get_ticks end end private def wait(nxt) while nxt > get_ticks + @accurary sleep(@accurary - 0.005) @count_sleep += 1 end while nxt > get_ticks # busy loop, do nothing end end def get_ticks SDL.getTicks / 1000.0 end end
これは、以下のように利用します。
require 'sdl' SDL.init( SDL::INIT_VIDEO ) screen = SDL.setVideoMode( 640, 480, 16, SDL::SWSURFACE ) image = SDL::Surface.load( 'icon.bmp' ) image.setColorKey( SDL::SRCCOLORKEY, image.getPixel(0,0) ) image = image.displayFormat image_x = 320 image_dx = 3 timer = FPSTimerSample.new # (1) timer.reset # (2) loop do while event = SDL::Event2.poll case event when SDL::Event2::Quit exit end end # なんかする (ここでは画像が往復する) screen.fillRect( 0, 0, 640, 480, [ 0, 0, 0 ] ) image_x += image_dx image_dx *= -1 if image_x < 0 || image_x > 640 screen.put( image, image_x, 200 ) timer.wait_frame do # (3) screen.updateRect( 0, 0, 0, 0 ) # (4) end end
まず、FPSTimerSampleのインスタンスを生成(1)し、動作準備をして(2)から、 FPSTimerSample#wait_frame(3)で、必要な時間待つと同時に、遅れた場合、 重い処理(ここでは画面更新)を飛ばす(4)ようにしてできます。
<URL:fpstimer.rb>にもう少し機能を充実したものと、効率化したものが あります。