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ブランク割込とは、画面の描画タイミングに合わせて発生する
垂直同期割込とか言われるやつです。興味のある方はググって下さい。

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

0 件のコメント:

コメントを投稿