SDL_imageとmrubyを使ってみた

なんでもSDL_imageというやつを使わないとPNGとかJPEGは扱えないらしい。bmpだけというわけにもいかないのでそれをインストールしようとまたも試行錯誤。
まずSDL_Imageのソースをゲット。
http://www.libsdl.org/projects/SDL_image/
configureしたらなんかlibpngとかjpegとかがないと言う警告らしきものが表示された。そのへんのライブラリを先にインストールしておかないとビルドしても使えないようだ。
このへんからソースを落とす。
http://www.libpng.org/pub/png/libpng.html
http://www.ijg.org/
libpngはzipファイルをダウンしたらconfigureできなかったので、tar.gzのほうをダウンしなおす。んでconfigureしたら今度はzlibが無いといってエラーになる。
zlibは開発用のセットがあるみたいなのでhttp://gnuwin32.sourceforge.net/downlinks/zlib-lib-zip.phpからダウンロードしてmingw下のincludeとlibにファイルをコピーして、configureしてmake、make installができた。
jpegのほうもzipファイルをダウンしたらconfigureできたけどmakeできなかった。UnixFormatとかWindowsFormatとか書いてあるけどWindowsなのにWindowsFormatが使えなくてなんだかもうわけがわからないよ。MSYS.batで動かしてるからUnixFormatじゃないとダメなのかしらん。まあなんしかこれでmake installまでできた。
この2つがあればいいんじゃなーいってことで、これでSDL_imageをconfigureしてmake、make installしてみる。なんかうまくいったらしい。テスト用にshowimageなるプログラムがあるらしいのでそれを動かしたらjpgファイルの画像が表示された。よかった。
それにしても、環境にあわせて存在するファイルを使うように自動的に構成してmakefile作ってくれる仕掛けというのは、背筋が寒くなるほどとんでもない技術だと思う。
ちなみにshowimageを通常のコマンドプロンプトからコンパイルするには、gccのオプションに-lSDL_imageを追加する必要があった。

んで、これとmrubyを組み込んで描画する機能を作ってみる。


ソースをちょっぴり整理して、描画処理まわりは昨日の紅音製作所さま(http://www5.big.or.jp/~high/VENIO/kuz/index.htm)を再び参考に。
そしてようやくmruby。ディレクトリの構成は

d:\usr---mruby-+-mruby
               |
               +-glmruby

とかいう感じにしてみた。mrubyはファイルをそこに一式置いてmakeってするだけでできる。そう、gccならね。VCの場合はCMakeをインストールしてアレコレやらないといけない。いずれDirectX+mrubyみたいなことをする時が来たら書くかもしれない。
mrubyをリンクするためにはコンパイルオプションを追加して、-Iでincludeディレクトリを、-Lでlibディレクトリを、-lでmrubyのライブラリを指定する。上記ディレクトリ構成では、glmruby内からだと相対パス指定でこのようになる。

D:\usr\mruby\glmruby>gcc -o glmruby.exe glmruby.c -g -O0 -Ic:/ruby193/mingw/include/sdl -I./../mruby/include -D_GNU_SOURCE=1 -Dmain=SDL_main -DHAVE_OPENGL -Lc:/ruby193/minw/lib -L./../mruby/lib -lmingw32 -lSDLmain -lSDL -mwindows -lSDL_Image -lmruby

mrubyの関数を扱う方法は主にこちらのわぁ,ソースは後かさま(http://blogchof.blogspot.jp/2012_06_01_archive.html)およびmrubyのtime.c、それからmruby本体のソースいろいろを参考にして、試行錯誤の末、何とか動くようにした。古い情報だとAPIが変わっていて動かなかったりするのでそのへんうまいこと情報へのポインタがあればいいのだけど。

#include <SDL.h>
#include <SDL_image.h>

#include "mruby.h"
#include "mruby/class.h"
#include "mruby/data.h"
#include "mruby/compile.h"
#include "mruby/variable.h"
#include <stdio.h>

/* メインのウィンドウサーフェイス */
SDL_Surface *mWIN;

/* Spriteクラス */
struct RClass *cSprite;

/* Sprite構造体 */
struct glmrb_sprite {
  SDL_Surface *surface;
};

static void glmrb_sprite_free(mrb_state *mrb, void *ptr);

/* mruby用Spriteクラスタイプ定義 */
static struct mrb_data_type mrb_sprite_type = { "Sprite", glmrb_sprite_free };

/* タイマ用 */
struct Timer {
  Uint32 now,
         wit,
         lev;
} timer;

/* イベント処理 */
int quits(void)
{
  SDL_Event evnts;
  if (SDL_PollEvent(&evnts)) {
    switch(evnts.type)
    {
    case SDL_QUIT:
      return 0;
      break;
    case SDL_KEYDOWN:
      if (evnts.key.keysym.sym==SDLK_ESCAPE ||
          evnts.key.keysym.sym==SDLK_q) return 0;
      break;
    default: break;
    }
  }
  return 1;
}

/* Sprite#initialize */
static mrb_value
glmrb_sprite_initialize(mrb_state *mrb, mrb_value self)
{
  struct glmrb_sprite *sprite;
  mrb_value vsprite, vx, vy;
  char *s;

  mrb_get_args(mrb, "ooz", &vx, &vy, &s);
  sprite = (struct glmrb_sprite *)mrb_malloc(mrb, sizeof(struct glmrb_sprite));
  if (!sprite) mrb_raise(mrb, E_RUNTIME_ERROR, "out of memory.");
  DATA_PTR(self) = sprite;
  DATA_TYPE(self) = &mrb_sprite_type;
  mrb_iv_set(mrb, self, mrb_intern(mrb, "@x"), vx);
  mrb_iv_set(mrb, self, mrb_intern(mrb, "@y"), vy);

  /* 画像読み込み */
  sprite->surface = IMG_Load(s);
  if (!sprite->surface) mrb_raise(mrb, E_RUNTIME_ERROR, "file not found.");

  return vsprite;
}

/* Sprite#draw */
static mrb_value
glmrb_sprite_draw(mrb_state *mrb, mrb_value self)
{
  struct glmrb_sprite *sprite = DATA_PTR(self);
  SDL_Rect src, drw;
  mrb_value v;

  /* 描画座標設定 */
  src.x = 0;
  src.y = 0;
  src.w = -1;
  src.h = -1;
  drw.x = mrb_fixnum(mrb_iv_get(mrb, self, mrb_intern(mrb, "@x")));
  drw.y = mrb_fixnum(mrb_iv_get(mrb, self, mrb_intern(mrb, "@y")));

  /* 描画 */
  SDL_BlitSurface(sprite->surface, &src, mWIN, &drw);
  return self;
}

static void
glmrb_sprite_free(mrb_state *mrb, void *ptr)
{
  mrb_free(mrb, ptr);
}

int main(int argc, char *argv[])
{
  SDL_Event event;
  mrb_value s;
  FILE *fp;
  int x = 0;

  mrb_state *mrb  = mrb_open();
  cSprite = mrb_define_class(mrb, "Sprite", mrb->object_class);
  MRB_SET_INSTANCE_TT(cSprite, MRB_TT_DATA);

  mrb_define_method(mrb, cSprite, "initialize", glmrb_sprite_initialize, ARGS_REQ(3));
  mrb_define_method(mrb, cSprite, "draw", glmrb_sprite_draw, ARGS_NONE());

  fp = fopen("main.rb", "r");
  s = mrb_load_file(mrb, fp);
  fclose(fp);

  /* SDL初期化 */
  if (SDL_Init(SDL_INIT_VIDEO) < 0 ) {
    exit(-1);
  }

  /* フレームバッファ作成 */
  mWIN = SDL_SetVideoMode(640, 480, 0, 0);
  if (!mWIN) {
    SDL_Quit();
    exit(-1);
  }

  /* キャプション設定 */
  SDL_WM_SetCaption("glmruby application",NULL);

  /* メインループ */
  while(quits() != 0) {

    mrb_funcall(mrb, s, "update", 0);

    /* 画面消去 */
    SDL_FillRect(mWIN, NULL, SDL_MapRGB(mWIN->format, 0, 0, 0));

    mrb_funcall(mrb, s, "draw", 0);

    /* ウェイト処理 */
    timer.now = SDL_GetTicks();
    timer.wit = timer.now - timer.lev;
    if (timer.wit < 16) SDL_Delay(16 - timer.wit);
    timer.lev = SDL_GetTicks();

    /* 画面更新 */
    SDL_UpdateRect(mWIN, 0, 0, 0, 0);
  }

  /* 終了処理 */
  SDL_Quit();
  return 0;
}

Spriteクラスを定義している。SDL_Surface*を持つCの構造体をラッピングしたデータを持つ。Spriteクラスはinitializeとdrawのみ定義してあり、initializeはx座標、y座標、画像のファイル名を引数にする。xとyはインスタンス変数に格納される。画像データへのアクセスはまだできない。drawはインスタンス変数から座標を取得して保持している画像を描画する。
このサンプルのポイントはCの構造体をラップしたクラスであること、インスタンス変数へのアクセスをしていること、あたりか。でもいい加減な作りなのでインスタンス変数にfloat型を入れたりすると動かなくなる。引数を取得する関数は型を変換してくれるけどインスタンス変数から取得するときはFixnumとFloatで取得方法が変わるから、そのへん統一して扱えるマクロが欲しいところだ。NUM2INTみたいなやつ。
動作としては、カレントディレクトリのmain.rbを読み、実行して、スクリプトが返してきたオブジェクトに対して毎フレームupdateとdrawを呼び出すという単純なものだ。とりあえずこんなコードを書いておけば絵が横に動く。

class TestSprite < Sprite
  def update
    @x+=1
  end
end

TestSprite.new(100,105,"ruby.png")

仕掛けはシンプルだが、実はゲームの骨格部分などこれで十分じゃないかと言う気がする。例えばSceneクラスを作ってそのオブジェクトを返せば複数のキャラを同時に動かすことができる。

class TestSprite < Sprite
  def update
    @x+=1
  end
end

class Scene
  def initialize
    @sprites = [TestSprite.new(100,100,"ruby.png"),
                TestSprite.new(100,200,"ruby.png")]
  end

  def update
    @sprites.each do |s|
      s.update
    end
  end

  def draw
    @sprites.each do |s|
      s.draw
    end
  end
end

Scene.new

また、更にもう一つ上にApplicationクラスを作れば、Sceneの遷移もできるようになるだろう。とはいえ、せっかくmrubyなのにそのあたりまでユーザに書いてもらうわけにもいかない。SceneやApplicationは作りこんでおいてmrblibにでも突っ込んで置けばいいんじゃないかと思う。遅ければCで実装すればよい。あとはその他のクラスやモジュールとして機能をどれだけどういう仕様で作っていくかという話になる。
まあ、今回はとりあえずこんな感じで作ってみたら動いたという感じで。