【ゲーム開発のためのC#入門講座・基礎拡張編】最終課題にチャレンジしてみよう【#10】

2.0_C#基礎拡張編

まずは一言

ここまでありがとうございました&お疲れ様でした!

このあとは基礎強化編

基礎拡張編の次は基礎強化編を予定しています。

えー、まだ基礎編続くの? と思われるかもですが、次の基礎強化編で終わりです。

基礎強化編を終えた先の応用編では、現代プログラミングの真髄であり、数多のプログラミング初心者の心を折ってきたオブジェクト指向に挑むことになります。

基礎強化編ではそのオブジェクト指向を少しでもスムーズに理解するために、これまで学習してきたことのより深い理解を目指します。きちんとした土台を構築することで、難敵オブジェクト指向を一緒に乗り切りましょう!

それでは、これまで学習してきたすべてをぶつける最終課題にチャレンジ……の前に、これまで学んだことの補足をご紹介しておきましょう。

実は二種類あるインクリメント・デクリメント

forループで学習した、現在の値に±1する演算子、インクリメント・デクリメント。

実はそれぞれ「前置きバージョン」と「後ろ置きバージョン」の二種類があります。

演算子内容
++変数(命令を実行する前に)現在の値に1足す++number
–変数(命令を実行する前に)現在の値から1引く–number
変数++(命令を実行してから)現在の値に1足すnumber++
変数–(命令を実行してから)現在の値から1引くnumber–

内容を確認してみると、(命令を実行する前に)(命令を実行してから)というところだけ違いがありますね。

これだけだと何のことかわからないので、実際に例を見てみましょう。

public class Hello{
    public static void Main(){
        int number = 0;
        
        // 処理を実行する前に=前置きバージョン
        System.Console.WriteLine(++number);
        
        // 処理を実行してから=後ろ置きバージョン
        System.Console.WriteLine(number++);
    }
}
出力エリア

1
1

あれ、1ずつ加算しているにもかかわらず、両方とも結果が同じになってしまいました。

バグっているんでしょうか? Microsoftにクレームを入れにいきましょう!

と、そういう訳ではないんですね。

最後にもう一度、変数の値を表示してみましょう。

public class Hello{
    public static void Main(){
        int number = 0;
        
        // 処理を実行する前に=前置きバージョン
        System.Console.WriteLine(++number);
        
        // 処理を実行してから=後ろ置きバージョン
        System.Console.WriteLine(number++);
        System.Console.WriteLine(number);
    }
}
出力エリア

1
1
2

きちんと+1された値が設定されているのを確認できました。

どういうことかというと、ここで(命令を実行する前に)と(命令を実行してから)が関わってきます。

今回、インクリメントを「System.Console.WriteLine」という関数の引数として使いましたね。これは「引数として渡された値を出力エリア(コンソール)に表示しなさい」という命令でした。

// 引数として渡された値を出力エリアに表示する命令
System.Console.WriteLine(number);

前置きバージョンの場合はこの命令を実行する前に計算が実行されます。

// 前置きバージョンは、WriteLineする前に計算が実行される
System.Console.WriteLine(++number);
​
// つまりやっていることは↓と同じ
number += 1;
System.Console.WriteLine(number);

反対に後ろ置きバージョンの場合はこの命令を実行した後に計算が実行されます。

// 後ろ置きバージョンは、WriteLineした後に計算が実行される
System.Console.WriteLine(number++);
​
// つまりやっていることは↓と同じ
System.Console.WriteLine(number);
number += 1;

これが前置きバージョンと後ろ置きバージョンの違いです。

改めて先程のプログラムを確認してみましょう。

public class Hello{
    public static void Main(){
        int number = 0;
        
        // 前置きバージョンだから+1してからWriteLineする
        System.Console.WriteLine(++number);
        
        // 後ろ置きバージョンだからWriteLineしてから+1する
        System.Console.WriteLine(number++);
        
        // +1されたあとだから2が表示される
        System.Console.WriteLine(number);
    }
}
出力エリア

1
1
2

どうして一回目と二回目が両方1になるのか、理由がわかりましたね。

どういう時に使い分けるものなの?

前置きバージョンの使い方

例えばRPGやミニゲームなどでコンボ表示を行いたいとします。

もしコンボが成功したら「コンボに+1してから画面に表示」しますよね。

この場合は前置きバージョンを使うと、1行でコードを書くことができます。

public class Hello{
    public static void Main(){
        int combo = 0;
        
        // 前置きインクリメントを使わないと2行必要だが、
        combo += 1;
        System.Console.WriteLine(combo);
        
        // 前置きインクリメントを使えば1行で書ける
        System.Console.WriteLine(++combo);
    }
}

後ろ置きバージョンの使い方

こっちは具体的にこういう時に便利! っていうほどのシチュエーションはないです。

ただ、前置きができる算術演算子は他にないんですよね。

なので、この後ろ置きバージョンの方が視覚的にはしっくりくるように感じます。感覚的な話で申し訳ないですが。

// 他の算術演算子も基本的には何かの後ろについているから
number -= 1;

// 前に+記号がついてるのって何か見慣れない
++number;

// なので、インクリメントも後ろの方がしっくりくる気がする!
number++;

そのため、forループの増減処理や、命令とセットでは使わない単純な加算・減算など、汎用的な処理として使いたい場合は後ろ置きバージョンの方がわかりやすいかなと個人的には思います。

// forループの増減処理や、
for (int i = 0; i < 10; i++)
{
    System.Console.WriteLine(i);
}

// 単純な加算・減算なら後ろ置きの方がいいかも
int result = 0;
result++;

絶対使わないとだめ?

結論からいうと別に使わなくてもいいです。

先程もお伝えした通り、やっていることは下記と同じです。なので、使わなくても実装には困らないです。

// 前置きバージョンは、WriteLineする前に計算が実行される
System.Console.WriteLine(++number);

// つまりやっていることは↓と同じ
number += 1;
System.Console.WriteLine(number);
// 後ろ置きバージョンは、WriteLineした後に計算が実行される
System.Console.WriteLine(number++);

// つまりやっていることは↓と同じ
System.Console.WriteLine(number);
number += 1;

ただ、2行必要なコードを1行で書けると多少時間短縮になること、他の人のコードを見た時に「なんだこれ」という状況に陥らないようにしたかったので、このタイミングで紹介させてもらいました。

使ってみようかなと思えるようになったら使ってみる、そのぐらいの感覚でよいと思います。

最終課題

それでは改めて、最終課題にチャレンジしてみましょう。

今回は基礎拡張編で学んだ知識を活かし、パーティ全員のHPが満タンになるまで回復する関数を作ってみましょう。

それでは、グッドファイト!(KOFアテナ風

仕様

・関数「Main」にint型の配列「playersHP」を作成する。
 値は、25, 7, 1。

・関数「Main」にint型の配列「playersMaxHP」を作成する。
 値は50, 34, 25。

・関数「Heal」を作成する。
 引数 :なし
 戻り値:int型
 処理 :値5を返す

・関数「AllHeal」を作成する。
 引数 :int型配列「playersHP」、int型配列「playersMaxHP」
 戻り値:int型配列
 処理 :playersHPのデータ数分、下記のループ処理を実行する。
     ループ処理が終わったら、playersHPを戻り値として返す。
 <ループ処理>
 playersHP[i]がplayersMaxHP[i]以上になるまで関数「Heal」の戻り値を加算する。その後、playersHP[i]がplayersMaxHP[i]を越えていた場合、playersHP[i]にplayersMaxHP[i]の値を代入する(上限を超えないよう補正)。

・関数「Main」で関数「AllHeal」を呼び出す。
 引数にはplayersHPとplayersMaxHPを設定する。
 戻り値はplayersHPに代入し、forループでplayersHPのすべての値を出力エリアに表示する。

テンプレート
public class Hello{
    public static void Main(){
        // ↓ここに仕様を満たすコードを書いていこう!
        
        // ↑ここまで
    }
    // ↓ここに仕様を満たす関数を定義していこう!
    
    // ここまで
}

答え合わせ

public class Hello{
    public static void Main(){
        // ↓ここに仕様を満たすコードを書いていこう!
        int[] playersHP = {
            25, 
            7, 
            1
        };
        
        int[] playersMaxHP = {
            50, 
            34, 
            25
        };
        
        playersHP = AllHeal(playersHP, playersMaxHP);
        for (int i = 0; i < playersHP.Length; i++)
        {
            System.Console.WriteLine(playersHP[i]);
        }
        // ↑ここまで
    }
    // ↓ここに仕様を満たす関数を定義していこう!
    public static int Heal()
    {
        return 5;
    }
    
    public static int[] AllHeal(int[] playersHP, int[] playersMaxHP)
    {
        for (int i = 0; i < playersHP.Length; i++) 
        {
            while (playersHP[i] < playersMaxHP[i])
            {
                playersHP[i] += Heal();
            }
            if (playersHP[i] > playersMaxHP[i])
            {
                playersHP[i] = playersMaxHP[i];
            }
        }
        return playersHP;
    }
    // ここまで
}
出力エリア

50
34
25

最後に

改めてお疲れ様でした!

最後に作ってもらったものにMPの概念を導入すれば、ドラクエの「まんたん」コマンドが再現できそうですね。

さて、ここで次に繋がる謎をひとつ残しておきたいと思います。

今回関数「AllHeal」は戻り値として、回復後のパーティ全員のHPが格納された配列を返しました。それを代入することで、関数「Main」のint型配列「playersHP」を更新していますね。

// ↓上書きして更新している
playersHP = AllHeal(playersHP, playersMaxHP);

public static int[] AllHeal(int[] playersHP, int[] playersMaxHP)
{
    ……回復処理……
    // ↓回復後のHPが格納された配列を返している
    return playersHP;
}

ですが、実は何も返さなかったとしても、同じ結果になります

public class Hello{
    public static void Main(){
        int[] playersHP = {
            25, 
            7, 
            1
        };
        
        int[] playersMaxHP = {
            50, 
            34, 
            25
        };
        
        // ↓戻り値がなくなったので代入処理を削除 
        AllHeal(playersHP, playersMaxHP);
        for (int i = 0; i < playersHP.Length; i++)
        {
            System.Console.WriteLine(playersHP[i]);
        }
    }

    public static int Heal()
    {
        return 5;
    }
    
    // ↓戻り値のデータ型をvoidに変更
    public static void AllHeal(int[] playersHP, int[] playersMaxHP)
    {
        for (int i = 0; i < playersHP.Length; i++) 
        {
            while (playersHP[i] < playersMaxHP[i])
            {
                playersHP[i] += Heal();
            }
            if (playersHP[i] > playersMaxHP[i])
            {
                playersHP[i] = playersMaxHP[i];
            }
        }
        // 何も返さないのでコメントアウト
        //return playersHP;
    }
    // ここまで
}
出力エリア

50
34
25

結果、変わってないですよね。

えー、何で!? 意味わかんないんだけど!?

と思った方、この謎は次の基礎強化編で解き明かされることになります。

よろしければ引き続きお付き合いくださいませ。

それでは、また次の記事でお会いしましょう!

Posted by 夕目紅