【GBDK】第二回 自作ゲームボーイソフトを作ろう! 〜ゲーム制作編「倉庫番」〜

スポンサーリンク
PC創作
マイケル
マイケル
みなさんこんにちは!
マイケルです!
エレキベア
エレキベア
こんにちクマ〜〜
マイケル
マイケル
今日は前回に引き続き、GBDKを使ったゲームボーイソフト制作
を進めていくよ!
マイケル
マイケル
第2回ということで、お待ちかね ゲーム制作編 です!
エレキベア
エレキベア
楽しみクマ〜〜〜
どんなゲームを作るクマ??
マイケル
マイケル
ゲームボーイのタイルを並べる画面構成に合わせて、
簡単な 倉庫番ゲーム を作ろうと思うよ!
完成品としては以下のような感じだ!
01 倉庫番
↑制作する倉庫番ゲーム
マイケル
マイケル
頑張ってマップの自動生成処理も入れてみたんだけど、
そこは次回に回して、メイン処理の部分だけざっと紹介します!
エレキベア
エレキベア
わりと本格的クマね〜〜
マイケル
マイケル
一通り遊べるようにはしたんだ!
ソースはGitHubにアップしてあるので、自由に使ってください。

エレキベア
エレキベア
(エンジニアっぽいクマ・・・)
マイケル
マイケル
それでは早速やっていこう!
スポンサーリンク

GBDKの基本操作

マイケル
マイケル
ゲーム制作に入る前に、軽くゲームボーイの仕様と
GBDKの基本操作を確認しておきましょう!

画像の表示方法

マイケル
マイケル
まず画像の表示ですが、ゲームボーイでは下記のように
8*8pxを1タイル として表示します。
そしてタイルを描画できる全体の領域は 256*256px ですが、
画面に表示できる範囲は 160*144px の範囲になります。
ScreenShot 2021 06 13 0 32 38
 ↑8*8px を1タイルとして表示、画面表示領域は160*144px
エレキベア
エレキベア
このタイル単位でしか描画できないクマね
マイケル
マイケル
用意した画像をタイルとして事前に登録しておくことで、
指定して描画できるようになるわけだね!
タイル画像の作成
マイケル
マイケル
タイル画像を表示するには、16ビットのデータに変換してソースファイルに記述
しなければいけません。
GBTD というツールを使えば簡単に変換できるようなので、こちらを使用します。

※Windows専用

マイケル
マイケル
ツールを使って下記のように適当に書いたら、
C言語のソースファイルとして出力します。
ScreenShot 2021 06 05 14 31 57
↑タイルを描く
ScreenShot 2021 06 05 14 34 11
↑C言語のソースファイルとして出力(ラベル名は任意)
unsigned char block_tile[] =
{
  0xF7,0x08,0xF7,0x08,0xF7,0x08,0x00,0xFF,
  0x7F,0x80,0x7F,0x80,0x7F,0x80,0x00,0xFF
};
↑出力されたファイル
マイケル
マイケル
このように出力されればオーケーです!
このソースを使って表示していきましょう!
エレキベア
エレキベア
やったるクマ〜〜〜
タイル画像の描画
マイケル
マイケル
それではタイル画像を描画してみましょう!
手順としては タイル画像を登録して表示する 流れになります。
マイケル
マイケル
登録できるタイルには下記の3種類があります。

・BG(背景の表示に使用)
・ウィンドウ(メッセージ等の表示に使用。背景の上に表示できる。)
・スプライト(キャラクター等の表示に使用。重ねたりできる。)


今回作るゲームは背景表示の切り替えだけでゲームが実現できそうなので、
BGのタイルとして登録して使用 しましょう。

エレキベア
エレキベア
いろんな種類があるクマね
マイケル
マイケル
BGタイルとして登録するため、
まずは出力したファイルを下記のように修正しましょう!

/* BGタイル配列 */
unsigned char block_tile[] =
{
  /* 0x00 白背景 */
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  /* 0x01 ブロック */
  0xF7,0x08,0xF7,0x08,0xF7,0x08,0x00,0xFF,
  0x7F,0x80,0x7F,0x80,0x7F,0x80,0x00,0xFF
};

/* 白背景 */
unsigned char blank[]  = { 0x00 };
/* ブロック */
unsigned char block[]  = { 0x01 };
unsigned char block_16[]  = { 0x01, 0x01, 0x01, 0x01 };

↑タイル情報の修正
マイケル
マイケル
BGは 最初のタイルがデフォルトで全領域に表示される ようなので、
頭に 白背景(全て0x00)を挿入 します。
登録したタイルは、16ビットごとに0x00、0x01、0x02〜と番号が振られて使用できる ようになります。
マイケル
マイケル
ソースを修正して読み込んだら、下記のように記述してタイル画像を描画できます!
#include <gb/gb.h>
#include "block.c"

void main(void)
{
    // 背景を有効化
    SHOW_BKG;
    // BGタイルを登録
    set_bkg_data(0, sizeof(block_tile)/16, block_tile);
    // タイルを表示(8x8)
    set_bkg_tiles(2, 4, 1, 1, block);
    // タイルを表示(16x16)
    set_bkg_tiles(8, 10, 2, 2, block_16);
}
↑タイル画像の描画
ScreenShot 2021 06 13 1 04 23
↑実行結果
エレキベア
エレキベア
表示の仕方は大体分かったクマ〜〜
マイケル
マイケル
続けてボタンの入力操作を見ていこう!

ボタン入力

マイケル
マイケル
ボタンの入力は、 joypad() より取得します!
下記のように記述することで検知することができます!
#include <gb/gb.h>
#include "block.c"

void main(void)
{
    // 背景を有効化
    SHOW_BKG;
    // BGタイルを登録
    set_bkg_data(0, sizeof(block_tile)/16, block_tile);
    // タイルを表示(8x8)
    set_bkg_tiles(2, 4, 1, 1, block);
    // タイルを表示(16x16)
    set_bkg_tiles(8, 10, 2, 2, block_16);

    // ボタン押下情報
    char button;
    char pre_button;
    // ブロック位置
    int block_pos[2] = {2, 4};
    while (1) {
        // 入力検知
        button = joypad();
        if (button != pre_button) {
            // 押下されたボタンに応じて移動処理を行う
            if (button & J_UP   ) block_pos[1]-=1;
            if (button & J_DOWN ) block_pos[1]+=1;
            if (button & J_LEFT ) block_pos[0]-=1;
            if (button & J_RIGHT) block_pos[0]+=1;
            // ブロックを移動させる
            set_bkg_tiles(block_pos[0], block_pos[1], 1, 1, block);
        }
        pre_button = button;
    }
}
↑ボタン入力の検知
02 ブロック移動
↑ボタン入力に応じて移動する
エレキベア
エレキベア
動いたクマ〜〜〜
マイケル
マイケル
これで基本操作は押さえたので、
ゲーム制作にとりかかっていきましょう!
スポンサーリンク

倉庫番ゲームの制作

マイケル
マイケル
それでは、冒頭でお見せした倉庫番ゲームを作っていきます!
こちらは全部で3つのソースファイルで構成されています!
.
├── map.c.     マップ関連処理
├── sokoban.c  メイン処理
└── tile.h.    タイル関連処理
↑倉庫番ゲームのファイル構成
エレキベア
エレキベア
シンプルで分かりやすいクマ
マイケル
マイケル
それぞれのファイルの内容について、

・BGタイルの定義
・マップの定義
・メイン処理

の順番で解説していきます!

BGタイルの定義

マイケル
マイケル
まずはBGタイルの定義!
こちらはGBTDツールで下記4枚の画像を作成し、定義しています!
ScreenShot 2021 06 13 11 49 35
↑ブロック(8*8px)
ScreenShot 2021 06 13 11 49 53
↑ボックス(16*16px)
ScreenShot 2021 06 13 11 50 08
↑プレイヤー(16*16px)
ScreenShot 2021 06 13 11 50 20
↑ポイント(16*16px)
マイケル
マイケル
今回作るゲームは、基本的に16*16を1タイルとして扱う ため、
4つのデータを1つの配列としてまとめました!
#ifndef TILE_H
#define TILE_H

/* BGタイル配列 */
unsigned char tile[] =
{
  /* 0x00 白背景 */
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  /* 0x01 ブロック */
  0xF7,0x08,0xF7,0x08,0xF7,0x08,0x00,0xFF,
  0x7F,0x80,0x7F,0x80,0x7F,0x80,0x00,0xFF,
  /* 0x02-0x05 エレキベア */
  0x00,0x0C,0x00,0x1E,0x01,0x1E,0x03,0x0C,
  0x00,0x0F,0x02,0x0F,0x01,0x0E,0x03,0x0F,
  0x00,0x1F,0x00,0x3E,0x00,0x3C,0x00,0x3C,
  0x00,0x3C,0x38,0x04,0x00,0x0F,0x00,0x15,
  0x40,0x30,0xC0,0x38,0xE0,0x18,0xC0,0x30,
  0x80,0x70,0x40,0xF0,0x80,0x70,0xC0,0xF0,
  0x00,0xF8,0x00,0x7C,0x00,0x3C,0x00,0x3C,
  0x00,0x3C,0x1C,0x20,0x00,0xF0,0x00,0xA8,
  /* 0x06-0x09 ポイント */
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x01,0x00,0x03,0x00,0x06,
  0x00,0x06,0x00,0x03,0x00,0x01,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  0x00,0x00,0x00,0x80,0x00,0xC0,0x00,0x60,
  0x00,0x60,0x00,0xC0,0x00,0x80,0x00,0x00,
  0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
  /* 0x0a-0x0d 箱 */
  0x00,0x00,0x7F,0x7F,0x7F,0x40,0x77,0x48,
  0x70,0x4F,0x65,0x5A,0x78,0x47,0x7B,0x44,
  0x60,0x5F,0x71,0x4E,0x71,0x4E,0x6A,0x55,
  0x7B,0x44,0x7F,0x40,0x7F,0x7F,0x00,0x00,
  0x00,0x00,0xFE,0xFE,0xFE,0x02,0xBE,0x42,
  0x06,0xFA,0x4E,0xB2,0x86,0x7A,0xB6,0x4A,
  0x86,0x7A,0xB6,0x4A,0x86,0x7A,0xB6,0x4A,
  0x86,0x7A,0xFE,0x02,0xFE,0xFE,0x00,0x00
};

/* BGタイル番号配列 */
unsigned char blank_tile[]  = { 0x00, 0x00, 0x00, 0x00 }; // ブランク(白背景)2x2
unsigned char block_tile[]  = { 0x01, 0x01, 0x01, 0x01 }; // ブロック 2x2
unsigned char player_tile[] = { 0x02, 0x04, 0x03, 0x05 }; // プレイヤー 2x2
unsigned char point_tile[]  = { 0x06, 0x08, 0x07, 0x09 }; // ポイント 2x2
unsigned char box_tile[]    = { 0x0a, 0x0c, 0x0b, 0x0d }; // ボックス 2z2
unsigned char blank_single_tile[]  = { 0x00 }; // ブランク(白背景)1x1
unsigned char block_single_tile[]  = { 0x01 }; // ブロック 1x1

/* マップ用のタイル番号 */
const int BLANK_TILE_NO  = 0; // ブランク(白背景)
const int BLOCK_TILE_NO  = 1; // ブロック
const int PLAYER_TILE_NO = 2; // プレイヤー
const int POINT_TILE_NO  = 3; // ポイント
const int BOX_TILE_NO    = 4; // ボックス
const int WALK_TILE_NO   = 9; // 歩行ポイント(自動生成用)

/* BGタイル番号配列取得処理 */
char* get_tile(int index)
{
  if (index == BLOCK_TILE_NO) {         // 1: ブロック
    return block_tile;
  } else if (index == PLAYER_TILE_NO) { // 2: プレイヤー 
    return player_tile;
  } else if (index == POINT_TILE_NO) {  // 3: ポイント
    return point_tile;
  } else if (index == BOX_TILE_NO) {    // 4: ボックス
    return box_tile;
  } else {                              // その他: ブランク(白背景)
    return blank_tile;
  }
}

#endif // TILE_H
↑タイルの定義
エレキベア
エレキベア
作った画像をこんな感じの配列にまとめたら
番号が割り振られるクマね
マイケル
マイケル
ちなみにマップ用のタイル番号 については、
この後解説するマップで扱う定数になります!

マップの定義

マイケル
マイケル
次はマップの定義についてです!
先ほど書いた通り今回のゲームでは 16*16を1タイルとして扱うため、
マップ全体を10*9の配列として定義 しています!
#include <stdio.h>
#include <rand.h>
#include "tile.h"

/* マップ情報(10x9) */
const int MAP_WIDTH = 10;
const int MAP_HEIGHT = 9;
// マップ配列
unsigned int map[9][10];
// 初期化用デフォルトマップ
unsigned int default_map[9][10] = 
{
  {1,1,1,1,1,1,1,1,1,1},
  {1,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,1},
  {1,0,0,0,0,0,0,0,0,1},
  {1,1,1,1,1,1,1,1,1,1},
};
// タイトル画面表示用マップ
unsigned int title_map[18][20] = 
{
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,0,1,0,0,1,0,0,0,1,0,0,1,1,1,1,0,1,1},
  {1,1,0,0,1,0,1,0,1,1,1,1,0,0,0,0,1,0,1,1},
  {1,1,0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,1,1},
  {1,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,1,0,1,1},
  {1,1,0,1,1,0,0,0,0,1,1,0,0,1,1,1,1,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,0,1,0,1,1,0,1,0,1,1,0,1,0,0,1,1},
  {1,1,0,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,1,1},
  {1,1,0,0,0,1,0,1,0,0,0,0,0,0,0,1,0,0,1,1},
  {1,1,0,0,1,0,0,0,1,0,0,0,1,1,1,0,0,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
};
// クリア文字表示用マップ
unsigned int clear_map[18][20] = 
{
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,1,1},
  {1,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,1},
  {1,1,0,0,0,0,0,1,1,1,0,0,1,1,1,1,1,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1},
  {1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1},
};

・・・以下略(マップ自動生成処理は次回解説)・・・
マイケル
マイケル
この時、タイルの定義で記述した定数(ブロックは1、プレイヤーは2など)
を使って、マップを表現しています!
マイケル
マイケル
例えば、下記のようなマップだとこのような配列になります。
ScreenShot 2021 06 13 11 55 26

  {1,1,1,1,1,1,1,1,1,1},
  {1,0,1,1,0,1,1,1,0,1},
  {1,1,1,0,0,0,0,1,1,1},
  {1,1,0,0,0,3,0,0,1,1},
  {1,1,1,0,4,4,0,1,0,1},
  {1,1,0,0,2,0,0,0,0,1},
  {1,0,0,1,0,0,0,3,0,1},
  {1,0,0,0,0,0,0,0,1,1},
  {1,1,1,1,1,1,1,1,1,1},

↑上記マップを表した配列
マイケル
マイケル
なおマップ配列の自動生成処理の解説については、
長くなってしまうため次回に持ちこそうと思います…。
エレキベア
エレキベア
そっちも楽しみクマね

メイン処理

マイケル
マイケル
そして最後にメイン処理!
準備したタイル、マップの定義を使ってゲームを実装しています!
マイケル
マイケル
全体の処理としては下記になります。
#include <gb/gb.h>
#include <string.h>
#include "tile.h"
#include "map.c"

/* プレイヤー */
typedef struct
{
  int x;
  int y;
} Player;
Player player;

/* ボタン押下情報 */
char button;
char pre_button;

/* プロトタイプ宣言 */
void disp_title();   // ゲームタイトル画面表示
void game_start();   // ゲーム開始処理
void init();         // ゲーム初期化処理
void update();       // ゲーム更新処理
void disp_clear();   // ゲームクリア画面表示
char* get_tile(int); // BGタイル番号配列取得処理

/* メイン処理 */
void main(void)
{
  // 初期処理(背景有効化、BGタイルの設定)
  SHOW_BKG;
  set_bkg_data(0, sizeof(tile)/16, tile);
  // タイトル画面の表示
  disp_title();
  // ゲームスタート
  game_start();
}

/* タイトル画面表示 */
void disp_title()
{
  // 乱数シードを保持
  UWORD seed = DIV_REG;

  // タイトル画面の表示
  for (int y = 0; y < MAP_HEIGHT*2; y++) {
    for (int x = 0; x < MAP_WIDTH*2; x++) {
      if (title_map[y][x] == BLOCK_TILE_NO) {
        set_bkg_tiles(x, y, 1, 1, block_single_tile);
      } else {
        set_bkg_tiles(x, y, 1, 1, blank_single_tile);
      }
    }
  }

  // ボタンの入力検知
  button = joypad();
  pre_button = button;
  while (1) { 
    button = joypad();
    if (pre_button != button) {
      // スタートボタン押下で次処理へ
      if (button & J_START) {
        break;
      }
    }
    pre_button = button;
  }

  // 乱数初期化
  // ※ボタン押下までの時間を使用してランダム調整
  seed |= (UWORD)DIV_REG << 8;
  initrand(seed);

  // ゲームスタート
  game_start();
}

/* ゲーム開始処理 */
void game_start()
{
  init();
  update();
}

/* ゲーム初期化処理 */
void init()
{
  // マップ配列生成
  generate_map();
  
  // マップの描画
  for (int y = 0; y < MAP_HEIGHT; y++) {
    for (int x = 0; x < MAP_WIDTH; x++) {
      // 番号に紐づくオブジェクトを描画
      set_bkg_tiles(x*2, y*2, 2, 2, get_tile(map[y][x]));
      // プレイヤータイルの場合、変数に設定
      if (map[y][x] == PLAYER_TILE_NO) {
        player.x = x;
        player.y = y;
      }
    }
  }
}

/* ゲーム更新処理 */
void update()
{
  int player_pos[2]; // 移動後のプレイヤー位置
  int box_pos[2];    // 移動後のボックス位置
  int is_clear = 0;  // クリアフラグ
  int is_retly = 0;  // リトライフラグ

  while(1) {
    // ボタンの入力検知
    button = joypad();
    if (pre_button != button) {
      // スタートボタン押下でリトライ
      if (button & J_START) {
        is_retly = 1;
        break;
      }

      // プレイヤーの移動後位置を求める
      player_pos[0] = player.x;
      player_pos[1] = player.y;
      if (button & J_UP   ) player_pos[1]-=1;
      if (button & J_DOWN ) player_pos[1]+=1;
      if (button & J_LEFT ) player_pos[0]-=1;
      if (button & J_RIGHT) player_pos[0]+=1;
      // 以下の場合、移動処理を行わない
      // ① プレイヤーの移動後位置がマップ領域を超えている場合
      if (player_pos[0] < 0 || MAP_WIDTH-1 < player_pos[0]
          || player_pos[1] < 0 || MAP_HEIGHT-1 < player_pos[1]) {
        continue;
      }
      // ② プレイヤーの移動後位置がブロックの場合
      if (map[player_pos[1]][player_pos[0]] == BLOCK_TILE_NO) {
        continue;
      }

      // プレイヤーの移動後位置がボックスの場合
      if (map[player_pos[1]][player_pos[0]] == BOX_TILE_NO) {
        // ボックスの移動後位置を求める
        box_pos[0] = player_pos[0];
        box_pos[1] = player_pos[1];
        if (button & J_UP   ) box_pos[1]-=1;
        if (button & J_DOWN ) box_pos[1]+=1;
        if (button & J_LEFT ) box_pos[0]-=1;
        if (button & J_RIGHT) box_pos[0]+=1;
        // 以下の場合、移動処理を行わない
        // ① ボックスの移動後位置がマップ領域を超えている場合
        if (box_pos[0] < 0 || MAP_WIDTH-1 < box_pos[0]
            || box_pos[1] < 0 || MAP_HEIGHT-1 < box_pos[1]) {
          continue;
        }
        // ② ボックスの移動後位置がブロックの場合
        if (map[box_pos[1]][box_pos[0]] == BLOCK_TILE_NO) {
          continue;
        }
        // ボックス移動処理
        if (map[box_pos[1]][box_pos[0]] == POINT_TILE_NO) {
          // 移動後位置がポイントの場合、ボックスとポイントを消す
          map[player_pos[1]][player_pos[0]] = BLANK_TILE_NO;
          map[box_pos[1]][box_pos[0]]       = BLANK_TILE_NO;
          set_bkg_tiles(player_pos[0]*2, player_pos[1]*2, 2, 2, blank_tile);
          set_bkg_tiles(box_pos[0]*2, box_pos[1]*2, 2, 2, blank_tile);
        } else {
          // ボックスを移動させる
          map[player_pos[1]][player_pos[0]] = BLANK_TILE_NO;
          map[box_pos[1]][box_pos[0]]       = BOX_TILE_NO;
          set_bkg_tiles(player_pos[0]*2, player_pos[1]*2, 2, 2, blank_tile);
          set_bkg_tiles(box_pos[0]*2, box_pos[1]*2, 2, 2, box_tile);
        }
      }

      // プレイヤー移動処理
      // 現在位置がプレイヤーのタイル番号の場合、白背景に設定
      if (map[player.y][player.x] == PLAYER_TILE_NO) {
        map[player.y][player.x] = BLANK_TILE_NO;
      }
      // 元いた場所をマップ指定のオブジェクトに戻す
      set_bkg_tiles(player.x*2, player.y*2, 2, 2, get_tile(map[player.y][player.x]));   
      // プレイヤーを移動させる
      player.x = player_pos[0];
      player.y = player_pos[1];
      set_bkg_tiles(player.x*2, player.y*2, 2, 2, player_tile);

      // マップにボックスが1つも無ければクリア
      is_clear = 1;
      for (int y = 0; y < MAP_HEIGHT; y++) {
        for (int x = 0; x < MAP_WIDTH; x++) {
          if (map[y][x] == BOX_TILE_NO) {
            is_clear = 0;
          }
        }
      }
      if (is_clear == 1) {
        break;
      }
    }
    // 押下ボタンを保持
    pre_button = button;
  }

  // 以下、ループから抜けた後の処理
  // クリア処理
  if (is_clear == 1) {
    disp_clear();
    return;
  }
  // リトライ処理
  if (is_retly == 1) {
    game_start();
    return;
  }
}

/* ゲームクリア画面表示 */
void disp_clear()
{
  // クリア文字の表示
  for (int y = 0; y < MAP_HEIGHT*2; y++) {
    for (int x = 0; x < MAP_WIDTH*2; x++) {
      if (clear_map[y][x] == BLOCK_TILE_NO) {
        set_bkg_tiles(x, y, 1, 1, block_single_tile);
      } else {
        set_bkg_tiles(x, y, 1, 1, blank_single_tile);
      }
    }
  }
  // ボタンの入力検知
  button = joypad();
  pre_button = button;
  while (1) { 
    button = joypad();
    if (pre_button != button) {
      // スタートボタンかAボタン押下で次処理へ
      if ((button & J_START) || (button & J_A)) {
        break;
      }
    }
    pre_button = button;
  }
  // ゲーム再スタート
  game_start();
}
↑倉庫番ゲームメイン処理
エレキベア
エレキベア
長くてわからんクマ・・・
マイケル
マイケル
少しずつ分けて見ていきましょう!
タイトル画面の表示
マイケル
マイケル
まずはタイトル画面の表示!
事前に用意したタイトル画面のマップを表示してボタンの入力検知を行なっています。

/* タイトル画面表示 */
void disp_title()
{
  // 乱数シードを保持
  UWORD seed = DIV_REG;

  // タイトル画面の表示
  for (int y = 0; y < MAP_HEIGHT*2; y++) {
    for (int x = 0; x < MAP_WIDTH*2; x++) {
      if (title_map[y][x] == BLOCK_TILE_NO) {
        set_bkg_tiles(x, y, 1, 1, block_single_tile);
      } else {
        set_bkg_tiles(x, y, 1, 1, blank_single_tile);
      }
    }
  }

  // ボタンの入力検知
  button = joypad();
  pre_button = button;
  while (1) { 
    button = joypad();
    if (pre_button != button) {
      // スタートボタン押下で次処理へ
      if (button & J_START) {
        break;
      }
    }
    pre_button = button;
  }

  // 乱数初期化
  // ※ボタン押下までの時間を使用してランダム調整
  seed |= (UWORD)DIV_REG << 8;
  initrand(seed);

  // ゲームスタート
  game_start();
}
↑タイトル画面表示処理
ScreenShot 2021 06 13 12 00 03
↑タイトル画面
マイケル
マイケル
ほんとは文字も画像として用意した方がいいんだろうけど、
面倒臭いのでブロックで無理やり書きました・・・。
エレキベア
エレキベア
(適当クマ・・・・。)
マイケル
マイケル
ちなみにマップの自動生成処理の際に乱数取得を行うため、
ボタン入力までの時間を利用して乱数シードの設定 も行なっています!
こちらは次回みていきましょう!
エレキベア
エレキベア
ゲームボーイだと時刻機能も無いから
そういう工夫が必要なのクマね・・・
マップの描画処理
マイケル
マイケル
タイトル画面でボタンが押下されたら次はマップ生成処理!
・・・なのですが、こちらの自動生成処理は次回解説します。

/* ゲーム初期化処理 */
void init()
{
  // マップ配列生成
  generate_map();
  
  // マップの描画
  for (int y = 0; y < MAP_HEIGHT; y++) {
    for (int x = 0; x < MAP_WIDTH; x++) {
      // 番号に紐づくオブジェクトを描画
      set_bkg_tiles(x*2, y*2, 2, 2, get_tile(map[y][x]));
      // プレイヤータイルの場合、変数に設定
      if (map[y][x] == PLAYER_TILE_NO) {
        player.x = x;
        player.y = y;
      }
    }
  }
}
↑マップ生成処理
エレキベア
エレキベア
(気になるクマ・・・。)
プレイヤーを動かす処理
マイケル
マイケル
そして次はメインとなるプレイヤー移動処理!
押下されたボタンに応じてマップを移動するようにしています!

/* プレイヤー */
typedef struct
{
  int x;
  int y;
} Player;
Player player;

/* ボタン押下情報 */
char button;
char pre_button;

・・・略・・・

/* ゲーム更新処理 */
void update()
{
  int player_pos[2]; // 移動後のプレイヤー位置
  int box_pos[2];    // 移動後のボックス位置
  int is_clear = 0;  // クリアフラグ
  int is_retly = 0;  // リトライフラグ

  while(1) {
    // ボタンの入力検知
    button = joypad();
    if (pre_button != button) {
      // スタートボタン押下でリトライ
      if (button & J_START) {
        is_retly = 1;
        break;
      }

      // プレイヤーの移動後位置を求める
      player_pos[0] = player.x;
      player_pos[1] = player.y;
      if (button & J_UP   ) player_pos[1]-=1;
      if (button & J_DOWN ) player_pos[1]+=1;
      if (button & J_LEFT ) player_pos[0]-=1;
      if (button & J_RIGHT) player_pos[0]+=1;
      // 以下の場合、移動処理を行わない
      // ① プレイヤーの移動後位置がマップ領域を超えている場合
      if (player_pos[0] < 0 || MAP_WIDTH-1 < player_pos[0]
          || player_pos[1] < 0 || MAP_HEIGHT-1 < player_pos[1]) {
        continue;
      }
      // ② プレイヤーの移動後位置がブロックの場合
      if (map[player_pos[1]][player_pos[0]] == BLOCK_TILE_NO) {
        continue;
      }

・・・略・・・

      // プレイヤー移動処理
      // 現在位置がプレイヤーのタイル番号の場合、白背景に設定
      if (map[player.y][player.x] == PLAYER_TILE_NO) {
        map[player.y][player.x] = BLANK_TILE_NO;
      }
      // 元いた場所をマップ指定のオブジェクトに戻す
      set_bkg_tiles(player.x*2, player.y*2, 2, 2, get_tile(map[player.y][player.x]));   
      // プレイヤーを移動させる
      player.x = player_pos[0];
      player.y = player_pos[1];
      set_bkg_tiles(player.x*2, player.y*2, 2, 2, player_tile);

      // マップにボックスが1つも無ければクリア
      is_clear = 1;
      for (int y = 0; y < MAP_HEIGHT; y++) {
        for (int x = 0; x < MAP_WIDTH; x++) {
          if (map[y][x] == BOX_TILE_NO) {
            is_clear = 0;
          }
        }
      }
      if (is_clear == 1) {
        break;
      }
    }
    // 押下ボタンを保持
    pre_button = button;
  }

  // 以下、ループから抜けた後の処理
  // クリア処理
  if (is_clear == 1) {
    disp_clear();
    return;
  }
  // リトライ処理
  if (is_retly == 1) {
    game_start();
    return;
  }
}
↑プレイヤー移動処理
マイケル
マイケル
移動先がブロックの場合には移動しない等、
細かいチェックも入れています!
エレキベア
エレキベア
マップ配列を使って移動処理を行なっているクマね
ボックスを動かす処理
マイケル
マイケル
そして倉庫番ゲームということで、移動後の位置にボックスがあったら動かさなければなりません。
そちらの判定は下記のように実装しています!

/* プレイヤー */
typedef struct
{
  int x;
  int y;
} Player;
Player player;

/* ボタン押下情報 */
char button;
char pre_button;

・・・略・・・

/* ゲーム更新処理 */
void update()
{
  int player_pos[2]; // 移動後のプレイヤー位置
  int box_pos[2];    // 移動後のボックス位置

・・・略・・・

  while(1) {

・・・略・・・

      // プレイヤーの移動後位置がボックスの場合
      if (map[player_pos[1]][player_pos[0]] == BOX_TILE_NO) {
        // ボックスの移動後位置を求める
        box_pos[0] = player_pos[0];
        box_pos[1] = player_pos[1];
        if (button & J_UP   ) box_pos[1]-=1;
        if (button & J_DOWN ) box_pos[1]+=1;
        if (button & J_LEFT ) box_pos[0]-=1;
        if (button & J_RIGHT) box_pos[0]+=1;
        // 以下の場合、移動処理を行わない
        // ① ボックスの移動後位置がマップ領域を超えている場合
        if (box_pos[0] < 0 || MAP_WIDTH-1 < box_pos[0]
            || box_pos[1] < 0 || MAP_HEIGHT-1 < box_pos[1]) {
          continue;
        }
        // ② ボックスの移動後位置がブロックの場合
        if (map[box_pos[1]][box_pos[0]] == BLOCK_TILE_NO) {
          continue;
        }
        // ボックス移動処理
        if (map[box_pos[1]][box_pos[0]] == POINT_TILE_NO) {
          // 移動後位置がポイントの場合、ボックスとポイントを消す
          map[player_pos[1]][player_pos[0]] = BLANK_TILE_NO;
          map[box_pos[1]][box_pos[0]]       = BLANK_TILE_NO;
          set_bkg_tiles(player_pos[0]*2, player_pos[1]*2, 2, 2, blank_tile);
          set_bkg_tiles(box_pos[0]*2, box_pos[1]*2, 2, 2, blank_tile);
        } else {
          // ボックスを移動させる
          map[player_pos[1]][player_pos[0]] = BLANK_TILE_NO;
          map[box_pos[1]][box_pos[0]]       = BOX_TILE_NO;
          set_bkg_tiles(player_pos[0]*2, player_pos[1]*2, 2, 2, blank_tile);
          set_bkg_tiles(box_pos[0]*2, box_pos[1]*2, 2, 2, box_tile);
        }
      }

・・・略・・・

  }

・・・略・・・

}
↑ボックス移動処理
マイケル
マイケル
プレイヤーとボックスのどちらも移動が可能かを
判定する必要があることに注意です!
エレキベア
エレキベア
これで基本的な動きはできるようになったクマね
クリア画面の表示
マイケル
マイケル
最後はゲームクリア処理!
マップ上にボックスが無くなったことを検知したら下記処理に遷移します!

/* ゲームクリア画面表示 */
void disp_clear()
{
  // クリア文字の表示
  for (int y = 0; y < MAP_HEIGHT*2; y++) {
    for (int x = 0; x < MAP_WIDTH*2; x++) {
      if (clear_map[y][x] == BLOCK_TILE_NO) {
        set_bkg_tiles(x, y, 1, 1, block_single_tile);
      } else {
        set_bkg_tiles(x, y, 1, 1, blank_single_tile);
      }
    }
  }
  // ボタンの入力検知
  button = joypad();
  pre_button = button;
  while (1) { 
    button = joypad();
    if (pre_button != button) {
      // スタートボタンかAボタン押下で次処理へ
      if ((button & J_START) || (button & J_A)) {
        break;
      }
    }
    pre_button = button;
  }
  // ゲーム再スタート
  game_start();
}
↑クリア画面の表示
ScreenShot 2021 06 13 12 00 14↑クリア画面
マイケル
マイケル
こちらもタイトル画面と同じく、
用意したマップ配列を表示しているだけですね!
エレキベア
エレキベア
これで完成クマ〜〜〜!!!
スポンサーリンク

おわりに

マイケル
マイケル
というわけで以上が倉庫番ゲームの解説でした!
どうだったかな?
エレキベア
エレキベア
小さなところから作っていけば案外難しくなかったクマ
マイケル
マイケル
使い方さえ覚えてしまえば案外いけるし、
制限がある分、逆に悩まなくて済むというのもあるよね。
マイケル
マイケル
ただ個人的にはC言語が書き慣れておらず、
オブジェクト指向感覚で書いてるといろいろ苦戦しました・・・。
(コードが汚く申し訳ないです・・・。)
エレキベア
エレキベア
今ほど便利じゃないからいろいろ困るクマね・・・
マイケル
マイケル
でもまあとりあえずゲームは完成したので、次回はいよいよ実機実行!
・・・の前に、マップの自動生成処理の解説だけちょこっと挟もうと思います。
エレキベア
エレキベア
第2.5回ってところクマね
マイケル
マイケル
それでは今日はこの辺で!
アデュー!!
エレキベア
エレキベア
クマ〜〜〜〜〜〜

【GBDK】第二回 自作ゲームボーイソフトを作ろう! 〜ゲーム制作編「倉庫番」〜 〜完〜

※続きはこちら

コメント