★ゲームを作ろう8(模擬戦闘:ファイル分割編)★


以前、1対1の模擬戦闘プログラムを作りました。

これを少しだけ改良し、ファイルを分割して作ってみましょう。


長めのプログラムを作る際には、前もって色々と考えておく事が重要です。

適当に作り始めると途中で破たんすることが多いです。

特に、複数のメンバーで制作する際には、情報を共有することが大事になります。

メンバーそれぞれが違う考えで作ったプログラムを組み合わせてみると、全く動かないプログラムが出来上がります。

大きな枠組みから考え始め、細かい部分(関数)まで考え、イメージを共有出来るまで作り始めない事です。


では、模擬戦闘の大枠を考えます。

1対1で戦う

ターン制で戦うが、どちらが先に攻撃するかはランダム

どちらかのヒットポイントが0以下になれば戦闘を終了する

次に必要なデータを考えます。

プレイヤーと敵の構造体
 メンバ変数は、名前、ヒットポイント、攻撃力、防御力の4つ

名前(半角20文字まで)
 ・プレイヤー名は自分で入力する
 ・敵は文字を組み合わせて適当に付ける(3〜8文字)
   敵の名前の頭文字は大文字にする

ヒットポイント
 プレイヤーも敵も 200〜300

攻撃力
 プレイヤーも敵も 15〜25

防御力
 プレイヤーも敵も 10〜15

次にプログラム全体の大まかな流れを考えます。

※main関数から呼び出す関数をイメージします。

1.プレイヤーの初期化
   名前、ヒットポイント、攻撃力、防御力の設定

  ↓

2.敵の初期化
   名前、ヒットポイント、攻撃力、防御力の設定

  ↓

3.ステータスの表示

  ↓

4.戦闘

  ↓

5.ステータスの表示

次にもう少し細かく考えます。

1.プレイヤーの初期化
役割
 名前、ヒットポイント、攻撃力、防御力の設定

関数 InitializePlayer
 引 数 Player構造体のポインタ
 戻り値 なし

ファイル
 player.cpp

2.敵の初期化
役割
 名前、ヒットポイント、攻撃力、防御力の設定

関数 InitializeEnemy
 引 数 Enemy構造体のポインタ
 戻り値 なし

ファイル
 enemy.cpp

3、5.ステータスの表示
役割
 プレイヤーのステータス表示

関数 ShowPlayerStatus
 引 数 Player構造体のconstポインタ
 戻り値 なし

ファイル
 status.cpp
役割
 敵のステータス表示

関数 ShowEnemyStatus
 引 数 Enemy構造体のconstポインタ
 戻り値 なし

ファイル
 status.cpp

4.戦闘
役割
 プレイヤーと敵の戦闘

関数 battle
 引 数 プレイヤー構造体のポインタ
     敵構造体のポインタ
 戻り値 なし

ファイル
 battele.cpp

これで準備が整ったと思わないでください。

内容が一番アバウトな関数は battle.cpp です。

今回の戦闘は、

 ターン制で戦うが、どちらが先に攻撃するかはランダム

というルールがあります。

プログラム的に考えると↓のようになります。

if(rand() % 2) {

  /* プレイヤーの攻撃 */

  /* 敵の攻撃 */
}
else {

  /* 敵の攻撃 */

  /* プレイヤーの攻撃 */
}

これを実装(プログラムを実際に組む)しようと考えると、それぞれの攻撃も関数にした方が楽です。

そこで戦闘の部分をもう少し細かく考えます。

battle関数から呼ばれる関数
役割
 プレイヤーから敵への攻撃

関数 PlayerAttack
 引 数 プレイヤーと敵の構造体ポインタ
 戻り値 1 敵を倒した
     0 倒せなかった

ファイル
 player.cpp
役割
 敵からプレイヤーへの攻撃

関数 EnemyAttack
 引 数 プレイヤーと敵の構造体ポインタ
 戻り値 1 プレイヤーを倒した
     0 倒せなかった

ファイル
 enemy.cpp

とりあえず、ここまで考えました。


上で考えた情報を元にプログラムを組んでいきます。

※途中経過は省き、1ファイルごと一気に作っていきます。


<sample program 162-01>

/* main.h */

#pragma once

#define NAME_MAX 21

typedef struct {
    char name[NAME_MAX];
    int hp;
    int atk;
    int def;
} Player;

typedef struct {
    char name[NAME_MAX];
    int hp;
    int atk;
    int def;
} Enemy;

#include "player.h"
#include "enemy.h"
#include "status.h"
#include "battle.h"

main.h では、各ファイルで使用する構造体を宣言します。

また、main.cpp において各ファイルで作成された関数を呼び出すため、他のヘッダファイルをインクルードしています。


<sample program 162-02>

/* main.cpp */

#include "main.h"

#include <stdlib.h>
#include <time.h>

int main(void)
{
    srand((unsigned int)time(NULL));

    Player player;

    Enemy enemy;

    InitializePlayer(&player);
    InitializeEnemy(&enemy);

    ShowPlayerStatus(&player);
    ShowEnemyStatus(&enemy);

    Battle(&player, &enemy);

    ShowPlayerStatus(&player);
    ShowEnemyStatus(&enemy);

    return 0;
}

main.cpp では、乱数の種をセット、構造体の実体作成を行います。

その後、プログラム全体の流れを書いていきます。


<sample program 162-03>

/* player.h */

#pragma once

#include "main.h"

void InitializePlayer(Player* pPlayer);

int PlayerAttack(Player* pPlayer, Enemy* pEnemy);

player.h では、構造体を使用するため main.h をインクルードします。

また、main.cpp や battle.cpp で使用する関数のプロトタイプ宣言を書きます。


<sample program 162-04>

/* player.cpp */

#include "player.h"

#include <stdio.h>
#include <stdlib.h>

void InitializePlayer(Player* pPlayer)
{
    printf("プレイヤー名を決めてください。\n(半角20文字以内)\n");
    
    scanf("%s", pPlayer->name);

    pPlayer->hp = rand() % 101 + 200;

    pPlayer->atk = rand() % 11 + 15;

    pPlayer->def = rand() % 6 + 10;
}

int PlayerAttack(Player* pPlayer, Enemy* pEnemy)
{
    int damage;

    printf("%sの攻撃!\n", pPlayer->name);

    getchar();

    damage = (pPlayer->atk - pEnemy->def) + rand() % 10;

    printf("%sは%sに%dのダメージを与えた!\n", pPlayer->name, pEnemy->name, damage);

    pEnemy->hp -= damage;

    getchar();

    if (pEnemy->hp < 0) {
        pEnemy->hp = 0;
    }

    if (pEnemy->hp <= 0) {
        printf("%sを倒した!\n", pEnemy->name);
        getchar();
        return 1;
    }

    return 0;
}

player.cpp では、scanfやprintf、getcharを使うため<stdio.h>を、randを使うため<stdlib.h>をインクルードします。

InitializePlayer関数で、プレイヤー名を入力し、各パラメーターの初期化を行っています。

PlayerAttack関数では、プレイヤーが敵に攻撃するプログラムを作りました。

ダメージには0〜9ポイントの補正が付くようにしてあります。

※プログラム内の101、200、11、15、6、10などの数値は #define で定義した方が良かったですね・・・


<sample program 162-05>

/* enemy.h */

#pragma once

#include "main.h"

void InitializeEnemy(Enemy* pEnemy);

int EnemyAttack(Player* pPlayer, Enemy* pEnemy);

enemy.h では、構造体を使用するため main.h をインクルードします。

また、main.cpp や battle.cpp で使用する関数のプロトタイプ宣言を書きます。


<sample program 162-06>

/* enemy.cpp */

#include "enemy.h"

#include <stdio.h>
#include <stdlib.h>

void InitializeEnemy(Enemy* pEnemy)
{
    int nameLen = rand() % 5 + 3;

    int i;

    pEnemy->name[0] = rand() % 26 + 'A';

    for (i = 1; i < nameLen; i++) {
        pEnemy->name[i] = rand() % 26 + 'a';
    }

    pEnemy->name[i] = '\0';

    pEnemy->hp = rand() % 101 + 200;

    pEnemy->atk = rand() % 11 + 15;

    pEnemy->def = rand() % 6 + 10;
}

int EnemyAttack(Player* pPlayer, Enemy* pEnemy)
{
    int damage;

    printf("%sの攻撃!\n", pEnemy->name);

    getchar();

    damage = (pEnemy->atk - pPlayer->def) + rand() % 10;

    printf("%sは%sに%dのダメージを与えた!\n", pEnemy->name, pPlayer->name, damage);

    pPlayer->hp -= damage;

    getchar();

    if (pPlayer->hp < 0) {
        pPlayer->hp = 0;
    }

    if (pPlayer->hp <= 0) {
        printf("%sは倒れた!\n", pPlayer->name);
        getchar();
        return 1;
    }

    return 0;
}

enemy.cpp では、printfやgetcharを使うため<stdio.h>を、randを使うため<stdlib.h>をインクルードします。

InitializeEnemy関数で、敵の名前を生成し、各パラメーターの初期化を行っています。

※生成の仕組みは皆さんで考えてみてください。

※すごく適当な生成法ですが、結構読めそうな名前が出来るんですよ(笑

EnemyAttack関数では、敵がプレイヤーに攻撃するプログラムを作りました。


<sample program 162-07>

/* status.h */

#pragma once

#include "main.h"

void ShowPlayerStatus(const Player* pPlayer);

void ShowEnemyStatus(const Enemy* pEnemy);

status.h では、構造体を使うため main.h をインクルードしています。

また、main.cpp や battle.cpp で使用する関数のプロトタイプ宣言を書きます。


<sample program 162-08>

/* status.cpp */

#include "status.h"

#include <stdio.h>

void ShowPlayerStatus(const Player* pPlayer)
{
    printf("%s's Status\n", pPlayer->name);
    printf("HP = %3d : ", pPlayer->hp);
    printf("ATK = %2d : ", pPlayer->atk);
    printf("DEF = %2d\n\n", pPlayer->def);
}

void ShowEnemyStatus(const Enemy* pEnemy)
{
    printf("%s's Status\n", pEnemy->name);
    printf("HP = %3d : ", pEnemy->hp);
    printf("ATK = %2d : ", pEnemy->atk);
    printf("DEF = %2d\n\n", pEnemy->def);
}

status.cpp では、printfを使うため<stdio.h>をインクルードしています。

それぞれのステータスを表示する関数の本体を書きます。


<sample program 162-09>

/* battle.h */

#pragma once

#include "main.h"

#include "status.h"

void Battle(Player *pPlayer, Enemy *pEnemy);

battle.h では、構造体を使うため main.h をインクルードしています。

戦闘中にプレイヤーと敵のステータスを表示しなければなりませんので、status.h もインクルードしています。

また、main関数で使用される Battle関数のプロトタイプ宣言を書きます。


<sample program 162-10>

/* battle.cpp */

#include "battle.h"

void Battle(Player *pPlayer, Enemy *pEnemy)
{
    for (;;) {

        if (rand() % 2) {

            if (PlayerAttack(pPlayer, pEnemy)) {
                break;
            }

            if (EnemyAttack(pPlayer, pEnemy)) {
                break;
            }
        }
        else {

            if (EnemyAttack(pPlayer, pEnemy)) {
                break;
            }

            if (PlayerAttack(pPlayer, pEnemy)) {
                break;
            }
        }

        ShowPlayerStatus(pPlayer);
        ShowEnemyStatus(pEnemy);
    }
}

battle.cpp では、乱数を使って先攻後攻を決めています。

戦闘が終わるまでループを繰り返し、1ターンごとにステータスを表示します。


<実行結果>

プレイヤー名を決めてください。
(半角20文字以内)
Asyuma
Asyuma's Status
HP = 267 : ATK = 25 : DEF = 11

Zrd's Status
HP = 285 : ATK = 18 : DEF = 14

Zrdの攻撃!
ZrdはAsyumaに8のダメージを与えた!

Asyumaの攻撃!
AsyumaはZrdに15のダメージを与えた!

Asyuma's Status
HP = 259 : ATK = 25 : DEF = 11

Zrd's Status
HP = 270 : ATK = 18 : DEF = 14

Zrdの攻撃!
ZrdはAsyumaに11のダメージを与えた!

Asyumaの攻撃!
AsyumaはZrdに15のダメージを与えた!

Asyuma's Status
HP = 248 : ATK = 25 : DEF = 11

Zrd's Status
HP = 255 : ATK = 18 : DEF = 14

・
・
・
途中省略
・
・
・

Zrdの攻撃!
ZrdはAsyumaに10のダメージを与えた!

Asyumaの攻撃!
AsyumaはZrdに16のダメージを与えた!

Asyuma's Status
HP =  75 : ATK = 25 : DEF = 11

Zrd's Status
HP =   9 : ATK = 18 : DEF = 14

Zrdの攻撃!
ZrdはAsyumaに12のダメージを与えた!

Asyumaの攻撃!
AsyumaはZrdに13のダメージを与えた!

Zrdを倒した!

Asyuma's Status
HP =  63 : ATK = 25 : DEF = 11

Zrd's Status
HP =   0 : ATK = 18 : DEF = 14

続行するには何かキーを押してください・・・

一気に全部書きましたが、実際には少しずつ調整しながら作ります。

ただし、最初にしっかり考えることは重要です。

プログラムに慣れてくると、最初からプログラムを書きがちですが、逆に慣れれば慣れるほど前もって考える時間が増える方が望ましいです。

まずは全体像をイメージし、少しずつダウンサイジングしていき、関数まで落とし込むよう考えてください。

そこからプログラムを作った方が、手戻りも少なく、しっかりとしたプログラムが作れると思います。

どのように関数に分割し、どのファイルに書くべきか色々考えながら作りましょう。


次へ

戻る

目次へ