ぶろぐめんどくさい

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

かぐや様は告らせたい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

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

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

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

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

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

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

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

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

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

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

です。