ぶろぐめんどくさい

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

2DゲーのキャラクターにつけるRigidbody

f:id:be116:20180513153904p:plain

壺おじさんは上下左右にしか動けない。 壺おじさんの体は地形に合わせて傾く。 壺おじさんの体は常にこちらを向いている。 そんな壺おじさんの物理挙動をUnityで再現したい。

キャラクターにRigidbodyをつけるだけでは全方向に進み、回転する。 それでは、2DゲーにおけるRigidbodyのConstraintsをどうすべきか。

答えはこれ。

f:id:be116:20180513154212p:plain

  • Freeze PositionのZにチェック
  • Freeze RotionのXYにチェック

これは、キャラクターがこちらを向いている場合の制約である。 キャラクターが進行方向を向いている場合XとZが入れ替わる。

これで上下左右に動き、地形に合わせて傾き、常に一定の方向を見ているキャラクターができあがる。

JavaでHelloWorldを出力するTシャツ

JavaでHelloWorldを出力するプログラムをカンニングすることができるTシャツです。

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でオブジェクトを結合。

以上。