まだプログラマーですが何か?

プログラマーネタ中心。たまに作成したウェブサービス関連の話も https://twitter.com/dotnsf

2023/06

過去5回にわたり、準備段階から作り続けた GBDK によるオリジナルゲームネタブログ、前回の「モールモール」完成で幕を閉じるつもりが、まさかの延長戦です。

前回までの内容はこちらです:
GBDK でゲームボーイのオリジナルゲームを作る(0)
GBDK でゲームボーイのオリジナルゲームを作る(1)
GBDK でゲームボーイのオリジナルゲームを作る(2)
GBDK でゲームボーイのオリジナルゲームを作る(3)
GBDK でゲームボーイのオリジナルゲームを作る(4)

前回のネタでは GBDK というゲームボーイ開発キットを使って「モールモール」という一人用パズルゲームを作り、エミュレータを使って実際に動かす、という内容を紹介しました。

今回は調子に乗って「ゲームボーイ実機で動かす」ことに挑戦しました。ゲームボーイは任天堂から 1989 年(平成元年!)に発売が開始された乾電池で動く小型ゲーム機です。当初は白黒画面でしたが後に改良版のゲームボーイ・カラーが発売され、ゲームもカラー対応されていきました。 また2001年には後継であるゲームボーイアドバンスが発売されましたが「ゲームボーイのゲームも動く」という互換性が確保されたことで長い期間にわたって親しまれたゲーム機となりました。なおゲームボーイは 2007 年、ゲームボーイアドバンスは 2012 年に生産・販売サポートが終了しています。もう10年以上前なんですね。ちなみに自分は白黒ゲームボーイをかなり早い段階で入手して遊んでいました。


【準備(要購入)】
当然のように本体は持ってなかったので中古で購入しました。今回の目的だけであれば初代の白黒ゲームボーイでも充分なのですが、「色々思い出してるうちにアドバンスのゲームもやりたくなるかも・・」などと思い出して、結局最終モデルであるゲームボーイアドバンス SP を購入しました(以下で紹介する内容を行う上でアドバンスである必要はなく、普通のゲームボーイ実機でも構いません):



単にゲームを遊ぶだけであれば、この本体とゲームカートリッジがあればいいのですが、自作ゲームを本体実機で遊ぶにはもう少し準備が必要です。エミュレータではなく本体で遊ぶには自作ゲームを書き込んだゲームカートリッジが必要です。自作ゲームは前回作成した molemole.gb を使うものとして(自分でゲームを作った人はそれを使っていただくのが楽しいと思います)、これを書き込む先の(空の)ゲームカートリッジが必要です。自分はこの GB SMART CARD USB を注文しました:



また PC からこの GB SMART CARD USB にゲームを書き込む際には USB miniB という今となっては懐かしい規格の USB ケーブルが必要です。100 円ショップなどではほとんど見かけなくなっていて、必要であればこれも買っておく必要があります:



おまけに、自分はゲームボーイアドバンス(SP)も乾電池駆動できると勘違いしていたのですが、アドバンスは充電式のみの対応だったんですね。というわけでゲームボーイアドバンス用の充電ケーブルも必要です。これも特殊な形状のケーブルなので、必要であれば購入しておく必要があります:



事前に購入しておく必要があるのはこんな所です。次にゲーム書き込み前にやっておく準備を説明します。


【準備(セットアップ)】
簡単に言うと「カートリッジ(GB SMART CARD USB)を PC に接続した時に、 PC がこのカートリッジを認識できればよい」のですが、意外と大変でした。以下、その理由も含めて解説しながら手順を紹介します。

カートリッジに PC 上のゲーム ROM をコピーするツールそのものはこちらです(ドライバも含まれています):
ems-qart

ダウンロード&展開して、(Windows であれば)zadig_2.2.exe を実行してドライバをインストール、、するのですが、結論を先に言うとこれがうまくいきません:
2023063001


自分は Windows 11 を使っているのですが、Windows 10(64bit)以降では各種ドライバのインストール時に「署名済みのドライバのみインストールできる」ような仕様に変わりました。以前のバージョンでは未署名ドライバを導入しようとすると「このドライバは署名されてないけど、どうする?」みたいに聞かれ、リスクを理解した上でインストールすることもできたのですが、Win10 以降では少なくともこの方法ではできなくなりました(未署名ドライバは問答無用でインストール時にエラーになります。より安全になった、とも言えます)。そして今回ゲーム ROM を書き込む GB SMART CARD USB のドライバは未署名なので、この問題を解決する必要があるのでした。

で、この仕様を回避してドライバをインストールする方法について説明されているのがこちらのサイトです。要はレジストリを変更して、Windows のデフォルトの挙動として未署名ドライバもインストールするように切り替えて再起動する、という方法です。リスクを伴う作業であることを理解の上で実行してください:
ドライバー署名の強制を無効にする方法

この手順で未署名ドライバもインストールできるように変更した上でドライバをインストールします。先ほど同様に zadig_2.2.exe を実行します。最初にオンラインアップデートを有効にするか聞かれるのでどちらかを選んでください:
2023063002


ドライバのインストーラーである zadig が起動します。"Install WCID Driver" をクリックしてインストールします:
2023063003


インストール中の画面・・・
2023063004


このメッセージが出ればインストールは完了です:
2023063005


インストール成功後に GB SMART CARD USB を PC に接続してから emu-qart.exe を実行します:
2023063006


ゲーム ROM の書き込みツール(機能的には読み込みもできるっぽいけど)である emus-qart が起動します。ドライバが正しくインストールされていれば、緑のマークとその横に "Cart connected" と表示されます。これで面倒な事前準備は完了です:
2023063007



【ゲーム書き込み】
ems-qart を使って GB SMART CARD USB に PC 内のゲームファイルを書き込みます。まず "I/O direction" と書かれた箇所は GB SMART CARD USB に書き込みたいので "Write to cart" を選択します。また "Memory" 欄は "ROM(.gb/.gbc files)" を選択しておきます。そしてゲーム ROM ファイル(この例では molemole.gb)を指定し、"Start" ボタンで書き込みを開始します:
2023063008


 
"Transfer completed!" と表示されれば書き込み成功です!
2023063009



【ゲームボーイ起動】
ではゲームボーイ実機を使ってゲームを起動してみましょう。ゲーム ROM を書き込んだ GB SMART CARD USB をゲームボーイ(自分の場合はゲームボーイアドバンスSP)に差し込んで、スイッチオン・・・



興味本位で作り始めた GBDK でのゲーム開発、エミュレーターでの動作もうれしかったけど、実機の中で動くのを見るのはちょっと違った感動がありました。




全5回にわたって作り続けてきた GBDK によるオリジナルゲームネタ、今回が最終回です。

前回までの内容はこちらです:
GBDK でゲームボーイのオリジナルゲームを作る(0)
GBDK でゲームボーイのオリジナルゲームを作る(1)
GBDK でゲームボーイのオリジナルゲームを作る(2)
GBDK でゲームボーイのオリジナルゲームを作る(3)


前回途中まで作ったパズルゲーム「モールモール」を完成させます。ステージクリアの判断と次のステージへの移動(Aボタン)、ステージやり直し(Bボタン)、そして全ステージクリアの判断を追加しました。更に現在のステージ番号と、残りの芋の数を画面上部に表示するようにしました。

最終的に作ったコードを GitHub で公開します(ソースコードもゲーム ROM も MIT ライセンスです。改変含めて自由に利用ください)。このシリーズで紹介した helloworld, keys, move に加えて molemole まで含め、全てのソースコードとコンパイル済み ROM が各フォルダ内に含まれています:
https://github.com/dotnsf/gbdk

2023062301


(とりあえずの)完成形としてのモールモールのゲーム ROM はこちらからダウンロードできます。
https://github.com/dotnsf/gbdk/blob/main/4.molemole/molemole.gb

画面右の方にダウンロードボタンがあるのでここをクリックして molemole.gb をダウンロードしてください:
2023062302


あとはゲームボーイエミュレータを起動して、ダウンロードしたゲーム ROM を開けば遊べる、はず、です:




1ステージ目はともかく、2ステージ目は簡単ではないと思います。3ステージ目は更に難しくしたつもりです。Bボタンを押すと現在のステージを最初からやり直しできます。ステージクリア後にAボタンで次のステージに行けます。3ステージクリアでゲームクリアとなります。ちなみに3ステージをクリアするとこんな画面になります:
2023062301


今回はグラフィックをほぼ省略して作ったのであまりにシンプルなものになってしまいましたが、でもこれは「シンプルなゲームも作れる」ことを意味しているわけで、ちょっとしたイベントの企画などでも試せる環境だと思いました。個人的には次はグラフィックに対応させて、ロゴなども登場させるようなゲームを作れたら面白そうだな、と思ってます。

あとゲームボーイの ROM カートリッジでどこかで入手できたりするんだろうか? こうなるとエミュレータじゃなくて実機で動かしてみたい欲もでてきました。本体は秋葉原あたりで入手できそうな気がするけど、カートリッジ化は別のハードルになりそうな気もしてます。

集中連載ブログとしてはいったんこれで終わりとします。いずれ補足的に実機プレイとかデバッグ方法とかについてもブログ化するかもしれません(実はいまだに効率いいデバッグ方法がわかってない・・)が、まずは同じような挑戦をする人の最初の一歩の手助けになれば。。



ゲームボーイ SDK である GBDK を使ってオリジナルゲームを作ろうとしている様子をブログで公開しています。前回までの内容はこちらです:
GBDK でゲームボーイのオリジナルゲームを作る(0)
GBDK でゲームボーイのオリジナルゲームを作る(1)
GBDK でゲームボーイのオリジナルゲームを作る(2)


前回はゲームボーイの方向ボタンが押された操作を認識して、キャラクターが画面上の押された方向に移動するような処理を実現しました。今回は(このシリーズの目標である)モールモールというゲームの画面を生成して、その画面内を主人公キャラクターが動き回れるようにしてみます。この辺りからは具体的なゲームのルールを意識した内容になってきます。


【モールモールの画面とルールを理解する】
拙作で申し訳ないのですが、私が作ってウェブ上で実際に遊べるようにしたモールモールの画面を一度確認しておこうと思います。PC ブラウザでこちらをご覧ください:
https://dotnsf.github.io/molemole/

2023062301


画面左上に主人公のモグラ「モール」君がいます。モール君は矢印キーで動かすことができます。簡単にルールを説明すると、
(1)モール君を動かして、画面内のすべての「芋」(画面下に2つあります)を取ってから、
(2)ドア(画面右上)に行けばステージクリア
という単純なものです。ただし画面内には重力があり、モール君は下に何もない所に移動すると下に何かがある所まで落ちてしまいます。また上に向かうには階段が必要です。 土(緑のブロック)は掘って進むことができますが、石(灰色のブロック)は掘れません。また石も重力の影響を受けて落下します。上のステージは簡単にクリアできますが、先に進むと、石を落下させておかないと詰まってしまうステージもあったりします。そのような場合は "retry" ボタンでやり直しできます。いわゆる「敵キャラ」のいないパズルゲームなので、じっくり考えて進めることができるものです(作る立場でも敵キャラがいないのは楽で助かりますw)。

とりあえずこの画面を使ってモールモールを理解しておいてください。これをゲームボーイ画面で再現するのが今回のゴールです。上のウェブページのは「いらすとや」の画像を利用して作っていますが、ゲームボーイでは(今回は)キャラクターベースの画面で、こんな感じで作ることにします(右列の「識別子」はゲーム内でこれらの対象を識別するために数字で表現したものです、後で使います):

対象文字識別子
モール君 @
何もない所半角スペース0
. 1
階段 < 2
イモ * 3
# 4
ドア D 5


またゲーム盤の情報は2次元配列で管理するものとします。例えば上のゲーム画面は 4x4 のマスを使っていて、識別子で表すとこのようになります(モール君は無視して表現しています):

0005
2142
2442
2332


これをC言語の(4x4 の)2次元配列で管理するので、(この面の例であれば)このようなデータになります:
const uint8_t board[4][4] = {
  { 0, 0, 0, 5 },
  { 2, 1, 4, 2 },
  { 2, 4, 4, 2 },
  { 2, 3, 3, 2 }
};

このデータと、モール君の座標位置(pos_x, pos_y)を別途管理して、「ゲーム盤を表示して、その上にモール君を上書きする」ことで現在の画面が表示できるようにします。

なお今回のゲームボーイ版ではゲーム盤のサイズは(ステージに関係なく)常に 10x10 とします。で、試しに第一ステージをこのように定義してみました:
const uint8_t board[10][10] = {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 5 },
        { 2, 4, 4, 4, 4, 4, 4, 4, 4, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 3, 3, 3, 3, 3, 3, 3, 3, 2 }
};

これとモール君の初期座標(pos_x = 0, pos_y = 0)を重ねると、初期ゲーム画面はこのようになります:
2023062302


そしてモール君が矢印キーで移動したら(移動できる場所かどうかを判定した上で)座標を変更し、その移動によってゲーム盤情報が変わったら(土を掘ったとか、芋を取ったとか、石が落ちてきたとか、・・)ゲーム盤情報を更新した上で画面を再描画して、・・というのを繰り返し、
・画面内の芋の数が0になって、
・モール君の座標位置がドア(5)と一致
したらステージクリア、という判断があればゲームとしては成立しそうです。


【モールモールを作ってみる】
細かな制御は次回に回すとして、まずはモールモールとしての画面内をモール君が動き回れるようにするところまでを今回の目標として作ってみました。またステージは1つだけでなく、複数ステージのデータを入れておきます(ステージクリアして次のステージにいけるのは次回とします)。

で、ソースコード(molemole.c)はこんな感じになりました(ゲームデータなども含む内容になったので、300 行を超える結構なボリュームになってしまいました):
#include <gb/gb.h>
#include <gbdk/console.h>
#include <stdint.h>
#include <stdio.h>

#define SIZE 10

#define min_x 1
#define min_y 3
#define max_x ( min_x + SIZE - 1 )
#define max_y ( min_y + SIZE - 1 )
/*
-    主人公 @
- 0. 何もない (半角スペース)
- 1. 土 .
- 2. はしご >
- 3. 芋 *
- 4. 石 #
- 5. ドア D
*/
const uint8_t boards[3][SIZE][SIZE] = {
    {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 5 },
        { 2, 4, 4, 4, 4, 4, 4, 4, 4, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 3, 3, 3, 3, 3, 3, 3, 3, 2 }
    },
    {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 5 },
        { 2, 4, 4, 4, 4, 4, 4, 4, 4, 2 },
        { 2, 4, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 2, 3, 3, 3, 3, 3, 3, 3, 3, 2 }
    },
    {
        { 0, 0, 0, 0, 0, 0, 0, 0, 0, 5 },
        { 1, 4, 4, 4, 4, 4, 4, 4, 4, 2 },
        { 1, 4, 1, 1, 1, 1, 1, 1, 4, 2 },
        { 1, 4, 1, 0, 0, 0, 0, 1, 4, 2 },
        { 1, 4, 1, 0, 3, 3, 0, 1, 4, 2 },
        { 1, 4, 1, 0, 1, 1, 0, 1, 4, 2 },
        { 1, 4, 1, 1, 1, 1, 1, 1, 4, 2 },
        { 1, 1, 4, 4, 4, 4, 4, 4, 1, 2 },
        { 1, 1, 1, 1, 1, 1, 1, 1, 1, 2 },
        { 1, 1, 1, 1, 1, 1, 1, 1, 1, 2 }
    }
};
const uint8_t init_positions[2][2] = {
    { 0, 0 },
    { 0, 0 },
    { 4, 0 }
};
uint8_t stage = 0;
int items = 0;
uint8_t current_board[SIZE][SIZE];
uint8_t pos_x, pos_y;

void clearScreen( void ){
    uint8_t i;
    char line[SIZE+3];

    //. line 生成
    for( i = 1; i < SIZE + 1; i ++ ){
        line[i] = ' ';
    }
    line[0] = '#';
    line[SIZE+1] = '#';
    line[SIZE+2] = NULL;

    for( i = 3; i < 3 + SIZE; i ++ ){
        gotoxy( 0, i );
        printf( line );
    }
}

void displayChar( void ){
    gotoxy( pos_x + 1, pos_y + 3 );
    printf( "@" );
}

void displayBoard( void ){
    uint8_t i, j;
    char line[SIZE+3];

    for( i = 0; i < SIZE; i ++ ){
        //. line 生成
        for( j = 0; j < SIZE; j ++ ){
            if( current_board[i][j] == 0 ){
                line[1+j] = ' ';
            }else if( current_board[i][j] == 1 ){
                line[1+j] = '.';
            }else if( current_board[i][j] == 2 ){
                line[1+j] = '>';
            }else if( current_board[i][j] == 3 ){
                line[1+j] = '*';
            }else if( current_board[i][j] == 4 ){
                line[1+j] = '#';
            }else if( current_board[i][j] == 5 ){
                line[1+j] = 'D';
            }
        }

        line[0] = '#';
        line[SIZE+1] = '#';
        line[SIZE+2] = NULL;

        gotoxy( 0, i + 3 );
        printf( line );
    }

    for( j = 0; j < SIZE + 2; j ++ ){
        line[j] = '#';
    }
    line[SIZE+2] = NULL;
    gotoxy( 0, SIZE + 3 );
    printf( line );
}

void displayStage( void ){
    //clearScreen();
    displayBoard();
    displayChar();
}

int checkMove( int dir ){  //. dir: 0=UP, 1=RIGHT, 2=DOWN, 3=LEFT
    uint8_t cnt = 0;
    
    if( dir == 0 ){
        if( pos_y > 0 && current_board[pos_y][pos_x] == 2 && current_board[pos_y-1][pos_x] != 4 ){
            pos_y --;
            if( current_board[pos_y][pos_x] != 2 ){
                current_board[pos_y][pos_x] = 0;
            }

            return 1;
        }else{
            return 0;
        }
    }else if( dir == 1 ){
        if( pos_x < SIZE - 1 && current_board[pos_y][pos_x+1] != 4 ){
        	//. 上が石だった場合
        	if( pos_y > 0 && current_board[pos_y-1][pos_x] == 4 ){
        		current_board[pos_y-1][pos_x] = 0;
        		current_board[pos_y][pos_x] = 4;
        	}
        	
            pos_x ++;
            if( current_board[pos_y][pos_x] != 2 ){
                current_board[pos_y][pos_x] = 0;
            }

            //. Gravity
            while( pos_y < SIZE - 1 && current_board[pos_y+1][pos_x] == 0 ){
	        	//. 上が石だった場合
        		if( pos_y > 0 && current_board[pos_y-1][pos_x] == 4 ){
        			current_board[pos_y-1][pos_x] = 0;
        			current_board[pos_y][pos_x] = 4;
        		}
                pos_y ++;
            }

            return 1;
        }else{
            return 0;
        }
    }else if( dir == 2 ){
        if( pos_y < SIZE - 1 && current_board[pos_y+1][pos_x] != 4 ){
        	//. 上が石だった場合
        	if( pos_y > 0 && current_board[pos_y-1][pos_x] == 4 ){
        		current_board[pos_y-1][pos_x] = 0;
        		current_board[pos_y][pos_x] = 4;
        	}

            pos_y ++;
            if( current_board[pos_y][pos_x] != 2 ){
                current_board[pos_y][pos_x] = 0;
            }
        	
            //. Gravity
            while( pos_y < SIZE - 1 && current_board[pos_y+1][pos_x] == 0 ){
	        	//. 上が石だった場合
        		if( pos_y > 0 && current_board[pos_y-1][pos_x] == 4 ){
        			current_board[pos_y-1][pos_x] = 0;
        			current_board[pos_y][pos_x] = 4;
        		}
                pos_y ++;
            }

            return 1;
        }else{
            return 0;
        }
    }else if( dir == 3 ){
        if( pos_x > 0 && current_board[pos_y][pos_x-1] != 4 ){
        	//. 上が石だった場合
        	if( pos_y > 0 && current_board[pos_y-1][pos_x] == 4 ){
        		current_board[pos_y-1][pos_x] = 0;
        		current_board[pos_y][pos_x] = 4;
        	}
        	
            pos_x --;
            if( current_board[pos_y][pos_x] != 2 ){
                current_board[pos_y][pos_x] = 0;
            }

            //. Gravity
            while( pos_y < SIZE - 1 && current_board[pos_y+1][pos_x] == 0 ){
	        	//. 上が石だった場合
        		if( pos_y > 0 && current_board[pos_y-1][pos_x] == 4 ){
        			current_board[pos_y-1][pos_x] = 0;
        			current_board[pos_y][pos_x] = 4;
        		}
                pos_y ++;
            }

            return 1;
        }else{
            return 0;
        }
    }else{
    	return 0;
    }
}

int countLeftItems( void ){
    uint8_t i, j, cnt = 0;
    for( i = 0; i < SIZE; i ++ ){
        for( j = 0; j < SIZE; j ++ ){
            if( current_board[i][j] == 3 ){
                cnt ++;
            }
        }
    }

	items = cnt;
	if( cnt == 0 ){
		if( current_board[pos_y-1][pos_x-1] == 5 ){
			return -1;
		}else{
			return 0;
		}
	}else{
		return cnt;
	}
}

void initStage( void ){
    uint8_t i, j;
    for( i = 0; i < SIZE; i ++ ){
        for( j = 0; j < SIZE; j ++ ){
            current_board[i][j] = boards[stage][i][j];
        }
    }
    pos_x = init_positions[stage][0];
    pos_y = init_positions[stage][1];

    displayStage();
}

void initGame( void ){
    stage = 0;
    initStage();
}

void main( void ){
    uint8_t key;
    int dir, chk;

    gotoxy( 2, 1 );
    printf( "MoleMole" );

    initGame();

    CRITICAL {
        add_SIO(nowait_int_handler);
    }
    set_interrupts(SIO_IFLAG);

    while( 1 ) {
        key = waitpad( J_A | J_B | J_START | J_SELECT | J_UP | J_DOWN | J_LEFT | J_RIGHT );
        waitpadup();

        dir = -1;
        if( key == J_A ){
            // A Button
        	if( items == -1 ){
        		if( stage < 2 ){
	        		//. 次のステージ
        			stage ++;
        			initGame();
        		}else{
        			//. ゲーム終了
        		}
        	}
        }else if( key == J_B ){
            // B Button
            //printf( "B" );
        }else if( key == J_START ){
            // START Button
            //printf( "START" );
        }else if( key == J_SELECT ) {
            // SELECT Button
            //printf( "SELECT" );
        }else if( key == J_UP ) {
            // UP Button
        	if( items > -1 ){
	            dir = 0;
        	}
        }else if( key == J_DOWN ) {
            // DOWN Button
        	if( items > -1 ){
	            dir = 2;
        	}
        }else if( key == J_LEFT ) {
            // LEFT Button
        	if( items > -1 ){
	            dir = 3;
        	}
        }else if( key == J_RIGHT ) {
            // RIGHT Button
        	if( items > -1 ){
	            dir = 1;
        	}
        }

        if( dir != -1 ){
            if( checkMove( dir ) ){
                displayStage();
                if( countLeftItems() == -1 ){
                    //. ステージクリア

                }
            }
        }

        /* In case of user cancellation */
        waitpadup();
    }
}

ゲームデータ部分を除けば前回の内容からさほど大きく変わってはいません。矢印キーが押された時の処理を単純に動かすのではなく、ゲーム画面的な要素も加えた上で移動可能な方向かどうかを判断し、移動した先でも重力の影響を受ける場合は地面につくまで落下させ、新しいゲーム状態の画面を描画する、という内容です。また3ステージぶんのゲームデータが含まれていますが、ゲーム進行状況に応じて内容が書き換わってもいいように(次回実装予定ですが、ステージの初めからやり直すための機能もつけたいので)、3ステージぶんのゲームデータは boards という3次元配列に、現在のゲーム版は current_board という2次元配列に、そして3ステージぶんのモール君の初期位置は init_positions という2次元配列で最初に定義しています。 ※ゲームとして完成しているわけではありません。次回完成予定です。


で、このコードをコンパイルしてゲーム ROM を作ります。前回同様に Windows であれば compile.bat を、macOS/Linux であれば Makefile を以下のように作成して、それぞれ "compile" / "make" を実行します:
(compile.bat)
c:\MyApps\gbdk\bin\lcc -Wa-l -Wl-m -Wl-j -o molemole.gb molemole.c

(Makefile)
CC	= ~/gbdk/bin/lcc -Wa-l -Wl-m -Wl-j

BINS	= molemole.gb

all:	$(BINS)

# Compile and link single file in one pass
%.gb:	%.c
	$(CC) -o $@ $<

clean:
	rm -f *.o *.lst *.map *.gb *~ *.rel *.cdb *.ihx *.lnk *.sym *.asm *.noi


成功すると molemole.gb という ROM ファイルができあがります。ゲームボーイエミュレータを起動して同ファイルを読み込むと、ソースコード内の boards[0] の内容のゲーム盤と主人公モール君("@")が表示され、入力した方向キーの内容に従ってモール君が上下左右に動いている様子を確認できます(動作している様子のスクリーン動画を撮ってみました):




かなりゲームっぽい画面に近づいてきました。次回はステージクリアややり直しなどの条件も加え、いよいよ「モールモールを完成させる」ことに挑戦する予定です。



ゲームボーイ SDK である GBDK を使ってオリジナルゲームを作ろうとしている様子をブログで公開しています。前回までの内容はこちらです:
GBDK でゲームボーイのオリジナルゲームを作る(0)
GBDK でゲームボーイのオリジナルゲームを作る(1)

前回はゲームボーイの各ボタンが押された操作を認識できるようにしました。今回は推された矢印ボタンに合わせてキャラクターが移動するように改良してみます。


【座標の取り扱い】
今回作成しているゲームは複雑なグラフィック操作を行わず、テキストベースの画面で作ることを想定しています。このテキストを表示する座標(座標)を gotoxy( 横座標, 縦座標 ) という関数で座標を指定してから printf() を実行する、という形で実現します。ちなみに画面左上の座標は ( 0, 0 ) です、つまり横座標も縦座標も 0 から始まります。

キャラクターが移動可能な座標範囲を 10x10 として、キャラクターを "@" という文字であらわすことにして、キャラクターの座標を pos_x, pos_y という変数で管理しながら「矢印ボタンが押されたら画面をクリアしてから該当座標にキャラクターを表示する」というロジックでソースコード(move.c)を作るとこんな感じになります(前回からの変更点を青字にしています):
#include <gb/gb.h>
#include <gbdk/console.h>
#include <stdint.h>
#include <stdio.h>

#define SIZE 10

#define min_x 1
#define min_y 3
#define max_x ( min_x + SIZE - 1 )
#define max_y ( min_y + SIZE - 1 )

void clearScreen(){
    uint8_t i;
    char line[SIZE+1];

    //. line 生成
    for( i = 0; i < SIZE; i ++ ){
        line[i] = ' ';
    }
    line[SIZE] = NULL;

    for( i = min_y; i <= max_y; i ++ ){
        gotoxy( min_x, i );
        printf( line );
    }
}

void moveTo( uint8_t x, uint8_t y ){
    clearScreen();
    gotoxy( x, y );
    printf( "@" );
}

void main( void ){
    uint8_t key;
    uint8_t pos_x = 1, pos_y = 3;

    gotoxy( 1, 1 );
    printf( "move" );

    CRITICAL {
        add_SIO(nowait_int_handler);
    }
    set_interrupts(SIO_IFLAG);

    while( 1 ) {
        key = waitpad( J_A | J_B | J_START | J_SELECT | J_UP | J_DOWN | J_LEFT | J_RIGHT );
        waitpadup();

        clearScreen();

        if( key == J_A ){
            // A Button
            //printf( "A" );
        }else if( key == J_B ){
            // B Button
            //printf( "B" );
        }else if( key == J_START ){
            // START Button
            //printf( "START" );
        }else if( key == J_SELECT ) {
            // SELECT Button
            //printf( "SELECT" );
        }else if( key == J_UP ) {
            // UP Button
            if( pos_y > min_y ){
                pos_y --;
            }
        }else if( key == J_DOWN ) {
            // DOWN Button
            if( pos_y < max_y ){
                pos_y ++;
            }
        }else if( key == J_LEFT ) {
            // LEFT Button
            if( pos_x > min_x ){
                pos_x --;
            }
        }else if( key == J_RIGHT ) {
            // RIGHT Button
            if( pos_x < max_x ){
                pos_x ++;
            }
        }
        moveTo( pos_x, pos_y );

        /* In case of user cancellation */
        waitpadup();
    }
}


最初に "#define SIZE 10" 行で画面のサイズ(縦と横の大きさ)を決め、実際に移動できる座標範囲を ( 1, 3 ) ~ (10, 12 ) と定義します(この範囲を超えての移動ができないようにします)。

また画面をクリアするための関数 clearScreen() と、指定座標に "@" キャラクターを表示する moveTo() 関数を定義しておきます。

そして前回同様に main() 関数のループ内でボタン入力を検知しますが、矢印ボタンだった場合はキャラクターの新しい表示座標を計算した上で moveTo() 関数を実行する、という内容です。


この move.c をビルドします。前回までと同様、Windows の場合は compile.bat を、macOS や Linux の場合は Makefile を用意し、それぞれ "compile.bat" と "make" を実行して move.gb をビルドします:

(compile.bat)
c:\MyApps\gbdk\bin\lcc -Wa-l -Wl-m -Wl-j -o move.gb move.c

(Makefile)
CC	= ~/gbdk/bin/lcc -Wa-l -Wl-m -Wl-j

BINS	= move.gb

all:	$(BINS)

# Compile and link single file in one pass
%.gb:	%.c
	$(CC) -o $@ $<

clean:
	rm -f *.o *.lst *.map *.gb *~ *.rel *.cdb *.ihx *.lnk *.sym *.asm *.noi


成功すると move.gb という ROM ファイルができあがります。ゲームボーイエミュレータを起動して同ファイルを読み込むと、入力した方向キーの内容に従ってキャラクター("@")が上下左右に動いている様子を確認できます(動作している様子のスクリーン動画を撮ってみました):



キー入力に連動してキャラクターが動かせるようになりました。ここまでできると今回目標としている「モールモール」や「倉庫番」のようなゲームを作る目途が立ちましたね。

次回はいよいよ「モールモールっぽい画面を作る」ことに挑戦する予定です。



ゲームボーイ SDK である GBDK を使ってオリジナルゲームを作ろうとしている様子をブログで公開しています。準備編とハローワールドの前回のはこちらです:
GBDK でゲームボーイのオリジナルゲームを作る(0)


今回はゲームに必須の「ボタン操作」を実装してみます。方向キーで動く方向を指示したり、SELECT したり START したり、「はい/いいえ」を選んだり、という操作で必要になるであろう各種ボタン操作のハンドリングを行います。環境構築を含めた GBDK のセットアップ方法については前回のエントリを参照してください。


【ボタン操作の認識】
今回作ろうとしているモールモールではややこしいグラフィック操作を行うつもりはありませんが、ゲームボーイで操作する以上は「START ボタンで始まって、方向キーで動かして、Aボタンで先に進んで、・・」といったボタン操作は避けて通ることができません。というわけで今回はボタン操作を実装しました。ソースコード(keys.c)はこんな感じ、前回から加えた部分を青字にしています:
#include <gb/gb.h>
#include <stdint.h>
#include <stdio.h>

void main( void ){
    uint8_t key;

    printf( "Hello World!\n" );

    CRITICAL {
        add_SIO(nowait_int_handler);
    }
    set_interrupts(SIO_IFLAG);

    while( 1 ) {
        key = waitpad( J_A | J_B | J_START | J_SELECT | J_UP | J_DOWN | J_LEFT | J_RIGHT );
        waitpadup();

        if( key == J_A ){
            // A Button
            printf( "A" );
        }else if( key == J_B ){
            // B Button
            printf( "B" );
        }else if( key == J_START ){
            // START Button
            printf( "START" );
        }else if( key == J_SELECT ) {
            // SELECT Button
            printf( "SELECT" );
        }else if( key == J_UP ) {
            // UP Button
            printf( "UP" );
        }else if( key == J_DOWN ) {
            // DOWN Button
            printf( "DOWN" );
        }else if( key == J_LEFT ) {
            // LEFT Button
            printf( "LEFT" );
        }else if( key == J_RIGHT ) {
            // RIGHT Button
            printf( "RIGHT" );
        }
        printf( "\n" );

        /* In case of user cancellation */
        waitpadup();
    }
}

waitpad() という関数でキー入力を取得します。その際に特定種類のキー入力だけを取得したい場合は waitpad() 関数の引数に対象キー(のビット和)を指定します(上の例では全てのキーを対象にしています)。そして if 文で入力されたキーを識別して、入力されたキーを printf で表示する、という内容です。この一連の処理を while で無限ループにしています。 また("Hello World!" 部分も含めて)1行ずつ表示したいので、printf の最後に "\n" を入れて1回ずつ改行するようにしています。

この keys.c をビルドします。前回同様、Windows の場合は compile.bat を、macOS や Linux の場合は Makefile を用意し、それぞれ "compile.bat" と "make" を実行して keys.gb をビルドします:
(compile.bat)
c:\MyApps\gbdk\bin\lcc -Wa-l -Wl-m -Wl-j -o keys.gb keys.c

(Makefile)
CC	= ~/gbdk/bin/lcc -Wa-l -Wl-m -Wl-j

BINS	= keys.gb

all:	$(BINS)

# Compile and link single file in one pass
%.gb:	%.c
	$(CC) -o $@ $<

clean:
	rm -f *.o *.lst *.map *.gb *~ *.rel *.cdb *.ihx *.lnk *.sym *.asm *.noi

成功すると keys.gb という ROM ファイルができあがります。ゲームボーイエミュレータを起動して同ファイルを読み込むと、入力したキーの内容がそのまま画面に出力されるのが確認できます(動作している様子のスクリーン動画を撮ってみました):


(↑有名なコナミコマンドを入力してみました)


キー入力を識別できるようになったので、次回は「キャラクターを動かす」ことに挑戦する予定です。


このページのトップヘ