Haskellでゲーム開発 - SDL-Imageで画像を表示

とりあえず、ゲームを作るなら画像が描画できなくては話になりませんね。
というワケで、どうやってSDLで表示した画面に画像を表示するかという話を書きます。
もともとSDLにはBMPを操作するための関数が用意されていたりしますが、対応してるフォーマットがBMPだけなんですね、コレ。

少なくとも、キャラクターを背景にかぶせた描画はしたいです。そういう事を色々考えると透過色が使える画像フォーマットが便利という事になります。
で、透過PNGとかGIFとかJPGとか使うためには、SDL-Imageという別のパッケージをインストールして使う必要があるんですね。

cabalからだとこんな感じに見えるヤツです。

$ cabal list sdl-image
* SDL-image
    Synopsis: Binding to libSDL_image
    Default available version: 0.6.1
    Installed versions: 0.6.1
    License:  BSD3

コイツをインストールしてやって、SDLとは別でimportしてやらないといけないというワケです。



というわけで、ここから先はSDLSDL-Imageがインストールされていて、ghcから使えるようになっている前提で話を進めます。
以下の三枚の画像を用意して、hsファイルと同じ場所に設置しましょう。

OnpuB.png:

OnpuG.png:

OnpuR.png:

見ての通り、「おんぷ」です。
形が微妙に歪んでるとか言わないでください。マウス描きなんです。気合入れて描きました。

ちゃんと透過色を透明に処理してくれているか確認するために、この3枚を少しづつずらして重ねながら描画しましょう。
下のようなソースコードになります。

module Main where
import Data.Word
import qualified Graphics.UI.SDL as SDL
import qualified Graphics.UI.SDL.Image as SDLi

gameinit :: IO ()
gameinit = do
  SDL.init [SDL.InitEverything]
  SDL.setVideoMode 640 480 32 []
  return ()

dispImage :: IO ()
dispImage = do
  --準備
  screen <- SDL.getVideoSurface
  imgR <- SDLi.load "OnpuR.png"
  imgG <- SDLi.load "OnpuG.png"
  imgB <- SDLi.load "OnpuB.png"
  --表示
  SDL.blitSurface imgR (Just $ SDL.Rect 0 0 100 100) screen (Just $ SDL.Rect 245 165 100 100) 
  SDL.blitSurface imgG (Just $ SDL.Rect 0 0 100 100) screen (Just $ SDL.Rect 270 190 100 100) 
  SDL.blitSurface imgB (Just $ SDL.Rect 0 0 100 100) screen (Just $ SDL.Rect 295 215 100 100) 
  SDL.flip screen
  --画像を解放
  SDL.freeSurface imgR 
  SDL.freeSurface imgG
  SDL.freeSurface imgB
  
  --画面が閉じられるまで再帰
  e <- SDL.waitEvent
  if e == SDL.Quit then return () else dispImage

main :: IO ()
main = gameinit >> dispImage >> SDL.quit

再帰の度に画像読み込んで描画して閉じてとかやってるのは、うん、気にしないでください。
テストプログラムなので関数毎の関係を明確にしたかったため、一気にガーっと書いたのでこうなってます。
物凄く手続き的でHaskellっぽく無いです。

とにかく、これを実行すると、次のような画面が表示されます。

問題なく画像表示できているようです。



というわけで、はじめて出てきた関数をちゃちゃっと見ていきましょう。

まず、getVideoSurfaceです。

*Main> :t SDL.getVideoSurface
SDL.getVideoSurface :: IO SDL.Surface

前回もちょっと出てきたSurface型ですが、どうやらコレはメモリ上のグラフィック領域へのアクセスを管理するための構造体みたいですね。

typedef struct SDL_Surface {
        Uint32 flags;
        SDL_PixelFormat *format;
        int w, h;
        Uint16 pitch;
        void *pixels;

        /* clipping information */
        SDL_Rect clip_rect;

        /* Reference count -- used when freeing surface */
        int refcount;

	/* This structure also contains private fields not shown here */
} SDL_Surface;

定義を見る感じでは、pixelsというポインタの先に、他のメンバで定義されたフォーマットに応じたグラフィックスデータがあると考えれば良さそうです。外から使う分には特にここまで見る必要は無いですけど。

で、getVideoSurfaceはその名の通り、画面のサーフェスを返す関数です。
表示する画面もグラフィックなので、サーフェスとして保持されているワケですね。
最初のコードでは、screenという変数がgetVideSurfaceの戻り値に束縛されます。

続いて、SDL-Imageパッケージのload関数で、画像の読み込みを実行します。

*Main> :t SDLi.load
SDLi.load :: FilePath -> IO SDL.Surface

型定義を見れば一発なので、説明不要でしょう。



いよいよ、サーフェスからサーフェスに画像を転送する処理を書きます。
今回の主役、blitSurface関数の登場です。
複雑と言うほどでは無いですが、ちゃんと書くとちょっとだけ長丁場なので区切り入れます。

*Main> :t SDL.blitSurface
SDL.blitSurface
  :: SDL.Surface
     -> Maybe SDL.Rect
     -> SDL.Surface
     -> Maybe SDL.Rect
     -> IO Bool

最初の2つの引数が、「転送元」の情報。
残りの2つの引数が「転送先」の情報。

最後のBool型の戻り値なのですが、これ、Hackageにはちゃんと説明無いのでちょっと困りました。
以下が引用ですが、コレだけっきゃ書いて無いんですねー(´・ω・`)

blitSurface :: Surface -> Maybe Rect -> Surface -> Maybe Rect -> IO BoolSource

This function performs a fast blit from the source surface to the destination surface.

「元サーフェスから先サーフェスへ高速転送を行う」とだけしか書かれていないのですが、どうやらラップ元の関数はint型を返すようになっているらしく、SDLのWikiには次のように書かれています。

Return Value

Returns 0 if the blit is successful or a negative error code on failure; call SDL_GetError() for more information.

何かエラーがあったら負数のエラーコードを返し、SDL_GetError()関数でエラーの具体的な内容を得られる、という事みたいです。

HaskellにもちゃんとgetError関数がラップされています。

*Main> :i SDL.getError
SDL.getError :: IO (Maybe String)
  	-- Defined in Graphics.UI.SDL.General

普通に実行してこの戻り値を監視していると、常にTrueを返しているようなので、Falseが返ってきたら転送失敗なので、この関数を使ってエラー内容を取得する・・・みたいな使い方であってると思います。

・・・思います。

話を戻して、第二引数と第四引数のRect型ですが、これは転送元先のサーフェスから切り出す「範囲」を指定するための構造体みたいですね。
定義は次の通り、左上位置と大きさを表す四値だけ持ってるシンプルな型です。

*Main> :i SDL.Rect
data SDL.Rect
  = SDL.Rect {SDL.rectX :: Int,
              SDL.rectY :: Int,
              SDL.rectW :: Int,
              SDL.rectH :: Int}
  	-- Defined in Graphics.UI.SDL.Rect
instance Eq SDL.Rect -- Defined in Graphics.UI.SDL.Rect
instance Ord SDL.Rect -- Defined in Graphics.UI.SDL.Rect
instance Show SDL.Rect -- Defined in Graphics.UI.SDL.Rect

つまるところ、blitSurfaceを日本語で長たらしく説明すると・・・

第一引数のサーフェスから、第二引数の範囲を切り出して、第三引数のサーフェスの第四引数の範囲に転送を行い、処理が失敗した場合はFalseを返却する。

というような感じになるのでしょうか。
といっても、使い方が分かればそれほど難しい事は無さそうです。



画面へ画像の転送が終わったら、今度はflip関数で画面の更新をしてやります。

*Main> :t SDL.flip
SDL.flip :: SDL.Surface -> IO ()

この関数は、ハードウェアでダブルバッファをサポートしていない場合、updateRectと同等の動きをします。

*Main> :t SDL.updateRect
SDL.updateRect :: SDL.Surface -> SDL.Rect -> IO ()

ハードウェアがダブルバッファリングをサポートしていて、かつsetVideoMode関数でダブルバッファを使うよう初期化すると、ハードウェア側によるバッファ切り替えが行われます。
とにかく、画像転送処理後、画面全体を再描画するには、flip関数を呼び出せば良いと考えれば良いみたいです。

これらはあくまでC言語のライブラリをラップしてるダケなので、最後に使い終わったリソースはちゃんと解放してやらないとメモリが大変な事になります。
というわけで、サーフィスの解放には、freeSurface関数を使います。

*Main> :t SDL.freeSurface
SDL.freeSurface :: SDL.Surface -> IO ()

これも型定義を見れば直ぐに解ると思いますので、これ以上のグダグダ書くことも無いです。

あとはこれらの処理を前回のメインループ内に上手く組み込んでやれば、アニメーション処理なんかもできるハズ・・・