ぶろぐめんどくさい

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

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ちょっとつかえる。

Visual Studio CodeでXMLドキュメントのコメントを効率化する

VS Codeには、///と打つだけで<summary>などのコメントを補完してくれる「C# XML Documentation Comments」という拡張機能があります。 拡張機能のインストール方法は以下のページに記載されています。(英文注意)

marketplace.visualstudio.com

インストール方法のみを抜粋して説明します。

  1. Visual Studio Codeのバージョン1.22.0以上をインストール
  2. Visual Studio Codeを起動
  3. Ctrl-Shift-X (Windows, Linux)かCmd-Shift-X (macOS)で拡張機能ビューを開く
  4. C# XML Documentation Comments”を検索してインストール
  5. Visual Studio Codeを再起動

4.の行程が終わると画像のような表示になるはずです。

f:id:be116:20180511105508p:plain

Visual Studio Codeを再起動して、拡張機能が有効になっているか確認します。

f:id:be116:20180511111032g:plain

うまく働いてくれているようです。

アルソックの警備員が来た日

その日はいつもよりちょっと遅めに起きました。 時計を確認するとびっくりしました。 既にアラームが鳴った後に起きたようです。 レム睡眠ってありますよね。 睡眠が深い状態であるレム睡眠、 浅い状態であるノンレム睡眠があるそうです。 おそらくレム睡眠の時間帯に無理やり起きたせいでしょう。 その日はとても眠かった。

ひとまずご飯を食べようと思い、 トーストをつくるため、フライパンに食パンを乗せて火をつけました。(IHですが) そして、あまりに眠かったので、ちょっとだけと思い布団に潜りました。

これが、明らかな失敗でした。 ちょっと横になるだけ。フライパンが温まるまでスマホを見るだけ。 そう思っていたはずなのに、気がついたときには、眠りに落ちていました。

なにかが焦げた匂いで起こされました。 火を止めなきゃと立ち上がった瞬間、火災警報が鳴りました。 部屋の中には煙が充満していました。 トーストも焼きすぎると煙が出るんですね。 おそろしいなぁ。 それよりもパニックです。 警報装置を止めようとしました。 ブレーカー式ではなく、解除ボタンがあるのでそれをひたすら押していました。 ですが、解除ボタンが機能しません。 警備システムがメインの警報装置だったため、 火災警報の場合は警報状態を解除をすることができないようです。 起き抜けの頭で必死だった私は、なにか反応するところはないかと思い、 非常と書かれたボタンを押しました。 携帯電話が鳴りました。

やばいと思いました。 電話に出ると、 「アルソックの〇〇です。どうされましたか」と早口でまくしたててきます。 非常事態でもないし、火災も起きていない。 「すみません。誤操作です。すみません」と私は謝りました。 「もう警備員が向かっていますので」とアルソックの方が。 ピザ警察を思い出す流れです。 オペレーターが通報に出ると、ピザの注文の電話でした。 オペレーターははじめ間違い電話ではないかと聞きましたが、様子がおかしいことに気づき、通報者に「はい」か「いいえ」で答えるように指示しました。 結果、ピザの注文は犯人にばれないためのカモフラージュで、犯人を捕まえることに成功しました。 ですが私はなんのカモフラージュもしていません。 ピザの注文などしておりません。 向かってくる警備員を止める術はありませんでした。

警備員が来るまでの間、トイレにいけないじゃないか。 来なくていいのにと思いながら20分ほど待つと、警備員がきました。 部屋の中に上がってもらい非常警報を解除してもらいました。 (火災警報は窓を開けて換気しているうちに止まりました) 最後に、「報告書(だったような)はいりますか?」と聞かれ、「いや、いいです」と答えました。

今回の件から得た教訓は、

  • 焦っても非常ボタンを押さない
  • 火をつけたまま布団に潜らない(寝ない)

です。

Rigidbodyの重心をずらすスクリプト(Unity)

まずは実行結果を示します。

使用したスクリプトはこれになります。 といっても、ここ(https://docs.unity3d.com/ja/current/ScriptReference/Rigidbody-centerOfMass.html)の改変です。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

/// <summary>
/// 重心の位置を変更
/// </summary>
public class CenterOfMass : MonoBehaviour {

    /// <summary>
    /// 重心の位置
    /// </summary>
    public Transform com;

    /// <summary>
    /// オブジェクトがEnableになったとき
    /// </summary>
    void OnEnable () {
        Rigidbody rb = GetComponent<Rigidbody>();
        rb.centerOfMass = com.localPosition;
    }
}

このスクリプトは、重心をずらしたいゲームオブジェクトにアタッチして使います。 このゲームオブジェクトの子に新たにゲームオブジェクトを作成し、 Com変数に作成したオブジェクトをアタッチします。 このオブジェクトが新しい重心となり、任意の位置に動かすことで重心をずらすことができます。

f:id:be116:20180507194812p:plain

ProBuilderでだるまをつくる(オブジェクトの結合とピボットの中央寄せ)

まずは成果物。 作成したモデルにRigid Bodyをつけています。

実行手順。

ProBuilderウィンドウを開いておきます。

ウィンドウからNewShapeを選択。 形状はicosahedron(球、正しくは正二十面体)。ヒエラルキーではicosphereとなっています。 パラメータは特にいじらずBuild Icosahedronをクリック。

Center Pivotでピボットを中央に。 オブジェクトを(0, 0, 0)へ移動。

もう一度、球を生成して同じ手順で(0, 1, 0)へ配置。

スケールを変更。

Vertex Colorで球ごとに色を塗る。 球を選択。適当な色を選択。

2つの球を選択。 Merge Objectsでオブジェクトを結合。

以上。

UnityとHTC ViveでVR空間に鏡をつくる

動作環境はUnity ver 2017.3.0。

実行

準備

Vive Stereo Rendering Toolkitを使用します。

手順

  1. ヒエラルキーにPlaneを配置
  2. Plane -> Rotation -> xを-90に
  3. Plane -> Add Component -> Stereo Rendererをアタッチ
  4. このタイミングでPlane配下にカメラが追加されます。誤って削除した場合、この手順をやり直してください。
  5. Stereo Renderer -> isMirrorにチェック

f:id:be116:20180506103006p:plain

この画像のような設定ができたら大丈夫です。 (Scaleは適当に設定しています)

VRコスプレでキャリブレーションするスクリプト

2018/05/04 16:07 スクリプト修正

UNITYを使って、VRコスプレでユニティちゃんになりきるものを作りました。 使ったアセットは定番のFinal IK(ようやく買えた)です。

VRコスプレを初めてしてみて、モデルとの身長差が気になりました。 私の身長は175cmに対して、ユニティちゃんの身長は200cm。 25cmの差だけ視点がユニティちゃんの頭上に突き抜けたり、カメラの位置が低すぎてユニティちゃんがコマネチをしたりと大変だったのです。 前者はまだいいのですが、後者はポージングに影響がでるため絶対に避けたい問題。 この身長差を埋めるために、キャリブレーションをする必要がありました。

キャリブレーションには(おそらく)2通りの方法があります。

  1. モデルのスケールを変更
  2. モデルの視点とカメラの位置を合わせる

1番目は、モデルのスケールを変更して自分の体格と合わせようという方法です。 「VRIK キャリブレーション」とぐぐると一番にヒットする対処法がこれです。 2番目は、カメラの位置を調整して、モデルの目の位置に持ってくればいいじゃないというものです。 VRは立位、立って遊ぶことがほとんどです。 そのため、VRコスプレでは、直立姿勢でモデルの顔の位置にカメラをもってくることが望ましい。 そこで、ユーザが直立した状態でカメラの位置を調整することで、これを解決します。

1番目の方法は、モデルのスケールが変化してしまいます。 せっかく、等身大(といっても2mありますが)のモデルがあるのですから、スケールはなるべく崩したくない。 ですので、私は2番目の方法で身長差の解決を測りました。

方法を説明します。といっても方法は単純です。 [CameraRig]オブジェクトの高さを変更するだけ。

[CameraRig]はトラッキングの高さを指定することができます。 ですので[CameraRig]の高さをモデルの身長に応じて、変更すれば良いのです。 具体的にはTransformプロパティのpositionのy座標を適当な値に変更すること。それだけです。

試行錯誤的にプロパティをいじっても良いのですが、せっかくなのでプログラミングで解決したい問題です。 そこで、以下のスクリプトを書きました。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Calibrator : MonoBehaviour {

    /**
    * カメラリグのオブジェクト
    */
    [SerializeField]
    Transform cameraRig;

    /**
    * カメラオブジェクト 
    */
    [SerializeField]
    Transform hmdEye;

    /**
    * モデルの高さ
    */
    [SerializeField, Range(0f, 3.0f)]
    float charaHeight;

    /**
    * キャリブレーション 
    */
    public void Calibrate () {
        float height =  charaHeight - hmdEye.transform.localPosition.y;
        cameraRig.position = new Vector3 (0, height, 0);
    }

}

このスクリプトでやっていることは、 単純にカメラリグの位置を、モデルと自分との身長差で調整しているだけです。 その際にカメラの位置を使って、自分の身長を測定しています。(正確には目の高さですが)

直立姿勢でこのスクリプトのCalibrateを実行することで、 モデルと自分の身長差を埋めることができます。 私はviveコントローラーのトリガーを引いたときにSendMessageを送り、Calibrateを実行するようにしました。

使用したヒエラルキーはこれです。

f:id:be116:20180504145915p:plain

空のゲームオブジェクトを作ってCallibratorを追加し、 cameraRigに[CameraRig]、hmdEyeにCamera(Eye)を置き、charaHeightを1.9(モデルの目の位置)にしています。