10
1

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

【Unity】Unityで学ぶデザインパターン12: Proxy パターン【デザパタ】

Last updated at Posted at 2023-06-23

はじめに

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

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

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

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

  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 パターン

Proxy パターンについて

代理人 という意味を持つ Proxy
具体的には、ある処理を行うときに、その処理が複雑だったり長かったり、レスポンスに時間を要する場合に、別のクラスやモジュールに処理を依頼することがあります。この処理を依頼された側を proxy と呼称します。

Facade パターンとの違い

代理人という意味では窓口になる Facade パターンも、Proxyパターンの一種として見なすこともできます。
確かに単純なインターフェース実行するときには最初にFacadeパターンが窓口になって代理実行して返すこともあります。
また、Facade内でも処理が長くなる場合は、機能を分割したサブクラスに実行依頼をするという proxy が別のproxy に代理実行依頼をするようなパターンもあります。
ただ、Facadeパターンは以前の記事でも解説した通り 複雑なインターフェースの代わりに簡易なAPIを提供する 役目もあるため、そこは明確にproxyパターンとの役割の違いがあります。

Proxy パターンの種類

Proxy パターンは、その特性から大きく4つに大別されます。

  1. リモートプロキシ
  2. 仮想プロキシ
  3. プロテクションプロキシ
  4. スマートリファレンスプロキシ

リモートプロキシ

リモートプロキシはおそらく一番有名なプロキシパターンの使い方です。
ローカルにあるObjectがリモートにあるObjectの代理として存在するような場合です。
そしてリモートにあるObjectを操作するためにローカルObjectからRPC(RemoteProcedureCall)を発行することでローカルObjectと同じ動きをRemoteにあるObjectにも適応することができます。

ゲーム開発では多人数プレイのゲームでは必須級の技術です。

仮想プロキシ

仮想プロキシとは、あたかも実体(インスタンス)があるかのように振る舞うプロキシです。
特に重い処理(I/O や描画等)を行う場合に、Commandパターンを利用して処理を遅延するという使い方もあるため 軽量プロキシ とも呼ばれます。

プロテクションプロキシ

依頼主の権限レベルに合わせた適切なProxyを渡すことで、セキュリティレベルを制御するという方法があります。

スマートリファレンスプロキシ

C#ではあまり意識しませんが、C++ではスマートポインタと呼ばれるポインタ操作用の仕組みがあります。
またObjective-cではポインタのライフサイクル管理のために参照カウントを持って、参照カウントが0になったタイミングで初めてリソースの解放を行うように作ります。

この「Objectの間接参照時」に何か操作を加える(今回であれば参照カウントを増やす・減らす)ようなパターンがスマートリファレンスプロキシと呼ばれるようです。

ゲーム における Proxy パターン

おそらくゲームでも小規模のプロジェクト(例えばワンボタンアクションのゲーム等)を除けば、処理の分割・委譲 というのは日常茶飯事で起きることかと思います。
この委譲先のサブクラスで同じインターフェースを実装していれば、まさに Proxy パターンそのものになるということです。

さてここからはプロキシパターンの使い方でゲームに特化した例を見ていきましょう。

リモートプロキシ

先ほどの説明の通りオンラインマルチプレイゲームでは必須テクニックです。
Unityでいえば Photon のRPC などが有名でしょう。

仮想プロキシ

ゲームでの仮想プロキシ例として オープンワールドゲーム における Object操作 があります。
オープンワールドゲームでは広大なフィールドが舞台なため、基本的にプレイヤーの周囲以外のObjectは描画されません。

しかし、描画されないObjectは止まっているのかというとそうではありません。そのため描画対象外のObjectでも、情報としてObjectの更新は必要です。

そこで以下のように Transform 情報を持つProxyクラスを用意して、基本的なループはこれを更新し、描画対象になったタイミングで初めてObjectをInstantiateしたりGameObject.SetActive(true)を呼ぶことで実体を生成するという方法があります。

このようにしておくことで、最初から全てのObjectを生成せずとも座標情報の更新が行えるというメリットがあります

GameObjectProxy.cs

using UnityEngine;


public class GameObjectProxy
{
    public Vector3 Position { get; protected set; }
    public Quaternion Rotation { get; protected set; }
    public Vector3 Scale { get; protected set; }
    public bool IsActive { get; protected set; }
    private GameObject m_instance = null;
    private Object m_prefab = null;

    public ObjectProxy(Object prefab)
    {
        m_prefab = prefab;
    }
    
    public void SetPosition(Vector3 pos)
    {
        Position = pos;
        m_instance?.transform.SetPositionAndRotation(Position, Rotation);
    }
    public void SetRotation(Quaternion rot)
    {
        Rotation = rot;
        m_instance?.transform.SetPositionAndRotation(Position, Rotation);
    }
    public void SetScale(Vector3 scale)
    {
        Scale = scale;
        if(m_instance!=null) m_instance.transform.localScale = Scale;
    }

    public void Activate()
    {
        IsActive = true;
        if (m_instance == null) CreateInstance();
        m_instance.SetActive(true);
    }
    
    public void Deactivate()
    {
        IsActive = false;
        m_instance?.SetActive(false);
    }

    protected void CreateInstance()
    {
        m_instance = (GameObject)Object.Instantiate(m_prefab, Position, Rotation);
    }
}


プロテクションプロキシ

例えば、マルチプレイにあたって、自分自身はパラメータを変更可能 ですが、他プレイヤーのパラメータに関しては変更するのは非常にまずいです。場合によってはチート行為を助長することにもつながりかねません。

そこでキャラクターパラメータにReadOnly と普通のと用意し、それぞれを権限保持状態に応じて渡すことでコード的にアクセスをブロックすることができます。

CharacterParamProxy.cs

public class CharacterParameter
{
    public string Name;
    public int MaxHP;
    public int CurrentHP;
    public int ATK;
    public int DEF;
}

public interface ICharacterParamOperator
{
    string GetName();
    void SetName(string name);

    int GetMaxHP();
    void SetMaxHP(int hp);

    int GetCurrentHP();
    void SetCurrentHP(int hp);

    int GetATK();
    void SetATK(int atk);

    
    int GetDEF();
    void SetDEF(int def);
}

public class LocalCharaProxy : ICharacterParamOperator
{
    private CharacterParameter m_myParam = new CharacterParameter();

    #region ===== ICharacterParamOperator =====

    string ICharacterParamOperator.GetName() => m_myParam.Name;

    void ICharacterParamOperator.SetName(string name) { m_myParam.Name = name;}

    int ICharacterParamOperator.GetMaxHP() => m_myParam.MaxHP;
    void ICharacterParamOperator.SetMaxHP(int hp){ m_myParam.MaxHP = hp;}

    int ICharacterParamOperator.GetCurrentHP() => m_myParam.CurrentHP;
    void ICharacterParamOperator.SetCurrentHP(int hp){ m_myParam.CurrentHP = hp;}

    int ICharacterParamOperator.GetATK() => m_myParam.ATK;
    void ICharacterParamOperator.SetATK(int atk){ m_myParam.ATK = atk;}

    
    int ICharacterParamOperator.GetDEF() => m_myParam.DEF;
    void ICharacterParamOperator.SetDEF(int def){ m_myParam.DEF = def;}

    #endregion //) ===== ICharacterParamOperator =====
}

public class RemoteCharaProxy : ICharacterParamOperator
{
    private CharacterParameter m_myParam = new CharacterParameter();

    #region ===== ICharacterParamOperator =====

    string ICharacterParamOperator.GetName() => m_myParam.Name;

    void ICharacterParamOperator.SetName(string name) { }

    int ICharacterParamOperator.GetMaxHP() => m_myParam.MaxHP;
    void ICharacterParamOperator.SetMaxHP(int hp){ }

    int ICharacterParamOperator.GetCurrentHP() => m_myParam.CurrentHP;
    void ICharacterParamOperator.SetCurrentHP(int hp){ }

    int ICharacterParamOperator.GetATK() => m_myParam.ATK;
    void ICharacterParamOperator.SetATK(int atk){ }

    
    int ICharacterParamOperator.GetDEF() => m_myParam.DEF;
    void ICharacterParamOperator.SetDEF(int def){ }

    #endregion //) ===== ICharacterParamOperator =====
}

このように同じインターフェースでも権限毎に実装を変えたプロキシクラスを用意しておくことで、安全にパラメータ操作を行うことが可能です。

スマートリファレンスプロキシ

Unityでいえば Addressables の管理システムがまさにこれです。
AddressablesAssetBundle システムをラッパーした仕組みになっているのですが、このAssetBundleというファイルは内部に1つ以上のAssetを内包しています。
このとき、AssetをAssetBundleから取り出すタイミングで、AssetBundleからロードすればいいのですが、1度Open状態になっているAssetBundleファイルに再度Openリクエストを発行すると例外が発生します。
そのためAssetBundleはOpen中なのかどうかを管理する必要があります。

Addressbles登場前はここを自前で管理・設計するため難易度が高かったのですが、Addressablesは参照カウントを公式がサポートしてくれたためにだいぶ楽になりました。

参考:https://qiita.com/Cova8bitdot/items/20e8962ed0ca0544289d

まとめ

Proxy パターンは非常に多岐にわたって使われるデザインパターンです。
ただ委譲するだけでなく、権限やパフォーマンス稼ぎなどさまざまな用途に利用可能なので、ぜひ習得しましょう!

参考

10
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
10
1