建設とUnity

Unityを使って、建設業界向けのソフトを開発するノウハウを紹介します。

【最小構成】Unityでモデルを選択/複数選択/選択解除できるシステムをつくってみた

以下の記事で説明したように3Dデータを読みこみ、モデルを生成したあとはモデルを操作したくなります。

【Blender→Unity】STLのランタイム読込とZ-up・右手系からY-up・左手系への変換方法 - 建設とUnity (hatenablog.jp)

【親子関係】Unityでメッシュの中心位置とtransformの位置を一致させる方法 - 建設とUnity (hatenablog.jp)

ご存知の通り、Unityエディタのシーン上ではモデルを選択して操作できます。モデルを選択すると、オレンジのアウトラインが表示され、ガムボールと呼ばれる3軸の矢印(回転の場合は3平面の円などと状態によって変わる)で構成された操作機能が表示されます。

今後、アウトラインやガムボールについても説明していきますが、今回はまずモデルを選択して、選択解除できるシステムをつくるところを説明します。モデルを選択/操作することができるツールをつくる際は、選択/選択解除の機能が重要になってきます。

UniRxの利用

こちらの機能を実装するにあたり、UniRxを利用します。もちろん利用しなくても実装できますが、こちらが便利ですし、UniRxを用いて、MV(Reactive)Pを意識してコアなシステムを構築することでスパゲッティコードになりにくくなります(個人の感想です)。今回はMVPのうち、ModelとViewのみですが(コードとしてはModelのみ)、おいおいPresenterについても説明していきます。

UniRxはUnityのパッケージマネージャーからインポートするか、以下のURLからダウンロードして利用してください。

GitHub - neuecc/UniRx: Reactive Extensions for Unity

以下に記載するコードの中で、UniRxを使ってSubscribeする際はすべてAddTo(this)をつけていますが、 UniRx.Triggersで実装されているObservableTriggersについては、 GameObjectが破棄されるとOnCompleteメッセージが発行されるため、本来不要です。

ただ、UniRxによるものか、UniRx.Triggersのものか間違えて、破棄し忘れるとよくない(区別するのがめんどう…)ので、 すべてにAddTo(this)をつけるようにしています。

Modelの実装

今回、実装する主なクラスは2つです。それぞれSelectSystem、Modelというクラスです。 最後にModelを継承したCubeModelも実装しますが、肝心なのはこの2つのクラスです。

SelectSystem

まず、SelectSystemは以下の通りです。 こちらのクラスの役割は単純で、選択された場合は選択されたModelをコレクションに追加して、選択解除された場合は解除するようになっています。

また、実装の都合上、クリック時に対象のモデルがない場合(空選択の場合)、コレクションを初期化するようになっています。

using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class SelectSystem : MonoBehaviour
{
    public IReadOnlyReactiveCollection<Model> SelectedModelRc => selectedModelRc;
    private ReactiveCollection<Model> selectedModelRc = new ReactiveCollection<Model>();

    private void Start()
    {
        // クリック時に選択対象がない場合は選択解除
        this.UpdateAsObservable()
            .Where(_ => Input.GetMouseButtonDown(0))
            .Subscribe(_ =>
            {
                var cameraRay = Camera.main.ScreenPointToRay(Input.mousePosition);
                if (!Physics.Raycast(cameraRay, out RaycastHit hit))
                    Clear();
            })
            .AddTo(this);
    }

    public void Add(Model model)
    {
        selectedModelRc.Add(model);
    }

    public void Remove(Model model)
    {
        selectedModelRc.Remove(model);
    }

    public int Count()
    {
        return selectedModelRc.Count;
    }

    public void Clear()
    {
        foreach (Model model in selectedModelRc)
        {
            model.Clear();
        }
        selectedModelRc.Clear();
    }
}

Model

次のModelは以下の通りです。 このクラスで管理する最も重要なパラメータは選択されたか、されていないかを表すActiveRpです。

マウスでモデルをクリックした際に、selectSystemにまだ選択されていなければ追加、選択されていれば選択解除するようになっています。 また、先ほどのSelectSystemで説明したように、たとえばマウスが対象のモデル以外をクリックしたときに選択解除するためのClearというメソッドを用意しています。

なお、実装の都合上、左側のCtrlを押していない場合は新規選択することとして、それまで選択していたモデルをすべて解除した上で選択するようにしています。 なお、左側のCtrlを押している場合は、モデルを複数選択できるように実装しています。

using UnityEngine;
using UniRx;
using UniRx.Triggers;

public class Model : MonoBehaviour
{
    [SerializeField] private SelectSystem selectSystem;

    public IReadOnlyReactiveProperty<bool> ActiveRp => activeRp;
    private ReactiveProperty<bool> activeRp = new ReactiveProperty<bool>(false);

    protected virtual void Start()
    {
        this.OnMouseDownAsObservable()
            .Subscribe(_ =>
            {
                if (!Input.GetKey(KeyCode.LeftControl))
                    selectSystem.Clear();

                Select();
            })
            .AddTo(this);
    }

    private void Select()
    {
        activeRp.Value = !activeRp.Value;
        if (activeRp.Value)
            selectSystem.Add(this);
        else
            selectSystem.Remove(this);
    }

    public void Clear()
    {
        activeRp.Value = false;
    }
}

SelectSystemとModelの使い方

SlectSystemはシーン上の空のゲームオブジェクトにアタッチして、Modelは選択対象のモデルにアタッチして使用します。

ただこのまま起動しても、裏側のコード上で選択されたり、選択解除されるだけで選択状態が目に見えません。 そのため、Modelを継承したCubeModelというクラスを追加で実装します。

CubeModelでは、選択状態の変化をActiveRpを通じて検知して、 選択されればモデルの色を黄色にして、選択解除されれば白色に戻すようにしています。

選択対象のモデルに、Modelではなく、CubeModelをアタッチすることで、選択と選択解除の動作確認ができます。 ここで、モデルを複数用意した場合は、選択と選択解除に加えて、複数選択できます。

f:id:sayadoki:20220112205705p:plain

なお、CubeModelのコードではmeshRenderer.material.colorとして簡易に実装しています。 実はUnityではmaterialにアクセスしてしまうとコピーが生成されて、描画負荷に影響を与えるSetPass callsが増えます※。 とはいえ、sharedMaterialを使った場合は同じマテリアルを共有しているモデルすべての色が変わってしまうため、NGです。 ※Built-in RPの場合です。URPやHDRPのSRPについては詳しくないので現状はわかりません。SRP Batcherなどでよしなにしてくれる可能性もあります。

SetPass callsを不必要に増やさない方法は今後記事にします。

using UnityEngine;
using UniRx;

public class CubeModel : Model
{
    private MeshRenderer meshRenderer;

    protected override void Start()
    {
        base.Start();

        meshRenderer = GetComponent<MeshRenderer>();

        ActiveRp
            .Subscribe(isActive => 
            {
                if (isActive)
                    meshRenderer.material.color = Color.yellow;
                else
                    meshRenderer.material.color = Color.white;
            })
            .AddTo(this);
    }
}

実装のポイント

今回は、選択/複数選択/選択解除するための最小構成の選択システムを紹介しています。 これを応用することで、たとえば建築部材を選択した際はガムボールで動かしたり、 トラックを選択した際は走行させたり、重機を選択した際は重機にあった操作をしたり、といったことができます。

ポイントはModelのActiveRpによって選択か非選択かを管理していることです。 どのような操作をするかは、モデル(トラックか重機かなど)の種類に合わせてModelを継承した上で実装することになります。