ぶろぐめんどくさい

技術系の記事と漫画レビューが入り混じった混沌

Arborを使ったスクリプティング

ArborはUnityでのビジュアルプログラミングを提供してくれる非常に便利なツールです。 現在バージョン3が発売されています。 私はバージョン2のArborを持っていたのですが、バージョン3になって機能が増えたらしく、セールが来たタイミングで購入しました。

ArborにはさまざまなBehaiver(挙動)が用意されています。 演算ノードも加わり、用意されているものだけでも十分にゲームを創ることが可能です。 ですが、ユーザのスクリプティングを視野に入れたツールのため、たまにかゆいところに手が届かないときもあります。 例えば、マウスの位置を取得する演算ノードがなかったり、TweenをWorldl座標で行う挙動がなかったりします。 そういうときには、自分で演算ノードや挙動を用意する必要があります。

ビジュアルプログラミングのツールを使ってるのに、スクリプトはあまり書きたくないと思われる方は多いでしょう。 僕もその口です。 ですが、用意されたものだけで理想の動作を目指すよりも、スクリプトを組んだ方が往々にして早いです。 理由は単純で、コピペでスクリプトが組めるからです。

Arborでは、用意されている挙動や演算ノードのスクリプトを読むことができます。 以下のスクリプトはTweenPositionのものです。

using UnityEngine;
using UnityEngine.Serialization;
using System.Collections;

namespace Arbor.StateMachine.StateBehaviours
{
#if ARBOR_DOC_JA
    /// <summary>
    /// 座標を徐々に変化させる。
    /// </summary>
#else
    /// <summary>
    /// Gradually change position.
    /// </summary>
#endif
    [AddComponentMenu("")]
    [AddBehaviourMenu("Tween/TweenPosition")]
    [BuiltInBehaviour]
    public sealed class TweenPosition : TweenBase, INodeBehaviourSerializationCallbackReceiver
    {
       #region Serialize fields

#if ARBOR_DOC_JA
        /// <summary>
        /// 対象となるTransform。<br/>
        /// TypeがConstantの時に指定しない場合、ArborFSMがアタッチされているGameObjectのTransformとなる。
        /// </summary>
#else
        /// <summary>
        /// Transform of interest.<br/>
        /// If Type is Constant and nothing is specified, ArborFSM is the Transform of the attached GameObject.
        /// </summary>
#endif
        [SerializeField]
        private FlexibleTransform _Target = new FlexibleTransform();

#if ARBOR_DOC_JA
        /// <summary>
        /// 開始した状態からの相対的な変化かどうか。
        /// </summary>
#else
        /// <summary>
        /// Whether the relative change from the start state.
        /// </summary>
#endif
        [SerializeField]
        private bool _Relative = false;

#if ARBOR_DOC_JA
        /// <summary>
        /// 開始地点。
        /// </summary>
#else
        /// <summary>
        /// Starting point.
        /// </summary>
#endif
        [SerializeField]
        private FlexibleVector3 _From = new FlexibleVector3();

#if ARBOR_DOC_JA
        /// <summary>
        /// 目標地点。
        /// </summary>
#else
        /// <summary>
        /// Target point.
        /// </summary>
#endif
        [SerializeField]
        private FlexibleVector3 _To = new FlexibleVector3();

        [SerializeField]
        [HideInInspector]
        private int _SerializeVersion;

       #region old

        [SerializeField, FormerlySerializedAs( "_Target" )]
        [HideInInspector]
        private Transform _OldTarget = null;

        [SerializeField, FormerlySerializedAs( "_From" )]
        [HideInInspector]
        private Vector3 _OldFrom = Vector3.zero;

        [SerializeField, FormerlySerializedAs( "_To" )]
        [HideInInspector]
        private Vector3 _OldTo = Vector3.zero;

       #endregion // old

       #endregion // Serialize fields

        Transform _MyTransform;
        Transform cachedTarget
        {
            get
            {
                Transform transform = _Target.value;
                if (transform == null && _Target.type == FlexibleTransform.Type.Constant )
                {
                    if( _MyTransform == null )
                    {
                        _MyTransform = GetComponent<Transform>();
                    }

                    transform = _MyTransform;
                }
                return transform;
            }
        }

        void SerializeVer1()
        {
            _Target = (FlexibleTransform)_OldTarget;
            _From = (FlexibleVector3)_OldFrom;
            _To = (FlexibleVector3)_OldTo;
        }

        void INodeBehaviourSerializationCallbackReceiver.OnBeforeSerialize()
        {
            if (_SerializeVersion == 0)
            {
                SerializeVer1();
                _SerializeVersion = 1;
            }
        }

        void INodeBehaviourSerializationCallbackReceiver.OnAfterDeserialize()
        {
            if (_SerializeVersion == 0)
            {
                SerializeVer1();
                _SerializeVersion = 1;
            }
        }
        
        Vector3 _StartPosition;

        protected override void OnTweenBegin()
        {
            Transform target = cachedTarget;

            if (_Relative && target != null)
            {
                _StartPosition = target.localPosition;
            }
            else
            {
                _StartPosition = Vector3.zero;
            }
        }

        protected override void OnTweenUpdate(float factor)
        {
            Transform target = cachedTarget;
            if (target != null)
            {
                target.localPosition = _StartPosition + Vector3.Lerp(_From.value, _To.value, factor);
            }
        }
    }
}

なっが。

このスクリプトをコピペしてWorld座標でTweenするスクリプトを作ります。 元のスクリプトは長いですが、変更点は3つだけです。 [AddBehaviourMenu("Tween/TweenPosition")]の中身とクラス名を任意のものに変更し、localPositionと書かれている部分をpositionに書き換えるだけで、目的のものができあがります。

具体的な変更点を以下に挙げます。

  • [AddBehaviourMenu("Tween/TweenPosition")][AddBehaviourMenu("MyScripts/Tween/TweenWorldPosition")]
  • クラス名TweenPositionTweenWorldPosition
  • localPositionpositionで置き換え

これらの修正をしたスクリプトが以下のものです。

using UnityEngine;
using UnityEngine.Serialization;
using System.Collections;

namespace Arbor.StateMachine.StateBehaviours
{
#if ARBOR_DOC_JA
    /// <summary>
    /// 座標を徐々に変化させる。
    /// </summary>
#else
    /// <summary>
    /// Gradually change world position.
    /// </summary>
#endif
    [AddComponentMenu("")]
    [AddBehaviourMenu("MyScripts/Tween/TweenWorldPosition")]
    [BuiltInBehaviour]
    public sealed class TweenWorldPosition : TweenBase, INodeBehaviourSerializationCallbackReceiver
    {
       #region Serialize fields

#if ARBOR_DOC_JA
        /// <summary>
        /// 対象となるTransform。<br/>
        /// TypeがConstantの時に指定しない場合、ArborFSMがアタッチされているGameObjectのTransformとなる。
        /// </summary>
#else
        /// <summary>
        /// Transform of interest.<br/>
        /// If Type is Constant and nothing is specified, ArborFSM is the Transform of the attached GameObject.
        /// </summary>
#endif
        [SerializeField]
        private FlexibleTransform _Target = new FlexibleTransform();

#if ARBOR_DOC_JA
        /// <summary>
        /// 開始した状態からの相対的な変化かどうか。
        /// </summary>
#else
        /// <summary>
        /// Whether the relative change from the start state.
        /// </summary>
#endif
        [SerializeField]
        private bool _Relative = false;

#if ARBOR_DOC_JA
        /// <summary>
        /// 開始地点。
        /// </summary>
#else
        /// <summary>
        /// Starting point.
        /// </summary>
#endif
        [SerializeField]
        private FlexibleVector3 _From = new FlexibleVector3();

#if ARBOR_DOC_JA
        /// <summary>
        /// 目標地点。
        /// </summary>
#else
        /// <summary>
        /// Target point.
        /// </summary>
#endif
        [SerializeField]
        private FlexibleVector3 _To = new FlexibleVector3();

        [SerializeField]
        [HideInInspector]
        private int _SerializeVersion;

       #region old

        [SerializeField, FormerlySerializedAs( "_Target" )]
        [HideInInspector]
        private Transform _OldTarget = null;

        [SerializeField, FormerlySerializedAs( "_From" )]
        [HideInInspector]
        private Vector3 _OldFrom = Vector3.zero;

        [SerializeField, FormerlySerializedAs( "_To" )]
        [HideInInspector]
        private Vector3 _OldTo = Vector3.zero;

       #endregion // old

       #endregion // Serialize fields

        Transform _MyTransform;
        Transform cachedTarget
        {
            get
            {
                Transform transform = _Target.value;
                if (transform == null && _Target.type == FlexibleTransform.Type.Constant )
                {
                    if( _MyTransform == null )
                    {
                        _MyTransform = GetComponent<Transform>();
                    }

                    transform = _MyTransform;
                }
                return transform;
            }
        }

        void SerializeVer1()
        {
            _Target = (FlexibleTransform)_OldTarget;
            _From = (FlexibleVector3)_OldFrom;
            _To = (FlexibleVector3)_OldTo;
        }

        void INodeBehaviourSerializationCallbackReceiver.OnBeforeSerialize()
        {
            if (_SerializeVersion == 0)
            {
                SerializeVer1();
                _SerializeVersion = 1;
            }
        }

        void INodeBehaviourSerializationCallbackReceiver.OnAfterDeserialize()
        {
            if (_SerializeVersion == 0)
            {
                SerializeVer1();
                _SerializeVersion = 1;
            }
        }
        
        Vector3 _StartPosition;

        protected override void OnTweenBegin()
        {
            Transform target = cachedTarget;

            if (_Relative && target != null)
            {
                _StartPosition = target.position;
            }
            else
            {
                _StartPosition = Vector3.zero;
            }
        }

        protected override void OnTweenUpdate(float factor)
        {
            Transform target = cachedTarget;
            if (target != null)
            {
                target.position = _StartPosition + Vector3.Lerp(_From.value, _To.value, factor);
            }
        }
    }
}

長すぎて変更点がわからない?それだけ変更点が少ないということです。

一方で、短いスクリプトもあります。 マウスカーソルのWorld座標を取得する演算ノードを自作すると50行も行きません。

using UnityEngine;
using System.Collections;

namespace Arbor
{
#if ARBOR_DOC_JA
    /// <summary>
    /// マウスの位置を取得する
    /// </summary>
#else
    /// <summary>
    /// Get mouse Position
    /// </summary>
#endif
    [AddComponentMenu("")]
    [AddCalculatorMenu("MyScripts/Vectior3/MousePosition")]
    [BuiltInBehaviour]
    public sealed class MousePosition : Calculator
    {
       #region Serialize fields

#if ARBOR_DOC_JA
        /// <summary>
        /// マウスの位置
        /// </summary>
#else
        /// <summary>
        /// mouse position
        /// </summary>
#endif
        [SerializeField]
        private OutputSlotVector3 _Result = new OutputSlotVector3();

        #endregion

        public override void OnCalculate()
        {
            Vector3 mousePosition = Camera.main.ScreenToWorldPoint(Input.mousePosition);
            _Result.SetValue(mousePosition);
        }
    }

}

このスクリプトでは、ScreenToWorldPointで得たマウスカーソルのWorld座標を出力変数に設定しています。 スクリプトが短い理由は明白で、行うことが少ないからです。 このスクリプトも、適当な演算ノードのスクリプトをコピペして作っています。 コピペするだけでスクリプトが書けるのでArborは思った以上に敷居が低いです。

所感ですが、値段や日本産という点を考えると、PlaymakerよりもArborを買うほうが良いです。 私は、Playmakerは使いこなせませんが、Arborはある程度使えます。 Unityでビジュアルプログラミングをしたい皆さん、せっかくなのでArborを買いましょう。

わたしArborちょっとつかえる。