📝

【Unity】MonoBehaviourを使うことで発生する問題について

2021/02/06に公開

環境

Unity 2020.2.0f1

はじめに

UnityでMonoBehaviourクラスを作ることによって発生する問題について書いてみます。

キャラクタークラス

Unityでゲームを作ることを考えてみましょう。

ユーザーが操作するキャラクターのクラスを作るとき、
以下のようなMonoBehaviourクラスを作ることが多いのではないでしょうか。

PlayerCharacter.cs
using UnityEngine;

public class PlayerCharacter : MonoBehaviour
{
}

キャラクターの表示・非表示を実装する

以下のような機能を実装したいとしましょう

Spaceキーを押したら、キャラクターの表示・非表示を切り替える

キャラクターの実体と、それを操作するクラスは別クラスに分けた方が拡張性が高くなりそうです。
今回はプレイヤーを操作するPlayerControllerクラスを作成します。

PlayerController.cs
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    // 動かす対象のプレイヤーキャラ
    [SerializeField] private PlayerCharacter character;

    void FixedUpdate()
    {
        // Spaceキーを押したら、ON・OFF切り替え
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (character.gameObject.activeSelf)
                character.gameObject.SetActive(false);
            else
                character.gameObject.SetActive(true);
        }
    }
}

クラスの参照関係は以下のようになります。

キャラクター表示はメソッドで行いたい

PlayerCharacterクラスにSetActiveメソッドを追加して、表示・非表示機能を実装した方がキレイに見えそうです。

PlayerCharacter.cs
using UnityEngine;

public class PlayerCharacter : MonoBehaviour
{
    // キャラクターの表示・非表示
    public void SetActive(bool active)
    {
        gameObject.SetActive(active);
    }
}

そして、PlayerControllerクラスはSetActiveメソッドを利用してキャラクターのON/OFFします。

PlayerController.cs
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    // 動かす対象のプレイヤーキャラ
    [SerializeField] private PlayerCharacter character;

    void FixedUpdate()
    {
        // Spaceキーを押したら、ON・OFF切り替え
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (character.gameObject.activeSelf)
                character.SetActive(false);
            else
                character.SetActive(true);
        }
    }
}

Spaceキーを押せばキャラクターON/OFFが切り替わるので、問題無いように見えます。

SetActiveの実装を変更するとバグる

しばらくしてから、
「キャラクターの表示・非表示はやっぱりMeshRendererのON/OFFでやりたい」
と思って、SetActiveメソッドを以下のように書き換えたとしましょう。

PlayerCharacter.cs
using UnityEngine;

public class PlayerCharacter : MonoBehaviour
{
    [SerializeField] private MeshRenderer meshRenderer;
    
    // アクティブかどうかを取得するためのフラグも用意してみた
    public bool IsActive => meshRenderer.enabled;

    public void SetActive(bool active)
    {
        // 表示・非表示はMeshRendererでやりたくなった
        meshRenderer.enabled = active;
    }
}

すると、PlayerController側のキャラON/OFF機能がバグります。

PlayerController.cs
// Spaceキーを押したら、ON・OFF切り替え
if (Input.GetKeyDown(KeyCode.Space))
{
    if (character.gameObject.activeSelf)
        character.SetActive(false);
    else
        character.SetActive(true);
}

本来ならばcharacter.IsActiveでアクティブ状態を取ってきてほしいのに、
character.gameObject.activeSelfを参照してしまっているため、
想定外の挙動を行うようになってしまいました。

【問題点】二通りの方法が取れてしまう

キャラクターのON/OFFを行う際に二通りの手段を取れてしまうという点に問題があります。

// どちらの方法でも、キャラクターを非表示にできてしまう
character.SetActive(false);
character.gameObject.SetActive(false);
// どちらの方法でも、キャラクターのアクティブを取得できるように見えてしまう
if (character.gameObject.activeSelf) { }
if (character.IsActive) { }

問題の解決策 : MonoBehaviourを隠蔽する

今回の問題は、PlayerCharacterクラスにて MonoBehaviourの継承をやめることによって回避できます。

PlayerCharacter.cs
public class PlayerCharacter
{
    private MeshRenderer meshRenderer;
    public bool IsActive => meshRenderer.enabled;
    
    // 初期化クラス
    public void Initialize(MeshRenderer meshRenderer)
    {
        this.meshRenderer = meshRenderer;
    }

    public void SetActive(bool active)
    {
        meshRenderer.enabled = active;
    }
}

PlayerController側のON/OFF機能の実装は以下の一つの書き方に定まります。
(character.gameObjectと書くと、コンパイルエラーになります)

PlayerController.cs
// Spaceキーを押したら、ON・OFF切り替え
if (Input.GetKeyDown(KeyCode.Space))
{
    if (character.IsActive)
        character.SetActive(false);
    else
        character.SetActive(true);
}

MonoBehaviourを継承しない場合

PlayerCharacterがMonoBehaviourを継承しない場合、
SerializeFieldが使えないので、
何かしらの形でPlayerCharacterの参照を外部から設定してあげる必要が出てきます。

プレイヤーキャラの見た目を管理する Viewクラス

PlayerView.cs
using UnityEngine;

// Inspector上で設定したいデータを持ったMonoBehaviourクラス
public class PlayerView : MonoBehaviour
{
    [field: SerializeField] public MeshRenderer meshRenderer { get; private set; }
}

プレイヤーキャラの操作を行うControllerクラス

PlayerController.cs
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    [SerializeField] private PlayerView view;
    
    // 動かす対象のプレイヤーキャラ
    private PlayerCharacter character;

    // 初期化
    void Start()
    {
        character.Initialize(view);
    }
    
    // 更新処理
    void FixedUpdate()
    {
        // Spaceキーを押したら、ON・OFF切り替え
        if (Input.GetKeyDown(KeyCode.Space))
        {
            if (character.IsActive)
                character.SetActive(false);
            else
                character.SetActive(true);
        }
    }
}

プレイヤーキャラ

PlayerCharacter.cs
public class PlayerCharacter
{
    private MeshRenderer meshRenderer;
    public bool IsActive => meshRenderer.enabled;
    
    // 初期化クラス
    public void Initialize(PlayerView view)
    {
        meshRenderer = view.meshRenderer;
    }

    public void SetActive(bool active)
    {
        meshRenderer.enabled = active;
    }
}

Discussion