ファミコンROM作ってみた:開発編(コード設計)


ゲーム内容

作成ROM
ファミコンROM作ってみた

上記リンクにあるゲームのコード設計情報になります。

ポイント

C言語はC+系に比べてクラスという存在がないため、どうしても可読性の面で落ちます。
特に下記2点の問題をどう解決するか工夫したほうがよいかなと考えました。
・広範囲になりがちな変数のスコープ対策
・とりづらい多態性

スコープ対策について

スコープ対策の為、複数のファイルに分割してビルドできるように各サイトを参考にMakeFileを作成し、気兼ねせずファイルを分割できる環境を用意しました。(関連記事1(環境),関連記事2(ビルド))

その環境上で以下のように目的別にファイルを分割、グローバル変数(および、スタティック変数)は、各ファイル内のスコープ内で処理し、必要な変数のみヘッダーにexternで記載するようにすることで、簡単に言うとファイル単位でのカプセル化を行う事でC++クラスにおけるpublicとprivateのようなスコープの区別をしやすくしています。

NesMain処理コード

main.c,h

汎用コード

global.c,h

フロー処理コード

fl_title.c,h
fl_game.c,h
fl_result.c,h

ゲーム内スプライト機能コード

gm_player.c,h
gm_monster.c,h
gm_coin.c,h
gm_bullet.c,h

スプライトパターン

pattern.c,h

ライブラリなどのコード

fcsub.c.h
startup.asm

MakeFile関連

MakeFile
build.cfg

多態性について

Cコードではよく使われる手法かと思いますが、関数ポインタを利用して、処理ループ内でswitch文を使わず一貫した呼び出しでスプライトの処理を分岐する予定でしたが、関数ポインタがうまく動作しないため断念。。。(Stack関連だろうか…)

最終的なステート図と各ファイル

最終的なステート図と関連する各ファイルを関連づけた全体像を示すと下記のような処理フローになっています。(PlantUMLで記載)

pattern.hの.c,hへの分離コード

下記リンク先の記事で元コードを改変して情報を追記したpattern.hを作成しましたが、複数ファイルでMakeする場合、定義と実テーブルが混在しているとせっかくスクリプト出力している定義が使えなくなってしまうので.cに実テーブルを分離する処理を記載しました。
ファミコンROM作ってみた:開発編(画像コンバーター)

pattern.h作成用のスクリプトコード処理の後に、下記に記載した関数を呼び出して処理してあります。

具体的なPythonコードは下記のとおりですが、上記リンク先のコードで出力したpattern.hを読み込み、.c側にテーブルデータを逃がして、.hは再入しないように定義を追加、テーブル定義のextern宣言も.h側に追記してあるコードとなっています。

def separate_code(output_file):
    if os.path.exists(output_file) == False:
        return;

    txtinfile = open(output_file,"r")
    txtlist = txtinfile.readlines()
    txtinfile.close()

    output_file_h = output_file
    output_file_c = output_file.replace(".h",".c")


    fbuf_h = "#ifndef __" + output_file_h.upper() + "__\n"
    fbuf_h +="#define __" + output_file_h.upper() + "__\n"
    fbuf_h +="//" + output_file_h + "\n"
    fbuf_c = "//" + output_file_c + "\n"
    fbuf_c += "#include \"" + os.path.basename(output_file_h) + "\"\n"

    fbuf_h += "\n"
    fbuf_c += "\n"

    extern_header =""
    for txt_y in range(len(txtlist)):
        if(txtlist[txt_y].find("#define")>=0):
            fbuf_h += txtlist[txt_y];
            continue;

        if(txtlist[txt_y].find("//")>=0):
            fbuf_h += txtlist[txt_y];
            continue;

        if(txtlist[txt_y]=="\n"):
            fbuf_h += "\n"
            fbuf_c += "\n"
            continue;

        if(txtlist[txt_y].find("char")>=0):
            extern_header += "extern " + txtlist[txt_y][0:txtlist[txt_y].find("=")] +";\n"

        fbuf_c += txtlist[txt_y];

    fbuf_h += "\n"
    fbuf_h += extern_header;
    fbuf_h += "\n"
    fbuf_h += "#endif\n"

    outfile = open(output_file_h, "w")  #.hファイルの出力
    outfile.write(fbuf_h)
    outfile.close()

    outfile = open(output_file_c, "w")  #.cファイルの出力
    outfile.write(fbuf_c)
    outfile.close()

参考記事一覧

CC65
https://www.cc65.org/

cc65@wiki
https://w.atwiki.jp/cc65/

日経BP発行「日経ソフトウエア」2021年1月号
特集記事「ファミコンで動くゲームを作ろう 第2部 ライブラリの自作と実験プログラム」(著者:松原拓也氏)

ムトー研究所
C言語でNESアプリを作成(技術的な話題)
http://muto.world.coocan.jp/nesapp/nesapp-tech.html
C言語でNESアプリを作成(標準関数を使ったサンプル)
http://muto.world.coocan.jp/nesapp/nesapp-cui.html

Yoji Suzuki ファミコンで始める ~ 6502マシン語ゲームプログラミング
https://github.com/suzukiplan/mgp-fc

関連記事

ファミコンROM作ってみた
ファミコンROM作ってみた:開発編(画像コンバーター)
ファミコンROM作ってみた:開発編(環境)
ファミコンROM作ってみた:開発編(ビルド)
ファミコンROM作ってみた:開発編(コード設計)
ファミコンROM作ってみた:開発編(共通関数ライブラリ)
ファミコンROM作ってみた:開発編(プロダクト用関数ライブラリ)
ファミコンROM作ってみた:開発編(mainとフローの処理コード)
ファミコンROM作ってみた:開発編(キャラクター制御コード)