【ゲーム開発のためのC#入門講座・応用学習編】オブジェクト指向を学ぼう【#2】

2022-01-184.0_C#応用学習編

オブジェクト指向が登場するまでのプログラミング

オブジェクト指向が登場するまでは、これまで基礎編で学んだ変数やループ処理・関数などの技術、そして複数データをまとめる配列や構造体を使ってプログラミングが行われていました。

例えば下記のプログラムを見てみましょう。

BattleCharacter link = new BattleCharacter();
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.ATK = 10;

BattleCharacter goblin = new BattleCharacter();
goblin.Name = "ゴブリン";
goblin.Level = 5;
goblin.HP = 10;
goblin.ATK = 7;

while (true)
{
    goblin.HP -= Attack(link);
    if (goblin.HP <= 0)
    {
        break;
    }
    link.HP -= Attack(goblin);
    if (link.HP <= 0)
    {
        break;
    }
}

if (goblin.HP <= 0)
{
    Console.WriteLine("勝利!");
}
else
{
    Console.WriteLine("ゲームオーバー!");
}

static int Attack(BattleCharacter attacker)
{
    return attacker.ATK + attacker.Level * 2 / 5;
}

public struct BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
}
コンソール画面

勝利!

これまでも何度か作ってきた1:1の戦闘シミュレーションプログラムを、構造体を使って実装したものとなります。

やっぱり構造体を使うと、関連性のあるものをひとつにまとめられるし、配列と違って個々のデータには名前でアクセスできるので、とてもわかりやすくなりますね。

プログラムを再利用したい

いいプログラムができたと一人悦に浸っていたところ、共にゲーム開発に向けて勉強しているY氏が不意にコードを覗き込んできました。

「え、何そのバトルキャラクターとかいうコード。格好いいじゃん!」

「せやろ?(ドヤッ」

「俺もそのコード使いたい。そのコードくれよ」

「じゃあこのプログラムあげるよ」

「いや、その結果表示部分はいらない、ださいし。バトルキャラクターだけくれよ」

「……(こいつ注文の多いやつやな。しかもさらっとディスってきたぞ)」

「ん、ダメージ計算の関数もあるのか。別々に実装されてるから気付かなかったわ。それもくれ」

「へいへい」

「あー、でもダメージの計算式は敵と味方で変えたいんだよな。うーん、そのままじゃ使えないか」

「ふふん、残念だったな。自分で同じものを作りたまえ」

このような会話がこの世界の片隅で行われたかどうかは定かではありませんが、オブジェクト指向が登場する前というのはプログラムを部分的に提供するのが難しかった、つまりプログラムの再利用がしにくかったんですね。

では、なぜプログラムの再利用がしにくいのでしょう?

ざっくり言うと、それは先程のY氏の台詞にある通り、

  1. データと処理が別々に定義されていて、ひとまとめになっていないから
  2. 一部分だけ変更することは出来ないから

です。

今回のように友人関係にあるならソースコードを直接見せて「この部分コピーしなよ」とも言えますが、企業の場合は立派な資産です。そんな簡単に無料で見せる訳にもいきません。そのため、プログラムは配布したとしても暗号化された状態に置き換えられています。そうすると一部分だけ自分仕様に変更して使おう、なんてこともできない訳です。

こらあかん、もっとプログラムの再利用が簡単な、効率のよい方法はないのか?

ということで注目を集めたのがオブジェクト指向です。

オブジェクト指向が登場してからのプログラミング

それでは、先程のプログラムをオブジェクト指向で作り直してみましょう。

おまじないも出てきますが、次回以降きちんと解説するので、まずはコメントを目印に違いを確認してみてください。

BattleCharacter link = new BattleCharacter();
link.Name = "リンク";
link.Level = 10;
link.HP = 25;
link.ATK = 10;

BattleCharacter goblin = new BattleCharacter();
goblin.Name = "ゴブリン";
goblin.Level = 5;
goblin.HP = 10;
goblin.ATK = 7;

while (true)
{
    // ↓構造体の時は「Attack(link)」だったのに、
    //  今は逆転して「link.Attack()」になっています!
    goblin.HP -= link.Attack();
    if (goblin.HP <= 0)
    {
        break;
    }
    // ↓構造体の時は「Attack(goblin)」だったのに、
    //  今は逆転して「goblin.Attack()」になっています!
    link.HP -= goblin.Attack();
    if (link.HP <= 0)
    {
        break;
    }
}

if (goblin.HP <= 0)
{
    Console.WriteLine("勝利!");
}
else
{
    Console.WriteLine("ゲームオーバー!");
}

// ↓struct(構造体)ではなく、class(クラス)になっています
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
    
    // ↓関数がクラスの中に入っています
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

どうでしょうか?

構造体の時はデータ(BattleCharacterが持つNameやATKなどの情報)と処理(攻撃力を計算する関数「Attack」)がバラバラに実装されていました。

それに対してオブジェクト指向では、データと処理がひとまとめになっています。

このデータと処理をひとまとめにしたものをクラスといいます

実は今までもクラスを使っていました。散々活用してきた関数「Console.WriteLine」、実はこれ「Console」の部分がクラスなのです。

つまり「Console」というクラスの中に、コンソール画面のデータと「WriteLine」という処理がまとめられていたんですね。

オブジェクト指向ではこのように、関連性のあるデータと処理だけをひとつのクラスにまとめることができます。そして皆さんがコンソール画面に対して色んな情報を書き出してきたように、そのデータと処理をまとめて再利用することができます。

プログラム再利用のしやすさがアップしましたね!

「BattleCharacter」というクラスを丸ごとあげれば、Y氏も喜んでくれそうです。

あ、でも「一部分だけ変更することは出来ない」という問題の方はまだ解消されていませんね?

実はこの問題もオブジェクト指向は解決してくれるのですが、それはもう少し後で学習することとしましょう。

オブジェクト指向の何がオブジェクトなの?

仮にアプリを実行している存在を擬人化してプログラム君と呼ぶとしましょう。

オブジェクト指向の登場前では、プログラム君がデータを把握し、そのデータを使って自分自身が持つ関数を呼び出して処理を実行していました。

// ↓プログラム君がデータを使って、
goblin.HP -= Attack(link);
if (goblin.HP <= 0)
{
    break;
}
// ↓自分自身が持つ関数「Attack」を呼び出し、
link.HP -= Attack(goblin);
if (link.HP <= 0)
{
    break;
}
​
// ↓自分自身でダメージを計算する
static int Attack(BattleCharacter attacker)
{
    return attacker.ATK + attacker.Level * 2 / 5;
}

けれど、オブジェクト指向では、プログラム君から依頼を受けて、「リンク」や「ゴブリン」が自分自身のデータを使って処理を実行します。

// ↓プログラム君は「link」に「Attack」をお願いするだけ
goblin.HP -= link.Attack();
if (goblin.HP <= 0)
{
    break;
}
// ↓プログラム君は「goblin」に「Attack」をお願いするだけ
link.HP -= goblin.Attack();
if (link.HP <= 0)
{
    break;
}
​
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
​
    // ↓「Attack」を実行するのはプログラム君ではなく、
    // 「BattleCharacter」であるリンクやゴブリン
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}

「link.Attack」とか「goblin.Attack」なんて、まるでキャラクター達が実際にそんな行動を起こしているかのようですよね。

そう、データと処理をひとまとめにしたものって、自分自身を表す情報を持ち何らかの行動を起こす「モノ(オブジェクト)」であるかのように捉えることができるのです。

今回作ろうとしているのはまさに「リンクがゴブリンを攻撃し、ゴブリンがリンクを攻撃する戦闘シミュレーション」です。

「Attack(link)」のようにデータを関数で処理するという記述よりも、「link.Attack」のようにモノ(オブジェクト)が行動するという記述の方が実際の内容に即していて、直感的でわかりやすいですよね。

このように、データと処理をひとまとめにしたものを「モノ(オブジェクト)」と捉え、「モノ(オブジェクト)」が行動した結果の積み重ねとしてプログラムを構築しよう、という考え方がオブジェクト指向です。

そうすることで、理解しやすくて再利用性も高いプログラムを作れるようにしよう、ということなんですね。

オブジェクト指向の難しさと誤解

このように、データと処理をひとまとめにしたものを「モノ(オブジェクト)」って捉えると直感的で理解しやすいプログラムにならんか? という考えからオブジェクト指向は生まれました。

実際、どうでしょうか?

今回の戦闘シミュレーションにおいては、「link」や「goblin」は確かに 「モノ(オブジェクト)」 といえそうです。

でも、例えば「日付」は「モノ(オブジェクト)」でしょうか? 「乱数生成機能」は「モノ(オブジェクト)」でしょうか?

正直、「モノ(オブジェクト)」とは言い難いですよね。

でもC#においては「日付」は「DateTime」というクラスですし、「乱数生成機能」も「Random」というクラスで表されています。

なので、何が何でもすべてをモノ(オブジェクト)として捉えなければいけないという訳ではないです。

あくまでクラスの本質的な役割は「関連性のあるデータと処理をひとまとめにする」です。

そう考えると「DateTime」は日付に関連する情報や日付を算出する処理をまとめたものだし、「Random」は乱数を生成するための情報や乱数を生成する処理をまとめたものとなるため、別段違和感はなくなるんですよね。

概念もまたオブジェクトなんだ、と無理にすべてを「モノ(オブジェクト)」として捉えようとする人もいます。自分も「ぴんとこないのは、きっと自分の理解が足りないからなんだろうな」と思っていたことがありました。

でも、オブジェクト指向はあくまで「指向(考え方の方向性)」です。

目的は「わかりやすくて再利用しやすいプログラムを作ること」であり、「すべてを「モノ(オブジェクト)」として表すこと」ではありません

「モノ(オブジェクト)」として考えた方がわかりやすいなら「モノ(オブジェクト)」として考えればいいし、逆にわかりにくくなるなら「関連性のあるデータと処理をひとまとめにしたもの」という原点に立ち返る。

そういう風に決めておくと、少しはがんじがらめにならずに済むんじゃないかなと思います。

Unityでの活躍ポイント

C#という言語はオブジェクト指向言語であるため、あらゆるものがクラスで表されます。

そしてUnityもまた、すべてのものをクラスで扱います。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
​
public class NewBehaviourScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }
​
    // Update is called once per frame
    void Update()
    {
        
    }
}

上記はUnity上で作成したC#のスクリプトファイルです。初期設定のコードからして「class」になっていますよね。

そのため、Unityでゲーム開発を行うためには、このクラスを扱えるようになる必要があります

この応用学習編で、クラスの使い方を学んでいきましょう。

まとめ

  • オブジェクト指向が登場するまではプログラムの再利用がしにくかった
  • ざっくりとした理由としては、プログラムがまとめ辛く、部分的な変更もできなかったから
  • オブジェクト指向では、クラスという、データと処理をひとまとめにしたものを扱える
  • データと処理をひとまとめにしたものを「モノ(オブジェクト)」と捉え、「モノ(オブジェクト)」が行動した結果の積み重ねとしてプログラムを構築しよう、という考え方がオブジェクト指向
  • オブジェクト指向はあくまで「指向(考え方の方向性)」。何が何でもすべてをモノ(オブジェクト)として捉えなければいけないという訳ではないので、「データと処理をひとまとめにする」という目的を忘れないようにしよう

今回は知識だけなので演習はありません。

次回は「クラス」を実際に使いながらもう少し深い理解を目指します。

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

Posted by yuumekou