GB ゲーム開発覚え書き: バックグラウンドを設定する2

Game, C, GameBoy

前回の続き。

おさらい

前回は、バックグラウンドのために 2 つのデータを作成した。

  1. タイルデータ(Backgrounds.c)
  2. タイルマップ(StageMap.c)

これを GBDK で読み込み、ゲームのステージマップとして使うのが目的。
今回は、第 1 回で書いたコードに、ステージマップを実装する

最終的なコードはこちら

※コードの変更点

スプライトのタイルデータがプレイヤーとフレンド 2 つになったので、タイルの番号や読み込み数が変わってます。

src/main.c

// 前回
set_sprite_data(0, 6, Charcters); 
set_sprite_tile(1, 5); // フレンドのタイル割り当て

// 今回
set_sprite_data(0, 2, Charcters);
set_sprite_tile(1, 1); // フレンドのタイル割り当て

バックグラウンドの設定

スプライトの set_sprite_data 同様、バックグラウンドも set_bkg_data を使ってセットする。
スプライトは $8000 以降、バックグラウンドは $9000 にタイルデータが読み込まれる。

src/main.c

#include <gb/gb.h>
#include <stdio.h>
#include "Characters.c"
#include "Backgrounds.c" // タイルデータ
#include "StageMap.c" // タイルマップ

/* 省略 */

void main()
{
  set_sprite_data(0, 2, Charcters); // $8000〜$8001
  set_bkg_data(0, 2, Backgrounds); // $9000〜$9001
  
  /* 省略 */
}

さらに、タイルマップをセットする必要がある。
それは set_bkg_tiles(uint8_t x, uint8_t y, uint8_t w, uint8_t h, const uint8_t *tiles) で行う。
x と y は 0 のままで OK。20×18 のマップを作ったので w は 20 で h は 18。
最後の引数に StagaMap.c で作成した "StageMap" を渡す。

src/main.c

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

/* 省略 */

void main()
{
  set_sprite_data(0, 2, Charcters);
  set_bkg_data(0, 2, Backgrounds);
  set_bkg_tiles(0, 0, 20, 18, StageMap); // タイルマップ読み込み
  
  /* 省略 */
}

あとは set_bkg_submap という関数もある。これは GBDK-2020 バージョン 4.0.3 で追加されたもの。
タイルマップのサブマップ(9C00 のこと?)を利用して、32×32 マスを越えるタイルマップを設定できる。
GB で横スクロールなど、32 マスを越えるマップを表現するには、画面外のタイルマップを書き換えないといけないので、そこの処理が楽になりそう。今回は使用しない。

バックグラウンドもスプライト同様、初期状態は非表示になっているので、LCDC の bit 0 を 1 にする。
GBDK では SHOW_BKG マクロを使う。

src/main.c

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

/* 省略 */

void main()
{
  set_sprite_data(0, 2, Charcters);
  set_bkg_data(0, 2, Backgrounds);
  set_bkg_tiles(0, 0, 20, 18, StageMap); // タイルマップ読み込み
  
  /* 省略 */
  
  SHOW_BKG;
  SHOW_SPRITES;
  
  /* 省略 */
}

あとは、第 1 回で指定したキャラクターのスタート位置を、壁の内側に変えておく。

src/main.c

/* 省略 */

void main()
{
  /* 省略 */

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

  // フレンド
  set_sprite_tile(1, 1);
  friend_pos[0] = 152; // 160 -> 152
  friend_pos[1] = 144; // 152 -> 144
  move_sprite(1, friend_pos[0], friend_pos[1]);
  
  /* 省略 */
}

これで OK。ただし、当たり判定を設定していないので、壁をすり抜けられてしまう。

バックグラウンドにタイルマップを適用したゲーム画面

バックグラウンドの当たり判定

配列として出力されているタイルマップの配列を、そのまま当たり判定マップとして使えば OK。

GBDK-2020 には一応 get_bkg_tile_xy(uint_8 x, uint_8 y) という関数がある。
x, y のタイル番号を出してくれるというもの。
ただ、現行バージョンだとエラーが出て使用できない。
あと、VRAM にアクセスするので、パフォーマンスからあまり推奨されていないらしい。
Gameboy Development Forum / ?ASlink-Warning-Byte PCR relocation error for symbol .set_tile_xy
今回のゲームだと player_pos[x, y] からタイルマップ配列の index を求められる。

計算方法としてはこういうのがある。
RPG みたいに 4 方向 1 マスずつの移動ならこれでよさそう。

src/main.c

#define STAGE_WIDTH 20

BOOLEAN is_colliding(UINT8 next_player_x, UINT8 next_player_y) {
  UINT16 grid_X = (next_player_x - 8) / 8;
  UINT16 grid_Y = (next_player_y - 16) / 8;
  UINT16 tile_index = STAGE_WIDTH * grid_Y + grid_X;
  return stageMap[tile_index];
}

今回のマップは横幅 20 で作成しているので、さっきのコードだと乗算が最適化されない。
32×32 で作っておいた方が良かった…。

第 1 回で作った is_colliding_xis_colliding_y を消して is_colliding を作成する。

src/main.c

BOOLEAN is_colliding(UINT8 next_player_x, UINT8 next_player_y)
{
  // 画面端
  if (next_player_x - 1 >= 160 || next_player_x - 1 <= 1 || next_player_y - 1 >= 152 || next_player_y - 1 <= 16) {
    return TRUE;
  }

  UINT16 grid_X;
  UINT16 grid_Y;
  UINT16 tile_index;
  BOOLEAN result = FALSE;
  UINT8 i;

  for (i = 0; i < 8; i++) {
    grid_X = (next_player_x - 8 + i) / 8;
    grid_Y = (next_player_y - 16 + i) / 8;
    tile_index = STAGE_WIDTH * grid_Y + grid_X;
    if (StageMap[tile_index] != 0x00) {
      result = TRUE;
    }
  }

  return result;
}

そして while 内を変更した。

src/main.c

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

    if (joypad_control & J_UP)
    {
      if (!is_colliding(player_pos[0], player_pos[1] - 1))
      {
        player_pos[1]--;
        move_sprite(0, player_pos[0], player_pos[1]);
      }
    }
    else if (joypad_control & J_DOWN)
    {
      if (!is_colliding(player_pos[0], player_pos[1] + 1))
      {
        player_pos[1]++;
        move_sprite(0, player_pos[0], player_pos[1]);
      }
    }
    if (joypad_control & J_RIGHT)
    {
      if (!is_colliding(player_pos[0] + 1, player_pos[1]))
      {
        player_pos[0]++;
        move_sprite(0, player_pos[0], player_pos[1]);
      }
    }
    else if (joypad_control & J_LEFT)
    {
      if (!is_colliding(player_pos[0] - 1, player_pos[1]))
      {
        player_pos[0]--;
        move_sprite(0, player_pos[0], player_pos[1]);
      }
    }
    
    is_end = is_colliding_friend();

    wait_vbl_done();
  }

終わり

とりあえずバックグラウンドを置いて、簡素な当たり判定を追加できた。
ウィンドウ編 に続く。