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秒待っています。

  1. の引数に SDL::INIT_AUDIO などを `|'(OR)で指定すれば音を出したり

ジョイスティックを利用したりできるようになります。

  1. の最後の引数に 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

上のサンプルには以下の様な問題があります。

で、それに対応する対応策に以下の様なものがあります。

これを実装したのが以下のクラスです。

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>にもう少し機能を充実したものと、効率化したものが あります。