【Unity】作るものから学ぶオブジェクト指向

プログラミングの学習を進める上で、初学の次に出てくるのがオブジェクト指向です
ただ、一所懸命に理解しようとしても、難しくてよくわからないと思ってる方も多いと思います
指向というくらいですから、暗記をしようと思うとまず無理です
ではどうすればいいのでしょうか
考え方なので、実際に使う必要が出てくるときに一緒に学ぶのが一番です

ゲームの制作で考える

次のような相撲の力士の情報からゲームを作ることになったとしましょう
格闘ゲームとか好きな人はピンとくると思いますが、そうでない人でも大丈夫です
サッカーゲームでもバスケットでも野球でも根本は一緒ですから

サンプルデータ

力士個人のパラメータ(項目)がありますね

  • 年齢、出身地、身長、体重、得意技
  • 力、技、速さ、体力

何か、感じましたか
どの力士もパラメータの項目は一緒ですが、値が違います

プログラミング

初学で学んだ範囲で作ってみる

単純化するために、取り組みは力のパラメータが大きい方が勝つこととしましょう
すると、こんなプログラムになるでしょうか?

string player1Name = "松鳳山";
int Player1Power = 60;

string player2Name = "玉鷲";
int Playe2Power = 50;

if (Player1Power > Playe2Power)
{
    Console.WriteLine(player1Name + "の勝ち");
}
else if (Player1Power < Playe2Power)
{
    Console.WriteLine(player2Name + "の勝ち");
}
else
{
    Console.WriteLine("とりなおし");
}

結果

松鳳山の勝ち

良さそうですね
しっかりと、変数の宣言、代入、if文で条件分岐といった文法が使えています

でも、ゲームらしくするためには、取り組み相手を選びたいですよね
どういうふうに変更しましょう?

力士を3人に増やして、そのうちの2人を選んで対戦することにします
コードを見てください
松鳳山の勝利パターンまでしか書いていませんが、なかなか大変です

というか、増やすことが辛くなります
作ることができる気がしませんし、やる気も湧きません

string player1Name = "松鳳山";
int Player1Power = 60;

string player2Name = "玉鷲";
int Playe2Power = 50;

string playe32Name = "大翔丸";
int Playe3Power = 30;


Console.WriteLine("1人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select1 = int.Parse(Console.ReadLine());

Console.WriteLine("2人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select2 = int.Parse(Console.ReadLine());

if (select1 == 1 && select2 == 2)
{
    Console.WriteLine(player1Name + "の勝ち");
}
else if (select1 == 1 && select2 == 3)
{
    Console.WriteLine(player1Name + "の勝ち");
}
else if (select1 == 2 && select2 == 1)
{
    Console.WriteLine(player1Name + "の勝ち");
}
else if (select1 == 3 && select2 == 1)
{
    Console.WriteLine(player1Name + "の勝ち");
}

なんかいい方法ない?

雰囲気で考えてみます
力士はそれぞれが人ですよね
当然です
それぞれ生物(=物体)と考えてみては・・・というのがオブジェクト指向なのです

物体 = オブジェクト

プログラミングは文字表現なので次のように属性を記述するルールにします
この書き方では誰かは特定していません
あくまで、力士とはこういう人ですよって書いているだけですね

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;
}

使ってみる

では、コードを変更してみましょう

string player1Name = "松鳳山";
int Player1Power = 60;

これは、次のように書き直します
1行増えただけで余計に難しくなった!と思うかもしれませんが、続けてみてみましょう

Rikishi player1 = new Rikishi();
player1.name = "松鳳山";
player1.power = 60;

3人とも変更しただけですが、次のようになります
・・・うーん、あまりメリットを感じませんね

Rikishi player1 = new Rikishi();
player1.name = "松鳳山";
player1.power = 60;

Rikishi player2 = new Rikishi();
player2.name = "玉鷲";
player2.power = 50;

Rikishi player3 = new Rikishi();
player3.name = "大翔丸";
player3.power = 30;

Console.WriteLine("1人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select1 = int.Parse(Console.ReadLine());

Console.WriteLine("2人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select2 = int.Parse(Console.ReadLine());

if (select1 == 1 && select2 == 2)
{
    Console.WriteLine(player1.name + "の勝ち");
}
else if (select1 == 1 && select2 == 3)
{
    Console.WriteLine(player1.name + "の勝ち");
}
else if (select1 == 2 && select2 == 1)
{
    Console.WriteLine(player1.name + "の勝ち");
}
else if (select1 == 3 && select2 == 1)
{
    Console.WriteLine(player1.name + "の勝ち");
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;
}

勝敗の判断するところは、次のように変更できます
オブジェクト指向を考えない場合、すべてのパターンを記述するといった現実的ではない仕組みから、勝敗判断のところについては無限に膨らむことがないコードになっていることがわかりますか?

Rikishi player1 = new Rikishi();
player1.name = "松鳳山";
player1.power = 60;

Rikishi player2 = new Rikishi();
player2.name = "玉鷲";
player2.power = 50;

Rikishi player3 = new Rikishi();
player3.name = "大翔丸";
player3.power = 30;

Console.WriteLine("1人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select1 = int.Parse(Console.ReadLine());

Console.WriteLine("2人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select2 = int.Parse(Console.ReadLine());


Rikishi rikishi1;

if (select1 == 1)
{
    rikishi1 = player1;
}
else if (select1 == 2)
{
    rikishi1 = player2;
}
else if (select1 == 3)
{
    rikishi1 = player3;
}
else
{
    rikishi1 = null;
}

Rikishi rikishi2;

if (select2 == 1)
{
    rikishi2 = player1;
}
else if (select2 == 2)
{
    rikishi2 = player2;
}
else if (select2 == 3)
{
    rikishi2 = player3;
}
else
{
    rikishi2 = null;
}

if (rikishi1.power > rikishi2.power)
{
    Console.WriteLine(rikishi1.name + "の勝ち");
}
else if (rikishi1.power < rikishi2.power)
{
    Console.WriteLine(rikishi2.name + "の勝ち");
}
else
{
    Console.WriteLine("とりなおし");
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;
}

続いて、選択された力士の判断のif文が膨らんでいくところを考えてみます
もし、あなたが配列を学んでいれば次のようにできることがわかると思います
まだ学んでいない場合、配列のメリットを感じてもらえれば学習モチベーションにしていただけると思います

Rikishi[] rikishis = new Rikishi[3];

rikishis[0] = new Rikishi();
rikishis[0].name = "松鳳山";
rikishis[0].power = 60;

rikishis[1] = new Rikishi();
rikishis[1].name = "玉鷲";
rikishis[1].power = 50;

rikishis[2] = new Rikishi();
rikishis[2].name = "大翔丸";
rikishis[2].power = 30;

Console.WriteLine("1人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select1 = int.Parse(Console.ReadLine());

Console.WriteLine("2人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select2 = int.Parse(Console.ReadLine());


if (rikishis[select1].power > rikishis[select2].power)
{
    Console.WriteLine(rikishis[select1].name + "の勝ち");
}
else if (rikishis[select1].power < rikishis[select2].power)
{
    Console.WriteLine(rikishis[select2].name + "の勝ち");
}
else
{
    Console.WriteLine("とりなおし");
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;
}

初期値の代入方法の変更

c#では、次のように記述することもできます

Rikishi[] rikishis = new Rikishi[3];

rikishis[0] = new Rikishi
{
    name = "松鳳山",
    power = 60
};

rikishis[1] = new Rikishi
{
    name = "玉鷲",
    power = 50
};

rikishis[2] = new Rikishi
{
    name = "大翔丸",
    power = 30
};

Console.WriteLine("1人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select1 = int.Parse(Console.ReadLine());

Console.WriteLine("2人目を選んでください(1:松鳳山 2:玉鷲 3:大翔丸");
int select2 = int.Parse(Console.ReadLine());


if (rikishis[select1].power > rikishis[select2].power)
{
    Console.WriteLine(rikishis[select1].name + "の勝ち");
}
else if (rikishis[select1].power < rikishis[select2].power)
{
    Console.WriteLine(rikishis[select2].name + "の勝ち");
}
else
{
    Console.WriteLine("とりなおし");
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;
}

作業ごとをメソッドにしてみましょう
ここでさらにC#の文法を取り入れます
複数の戻り値が得られるタプルを使います

はい、これで力士の追加はTorokuメソッドの内容追加だけで済むようになります


// 3人分の力士登録配列
Rikishi[] rikishis = new Rikishi[3];

// 登録します
Toroku(rikishis);
// 取り組む相手の選択
var selRikishis = Sentaku(rikishis);

// 勝敗の判断
Handan(selRikishis.rikishi1, selRikishis.rikishi2);

// --------------------------------------------

// 力士の登録処理メソッド
static void Toroku(Rikishi[] rikishis)
{
    rikishis[0] = new Rikishi
    {
        name = "松鳳山",
        power = 60
    };

    rikishis[1] = new Rikishi
    {
        name = "玉鷲",
        power = 50
    };

    rikishis[2] = new Rikishi
    {
        name = "大翔丸",
        power = 30
    };
}

// 力士の選択メソッド
static (Rikishi rikishi1, Rikishi rikishi2) Sentaku(Rikishi[] rikishis)
{
    Console.Write("1人目を選んでください ");
    for (int i = 0; i < rikishis.Length; i++)
    {
        Console.Write($"{i + 1}: {rikishis[i].name}");
    }

    int select1 = int.Parse(Console.ReadLine());

    Console.Write("2人目を選んでください ");
    for (int i = 0; i < rikishis.Length; i++)
    {
        Console.Write($"{i + 1}: {rikishis[i].name}");
    }

    int select2 = int.Parse(Console.ReadLine());

    return (rikishis[select1 - 1], rikishis[select2 - 1]);
}

// 勝敗の判断メソッド
static void Handan(Rikishi rikishi1, Rikishi rikishi2)
{
    if (rikishi1.power > rikishi2.power)
    {
        Console.WriteLine(rikishi1.name + "の勝ち");
    }
    else if (rikishi1.power < rikishi2.power)
    {
        Console.WriteLine(rikishi2.name + "の勝ち");
    }
    else
    {
        Console.WriteLine("とりなおし");
    }
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;
}

配列の場合、[3]とか最初に人数を登録する必要がありますが、これをC#のライブラリを使ってみます
それはListです。まだ学んでない方はメリットを感じてください
さらにコンストラクタを使って初期値の代入の簡略化も同時にしています


// 力士登録リスト
List<Rikishi> rikishis = new List<Rikishi>();

// 登録します
Toroku(rikishis);
// 取り組む相手の選択
var selRikishis = Sentaku(rikishis);

// 勝敗の判断
Handan(selRikishis.rikishi1, selRikishis.rikishi2);

// --------------------------------------------

// 力士の登録処理メソッド
static void Toroku(List<Rikishi> rikishis)
{
    rikishis.Add(new Rikishi("松鳳山", 60));
    rikishis.Add(new Rikishi("玉鷲", 50));
    rikishis.Add(new Rikishi("大翔丸", 30));
}

// 力士の選択メソッド
static (Rikishi rikishi1, Rikishi rikishi2) Sentaku(List<Rikishi> rikishis)
{
    Console.Write("1人目を選んでください ");
    for (int i = 0; i < rikishis.Count; i++)
    {
        Console.Write($"{i + 1}: {rikishis[i].name}");
    }

    int select1 = int.Parse(Console.ReadLine());

    Console.Write("2人目を選んでください ");
    for (int i = 0; i < rikishis.Count; i++)
    {
        Console.Write($"{i + 1}: {rikishis[i].name}");
    }

    int select2 = int.Parse(Console.ReadLine());

    return (rikishis[select1 - 1], rikishis[select2 - 1]);
}

// 勝敗の判断メソッド
static void Handan(Rikishi rikishi1, Rikishi rikishi2)
{
    if (rikishi1.power > rikishi2.power)
    {
        Console.WriteLine(rikishi1.name + "の勝ち");
    }
    else if (rikishi1.power < rikishi2.power)
    {
        Console.WriteLine(rikishi2.name + "の勝ち");
    }
    else
    {
        Console.WriteLine("とりなおし");
    }
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;

    public Rikishi(string name, int power)
    {
        this.name = name;
        this.power = power;
    }
}

力士の選択をシンプルにしてみましょう

// 力士登録リストの宣言
List<Rikishi> rikishis = new List<Rikishi>();

// 登録します
Toroku(rikishis);

// 取り組む相手の選択
Rikishi[] selRikishi = new Rikishi[2];

Console.Write("1人目を選択してください ");
selRikishi[0] = SentakuRikishi(rikishis);

Console.Write("2人目を選択してください ");
selRikishi[1] = SentakuRikishi(rikishis);

// 勝敗の判断
Handan(selRikishi);

// -----------メソッド---------------------------------

// 力士の登録処理メソッド
static void Toroku(List<Rikishi> rikishis)
{
    rikishis.Add(new Rikishi("松鳳山", 60));
    rikishis.Add(new Rikishi("玉鷲", 50));
    rikishis.Add(new Rikishi("大翔丸", 30));
}

// 力士の選択メソッド
static Rikishi SentakuRikishi(List<Rikishi> rikishis)
{
    for (int i = 0; i < rikishis.Count; i++)
    {
        Console.Write($"{i + 1}: {rikishis[i].name}");
    }

    int select = int.Parse(Console.ReadLine());
    return rikishis[select - 1];
}

// 勝敗の判断メソッド
static void Handan(Rikishi[] rikishis)
{
    if (rikishis[0].power > rikishis[1].power)
    {
        Console.WriteLine(rikishis[0].name + "の勝ち");
    }
    else if (rikishis[0].power < rikishis[1].power)
    {
        Console.WriteLine(rikishis[1].name + "の勝ち");
    }
    else
    {
        Console.WriteLine("とりなおし");
    }
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;

    public Rikishi(string name, int power)
    {
        this.name = name;
        this.power = power;
    }
}

力士のデータが多いときにファイルから読み取ることを想定してみましょう
ここでは、C#のインターフェースの機能も取り込んでいます

// 力士登録リストの宣言
List<Rikishi> rikishis = new List<Rikishi>();

IData torokuData;

// 登録します(直接コードに入れるシミュレーション)
// torokuData = new RikishiData();
// 登録します(ファイルから情報を読み込むシミュレーション)
torokuData = new RikishiDataFromFile();

torokuData.Toroku(rikishis);

// 取り組む相手の選択
Rikishi[] selRikishi = new Rikishi[2];

Console.Write("1人目を選択してください ");
selRikishi[0] = SentakuRikishi(rikishis);

Console.Write("2人目を選択してください ");
selRikishi[1] = SentakuRikishi(rikishis);

// 勝敗の判断
Handan(selRikishi);

// -----------メソッド---------------------------------

// 力士の選択メソッド
static Rikishi SentakuRikishi(List<Rikishi> rikishis)
{
    for (int i = 0; i < rikishis.Count; i++)
    {
        Console.Write($" {i + 1}:{rikishis[i].name}");
    }

    Console.Write(" ? ");
    int select = int.Parse(Console.ReadLine());
    return rikishis[select - 1];
}

// 勝敗の判断メソッド
static void Handan(Rikishi[] rikishis)
{
    if (rikishis[0].power > rikishis[1].power)
    {
        Console.WriteLine(rikishis[0].name + "の勝ち");
    }
    else if (rikishis[0].power < rikishis[1].power)
    {
        Console.WriteLine(rikishis[1].name + "の勝ち");
    }
    else
    {
        Console.WriteLine("とりなおし");
    }
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;

    public Rikishi(string name, int power)
    {
        this.name = name;
        this.power = power;
    }
}

interface IData
{
    void Toroku(List<Rikishi> rikishies);
}

// 力士のデータを直接コードに書き込んでいる例
class RikishiData : IData
{
    // 力士の登録処理メソッド
    public void Toroku(List<Rikishi> rikishis)
    {
        rikishis.Add(new Rikishi("松鳳山", 60));
        rikishis.Add(new Rikishi("玉鷲", 50));
        rikishis.Add(new Rikishi("大翔丸", 30));
    }

}

// 力士のデータが大量にあった場合を想定(ファイルから取得)
class RikishiDataFromFile : IData
{
    public void Toroku(List<Rikishi> rikishis)
    {
        rikishis.Add(new Rikishi("File1", 60));
        rikishis.Add(new Rikishi("File2", 50));
        rikishis.Add(new Rikishi("File3", 30));
    }
}

クラス図

メリット

ここでオブジェクト指向でコードを記述した時のメリットを整理します

  • 力士を増やしたり減らしたりしたい時、RikisiDataクラス(または、RikishiDataFromFaileクラス)の情報を追加するだけです
  • 勝敗の判断が今は力だけですが、技の要素も入れたい場合、Handanメソッドを更新するだけです

次の例は、waza要素を追加したコードになります
勝ち負けの判断は、力+技の合計の比較としてみました
どのように更新されたかを確認してみてください

// 力士登録リストの宣言
List<Rikishi> rikishis = new List<Rikishi>();

IData torokuData;

// 登録します(直接コードに入れるシミュレーション)
// torokuData = new RikishiData();
// 登録します(ファイルから情報を読み込むシミュレーション)
torokuData = new RikishiDataFromFile();

torokuData.Toroku(rikishis);

// 取り組む相手の選択
Rikishi[] selRikishi = new Rikishi[2];

Console.Write("1人目を選択してください ");
selRikishi[0] = SentakuRikishi(rikishis);

Console.Write("2人目を選択してください ");
selRikishi[1] = SentakuRikishi(rikishis);

// 勝敗の判断
Handan(selRikishi);

// -----------メソッド---------------------------------

// 力士の選択メソッド
static Rikishi SentakuRikishi(List<Rikishi> rikishis)
{
    for (int i = 0; i < rikishis.Count; i++)
    {
        Console.Write($" {i + 1}:{rikishis[i].name}");
    }

    Console.Write(" ? ");
    int select = int.Parse(Console.ReadLine());
    return rikishis[select - 1];
}

// 勝敗の判断メソッド
static void Handan(Rikishi[] rikishis)
{
    if (rikishis[0].power + rikishis[0].waza > rikishis[1].power + rikishis[1].waza)
    {
        Console.WriteLine(rikishis[0].name + "の勝ち");
    }
    else if (rikishis[0].power + rikishis[0].waza < rikishis[1].power + rikishis[1].waza)
    {
        Console.WriteLine(rikishis[1].name + "の勝ち");
    }
    else
    {
        Console.WriteLine("とりなおし");
    }
}

class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;
    // 技
    public int waza;

    public Rikishi(string name, int power, int waza)
    {
        this.name = name;
        this.power = power;
        this.waza = waza;
    }
}

interface IData
{
    void Toroku(List<Rikishi> rikishies);
}

// 力士のデータを直接コードに書き込んでいる例
class RikishiData : IData
{
    // 力士の登録処理メソッド
    public void Toroku(List<Rikishi> rikishis)
    {
        rikishis.Add(new Rikishi("松鳳山", 60, 10));
        rikishis.Add(new Rikishi("玉鷲", 50, 10));
        rikishis.Add(new Rikishi("大翔丸", 30, 30));
    }

}

// 力士のデータが大量にあった場合を想定(ファイルから取得)
class RikishiDataFromFile : IData
{
    public void Toroku(List<Rikishi> rikishis)
    {
        rikishis.Add(new Rikishi("File1", 60, 10));
        rikishis.Add(new Rikishi("File2", 50, 10));
        rikishis.Add(new Rikishi("File3", 30, 30));
    }
}

クラス図

wazaパラメータが増えただけで大きく変わってないですね

クラスをファイルに分けてもわかりやすくなります

// 力士登録リストの宣言
List<Rikishi> rikishis = new List<Rikishi>();

IData torokuData;

// 登録します(直接コードに入れるシミュレーション)
// torokuData = new RikishiData();
// 登録します(ファイルから情報を読み込むシミュレーション)
torokuData = new RikishiDataFromFile();

torokuData.Toroku(rikishis);

// 取り組む相手の選択
Rikishi[] selRikishi = new Rikishi[2];

Console.Write("1人目を選択してください ");
selRikishi[0] = SentakuRikishi(rikishis);

Console.Write("2人目を選択してください ");
selRikishi[1] = SentakuRikishi(rikishis);

// 勝敗の判断
Handan(selRikishi);

// -----------メソッド---------------------------------

// 力士の選択メソッド
static Rikishi SentakuRikishi(List<Rikishi> rikishis)
{
    for (int i = 0; i < rikishis.Count; i++)
    {
        Console.Write($" {i + 1}:{rikishis[i].name}");
    }

    Console.Write(" ? ");
    int select = int.Parse(Console.ReadLine());
    return rikishis[select - 1];
}

// 勝敗の判断メソッド
static void Handan(Rikishi[] rikishis)
{
    if (rikishis[0].power + rikishis[0].waza > rikishis[1].power + rikishis[1].waza)
    {
        Console.WriteLine(rikishis[0].name + "の勝ち");
    }
    else if (rikishis[0].power + rikishis[0].waza < rikishis[1].power + rikishis[1].waza)
    {
        Console.WriteLine(rikishis[1].name + "の勝ち");
    }
    else
    {
        Console.WriteLine("とりなおし");
    }
}
// 力士のパラメータを持ったクラス
class Rikishi
{
    // 名前
    public string name;
    // 力
    public int power;
    // 技
    public int waza;

    public Rikishi(string name, int power, int waza)
    {
        this.name = name;
        this.power = power;
        this.waza = waza;
    }
}
// Torokuメソッドを必須とすることにします
interface IData
{
    void Toroku(List<Rikishi> rikishies);
}
// 力士のデータを直接コードに書き込んでいる例
class RikishiData : IData
{
    // 力士の登録処理メソッド
    public void Toroku(List<Rikishi> rikishis)
    {
        rikishis.Add(new Rikishi("松鳳山", 60, 10));
        rikishis.Add(new Rikishi("玉鷲", 50, 10));
        rikishis.Add(new Rikishi("大翔丸", 30, 30));
    }
}
// 力士のデータが大量にあった場合を想定(ファイルから取得)
class RikishiDataFromFile : IData
{
    public void Toroku(List<Rikishi> rikishis)
    {
        rikishis.Add(new Rikishi("File1", 60, 10));
        rikishis.Add(new Rikishi("File2", 50, 10));
        rikishis.Add(new Rikishi("File3", 30, 30));
    }
}

C#

Posted by hidepon