#ゲーム内容
作成ROM
ファミコンROM作ってみた
上記リンクにあるゲームのコードになります。
コード全体のイメージとしては下記の記事をご参照ください。
ファミコンROM作ってみた:開発編(コード設計)
コード内で利用している共通ライブラリについては下記の記事をご参照ください。
ファミコンROM作ってみた:開発編(共通関数ライブラリ)
コード内で利用しているプロダクト用関数ライブラリについては下記の記事をご参照ください。
ファミコンROM作ってみた:開発編(プロダクト用関数ライブラリ)
##参考・出典
日経BP発行「日経ソフトウエア」2021年3月号の特集記事「ファミコンで動くゲームを作ろう 第3部 オリジナルのゲームを完成させる」(著者:松原拓也氏)
※上記記事に掲載されたコードや内容を参考にしました。掲載にあたっては、著者および編集部の承諾をいただきました。
##src/main.c
メインループでは、初期設定の後は、各フローをwhile関数で回しているだけの処理になっています。
ゲームだとこのループ内にvsync待ちの処理やパッド情報の更新処理など、vsync中に1度だけ処理したいケースを記載したりすることも多いかもしれませんが、あまりmainループ内に色々書いてパラメータで制御していくと、必要のないコードまでヒープ(RAM領域)を利用する事になるかと思いましたので、メモリ効率を考えて、各フロー内でvsyncループがある形を踏襲しました。
#include "global.h"
#include "fl_title.h"
#include "fl_game.h"
#include "fl_result.h"
char NesMain()
{
fc_init();
ppu_pattern((unsigned char*)pattern_tbl, 0, SPRITEPATTERN_NUM);
ppu_palette((char*)(&color_tbl[PALETTE_TOP*4]), 0, 8);
while (1) {
title();
game();
result();
}
return 0;
}
char NMIProc()
{
return 0;
}
##src/fl_title.h,c
タイトルのコードはBGに文字列を設定してパッド入力待ちをしているだけのコードです。
ここにbg_printstr_kana関数にかなカナ交じりの文章をいれて処理してる箇所があります。
#ifndef __FL_TITLE__H__
#define __FL_TITLE__H__
void title();
#endif
#include "global.h"
#include "fl_title.h"
//タイトル画面
void title()
{
char strtitle1[] = "Vividnia Shooting";
char strtitle2[] = "</シラユリ/>ヘブン</チャンネル/>";
char strtitle3[] = "チョウキョウベヤ";
char strpush[] = "PUSH ANY BUTTON";
ppu_enable(0);
bg_cls();
sp_cls();
bg_printstr(16 - (n_strlen(strtitle1) / 2), 9, strtitle1);
bg_printstr_kana(16 - (n_strlen(strtitle2) / 2), 11, strtitle2, nFALSE);
bg_printstr_kana(16 - (n_strlen(strtitle3) / 2), 12, strtitle3, nTRUE);
bg_printstr(8, 18, strpush);
/////////////////////////
ppu_enable(1);
while (1) {
g_pre_pad1 = g_now_pad1;
g_now_pad1 = controller1();
if (g_pre_pad1 != g_now_pad1) {
break;
}
ppu_vsync();
sp_dmastart();
bg_scroll(0, 0);
}
}
##src/fl_game.h,c
ゲームのフローコードも各初期設定した後は作成されている各スプライトの処理を回しているコードになっています。
敵キャラの作成は参考元のコードの条件まま、16frに一回の乱数チェックでlevel以下なら作成になります。
なお、コード内にも記載していますが関数ポインタがちゃんと利用できればもう少しすっきりするコードになるはず。
スプライトテーブルのポインタを渡してもいいのですが、構造体にあまり変数を追加するとRAM領域がなくなってコンパイルエラーが出たので、利用メモリ量は注意しつつの対応を実感。
#ifndef __FL_GAME__H__
#define __FL_GAME__H__
void game();
#endif
#include "global.h"
#include "fl_game.h"
#include "gm_player.h"
#include "gm_monster.h"
#include "gm_coin.h"
#include "gm_bullet.h"
#define BG_START_Y 20
#define BG_END_Y (BG_Y_MAX)
#define MONSTER_ENCOUNT_CHK_CNT 15
void init_gamebg(){
unsigned char x, y;
unsigned char tileno;
//BGにタイル配置
for (y = BG_START_Y; y < BG_END_Y; y++) { //
for (x = 0; x < BG_X_MAX; x++) { //
tileno = (SPRITE_BG_TOP+1) + (x&1) + (y&1);
bg_printch(x, y, tileno); //page1
bg_printch(x, y + BG_VIRTICAL_OFFSET_Y, tileno);//page2
}
}
}
unsigned char IsEntryMonster()
{
if((random() & 0xF) <= g_gameScore.level){
return (random() % (BG_START_Y/2-1)) *SPRITE_H2 + SPRITE_H2;
}
return OUTSIDE;
}
//メインゲーム
void game()
{
char loopcnt = MONSTER_ENCOUNT_CHK_CNT;
unsigned char scl_x = 0;
signed char spr_no, spr_id, cur_id;
unsigned char x, y;
unsigned char spr_type;
ppu_enable(0);
pulse1(300);
bg_cls();
sp_cls();
//初期設定
g_gameScore.score = 0;
g_gameScore.pddiing_num = 0;
g_gameScore.gameover = 0;
g_gameScore.level = 3;
init_gamebg();
player_init();
monster_init();
coin_init();
bullet_init();
//////////////////////
ppu_enable(1);
init_spr_idtbl();
g_player_spr_id = player_start(64, 120);
while (g_gameScore.gameover < 180) {
cur_id = -1;
g_pre_pad1 = g_now_pad1;
g_now_pad1 = controller1();
for (spr_no = 0; spr_no < SPRITE_NUM_MAX; spr_no++) {
if (g_spr_idtbl[spr_no].id == SPRITE_DISABLE_NO) {
//利用していないスプライト
continue;
}
spr_id = g_spr_idtbl[spr_no].id;
if (cur_id == spr_id) {
//処理被らないようにする
continue;
}
cur_id = spr_id;
y = sp_gety(spr_id);
x = sp_getx(spr_id);
//各スプライト処理
//g_spr_idtbl[spr_id].func(spr_id, x, y); //関数ポインタうまく動かない…
spr_type = getSpriteType(sp_gettile(spr_id));
switch (spr_type) {
case TYPE_CHAR: player(spr_id, x, y); break;
case TYPE_ENEMY: monster(spr_id, x, y); break;
case TYPE_BULLET: bullet(spr_id, x, y); break;
case TYPE_ITEM: coin(spr_id, x, y); break;
default:
continue;
break;
}
}
--loopcnt;
if (loopcnt == 0) {
if ((y=IsEntryMonster()) != OUTSIDE) {
monster_start(OVER_SIDE_X, y,spr_id);
}
loopcnt = MONSTER_ENCOUNT_CHK_CNT;
}
if (g_gameScore.gameover > 0) { ++g_gameScore.gameover; }
scl_x = (scl_x + 1) & 255;
ppu_vsync();
sp_dmastart();
bg_scroll(scl_x, 0);
}
}
##src/fl_result.h,c
リザルトフローはスプライトをすべて消した後、ゲームのスコアをBGに設定してパッド入力を待つ処理になっています。
プリンだけスプライトで表示になっているのですが、後ほどゲーム中にスコア表示することも想定すると、BGパターン側にパレットと一緒にデータを設定してBG側で表示できるようにしたほうがいいかもしれません。
#ifndef __FL_RESULT__H__
#define __FL_RESULT__H__
void result();
#endif
#include "global.h"
#include "fl_result.h"
//リザルト
void result()
{
char strgover[] = "GAME OVER";
char strscore[] = "SCORE \\";
char strplayer[] = "PLAYER:";
char tmpstr[6];
char i;
///////////////////////
ppu_enable(0);
bg_cls();
sp_cls();
for (i = 0; i < SPRITE_NUM_MAX; i++) {
if (g_spr_idtbl[i].id != SPRITE_DISABLE_NO) {
char tile_type = getSpriteType(g_spr_idtbl[i].tileno);
switch (tile_type) {
case TYPE_CHAR:
case TYPE_ENEMY:
SetRemoveSprite4(g_spr_idtbl[i].id);
break;
case TYPE_ITEM:
case TYPE_BULLET:
SetRemoveSprite1(g_spr_idtbl[i].id);
break;
default: break;
}
g_spr_idtbl[i].id = SPRITE_DISABLE_NO;
}
}
//////////////////
bg_printstr(11, 11, strgover);
bg_printstr(10, 15, strscore);
num_to_str_n(tmpstr, g_gameScore.score);
bg_printstr(17, 15, tmpstr);
if (g_gameScore.score > 0) {
bg_printstr(17 + n_strlen(tmpstr), 15, "000");
}
if (g_gameScore.pddiing_num > 0) {
AddSpriteSet1(SPRITE_PUDDING_TOP, 14 * 8, 16 * 8, 0); //コイン
bg_printstr(16, 16, "x");
num_to_str_n(tmpstr, g_gameScore.pddiing_num);
bg_printstr(17, 16, tmpstr);
}
////////////////////////
ppu_enable(1);
while (1) {
g_pre_pad1 = g_now_pad1;
g_now_pad1 = controller1();
if (g_pre_pad1 != g_now_pad1) {
break;
}
ppu_vsync();
sp_dmastart();
bg_scroll(0, 0);
}
pulse1(300);
}
#関連記事一覧
ファミコンROM作ってみた
ファミコンROM作ってみた:開発編(画像コンバーター)
ファミコンROM作ってみた:開発編(環境)
ファミコンROM作ってみた:開発編(ビルド)
ファミコンROM作ってみた:開発編(コード設計)
ファミコンROM作ってみた:開発編(共通関数ライブラリ)
ファミコンROM作ってみた:開発編(プロダクト用関数ライブラリ)
ファミコンROM作ってみた:開発編(mainとフローの処理コード)
ファミコンROM作ってみた:開発編(キャラクター制御コード)