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

2022-01-133.0_C#基礎強化編

まずは一言

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

いよいよ応用編突入

これにて基礎編は完結です。

いよいよ応用編、オブジェクト指向に挑んでいくことになります。現代のプログラミングの真髄であり、数多のプログラミング初心者の心を折ってきたこのオブジェクト指向を乗り越えられるかどうかが大きなポイントになります。このオブジェクト指向さえ何とかなるなら、この後のことはどんなことでも乗り越えていけるでしょう。

それでは、これまで学習してきたすべてをぶつける最終課題にチャレンジ……の前に、型にまつわる補足事項、型変換型推論について学んでおきましょう。

型変換って?

型変換とは、文字通り異なる型に変換する技術のことです。

型が違うということは、扱えるデータ種類が違うということ。なので、C#では型が合っていないデータを変数に代入することはできません。

public class Hello{
    public static void Main(){
        int score = 5;
        // ↓intをstring型の変数に代入しようとするとエラーになるよ!
        string scoreText = score;
        System.Console.WriteLine(scoreText);
    }
}
出力エリア

Compilation failed: 1 error(s), 0 warnings
Main.cs(5,28): error CS0029: Cannot implicitly convert type int' tostring’

けれど、int型のデータを小数も扱えるfloat型に置き換えたり、int型のデータを文字列として結合したり、別の型として扱いたい場面もあります。

ここで役立つのが型変換という技術です。

まずは最も初歩的な「文字列に変換する」型変換を学習しましょう。

①文字列への型変換

文字列への型変換は非常に簡単です。

すべてのデータは、実は自分自身を文字列に変換する技術「ToString」を持っています。

早速見てみましょう。

public class Hello{
    public static void Main(){
        int score = 5;
        string scoreText = score.ToString();
        System.Console.WriteLine(scoreText);
    }
}
出力エリア

5

無事、文字列に変換した上で、string型の変数に代入することができました。

このように「データ.ToString()」という命令を実行すると、そのデータを文字列に変換することができます

変数だけでなく、計算結果などの値に対しても実行することができます。

public class Hello{
    public static void Main(){
        string scoreText = (5 + 5).ToString();
        System.Console.WriteLine(scoreText);
    }
}
出力エリア

10

便利ですね!

「あれ、関数なら関数名(データ)という書き方になるんじゃないの? 何でデータの後ろに関数がつくの?」ともし疑問に思われる方がいたら、非常に鋭いです。

これこそがオブジェクト指向で学ぶ技術メソッドと呼ばれるものです。

詳細は応用編で学習しますので、今は「データ.ToString()」で文字列に変換できるんだなということだけご理解下さい。

なお、配列などの「複数データの集合体」に対して「ToString()」を実行した場合、実データではなくデータ種類が文字列に変換されます。全部まとめて文字列化してくれる訳ではないので、そこはご注意下さい。

public class Hello{
    public static void Main(){
        int[] scores = {
            0,
            5
        };
        string scoresText = scores.ToString();
        System.Console.WriteLine(scoresText);
    }
}
出力エリア

System.Int32[]

②文字列を数値に変換

今度は"1″や"2″などの、文字列だけれど中身は数値というデータを、intやfloatなどの型に変換する方法です。

こちらは「データ型.Parse(文字列)」という変換用の関数が各データ型に用意されています

早速試してみましょう。

public class Hello{
    public static void Main(){
        string score = "5";
        int scoreInt = int.Parse(score) + 5;
        float scoreFloat = float.Parse(score) + 0.25f;
        System.Console.WriteLine(scoreInt);
        System.Console.WriteLine(scoreFloat);
    }
}
出力エリア

10
5.25

無事、文字列をそれぞれの型に変換した上で計算することができました。

こちらも便利ではあるのですが、ひとつだけ注意点があります。

それは変換できない文字列を引数にするとエラーになってしまうということ。

public class Hello{
    public static void Main(){
        // ↓数値じゃなくて文字にすると……
        string score = "A";
        int scoreInt = int.Parse(score);
        System.Console.WriteLine(scoreInt);
    }
}
出力エリア

Unhandled Exception:
System.FormatException: Input string was not in a correct format.
at System.Number.ThrowOverflowOrFormatException (System.Boolean overflow, System.String overflowResourceKey) [0x00020] in <12b418a7818c4ca0893feeaaf67f1e7f>:0
at System.Number.ParseInt32 (System.ReadOnlySpan`1[T] value, System.Globalization.NumberStyles styles, System.Globalization.NumberFormatInfo info) [0x0001c] in <12b418a7818c4ca0893feeaaf67f1e7f>:0
at System.Int32.Parse (System.String s) [0x00019] in <12b418a7818c4ca0893feeaaf67f1e7f>:0
at Hello.Main () [0x00008] in /workspace/Main.cs:5

ぎゃー、とんでもない長いエラーが出てきました!

こんな風にエラーが発生してしまうので、ちゃんと変換できるものを引数として渡してあげているか、気を付けてあげましょう。

実は変換できるかどうか確認してくれる方法もあるのですが、それはまだ解説していない内容を含むので、いずれ解説したいと思います。

③値型を別の値型に変換する方法

値型を別の値型に変換する方法は、①②の合わせ技で実現することができます。

public class Hello{
    public static void Main(){
        int score = 5;
        // ↓int型をfloat型に変換してから計算してるよ!
        float scoreFloat = float.Parse(score.ToString()) + 0.25f;
        System.Console.WriteLine(scoreFloat);
    }
}
出力エリア

5.25

一旦文字列に変換し、それを「データ型.Parse」の引数に設定することで目的のデータ型に変換できる、という訳ですね。

メモリに優しく、ということで様々な型が存在する値型ですが、そのせいで異なる型同士で何かをするには型を合わせなければならない(型変換しなければならない)というネックがあります。

この型変換の技術を駆使して、そのデメリットは少しでも帳消しにしてやりましょう。

型推論

ついにこの型推論をご紹介できる時がきました。

今まで型を明示しなければならないということを常々言ってきましたが、実はC#では言語機能として「コードの内容から恐らくこの型がふさわしいだろう」と自動で型を判定してくれる機能があります。

それが型推論です。

この機能を使えば、これはこの型で、こっちはこの型でと、いちいち型を個別に指定する必要がなくなります。すべてvarという疑似的な型名を記述すれば済むのです。

早速試してみましょう。

public class Hello{
    public static void Main(){
        // 値が「5」なのでint型と推論
        var score = 5;
        // 値が「2.5f」なのでfloat型と推論
        var scoreFloat = 2.5f;
        // 値がToString()の結合なのでstring型と推論
        var text = score.ToString() + scoreFloat.ToString();
        System.Console.WriteLine(text);
    }
}
出力エリア

52.5

神!!!!!

これを使ってしまうといちいち型の記載を使い分けていたのがアホらしくなってしまいますね。

ただし、変数「score」の型が緩めのint型で判定されていることからわかるように、厳密なメモリ運用を行える訳ではありません。また、命令内容から推論を行えない場合も利用不可です。

決して万能という訳ではない、という点はご理解下さい。

それと、忘れないでいただきたいのは、あくまでこれは型を推論して代わりに設定してくれる機能だということ。C#という言語の基本はこれまで学習してきた通り、様々な型を明示的に指定するものです。

この原理原則を理解していただく必要があるので、これまでこの存在を伏せてきました。

が、今の皆さんは値型・参照型・スタックメモリ・ヒープメモリなど、C#の基礎はすべてマスターした立派なプログラマーです。

理解した上で使うならば非常に便利な機能なので、今後は積極的にこの型推論機能を活用してみるのもよいでしょう。

なお、自分は職場規定の関係から型を明示することが習慣になってしまっているのと、解説としては型が明示的な方がわかりやすいだろうという判断から、今後も型を明示して記述していきます。ご了承下さいませ。

最終課題

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

今回は基礎編の集大成ということで、基礎学習編で作成した「君と殴り合う戦闘シミュレーション」を「3VS1」のボスバトル仕様で作成してみましょう。

基本的な枠組みはこちらで作成しましたので、足りない部分を埋めてください。

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

前提

変数「playersHP」と変数「playersAttack」は、
同じインデックスであれば同一キャラのステータスと見なす。
<例>
playersHP[0] <= player1の体力
playersAttack[0] <= player1の攻撃力

仕様

・関数「PlayersTurn」に下記の処理を実装してください。
 ループ処理にて、引数「playersHP」の値が0より大きかったら、
 引数「enemyHP」から
 引数「playersAttack」の同じインデックスの値を引く。
 (生存しているパーティキャラが1回ずつ攻撃しているようなイメージ)
  引数「enemyHP」 を戻り値として返す。
・関数「EnemyTurn」に下記の処理を実装してください。
 引数「playersHP」のインデックス0から引数「enemyAttack」の値を引く。
 もしインデックス0の値が0以下ならインデックス1を、
 インデックス1の値が0以下ならインデックス2から値を引く。
 (パーティのうち、生存しているキャラをボスが攻撃するようなイメージ)
・関数「IsGameOver」に下記の処理を実装してください。
 引数「playersHP」の値がすべて0以下の場合はtrueを、
 それ以外の場合はfalseを返す。

テンプレート
public class Hello{
    public static void Main(){
        int[] playersHP = {
            25, // 1人目のHP
            30, // 2人目のHP
            21  // 3人目のHP
        };
        
        int[] playersAttack = {
            10, // 1人目のAttack
            12, // 2人目のAttack
            6   // 3人目のAttack
        };
        
        int enemyHP = 200;
        int enemyAttack = 8;
        
        bool isGameOver = false;
        
        while (true)
        {
            enemyHP = PlayersTurn(playersHP, playersAttack, enemyHP);
            System.Console.WriteLine("enemyHP:" + enemyHP.ToString());
            if (enemyHP <= 0)
            {
                break;
            }
            
            EnemyTurn(playersHP, enemyAttack);
            for (int i = 0; i < playersHP.Length; i++)
            {
                string target = "playerHP" + (i + 1).ToString();
                System.Console.WriteLine(target + ":" + playersHP[i].ToString());
            }
            if (IsGameOver(playersHP))
            {
                break;
            }
        }
        
        if (enemyHP <= 0)
        {
            System.Console.WriteLine("勝利!");
        }
        else
        {
            System.Console.WriteLine("ゲームオーバー");
        }
    }
    
    public static int PlayersTurn(int[] playersHP, int[] playersAttack, int enemyHP)
    {
        // ↓ここに処理を実装しよう!
        
        // ここまで
    }
    
    public static void EnemyTurn(int[] playersHP, int enemyAttack)
    {
        // ↓ここに処理を実装しよう!
        
        // ここまで
    }
    
    public static bool IsGameOver(int[] playersHP)
    {
        // ↓ここに処理を実装しよう!
        
        // ここまで
    }
}

答え合わせ

public class Hello{
    public static void Main(){
        int[] playersHP = {
            25, // 1人目のHP
            30, // 2人目のHP
            21  // 3人目のHP
        };
        
        int[] playersAttack = {
            10, // 1人目のAttack
            12, // 2人目のAttack
            6   // 3人目のAttack
        };
        
        int enemyHP = 200;
        int enemyAttack = 8;
        
        bool isGameOver = false;
        
        while (true)
        {
            enemyHP = PlayersTurn(playersHP, playersAttack, enemyHP);
            System.Console.WriteLine("enemyHP:" + enemyHP.ToString());
            if (enemyHP <= 0)
            {
                break;
            }
            
            EnemyTurn(playersHP, enemyAttack);
            for (int i = 0; i < playersHP.Length; i++)
            {
                string target = "playerHP" + (i + 1).ToString();
                System.Console.WriteLine(target + ":" + playersHP[i].ToString());
            }
            if (IsGameOver(playersHP))
            {
                break;
            }
        }
        
        if (enemyHP <= 0)
        {
            System.Console.WriteLine("勝利!");
        }
        else
        {
            System.Console.WriteLine("ゲームオーバー");
        }
    }
    
    public static int PlayersTurn(int[] playersHP, int[] playersAttack, int enemyHP)
    {
        // ↓ここに処理を実装しよう!
        for (int i = 0; i < playersHP.Length; i++)
        {
            if (playersHP[i] <= 0)
            {
                continue;
            }
            enemyHP -= playersAttack[i];
        }
        return enemyHP;
        // ここまで
    }
    
    public static void EnemyTurn(int[] playersHP, int enemyAttack)
    {
        // ↓ここに処理を実装しよう!
        for (int i = 0; i < playersHP.Length; i++)
        {
            if (playersHP[i] <= 0)
            {
                continue;
            }
            playersHP[i] -= enemyAttack;
            break;
        }
        // ここまで
    }
    
    public static bool IsGameOver(int[] playersHP)
    {
        // ↓ここに処理を実装しよう!
        for (int i = 0; i < playersHP.Length; i++)
        {
            if (playersHP[i] > 0)
            {
                return false;
            }
        }
        return true;
        // ここまで
    }
}

細かい実装の差異は気にしないでOKです。

出力結果の最後が下記のようになっていれば成功です。

出力エリア

playerHP1:-7
playerHP2:-2
playerHP3:5
enemyHP:-2
勝利!

どうやら死闘だったようですね!

最後に

改めてお疲れ様でした!

無事、3VS1のボスバトルを実装できましたね。ここに敵の情報も入れれば、パーティ同士のバトルも実装することができそうです。

ただ、今やっている方法だと実は少し非効率的だったりします。

これから学ぶオブジェクト指向を駆使すれば、もっと楽に、もっと直感的に、もっと自由にプログラムを作ることができるようになります。その時こそ、Unityでのゲーム開発の扉が開かれることでしょう。

ただし、何回も言って申し訳ないですが、このオブジェクト指向が難敵です。

そもそも概念自体が何かふわっとしてるんですよね。言葉も「指向」とか曖昧だし。

指向なのに技術なの? 何なの? みたいな気持ちになりますが、誰もが通る道です。そして世に多くのプログラマーがいるように、多くの人が乗り越えてきた道でもあります。

ここまでしっかりレベル上げをして、しっかりアイテム(知識)も補充してきた皆さんならきっと大丈夫!

少しでも旅のお供として頑張れるよう、自分も引き続きやれるだけの工夫を凝らして皆さんに情報と理解をお届けしたいと思います。

それでは、今度は応用編でお会いしましょう!

Posted by 夕目紅