2013/06/18

簡単なゲームを作ってみよう(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日で作ったらこんなものでしょう。
お友達と一緒にスコアアタックとかやれば楽しめると思います。

2 件のコメント:

  1. こんにちは。
    ゲームボーイ開発を調べていたらこのブログにたどり着きました。
    質問ですが、音の出力の方法をご存知でしたら教えて頂けたら幸いです。

    返信削除
  2. 音については、以下を参考にされてはいかがでしょうか?
    http://www.geocities.jp/submarine600/html/sounddriver.html

    私は他のフリーのサウンドドライバを使わせていただきましたが
    残念ながら現在はページが無くなっているようです。

    返信削除