GB ゲーム開発覚え書き: スプライトを動かす3

Game, C, GameBoy

第1回 ではスプライト向けにタイルマップを作成、第2回 ではキャラを設置した。
今回はプレイヤーのスプライトを操作可能にして、ゲームがクリアできるところまでやる。

メインループの作成

キャラクターを設置したので、ゲームのためのループを書く。
BOOLEAN の is_end が TRUE になり while ループを抜けたら "GAME CLEAR" という文字列を表示する。

src/main.c

#include <gb/gb.h>
#include <stdio.h> // "GAME CLEAR" は printf で出すので、インポート
#include "Characters.c"

BOOLEAN is_end = FALSE; // ゲームが終わるかの判定
UINT8 player_pos[2] = {0};
UINT8 friend_pos[2] = {0};

void main() {
 set_sprite_data(0, 6, Charcters);

 set_sprite_tile(0, 0);
 player_pos[0] = 8; 
 player_pos[1] = 16;
 move_sprite(0, player_pos[0], player_pos[1]);

 set_sprite_tile(1, 5);
 friend_pos[0] = 160; 
 friend_pos[1] = 152;
 move_sprite(1, friend_pos[0], friend_pos[1]);
  
 SHOW_SPRITES;
  
 // ここからメインループ開始
 while(!is_end) {
   wait_vbl_done(); // VBlank 割り込み終了を待機
 }
 printf("GAME CLEAR");
}

wait_vbl_done() をおくと、VBlank の割り込み終了まで CPU をアイドルする。HALT 命令。
ゲームボーイの描画をおさらいすると、左上から描画をスタートし、左から右に 1 行ずつ描画する。最後の行の右、つまり右下の描画を終えると、また左上から描画を開始する。この左上に移るまでの期間が VBlank で、1 秒に約 60 回訪れる。
これをメインループに置くと 1 秒に約 60 回処理を実行、実機に近い数値で言えば 59.7 fps で動く。
一種のおまじないとは言え、ゲームが複雑になると、不要な待機命令でフレームレートを下げる可能性もある。

操作する

このゲームでは、十字キーでキャラクターの xy 軸を操作する。
コントローラーの入力は joypad() で取得する。
この関数で返る値は J_UP などの定数に入っているので、AND と組み合わせてボタンの押下を判定できる。
これを使って player_pos の値を増減させ、move_sprite するだけ。

src/main.c

while(!is_end) {
    UINT8 joypad_control = joypad();

    // キャラクターの操作
    if (joypad_control & J_UP)
    {
      player_pos[1]--;                              // プレイヤーの y 座標を -1 する
      move_sprite(0, player_pos[0], player_pos[1]); // スプライト番号 0 を動かす
    }
    
}

今回は、「8 方向に移動できる」「上下、左右同時の入力を認識しない」ということで以下のようにした。

src/main.c

  while(!is_end) {
    UINT8 joypad_control = joypad();

    // 上方向(y軸-1)
    if (joypad_control & J_UP)
    {
      player_pos[1]--;
      move_sprite(0, player_pos[0], player_pos[1]);
    }
    // 下方向(y軸+1)
    else if (joypad_control & J_DOWN)
    {
      player_pos[1]++;
      move_sprite(0, player_pos[0], player_pos[1]);
    }
    // 右方向(x軸+1)
    if (joypad_control & J_RIGHT)
    {
      player_pos[0]++;
      move_sprite(0, player_pos[0], player_pos[1]);
    }
    // 左方向(x軸-1)
    else if (joypad_control & J_LEFT)
    {
      player_pos[0]--;
      move_sprite(0, player_pos[0], player_pos[1]);
    }

    wait_vbl_done();
  }

これでプレイヤーを動かせるようになったものの、フレンドとくっついてもクリアできないし、画面外に移動できてしまう。 ※ 次の GIF 画像は 15fps にしてるのでカクカクしてますが、実際は 59.7fps で動きます。

主人公がスタート地点から、フレンドに向かって移動するが、近づいても何も起こらない。しかも、画面の外に移動できてしまう。

衝突判定の追加

まず、フレンドとの衝突判定(ゲームクリアの判定)を関数として追加。普通の衝突判定なので詳細は割愛。

src/main.c

BOOLEAN is_colliding_friend() {
  return ((player_pos[0] >= friend_pos[0] && player_pos[0] <= friend_pos[0] + 8) 
    && (player_pos[1] >= friend_pos[1] && player_pos[1] <= friend_pos[1] + 8))
    || ((friend_pos[0] >= player_pos[0] && friend_pos[0] <= player_pos[0] + 8)
    && (friend_pos[1] >= player_pos[1] && friend_pos[1] <= player_pos[1] + 8));
}

この結果を is_end に入れる。これでゲームがクリアできる。

src/main.c

while(!is_end) {
  // 〜〜〜〜省略〜〜〜〜
  is_end = is_colliding_friend();
  wait_vbl_done();
}

次に、画面外に移動できる問題を解消する。
ゲームボーイの画面に映されるのは 20×18 マス(以下、ビューポートと命名)だけど、実際にはバックグラウンドレイヤーが 32×32 マス分あるので、プレイヤーはビューポートからはみ出て移動できてしまう。
ビューポート外に移動できないよう、「画面端にいるか」を判定する雑な関数を用意する。

src/main.c

BOOLEAN is_colliding_x(BYTE delta) {
  if (delta > 0) { // -なら左、+なら右方向
    return player_pos[0] >= 160; // 横 20 マスにいるか
  } else {
    return player_pos[0] <= 8; // 横 1 マスにいるか
  }
}

BOOLEAN is_colliding_y(BYTE delta) {
  if (delta > 0) { // -なら上、+なら下方向
    return player_pos[1] >= 152; // 縦 18 マスにいるか
  } else {
    return player_pos[1] <= 16; // 縦 1 マスにいるか
  }
}

十字キーを押した時に衝突判定を調べ、 FALSE なら何もないということで移動させるように if を挟む。

src/main.c

if (joypad_control & J_UP)
{
  if (!is_colliding_y(-1)) // y に -1 移動するので -1 を渡す
  {
    player_pos[1]--;
    move_sprite(0, player_pos[0], player_pos[1]);
  }
}
/* 他も同様 */

プレイヤーが操作できて、画面からはみ出さず、感動のエンディングを迎えられるようになった。

プレイヤーを画面右端に動かす。画面の外にははみ出ない。そのままフレンドの元へ向かう。フレンドに接触すると、GAME CLEAR の文字列が出て、ゲームが終了する。

最終的なコード

このゲームのコードは、以下のようになった。

src/main.c

#include <gb/gb.h>
#include <stdio.h>
#include "Characters.c"

BOOLEAN is_end = FALSE;
UINT8 player_pos[2] = {0};
UINT8 friend_pos[2] = {0};

BOOLEAN is_colliding_friend()
{
  return ((player_pos[0] >= friend_pos[0] && player_pos[0] <= friend_pos[0] + 8) && (player_pos[1] >= friend_pos[1] && player_pos[1] <= friend_pos[1] + 8)) || ((friend_pos[0] >= player_pos[0] && friend_pos[0] <= player_pos[0] + 8) && (friend_pos[1] >= player_pos[1] && friend_pos[1] <= player_pos[1] + 8));
}

BOOLEAN is_colliding_x(BYTE delta)
{
  if (delta > 0)
  {
    return player_pos[0] >= 160;
  }
  else
  {
    return player_pos[0] <= 8;
  }
}

BOOLEAN is_colliding_y(BYTE delta)
{
  if (delta > 0)
  {
    return player_pos[1] >= 152;
  }
  else
  {
    return player_pos[1] <= 16;
  }
}

void main()
{
  set_sprite_data(0, 6, Charcters);

  // 主人公
  set_sprite_tile(0, 0);
  player_pos[0] = 8;
  player_pos[1] = 16;
  move_sprite(0, player_pos[0], player_pos[1]);

  // フレンド
  set_sprite_tile(1, 5);
  friend_pos[0] = 160;
  friend_pos[1] = 152;
  move_sprite(1, friend_pos[0], friend_pos[1]);

  SHOW_SPRITES;

  while (!is_end)
  {
    UINT8 joypad_control = joypad();

    if (joypad_control & J_UP)
    {
      if (!is_colliding_y(-1))
      {
        player_pos[1]--;
        move_sprite(0, player_pos[0], player_pos[1]);
      }
    }
    else if (joypad_control & J_DOWN)
    {
      if (!is_colliding_y(1))
      {
        player_pos[1]++;
        move_sprite(0, player_pos[0], player_pos[1]);
      }
    }
    if (joypad_control & J_RIGHT)
    {
      if (!is_colliding_x(1))
      {
        player_pos[0]++;
        move_sprite(0, player_pos[0], player_pos[1]);
      }
    }
    else if (joypad_control & J_LEFT)
    {
      if (!is_colliding_x(-1))
      {
        player_pos[0]--;
        move_sprite(0, player_pos[0], player_pos[1]);
      }
    }

    is_end = is_colliding_friend();

    wait_vbl_done();
  }
  printf("GAME CLEAR");
}

(余談)printf の注意点

"GAME CLEAR" に使っている printf は、GBDK の stdio から読み込む特殊なもの。
これを使うと、バックグラウンドのタイルマップに文字グラフィックがぶち込まれてしまう。
通常は、あくまで GBDK で開発する時のデバッグ向けのもの。

SameBoyのVRAMビューアーで、バックグラウンドのタイルマップを確認している。タイルマップに文字グラフィックが勝手に入っている。

つづく

スプライトにタイルを設定してそれを動かし、ゲームがクリアできるようになった。
次はバックグラウンドレイヤーを使って、障害物の設置などをする予定。