ゲームプログラマになる前に覚えておきたい技術 Ch.2 - スクロール

大きなステージを読み込んだときにはプレイヤーを中心にした一画面分だけを描画するか、

やってみた。

妄想した動作は、ステージが画面に

  • 収まる => 画面の真ん中にステージを表示して動かない
  • 収まらない => 基本的にプレイヤーを中心に表示する
    • ただし、ステージの上下左右の外側まで表示する必要はないので、プレイヤーを中心とする一画面分がステージの上下左右からはみ出るようなら端を合わせる

描画する処理をまとめておく

まず、今のところ画面に表示するのは色つきの四角形しかないので、これをまとめてしまう。 vram やら width やらを扱う Screen というクラスを作って、四角形を描くメンバ関数を作った。四角形の左上と右下をそれぞれ座標で指定して、color で塗りつぶす。for 文の初期値と終了条件で max(..., 0) やら min(..., mHeight) とすることで範囲外は飛ばす(書き込まない)ようにしてある。

void Screen::drawRect (const Coord& from, const Coord& to, unsigned color) {
   for ( int y = max(from.y, 0); x < min(to.y, mHeight); ++y ) {
      for ( int x = max(from.x, 0); y < min(to.x, mWidth); ++x ) {
         mVram[y * mWidth + x] = color;
      }
   }
}

ステージをズレた位置に描画する

Stage クラスの方で Screen のインスタンスを作って、各セルを drawRect していく。各セルは

  • ステージ上で何個目のセルか
  • それを描画すると(1セルが 16x16 とかなので)ステージ左上からのピクセル数で (x1, y1) を左上, (x2, y2) を右下とする四角形になる
  • ステージを画面に対してズレた位置に置くのでその分各セル/ピクセルの位置もずらす

これを計算して Screen::drawRect に投げればおk。画面外のセルを投げちゃっても Screen::drawRect が無視してくれるので気にしなくていい。

void Stage::draw () const {
   Screen screen;
   Coord offset = calcOffset(screen);

   for ( Position pos(this); pos < (mHeight * mWidth); ++pos ) {
      Coord from(pos.x * CELL_SIZE, pos.y * CELL_SIZE);
      Coord to(from.x + CELL_SIZE, from.y + CELL_SIZE);
      unsigned color = mCells[pos].color();

      screen.drawRect(from + offset, to + offset, color);
   }
}

offset には (x, y) をそれぞれどんだけずらすかをピクセルで。実際の計算は後述。
for で回してる pos は (0, 0) から始まって ++pos してくと (1, 0) -> (2, 0) -> (3, 0) と進んでいく。this を渡してるのは、this (= Stage) の width を見て x が端まで行ったら次は (0, 1) とするため。要するにこれで全部のセルに対して処理できる。
各セルに対してセル位置(x, y) * CELL_SIZE でステージを描画したピクセル位置の左上と、それに + CELL_SIZE した右下の座標を出して、drawRect に + offset して投げる。from/to と offset はどっちも Coord なので operator+ を定義してあって from + offset とか書けるんだけど、pos(セル位置)から描画されたステージ上のピクセル位置への変換が `Coord from(pos.x * CELL_SIZE, pos.y * CELL_SIZE)` とかグチャグチャなのでなんとかしたいところ。 Coord::operator * (int) でできるけど、それでいいのかちょっと迷う。

どんだけずらすかの計算。

Coord Stage::calcOffset (const Screen& screen) const {
   Position posPlayer = findPlayer();
   int stageWidth = mWidth * CELL_SIZE;
   int stageHeight = mHeight * CELL_SIZE;

   // とりあえず中心で初期化
   Coord offset( (screen.width() - stageWidth) / 2,
                 (screen.height() - stageHeight) / 2 );

   // 横方向: screen の幅が足りなかったら
   if ( screen.width() < stageWidth ) {
      // とりあえずプレイヤーを中心に
      offset.x = (screen.width() / 2) - (posPlayer.x * CELL_SIZE);
      if ( offset.x > 0 ) {
         //ステージ左端が画面左端より右に行ってるので合わせる
         offset.x = 0;
      }
      else if ( stageWidth + offset.x <= screen.width() ) {
         // ステージ右端が画面右端より内側になるので合わせる
         offset.x = screen.width() - stageWidth;
      }
   }
   // 縦方向についても同じことをする
   if ( screen.height() < stageHeight ) {
      ...
   }

   return offset;
}

感想

  • x/y と width/height はいいにしても、screen.width だったり stageWidth だったり mWidth だったりというのがめんどくさい。x, y, width, height を保持するクラス作ってみんなそれを継承させてしまえばすっきりしそう。ただし継承の使い方がまだ勉強中。
    • Stage クラス内で Stage.width() とかできないのが悲しい。this->width() でも同じなんだけど「this ってなによ?」となりそう。
  • ステージ上のセル位置、ステージを描画したときのステージ上のピクセル位置、画面上のピクセル位置が混在してるのがイヤ。
  • 画面外になるセルはどうせ描画しないんだから、先にチェックして画面外なら drawRect 呼ばないようにするべき、常識的に考えて。
  • ちょっとしたことでガンガン新しいクラス作りたくなる。それは良いのか悪いのか。