【C言語】ゲームのセーブ機能の実現方法(ファイルの応用利用)

ゲームのセーブ機能の実現方法の解説ページアイキャッチ

このページにはプロモーションが含まれています

このページでは、ゲームの拡張の例として「ゲームのセーブ機能」の実現方法について解説します!

今回は、下記で紹介した「じゃんけん」のプログラムをベースとし、これにじゃんけんの戦績をセーブする機能を追加していきます。ゲームの拡張を通じてプログラミングに必要な知識を学んでいただくことを目的としたページとなっており、特にプログラミング初心者の方向けに詳しく&簡単に解説を行っていきたいと思っています。

じゃんけんゲームの作り方の解説ページアイキャッチ 【C言語】じゃんけんゲームの作り方(ソースコードの可読性向上についても解説)

セーブ機能の実現方法

では、セーブ機能の実現方法について解説していきたいと思います。

変数やメモリーでは実現不可

さて、皆さんはプログラミングする時、データを一時保存する際には何を利用しますか?

基本的には、データは変数に保存すると思います。例えば計算結果のデータを保存する場合は、下記のように変数に値を代入することで保存すると思います。

計算結果の保存
int x;
x = 100 * 25;

ですが、変数に保存したデータはプログラムが終了すると消えてしまいます

もう少し詳しく説明すると、ローカル変数に保存したデータ、つまり関数内で宣言する変数に保存したデータは、その関数が終了したときに消えてしまいます(もっと詳しく言うと、他のデータで上書きされてしまいます)。

関数終了時にローカル変数のデータが消える様子

それに対し、グローバル変数に保存したデータは、その変数に他のデータで明示的に上書きしない限り、プログラムが終了するまで残り続けることになります。ですが、やっぱりプログラムが終了すると、そのデータは消えてしまうことになります。

プログラム終了時にグローバル変数のデータが消える様子

ということで、変数に保存したデータはプログラムが終了すると消えてしまうことになり、再度プログラムを起動したときには、元々覚えていたデータは既に存在しないことになります。もちろん、パソコンの電源を OFF した場合も、そのデータが消えてしまうことになります。そのため、ゲームのセーブ機能は、変数へのデータの保存では実現不可ということになります。

そもそも変数は PC のメモリー上に割り付けられるものになります。そして、そのメモリーのデータは電源を切ると消えてしまうことになります。なので、セーブ機能はメモリー以外を利用して実現する必要があります。

再起動によってメモリーのデータが消えてしまう様子

スポンサーリンク

ファイルでセーブ機能が実現可能

そこで登場するのが「ファイル」です。プログラムからファイルにデータを出力してやれば、そのデータがファイル内に保存されることになります。そして、このファイルはパソコンのハードディスクや SSD に保存されることになり、これらに保存されたファイルはパソコンの電源を切っても残り続けます。

ハードディスクのデータがPCを再起動しても残り続ける様子

そのため、ゲームで管理しているデータをファイルに出力しておけば、プログラムを終了しても、パソコンの電源を切ったとしても、プログラム再起動時にファイルからデータを読み込んでやれば、ファイルにデータを出力したときと同じ状況をプログラムで再現することができることになります。この復元する機能自体はロードであり、この復元と前述の保存を組み合わせることで、セーブ機能が実現できることになります。

ファイルを利用してセーブ機能を実現する様子

補足しておくと、ファイルを利用するというのはゲームのセーブ機能の実現手段の1つとなります。例えばデータベースを利用してセーブ機能を実現するようなことも多く、ネットゲームやソシャゲーなどはデータベースを利用することが多いと思います。要は、プログラム内のデータを、プログラムや PC が終了しても保持され続けられるハードに保存さえすれば、セーブ機能は実現可能です。

ただ、ファイルの利用によるセーブ機能は実現が簡単であるため、このページではセーブ機能をファイルを利用して実現することを前提に解説を進めさせていただきたいと思います。

セーブ機能実現の流れ

次は、セーブ機能実現の流れについて説明していきます。

保存するデータを決める

まずは、保存する必要のあるデータを考えましょう!

今回は、じゃんけんの戦績のセーブ機能を実現するのが目的なので、じゃんけんの「勝ち回数」と「負け回数」が保存する必要のあるデータとなります。

今回セーブするデータを説明する図

当然ですが、この保存するデータはゲームの種類によって異なります。例えば RPG であれば、パーティーの各メンバーの名前やレベル・各ステータス値などを保存する必要がありますし、現在地や持っているアイテム、各種フラグ(クリアしたダンジョンや会話した相手などを判別するデータ)なども保存する必要があり、多くのデータを保存する必要があります。そして、これらのデータが足りなければ、プログラム再起動時に前回セーブした状態を再現できなくなってしまいます。

必要なデータが保存されていない様子

ということで、セーブ機能を実現するためには、保存するデータをしっかり精査する必要があり、この精査がセーブ機能を実現するための一番難しいフェーズとなります。

スポンサーリンク

保存するタイミングを決める

また、どのタイミングでデータを保存するか?という点もセーブ機能を実現する上で重要なポイントとなります。

今回は、じゃんけんの勝負が決したタイミングで毎回データを保存するようにしたいと思います。

もちろん、この保存するタイミングもゲームによって異なります。例えば一昔前の「ドラゴンクエスト」という RPG ゲームであれば、教会でお祈りしたタイミングで保存が行われていましたし、ファイナルファンタジーであればセーブポイントでメニューからセーブが選択されたタイミングで保存が行われていました。

毎秒データを保存するようなことも可能ではありますが、保存する周期が短いほどゲームの処理が重くなることになるので注意が必要です。

復元するタイミングを決める

同様に、どのタイミングで保存されているデータを読み込むのか?という点も重要です。

今回は、プログラム起動時にデータの読み込みを行って、保存したデータで以前の状態を復元するようにしたいと思います。

コーディングする

ここまで説明してきた内容が決まれば、後はそれらに合わせてコーディングしていけばよいだけです。

保存したいデータを変数に保存する

まずは、保存したいデータを変数に保存していきます。

今回の場合は「勝ち回数」と「負け回数」を保存する必要があることになります。なので、単純に例えば win_numlose_num の変数を宣言し、じゃんけんに勝った時には win_num1 増加させ、負けたときには lose_num1 増加させるように勝ちと負けの回数をカウントするようにしてやればよいことになります。

ただ、前述の通り、ゲームによっては多くのデータを保存する必要がある場合もあるので、その時には構造体を利用するのがよいと思います。これにより、1つの変数で複数のデータを管理することができるようになって各データの関連性が分かりやすくなりますし、コーディングも楽に行えることになります。

この構造体に関しては下記のページで解説していますので、構造体について詳しく知りたい方は是非下記ページを読んでみてください。

構造体解説ページのアイキャッチ 【C言語】構造体について初心者向けに分かりやすく解説

今回も、保存するデータは少ないものの、構造体を利用してコーディングをしていきたいと思います。具体的には、下記の構造体 SCORE を定義し、このメンバの winlose で勝ち回数と負け回数を保存していきます。

SCORE
typedef struct {
    unsigned int win; // 勝ち回数
    unsigned int lose; // 負け回数
} SCORE;

データをファイルに保存する

あとは、先ほどデータを保存した変数をファイルに出力してやればセーブを行うことができることになります。

C言語でのファイルへの出力は下記の3つの関数によって実現することができます。これらは全て、stdio.h で宣言されていますので、これらを利用する際には予め stdio.h をインクルードしておく必要があります。

  • fopen:引数で指定したパスのファイルを開き、開いたファイルのファイルポインターを返却する
  • fwrite:引数で指定したアドレスのデータを、引数で指定したファイルポインターのファイルに出力する(書き込む)
  • fclose:引数で指定したファイルポインターのファイルを閉じる

基本的には、ファイルを扱う際には最初にファイルを開き、その次にファイルに書き込み(or 読み込み)を実施し、最後にファイルを閉じる処理が必要となりますので、この点に注意してください(特にファイルを閉じることを忘れがちなので注意が必要)。

C言語でのファイルの扱いについての詳細は下記ページで解説していますので、詳しく知りたい方は読んでみてください。

C言語のファイル入出力の解説ページアイキャッチ C言語のファイル入出力について解説

今回の場合は、じゃんけんが終了したタイミングで構造体 SCORE の変数のメンバを更新し、その後 fwrite で、その変数をファイルに出力してやればよいことになります。もちろん、fwrite の実行前に foepnfwrite の実行後に fclose も実行する必要があります。具体的なコードに関しては、後述の セーブ機能とロード機能 で紹介します。

ファイルからデータを復元する

ここまでのコーディングにより、ゲームのデータをファイルに保存することができるようになったことになります。ただし、セーブ機能では、ファイルへの保存するだけでなく、ファイルからデータを復元することも重要となります。要はロード機能が必要です。

この復元は、ファイルからデータを読み込み、その読み込んだデータを変数に保存することで実現可能です。例えば今回であれば、勝ち回数と負け回数をファイルに保存しておき、プログラム起動時にファイルから読み込んだデータを勝ち回数を管理する変数と負け回数を管理する変数(もしくは、これらを管理するメンバ)に保存してやれば、セーブした時点の状態が復元できることになります。

ロード機能の説明図

C言語でのファイルへからのデータの読み込みは下記の3つの関数によって実現することができます。

  • fopen:引数で指定したパスのファイルを開き、開いたファイルのファイルポインターを返却する
  • fread:引数で指定したファイルポインターのファイルの内容を読み込み、引数で指定したアドレスに保存する
  • fclose:引数で指定したファイルポインターのファイルを閉じる

今回の場合は、プログラム開始のタイミングで fread 関数を実行し、ファイルから読み込んだデータを構造体 SCORE の変数のアドレスに保存してやれば良いことになります。こちらに関しても、具体的なコードに関しては後述の セーブ機能とロード機能 で紹介します。

とりあえず、セーブ機能を実現するための流れは上記のようなものになります。

次は、実際にじゃんけんにセーブ機能を実装していきましょう!

スポンサーリンク

セーブ機能無しのじゃんけん

まずは、ベースとするじゃんけんのプログラムのソースコードを紹介していきたいと思います。

ソースコード

ベースとする、じゃんけんのソースコードは下記としたいと思います。

セーブ機能なしのじゃんけん
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 戦績を管理する構造体
typedef struct {
    unsigned int win; // 勝ち回数
    unsigned int lose; // 負け回数
} SCORE;

typedef enum {
    HAND_ROCK,
    HAND_SCISSORS,
    HAND_PAPER,
    HAND_NUM
} HAND;

typedef enum {
    RESULT_DRAW,
    RESULT_YOU_WIN,
    RESULT_YOU_LOSE,
    RESULT_NUM
} RESULT;

void init(void) {
    
    // 乱数の初期化
    srand((unsigned int)time(NULL));
}


HAND getComHand(void) {
    return rand() % HAND_NUM;
}

HAND getPlayerHand(void) {

    HAND player_hand;

    while (1) {
        printf("何を出しますか?( 0:グー / 1:チョキ / 2:パー ):");
        scanf("%d", &player_hand);

        if (player_hand >= HAND_NUM || player_hand < 0) {
            printf("出した手がおかしいです...\n\n");
        } else {
            break;
        }
    }
    printf("\n");

    return player_hand;
}

RESULT getResult(HAND player_hand, HAND com_hand) {
    RESULT result;

    if (player_hand == com_hand) {
        result = RESULT_DRAW;
    } else if (
        (player_hand == HAND_ROCK && com_hand == HAND_SCISSORS) ||
        (player_hand == HAND_SCISSORS && com_hand == HAND_PAPER) ||
        (player_hand == HAND_PAPER && com_hand == HAND_ROCK)
    ) {
        result = RESULT_YOU_WIN;
    } else {
        result = RESULT_YOU_LOSE;
    }

    return result;
}

void printHand(HAND hand) {
    if (hand == HAND_ROCK) {
        printf("グー\n");
    } else if (hand == HAND_SCISSORS) {
        printf("チョキ\n");
    } else if (hand == HAND_PAPER) {
        printf("パー\n");
    }
}

void printResult(RESULT result) {

    printf("結果は・・・");
    if (result == RESULT_DRAW) {
        printf("引き分けです!!!\n");
    } else if (result == RESULT_YOU_WIN) {
        printf("あなたの勝ちです!!!\n");
    } else {
        printf("あなたの負けです...\n");
    }
}

RESULT janken(void) {
    HAND player_hand;
    HAND com_hand;
    RESULT result;

    do {
        // コンピューターが出す手を決定する
        com_hand = getComHand();

        // プレイヤーが出す手の入力を受け付ける
        player_hand = getPlayerHand();
        
        // プレイヤーとコンピューターが出した手を表示する
        printf("あなたが出した手:");
        printHand(player_hand);

        printf("相手が出した手 :");
        printHand(com_hand);

        printf("\n");

        // じゃんけんの結果を判断する
        result = getResult(player_hand, com_hand);

        // じゃんけんの結果を表示する
        printResult(result);
        printf("\n");

    } while (result == RESULT_DRAW);

    return result;
}

int main(void) {

    RESULT result;
    SCORE score = {0, 0};

    init();

    while (1) {
        printf("じゃんけんを始めます!!!\n");
        printf("あなたの戦績: %d勝 %d敗\n", score.win, score.lose);
        printf("終了する場合は Ctrl + C を押してください\n\n");

        result = janken();

        if (result == RESULT_YOU_WIN) {
            score.win++;
        } else if (result == RESULT_YOU_LOSE) {
            score.lose++;
        }
    }
}

このソースコードは、下記ページの 関数化を行う の節で示したコードに対し、セーブ機能を追加しやすいよう変更を加え、さらに戦績を管理するために構造体 SCORE を導入したものになります。そのため、このソースコードについて分からない点があれば、下記ページを参照していただければと思います。

じゃんけんゲームの作り方の解説ページアイキャッチ 【C言語】じゃんけんゲームの作り方(ソースコードの可読性向上についても解説)

スポンサーリンク

実行結果

先ほど紹介したソースコードをコンパイルして実行し、動作を確認してみましょう!

下記のように、戦績は最初 0 勝 0 敗 になっていますが、じゃんけんをプレイしていくと、どんどん勝敗結果に応じて戦績が更新されていくことが確認できると思います(下記の例では、最終的に戦績は 2 勝 4 敗 になっています)。

じゃんけんを始めます!!!
あなたの戦績: 0勝 0敗
終了する場合は Ctrl + C を押してください

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):1

あなたが出した手:チョキ
相手が出した手 :グー

結果は・・・あなたの負けです...

じゃんけんを始めます!!!
あなたの戦績: 0勝 1敗
終了する場合は Ctrl + C を押してください

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):

~略~

結果は・・・あなたの勝ちです!!!

じゃんけんを始めます!!!
あなたの戦績: 2勝 4敗

このようにゲームをプレイして戦績が加算されていったとしても、一度 Ctrl + c (Mac の場合は command + c) でプログラムを終了してしまうと、再度プログラムを起動したときには戦績が 0 勝 0 敗 に戻ってしまいます。

じゃんけんを始めます!!!
あなたの戦績: 0勝 0敗
終了する場合は Ctrl + C を押してください

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):

まぁ、main 関数の先頭で score = {0, 0} を実行しているため、勝ち回数も負け回数も 0 に戻るのは当たり前と言えば当たり前なのですが、この場合は、score にセットするべきデータが存在しない、つまり勝ち回数・負け回数が変数以外に保存されていないので復元しようがないという点が問題です。

前述の通り、プログラムが終了すると変数のデータは消えてしまうため、プログラムが終了してもデータが保持され続ける場所に勝ち回数・負け回数を保存しておく必要があります。その「プログラムが終了してもデータが保持され続ける場所」の代表例がハードディスクや SSD となります。そして、ファイルとして出力することで、それらのハードディスクや SSD にデータを保存することができます。

スポンサーリンク

セーブ機能付きじゃんけん

ということで、次はファイル保存を利用したセーブ機能を実装していきたいと思います。

ソースコード

先ほど示したセーブ機能無しのじゃんけんのソースコードを変更し、セーブ機能を追加した例が下記となります。

セーブ機能ありのじゃんけん
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

// 戦績を管理する構造体
typedef struct {
    unsigned int win; // 勝ち回数
    unsigned int lose; // 負け回数
} SCORE;

typedef enum {
    HAND_ROCK,
    HAND_SCISSORS,
    HAND_PAPER,
    HAND_NUM
} HAND;

typedef enum {
    RESULT_DRAW,
    RESULT_YOU_WIN,
    RESULT_YOU_LOSE,
    RESULT_NUM
} RESULT;

void init(void) {
    
    // 乱数の初期化
    srand((unsigned int)time(NULL));
}

void load(SCORE *score) {
    // 戦績の読み込み
    FILE *fp = fopen("score.dat", "rb");
    if (fp != NULL) {
        fread(score, sizeof(SCORE), 1, fp);
        fclose(fp);
    } else {
        // 未セーブの場合は戦績を初期化
        score->win = 0;
        score->lose = 0;
    }
}

void save(SCORE *score) {
    // 戦績の保存
    FILE *fp = fopen("score.dat", "wb");
    if (fp != NULL) {
        fwrite(score, sizeof(SCORE), 1, fp);
        fclose(fp);
    }
}

HAND getComHand(void) {
    return rand() % HAND_NUM;
}

HAND getPlayerHand(void) {

    HAND player_hand;

    while (1) {
        printf("何を出しますか?( 0:グー / 1:チョキ / 2:パー ):");
        scanf("%d", &player_hand);

        if (player_hand >= HAND_NUM || player_hand < 0) {
            printf("出した手がおかしいです...\n\n");
        } else {
            break;
        }
    }
    printf("\n");

    return player_hand;
}

RESULT getResult(HAND player_hand, HAND com_hand) {
    RESULT result;

    if (player_hand == com_hand) {
        result = RESULT_DRAW;
    } else if (
        (player_hand == HAND_ROCK && com_hand == HAND_SCISSORS) ||
        (player_hand == HAND_SCISSORS && com_hand == HAND_PAPER) ||
        (player_hand == HAND_PAPER && com_hand == HAND_ROCK)
    ) {
        result = RESULT_YOU_WIN;
    } else {
        result = RESULT_YOU_LOSE;
    }

    return result;
}

void printHand(HAND hand) {
    if (hand == HAND_ROCK) {
        printf("グー\n");
    } else if (hand == HAND_SCISSORS) {
        printf("チョキ\n");
    } else if (hand == HAND_PAPER) {
        printf("パー\n");
    }
}

void printResult(RESULT result) {

    printf("結果は・・・");
    if (result == RESULT_DRAW) {
        printf("引き分けです!!!\n");
    } else if (result == RESULT_YOU_WIN) {
        printf("あなたの勝ちです!!!\n");
    } else {
        printf("あなたの負けです...\n");
    }
}

RESULT janken(void) {
    HAND player_hand;
    HAND com_hand;
    RESULT result;

    do {
        // コンピューターが出す手を決定する
        com_hand = getComHand();

        // プレイヤーが出す手の入力を受け付ける
        player_hand = getPlayerHand();
        
        // プレイヤーとコンピューターが出した手を表示する
        printf("あなたが出した手:");
        printHand(player_hand);

        printf("相手が出した手 :");
        printHand(com_hand);

        printf("\n");

        // じゃんけんの結果を判断する
        result = getResult(player_hand, com_hand);

        // じゃんけんの結果を表示する
        printResult(result);
        printf("\n");

    } while (result == RESULT_DRAW);

    return result;
}

int main(void) {

    RESULT result;
    SCORE score = {0, 0};

    init();

    load(&score);

    while (1) {
        printf("じゃんけんを始めます!!!\n");
        printf("あなたの戦績: %d勝 %d敗\n", score.win, score.lose);
        printf("終了する場合は Ctrl + C を押してください\n\n");

        result = janken();

        if (result == RESULT_YOU_WIN) {
            score.win++;
        } else if (result == RESULT_YOU_LOSE) {
            score.lose++;
        }

        save(&score);
    }
}

セーブ機能とロード機能

上記のソースコードで、特にセーブ機能・ロード機能の実現においてポイントになる箇所について説明しておきます。上記のソースコードでは、セーブ機能無しのじゃんけん に比べて save 関数と load 関数を追加しています。

save 関数

save は下記のような関数となっています。

save関数
void save(SCORE *score) {
    // 戦績の保存
    FILE *fp = fopen("score.dat", "wb");
    if (fp != NULL) {
        fwrite(score, sizeof(SCORE), 1, fp);
        fclose(fp);
    }
}

この save 関数では、fopenscore.dat というファイルを wb モード(バイナリモード+書き込みモード)で開き、その後 fwrite 関数で、その開いたファイルに引数で受け取ったアドレスのデータを構造体 SCORE のサイズ分だけ書き込んでいます。そのため、引数に勝ち回数と負け回数をセットした構造体 SCORE の変数のアドレスを渡してやれば、勝ち回数と負け回数が  score.dat に書き込まれることになります。

save関数の動作を説明する図

ちなみに、上の図の 0x0000007B は10進数の 123 をサイズ4バイト&符号なしの16進数表記に変換したものです。同様に、0x0000015B は10進数の 347 を変換したものになります。従って、上の図では勝ち回数 123 ・負け回数 347 がファイルに保存されることになります。

また、この score.dat はプログラムを実行したフォルダ(作業フォルダ)に作成されることになり、このファイルはバイナリモードで開いてデータが書き込まれているためメモ帳などで開いても意味不明な中身となっています。

score.datをメモ帳で開いた様子

ですが、バイナリエディタ等で開けば、下の図のように勝ち回数と負け回数が記録されていることが確認可能です。最初の8個の数字が勝ち回数を表しており、後ろの8個の数字が負け回数を表しています。2つの数字が1セットで1バイトのデータとなっており、エンディアンの関係上、下位バイトから前側に表示されるようになっているので注意してください。

socre.datをバイナリエディタで開いた様子

ちなみに、構造体 SCOREwin と loseunsigned int 型であるため、それぞれ4バイトのデータでファイルに記録されることになります。また、ファイル全体のサイズは8バイトとなります(プログラムを実行する環境によってはサイズが変わる場合もあります)。

ということで、この save 関数を実行すれば、引数に応じた勝ち回数と負け回数がファイルに保存できることになります。

load 関数

そして、その保存されたファイルから勝ち回数と負け回数を読み込む関数が下記の load 関数となっています。

load関数
void load(SCORE *score) {
    // 戦績の読み込み
    FILE *fp = fopen("score.dat", "rb");
    if (fp != NULL) {
        fread(score, sizeof(SCORE), 1, fp);
        fclose(fp);
    } else {
        // 未セーブの場合は戦績を初期化
        score->win = 0;
        score->lose = 0;
    }
}

この関数では、勝ち回数と負け回数の保存先となるファイル、すなわち score.dat を開き、fread 関数でそのファイルから読み込んだデータを引数で与えられたアドレスに保存する処理を行っています。そのため、引数 score に構造体 SCORE の変数のアドレスを指定して実行すれば、その変数のメンバ winlose にファイルから読み込んだデータが書き込まれることになります。

load関数の動作を説明する図

ただ、例えば初めてプログラムを実行した時などのように、score.dat が存在しない場合もありますので、その際には初期値として各メンバに 0 を保存する処理も必要となります。

save 関数と load 関数の実行

これらの関数が用意できれば、後はじゃんけんで結果が決まって勝ち回数・負け回数を更新したタイミングで save 関数を実行するようにすれば、じゃんけんをプレイするたびに勝ち回数・負け回数がファイルに保存されるようになります。

さらに、プログラム起動直後に load 関数を実行すれば、ファイルに保存された勝ち回数・負け回数が復元されることになり、前回プレイ時の状態を引き継ぐことができるようになります。つまり、ゲームのセーブ機能が実現できることになります。

先ほど紹介したソースコードにおいては while ループの中でじゃんけんをプレイするための処理を実行しており、この while ループの最後で save 関数を実行することで、じゃんけんをプレイするたびに勝ち回数と負け回数がファイルに保存されるようにしています。また、while ループに入る前に load 関数を実行することで、前回のプログラム実行時の勝ち回数と負け回数を復元してセーブした状態をロードするようになっています。

saveとloadの実行
int main(void) {

    RESULT result;
    SCORE score = {0, 0};

    init();

    load(&score);

    while (1) {

        /* 略 */

        result = janken();

        if (result == RESULT_YOU_WIN) {
            score.win++;
        } else if (result == RESULT_YOU_LOSE) {
            score.lose++;
        }

        save(&score);
    }
}

スポンサーリンク

実行結果

この章の最後に、先ほど紹介したソースコードのプログラムの実行結果を確認しておきたいと思います。

初回のプログラム起動時は、セーブ機能無しのじゃんけん の時と同様に勝ち回数と負け回数が共に 0 の状態からゲームが開始されます。そして、じゃんけんをプレイしていくと、この勝ち回数と負け回数が更新されていくことになります。

じゃんけんを始めます!!!
あなたの戦績: 0勝 0敗
終了する場合は Ctrl + C を押してください

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):0

あなたが出した手:グー
相手が出した手 :チョキ

結果は・・・あなたの勝ちです!!!

じゃんけんを始めます!!!
あなたの戦績: 1勝 0敗
終了する場合は Ctrl + C を押してください

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):1

あなたが出した手:チョキ
相手が出した手 :グー

結果は・・・あなたの負けです...

じゃんけんを始めます!!!
あなたの戦績: 1勝 1敗
終了する場合は Ctrl + C を押してください

ただし、プログラムを再起動したときの動作が セーブ機能無しのじゃんけん の時とは異なることになります。何回かじゃんけんをプレイした後に Ctrl + c (Mac の場合は command + c) でプログラムを終了し、再度プログラムを起動してみてください。

すると、前回は勝ち回数と負け回数が 0 に初期化されていたのに対し、今回は勝ち回数と負け回数が前回じゃんけんをプレイした時から引き継がれていることが確認できると思います。

じゃんけんを始めます!!!
あなたの戦績: 1勝 1敗
終了する場合は Ctrl + C を押してください

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):

この変化は セーブ機能無しのじゃんけん からのソースコードの変更の効果であり、この効果よりセーブ機能が実現できていることが確認できたことになります。また、プログラムを実行したフォルダに score.dat が作成されており、バイナリエディタ等で開けば勝ち回数と負け回数が記録されていることも確認できると思います。

今回は「じゃんけんの戦績」というシンプルなデータを保存することでセーブ機能を実現しましたが、ファイルに出力するデータの種類を変えることで、もっと別のデータをセーブ機能で保存することができるようになりますし、もちろんじゃんけん以外のゲームにセーブ機能を搭載することも可能です。さらに、ゲームだけでなく、アプリの設定や状態等を「アプリの再起動後・PC の再起動後」に引き継ぐために、今回紹介したファイル保存が活用できるので、このファイル保存のやり方や重要性については是非理解しておきましょう!

課題:セーブデータの改ざん

さて、今回は、ゲームのセーブ機能を一番簡単な「ファイル保存」で実現しました。

このファイル保存によるセーブ機能は簡単に実現できるのですが、課題もあります。その1つがセーブデータの改ざんになります。

今回の例であれば、score.dat の中身を書き換えてしまえば、勝ち回数と負け回数を改ざんすることができてしまいます。今回の例であれば、下記のように書き換えてしまえば、

セーブデータの改ざんが行われた様子1

勝ち回数が 4294967295、負け回数が 0 になってしまいます。

じゃんけんを始めます!!!
あなたの戦績: 4294967295勝 0敗
終了する場合は Ctrl + C を押してください

何を出しますか?( 0:グー / 1:チョキ / 2:パー ):

このように、単純なファイル保存でのセーブ機能は簡単に実現できるものの、データの改ざんが簡単に行われてしまうというデメリットがあります。もちろん、こういった戦績だけでなく、RPG ゲームの場合はステータスを全てマックス値に改ざんすることもできてしまいますし、ソシャゲーの場合はガチャを引かなくても全景品をゲットした状態にデータを改ざんすることもできることになってしまいます。こういったことが行われると他のユーザーのやる気をそぐことになり、ゲームの人気を低下させる原因にもなってしまいます。

そのため、ゲームを開発する上では、こういったセーブデータの改ざんに注意が必要となります。このセーブデータの改ざんの防止策としては下記のようなものが挙げられます。今後、ゲーム関連の記事として下記についての解説ページも紹介していきたいと思います!

  • ハッシュ値(電子署名)
  • 暗号化
  • サーバーでのデータ保存

とりあえず、今回は、プログラム内で変数に保存したデータはプログラム終了時に消えてしまうことと、ファイルを使えばプログラム終了前の状態を、次にプログラムを起動したときに復元することができること(セーブ機能が実現できること)を理解しておいていただければよいと思います!

まとめ

このページでは、ゲームのセーブ機能について解説しました!

変数に保存したデータはプログラム終了時に消えてしまいますが、ファイルに保存しておくことでプログラム終了後もデータを残し続けることができ、これによってセーブ機能が実現できることになります。このゲームのセーブ機能の実現を通じて、ファイルの重要性を理解していただければと思います。

このセーブ機能のように、ゲームを拡張していくことでプログラミングに重要な知識を身に付けていくことが可能です。このサイトでは、今後もゲーム拡張に関連付けて様々な内容を解説していきたいと思いますので楽しみにしておいてください!

同じカテゴリのページ一覧を表示

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です