ぶろぐめんどくさい

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

MVCモデルとはニュース番組のモデル「事象・キャスター・脚色」と同一である

MVCモデルというものがある。 MVCモデルはWebアプリケーションの開発でよく使われる概念である。 MVCモデルの利点は、メンテナンス性を向上させることができるところにある。 MVCモデルがどのようにメンテナンス性を向上させるのか。 MVCモデルはWebアプリケーションの構造をModel層、View層、Controller層の3つの層に役割分割をしている。 Webアプリケーションの挙動を一箇所に集約するのではなく、3つの層に分割することで、いずれかの層に異常があった場合、異常のある層だけを修正することでWebアプリケーション全体をメンテナンスすることができる。

MVCモデルには3つの層による役割分割によりメンテナンス性を向上させる利点がある一方で、 それぞれの層に与えられている役割をちゃんと理解していないといけない。 本記事では、MVCモデルをWebアプリケーションではなくニュース番組に例えて、概念的な理解を促す。

f:id:be116:20180518070732p:plain

MVCモデルをニュース番組に例えると以下のようになる。

まず、「モデル」は「事象」である。 交通事故や殺人事件、山口メンバーなどがこれにあたる。 順番が前後するが次は、「コントローラー」、つまり「脚色」である。 一般的なニュース番組では「事象」をそのまま放送することはない。 時間が限られているため不要な部分をカットしたり、自分たちの伝えたい「事象」とは異なるため不都合な部分をカットしたり、「事象」は発信者の都合のいい形に「脚色」される。 また、出演者個人の感想など、ある程度「脚色」が加えられる場合もあるかもしれない。 ジャニーズの圧力がかかったり、犯人はアニメ好きだったという情報が付け加えられることもある。 次に「ビュー」は「キャスター」である。 「キャスター」は「脚色」された「事象」、つまりニュースをそのまま伝える。 一言一句たぐわないし、噛むこともない。 とにかく与えられた原稿を時間どおりに読み、視聴者に伝える。 「キャスター」には視聴者に情報を伝える権限を与えられているが、伝えられる情報は原稿にあるものに限られる。 「モデル・事象」と「コントローラー・脚色」と「ビュー・キャスター」の3層構造でニュース番組は成り立つ。 例えば、ビュー層でエラーが起きた、キャスターが噛み噛みだったという状況には、キャスターを変更したり、噛まないように練習したりするだけでよい。 事象も脚色もこのエラーには関与しない。 このような3層構造がMVCモデルのメンテナンス性を向上させている。

以上の説明はあくまでも概念的なものである。 可能であればちゃんとした文献を漁り、ちゃんとした知識を身に着けてほしい。 この記事はあくまでもその足がかりである。

かぐや様は告らせたい9巻のかぐや様もお可愛いこと

かぐや様は告らせたい9巻を読みました。 いやー、最新巻のかぐや様もあいかわらずお可愛いこと

9巻の主役は石上会計。 石上会計の過去が描かれ、 体育祭を通してそれを克服する様に涙します。

しかしそれはあくまで本編の話。 かぐや様は告らせたいという漫画は、 本編と並行して進む他愛もないお話が、 どうしようもなく面白いのです。 特にかぐや様がアホになられるお話は最の高です。

9巻では、かぐや様は告らせたいのもうひとりの主人公である生徒会会長白銀御行、その父親が出てきます。 お怖いい目をされている白銀父ですが、その性能は素晴らしく、かぐや様を芸術的なアホにしてくれました。 白銀父に白銀御行のことが好きなのかを聞かれたかぐや様は、はーさかさんがあきれるくらいにお可愛かった。

白銀父の言う通り、それはもう言ってるようなものです。 実はこの時点でかぐや様は白銀父の正体を知らないんです。 「会長の悪口を言う、目つきが悪いけどなぜか安心するおじさん」くらいにしか思ってません。 会長の悪口を言われたまま黙ってはいられないかぐや様。 弁明のため、会長のいいところ(好きなところ)を知らず知らずのうちに白銀父に説明するのです。 そしてこのお話のオチは、かぐや様が散々会長をべた褒めした後、かぐや様が目つきの悪いおじさんを白銀父と認識すること。

白銀父を認識したかぐや様は絶望的に頬を染めます

必死に平静を装うかぐや様は絶望的なまでにお可愛い。 ヤンデレみたいでいっぱいしゅき。

それとこれは別にどうでもいい話なんですが、 9巻には、 何回でもシコシコして良くてでも最低一回はシコってしなきゃいけないゲームを遊ぶお話が掲載されています。

のっけからこれですよ。 藤原書記の純粋な笑顔が眩しい。 冒頭のインパクトが強すぎて、石上会計の成長が霞んでしまいます。 かぐや様は告らせたいはやっぱりコメ寄りのラブコメだったんですね。 安心しました。

TextMeshProでフォントアセットをつくる

TextMeshProはリッチな3Dテキストをつくることができるアセットである。 例えば、TextMeshProで作成した3DテキストはUnity標準のものと違い、大きさによって滲まない。

TextMeshProを扱うためにはフォントアセットを作る必要がある。 フォントアセットは、OTFなどのフォントデータをText Mesh Pro専用の形式に加工したものである。 フォントアセットを作るためには、Font Asset Createrを用いる。 Window->TextMeshPro->Font Asset Createrで開くことができる。

f:id:be116:20180513155630p:plain

このスクリーンショットでは、 日本語のフォントを扱うため、 Character SetをCustom Charactersにしている。 Character SetがCustom Charactersの場合、ゲーム内で使用する文字をCustom Character Listに入力する。

フォントアセットを作る手順は単純で、 Generate Font AtlasをクリックしSave TextMeshPro Font Assetをクリックするだけ。 作成したフォントアセットを用いて、ゲーム内に3Dテキストを配置することができる。

f:id:be116:20180513160812p:plain

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

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