【ゲーム開発のためのC#入門講座・応用拡張編】継承で再利用性を高めよう【#3】

5.0_C#応用拡張編

プログラムの再利用がしにくいもうひとつの理由

応用学習編の第二回「オブジェクト指向を学ぼう」で、オブジェクト指向が登場するまでは、プログラムの再利用がしにくかった理由を覚えていますか?

ひとつは、データと処理が別々に定義されていて、ひとまとめになっていないから。

これはオブジェクト指向により導入された技術「クラス」によって解消されましたね。また、前回・前々回と、クラスにまとめたからこそできるようになったクラス内外のアクセス制御、カプセル化についても学びました。

そしてもうひとつの理由、一部分だけ変更することは出来ないから。

今回はこの問題を解消するオブジェクト指向の三大要素のひとつ継承について学びます。

部分的に共通するクラス

プログラムの再利用がしにくかった理由を説明するための小話で、プログラマーY氏はこう言っていましたね。

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

public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
​
    public int Attack()
    {
        // ↓この計算式を敵と味方で変えたい!
        return ATK + Level * 2 / 5;
    }
}

この問題に対処する方法としては、敵なのか味方なのかを表す変数「IsEnemy」を用意して、処理を分けるという手があります。

public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
    // ↓敵か味方かを判断するためのメンバー変数を用意して、
    public bool IsEnemy;
​
    public int Attack()
    {
        // ↓処理を分けるという手がある
        if (IsEnemy)
        {
            return ATK + 1;
        }
        else
        {
            return ATK + Level * 2 / 5;   
        }
    }
}

ただ、もし他にもたくさん敵か味方かで処理を分けたいメソッドがあったり、敵でしか使わないメソッドや味方でしか使わないメソッドがあったりしたらどうでしょうか?

public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
    public bool IsEnemy;
​
    public int Attack()
    {
        if (IsEnemy)
        {
            return ATK + 1;
        }
        else
        {
            return ATK + Level * 2 / 5;   
        }
    }
    
    // ↓他にも敵か味方かで分けたい処理があったり、
    public int Skill()
    {
        if (IsEnemy)
        {
            return ATK * 2 + 1;
        }
        else
        {
            return (ATK + Level * 2 / 5) * 2;   
        }
    }
    
    // ↓敵でしか使わないメソッド(プレイヤーは逃走不可)や、
    public bool Escape()
    {
        if (HP < HP / 2)
        {
            return true;
        }
        else
        {
            return false;   
        }
    }
    
    // ↓味方でしか使わないメソッド(敵はガードしない)があったら……
    public int Guard(int damage)
    {
        return damage / 2;
    }
}

条件分岐が多いせいで処理がごちゃごちゃして見えますね。

また、この構造だと「敵」のインスタンスでメソッド「Guard」を利用したり、「味方」のインスタンスでメソッド「Escape」を実行できてしまいます。再利用しやすいプログラムとは言えないですね。

となると、いっそクラスを「Hero」と「Enemy」のふたつに分けた方がよさそうです。

// 味方用の専用クラス「Hero」を作成
public class Hero
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
​
    public int Attack()
    {
        return ATK + Level * 2 / 5; 
    }
​
    public int Skill()
    {
        return (ATK + Level * 2 / 5) * 2;
    }
​
    public int Guard(int damage)
    {
        return damage / 2;
    }
}
// 敵用の専用クラス「Enemy」を作成
public class Enemy
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
​
    public int Attack()
    {
        return ATK + 1;
    }
    
    public int Skill()
    {
        return ATK * 2 + 1;
    }
    
    public bool Escape()
    {
        if (HP < HP / 2)
        {
            return true;
        }
        else
        {
            return false;   
        }
    }
}

確かにこの方がすっきりして見えますね。

ただ、下記の部分はどちらのクラスでも全く同じなんですよね。

// ↓この部分は全く一緒
public string Name;
public int Level;
public int HP;
public int ATK;

今回はこの程度の量なのであまり苦にはならないですが、もし共通している部分が千行、一万行あったらどうでしょう?

全く同じ処理なのに別々に実装していて、何だか冗長ですよね。

もしシステム共通のステータスを増やしたくなったら、ステータスを保持しているすべてのクラスに追加しなければいけなくなってしまいます。

人生は有限、タイムイズマネー。

もっと効率よくプログラミングできないものでしょうか?

クラスは設計図、だからこそできることがある

応用学習編の第三回「クラスの扱い方を学ぼう」で、クラスはよく設計図に例えられるという話をしました。

クラスはあくまで「こういうデータを持っていて、こういう行動ができるもの」という設計図に過ぎず、その設計図をもとに作られたインスタンスこそがモノ(オブジェクト)にあたる、ということでしたね。

もしクラスが設計図だというのなら、こういう考え方もできます。

「その設計図をベースに、足りないところだけを追加設計することはできないか?」

この考え方により導入された技術が継承です。

継承って?

早速実例を見てみましょう。

// ↓元となるクラスだよ!
public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
}
​
// ↓クラス「BattleCharacter」を継承したクラス「Hero」だよ!
public class Hero : BattleCharacter
{
    public int Attack()
    {
        return ATK + Level * 2 / 5; 
    }
​
    public int Skill()
    {
        return (ATK + Level * 2 / 5) * 2;
    }
​
    public int Guard(int damage)
    {
        return damage / 2;
    }
}

クラス「Hero」はクラス「BattleCharacter」を継承しています。

継承の構文は下記の通りです。

アクセス修飾子 class クラス名 : 継承元のクラス名
{
    ……継承元にはないデータやメソッドの実装……
}

実はクラスというのは、別のクラスをベースにすることができるんですね。そしてその元となるクラスのデータやメソッドを「継承」しつつ、独自のデータやメソッドを追加することができるのです。

つまり「Hero」クラスは、継承元である「BattleCharacter」が持っているすべてのメンバー変数を引き継いでいるのです。その上で、独自のメソッド「Attack」や「Skill」、「Guard」といった機能を追加で実装しています。

そのため、「Hero」クラスだけを見ると何もメンバー変数を実装していないように見えますが、使う際はちゃんと「Level」「HP」といった継承元のメンバー変数にアクセスすることができます。

Hero hero = new Hero();
// ↓継承元である「BattleCharacter」の
//  メンバー変数にアクセスできるし、
hero.Name = "Link";
hero.Level = 10;
hero.HP = 20;
hero.ATK = 10;
​
// ↓そのメンバー変数を使った、
//  「Hero」クラスで追加されたメソッドも実行できるよ!
Console.WriteLine(hero.Attack());

これなら、「Enemy」クラスも同じクラスを継承することで、同じメンバー変数を引き継ぐことができますね。

もし仮にステータスが追加になったとしても、継承元のクラスを修正するだけで、すべてのクラスに該当ステータスが追加されます。

// ↓同じクラスを継承すれば、同じメンバー変数やメソッドを引き継げるよ!
public class Enemy : BattleCharacter
{
    public int Attack()
    {
        return ATK + 1;
    }
    
    public int Skill()
    {
        return ATK * 2 + 1;
    }
    
    public bool Escape()
    {
        if (HP < HP / 2)
        {
            return true;
        }
        else
        {
            return false;   
        }
    }
}
public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
    // ↓元となるクラスにメンバー変数を追加するだけで、
    //  それを継承しているクラスすべてに追加されたことになるよ!
    public int DEF;
}

また共通のプロパティやメソッドがあれば、こちらも継承元クラスに実装することで、継承先のすべてのクラスでアクセスしたり実行したりすることができます。

public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
    public int DEF;
    
    // ↓継承元にメソッドを実装すれば、
    //  継承先のすべてのクラスで使うことができるよ!
    public bool IsDead()
    {
        if (HP <= 0)
        {
            return true;
        }
        else
        {
            return false;
        }
    }
}

Hero hero = new Hero();
// ↓Heroクラスでも「IsDead」メソッドが使えるよ!
Console.WriteLine(hero.IsDead());

この継承関係を、

  • スーパークラスとサブクラス
  • または親子関係に例えて、親クラスと子クラス

と呼びます(自分は親クラス子クラス派なので、以降そう呼びます)。

このように継承を上手に使うと、それまではクラス毎にバラバラに実装していた内容をひとつにまとめることが出来、差異がある部分だけを追加実装すればOKという状況を作ることができます。

無駄がなく拡張しやすいクラスを作れるようになる――まさにオブジェクト指向の目的である「理解しやすく、再利用しやすいプログラムを作る」を実現する機能ですね。

と、言いたいところなのですが……

はっきり言いましょう。

再利用できる範囲は確かに大きく広がるが、理解しやすくはならない

実装内容がひとつのクラスの中だけで完結しなくなるので、全体像が把握し辛いんですよね。親クラスのことを全く意識しなくていいならまだいいのですが、なかなかそうもいかないことの方が多いです。

それにこの継承クラスは非常に取り扱いが難しいです(詳細は次回話します)。

なので、慣れないうちは無理に使わなくても構いません。ただ、上手に使えば間違いなく「再利用しやすく」はなるので、少しずつ色々試しながら自分なりにしっくりくる使い方を模索してみるとよいでしょう。

Unityでの活躍ポイント

慣れないうちは無理に使わなくてもよいと言ったばかりですが、Unityではとあるポイントで強制的に使うことになります。

というのも、Unityで扱うゲームオブジェクト(画像や3Dキャラクターなど)に対してスクリプトを設定する場合、必ず「MonoBehaviour」というクラスを継承しなければならないというルールになっているからです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

// ↓クラス「MonoBehaviour」を継承している
public class NewBehaviourScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

上記はUnity上で作成したC#のスクリプトファイルです。初期設定のコードからして当たり前のように「MonoBehaviour」クラスを継承していますよね。

これにはもちろんきちんとした理由がありますし、その理由を理解しておくことで、Unityでのゲーム開発がスムーズになります。

なので、継承という仕組み自体はここできっちり理解しておくとよいでしょう。

実践演習

それでは、実際に継承を使ってみましょう。

演習①

仕様

クラス「Hero」をさらに継承したクラス「Magician」を作成し、int型のpublicなメンバー変数「MP」を追加してください。

テンプレート
public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
}
// ↓このクラスを継承した新しいクラスを作成し、追加のメンバー変数を定義しよう!
public class Hero : BattleCharacter
{
    public int Attack()
    {
        return ATK + Level * 2 / 5; 
    }

    public int Skill()
    {
        return (ATK + Level * 2 / 5) * 2;
    }

    public int Guard(int damage)
    {
        return damage / 2;
    }
}

演習②

継承を学習したため、ここで新しいアクセス修飾子を紹介します。

アクセス修飾子アクセス可能な範囲
protectedクラス内または子クラスのみ可

このアクセス修飾子を使うと、基本的にクラス外からはアクセス不可だが、子クラスだけはアクセス可とすることができます。

早速試してみましょう。

仕様

クラス「BattleCharacter」のうち、子クラス「Hero」でも使いたいメンバー変数はアクセス修飾子を「protected」に変更してください。

テンプレート
public class Hero : BattleCharacter
{
    public int Attack()
    {
        return ATK + Level * 2 / 5; 
    }

    public int Skill()
    {
        return (ATK + Level * 2 / 5) * 2;
    }

    public int Guard(int damage)
    {
        return damage / 2;
    }
}
public class BattleCharacter
{
    // ↓Heroクラスでも使いたいものはprotectedに変更しよう!
    private string Name;
    private int Level;
    private int HP;
    private int ATK;
}

答え合わせ

演習①の答え

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

// ↓名前空間はプロジェクト名なので差異があってもOK!
namespace Csharp5_3
{
    internal class Magician : Hero
    {
        public int MP;
    }
}

演習②の答え

public class BattleCharacter
{
    // ↓Heroクラスでも使いたいものはprotectedに変更しよう!
    private string Name;
    protected int Level;
    private int HP;
    protected int ATK;
}

まとめ

  • 継承とは、元となるクラスのデータやメソッドを継承しつつ、独自のデータやメソッドを追加した新しいクラスを作ることができる機能
  • 継承関係を親子に例えて親クラス・子クラスと呼ぶ
  • 上手に使えば間違いなく便利な機能ではあるのだが、決して理解しやすい技術ではないので、色々試しながら使い方を模索していこう
  • 「protected」は「クラス内または子クラスのみアクセス可」とするアクセス修飾子

次回はこの継承のさらなる拡張機能「オーバーライド」について学習します。お楽しみに!

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

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

Posted by yuumekou