LoginSignup
6
1

【Unity】Unityで学ぶデザインパターン04: Prototype パターン【デザパタ】

Last updated at Posted at 2023-05-26

はじめに

様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例 いまいちピンとこないコード で説明されてることが多く、
結局これっていつ使うの? という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。

そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。

デザインパターンを学ぶ理由

デザインパターンを学ぶ理由としては

  1. 車輪の再発明の防止
  2. 長文で読みにくいコード(可読性の低いコード)を減らす
  3. コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
  4. モジュールとして使いまわせるように、コードの再利用性を高める
    といった効果を期待できます。

対象読者

Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。

  • MonoBehaviour 継承クラスでコードを書いたことがある
  • C# のピュアクラスを用いた自作クラスを作ったことがある
  • クラスの継承という概念は知っている

そのため、脱・初心者 中級者へのステップアップ として デザインパターンを学ぶ のが良いと思います。

デザパタ記事リンク

生成系

  1. AbstractFactory パターン
  2. Builder パターン
  3. FactoryMethod パターン
  4. Prototype パターン(本記事)
  5. Singleton パターン

構造系

  1. Adapter パターン
  2. Bridge パターン
  3. Composite パターン
  4. Decorator パターン
  5. Facade パターン
  6. Flyweight パターン
  7. Proxy パターン

様態・ふるまい系

  1. Chain of Responsibility パターン
  2. Command パターン
  3. Interpreter パターン
  4. Iterator パターン
  5. Mediator パターン
  6. Memento パターン
  7. Observer パターン
  8. State パターン
  9. Strategy パターン
  10. TemplateMethod パターン
  11. Visitor パターン

Prototype パターンについて

一言でいうと自分自身のコピーを作成するメソッドを用意するというものです。
プロトタイプという言葉はゲーム開発でも「プロト作成」「アルファ版」「ベータ版」「マスターアップ」という工程でも出てくる用語で「試作品」という意味を持っています。

ただこのコピーの作成ですが、もう少しコンピューターサイエンス的な言い方をすれば「DeepCopy」を提供するものであることには注意しましょう。

ShallowCopy vs DeepCopy

プログラムを組んでいると必ず出くわす概念です。Shallow(浅い)CopyとDeep(深い) コピーです。
shallow という言葉は馴染みのない人もいるかもしれませんが、Gitの最新のみをCloneするときは shallow clone すると言い換えるときもあるので、是非これを機会に覚えておきましょう。

さて、この2つには明確に違いがあります。簡潔に説明すると参照型のオブジェクトは共有するかどうかの違いです。
DeepCopy は本当にクローンを生成するため、参照型のオブジェクトが内部にあった場合でも 完全に別のインスタンスとして複製 します。一方ShallowCopyは値型はコピーされますが、参照型オブジェクトはコピーされず同じアドレスを指して 共有します。

直感的にはコピー=DeepCopyですが、メモリを抑えたいなどの場合ではShallowCopyも使ったりします。
また、意識的に参照型オブジェクトを複製しないと、DeepCopyだと思ったらShallowCopyで、コピーしたオブジェクトを変更しようとしたら元のオブジェクトにも変更が反映されてしまったという失敗事例はよくある話です。

Prototypeパターンの実装

DeepCopyさえできればいいのでやり方はいくつかあります。

  1. DeepCopy を行うコピーコンストラクタを作る
  2. DeepCopy用メソッド/インターフェースを定義

コピーコンストラクタ

MonoBehaviour を継承しないピュアクラスの場合、初期化はコンストラクタで可能です。
そこで、コンストラクタに同クラスのインスタンスを設定することで、各種パラメータをコピーする コピーコンストラクタ というやりかたで初期化が出来ます。

例えば一般的なゲームのキャラクターの現在状況をコピーする実装例を見てみましょう

DeckChara.cs

using System.Collections.Generic;

public class DeckChara 
{
    public string Name { get; protected set; }
    /// <summary>
    /// 現在HP
    /// </summary>
    public int HP { get; protected set; }
    /// <summary>
    /// 最大HP
    /// </summary>
    public int MaxHP { get; protected set; }
    /// <summary>
    /// 現在Skillポイント
    /// </summary>
    public int SP { get; protected set; }
    /// <summary>
    /// 最大スキルポイント
    /// </summary>
    public int MaxSP { get; protected set; }
    /// <summary>
    /// 攻撃力
    /// </summary>
    public int ATK { get; protected set; }
    /// <summary>
    /// 防御力
    /// </summary>
    public int DEF { get; protected set; }
    /// <summary>
    /// 魔法攻撃力
    /// </summary>
    public int M_ATK { get; protected set; }
    /// <summary>
    /// 魔法防御力
    /// </summary>
    public int M_DEF { get; protected set; }
    /// <summary>
    /// 敏捷
    /// </summary>
    public int DEX { get; protected set; }
    /// <summary>
    /// 運
    /// </summary>
    public int LUC { get; protected set; }
    /// <summary>
    /// スキルIDリスト
    /// </summary>
    public List<int> SkillIds { get; protected set; }

    /// <summary>
    /// 通常のコンストラクタ
    /// </summary>
    public DeckChara()
    {
        Name = string.Intern("UNKNOWN");
        MaxHP = 10;
        HP = 10;
        MaxSP = 1;
        SP = 1;
        ATK = 5;
        DEF = 0;
        M_ATK = 3;
        M_DEF = 0;
        DEX = 5;
        LUC = 0;
        SkillIds = new List<int>();
    }

    /// <summary>
    /// コピーコンストラクタ
    /// </summary>
    public DeckChara(DeckChara original)
    {
        Name = original.Name;
        MaxHP = original.MaxHP; 
        HP = original.HP; 
        MaxSP = original.MaxSP;
        SP = original.SP;
        ATK = original.ATK;
        DEF = original.DEF;
        M_ATK = original.M_ATK;
        M_DEF = original.M_DEF;
        DEX = original.DEX;
        LUC = original.LUC;
        SkillIds = new List<int>(original.SkillIds.Count);
        for (int i = 0; i < original.SkillIds.Count; i++)
        {
            SkillIds.Add(original.SkillIds[i]);
        }
    }
}

上記のようにSkillIdの初期化でリストを新たに生成しないで SkillIds = original.SkillIds としてしまうと、ShallowCloneになってしまうため、しっかりDeepCopyになってないかの確認が重要です。

DeepCopy用メソッド/インターフェースを定義

もしインターフェースの場合は以下のようなものを定義すれば問題ありません。
(名前は ICloneable 等、もっと直接的の方が良いかもしれません)

IPrototype.cs
public interface IPrototype<T>
{
    T GetClone();
    T DeepCopy();
}

ジェネリクスにしておくことで、様々なクラスにそのまま転用可能なため、先ほどのクラスに適応すると以下のようになります。

PrototypedDeckChara.cs
public class DeckChara : IPrototype<DeckChara>
{
    public string Name { get; protected set; }
    /// <summary>
    /// 現在HP
    /// </summary>
    public int HP { get; protected set; }
    /// <summary>
    /// 最大HP
    /// </summary>
    public int MaxHP { get; protected set; }
    /// <summary>
    /// 現在Skillポイント
    /// </summary>
    public int SP { get; protected set; }
    /// <summary>
    /// 最大スキルポイント
    /// </summary>
    public int MaxSP { get; protected set; }
    /// <summary>
    /// 攻撃力
    /// </summary>
    public int ATK { get; protected set; }
    /// <summary>
    /// 防御力
    /// </summary>
    public int DEF { get; protected set; }
    /// <summary>
    /// 魔法攻撃力
    /// </summary>
    public int M_ATK { get; protected set; }
    /// <summary>
    /// 魔法防御力
    /// </summary>
    public int M_DEF { get; protected set; }
    /// <summary>
    /// 敏捷
    /// </summary>
    public int DEX { get; protected set; }
    /// <summary>
    /// 運
    /// </summary>
    public int LUC { get; protected set; }
    /// <summary>
    /// スキルIDリスト
    /// </summary>
    public List<int> SkillIds { get; protected set; }

    /// <summary>
    /// 通常のコンストラクタ
    /// </summary>
    public DeckChara()
    {
        Name = string.Intern("UNKNOWN");
        MaxHP = 10;
        HP = 10;
        MaxSP = 1;
        SP = 1;
        ATK = 5;
        DEF = 0;
        M_ATK = 3;
        M_DEF = 0;
        DEX = 5;
        LUC = 0;
        SkillIds = new List<int>();
    }
    /// <summary>
    /// コピーコンストラクタ
    /// </summary>
    public DeckChara(DeckChara original)
    {
        Name = original.Name;
        MaxHP = original.MaxHP; 
        HP = original.HP; 
        MaxSP = original.MaxSP;
        SP = original.SP;
        ATK = original.ATK;
        DEF = original.DEF;
        M_ATK = original.M_ATK;
        M_DEF = original.M_DEF;
        DEX = original.DEX;
        LUC = original.LUC;
        SkillIds = new List<int>(original.SkillIds.Count);
        for (int i = 0; i < original.SkillIds.Count; i++)
        {
            SkillIds.Add(original.SkillIds[i]);
        }
    }
    
    DeckChara IPrototype<DeckChara>.GetClone()
    {
        return new DeckChara(this);
    }
    DeckChara IPrototype<DeckChara>.DeepCopy()
    {
        return new DeckChara(this);
    }
}

ゲーム中の使い方

要するにコピーを生成するので以下のような利用例が考えられます

  • 操作はAI任せのデコイを生成
  • ビット(ファンネル) などの追加・複製
  • プレイヤーそっくりの敵の生成

また、AbstractFactory 等で生成するインスタンスの初期値を変更する場合、Prototypeパターンがあれば
パラメータの引き継ぎなどがしやすくなるでしょう。

まとめ

Prototype パターンはクローンを生成するデザインパターンです。

他のインスタンスから初期化したい場合は コピーコンストラクタ , 自身のクローンを作成する場合はメソッドを生やしてコピーコンストラクタに自身を入れて生成したインスタンスを返せばOKです。

また、この方法は別記事で紹介するメメントパターンにも通ずるテクニックなので覚えておいて損はないでしょう。

6
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
1