【ゲーム開発のためのC#入門講座・応用拡張編】オーバーライドで再利用性を強化しよう【#4】

5.0_C#応用拡張編

おさらい

前回は、クラスの持つ要素(データやメソッド)を引き継ぎながら、追加要素のある新しいクラスを作る技術、継承について学びました。

これによって「共通の要素を持っているけど、部分的に実装が異なるクラス」を作りやすくなりましたね。敵と味方でダメージ計算式を変えたいと言っていたプログラマーY氏の願いも、これならば叶えることができるでしょう。

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

と言いたいところなのですが、それをするには元あった「BattleCharacter」というクラスを作り直さなければなりません。

// ↓もともと作っていたクラスで、
public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
​
    // ↓敵と味方でダメージ計算式を変えたいとなると、
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}
// ↓元のクラスからダメージ計算のメソッドを削除した上で、
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 class Enemy : BattleCharacter
{
    public int Attack()
    {
        return ATK + 1;
    }
}

これって結構な労力ですよね。

また、プログラマーY氏は処理を分けたいと言っていましたが、他の人は元のままでも困らないかもしれません。それなのに勝手に「BattleCharacter」クラスから「Attack」メソッドを削除してしまうと、そのままでよかった人のプログラムがある日突然動かなくなってしまいます。

public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
    /*
    ある日突然、メソッドが削除されてしまうと、
    public int Attack()
    {
        return ATK + Level * 2 / 5;
    }
    */
}
​
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 -= link.Attack(); //<=Attackメソッドがないのでエラーになる!
    if (goblin.HP <= 0)
    {
        break;
    }
    link.HP -= goblin.Attack(); //<=Attackメソッドがないのでエラーになる!
    if (link.HP <= 0)
    {
        break;
    }
}

うーん、弱りましたね。

何とかして、元のクラス構造を変えずに部分的に実装を変えることはできないものでしょうか?

オーバーライドって?

設計図に薄い紙を重ねると、元の設計図の構造がうっすらと浮かび上がって見えますよね。その状態でまっさらなところに新しい要素を足していくと、元の設計図を活かした新しい設計図を作ることができます。

これが継承という技術の考え方です。

その考え方を踏まえると、実はこんなこともできるのではないでしょうか?

「元の設計図の一部分を上書きして、部分的に構成を変更した新しい設計図を描く」

そう、継承は新しい要素を追加するだけでなく、元の要素を上書きすることもできるのです。

これがオーバーライドです。

蓋を開けてみると文字通りの技術ですね。早速例を見てみましょう。

public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
​
    // ↓新しいキーワード「virtual」が追加されたよ!
    public virtual int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}
​
public class Enemy : BattleCharacter
{
    // ↓新しいキーワード「override」が追加されたよ!
    public override int Attack()
    {
        return ATK + 1;
    }
}

キーワード「override」をつけると、親クラスの同名プロパティや同名メソッドを上書きして、新しい実装に定義し直すことができます。

ただし、せっかく作ったクラスを何でもかんでも上書きされてしまうと、本来行うべき処理などが行われず、思いがけないバグに繋がってしまうリスクがあります。

そのため、親クラスで「virtual」または「abstract」とつけられたものだけが上書きできるルールとなっています。

いわば、クラスを作った人が上書きしても問題ないと許可を出した証、上書きして使うことを想定して設計された部分ということですね。

今回「abstract」は使っていませんが、これは「単純な上書き」とは少し異なる振る舞いをするものです。いずれ解説予定なので、今回は上書きを許可する「virtual」と上書きを意味する「override」のふたつのキーワードを覚えてもらえればと思います。

実際に上書きされているか、確認してみましょう。

// ↓下記のプログラムを実行してみよう!
Enemy enemy = new Enemy();
Console.WriteLine(enemy.Attack());
​
public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
​
    public virtual int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}
​
public class Enemy : BattleCharacter
{
    public override int Attack()
    {
        return ATK + 1;
    }
}
コンソール画面

1

もし元の実装内容のままであれば「0 + 0 * 2 / 5」なので「0」が出力されるはずです。が、きちんと上書きされているため、「0 + 1」で「1」が出力されましたね。

このように、オーバーライドを活用することで新しい要素を追加するだけでなく、元の部分的な構造を変更することも可能になります。

継承という技術の幅が広がって、さらに再利用しやすくなるんですね。

なお、「virtual」というキーワードは「上書きしてもいい」を意味するものであるため、上書きしなければならないという訳ではありません。

特に上書きしなかった場合は元の実装内容がそのまま実行されます。

// ↓下記のプログラムを実行してみよう!
Hero hero = new Hero();
Console.WriteLine(hero.Attack());
​
public class BattleCharacter
{
    public string Name;
    public int Level;
    public int HP;
    public int ATK;
​
    public virtual int Attack()
    {
        return ATK + Level * 2 / 5;
    }
}
​
public class Hero : BattleCharacter
{
    // ↓新しいメソッドは追加してるけど、
    //  Attackメソッドの上書きはしてないよ!
    public int Skill() 
    {
        return ATK * 2;
    }
}
コンソール画面

0

これなら部分的に変更したいプログラマーY氏、元のままでも構わない人、どちらの要望も満たすことができますね!

クラスを作成する側としても、「virtual」というキーワードをつけるだけでOK=元のクラス構造を変更しないで済むので、非常に楽ちんです。

継承とオーバーライドを使うことで、新しい要素を追加できるだけでなく、元の構造を部分的に変更した新しいクラスを作成することまで出来てしまうんですね。

継承という機能の難しさ

オブジェクト指向の三大要素のひとつ「継承」は、これまで見てきた通り非常にプログラミングの自由度を広げてくれる画期的な技術です。

一方で、親と子で実装が分かれているだけでなく、子が親の実装内容を上書きしている可能性まで考慮してプログラムを見なければいけなくなってしまうので、プログラムの複雑度も加速度的に増加します。

だからこそ、C#では多重継承を認めていません

ようは1クラスにつき継承できるのは1つだけ、と決まっているんですね。ただでさえ複雑なのに色んなクラスを継承してしまうと、本当に訳が分からなくなってしまいますから。

一方でこの縛りが、継承を使う上での難しさにも繋がってきます。

例えば前回・今回と、「BattleCharacter」と「Hero」「Enemy」クラスで継承関係を作りました。これは戦闘シーンという観点では処理を共通化できてよかったかもしれませんが、一方でゲームは戦闘シーンだけではないですよね。

一歩フィールドに出れば、「NPC」は「歩く」「走る」といった行為がプレイヤーキャラクターである「Hero」と似ているかもしれません。でも「Hero」クラスは既に継承を使ってしまっていますから、他のクラスを継承して処理を共通化することはできない訳です。

このように、ある場面において「処理が似ているから」という理由で安易に継承関係を作ってしまうと、それ以外の場面において逆に枷になってしまう恐れがあります。

この継承を上手に扱うには、プログラム全体の構造を意識する必要があります。だからこそ、継承は概念的にも実践的にも非常に難しい機能なのです。

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()
    {
        
    }
}

ただ、何かをオーバーライドしなきゃ使えないとか、そういうことは全くありません。

そのため、元のクラスの要素を引き継いだ新しいクラスを作れるという、継承の根本的な役割さえ理解できていれば十分です。

未経験者・初心者の方は覚えることがたくさんでしんどいと思うので、何かこういうこともできるんだなーってことだけ頭の片隅にでも置いておいてください。いざって時に思い出せるだけでも、すごく価値があります。

実践演習

それでは、実際にオーバーライドを使ってみましょう。

仕様

クラス「Healer」を継承したクラス「Priest」があります。
「Heal」クラスをオーバーライドできるよう、適切なキーワードを両クラスに設定してあげてください。

テンプレート
public class Healer
{
    protected int HealPower;
    
    public int MP { get; protected set; }
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    // ↓ここにキーワードを足そう!
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}
public class Priest : Healer
{
    // ↓ここにキーワードを足そう!
    public int Heal()
    {
        MP -= 5;
        return HealPower * 3;
    }
}

答え合わせ

public class Healer
{
    protected int HealPower;
    
    public int MP { get; protected set; }
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    // ↓ここにキーワードを足そう!
    public virtual int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}
public class Priest : Healer
{
    // ↓ここにキーワードを足そう!
    public override int Heal()
    {
        MP -= 5;
        return HealPower * 3;
    }
}

まとめ

  • オーバーライドとは、親クラスのプロパティやメソッドの内容を上書きする機能のこと
  • 何でもかんでも上書きされると困るので、許可された項目だけが上書きできるようになっている
  • オーバーライドを活用することで、元のクラスの構造を変えずに、部分的に改造した新しいクラスを作ることができる
  • 継承やオーバーライドを使っていくと非常に複雑になるため、C#では多重継承が認められていない。一方で、ひとつしか継承できないからこその難しさもある

次回はオブジェクト指向三大要素の最後「ポリモーフィズム(多態性)」について学びます。お楽しみに!

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

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

Posted by 夕目紅