C#をどこまでわかっているのかを、自分で確認するためのサンプル

イメージするゲームの最終完成形

RPGゲームを考えます(ドラクエのようなもの)
登場するのは、勇者や魔術師、魔導士、戦士などプレイヤー側と、スライムやゴブリン、ショゴス(クリーチャー)などがいます
勇者は色々な武器、防具、薬草、毒消草などの道具を駆使し、冒険に出かけます
敵は、雑魚キャラ、中ボス、ボスキャラ等、多彩なものがいて、飽きることなくゲームを勧められます
また、レアアイテムもシーンにはあります
プレイヤーは敵を攻撃します
敵は攻撃を受けると反応します(やられた〜)
プレイヤーは攻撃力と防御力、ライフや特殊技、確率による会心の一撃を繰り出す、毒状態、眠った状態、ライフを味方に渡すなど、色々な動作を繰り出し、ワクワクする状況を生み出します

はい、いきなりこのようなゲームが作れるのでしょうか?
答え・・・できませんね

多数の予算、多数の開発者、長い開発期間、こんな条件が揃わないといけません

では、いきなり作れないのがもうダメなのでしょうか?

違いますね
小さい仕組みを理解して、作ってコツコツとスキルを積み重ねるのが大切です

小さなシーンで練習(シーンのイメージを描けますか?)

将来の発展で複数のユニークなキャラクタが登場する予定なので、小さいシーンでもオブジェクト指向で考えていくようにします

プレイヤー1人と敵1匹から考えましょう
次のような感じのゲームにします

最初のコード(プレイヤーと敵をシーンに登場させる)

イラストも見ていただいてイメージを膨らませる練習をしましょう

Player player = new Player();
Enemy shoggoth = new Enemy();
public class Player
{
}

public class Enemy
{
}

攻撃のコード(敵は攻撃を受けている)

では、攻撃についてコードを書いてみましょう
敵が攻撃を受けたイメージをコードにしてみます
いかがでしょうか?
Take Damageダメージを受けていると考えます

Player player = new Player();
Enemy shoggoth = new Enemy();

shoggoth.TakeDamage();
public class Player
{
}

public class Enemy
{
    public void TakeDamage()
    {
        Console.WriteLine("攻撃を受けている");
    }
}

実行結果

攻撃を受けている

攻撃のコード(プレイヤーが攻撃している)

攻撃側(プレイヤー)についても考えてみましょう
プレイヤーは攻撃を与えていますので、次のようにイメージしていいでしょう

Player player = new Player();
Enemy shoggoth = new Enemy();

player.Attack();
shoggoth.TakeDamage();
public class Player
{
    public void Attack()
    {
        Console.WriteLine("攻撃を与えている");
    }
}

public class Enemy
{
    public void TakeDamage()
    {
        Console.WriteLine("攻撃を受けている");
    }
}

実行結果

攻撃を与えている
攻撃を受けている

敵のライフのコード(敵はダメージを受けるとライフが減る)

もう少しイメージを膨らませましょう

敵は生まれた時(シーンに登場した時)には、100のライフがあります

攻撃を受けるとライフが10減ります

Player player = new Player();
Enemy shoggoth = new Enemy();

player.Attack();
shoggoth.TakeDamage();
public class Player
{
    public void Attack()
    {
        Console.WriteLine("攻撃を与えている");
    }
}

public class Enemy
{
    int life = 100;

    public void TakeDamage()
    {
        life -= 10;
        Console.WriteLine($"攻撃を受けている 残りライフ {life}");
    }
}

実行結果

攻撃を与えている
攻撃を受けている 残りライフ 90

プレイヤーが敵に攻撃している

どうでしょうか?

プレイヤーの攻撃と、敵がダメージを受けるってことなのに、一切つながりがありません

もう少しイメージを膨らませましょう
次のようなコードを考えます

プレイヤーは、敵(ショゴス)に攻撃を与えた

player.Attack(shoggoth);

イメージを頭に描けますか?

このまま、このコードに変更してもエラーになりますよね
EnemyクラスのAttackメソッドに引数がないからです

インテリセンスに支援してもらいましょう

はい、Enemy型の引数で渡すようですね

参照について

難しい話になりますが、渡されるのはインスタンスなので参照型になります
参照先が渡されるだけなので、値型のようにコピーが渡されるわけではありません
なので、何か変更を加えるとnewで作成したインスタンス自体が変更されるようになります

次に、Attackメソッド側では渡された引数を使ってエネミーにダメージを与えることにします
引数には、プレイヤーが攻撃する敵が入っています
敵はEnemy型なので、TakeDamageメソッドを持っています
引数で渡された敵のメソッドを呼ぶことにすれば、その敵自体のライフが減ることになるのです

public void Attack(Enemy enemy)
{
    Console.WriteLine("攻撃を与えている");
    enemy.TakeDamage();
}

インテリセンスの自動作成のメソッド側の引数名はenemyに変更しています
Mainメソッドのショゴスが攻撃を受けるコードは削除しています

Player player = new Player();
Enemy shoggoth = new Enemy();

player.Attack(shoggoth);
public class Player
{
    public void Attack(Enemy enemy)
    {
        Console.WriteLine("攻撃を与えている");
        enemy.TakeDamage();
    }
}

public class Enemy
{
    int life = 100;

    public void TakeDamage()
    {
        life -= 10;
        Console.WriteLine($"攻撃を受けている 残りライフ {life}");
    }
}

実行結果

攻撃を与えている
攻撃を受けている 残りライフ 90

どうでしょう?
イメージができていますか?

小さなシーンで練習(登場物を増やす)

一旦、学習は終わりました
変数、代入、メソッド(引数に独自型を取る)、クラス、オブジェクト指向が盛り込まれていました

石を新たに増やす

あれれって思った方もいるでしょう

次のイラストを見てください
プレイヤーが石に攻撃してしまった時、どうしましょう
敵と同じくライフが減る?でいいでしょうか?
違いますよね

今回のイメージでは、ゴールドを出すこととしましょう

最初に石をシーンに登場させましょう

Player player = new Player();
Enemy shoggoth = new Enemy();
Rock rock = new Rock();

player.Attack(shoggoth);
public class Player
{
    public void Attack(Enemy enemy)
    {
        Console.WriteLine("攻撃を与えている");
        enemy.TakeDamage();
    }
}

public class Enemy
{
    int life = 100;

    public void TakeDamage()
    {
        life -= 10;
        Console.WriteLine($"攻撃を受けている 残りライフ {life}");
    }
}

public class Rock
{

}

では、攻撃したらゴールドを出すようにしましょう

インテリセンスにも手伝ってもらってコードを作成していきましょう
エラーをなくすように進めていくようにしましょう

Player player = new Player();
Enemy shoggoth = new Enemy();
Rock rock = new Rock();

player.Attack(shoggoth);
player.Attack(rock);
public class Player
{
    public void Attack(Enemy enemy)
    {
        Console.WriteLine("攻撃を与えている");
        enemy.TakeDamage();
    }

    internal void Attack(Rock rock)
    {
        Console.WriteLine("石に力を加えている");
        rock.TakeDamage();
    }
}

public class Enemy
{
    int life = 100;

    public void TakeDamage()
    {
        life -= 10;
        Console.WriteLine($"攻撃を受けている 残りライフ {life}");
    }
}

public class Rock
{
    internal void TakeDamage()
    {
        Console.WriteLine("ゴールドを吐き出した");
    }
}

実行結果

攻撃を与えている
攻撃を受けている 残りライフ 90
石に力を加えている
ゴールドを吐き出した

うまくいきましたね!
Attackメソッドという同じ名前のメソッドが複数あることが許されることをオーバーロードと言います
ルールとして、引数の型は違う必要があります

複雑になるのは嫌だ

でも、この調子で登場物を増やしていくと・・・

何か気づきましたか?

プレイヤーのメソッドが無限に増えていきます!!
最初に言ったように多くのキャラクターや物をシーンに増やしていきたいのに・・・

Playerクラスのメソッドが増殖しなくても同じ処理ができればなあ・・・

はい、C#にはあります

まず、ダメージを受けることは敵にも石にも当てはまりますので次のようなコードを追加しておきます

ダメージを受けるメソッドを持ちなさい(実装しなさい)という命令のコードです

共通の動作を作る

これがインターフェースです

ポイントとして、まとめた形をクラスにしない(継承にしない)ことです
なぜなら、継承の条件としてis aの関係、xxはooである関係になっていなければならないからです

[

敵は「ダメージを受けるである」や 石は「ダメージを受けるである」はおかしな日本語ですね

public interface IDamagable
{
    void TakeDamage();
}

Player player = new Player();
Enemy shoggoth = new Enemy();
Rock rock = new Rock();

player.Attack(shoggoth);
player.Attack(rock);
public interface IDamagable
{
    void TakeDamage();
}

public class Player
{
    public void Attack(Enemy enemy)
    {
        Console.WriteLine("攻撃を与えている");
        enemy.TakeDamage();
    }

    internal void Attack(Rock rock)
    {
        Console.WriteLine("石に力を加えている");
        rock.TakeDamage();
    }
}

public class Enemy
{
    int life = 100;

    public void TakeDamage()
    {
        life -= 10;
        Console.WriteLine($"攻撃を受けている 残りライフ {life}");
    }
}

public class Rock
{
    internal void TakeDamage()
    {
        Console.WriteLine("ゴールドを吐き出した");
    }
}

共通の動作を敵と石に強制されるようにする

前回のRockクラスでのメソッド自動作成の時、アクセス修飾子がinternalになっていたので変更の必要がありましたが、これをpublicに前回変更していれば、今回では変更なかったですね

Playerクラスに共通で使えるコードを追加する

3つ目のAttackメソッド(IDamagable型を引数に取っています)
この場合、メソッドを追加できます
メソッドのオーバーロードと言います

Player player = new Player();
Enemy shoggoth = new Enemy();
Rock rock = new Rock();

player.Attack(shoggoth);
player.Attack(rock);
public interface IDamagable
{
    void TakeDamage();
}

public class Player
{
    public void Attack(Enemy enemy)
    {
        Console.WriteLine("攻撃を与えている");
        enemy.TakeDamage();
    }

    internal void Attack(Rock rock)
    {
        Console.WriteLine("石に力を加えている");
        rock.TakeDamage();
    }

    public void Attack(IDamagable damageObj)
    {
        Console.WriteLine("ダメージを与えた");
        damageObj.TakeDamage();
    }
}

public class Enemy : IDamagable
{
    int life = 100;

    public void TakeDamage()
    {
        life -= 10;
        Console.WriteLine($"攻撃を受けている 残りライフ {life}");
    }
}

public class Rock : IDamagable
{
    public void TakeDamage()
    {
        Console.WriteLine("ゴールドを吐き出した");
    }
}

実行結果

攻撃を与えている
攻撃を受けている 残りライフ 90
石に力を加えている
ゴールドを吐き出した

あれ、「ダメージを与えた」が表示されません
どうも優先順位が低いようです

ダメージを受けるグループなら実行できるようにメソッドを1つにする

では、個別に呼び出していた最初の2つのAttackメソッドのオーバーロードを削除してみましょう

Player player = new Player();
Enemy shoggoth = new Enemy();
Rock rock = new Rock();

player.Attack(shoggoth);
player.Attack(rock);
public interface IDamagable
{
    void TakeDamage();
}

public class Player
{
    public void Attack(IDamagable damageObj)
    {
        Console.WriteLine("ダメージを与えた");
        damageObj.TakeDamage();
    }
}

public class Enemy : IDamagable
{
    int life = 100;

    public void TakeDamage()
    {
        life -= 10;
        Console.WriteLine($"攻撃を受けている 残りライフ {life}");
    }
}

public class Rock : IDamagable
{
    public void TakeDamage()
    {
        Console.WriteLine("ゴールドを吐き出した");
    }
}

実行結果

ダメージを与えた
攻撃を受けている 残りライフ 90
ダメージを与えた
ゴールドを吐き出した

イメージ

クラス図

ポリモーフィズムを使うことで、EnemyではPlayerから向いていた矢印が今度は、IDamagableを向くようになりました

これを依存性逆転と言います

抽象に依存すると言ったらこれのことです

最初のコード

最後のコード

シーケンス図

最初のコード

最後のコード

基本形のサンプル

このサンプルでは、IExampleInterfaceというインターフェースを定義し、ExampleClassクラスでそれを実装しています。その後、ExampleクラスにExecuteMethodメソッドを定義し、IExampleInterfaceを引数に取り、SomeMethodを実行しています。

Mainメソッド(トップレベルステートメントで作っているので、1行目と2行目を指します)では、ExampleClassのインスタンスを作成し、Example.ExecuteMethodを呼び出しています。このようにして、インターフェースを引数に取るメソッドを使うことができます。

ExampleClass exampleClass = new ExampleClass();
// サンプルでは、Exampleクラスは静的クラスとしているのでnew演算子でインスタンスの作成は行なっていません
Example.ExecuteMethod(exampleClass); 

public interface IExampleInterface
{
    void SomeMethod();
}

public class ExampleClass : IExampleInterface
{
    public void SomeMethod()
    {
        Console.WriteLine("これは ExampleClass の SomeMethod です");
    }
}

public static class Example
{
    public static void ExecuteMethod(IExampleInterface exampleInterface)
    {
        exampleInterface.SomeMethod();
    }
}