C++によるプログラミング入門13
値を戻す関数の例

 今日は、懸案だった「大魔王とヒーローが対決するゲーム」の改良です。そこで、前回紹介した「値を戻す関数」を使います。
 前回は「値を戻す関数」を勉強しました。(繰り返しますが、数学の「関数」とC++の「関数」は違います。)値を戻す(返す)ということは、普通、C++では「報告」に使われます。関数というものはプログラムのどこかで使われる(これを、業界では「呼び出される」と言うのでした)のですが、その際に、使っている場所(業界用語では「呼び出している関数」となります。関数を使うのも関数であるからです。少し、ややこしいですね。)に、値を「戻す」からです。なんだかややこしいですね。でも、難しいことではないのです。
 もう一度、前回の例を見てみましょう。

//ex3.cpp
#include <iostream>
using namespace std;

//関数func()を定義します。
int func()
{
    cout<<"こんにちは。私はコンピュータです。"<<endl;
    return 1;   //整数値1を戻す
}

int main()
{
    int d;      //整数変数dの宣言(「整数dを使うよ」という意味)    
    d = func(); //考え込むと不思議な文ですが、次のような意味になります。
                //まず、func()の中身が実行される。
               
//そして、この関数から戻された値1がdに代入される。
    cout << "func()から戻された値=" << d << endl;  //dの値を出力
}

 ここで、funcはもちろん関数ですが、mainも関数なのでした。(mainは特別な関数です。)それで、上のプログラムは「関数mainが関数funcを呼び出している」ということができます。呼び出された関数funcは呼び出した関数mainに値1を戻しています。(「戻す」と言っても、もともとあったものを返還するという意味ではないのでした。上のプログラムではdに1が代入されることを「戻す」というのです。)
 ここで、もし、dに1が代入されていなければ、(上の例では単純すぎますが)なんらかの原因でエラーがおこったと判断できます。このような戻り値は何かの処理の成功・不成功の報告(エラーチェック)にもなるのです。

 上の例のfuncはいつも1を戻すように作ってありますが、いろいろな状況に応じて戻す値を変える方が普通です。その場合は、エラーチェックというより、プログラムの進行の細かいチェックになるわけです。つまり、呼び出した側で戻り値を見て次の作業をすすめるようにすることができるのです。
 入門9では「大魔王とヒーローが対決するゲーム」(Game.cpp)をつくりましたが、Game.cppは、対決が一度きりなので、ゲームの体裁になっていませんでした。これを「決着がつくまで対決を繰り返すプログラム」に改造することは今までの知識だけでできるはずです。元気のある人は自分でも考えてみてください。 そこまで元気はないという人も、下のサンプルを見る前に、入門9のプログラムの流れをもう一度見ておいてください。


 まず、ヒントを書き、次に答を書きますが、人によっては答を先に見てから、その後にヒントを読んだ方がわかりやすいかもしれません。読み方は各自工夫してください。

 さて、私なら次のように考えます。まず、なるべく入門9のコードを使って改造するのが得策ですから、対決には、Taiketu_basyoの関数taiketuをうまく使いたいと思います。(あとで少し修正します。)これをループの中に入れて

    while(決着がつかない間){
    taiketu();
    }

や、

   while(1){
    taiketu();
    決着がついたかどうか調べ、ついていればループをぬける
    }

とするのがよいでしょう。(入門11を参照)
 「決着」は大魔王かヒーローのパワーが負になった場合です。これは、少し複雑なので、下の方の無限ループの形を採用しましょうか。(もちろんそれほど本質的な違いはありませんが。)このコードを使って、「何度も繰り返し対決する関数」を書くのです。それはkurikaesi_taiketuとし、クラスTaiketu_basyoに加えることにします。
 しかし、決着がついたかどうかを判断するためには大魔王やヒーローのパワーを知る必要があります。パワーはどちらのクラスでもpowerとよばれているものですが、private(プライベート、つまり非公開)にしてあるので、Taiketu_basyoのkurikaesi_taiketuの中で直接使うことはできません。(そのように使えないようにprivateにしたのです。なぜ、そんな不便なことをしたかは、入門6を見てください。)
 そこで、クラス「ヒーロー」にも「大魔王」にも、powerを報告するだけの関数getPowerを付け足します。これはpowerを戻り値にするだけの関数なので、クラスの定義の中に書いてしまいます。すると、例えば、クラス「ヒーロー」は、次のようになります。

//ヒーローつまり英雄ですね
class Hero
{
    int power;                       //ヒーローのパワー
public:
    Hero() : power(100){}            //ヒーローのコンストラクタ、ヒーローのパワーをはじめ100とした
    int getPower(){ return power; }  //「powerを戻す」関数
    void kougeki_suru(int n);        //「ヒーローが攻撃する」関数
    void kougeki_sareru(int n);      //「ヒーローが攻撃される」関数
};


こうすると、例えば、youというHeroオブジェクトがあった場合、

    int temp;
    temp = you.getPower();

と書けば、tempにそのときのyouのパワーが代入されるのです。大魔王にも同様の関数を付け足せば、kurikaesi_taiketu()の定義は次のようになるでしょう。(youがHeroで、buがMaoです。)

void Taiketu_basyo::kurikaesi_taiketu()
{
    int temp;
    while(1){
        taiketu();
        temp = you.getPower();
        if(temp < 0){
            cout << "\n大魔王の前に正義のヒーローは敗れました。" << endl;
            break;
        }
        temp = bu.getPower();
        if(temp < 0){
            cout << "\n正義のヒーローの前に大魔王は敗れました。" << endl;
            break;
        }
    }
}

(ここで、「\n」というものを使いました。これは「改行」という意味です。つまり、\nがある箇所で、出力が、一度改行されるのです。これは、出力を見やすくするために入れてみました。なお、「\n」は、これで1文字と考えられることになっています。)

あるいは、上の関数は、you.getPower()などがそのまま戻り値に変わるということを考えれば、tempを使わずに

void Taiketu_basyo::kurikaesi_taiketu()
{
    while(1){
        taiketu();
        if(you.getPower() < 0){
            cout << "\n大魔王の前に正義のヒーローは敗れました。"<<endl;
            break;
        }
        if(bu.getPower() < 0){
            cout << "\n正義のヒーローの前に大魔王は敗れました。" << endl;
            break;
        }
    }
}

としても同じです。こちらの方がすっきりしていますよね。

 それから、taiketu 関数も少し変えます。攻撃はヒーローと大魔王が交互に行いますが、taiketuではいつもヒーローが先です。ここで、もしヒーローが先に大魔王を倒してしまえば(つまり大魔王のパワーが負になってしまえば)、次の大魔王の攻撃は行われないようにしなければなりません。そこで、taiketu内のヒーローの攻撃の後で、bu.getPower()を呼び出し、もし大魔王のパワーが負なら残りの部分(つまり大魔王の攻撃の部分)を実行する前に、taiketuを終了してしまうように改造します。
 
ところで、関数を終了させる命令は「return 値;」です。これは、「戻す値の設定」と説明してきましたが、この命令で「値を戻す」ことになるので、つまり「関数を終了させる」ことにもなるのです。)ただし、taiketuは値を戻さない関数なので「;」をすぐ後に付けて「return;」が、taiketuの「関数を終了させる命令」になります。これを利用することにします。
 また、ヒーローや大魔王が攻撃した際にパワーを使いすぎて死んでしまうこともあるでしょうから、これについてもチェックをいれるよう改造します。(以上の詳細は下のサンプルをみてください。returnはbreakによくにていることがわかると思います。ループなどからぬけるのがbreakで関数からぬけるのがreturnなのです。)

 あといくつか細かいこともあります。まず、入門9のGame.cでは、ユーザに攻撃をうながすときに

cout << "攻撃に使うパワー(100以下の数値)を入力してください。>> ";

としていましたが、今度は、攻撃のたびにエネルギーが減っていくので、これを

cout << "攻撃に使うパワー(" << you.getPower() << "以下の数値)を入力してください。>> ";

とします。ちょっと見にくいですが、まず、「攻撃に使うパワー(」を出力し、次に、「you.getPower()(つまり、getPowerの戻り値)」を出力し、「以下の数値)を入力してください。>> 」を出力するのです。これによって、現在のヒーローのエネルギーが表示されるようになります。
 それから、ヒーローも大魔王もkougeki_suru関数やkougeki_sareru関数でパワーがつきると何か言ってから倒れるのですが、さらにその後に

    cout<<"ヒーローは倒れました。"<<endl;

などという文があります。これは単純なプログラムを少しでもその時点でゲームらしくするために付け加えた文でしたが、実際には、自分で自分のことを「倒れました」などとコメントするのはおかしいですよね。プログラム的にもこの部分は蛇足なので削ります。そのかわり、「倒れた」というコメントはkurikaesi_taiketuの中に入れました。
 また、プログラムの構造とは関係ないのですが、kougeki_suruなどで自分のパワーをユーザに報告するのがあまりに唐突なので、

    cout << "現在のパワーは" << power << "だ。" << endl;

    cout << "(現在のパワーは" << power << "になった。)" << endl;

と変えることにしました。

 以上をまとめるとGame.cppは、次のように改良されます。

//Game2.cpp
#include <iostream>
#include <cstdlib>  //乱数のために必要
#include <ctime>    //乱数のために必要
using namespace std;

//ヒーローつまり英雄ですね
class Hero
{
    int power;                       //ヒーローのパワー
public:
    Hero() : power(100){}            //ヒーローのコンストラクタ、ヒーローのパワーをはじめ100とした
    int getPower(){ return power; }  //「powerを戻す」関数
    void kougeki_suru(int n);        //「ヒーローが攻撃する」関数
    void kougeki_sareru(int n);      //「ヒーローが攻撃される」関数
};

//「ヒーローが攻撃する」関数の定義、nは攻撃に使うパワー
void Hero::kougeki_suru(int n)
{
    cout << "悪党め。正義の攻撃を受けてみよ。" << endl;
    cout << "どか〜ん!!!" << endl;
    power -= n;  //攻撃したのでパワーを減らします。
    //もちろん本当にパワーをどこかにやったのではなく、単に数値を減らすだけでした。

    //もしパワーがまだ0以上ならよし、もし、負になったら、使いすぎで負けとした。
    //以下のifとelseはそういう意味です。
    if(power >= 0){
        cout << "(現在のパワーは" << power << "になった。)" << endl;
    }
    else{
        cout << "しまった!パワーを使いすぎた。" << endl;
        cout << "もうおしまいだ!!!がくっ。" << endl;
    }
}

//「ヒーローが攻撃される」関数の定義、nは攻撃されて減らされるパワーの量
void Hero::kougeki_sareru(int n)
{
    cout << "くそっ。悪党の攻撃も当たることがあるのか。" << endl;
    power -= n;  //攻撃されてパワーが減る。
    //以下の仕組みはkougeki_suru()とほぼ同じ。
    if(power >= 0){
        cout << "(現在のパワーは" << power << "になった。)" << endl;
    }
    else{
        cout << "やられた。がくっ。" << endl;
    }
}

//大魔王。実はヒーローとほとんど同じ構造です
class Daimao
{
    int power;                       //大魔王のパワー
public:
    Daimao() : power(100){}          //大魔王のコンストラクタ、大魔王のはじめのパワーは100とした
    int getPower(){ return power; }  //「powerを戻す」関数
    void kougeki_suru(int n);        //「大魔王が攻撃する」関数
    void kougeki_sareru(int n);      //「大魔王が攻撃される」関数
};

//「大魔王が攻撃する」関数の定義、nは攻撃に使うパワー
void Daimao::kougeki_suru(int n)
{
    cout << "大魔王様の一撃をうけてみよ。" << endl;
    cout << "どか〜ん。" << endl;
    power -= n;  //攻撃したのでパワーを減らします。
    //もちろん本当にパワーをどこかにやったのではなく、単に数値を減らすだけでした。

    //もしパワーがまだ0以上ならよし、もし、負になったら、使いすぎで負けとした。
    //しかし、大魔王のパワーは秘密なので書かない。
    if(power < 0){
        cout << "しまった!!!パワーを使いすぎた。" << endl;
        cout << "む、む、む。無念だ。がくっ。" << endl;
    }
}

//「大魔王が攻撃される」関数の定義。nは攻撃されて減らされるパワーの量
void Daimao::kougeki_sareru(int n)
{
    cout<< "くそっ。正義の味方の攻撃も当たることがあるのか。" <<endl;
    power -= n;  //攻撃されてパワーが減る。
    //以下の仕組みはkougeki_suru()とほぼ同じ。大魔王のパワーは秘密なので書かない。
    if(power < 0){
        cout << "やられた。がくっ。" << endl;
    }
}

//対決場所のクラス(ヒーローや大魔王が「もの」なら、対決場所も「もの」ですね。)
class Taiketu_basyo
{
    Daimao bu;         //対決場所にいる大魔王bu!
    Hero you;          //対決場所にいるヒーローyou!
    int bu_no_basho;   //大魔王のいる場所(1〜5の数値)、これは後で決まる
    int you_no_basho;  //ヒーローのいる場所(1〜5の数値)、これは後でユーザが決める
public:
    Taiketu_basyo();            //対決場所のコンストラクタ、定義はクラス宣言の外で
    void taiketu();             //「ヒーローと大魔王が1回対決する」関数
    void kurikaesi_taiketu();   //「繰り返し対決する」関数
};

//対決場所のコンストラクタの定義
Taiketu_basyo::Taiketu_basyo()
{
    srand( (unsigned)time( NULL ) ); //乱数の初期化
    bu_no_basho = rand() % 5 + 1;    //rand()% 5は0〜4の中のでたらめな数(乱数)
    //したがってrand() % 5 + 1は1〜5のうちのどれか(次回に説明します)、これで大魔王の場所が決まった
    cout << "あなたと大魔王ブーとの決戦です。\n" << endl;
    cout << "大魔王ブーは座標1〜5のどこかに潜んでいます。" << endl;
    cout << "あなたもどこかに身を潜めてください。" << endl;
    cout << "身を潜める座標(1〜5の数値)を入力してください。" << endl;
    cin >> you_no_basho;             //ヒーローの場所を入力
}

void Taiketu_basyo::taiketu()   //「ヒーローと大魔王が対決する」関数の定義
{
    int iti, kougeki;           //一時的に必要な「位置」と「攻撃量」の変数(いれもの)
    cout << "さあ、あなたの攻撃です。" << endl;
    cout << "攻撃の位置(1〜5の数値)を入力してください。>> ";
    cin >> iti;                //攻撃する位置をitiに代入
    cout << "攻撃に使うパワー(" << you.getPower() << "以下の数値)を入力してください。>> ";
    cin >> kougeki;            //攻撃に使うパワーをkougekiに代入
    cout << endl;
    you.kougeki_suru(kougeki); //ヒーローyouの攻撃
                               //攻撃量はkougekiに代入された値、この値だけヒーローのパワーは減る
    //taiketu()の改造した場所
    if(you.getPower() < 0){ //攻撃のしすぎで、ヒーローのパワーが負になれば、、、
        return;             //returnはその関数の実行を終了させる
    }
    if(bu_no_basho == iti){    //もしbu_no_bashoとitiが一致したら(つまり、ねらったところに大魔王がいたら)
        bu.kougeki_sareru(kougeki * 2);  //攻撃を受けます。このとき大魔王は
                                         //ヒーローが使ったパワーの2倍を消耗します。
                                         //kougeki * 2とはkougekiの2倍という意味です。
        //taiketu()の改造した場所
        if(bu.getPower() < 0){  //攻撃されて、大魔王のパワーが負になれば、、、
            return;             //returnはその関数の実行を終了させる
        }
    }
    else{
        cout << "あなたの攻撃ははずれたようです。" << endl;  //はずれたら、何も起こらない
    }
    cout << endl;
    cout << "大魔王の攻撃です。" << endl;
    cout << "(エンターキーを押してください。)" << endl;
    cin.sync();       //cinをフラッシュ
    cin.get();        //1時ストップ
    iti = rand() % 5 + 1;       //大魔王の攻撃の位置は乱数(ここでは1〜5までのうちのどれか)で決まる
    kougeki = rand() % 100 + 1; //大魔王の攻撃の量も乱数(1〜100までのうちのどれか)で決まる
    bu.kougeki_suru(kougeki);   //大魔王buの攻撃(大魔王のパワーが減る)
    //taiketu()の改造した場所
    if(bu.getPower() < 0){  //攻撃のしすぎで大魔王のパワーが負になれば、、、
        return;             //returnはその関数の実行を終了させる
    }
    if(you_no_basho == iti){              //当たったら
        you.kougeki_sareru(kougeki * 2);  //攻撃される
    }
    else{                                 //はずれたら
        cout << "大魔王ブーの攻撃ははずれたようです。" << endl;
    }
}

void Taiketu_basyo::kurikaesi_taiketu()
{
    while(1){
        taiketu();
        if(you.getPower() < 0){
            cout << "\n大魔王の前に正義のヒーローは敗れました。"<<endl;
            break;
        }
        if(bu.getPower() < 0){
            cout << "\n正義のヒーローの前に大魔王は敗れました。" << endl;
            break;
        }
    }
}

int main()
{
    Taiketu_basyo dokoka;  //対決場所Dokokaの生成
                           //ここでコンストラクタが働き、ヒーローと大魔王の位置が決められる。

    dokoka.kurikaesi_taiketu();      //dokokaの対決
}

Fig.1 Game2.exe

 まだまだ、不満はあります。たとえば、パワー切れで攻撃しても「どか〜ん」といってしまうところや、大魔王の攻撃が完全にランダムだというようなところです。でも、疲れましたね。今日はこの辺にしましょう。ゲーム作りがこの講座の目的ではないので(といっていいわけをしているような、、、)、このプログラムをおもしろくする方向の改造はみなさんにお任せして、以後はC++の技法を学ぶ方向に進むことにします。


 この「講座」は全くの初心者を想定して書かれていますが、この辺は初心者には厳しいかもしれません。そうでもない、という人は特別才能のある人でしょう。難しいと思っている人ががっかりすることはありません。そういう人は、上のプログラムをとにかく実行してみることと、ソースをプリントアウトして部分的にでも理解するようときどき眺めてみることをおすすめします。1週間くらい眺めていると(真剣に考えるのではなく、ただ、眺めているだけでよいと思います)そのうちわかってくると思います。まあ、そういうことです。^^)
目次のページ
前のページ 後のページ