2013/06/18

簡単な釣りゲームを作ってみよう

ご無沙汰しております。2年ぶりの更新です。
※ブログ内の日付は2013/6/18になっていますが(表示階層を合わせるため)
 本当の更新日は2015/8/20です

実は最初のゲームを作った後にもう一つ作ったのですが
色々と準備が面倒になって放置してしまいました。

今回は釣りゲームです。
こんな感じの画面で、ペンギンの乗った船を左右に動かし
Aボタンを押すとエサの付いた釣り糸を垂らします。
(Aボタンを押している間は海底へ落ちていき、離すと引き上げます)
エサを魚たちの口あたりに触れさせると魚がくっついて釣れます。
エサは10個しかありませんが、海底のカニを釣り上げるとエサが増えます。
(根気さえあれば永遠に釣り続けられます)

まずはグラフィックデータの準備です。
GBTDで開いて下さい。
背景用    SeaBkg.gbr
文字用    ascii_misaki.gbr
スプライト用 Penguin2.gbr

そしてマップデータです。
GBMBで開いて下さい。
背景用    SeaBkgMap.gbm

これらのデータをエクスポートしてCソースファイルにしてincludeします。

ソース一式はここからダウンロードできます。

メインのソースプログラムは以下の通りです。
長いといえば長いですが、これだけといえばこれだけなのです。

PenguinFishing.c
#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "SeaBkg.c"
#include "Penguin2Spr.c"
#include "Ascii_misaki.c"
// マップデータ
#include "SeaBkgMap.c"

#define GAMEMODE_INIT    0U
#define GAMEMODE_TITLE   2U
#define GAMEMODE_START   3U
#define GAMEMODE_PLAY    4U
#define GAMEMODE_END     5U

#define SPR_ESA          0U
#define SPR_FISH         1U
#define SPR_ITO         21U
#define SPR_HERO        34U
#define SPR_MAX         40U

#define HERO_ACT_NONE    0U
#define HERO_ACT_MOVE    1U
#define HERO_ACT_DOWN    2U
#define HERO_ACT_UP      3U

#define ENE_ACT_NONE     0U
#define ENE_ACT_MOVE     1U
#define ENE_ACT_UP       2U
#define ENE_ACT_DEAD     3U

#define ENE_MAX          5U

UBYTE intcnt;// カウンタ
UBYTE first;// ファーストフラグ
UBYTE game_mode;// ゲームモード
UBYTE modecnt;// カウンタ
UBYTE input_on, input_old, input_edge, input_trg;// キー入力用
fixed seed;// 乱数シード
UBYTE scrx;// スクロール用X座標
UBYTE scry;// スクロール用Y座標
UWORD score;// スコア
UWORD hiscore;// ハイスコア
UBYTE esa;// エサ

// プレイヤ用構造体
struct st_hero {
 UBYTE act; // アクションno
 UBYTE x;   // X座標
 UBYTE y;   // Y座標
 UBYTE ex;  // エサX座標
 UBYTE ey;  // エサY座標
 UBYTE cnt; // カウンタ
} hero;

// 敵用構造体
struct st_ene {
 UBYTE act; // アクションno
 UBYTE knd; // 種類
 UBYTE x;   // X座標
 UBYTE y;   // Y座標
 BYTE dir; // エサY座標
 UBYTE cnt; // カウンタ
} ene[ENE_MAX];
// 波で上下に揺れる
BYTE wavetbl[]={0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,-1,0,0,0,0,-1};
// 敵というか魚のスプライト番号
UBYTE ene_spr[]={0,0,16,17,18,19,20,21,22,23,24,0,25,0};
// 移動スピード(小さいほど速い)
UBYTE ene_wait[]={1,1,2,4,6,3,5,1};
// 点数
UBYTE ene_score[]={0,100,80,50,10,20,30,0};
// 敵というか魚のアタッチ確率
UBYTE ene_att[]={1,1,1,2,2,2,3,4,4,4,4,4,4,4,5,6};

void hero_ctrl();
void hero_disp();
void ene_touch();
void ene_attach();
void ene_mvdisp();
void bg_num_disp(UWORD n, UBYTE x, UBYTE y, UBYTE d);
void sprite_init();

// タイトル画面
void init_title()
{
 DISPLAY_OFF;// 画面全体を非表示
 scrx = 0;
 scry = 184;
 hero.x = 88;
 hero.y = 64;
 intcnt = 0;
 seed.b.l = DIV_REG;
 sprite_init();// スプライト座標初期化
 // タイトル文字
 gotoxy(2, 28);
 print("FISHING  PENGUIN");
 gotoxy(5, 2);
 print("PUSH START");
 gotoxy(5, 3);
 print("          ");
 gotoxy(1, 1);
 print("                  ");
 DISPLAY_ON;// 画面全体を表示
}
void proc_title()
{
 // 入力待ち
 if (input_trg & J_START) {
  seed.b.h = DIV_REG;
  initrand(seed.w);// 乱数初期化
  game_mode = GAMEMODE_START;// ゲームスタート処理
  first = 0;// ファーストフラグ
 }
}
// スタート・画面を下へスクロール
void init_start()
{
 score = 0;
 esa = 10;
 set_bkg_tiles(0, 0, 20, 3, SeaBkgMapLabel);// マップデータを設定する
 // エサやスコア
 bg_num_disp(esa, 3, 1, 2);// BGへ数字描画
 bg_num_disp(score, 12, 1, 5);// BGへ数字描画
}
void proc_start()
{
 if (scry != 0) scry++;
 else {
  game_mode = GAMEMODE_PLAY;// ゲーム処理
  first = 0;// ファーストフラグ
 }
}
// プレイ
void init_game()
{
 hero.act = HERO_ACT_MOVE;
}
void proc_game()
{
 hero_ctrl();
 ene_touch();
 ene_attach();
 ene_mvdisp();
}
// ゲームオーバー
void init_end()
{
 gotoxy(5, 3);
 print("GAME  OVER");
 if (hiscore < score) hiscore = score;
}
void proc_end()
{
 // 入力待ち
 if (input_trg & J_START) {
  game_mode = GAMEMODE_TITLE;// タイトル処理
  first = 0;// ファーストフラグ
 }
}

// ペンギンコントロール
void hero_ctrl()
{
 if (hero.act == HERO_ACT_MOVE) {
  if (esa == 0) {
   game_mode = GAMEMODE_END;// ゲームエンド処理
   first = 0;// ファーストフラグ
  }
  if (intcnt & 1) {
   if (hero.x > 24U && (input_on & J_LEFT)) hero.x--;
   if (hero.x < 152U && (input_on & J_RIGHT)) hero.x++;
  }
  if (input_trg & J_A) {
   hero.act = HERO_ACT_DOWN;
   esa--;
   bg_num_disp(esa, 3, 1, 2);// BGへ数字描画
  }
 }
 if (hero.act == HERO_ACT_DOWN) {
  hero.cnt++;
  if (hero.cnt == 96U || !(input_on & J_A)) hero.act = HERO_ACT_UP;
 }
 if (hero.act == HERO_ACT_UP) {
  if (hero.cnt != 0) hero.cnt--;
  else hero.act = HERO_ACT_MOVE;
 }
}
// ペンギン表示
void hero_disp()
{
 UBYTE i, chk;
 // 揺れる
 if (intcnt & 1) hero.y += wavetbl[((intcnt>>1) & 0x1fU)];
 // ペンギン&船表示
 for (i = 0; i != 2; i++) {
  set_sprite_tile( SPR_HERO+i, 2+i);// スプライトを設定
  move_sprite( SPR_HERO+i, hero.x-8+(i<<3), hero.y-scry-8);// スプライトの座標を設定
  set_sprite_prop( SPR_HERO+i, S_PRIORITY);
 }
 for (i = 0; i != 4; i++) {
  set_sprite_tile( SPR_HERO+2+i, 4+i);// スプライトを設定
  move_sprite( SPR_HERO+2+i, hero.x-16+(i<<3), hero.y-scry);// スプライトの座標を設定
  set_sprite_prop( SPR_HERO+2+i, S_PRIORITY);
 }
 // エサ表示
 hero.ex = hero.x-8;
 hero.ey = hero.y + hero.cnt - scry;
 set_sprite_tile( SPR_ESA, 1);// スプライトを設定
 move_sprite( SPR_ESA, hero.ex-4, hero.ey-4);// スプライトの座標を設定
 // 糸表示
 set_sprite_tile( SPR_ITO, 15);// スプライトを設定
 move_sprite( SPR_ITO, hero.ex-4, hero.y-scry-8);// スプライトの座標を設定
 // のびる糸
 chk = (hero.cnt >> 3);
 for (i = 0; i != 12; i++) {
  if (chk == i) {
   set_sprite_tile( SPR_ITO+i+1, 8 + (hero.cnt & 0x07U));// スプライトを設定
   move_sprite( SPR_ITO+i+1, hero.ex-4, hero.y-scry+(i<<3));// スプライトの座標を設定
  } else {
   if (chk > i) {
    set_sprite_tile( SPR_ITO+i+1, 15);// スプライトを設定
    move_sprite( SPR_ITO+i+1, hero.ex-4, hero.y-scry+(i<<3));// スプライトの座標を設定
   } else {
    move_sprite( SPR_ITO+i+1, 0, 0);// スプライトの座標を設定
   }
  }
 }
}
// 敵というか魚との当たり判定
void ene_touch()
{
 UBYTE i;
 if (hero.act == HERO_ACT_MOVE) return;
 for (i = 0; i != ENE_MAX; i++) {
  if (ene[i].act == ENE_ACT_MOVE) {
   if (hero.ex >= ene[i].x - 4U) {
    if (hero.ex <= ene[i].x + 4U) {
     if (hero.ey >= ene[i].y - 4U) {
      if (hero.ey <= ene[i].y + 4U) {
       ene[i].act = ENE_ACT_UP;
       hero.act = HERO_ACT_UP;
      }
     }
    }
   }
  }
 }
}
// 敵というか魚のアタッチ
void ene_attach()
{
 UBYTE i, r, spr;
 r = rand();// 乱数取得
 if ((r & 0xf8) != 0) return;
 for (i = 0; i != ENE_MAX; i++) {
  if (ene[i].act == ENE_ACT_NONE) {
   spr = SPR_FISH+(i<<1);
   if (r & 1) {
    ene[i].dir = 1;
    ene[i].x = 248;
    set_sprite_prop( spr, S_FLIPX);
    set_sprite_prop( spr+1, S_FLIPX);
   } else {
    ene[i].dir = -1;
    ene[i].x = 176;
    set_sprite_prop( spr, 0);
    set_sprite_prop( spr+1, 0);
   }
   ene[i].act = ENE_ACT_MOVE;
   ene[i].cnt = 0;
   r = rand() & 0x0f;// 乱数取得
   ene[i].knd = ene_att[r];
   set_sprite_tile( spr, ene_spr[(ene[i].knd<<1)]);// スプライトを設定
   set_sprite_tile( spr+1, ene_spr[((ene[i].knd<<1)+1)]);// スプライトを設定
   if (ene[i].knd == 3) {
    ene[i].y = 148;
   } else {
    r = rand() & 0x3fU;// 乱数取得
    ene[i].y = r + 80;
   }
   break;
  }
 }
}
// 敵というか魚表示
void ene_mvdisp()
{
 UBYTE i, spr;
 BYTE xofs;
 for (i = 0; i != ENE_MAX; i++) {
  if (ene[i].act != ENE_ACT_NONE) {
   spr = SPR_FISH+(i<<1);
   move_sprite( spr, ene[i].x-4, ene[i].y-4);// スプライトの座標を設定
   if (ene[i].dir == -1) {
    xofs = 8;
   } else {
    xofs = -8;
   }
   if (get_sprite_tile(spr) != 0) {
    move_sprite( spr+1, ene[i].x-4+xofs, ene[i].y-4);// スプライトの座標を設定
   }
   ene[i].cnt++;
   // 通常移動
   if (ene[i].act == ENE_ACT_MOVE) {
    if (ene[i].cnt == ene_wait[(ene[i].knd)]) {
     ene[i].cnt = 0;
     ene[i].x += ene[i].dir;
     if (ene[i].x >= 192U && ene[i].x <= 232U) {
      ene[i].act = ENE_ACT_DEAD;
     }
    }
   }
   // 釣られた
   if (ene[i].act == ENE_ACT_UP) {
    ene[i].y--;
    if (hero.cnt == 1) {
     ene[i].act = ENE_ACT_DEAD;
     score += ene_score[(ene[i].knd)];// スコア加算
     if (ene[i].knd == 3) esa += 2;// カニだったらエサ加算
     if (esa > 99U) esa = 99U;
     bg_num_disp(esa, 3, 1, 2);// BGへ数字描画
     bg_num_disp(score, 12, 1, 5);// BGへ数字描画
    }
   }
   // デタッチ
   if (ene[i].act == ENE_ACT_DEAD) {
    ene[i].act = ENE_ACT_NONE;
    move_sprite( spr, 0, 0);// スプライトの座標を設定
    move_sprite( spr+1, 0, 0);// スプライトの座標を設定
   }
  }
 }
}
// BGへ数字描画
// n=表示する数値 x=描画X座標(左端) y=描画Y座標 d=描画桁数
// 10で除算をやってるので処理が重いかも
void bg_num_disp(UWORD n, UBYTE x, UBYTE y, UBYTE d)
{
 UBYTE f = 1;// 最初の0は表示する
 do {
  d--;
  gotoxy(x+d, y);
  if (f == 0 && n == 0)
   setchar(0);// 空白
  else
   setchar(0x30U+(n%10));// 数字
  f = 0;
  if (n != 0) n /= 10;
 } while(d != 0);
}
// game_mode毎の初期化(ファースト)
void init()
{
 if (first == 0) {
  switch (game_mode) {
   case GAMEMODE_INIT:
    game_mode = GAMEMODE_TITLE;// タイトルへ
   case GAMEMODE_TITLE:
    init_title();
    break;
   case GAMEMODE_START:
    init_start();
    break;
   case GAMEMODE_PLAY:
    init_game();
    break;
   case GAMEMODE_END:
    init_end();
    break;
  }
  first = 1;
  modecnt = 0;
 }
}

// game_mode毎の処理
void proc()
{
 switch (game_mode) {
  case GAMEMODE_INIT:
   break;
  case GAMEMODE_TITLE:
   proc_title();
   break;
  case GAMEMODE_START:
   proc_start();
   break;
  case GAMEMODE_PLAY:
   proc_game();
   break;
  case GAMEMODE_END:
   proc_end();
   break;
 }
 set_bkg_data(15, 1, &SeaBkgTileLabel[240+(intcnt & 0x30U)]); // BGアニメ
 intcnt++;
 modecnt++;
}

// スプライト座標初期化
void sprite_init()
{
 UBYTE i;
 for (i = 0; i != SPR_MAX; i++) {
  set_sprite_tile( i, 0);
  move_sprite( i, 0, 0);
  set_sprite_prop( i, 0);
 }
}

// VBL割込で呼ばれる
void vbl_isr(void)
{
 // キー入力
 input_old = input_on;
 input_on = joypad();
 input_edge = input_on ^ input_old;
 input_trg = input_on & input_edge;
 // モード別ファースト処理
 init();
 // モード別実行処理
 proc();
 // BGの描画開始XY座標を設定
 move_bkg(scrx, scry);
 // プレイヤー描画
 hero_disp();
}

// メイン
void main()
{
 DISPLAY_OFF;// 画面全体を非表示
 HIDE_BKG;// BGを非表示
 HIDE_SPRITES;// スプライトを非表示

 print(" ");// ダミー

 // 先にスクロール座標を設定しておく
 init_title();
 move_bkg(scrx, scry);

 set_bkg_data(0, 16, SeaBkgTileLabel);// タイルデータをBGとして設定
 set_bkg_tiles(0, 2, 20, 16, SeaBkgMapLabel+40);// マップデータを設定する
 set_bkg_data(32, 64, AsciiBgLabel);// アスキー文字

 SPRITES_8x8;// スプライトモードを8*8に設定
 set_sprite_data(0, 26, Penguin2SpTileLabel); // タイルデータをスプライトとして設定(0番から26個)

 SHOW_SPRITES;// スプライトを表示
 SHOW_BKG;// BGを表示
 DISPLAY_ON;// 画面全体を表示

 hiscore = 0;//ハイスコア初期化

 disable_interrupts();// 割込無効
 add_VBL(vbl_isr);// VBL割込に追加
 enable_interrupts();// 割込有効
 set_interrupts(VBL_IFLAG);// VBL割込セット

 // メインループ(無くても良いけど何となく)
 while(1) {
  delay(1000UL);
 }
}

今回、特筆すべき所は、Vブランク割込を使用していることでしょうか。
前回はmain()関数の中で無限ループさせていましたが
これだと処理速度が一定になりません。

Vブランク割込で呼ばれるためのvbl_isr(void)関数を作り、main()関数内で登録します。
 disable_interrupts();// 割込無効
 add_VBL(vbl_isr);// VBL割込に追加
 enable_interrupts();// 割込有効
 set_interrupts(VBL_IFLAG);// VBL割込セット
Vブランク割込とは、画面の描画タイミングに合わせて発生する
垂直同期割込とか言われるやつです。興味のある方はググって下さい。

そんなこんなでゲームができました。
ゲーム本体はここからダウンロードできます。
タイトル画面
では、機会がありましたら、またお会いしましょう。

実機で動かしてみよう

せっかく作ったゲームソフトを実機で動かしてみたいと思いませんか?
という訳で実機で動かす方法をご紹介します。

まず「GB USB smart card 64M」というカートリッジを入手します。
※自作ソフトを動かす以外の用途には使わないで下さい。念のため。

入手したら、カートリッジをPCへUSBケーブルで接続します。
接続すると、自動的にドライバがインストールされたような記憶があります。

※私が使っているWindows7ではドライバのインストールが出来ず、使用できませんでした。
 WindowsXPのPCではドライバのインストールが出来ましたので、それを使っています。

付属CDの中身(専用ソフト)をコピーして「GB_USB.exe」を実行します。

起動すると、このようなウインドウが開きます。
右上にUSBのマークが表示されていれば、カートリッジと接続できています。

このカートリッジは32Mのページ2つ(Page NO.1/Page NO.2)を切り替えて使用します。
とりあえず、フォーマットしましょう。 下の方にあるFormatボタンを押します。

では、カートリッジへ書き込む準備をしましょう。
上の段の左下にあるAddボタンを押して、ソフト(*.gbファイル)を選択します。
選択すると、上の段のファイルリストに表示されます。複数選択が可能です。

選択が終わったら上の段の下にある赤い文字のWriteボタンを押せば、書き込み開始です。

書き込みが終わると、下の段のファイルリストに表示されます。
これで完了です。

なお、後からソフトを追加していくことは出来ませんので
一度フォーマットをしてから、入れたいソフトを一気に書き込みます。

あとはゲームボーイ本体へカートリッジを差し込んで電源を入れて下さい。
1ページ内に入れたソフトが1つの場合は、そのソフトが自動的に起動します。
ソフトを複数入れた場合はソフト選択画面が起動します。

もし、ページ2に書き込んだ場合は、電源を入れてから素早く電源を切って入れると
ページが切り替わります。

セーブデータを書き込む事も可能ですが、セーブデータはカートリッジ全体で
1つしか保持できません(最後に起動したゲームに上書きされます)。


自分で作ったものが実機で動くと感動します!

簡単なゲームを作ってみよう(6)

いよいよ仕上げです。
ゲームらしくするため、スコアや制限時間を設けました。
スコアや制限時間は地面にあるアイテム(コイン・時計)を取ることで増えます。
コインはBGアニメしています。

他にも、アイテムの当たり判定を広めにしたり、いろいろ調整しました。
こっそりポーズ(一時停止)も入れています。

Test5e.c
#include <stdio.h>
#include <gb.h>
#include <rand.h>

// タイルデータ
#include "PenguinSpr.c"
#include "IceBkg.c"
// マップデータ
//#include "IceBkgMap.c"

#define SPR_SCORE       0
#define SPR_TIME        6
#define SPR_HERO        9
#define SPR_HERO_SHADOW 11
#define SPR_MAX         40

#define HERO_ACT_NONE      0
#define HERO_ACT_WALK      1
#define HERO_ACT_JUMP      2
#define HERO_ACT_MISS      3

// プレイヤ用構造体
struct st_hero {
 UBYTE act; // アクションno
 UBYTE x;   // X座標
 UBYTE y;   // Y座標
 UBYTE cnt; // 汎用カウンタ
 UBYTE jmp; // ジャンプカウンタ
 UBYTE mtk; // 無敵カウンタ
} hero;

// アニメ用テーブル
UBYTE ani_tiles[]={0, 4, 12, 20};

// ジャンプ用テーブル
#define JMPTBL_MAX     31
BYTE jmptbl[]={
   0, -2, -4, -6, -8, -9,-10,-11,-12,-12,-13,-13,-14,-14,-15,-15,
 -15,-15,-14,-14,-13,-13,-12,-12,-11,-10, -9, -8, -6, -4, -2,  0,
 };

UBYTE intcnt;// カウンタ
UBYTE scrx;// スクロール用X座標
UBYTE scry;// スクロール用Y座標
UBYTE scrcnty;// スクロール用カウンタ
UBYTE bgsety;// BG描画用Y座標
UBYTE input_on, input_old, input_edge, input_trg;// キー入力用
fixed seed;// 乱数シード
UBYTE time, timecnt;// 時間用
#define SCORE_MAX    6
UBYTE scores[SCORE_MAX];

unsigned char bgbuf[4];// BGデータ取得用

void hero_ctrl();
void hero_disp();
void score_disp();
void time_disp();
void make_bghole(UBYTE y);
void make_bgitem(UBYTE y);
void title_disp();

void printxy(char *cp, UBYTE x, UBYTE y);
UBYTE keypad();
void sprite_init();

void main()
{
 UBYTE i;// ループ用
 unsigned char *coin;//アニメ用

 DISPLAY_OFF;// 画面全体を非表示
 
 print(" ");// ダミー描画
 SPRITES_8x16;// スプライトモードを8*16に設定
 set_sprite_data(0, 74, PenguinSpTileLabel); // タイルデータをスプライトとして設定(0番から74個)
 set_bkg_data(0, 8, IceBkgTileLabel); // タイルデータをBGとして設定
 //set_bkg_tiles(0, 0, 20, 32, IceBkgMapLabel); // マップデータを設定する
 
 DISPLAY_ON;// 画面全体を表示
 
 while (1) { //全体のループ
  HIDE_SPRITES;// スプライトを非表示
  HIDE_BKG;// BGを非表示
  
  move_bkg(0, 0);// BGの描画開始XY座標を設定
  cls();// 画面クリア
  sprite_init();// スプライト座標初期化
  title_disp();// タイトル画面を描画
  
  SHOW_BKG;// BGを表示
  
  seed.b.l = DIV_REG;
  
 // while (!(joypad() & J_START));// スタートキーが押されるまで無限ループ
 // waitpad(J_START);// スタートキーの入力待ち
  while (!(keypad() & J_START));// スタートキーが押されるまで無限ループ
  
  seed.b.h = DIV_REG;
 // initrand(seed.w);// 乱数初期化
  initarand(0);// 乱数初期化
  
  DISPLAY_OFF;// 画面全体を非表示
  HIDE_SPRITES;// スプライトを非表示
  HIDE_BKG;// BGを非表示
  
  cls();// 画面クリア
  
  // 初期のBGデータを生成
  for (bgsety = 0; bgsety < 19; bgsety+=2) {
   make_bghole(bgsety);
   make_bgitem(bgsety+1);
  }
  
  SHOW_BKG;// BGを表示
  SHOW_SPRITES;// スプライトを表示
  DISPLAY_ON;// 画面全体を表示
  
  // 初期値を設定
  hero.act = 1;
  hero.x = 88;
  hero.y = 64;
  hero.cnt = 0;
  hero.jmp = 0;
  hero.mtk = 60;
  scry = scrcnty = 0;
  time = 30;
  timecnt = 60;
  for (i = 0; i < SCORE_MAX; i++) scores[i] = 0;
  
  while (time > 0) {// プレイ中の無限ループ
   keypad();// キー入力
   
   if (hero.act != HERO_ACT_MISS) {
    if (++scrcnty > 1) {
     scrcnty = 0;
     scry++;
     // スクロール16ドット単位でBGデータを生成する
     if ((scry & 0x0f) == 0) {
      bgsety += 2;
      bgsety &= 0x1f;// 0~31にする
      make_bghole(bgsety);
      make_bgitem(bgsety+1);
     }
    }
   }
   move_bkg(scrx, scry);// BGの描画開始XY座標を設定
   
   hero_ctrl();// ペンギンコントロール
   hero_disp();// ペンギン表示
   score_disp();// スコア表示
   time_disp();// 時間表示
   
   coin = &IceBkgTileLabel[112+(intcnt & 0x30)];
   set_bkg_data(7, 1, coin); // BGアニメ
   
   // ポーズ
   if (input_trg & J_START) {
    while (!(keypad() & J_START));// スタートキーが押されるまで無限ループ
   }
   
   intcnt++;
   delay(10);// 10ミリ秒待つ
  }
 // gotoxy((6+(scrx>>3)&0x1f), (9+(scry>>3)&0x1f));
 // print("TIME UP!");
  printxy("TIME UP!", (7+(scrx>>3)&0x1f), (9+(scry>>3)&0x1f));
 // waitpad(J_START);// スタートキーの入力待ち
  while (!(keypad() & J_START));// スタートキーが押されるまで無限ループ
 }
}

// タイトル表示
void title_disp()
{
 gotoxy(2, 5);
 print("JUNPING  PENGUIN");
 gotoxy(5, 13);
 print("PUSH START");
}

// ペンギンコントロール
void hero_ctrl()
{
 UBYTE i, sx, sy;// BG調査用
 // ミス中は移動できない
 if (hero.act != HERO_ACT_MISS) {
  // 右入力
  if ((input_on & J_RIGHT) && (hero.x <  160)) hero.x++;
  // 左入力
  if ((input_on & J_LEFT)  && (hero.x >   16)) hero.x--;
  // A入力
  if ((input_trg & J_A)    && (hero.act == HERO_ACT_WALK)) {
   hero.jmp = 1;// ジャンプ開始
   hero.act = HERO_ACT_JUMP;
   hero.cnt = 0;
  }
 }
 scrx = (hero.x >> 1);
 // ジャンプ中
 if (hero.jmp > 0) {
  hero.jmp++;
  if (hero.jmp >= JMPTBL_MAX) {
   hero.jmp = 0;// ジャンプ終了
   hero.act = HERO_ACT_WALK;
   hero.cnt = 0;
  }
  // ミス中でなければ当たり判定
 } else if (hero.act != HERO_ACT_MISS) {
  sx = ((hero.x-8+scrx) >> 3) - 1;
  sy = ((hero.y-16+scry) >> 3);
  get_bkg_tiles(sx,sy,3,1, bgbuf);// BGのデータを取得する
  // 無敵中でなければミス判定
  if (hero.mtk == 0 && bgbuf[1] >= 3 && bgbuf[1] <= 5) {
   hero.act = HERO_ACT_MISS;
   hero.cnt = 0;
  }
  // 時計かコインの当たり判定
  for (i = 0; i < 3; i++) {
   if (bgbuf[i] == 6) {
    time += 3;
   }
   if (bgbuf[i] == 7) {
    scores[SCORE_MAX-2]++;
   }
   if (bgbuf[i] == 6 || bgbuf[i] == 7) {
    gotoxy(sx+i, sy);// 描画座標を指定
    setchar(0);// 空白を描画
   }
  }
 }
 // ミス中なら
 if (hero.act == HERO_ACT_MISS) {
  if (hero.cnt > 60) {// 60カウントを超えたら歩きにする
   hero.act = HERO_ACT_WALK;
   hero.cnt = 0;
   hero.mtk = 60;// 無敵時間の設定
  }
 }
 // 無敵カウンタをデクリメント
 if (hero.mtk > 0) hero.mtk--;
 // カウンタをインクリメント
 hero.cnt++;
}

// ペンギン表示
void hero_disp()
{
 UBYTE i;// ループ用
 UBYTE ani;// アニメ用
 // 無敵中は2回に1回表示する
 if ((hero.mtk & 0x01) == 0) {
  // 左右の2枚を表示
  for (i = 0; i < 2; i++) {
   // ペンギン表示
   ani = ani_tiles[(hero.act)] + (i*2) + ((hero.cnt/2) & 0x04);
   set_sprite_tile( SPR_HERO+i, ani);// スプライトを設定
   move_sprite( SPR_HERO+i, hero.x-8+(i*8), hero.y-16+jmptbl[(hero.jmp)]+(hero.act == HERO_ACT_MISS ? 3 :0));// スプライトの座標を設定
   //影表示
   if (hero.act < HERO_ACT_MISS) {
    set_sprite_tile( SPR_HERO_SHADOW+i, 2);// スプライトを設定
    move_sprite( SPR_HERO_SHADOW+i, hero.x-8+(i*8), hero.y-3);// スプライトの座標を設定
   } else {
    //表示座標を画面外にすることで非表示にしている
    move_sprite( SPR_HERO_SHADOW+i, 0, 0);// スプライトの座標を設定
   }
  }
  // 影の右側だけ左右反転
  set_sprite_prop( SPR_HERO_SHADOW+1, S_FLIPX);
 } else {
  // 表示座標を画面外にすることで非表示にしている
  for (i = 0; i < 2; i++) {
   move_sprite( SPR_HERO+i, 0, 0);// スプライトの座標を設定
   move_sprite( SPR_HERO_SHADOW+i, 0, 0);// スプライトの座標を設定
  }
 }
}

// BGに穴(または石ころ)を生成・描画する
void make_bghole(UBYTE y)
{
 UBYTE i, r, ana = 0;
 // 横32キャラ分を描画する
 for (i = 0; i < 32; i++) {
  r = rand();// 乱数取得
  gotoxy(i, y);// 描画座標を指定
  if (ana == 0) {
   if ((r & 0x70) == 0) {
    r &= 3;
    setchar(r);
    if (r == 3) ana = 1;// 3だったら穴モード
   } else setchar(0);// 空白を描画
  } else if (ana == 1) {// 穴モードだったら
   setchar(4);// 穴の真ん中
   if ((r & 1) == 0) ana = 2;// 乱数によっては穴モードを終了する
  } else {
   setchar(5);// 穴の右側
   ana = 0;// 穴モード終了
  }
 }
}

// BGにアイテムを生成・描画する
void make_bgitem(UBYTE y)
{
 UBYTE i, r;
 // 横32キャラ分を描画する
 for (i = 0; i < 32; i++) {
  gotoxy(i, y);// 描画座標を指定
  setchar(0);// 空白を描画
  if ((y & 0x0f) == 0x0f) {
   if (i == (r & 0x0f) + 4) setchar(6);//時計
  } else {
   r = rand();// 乱数取得
   if ((r & 0xf8) == 0) setchar(7);//コイン
  }
 }
}

// スコア表示
void score_disp()
{
 UBYTE i, dsp=0;
 //桁上げ
 for (i = 2; i <= SCORE_MAX-1; i++) {
  if (scores[SCORE_MAX-i] >= 10) {
   scores[SCORE_MAX-i] = 0;
   scores[SCORE_MAX-i-1]++;
  }
 }
 //表示
 for (i = 0; i < SCORE_MAX; i++) {
  if (scores[i] > 0) dsp = 1;
  if (i == SCORE_MAX - 1)  dsp = 1;
  if (dsp == 1) {
   set_sprite_tile( SPR_SCORE+i, 52 + scores[i]*2 );
   move_sprite( SPR_SCORE+i, 64+(i*8), 8);
  }
 }
}

// 時間表示
void time_disp()
{
 UBYTE i, timebd;
 if (timecnt-- == 0) {
  time--;
  timecnt = 60;
 }
 //表示
 set_sprite_tile( SPR_TIME, 72 );
 move_sprite( SPR_TIME, 136, 24);
 timebd = time;
 for (i = 1; i <= 2; i++) {
  set_sprite_tile( SPR_TIME+i, 52 + (timebd % 10)*2 );
  move_sprite( SPR_TIME+i, 160-(i*8), 24);
  timebd = (time /10);
 }
}

// 座標指定でPRINT
void printxy(char *cp, UBYTE x, UBYTE y) {
 while( *cp != NULL) {
  gotoxy(x, y);// 描画座標を指定
  setchar(*cp);// キャラ描画
  cp++;
  x++;
 }
}

//キー入力
UBYTE keypad()
{
 input_old = input_on;
 input_on = joypad();
 input_edge = input_on ^ input_old;
 input_trg = input_on & input_edge;
 return (input_trg);
}

// スプライト座標初期化
void sprite_init()
{
 UBYTE i;
 for (i = 0; i< SPR_MAX; i++) {
  set_sprite_tile( i, 0);
  move_sprite( i, 0, 0);
 }
}
print関数では横20タイルを超えると改行してしまいますので
setchar関数で描画する関数を用意しました。

waitpad関数が続くと連続で判定されてしまいますのでキー入力部分を関数化しました。

今回のゲームは処理が全体的に軽いので良いですが
時間表示部分で10で除算とかしているので多用すると処理が重くなると思われます。
(スコアは桁ごとにデータを持てるよう配列にしました)

完成したファイルはここからダウンロードできます。
お好きなように改造して遊んで下さい。

正直、大して面白くはありませんが、1日で作ったらこんなものでしょう。
お友達と一緒にスコアアタックとかやれば楽しめると思います。

簡単なゲームを作ってみよう(5)


ところで、前回のプログラムでは、同じBGの繰り返しになってしまいますので
意外性が全くありません。

今回は乱数を使ってBGデータを生成してみます。
ついでに簡単なタイトル画面を入れて、横スクロールするようにしてみました。

Test5d.c
#include <stdio.h>
#include <gb.h>
#include <rand.h>

// タイルデータ
#include "PenguinSpr.c"
#include "IceBkg.c"
// マップデータ
//#include "IceBkgMap.c"

#define SPR_HERO        0
#define SPR_HERO_SHADOW 2

#define HERO_ACT_NONE      0
#define HERO_ACT_WALK      1
#define HERO_ACT_JUMP      2
#define HERO_ACT_MISS      3

// プレイヤ用構造体
struct st_hero {
 UBYTE act; // アクションno
 UBYTE x;   // X座標
 UBYTE y;   // Y座標
 UBYTE cnt; // 汎用カウンタ
 UBYTE jmp; // ジャンプカウンタ
 UBYTE mtk; // 無敵カウンタ
} hero;

// アニメ用テーブル
UBYTE ani_tiles[]={0, 4, 12, 20};

// ジャンプ用テーブル
#define JMPTBL_MAX     31
BYTE jmptbl[]={
   0, -2, -4, -6, -8, -9,-10,-11,-12,-12,-13,-13,-14,-14,-15,-15,
 -15,-15,-14,-14,-13,-13,-12,-12,-11,-10, -9, -8, -6, -4, -2,  0,
 };

UBYTE scrx;// スクロール用X座標
UBYTE scry;// スクロール用Y座標
UBYTE scrcnty;// スクロール用カウンタ
UBYTE bgsety;// BG描画用Y座標
UBYTE input_on, input_old, input_edge, input_trg;// キー入力用
fixed seed;// 乱数シード

unsigned char bgbuf[2];// BGデータ取得用

void hero_ctrl();
void hero_disp();
void make_bghole(UBYTE y);
void title_disp();

void main()
{
 UBYTE i;// ループ用

 DISPLAY_OFF;// 画面全体を非表示
 HIDE_SPRITES;// スプライトを非表示
 HIDE_BKG;// BGを非表示

 SPRITES_8x16;// スプライトモードを8*16に設定
 set_sprite_data(0, 52, PenguinSpTileLabel); // タイルデータをスプライトとして設定(0番から52個)

 title_disp();// タイトル画面を描画

 SHOW_BKG;// BGを表示
 DISPLAY_ON;// 画面全体を表示

 seed.b.l = DIV_REG;

// while (!(joypad() & J_START));// スタートキーが押されるまで無限ループ
 waitpad(J_START);// スタートキーの入力待ち

 seed.b.h = DIV_REG;
 initrand(seed.w);// 乱数初期化

 DISPLAY_OFF;// 画面全体を非表示
 HIDE_SPRITES;// スプライトを非表示
 HIDE_BKG;// BGを非表示

 set_bkg_data(0, 6, IceBkgTileLabel); // タイルデータをBGとして設定
 //set_bkg_tiles(0, 0, 20, 32, IceBkgMapLabel); // マップデータを設定する

 cls();// 画面クリア

 // 初期のBGデータを生成
 for (bgsety = 0; bgsety < 19; bgsety+=2) {
  make_bghole(bgsety);
 }

 SHOW_BKG;// BGを表示
 SHOW_SPRITES;// スプライトを表示
 DISPLAY_ON;// 画面全体を表示

 // 初期値を設定
 hero.act = 1;
 hero.x = 88;
 hero.y = 64;
 hero.cnt = 0;
 hero.jmp = 0;
 hero.mtk = 60;
 scry = scrcnty = 0;

 while (1) {// 無限ループ
  //キー入力
  input_old = input_on;
  input_on = joypad();
  input_edge = input_on ^ input_old;
  input_trg = input_on & input_edge;
  
  if (hero.act != HERO_ACT_MISS) {
   if (++scrcnty > 1) {
    scrcnty = 0;
    scry++;
    // スクロール16ドット単位でBGデータを生成する
    if ((scry & 0x0f) == 0) {
     bgsety += 2;
     bgsety &= 0x1f;// 0~31にする
     make_bghole(bgsety);
    }
   }
  }
  move_bkg(scrx, scry);// BGの描画開始XY座標を設定
  
  hero_ctrl();// ペンギンコントロール
  hero_disp();// ペンギン表示
  
  delay(10);// 10ミリ秒待つ
 }
}

// タイトル表示
void title_disp()
{
 print(" ");// ダミー描画
 gotoxy(2, 5);
 print("JUNPING  PENGUIN");
 gotoxy(5, 13);
 print("PUSH START");
}

// ペンギンコントロール
void hero_ctrl()
{
 UBYTE sx, sy;// BG調査用

 // ミス中は移動できない
 if (hero.act != HERO_ACT_MISS) {
  // 右入力
  if ((input_on & J_RIGHT) && (hero.x <  160)) hero.x++;
  // 左入力
  if ((input_on & J_LEFT)  && (hero.x >   16)) hero.x--;
  // A入力
  if ((input_trg & J_A)    && (hero.act == HERO_ACT_WALK)) {
   hero.jmp = 1;// ジャンプ開始
   hero.act = HERO_ACT_JUMP;
   hero.cnt = 0;
  }
 }
 scrx = (hero.x >> 1);
 // ジャンプ中
 if (hero.jmp > 0) {
  hero.jmp++;
  if (hero.jmp >= JMPTBL_MAX) {
   hero.jmp = 0;// ジャンプ終了
   hero.act = HERO_ACT_WALK;
   hero.cnt = 0;
  }
  // 無敵中でなくてミス中でなければ当たり判定
 } else if (hero.mtk == 0 && hero.act != HERO_ACT_MISS) {
  sx = ((hero.x-8+scrx) >> 3);//
  sy = ((hero.y-16+scry) >> 3);
  get_bkg_tiles(sx,sy,1,1, bgbuf);// BGのデータを取得する
  if (bgbuf[0] >= 3 && bgbuf[0] <= 5) {
   hero.act = HERO_ACT_MISS;
   hero.cnt = 0;
  }
 }
 // ミス中なら
 if (hero.act == HERO_ACT_MISS) {
  if (hero.cnt > 60) {// 60カウントを超えたら歩きにする
   hero.act = HERO_ACT_WALK;
   hero.cnt = 0;
   hero.mtk = 60;// 無敵時間の設定
  }
 }
 // 無敵カウンタをデクリメント
 if (hero.mtk > 0) hero.mtk--;
 // カウンタをインクリメント
 hero.cnt++;
}

// ペンギン表示
void hero_disp()
{
 UBYTE i;// ループ用
 UBYTE ani;// アニメ用

 // 無敵中は2回に1回表示する
 if ((hero.mtk & 0x01) == 0) {
  // 左右の2枚を表示
  for (i = 0; i < 2; i++) {
   // ペンギン表示
   ani = ani_tiles[(hero.act)] + (i*2) + ((hero.cnt/2) & 0x04);
   set_sprite_tile( SPR_HERO+i, ani);// スプライトを設定
   move_sprite( SPR_HERO+i, hero.x-8+(i*8), hero.y-16+jmptbl[(hero.jmp)]+(hero.act == HERO_ACT_MISS ? 3 :0));// スプライトの座標を設定
   //影表示
   if (hero.act < HERO_ACT_MISS) {
    set_sprite_tile( SPR_HERO_SHADOW+i, 2);// スプライトを設定
    move_sprite( SPR_HERO_SHADOW+i, hero.x-8+(i*8), hero.y-3);// スプライトの座標を設定
   } else {
    //表示座標を画面外にすることで非表示にしている
    move_sprite( SPR_HERO_SHADOW+i, 0, 0);// スプライトの座標を設定
   }
  }
  // 影の右側だけ左右反転
  set_sprite_prop( SPR_HERO_SHADOW+1, S_FLIPX);
 } else {
  // 表示座標を画面外にすることで非表示にしている
  for (i = 0; i < 2; i++) {
   move_sprite( SPR_HERO+i, 0, 0);// スプライトの座標を設定
   move_sprite( SPR_HERO_SHADOW+i, 0, 0);// スプライトの座標を設定
  }
 }
}

// BGに穴(または石ころ)を生成・描画する
void make_bghole(UBYTE y)
{
 UBYTE i, r, ana = 0;

 // 横32キャラ分を描画する
 for (i = 0; i < 32; i++) {
  r = rand();// 乱数取得
  gotoxy(i, y);// 描画座標を指定
  if (ana == 0) {
   if ((r & 0x70) == 0) {
    r &= 3;
    setchar(r);
    if (r == 3) ana = 1;// 3だったら穴モード
   } else setchar(0);// 空白を描画
  } else if (ana == 1) {// 穴モードだったら
   setchar(4);// 穴の真ん中
   if ((r & 1) == 0) ana = 2;// 乱数によっては穴モードを終了する
  } else {
   setchar(5);// 穴の右側
   ana = 0;// 穴モード終了
  }
 }
}

随分と長いプログラムになってしまいました。

乱数を使うには、rand.hをインクルードします。
#include <rand.h>
そしてrand関数を呼べば0~255の値を返してくれますが
このままでは毎回同じパターンの乱数を返すため
乱数シードを使って乱数を初期化します。
fixed seed;// 乱数シード
この乱数シードを生成するために、タイトル画面で以下の事をやっています。
seed.b.l = DIV_REG;
waitpad(J_START);// スタートキーの入力待ち
seed.b.h = DIV_REG;
DIV_REGの値をseedの下位バイト・上位バイトへ代入しています。
DIV_REGは割込レジスタのようですが、詳細は分かりません。
waitpadで任意のキー入力待ちを行うことで、DIV_REGの値はプレイ毎に変化していく
ということです。
そしてinitrand関数に乱数シードを渡して乱数を初期化します。
initrand(seed.w);// 乱数初期化

文字列の表示にはprint関数を使用します。
GBDKでは、始めてprint関数(printfなども含む)またはsetchar関数を呼ぶと
BGのタイルデータへ自動的にASCII文字データをセットしてくれます。
(逆に言うと、それまでセットしておいたBGのタイルデータが上書きされる)

なので、タイトル画面の描画を行った後に「IceBkg.c」のタイルデータを上書きしています。

BGへの描画位置指定にはgotoxy関数を使用します。
BGの描画にはsetchar関数で描画したいタイルの番号を渡して呼びます。
setchar関数を呼んだ後、描画位置は変化しませんので
右にずらした値を設定して再度gotoxy関数を呼びます。
これで随分ゲームらしくなってきたような気がします。

次はスコアなどを入れてもっとゲームらしくしましょう。

簡単なゲームを作ってみよう(4)

穴との当たり判定を付けました。
なお、ペンギンは足元が原点となっています。

Test5c.c
#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "PenguinSpr.c"
#include "IceBkg.c"
// マップデータ
#include "IceBkgMap.c"

#define SPR_HERO        0
#define SPR_HERO_SHADOW 2

#define HERO_ACT_NONE      0
#define HERO_ACT_WALK      1
#define HERO_ACT_JUMP      2
#define HERO_ACT_MISS      3

//プレイヤ用構造体
struct st_hero {
 UBYTE act; //アクションno
 UBYTE x;   //X座標
 UBYTE y;   //Y座標
 UBYTE cnt; //汎用カウンタ
 UBYTE jmp; //ジャンプカウンタ
 UBYTE mtk; //無敵カウンタ
} hero;

//アニメ用テーブル
UBYTE ani_tiles[]={0, 4, 12, 20};

//ジャンプ用テーブル
#define JMPTBL_MAX     31
BYTE jmptbl[]={
   0, -2, -4, -6, -8, -9,-10,-11,-12,-12,-13,-13,-14,-14,-15,-15,
 -15,-15,-14,-14,-13,-13,-12,-12,-11,-10, -9, -8, -6, -4, -2,  0,
 };

UBYTE scry;//スクロール用Y座標
UBYTE scrcnty;//スクロール用カウンタ
UBYTE input_on, input_old, input_edge, input_trg;//キー入力用

unsigned char bgbuf[10];//BGデータ取得用(少し多めに確保)

void hero_ctrl();
void hero_disp();

void main()
{
 UBYTE i;//ループ用

 DISPLAY_OFF;// 画面全体を非表示
 HIDE_SPRITES;// スプライトを非表示
 HIDE_BKG;// BGを非表示

 SPRITES_8x16;// スプライトモードを8*16に設定
 set_sprite_data(0, 52, PenguinSpTileLabel); // タイルデータをスプライトとして設定(0番から52個)

 set_bkg_data(0, 6, IceBkgTileLabel); // タイルデータをBGとして設定
 set_bkg_tiles(0, 0, 20, 32, IceBkgMapLabel);   // マップデータを設定する

 SHOW_BKG;// BGを表示
 SHOW_SPRITES;// スプライトを表示
 DISPLAY_ON;// 画面全体を表示

 // 初期値を設定
 hero.act = 1;
 hero.x = 88;
 hero.y = 64;
 hero.cnt = 0;
 hero.jmp = 0;
 hero.mtk = 60;
 scry = scrcnty = 0;

 while (1) {// 無限ループ
  //キー入力
  input_old = input_on;
  input_on = joypad();
  input_edge = input_on ^ input_old;
  input_trg = input_on & input_edge;
  
  if (hero.act != HERO_ACT_MISS) {
   if (++scrcnty > 1) {
    scrcnty = 0;
    scry++;
   }
  }
  move_bkg(0, scry);// BGの描画開始XY座標を設定
  
  hero_ctrl();//ペンギンコントロール
  hero_disp();//ペンギン表示
  
  delay(10);//10ミリ秒待つ
 }
}

//ペンギンコントロール
void hero_ctrl()
{
 UBYTE sx, sy;//調査用

 //ミス中は移動できない
 if (hero.act != HERO_ACT_MISS) {
  // 右入力
  if ((input_on & J_RIGHT) && (hero.x <  160)) hero.x++;
  // 左入力
  if ((input_on & J_LEFT)  && (hero.x >   16)) hero.x--;
  // A入力
  if ((input_trg & J_A)    && (hero.act == HERO_ACT_WALK)) {
   hero.jmp = 1;//ジャンプ開始
   hero.act = HERO_ACT_JUMP;
   hero.cnt = 0;
  }
 }
 //ジャンプ中
 if (hero.jmp > 0) {
  hero.jmp++;
  if (hero.jmp >= JMPTBL_MAX) {
   hero.jmp = 0;//ジャンプ終了
   hero.act = HERO_ACT_WALK;
   hero.cnt = 0;
  }
  //無敵中でなくてミス中でなければ当たり判定
 } else if (hero.mtk == 0 && hero.act != HERO_ACT_MISS) {
  sx = ((hero.x-8) >> 3);//
  sy = ((hero.y-16+scry) >> 3);
  get_bkg_tiles(sx,sy,1,1, bgbuf);//BGのデータを取得する
  if (bgbuf[0] >= 3 && bgbuf[0] <= 5) {
   hero.act = HERO_ACT_MISS;
   hero.cnt = 0;
  }
 }
 if (hero.act == HERO_ACT_MISS) {
  if (hero.cnt > 30) {
   hero.act = HERO_ACT_WALK;
   hero.cnt = 0;
   hero.mtk = 60;
  }
 }
 //無敵カウンタをデクリメント
 if (hero.mtk > 0) hero.mtk--;
 //カウンタをインクリメント
 hero.cnt++;
}

//ペンギン表示
void hero_disp()
{
 UBYTE i;//ループ用
 UBYTE ani;//アニメ用

 //無敵中は2回に1回表示する
 if ((hero.mtk & 0x01) == 0) {
  //左右の2枚を表示
  for (i = 0; i < 2; i++) {
   //ペンギン表示
   ani = ani_tiles[(hero.act)] + (i*2) + ((hero.cnt/2) & 0x04);
   set_sprite_tile( SPR_HERO+i, ani);// スプライトを設定
   move_sprite( SPR_HERO+i, hero.x-8+(i*8), hero.y-16+jmptbl[(hero.jmp)]+(hero.act == HERO_ACT_MISS ? 4 :0));// スプライトの座標を設定
   //影表示
   if (hero.act < HERO_ACT_MISS) {
    set_sprite_tile( SPR_HERO_SHADOW+i, 2);// スプライトを設定
    move_sprite( SPR_HERO_SHADOW+i, hero.x-8+(i*8), hero.y-3);// スプライトの座標を設定
   } else {
    //表示座標を画面外にすることで非表示にしている
    move_sprite( SPR_HERO_SHADOW+i, 0, 0);// スプライトの座標を設定
   }
  }
  //影の右側だけ左右反転
  set_sprite_prop( SPR_HERO_SHADOW+1, S_FLIPX);
 } else {
  //表示座標を画面外にすることで非表示にしている
  for (i = 0; i < 2; i++) {
   move_sprite( SPR_HERO+i, 0, 0);// スプライトの座標を設定
   move_sprite( SPR_HERO_SHADOW+i, 0, 0);// スプライトの座標を設定
  }
 }
}

以下のようにget_bkg_tilesを用いて足元にあるBGのタイルの番号を取得します。
get_bkg_tiles(sx, sy, 1, 1, bgbuf);//BGのデータを取得する

歩いている時だけ当たり判定を行うようにしました。
また、穴にハマってから再び歩き始める際に無敵時間を設けました。

簡単なゲームを作ってみよう(3)

では、前回作ったBGを組み合わせてみましょう。

BGの画像ファイルはここからダウンロードできます。※少し絵を追加しています。
BGのマップファイルはここからダウンロードできます。※前回と同じです。

Test5b.c
#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "PenguinSpr.c"
#include "IceBkg.c"
// マップデータ
#include "IceBkgMap.c"

#define SPR_HERO        0
#define SPR_HERO_SHADOW 2

#define HERO_ACT_NONE      0
#define HERO_ACT_WALK      1
#define HERO_ACT_JUMP      2
#define HERO_ACT_MISS      3

//プレイヤ用構造体
struct st_hero {
 UBYTE act; //アクションno
 UBYTE x;   //X座標
 UBYTE y;   //Y座標
 UBYTE cnt; //汎用カウンタ
 UBYTE jmp; //ジャンプカウンタ
} hero;

//アニメ用テーブル
UBYTE ani_tiles[]={0, 4, 12, 20};

//ジャンプ用テーブル
#define JMPTBL_MAX     31
BYTE jmptbl[]={
   0, -2, -4, -6, -8, -9,-10,-11,-12,-12,-13,-13,-14,-14,-15,-15,
 -15,-15,-14,-14,-13,-13,-12,-12,-11,-10, -9, -8, -6, -4, -2,  0,
 };

UBYTE scry;//スクロール用Y座標
UBYTE scrcnty;//スクロール用カウンタ
UBYTE input_on, input_old, input_edge, input_trg;//キー入力用

void hero_ctrl();
void hero_disp();

void main()
{
 UBYTE i;//ループ用

 DISPLAY_OFF;// 画面全体を非表示
 HIDE_SPRITES;// スプライトを非表示
 HIDE_BKG;// BGを非表示

 SPRITES_8x16;// スプライトモードを8*16に設定
 set_sprite_data(0, 52, PenguinSpTileLabel); // タイルデータをスプライトとして設定(0番から52個)

 set_bkg_data(0, 6, IceBkgTileLabel); // タイルデータをBGとして設定
 set_bkg_tiles(0, 0, 20, 32, IceBkgMapLabel);   // マップデータを設定する

 SHOW_BKG;// BGを表示
 SHOW_SPRITES;// スプライトを表示
 DISPLAY_ON;// 画面全体を表示

 // 初期値を設定
 hero.act = 1;
 hero.x = 88;
 hero.y = 64;
 hero.cnt = 0;
 hero.jmp = 0;
 scry = scrcnty = 0;

 while (1) {// 無限ループ
  //キー入力
  input_old = input_on;
  input_on = joypad();
  input_edge = input_on ^ input_old;
  input_trg = input_on & input_edge;
  
  if (++scrcnty > 1) {
   scrcnty = 0;
   scry++;
  }
  move_bkg(0, scry);// BGの描画開始XY座標を設定
  
  hero_ctrl();//ペンギンコントロール
  hero_disp();//ペンギン表示
  
  delay(10);//10ミリ秒待つ
 }
}

//ペンギンコントロール
void hero_ctrl()
{
 // 右入力
 if (input_on & J_RIGHT && hero.x <  160) hero.x++;
 // 左入力
 if (input_on & J_LEFT  && hero.x >   16) hero.x--;
 // A入力
 if (input_trg & J_A     && hero.jmp == 0 && hero.act == HERO_ACT_WALK) {
  hero.jmp = 1;//ジャンプ開始
  hero.act = HERO_ACT_JUMP;
  hero.cnt = 0;
 }
 //ジャンプ中
 if (hero.jmp > 0) {
  hero.jmp++;
  if (hero.jmp >= JMPTBL_MAX) {
   hero.jmp = 0;//ジャンプ終了
   hero.act = HERO_ACT_WALK;
   hero.cnt = 0;
  }
 }
}

//ペンギン表示
void hero_disp()
{
 UBYTE i;//ループ用
 UBYTE ani;//アニメ用

 //左右の2枚を表示
 for (i = 0; i < 2; i++) {
  //ペンギン表示
  ani = ani_tiles[(hero.act)] + (i*2) + ((hero.cnt/2) & 0x04);
  set_sprite_tile( SPR_HERO+i, ani);// スプライトを設定
  move_sprite( SPR_HERO+i, hero.x-8+(i*8), hero.y-16+jmptbl[(hero.jmp)]);// スプライトの座標を設定
  //影表示
  if (hero.act < HERO_ACT_MISS) {
   set_sprite_tile( SPR_HERO_SHADOW+i, 2);// スプライトを設定
   move_sprite( SPR_HERO_SHADOW+i, hero.x-8+(i*8), hero.y-3);// スプライトの座標を設定
  } else {
   //表示座標を画面外にすることで非表示にしている
   move_sprite( SPR_HERO_SHADOW+i, 0, 0);// スプライトの座標を設定
  }
 }
 //影の右側だけ左右反転
 set_sprite_prop( SPR_HERO_SHADOW+1, S_FLIPX);
 //カウンタをインクリメント
 hero.cnt++;
}

まだ、地面の穴との当たり判定が入っていませんので、次回やります。

簡単なゲームを作ってみよう(2)

ところで、前回のプログラムでは
Aボタンを押しっぱなしにするとジャンプを繰り返します。

そのため、以下のように部分的に修正します。

Test5a.c
UBYTE input_on, input_old, input_edge, input_trg;//キー入力用
//キー入力
input_old = input_on;
input_on = joypad();
input_edge = input_on ^ input_old;
input_trg = input_on & input_edge;
// A入力
if (input_trg & J_A     && hero.jmp == 0 && hero.act == HERO_ACT_WALK) {

input_onは押されているキーのビットがONになっています。
input_trgは押された時のみキーのビットがONになりますので
ジャンプ時のAボタン押下判定に使用します。

これでジャンプし続けることはなくなります。

簡単なゲームを作ってみよう(1)

簡単なゲームを作ってみましょう。

まずはスプライトの画像を用意します。
なんとなくペンギンの絵を描いてみました。

ファミリーベーシックのペンペンに似てますが
微妙に違いますので気にしないで下さい。

画像のファイルはここからダウンロードできます。

Test5.c
#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "PenguinSpr.c"

#define SPR_HERO        0
#define SPR_HERO_SHADOW 2

#define HERO_ACT_NONE      0
#define HERO_ACT_WALK      1
#define HERO_ACT_JUMP      2
#define HERO_ACT_MISS      3

//プレイヤ用構造体
struct st_hero {
 UBYTE act; //アクションno
 UBYTE x;   //X座標
 UBYTE y;   //Y座標
 UBYTE cnt; //汎用カウンタ
 UBYTE jmp; //ジャンプカウンタ
} hero;

//アニメ用テーブル
UBYTE ani_tiles[]={0, 4, 12, 20};

//ジャンプ用テーブル
#define JMPTBL_MAX     31
BYTE jmptbl[]={
   0, -2, -4, -6, -8, -9,-10,-11,-12,-12,-13,-13,-14,-14,-15,-15,
 -15,-15,-14,-14,-13,-13,-12,-12,-11,-10, -9, -8, -6, -4, -2,  0,
 };

UBYTE input;//キー入力用

void hero_ctrl();
void hero_disp();

void main()
{
 UBYTE i;//ループ用

 DISPLAY_OFF;// 画面全体を非表示
 HIDE_SPRITES;// スプライトを非表示

 SPRITES_8x16;// スプライトモードを8*16に設定
 set_sprite_data(0, 52, PenguinSpTileLabel); // タイルデータをスプライトとして設定(0番から52個)


 SHOW_SPRITES;// スプライトを表示
 DISPLAY_ON;// 画面全体を表示

 // 初期値を設定
 hero.act = 1;
 hero.x = 88;
 hero.y = 64;
 hero.cnt = 0;
 hero.jmp = 0;
 scry = 0;

 while (1) {// 無限ループ
  input = joypad();// キー入力
  
  hero_ctrl();//ペンギンコントロール
  hero_disp();//ペンギン表示
  
  delay(10);//10ミリ秒待つ
 }
}

//ペンギンコントロール
void hero_ctrl()
{
 // 右入力
 if (input & J_RIGHT && hero.x <  160) hero.x++;
 // 左入力
 if (input & J_LEFT  && hero.x >   16) hero.x--;
 // A入力
 if (input & J_A     && hero.jmp == 0 && hero.act == HERO_ACT_WALK) {
  hero.jmp = 1;//ジャンプ開始
  hero.act = HERO_ACT_JUMP;
  hero.cnt = 0;
 }
 //ジャンプ中
 if (hero.jmp > 0) {
  hero.jmp++;
  if (hero.jmp >= JMPTBL_MAX) {
   hero.jmp = 0;//ジャンプ終了
   hero.act = HERO_ACT_WALK;
   hero.cnt = 0;
  }
 }
}

//ペンギン表示
void hero_disp()
{
 UBYTE i;//ループ用
 UBYTE ani;//アニメ用

 //左右の2枚を表示
 for (i = 0; i < 2; i++) {
  //ペンギン表示
  ani = ani_tiles[(hero.act)] + (i*2) + ((hero.cnt/2) & 0x04);
  set_sprite_tile( SPR_HERO+i, ani);// スプライトを設定
  move_sprite( SPR_HERO+i, hero.x-8+(i*8), hero.y-16+jmptbl[(hero.jmp)]);// スプライトの座標を設定
  //影表示
  if (hero.act < HERO_ACT_MISS) {
   set_sprite_tile( SPR_HERO_SHADOW+i, 2);// スプライトを設定
   move_sprite( SPR_HERO_SHADOW+i, hero.x-8+(i*8), hero.y-3);// スプライトの座標を設定
  } else {
   //表示座標を画面外にすることで非表示にしている
   move_sprite( SPR_HERO_SHADOW+i, 0, 0);// スプライトの座標を設定
  }
 }
 //影の右側だけ左右反転
 set_sprite_prop( SPR_HERO_SHADOW+1, S_FLIPX);
 //カウンタをインクリメント
 hero.cnt++;
}

これでペンギンが左右に動き、Aボタンでジャンプします。
変数の一部をグローバル変数にして、ペンギン関連の変数は構造体にしました。
影の左側はset_sprite_prop関数で左右反転を指定しています。

C言語においてグローバル変数はあまり使うべきではないと言われたりしますが
メモリ容量の少ないゲームボーイでは、全体的に使用される変数は
グローバル変数にして使いまわしたほうがメモリ効率が良くなると考えられます。

2013/06/17

BGを表示してみよう(3)

では、背景を動かしてみましょう。

Test4a.c
#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "IceBkg.c"
// マップデータ
#include "IceBkgMap.c"

void main()
{
 UBYTE scrx = 0;//スクロール用X座標
 UBYTE scry = 0;//スクロール用Y座標

 DISPLAY_OFF;// 画面全体を非表示
 HIDE_BKG;// BGを非表示

 set_bkg_data(0, 6, IceBkgTileLabel); // タイルデータをBGとして設定
 set_bkg_tiles(0, 0, 20, 32, IceBkgMapLabel);   // マップデータを設定する

 SHOW_BKG;// BGを表示
 DISPLAY_ON;// 画面全体を表示

 while (1) {// 無限ループ
  move_bkg(scrx, scry);//BGの描画開始XY座標を設定
 // SCX_REG = scrx;//スクロールXレジスタへ直接設定
 // SCY_REG = scry;//スクロールYレジスタへ直接設定
 // scroll_bkg(0,1);//相対的にスクロール位置を変えたい場合
  scry++; //描画開始Y座標を下へ動かすことでBGは下から上へ動く
    //UBYTEなので255の次は0になる
  delay(10);//10ミリ秒待つ
 }
}

いつものようにTest4a.cをコンパイルし、エミュレータで実行して下さい。
前回の「Test4.c」へ追記しても良いでしょう。

実行すると、画面が下から上へスクロールします。

move_bkg(scrx, scry);//BGの描画開始XY座標を設定
このようにmove_bkg関数へ座標を渡す方法もありますし
SCX_REG = scrx;//スクロールXレジスタへ直接設定
SCY_REG = scry;//スクロールYレジスタへ直接設定
このようにスクロールレジスタへ直接座標を代入する方法もあります。
また、
scroll_bkg(0,1);//相対的にスクロール位置を変えたい場合
scroll_bkg関数を呼んで現在の値から相対的に動かす事もできます。

BGを表示してみよう(2)

では、先ほどのタイルデータとマップデータをプログラムに組み込みます。

その前に、プログラムのソースファイルを作ります。
今回は「Test4.c」としましょう。
先ほど生成された「IceBkg.c」「IceBkgMap.c」も同じ場所に置いて下さい。

Test4.c
#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "IceBkg.c"
// マップデータ
#include "IceBkgMap.c"

void main()
{
 DISPLAY_OFF;// 画面全体を非表示
 HIDE_BKG;// BGを非表示

 set_bkg_data(0, 6, IceBkgTileLabel); // タイルデータをBGとして設定

 set_bkg_tiles(0, 0, 20, 32, IceBkgMapLabel);   // マップデータを設定する

 SHOW_BKG;// BGを表示
 DISPLAY_ON;// 画面全体を表示
}

やっている事はコメントの通りです。
いつものようにTest4.cをコンパイルし、エミュレータで実行して下さい。


こんな画面が表示されました。
GBMBで作成したマップデータと同じように表示されています。

マップデータの設定はここで行なっています。
set_bkg_tiles(0, 0, 20, 32, IceBkgMapLabel);   // マップデータを設定する
ゲームボーイのBG面は画面外を含めて256×256ドットありますので
8×8ドットのタイル数では32×32タイルとなります。
ゲームボーイの画面サイズは160×144ドットですので
8×8ドットのタイル数では20×19タイルとなります。

set_bkg_tilesの引数は(セット開始X座標、セット開始Y座標、セット幅、セット高さ)※単位はタイル数
となります。この場合は、横には画面いっぱい、縦には画面外全体へセットしています。

BGを表示してみよう(1)

スプライト同様、BG(背景)もGBTDで描きます。
こんな感じに何枚か描きました。

前回のスプライトと同じようにエクスポートします。
File - Export to で設定画面を開きます。
Type項目のプルダウンから「GBDK C file (*.c)」を選択します。
Filename項目は「IceBkg.c」とします。
Label項目は「IceBkgTileLabel」とします。
From項目は0のまま、To項目は5にします。

上記の設定でOKを押します。
すると、「IceBkg.c」「IceBkgh」の2ファイルが生成されます。

File - Save As で名前を付けて保存するのもお忘れなく。今回は「IceBkg」とします。
ここからダウンロードできます。

続いて、GBMBを起動します。
GBMBのzipファイルをダウンロードして、適当なフォルダに展開して下さい。

その中に同梱されているGBMB.EXEを実行して下さい。
起動直後ではグラフィックのデータを読み込んでいませんので
File - Map properties を選択します。
すると、以下のような設定画面が表示されますので
今回はSize項目のWidth項目は20、Height項目は32とします。
Tileset項目では「Browse」ボタンを押して先ほど保存したIceBkg.gbr」を選択します。

これがマップ編集画面です。
右側に並んでいるタイルを左クリックして選択し
左側のマップ画面へ右クリックで配置していきます。
左ドラッグで範囲選択し、Ctrl+Cでコピー、Ctrl+Vでペーストができます。

マップ編集が終わったら、マップデータをプログラムで読み込めるようにします。
File - Export to で設定画面を開きます。
まずはStandardタブでファイル名やタイプ、ラベル名の設定をします。
Type項目のプルダウンから「GBDK C file (*.c)」を選択します。
Filename項目は「IceBkgMap.c」とします。
Label項目は「IceBkgMapLabel」とします。
続いて、Location formatタブを選択します。
左側のPropertyの下にあるプルダウンより「Tile number」を選択します。
右側のPlane countの右にあるプルダウンより「1 plane [8 bits]」を選択します。
設定が終わったらOKボタンを押して下さい。
IceBkgMap.c」と「IceBkgMap.h」が生成されました。

最後に、マップデータを保存しておきましょう。
File - Save As で名前を付けて保存します。ファイル名は「IceBkgMap」とします。
すると拡張子「gbm」のファイルが出来上がります。
このgbmファイルはGBMB用ファイルとなります。
ここからダウンロードできます。

では、プログラムに組み込みます。

2013/06/14

キー入力

ゲームボーイには十字キーとAボタン、Bボタン、セレクトボタン、スタートボタンがあります。
これらのキー入力でスプライトを動かしてみましょう。

Test3.c
#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "testtile.c"

void main()
{
 UBYTE i;//ループ用
 UBYTE x,y;//x,y座標用
 UBYTE input;//キー入力用

 DISPLAY_OFF;// 画面全体を非表示
 HIDE_SPRITES;// スプライトを非表示

 SPRITES_8x8;// スプライトモードを8*8に設定
 set_sprite_data(0, 4, TestTileLabel); // タイルデータをスプライトとして設定(0番から4つ)

 set_sprite_tile( 0, 0);// スプライト0番にタイル0番を設定

 SHOW_SPRITES;// スプライトを表示
 DISPLAY_ON;// 画面全体を表示

 // 初期座標を設定
 x = 80;
 y = 72;

 while (1) {// 無限ループ
  input = joypad();// キー入力
  if (input & J_RIGHT) x++;// 右入力
  if (input & J_LEFT) x--;// 左入力
  if (input & J_DOWN) y++;// 下入力
  if (input & J_UP) y--;// 上入力
  move_sprite( 0, x, y);// スプライト0番の座標を設定
  
  delay(10);//10ミリ秒待つ
 }
}
これを、前回同様にコンパイルしてエミュレータで実行します。
これだけでは分からないと思いますが、十字キーを押すと、その方向へ0が動きます。

input = joypad();// キー入力
joypad()関数でキー入力を取得します。
すると、キーに対応したビットがONになりますので
if文でAND(論理積)をかけて判定します。

ゲームボーイはまだ終わってない!から抜粋させていただきますと、以下の通りとなります。

ボタン種類と定数の対応関係
ボタン種類  ボタン定数 値
 START J_START 0x80U
 SELECT J_SELECT 0x40U
 B J_B 0x20U
 A J_A 0x10U
 下 J_DOWN 0x08U
 上 J_UP 0x04U
 左 J_LEFT 0x02U
 右 J_RIGHT 0x01U

そして、main関数の中ではwhile(1)で無限ループさせています。
ループの最後にあるdelay(10)でウェイトをかけています。
この値が大きくなるほど、処理の間隔が開きますので、移動スピードが遅くなります。
ちなみに、delay()の引数の単位はミリ秒です。

スプライトを表示してみよう(3)

前回はスプライトを1枚だけ表示しましたので、今度はもっと多く表示しましょう。

Test2.c


#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "testtile.c"

void main()
{
 UBYTE i;//ループ用

 DISPLAY_OFF;// 画面全体を非表示
 HIDE_SPRITES;// スプライトを非表示

 SPRITES_8x8;// スプライトモードを8*8に設定
 set_sprite_data(0, 4, TestTileLabel); // タイルデータをスプライトとして設定(0番から4つ)

 for (i = 0; i < 8; i++) {
  set_sprite_tile( i, (i / 2));// スプライトi番にタイル(i/2)番を設定
  move_sprite( i, ((i % 2) * 8) + 80, ((i / 2) * 16) + 64);//スプライトi番の座標を適当に設定
 }

 SHOW_SPRITES;// スプライトを表示
 DISPLAY_ON;// 画面全体を表示
}

これをコンパイルします。


lcc -o Test2.gb Test2.c

実行すると、このように表示されます。
8枚のスプライトが表示されています。
スプライト0・1番は「0」、スプライト2・3番は「1」、スプライト4・5番は「2」、スプライト6・7番は「3」を
設定しています。

ちなみに
UBYTE i;//ループ用
このiは変数といいます。iの中には数値が入ります。
ゲームボーイは8ビットCPUですので、変数の値は基本的に8ビットです。

UBYTEは変数の型です。

BYTE型は符号ありの8ビットで、-128~127が入りますが
UBYTE型は符号なしの8ビットで、0から255が入ります。
どちらも値を超えたら戻ります。
(BYTEで128になったら-128になり、UBYTEで256になったら0になります)

8ビットの変数を内部的に2つ使うことで、扱える数値の範囲を増やすことができます。
WORD型は符号ありの16ビットで、-32768~32767が入りますが
UWORD型は符号なしの16ビットで、0から65535が入ります。

C言語におけるint型はCPUのビット数によって変わりますので、ゲームボーイでは
BYTE型と同じですが、int型を使わず明示的にBYTE型と型宣言しましょう。

スプライトを表示してみよう(2)

では、先ほどのタイルデータをプログラムに組み込みます。

その前に、プログラムのソースファイルを作ります。
今回は「Test1.c」としましょう。
先ほど生成された「Testtile.c」も同じ場所に置いて下さい。

Test1.c
#include <stdio.h>
#include <gb.h>

// タイルデータ
#include "testtile.c"

void main()
{
 DISPLAY_OFF;// 画面全体を非表示
 HIDE_SPRITES;// スプライトを非表示 

 SPRITES_8x8;// スプライトモードを8*8に設定
 set_sprite_data(0, 4, TestTileLabel); // タイルデータをスプライトとして設定(0番から4つ)

 set_sprite_tile( 0, 0);// スプライト0番にタイル0番を設定
 move_sprite( 0, 0, 0);//スプライト0番の座標を(0,0)に設定

 SHOW_SPRITES;// スプライトを表示 
 DISPLAY_ON;// 画面全体を表示
}

やっている事はコメントに書いてある通りです。
では、コンパイルしてみましょう。

コマンドプロンプトを開いて、以下のように入力します。
lcc -o Test1.gb Test1.c
すると「Test1.gb」が生成されますので、エミュレータで起動します。

しかし、何も表示されません。
表示座標をいじってみたところ
move_sprite( 0, 8, 16);//スプライト0番の座標を(8,16)に設定
これで左上に表示されました。
ちなみに、
SPRITES_8x16;// スプライトモードを8*16に設定
にすると、以下のようになります。
タイル0番と1番が上下に繋がって表示されます。

スプライトは画面の左上の座標が(8, 16)ということを覚えておきましょう。

スプライトを表示してみよう(1)

スプライトを表示するために、まずは絵を描きます。
GBTDのzipファイルをダウンロードして、適当なフォルダに展開して下さい。

その中に同梱されているGBTD.EXEを実行して下さい。
8×8ドットが1つのキャラクタの単位のようです。
※タイルと呼んでいるらしいので、同じようにタイルと呼ぶことにします。

とりあえず、テスト用に最初の4タイルに「0,1,2,3」と描いてみます。
せっかく描いたので保存しましょう。

File - Save As で名前を付けて保存します。今回は「Testtile」とします。
すると拡張子「gbr」のファイルが出来上がります。
このgbrファイルはGBTD用ファイルとなります。

ここからダウンロードできます

更に、このタイルデータをプログラムで読み込めるようにします。

File - Export to で設定画面を開きます。
Standardタブのまま
Type項目のプルダウンから「GBDK C file (*.c)」を選択します。
Filename項目は「Testtile.c」とします。
Label項目は「TestTileLabel」とします。
From項目は0のまま、To項目は3にします。

上記の設定でOKを押します。
すると、「Testtile.c」「Testtile.h」の2ファイルが生成されます。

GBTDを閉じる時に、保存するかどうかのダイアログが表示されますので保存して下さい。

では、プログラムに組み込みます。

画面について

ゲームボーイで表示できるものには
・BG面(BackGround・背景)
・スプライト面
・ウインドウ面
があります。

BG面

RPGなんかでいうところの、いわゆるフィールドやお城のような背景がBGです。
大抵は同じようなパターンの絵が敷き詰められて表示されていると思います。
そして、画面全体を上下左右に動かす(スクロールさせる)ことができます。
ゲームボーイは、このBG面を1つ持っています。
ゲームボーイの画面解像度は160×144ドットですが、BG面は画面外を含めて256×256ドットの広さがあります。
先ほどのHelloWorldはBG面に描画しています。

スプライト面

RPGなんかでいうところの、いわゆる主人公や街の人のような
アニメーションしながら細かく動いているキャラクターがスプライトです。
ゲームボーイはスプライトを最大で40枚表示できます。
1枚の大きさは8×8ドットまたは8×16ドットのどちらかを選択できます。
そしてスプライトは横に何枚か並べて表示すると、それ以上は消えてしまいます。
何枚で消えるのかは分かりませんが、ファミコンと同じだとすると8枚でしょうか。
スプライト面も画面外を含めて256×256ドットの広さがあります。

ウインドウ面

BG面の上に表示できる、もう1つのBG面みたいなものですが
BGを透過させることが出来ないので、下か右にスクロールさせて
BGが見えるようにしなければなりません。
画面下や右のステータス表示などに使えます。
(最初は名前からRPGなどのウインドウを想像していましたが違いました)

では、スプライトを表示してみましょう。

2013/06/13

定番のHelloWorld

ゲームボーイはまだ終わってない!にもありますが、定番のHelloWorldを作成します。

テキストエディタでHelloWorld.cを作成します。
ファイルの中身は以下の通りです。

HelloWorld.c
#include<stdio.h>

void main()
{
    printf("Hello World!");
}

HelloWorld.cファイルはSDKの中に置きましょう。
C:\SDK\gbz80-gb\2-1-5 配下にsrcフォルダを作成して、そこに置きます。
コマンドプロンプトをC:\SDK\gbz80-gb\2-1-5\src で開いたら
lcc -o HelloWorld.gb HelloWorld.c
と入力します。
問題なければ、HelloWorld.gbというファイルが作成されます。
この拡張子「gb」のファイルがゲームボーイ用ファイルです。
では、ゲームボーイのエミュレータで開いて下さい。
※エミュレータを開いてからgbファイルをドラッグすれば開けます。

すると、「Hello World!」という文字が表示されたかと思います。
これで完成です。

ここまではゲームボーイはまだ終わってない!と同じ流れでしたが、あくまで備忘録ということでお許し下さい。

まずはSDKのインストール

まずはSDKをインストールします。
※SDKとはソフトウェア開発キットの事で、今回の場合はGBDKを指します。

ゲームボーイはまだ終わってない!を参考に、GBDKのバージョン2.1.5をダウンロードします。

念のためダウンロードURLも記載しておきます。
http://sourceforge.net/projects/gbdk/files/gbdk/2.1.5/sdk-gbz80-gb-2.1.5.zip/download
こちらへアクセスすると、自動的にダウンロードが始まります。

ついでにHarry Mulder's Gameboy Developmentで公開されている
以下の関連ツールもダウンロードしておきます。
GBTD (Gameboy Tile Designer)
GBMB (Gameboy Map Builder)
※これらは後で使います。

では、GBDKのインストールに戻ります。

先ほどダウンロードしたzipファイルを展開するだけなのですが、ゲームボーイはまだ終わってない!にあるように、フォルダパスによってはうまく動作しないとの事ですので、Cドライブ直下にzipファイル内のSDKフォルダ以下をコピーします。

そして、ゲームボーイはまだ終わってない!にあるように、ライブラリの構築とやらが必要との事ですので、その通りにやります。

コマンドプロンプトを実行するのですが、私のPCはWindows7ですので、
スタートメニュー - アクセサリ - コマンドプロンプト
または、スタートメニュー内の「プログラムとファイルの検索」に「cmd」と入力します。
※Windowsの設定次第では、コマンドプロンプトを開きたいフォルダ上でシフトキーを押しながら右クリックすると右クリックメニューに「コマンドウィンドウをここで開く」が出るようになります。楽なのでオススメです。

開いたコマンドプロンプトのウインドウで
cd \SDK\gbz80-gb\2-1-5\lib
と入力してから
make.bat
と入力します。

インストールはこれで終わりですが、今後のためにコンパイラへのパスを通しておきます。

スタートメニュー - コンピュータ(右クリックメニュー) - プロパティ を開く
システムの詳細設定 - 環境変数 を開く
システム環境変数の「Path」を選択して「編集」を開いてから、
変数値の最後に「;C:\SDK\gbz80-gb\2-1-5\bin」を追加しOKを押します。
※最初のセミコロン「;」を忘れずに…。

もしかしたら、Windowsを再起動しないと反映されないかもしれません。

最後に、PC上で動作確認をするためエミュレータをダウンロードします。

私は以下の2つを使用しています。
・BGB
・TGB Dual
それぞれ検索すれば出てきますので、必要な方は適当にインストールして下さい。

では、いよいよプログラムを作りましょう。

はじめに

ふと、ゲームボーイのソフト開発をやってみたくなりました。

そこで、「ゲームボーイ 開発」で検索してみたところ、ゲームボーイはまだ終わってない!が一番目にヒットしましたので、こちらを読ませていただいたところ、GBDK(Gameboy Developers Kit)というものを使ってC言語で開発できるようです。

更に、「GBDK」で検索してみると、確かにGBDKによる開発関連のページがヒットするものの、肝心な情報を参照しようとすると、ほとんどのページはリンク切れになっています。

どうやら、2000年頃にはGBDKでの開発が流行っていたらしく、情報も豊富だったようですが、それから10数年経った今となっては、多くの情報が失われつつあります。

まさに「今さら」です。

こんな情報が少ない現状ですので、色々と調べながらの開発になると思いますが、折角なので備忘録を兼ねてブログに書いていきたいと思っています。

プログラム作成にはZ80アセンブラも検討しましたが、あまりに情報が少ない現状ですので、とりあえずはC言語でいくことしました。

そのため、ある程度のC言語の知識をお持ちの方が対象となります。
予めご了承ください。

また、調べながらの状態ですので、間違った情報を載せている可能性もあります。
その時はご指摘いただければと思います。