ゲームボーイ 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] の内容のゲーム盤と主人公モール君("@")が表示され、入力した方向キーの内容に従ってモール君が上下左右に動いている様子を確認できます(動作している様子のスクリーン動画を撮ってみました):




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