【Unity】MonoBehaviourを使うことで発生する問題について
環境
Unity 2020.2.0f1
はじめに
UnityでMonoBehaviourクラスを作ることによって発生する問題について書いてみます。
キャラクタークラス
Unityでゲームを作ることを考えてみましょう。
ユーザーが操作するキャラクターのクラスを作るとき、
以下のようなMonoBehaviourクラスを作ることが多いのではないでしょうか。
using UnityEngine;
public class PlayerCharacter : MonoBehaviour
{
}
キャラクターの表示・非表示を実装する
以下のような機能を実装したいとしましょう
Spaceキーを押したら、キャラクターの表示・非表示を切り替える
キャラクターの実体と、それを操作するクラスは別クラスに分けた方が拡張性が高くなりそうです。
今回はプレイヤーを操作するPlayerController
クラスを作成します。
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
メソッドを追加して、表示・非表示機能を実装した方がキレイに見えそうです。
using UnityEngine;
public class PlayerCharacter : MonoBehaviour
{
// キャラクターの表示・非表示
public void SetActive(bool active)
{
gameObject.SetActive(active);
}
}
そして、PlayerController
クラスはSetActive
メソッドを利用してキャラクターのON/OFFします。
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メソッドを以下のように書き換えたとしましょう。
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機能がバグります。
// 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の継承をやめることによって回避できます。
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
と書くと、コンパイルエラーになります)
// Spaceキーを押したら、ON・OFF切り替え
if (Input.GetKeyDown(KeyCode.Space))
{
if (character.IsActive)
character.SetActive(false);
else
character.SetActive(true);
}
MonoBehaviourを継承しない場合
PlayerCharacter
がMonoBehaviourを継承しない場合、
SerializeField
が使えないので、
何かしらの形でPlayerCharacter
の参照を外部から設定してあげる必要が出てきます。
プレイヤーキャラの見た目を管理する Viewクラス
using UnityEngine;
// Inspector上で設定したいデータを持ったMonoBehaviourクラス
public class PlayerView : MonoBehaviour
{
[field: SerializeField] public MeshRenderer meshRenderer { get; private set; }
}
プレイヤーキャラの操作を行うControllerクラス
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);
}
}
}
プレイヤーキャラ
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