20191129のUnityに関する記事は10件です。

【Unity】uGUIのTextに長体をかけたい【収まるように縮める】

現在グレンジでUnityを用いたゲーム開発を行っているみくりやと申します。
UIを実装する際にテキストエリアに関する話題はよく出るかと思います。

「文字数制限超えてテキストがレイアウトからはみ出てます」

とか

「仕様変更で文字数収まらなくなったのでレイアウトの調整お願いします」

とか

そんな話のさなか、デザイナーさんから長体かけたいという言葉が出ました。
Unityだけ触ってる自分には馴染みのない言葉でしたが言い換えると

指定幅に収まるようにテキストにスケールをかける

ことです。
参考:“長体(文字)” の意味・解説

ということで作ってみた

というライトな感じではいけず苦戦しました。
Textクラスを継承しOnPopulateMeshでゴニョゴニョする方針です。

private Matrix4x4 _scaleMatrix = Matrix4x4.identity;

protected override void OnPopulateMesh(VertexHelper toFill) {
    #region Textクラス処理
    if (font == null) {
        return;
    }

    // We don't care if we the font Texture changes while we are doing our Update.
    // The end result of cachedTextGenerator will be valid for this instance.
    // Otherwise we can get issues like Case 619238.
    m_DisableFontTextureRebuiltCallback = true;

    Vector2 extents = rectTransform.rect.size;

    var settings = GetGenerationSettings(extents);

    #region 追記箇所
    float overRate = preferredWidth / rectTransform.rect.width;
    if (overRate > 1f) {
        switch (alignment) {
            case TextAnchor.LowerLeft:
            case TextAnchor.LowerRight:
                settings.textAnchor = TextAnchor.LowerCenter;
                break;
            case TextAnchor.MiddleLeft:
            case TextAnchor.MiddleRight:
                settings.textAnchor = TextAnchor.MiddleCenter;
                break;
            case TextAnchor.UpperLeft:
            case TextAnchor.UpperRight:
                settings.textAnchor = TextAnchor.UpperCenter;
                break;
        }

        // 変換行列を作成
        _scaleMatrix = Matrix4x4.identity;
        // scale x
        _scaleMatrix.m00 = 1f / overRate;
        // scale y
        _scaleMatrix.m11 = 1f;
        // scale z
        _scaleMatrix.m22 = 1f;

        // テキストが切られないようにOverflow指定
        settings.horizontalOverflow = HorizontalWrapMode.Overflow;
    }
    #endregion

    cachedTextGenerator.PopulateWithErrors(text, settings, gameObject);

    // Apply the offset to the vertices
    IList<UIVertex> verts = cachedTextGenerator.verts;
    float unitsPerPixel = 1 / pixelsPerUnit;
    int vertCount = verts.Count;

    // We have no verts to process just return (case 1037923)
    if (vertCount <= 0) {
        toFill.Clear();
        return;
    }

    Vector2 roundingOffset = new Vector2(verts[0].position.x, verts[0].position.y) * unitsPerPixel;
    roundingOffset = PixelAdjustPoint(roundingOffset) - roundingOffset;
    toFill.Clear();
    if (roundingOffset != Vector2.zero) {
        for (int i = 0; i < vertCount; ++i) {
            int tempVertsIndex = i & 3;
            m_TempVerts[tempVertsIndex] = verts[i];
            m_TempVerts[tempVertsIndex].position *= unitsPerPixel;
            m_TempVerts[tempVertsIndex].position.x += roundingOffset.x;
            m_TempVerts[tempVertsIndex].position.y += roundingOffset.y;

            #region 追記箇所
            if (overRate > 1f) {
                m_TempVerts[tempVertsIndex].position = _scaleMatrix.MultiplyPoint3x4(m_TempVerts[tempVertsIndex].position);
            }
            #endregion

            if (tempVertsIndex == 3) {
                toFill.AddUIVertexQuad(m_TempVerts);
            }
        }
    } else {
        for (int i = 0; i < vertCount; ++i) {
            int tempVertsIndex = i & 3;
            m_TempVerts[tempVertsIndex] = verts[i];
            m_TempVerts[tempVertsIndex].position *= unitsPerPixel;

            #region 追記箇所
            if (overRate > 1f) {
                m_TempVerts[tempVertsIndex].position = _scaleMatrix.MultiplyPoint3x4(m_TempVerts[tempVertsIndex].position);
            }
            #endregion

            if (tempVertsIndex == 3) {
                toFill.AddUIVertexQuad(m_TempVerts);
            }
        }
    }

    m_DisableFontTextureRebuiltCallback = false;
    #endregion
}

まずTextクラスのOnPopulateMesh処理をゴリッと持ってきます (ここがイケてないところですが…)
参考:https://bitbucket.org/Unity-Technologies/ui/src/2019.1/

そして「#region 追記箇所」の位置に処理を追加しています。

最初の追記箇所ではTextの設定値に関する計算とどのくらい縮めるかの計算を行います。
以下の計算式でどのくらいはみ出してるかを割り出しました。

float overRate = preferredWidth / rectTransform.rect.width;

この値を用いてどのくらい縮めるかの変換行列を作成します。

// 変換行列を作成
_scaleMatrix = Matrix4x4.identity;
// scale x
_scaleMatrix.m00 = 1f / overRate;
// scale y
_scaleMatrix.m11 = 1f;
// scale z
_scaleMatrix.m22 = 1f;

今回は横方向のみに長体をかけますが、m11を使用することで縦に対しても反映できます。

次にGetGenerationSettingsで取得した設定値に関する処理です。
まずtextAnchorをCenterにしている箇所は LeftかRightだと頂点計算時の処理に手を加える必要があり ややこしくなりそうだったのですが、長体をかけるときはどうせrectの幅いっぱいの状態なので割り切って上書くようにしました。
horizontalOverflowも Wrapだと文字が切れる のでOverflowで上書きします。
また、設定値を直接上書きすると長体をかけないでいいときにもとに戻す処理がややこしくなるのでGetGenerationSettingsで得た値を書き換えます。
ややこしいんです。

あとは頂点計算後に_scaleMatrix.MultiplyPoint3x4してやればスケールをかけることが出来ます。

キャプチャ1-1.PNG
こんな感じのTextに文字を入力していくと

キャプチャ1-2.PNG
このように一文字の横幅が収まるサイズに縮んでくれます。やったね!

まとめ

Textの処理内容に直接手をかけることになりましたが一応希望の機能は作ることが出来ました。デフォであっていいような気もするのでUnityさん作ってくれないかな~。もうちょっとスマートな方法を模索したい今日このごろです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでサービスロケーター(ServiceLocator)を活用する

はじめに

サムザップ #1 Advent Calendar 2019 の12/2の記事です。

株式会社サムザップの尾崎です。Unityエンジニアです。

内容

Unityでサービスロケーターの活用について紹介します。

サービスロケーターとは

サービスロケーターはプログラムを特定の実装に依存させずに動作させたいときに用いる実装手法の一つです。
柔軟性のあるプログラムを作成できます。

用語

  • 本記事ではサービスをシステムと表現しています。
  • システムはいろんなクラスから呼び出される共通的なプログラムのことを表しています。サブシステムや基盤と呼ばれることもあります。 例えばゲームでは外部リソースやログを扱うクラスなどが該当します。

背景

newでオブジェクトを生成したり、staticやシングルトンでアクセスすると特定のクラスに依存することになります。
その依存したクラス内で外部環境を扱う処理を行っているとプログラムの動作確認が大変になってくることがあります。

例えば1日に1回だけ挑戦できるステージがあり、セーブデータシステムにステージクリアを記録すると翌日まで再挑戦できない状況があるとします。
開発中はこうした状況でも何度でもステージに挑戦できるようにしておきたいものです。

セーブデータクラスのメソッド内でデバッグ用の分岐を書くこともできますが、クラスが大きくなると分岐が増えて分かりにくいコードになりやすいです。

そこでセーブデータシステムのためのインターフェース定義してクラスを複数作成します。
正規の処理を行うクラスとデバッグ用の処理を行うクラスです。
サービスロケーターはこのクラスへのアクセス方法を提供します。

(セーブデータをクリアしたり日付を進めるなどデバッグ方法はいくつかありますがここでは実装を複数用意するという方向で。)

そしてもう一つ、共通システムはいろんなクラスから簡単に呼び出せるようにしておきたいものです。
staticやシングルトンが使われることが多いですが、便利な反面1つのクラスに依存してしまうのと、グローバル変数と同じ問題があるので、できるだけ使用しないようにしています。ユニットテストの妨げにもなってしまいます。

サービスロケーターの詳細

機能

  • システムのインターフェースに対するインスタンスを登録できます
  • システムのインターフェースを指定してインスタンスを取得できます
  • グローバルなアクセスを提供します

※ インターフェース以外にクラスでも大丈夫です

これらの機能によってシステム利用側のコードからシステムの具体的な実装とインスタンスの生成方法を分離します。

メリット

  • 複数の実装を切り替えることができ柔軟性のあるプログラムになります
  • 複数実装をのための分岐が初期化時の一箇所で済みます
  • インスタンスへの容易なアクセス
  • DIコンテナよりシンプルで高速
  • モック実装に切り替えることでユニットテストできるようになります

デメリット

  • ServiceLocatorクラスへの依存が増えます
  • DIコンテナよりクラスの依存関係が分かりにくくなります。一般的に柔軟性を得るための方法としてはDIコンテナの方が推奨されています。
  • シングルトンと同じようにインスタンスを単一にするためには別の対策が必要です

コード例

サービスロケーター本体

using System;
using System.Collections.Generic;

namespace Sumzap
{
    /// <summary>
    /// サービスロケーター
    /// </summary>
    public static class Locator
    {
        /// <summary>
        /// 単一インスタンス用ディクショナリー
        /// </summary>
        private static Dictionary<Type, object> _instanceDict = new Dictionary<Type, object>();

        /// <summary>
        /// 都度インスタンス生成用ディクショナリー
        /// </summary>
        private static Dictionary<Type, Type> _typeDict = new Dictionary<Type, Type>();

        /// <summary>
        /// 単一インスタンスを登録する
        /// 呼び直すと上書き登録する
        /// </summary>
        /// <typeparam name="T">型</typeparam>
        /// <param name="instance">インスタンス</param>
        public static void Register<T>(object instance) where T : class
        {
            _instanceDict[typeof(T)] = instance;
        }

        /// <summary>
        /// 型を登録する
        /// このメソッドで登録するとResolveしたときに都度インスタンス生成する
        /// </summary>
        /// <typeparam name="TContract">抽象型</typeparam>
        /// <typeparam name="TConcrete">具現型</typeparam>
        public static void Register<TContract, TConcrete>() where TContract : class
        {
            _typeDict[typeof(TContract)] = typeof(TConcrete);
        }

        /// <summary>
        /// 型を指定して登録されているインスタンスを取得する
        /// </summary>
        /// <typeparam name="T">型</typeparam>
        /// <returns>インスタンス</returns>
        public static T Resolve<T>() where T : class
        {
            T instance = default;

            Type type = typeof(T);

            if (_instanceDict.ContainsKey(type))
            {
                // 事前に生成された単一インスタンスを返す
                instance = _instanceDict[type] as T;
                return instance;
            }

            if (_typeDict.ContainsKey(type))
            {
                // インスタンスを生成して返す
                instance = Activator.CreateInstance(_typeDict[type]) as T;
                return instance;
            }

            if (instance == null)
            {
                Debug.LogWarning($"Locator: {typeof(T).Name} not found.");
            }
            return instance;
        }
    }
}

システム (サービス)

using UnityEngine;

namespace Sumzap
{
    /// <summary>
    /// システムのインターフェース
    /// </summary>
    public interface ISomeSystem
    {
        void SomeMethod();
    }

    /// <summary>
    /// 正式版のシステム
    /// </summary>
    public class SomeSystem : ISomeSystem
    {
        public void SomeMethod()
        {
            // 正式な処理
        }
    }

    /// <summary>
    /// デバッグ版のシステム
    /// </summary>
    public class DebugSomeSystem : ISomeSystem
    {
        public void SomeMethod()
        {
            // デバッグ用の処理
        }
    }
}

システムをサービスロケーターに登録 (プロジェクトの初期化)

#define DEBUG
using UnityEngine;

namespace Sumzap
{
    /// <summary>
    /// プロジェクトの初期化
    /// </summary>
    public static class ProjectInitializer
    {
        [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
        private static void Initialize()
        {
            // この変数を切り替えることで生成するインスタンス切り替えます
            // 単純化のためクラス内の#defineで定義しています
            // 実際にはScripting Define Symbolsや設定ファイルを読み込んだりして切り替えます
            bool useDebugSystem;

            #if DEBUG
            useDebugSystem = false;
            #endif

            if (useDebugSystem)
            {
                // 正式な処理を行うインスタンスを登録
                Locator.Register<ISomeSystem>(new SomeSystem());
            }
            else
            {
                // デバッグ用処理を行うインスタンスを登録
                Locator.Register<ISomeSystem>(new DebugSomeSystem());
            }
        }
    }
}

システムを利用

using UnityEngine;

namespace Sumzap
{
    public class SomeScene : MonoBehaviour
    {
        public void Start()
        {
            // システムの型を指定して登録されているインスタンスを取得
            var system = Locator.Resolve<ISomeSystem>();
            system.SomeMethod();

            // newの場合
            var system2 = new SomeSystem();
            system2.SomeMethod();

            // staticの場合
            SomeSystem.SomeMethod();

            // シングルトンの場合
            SomeSystem.Instance.SomeMethod();
        }
    }
}

構成

ServiceLocator

サービスロケーター実装本体です
型とインスタンスをセットで登録します。
型を指定してインスタンスを取得します。
事前にインスタンス生成しておくパターンと都度インスタンス生成するパターンに対応しています。
事前インスタンス生成パターンはシングルトンの代替になります。
Resources.LoadでPrefabを取得してMonoBehaviourを登録することもできます。

SomeSystem

何らかのシステムです。
例. セーブデータ、マスターデータ、サウンド、チュートリアルなど
ファイルやプラットフォーム周りを扱うシステムが対象になりやすいです。

ProjectInitializer

使用するクラスを決めるところです。
RuntimeInitializeOnLoadMethodによってどのシーンを実行しても最初に処理されます。Awakeより先に実行されます。

コード例ではインスタンスを生成して登録していてResolveされたときは単一インスタンスが使われます。

下記のようにするとResolveされる度にインスタンス生成されます。

Locator.Register<ISomeSystem, SomeSystem>();
SomeScene

サービスロケーターを使用してシステムを利用するところです。
比較のためnew、static、シングルトンのコードも配置しています。

活用例

アセットバンドルシステム

アセットバンドルをサーバーからロードする実装とローカルファイルをロードする実装を切り替えられるようにします。
システム利用側はアセットバンドルのロード先を意識せずに実装できます。
開発時はローカルファイルからロードすることでアセットバンドルビルドやサーバーから取得する手間をなくし効率良く開発できます。
AssetBundleManagerのSimulation ModeやAddressableのFast Modeと同じ機能です。

プラットフォームごとに異なる実装

iOSとAndroidなどプラットフォームによって実装が異なる機能を共通インターフェースで機能を提供します。
プラットフォームごとにクラスを作成します。
システム利用側はプラットフォームの違いを意識する必要がなくなり、システム実装側は1クラスにプラットフォームの分岐を多数記述する必要がなくなります。

その他活用案
  • 負荷の高いシステムを無効化する
  • システムにログを仕込む

補足

  • サムザップではDependency Injectionコンテナ(Zenject)を採用しているプロジェクトもあります。Factoryで複数の実装を切り替えています。 ただ、Zenjectはパフォーマンスの懸念と、学習コストが高い面がありサービスロケーターを採用しているプロジェクトもあります。
  • 一応シングルトンでも継承を利用することで複数の実装を切り替えることができます。
  • 実装は1つで良いと割り切ってシンプルなstaticクラスを採用することもあります。用途に合わせて選択すると良いかと思います。
  • サービスロケーターを使ってメリットが大きいクラスに使うと良いです。オブジェクトを引数で簡単に渡せる場合には渡した方が良いと思います。

最後に

明日は @tomeitou さんの記事です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

推しにクリスマスプレゼントと誕生日プレゼントを作って贈った話

はじめに

メリークリスマス!!!!!

https://qiita.com/advent-calendar/2019/engineer_tail
なんか布教したいエンジニア Advent Calendar 2019の12月24日クリスマスイブの記事です。



ということで、僕は比良坂芽衣様を布教します。
https://www.youtube.com/channel/UCckQQRZVF9x2GjroBjNIX_Q?sub_confirmation=1

みなさんご存じ,マシンガントーク系バーチャルユーチューバーの比良坂芽衣様.
ご存じでない方は名前だけでも覚えていってください.あと,チャンネル登録と通知のONと全動画の高評価とツイッターのフォローも.

僕の知っている限りでは,本人が自身の心の友の●●を祝うことができるのは,バーチャルユーチューバーの中では,唯一この方のみです.(な,何を言っているのかわからねーと思うが,自分も何を言っているのかわからなかった...わからねーやつは下の動画を見てくれ...)

バーチャルユーチューバーやVtuberの定義は人によって様々ですが,画面の向こう側の存在として顕現し活動しているということを定義とするならば,Vtuberとしての芽衣様の歴史は意外と長く,SHOWROOMで静止画配信を始めたのはもう3年以上前になります.


EBquz00UEAEoBiv.jpg

そんな比良坂芽衣という存在ができて4年以上(2019年12月24日現在)たつのに,いまだに3Dの身体が無いのは非常に遺憾です.



Youtuberを配信プラットフォームに移してもうじき1年,誕生日も1月2日です.
ちょうどいい(?)タイミングなので,3DCGの勉強するという名目で,フルスクラッチ(#フルスクラッチとは)でモデリングを行いアニメーションを作成するまでの工程を行います.

開発手法

推しのライブ配信や動画や配信アーカイブを見たり聴きながら,モチベーションを維持するHDD(Hirasakamei Driven Development)と,TwitterやDiscordに進捗をアップロードして分からないこととかアドバイスをもらうSDD(SNS Driven Development),そして参考書籍(物理本)を購入して「お金を使ったからにはやりきるぞ!」というプレッシャーを自身に与えるPDD(costPaid Driven Development)を実践しました.

芽衣様の配信は,基本的には沈黙がないので,ずっと芽衣様の声(たまにあへぷりん)を聴くことになるので.一切眠たくなることがありません.これはかなりおすすめの手法です.
以下にオススメの動画と配信アーカイブを置いておきます。一人でこんなに楽しそうにゲームをプレイすることができる人、いますか?

AIと無限UNO勝負地獄ですの!【新人Vtuber】
https://www.youtube.com/watch?v=b7kawyjScJQ
Goat Simulatorですの!【新人Vtuber】
https://www.youtube.com/watch?v=uOFROYMrN1w

また,お酒を飲みながら作業をするADD(Alcohol Driven Development)も実践しましたが,寝落ち率がひどいことになるのであまりおすすめはできません.

使用技術

  1. モデリング・リギング
    • Blender2.80(途中から2.81)
  2. テクスチャの作成
    • Photoshop CC
    • Illustrator CC
  3. アニメーションの作成
    • Unity(VRM)
    • MMD

技術レベル

  • Blender
    • 今年の10月中旬にインストールからはじめた.
    • モデル作成未経験,この制作をはじめる前にビールジョッキを制作
    • 3DCGの知識や技術は高校生のころに少しかじった程度(資格取得やMayaの使用等)
  • Photoshop
    • クソコラの作成などに利用
  • Illustrator
    • ウェブサイトのUIやコンテンツなどの作成に利用
  • Unity
    • 大学の研究などで使用,直近では個人開発のアプリ制作に利用
  • MMD
    • MMDを利用したアニメーション作成は未経験

開発環境

  • Windows8.1 64bit
  • Core i5-4460 3.20GHz
  • RAM16GB
  • GeForce GTX 760

制作過程

Youtubeに限定公開している動画でお茶を濁します。。。
https://www.youtube.com/playlist?list=PL51b37i0qDU20AWoMdENHaUTY0-Y8TikE
アニメーションの作成の項までありますが、ボーンの作成までしか終わりませんでした。あしからず。。。

モデリング

手順

制作の手順の参考にした資料(物理本)は,ソーテック社から出版されているBlender 3Dキャラクター メイキングテクニック.あとは参考URLにあるページなど.

  1. 輪郭の作成 メッシュを作成するために,まずはベースとなるキャラクターの輪郭を作成します.
  2. 顔,ボディのメッシュ作成 輪郭からメッシュを作成します.
  3. 髪の毛のメッシュ作成 アドオンを使いながら髪の毛のメッシュを作ります.
  4. 服のメッシュ作成 ボディから服のメッシュを作成します.

モデリングでの成果物
キャプチャ.PNG キャプチャ2.PNG キャプチャ3.PNG 

リギング

ボーンをつけるところまで終わりましたが、自動ウェイトが上手くいかず、ウェイトペイントまでは終わりませんでした。
https://www.youtube.com/watch?v=APchelX1dSU
zenさんの講座を見ながら学んでいきます。

テクスチャの作成

アニメーションの作成

終わりに

最終的には、制作過程に貼ったURLにアクセスできるQRコードを印刷しクリスマスプレゼントと同封したものを、比良坂芽衣様宛にいろはにぽぺとさんに送りました。
いろはにぽぺとさんは渋谷にある「常識の斜め上をイくオタク系クリエイター集団」というキャッチフレーズで有名なベンチャー企業さんです。竹花ノートさんが所属していたりVtuberのリアルイベントを度々開催していたりということで話題になっています。最近だと、AVATAR2.0の子たちがいろはにぽぺとさんの所属になったことでますますVtuber事業を拡大させていっていることが分かります。
あくまでも個人勢ですがそちらに住所をお借りしていて、プレゼントを贈ることができます。

明日は@tombo_gokuraku_1040さんの「ProgateでHTML&CSS&JavaScriptを完全に理解した(笑)」です.最終日、しまっていきましょう。

謝辞

10月に退職をしBlenderをはじめてから、色々な大勢の方にアドバイスをいただきました。名前をあげていいのかわからない方もいますので、まずは挨拶だけで。ありがとうございます。

参考URL

関連URL

Twitter:https://twitter.com/hirasakamei
Youttube:https://www.youtube.com/channel/UCckQQRZVF9x2GjroBjNIX_Q?sub_confirmation=1
公式サイト:https://www.hirasakamei.com/
vGardenプロジェクト:http://irohanipopeto.com/staff/vtuber/11.html
NaiA_music:https://music.naia-planet.com/music/wasurenagusa/
FANBOX:https://www.pixiv.net/fanbox/creator/15254771
LINEスタンプ1:https://store.line.me/stickershop/product/1394748/ja
LINEスタンプ2:https://store.line.me/stickershop/product/5505780/ja

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

TryGetComponentとout、out var のお話

Unity 2019.2 から

TryGetComponent という GetComponent 相当のメソッドが追加されています。
このメソッドの紹介をしたいのですが、そのためには切っても切れない、 out var(out変数宣言) についてから書きたいと思います。
「out var ぐらい知ってらぁ!!」という方は、 TryGetComponentからどうぞ。
「どっちも知ってらぁ!!」という方は・・・。お疲れ様でございました。

out変数宣言

out 宣言。 使ってます? 多分、Unityでゲーム作っている上で自作のなんらかでこのキーワードをわざわざ使ってメソッド宣言している人はほとんどいないでしょう(偏見)

そもそもの機能としては、「out を付けた引数で指定した変数はメソッド内で必ず結果が入りますよ。」 という宣言です。

outを使ったメソッド
        private void PlusMinusOne(int origin, out int plusResult, out int minusResult)
        {
            plusResult = origin + 1;
            minusResult = origin - 1; //out キーワードを付けた引数への代入が存在しない場合はエラー
        }
呼び出し側
        public void Start()
        {
            int plusResult
            int minusResult;
            PlusMinusOne(100, out plusResult, out minusResult); //呼び出す方も out キーワードを付ける必要がある
            Debug.Log($"plusResult:{plusResult},minusResult:{minusResult}");
        }
出力
plusResult:101,minusResult:99

ちなみに利点は、「必ず値がセットされる事が保証される」 です。今回とは趣旨が違うので詳細は省きますが・・・1

Try**

さて、自分でoutキーワードを使ったメソッドを作らないかもしれませんが、.netで用意されているクラスのメソッドにoutキーワードが使われている場合ももちろんあります。

(個人的に)一番使うであろうメソッドは Try系メソッドです。

例えば、文字列をintに変換しようとした場合、int.Parse("100");のように、パースメソッドを呼びます。
しかし、例えばint.Parse("ほげ"); こんなことをしたら変換に失敗してExceptionが発生してしまいます。
try~catchで括ってもいいですが、先に変換可能かどうか調べつつ変換できないのかな?。それ既にありますよ!!

bool int.TryParse(string s,out int result);

こんなのが用意されてます。 変換が出来る場合はメソッドの戻り値がtrueになり、さらにresultに結果が入ります。2
出ました!outキーワード!!

使う場合は

int result = 0;
if(int.TryParse("ほげー",out result) == false){
    Debug.Log("変換できませんでした");
}else{
    Debug.Log($"変換結果:{result}");
}

こんな感じで、if文の中に入れて、戻り値で分岐させるのがよく見る使い方です。

辛かったこと

さて、このoutキーワードを使うのに辛かった事が2点あります。

①変数を先に宣言しなくてはいけない
②変数の宣言に(仕組み上)varが使いづらい

①はまぁ、格納先なので、当たり前として。 ②は何かというと、varというやつは右辺から左辺の型を推論してくれる機能です。右辺がintならintstringならstringに宣言してくれているわけです。

では、今回のケースに無理矢理varを使うとしたらどうなるでしょう。 intなので・・・。

var plusResult = 0;
var minusResult = 0;

ってやります? この0はどこから来たんだよという話で。
varを使いたいがために差し当たりの無い値0を持ってきた に過ぎなく、手段と目的が逆転してます。

前置きが長かったね

それが、なんと、このように書けるようになります。

if (int.TryParse("ぼげー", out var result) == false)
{
    Debug.Log("変換できませんでした");
}
else
{
    Debug.Log($"result:{result}");
}

やったー! 宣言が中に入った!! そう、 out に指定する変数をその場で宣言できるようになりました!

注目してほしいのは、この

else
{
    Debug.Log($"result:{result}");
}

この、elseブロックにも result が使われている事です。

for文の中で宣言した変数なんかは

for(var i = 0;i < 10;++i)
{
  Debug.Log(i); //スコープ内
}
Debug.Log(i); //スコープ外なのでエラー

このようにブロックがスコープ(生存範囲)になりますが、このout変数宣言はその式が含まれているブロックがスコープになります。
まぁ、 1行前に変数宣言があるのと同じ。 と考えておけば大体OKです。

TryGetComponent

さてさて、ようやく TryGetComponent の話に移れます。
最初にも言いましたが、 Unity 2019.2 から TryGetComponent という GetComponent 相当のメソッドが追加されています。

bool TryGetComponent<T>(out T component)

今まで GetComponent は指定したComponentが取得できなかった場合は null が返ってくるので、それによって成否を判断していました。
それが、他の Tryメソッド 同様、 成否はbool を返すようになり、 out変数 に直接結果を入れてくれる仕組みです。
また出た!outキーワード!! デモモウコワクナイ!

具体的には今まで

        var sr = GetComponent<SpriteRenderer>();
        if (sr != null) //nullかどうかで成否判定
        {
            sr.color = Color.white;
        }

こうだったのが、TryGetComponentが追加されたこと、そして ouv変数宣言 が使えるようになったことで

        if (TryGetComponent<SpriteRenderer>(out var sr)) //TryGetComponentの戻り値自体が 成否、out宣言で中身が入る
        {
            sr.color = Color.white;
        }

このようになります。

どっちが速いかーーーというと、正直あんまり差が無いらしいんですが・・・。(テラシュールブログ様参照)

ちなみに

TryGetComponent<SpriteRenderer>(out var sr))

これは、Genericsの型パラメータに明示的に SpriteRenderer を渡しているので、 var sr というように、 var型推論 が効いている状態です。

逆に、使用される型から型パラメータを型推論 することも出来るので、

TryGetComponent(out SpriteRenderer sr))

こう書いてもよいです。 
どちらを使うかは完璧好みでしょうから、お好きな方を使えばいいと思います。 僕は前者です。

え?

TryGetComponent(out var sr))

はダメなのかって? 何をGetComponentするおつもりですか・・・?

補足

そういえば、今のところあまり見ませんが、

public class Main : MonoBehaviour
{
    private SpriteRenderer sr;

    void Start()
    {
        if (TryGetComponent(out sr)) //メンバ変数に格納
        {
            sr.color = Color.white;
        }
    }
}

こういう書き方も出来るはずですよね。 変数宣言と TryGetComponent の箇所が離れすぎていると、何をGetComponentしたのか一瞬戸惑いそうですけれど。

しかし、これは案外アリかもしれません。

僕はよく、Inspectorで自分自身が持っているComponentをセットさせる時に、(RequireComponentなど)

  • 実行時に GetComponentは重いらしいから、あんまりやりたくないな。
  • RequireComponent指定してあって、確実にそのComponentがあるってわかってるのにInspectorでわざわざドラッグ&ドロップとかかったるいな。

って時に、

using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))] //SpriteRendererを要求
public class ForceWhite : MonoBehaviour
{
    [SerializeField]
    private SpriteRenderer spriteRenderer;

    //このMonobehaviorをアタッチした瞬間、または コンテキストメニューから `Reset` を選んだ時に呼ばれる
    void Reset()
    {
        spriteRenderer = GetComponent<SpriteRenderer>();
    }

    void Start()
    {
        spriteRenderer.color = Color.white;
    }
}

Resetメソッド を使って、半自動的にInspector参照を解決させるんですが
アタッチした瞬間、SpriteRendererが自動でセットされるの図

    //このMonobehaviorをアタッチした時、または コンテキストメニューから `Reset` を選んだ時に呼ばれる
    void Reset()
    {
        TryGetComponent(out spriteRenderer); //呼びっぱなし
    }

そういう用途(Editor上でしか走らない箇所)であれば、こうやっちゃうのも悪くないのでは???(マサカリ案件)

なんにせよ

用法用量を守って、ステキなUnityライフを。 それでは。


  1. 気になる人は、 PlusMinusOne メソッドの呼び出しをコメントアウトしてみてください。 エラーになります。 

  2. ほかにも float.TryParseDateTime.TryParse など、Parse系メソッドはほぼ確実に TryParseメソッド が用意されています。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

すごい!!Unityって鍋の裏もレンダリングできるんだね!!【HDRP: 異方性反射マップ】

この記事は【unityプロ技】 Advent Calendar 2019の12/3の記事です。
【unityプロ技】 Advent Calendar 2019

鍋の裏とは、異方性反射とは

anisotropy.png

たかが鍋の裏、されど鍋の裏。従来のUnityでは難しかった異方性反射表現がHigh-Definition Render Pipeline(HDRP)で可能になりました。異方性反射とは、ライティングの反射光を上下左右に細長く変形させるテクニックで、アニソトロピー(Anisotropy)と呼ばれます。分かりやすい例を挙げると、鍋の裏やCDといった筋の入った金属表現です。上のサンプルだと、質感が全然ちがいますよね?

プリレンダリングのCGソフトでは割とメジャーなライティング表現でした。しかしながら、Unity向けのテクスチャの作成方法がいまいち文章化されていなかったので、この記事を書きます。
 

AnisotropyMapはただのマスクマップ

Unityのアニソトロピーマップは、レッド(R)成分を採用するマスクマップでしかありません。変化させたい部分をRで描き、マスクしたい部分は黒で塗りつぶします。ちょうどこのようなマップを描いたとすると、
anisotropy_maskmap.jpg
ライティングはこんな感じになります。
anisotropy_scale.gif
インスペクター内のスライダーで調整するスケールは、引き延ばす方向に左右します。-1でV方向、1でU方向に作用します。ワールドやローカルにおけるxyz軸には関係がありません。メッシュのUV座標を基準とします。
anisotropy_inspector.jpg

TangentMapで反射方向をコントロールする

異方性反射ライティングを行う上でもう一つ大切なマップがあります。それがタンジェントマップです。これはメッシュの接線方向を定義するために使用します。

モデルを形成するポリゴンの法線は各々一つしか持ちえませんが、ポリゴンの接線は必ず一つとは限りません。普段意識することがないのは、値を設定しないかぎり、テクスチャ座標上のU方向を接線ベクトルとして扱っているからです。タンジェントマップでは、その接線ベクトルを設定することができます。

では、なぜわざわざ接線ベクトルを定義する必要があるのでしょうか? それはU方向に偏りがあると、反射光を引き延ばす方向にも偏りが出るためです。簡単な例として、さきほどのメッシュを回転させてみます。その様子がこちら。
anisotropy_gif2.gif
反射光が引き延ばされる角度と引き延ばされない角度があることがわかります。サンプルはアニソトロピースケールを-1でライティングしています。(スケールが-1、つまりV方向に引き延ばしているということです。)

なぜ、反射光が全く変化しない角度が存在するのでしょうか? アニソトロピーライティングの簡単な図解を見てみます。

リアルタイムレンダリングでは、メッシュへの入射角に対してその反射角方向に映るキューブマップをライティングに加算します。キューブマップとは、上下左右前後ろの6枚をあらかじめレンダリングしておき、疑似的な反射光として利用するテクスチャの一種です。Unityではリフレクションプルーブという名前で使われます。

Unityの異方性反射では、この反射角度を回転させることで、キューブマップをずらし、あたかも反射が歪んでいるかのようにライティングしているわけです。正確にはキューブマップを動かしているのではなく、本来取るべきキューブマップのテクスチャ座標をずらしている、ということです。以下の図説は座標軸もわかりやすく傾けていますが、法線や従法線が実際に傾いているわけではありません。
anisotropy_lighting.gif

先ほどのサンプルでは、タンジェントマップを設定せずにアニソトロピースケールを-1に設定しました。つまりV方向にずらしています。上の図説のように従法線が上向いた状態となります。ですので、従法線方向が奥行になる視点からメッシュをレンダリングすると、反射光が歪んで見えます。一方で、接線方向が奥行になる視点からでは、まったく歪みが現れません。接線が上向いているわけではないからです。逆に、アニソトロピースケールを1に設定すると、接線が上向いた状態となり、反射光がU方向にずれたライティングになります。

以上のことをまとめると、タンジェントマップの役割は、反射角度のヨーを担っているといえます。そして、インスペクターのスケール調整スライダーはロールとピッチを担っているということになります。

また、ノーマルマップとは違って、異方性反射は法線を動かしません。つまり、どの角度から見ても凹んでいるようには見せずに反射光を曲げたライティングができる、ということがノーマルマップとの最大の違いになります。

axis.jpg

 

タンジェントマップの作り方

タンジェントマップではベクトルのxがレッド(R)成分、yがグリーン(G)成分に相当します。ベクトルを書き込むツールはペイントソフトには搭載されていないので、法線軸の回転(オイラー角)を白黒のグラデーションで書き込み、のちにベクトルに変換します。

ここでは黒を回転角度0、白を回転角度360として扱っており、グラデーションマップで色変換を行っています。グラデーションマップはgithubにて配布しますので、ご自由にお使いください(ダウンロードはこちら)。グラデーションのかけ方やグラデーションマップの適用方法は割愛します。
gradationmap.jpg

何個かテクスチャを作ってみてわかったことですが、円形に循環するようにタンジェントを組んであげる方がルックを強力にできます。円形にすることで、必ずビュー方向に向くタンジェントが存在するようになり、反射光の拡散が常にライティングされるためです。以下のサンプルだと、左よりも右のタンジェントマップのほうが異方性拡散を視認しやすくなります。
tangentsample.jpg tangentsample2.jpg

アニソトロピーマップの使用例

アニソトロピーマップというよりタンジェントマップの活用例といった方が正しいかもしれません。できるだけ効果を視認しやすいマテリアルを選びました。

パールメッキ

2048x1152_114.jpg

亜鉛メッキ

2048x1152_115.jpg

ウロコスピン

2048x1152_116.jpg

円形紋様

2048x1152_117.jpg

トライアングル

2048x1152_118.jpg

アニソトロピーマップを使う上での注意点

  • スムーズネスを1に設定した場合、アニソトロピーマップが無視されて完全反射になります。アニソトロピーマップを使用する際は、スムーズネスを0.99以下に設定してください。
  • スケールはマイナス方向に動かした方が、ルックがそれっぽくなります。経験則からすると、スムーズネスを0.7、アニソトロピースケールを-0.3にすると、一番映える気がします。
  • 複雑なアルベドマップを作らない方が色味の変化を視認しやすくなります。色を塗る場合は単色で塗るほうが良いです。

まとめ

Unityの異方性反射ライティングは、アニソトロピーマップが難しいのではなく、タンジェントマップがややこしいです。ノーマルマップに比べると、効果が地味な部分もありますが、異方性反射にしかできない質感があります。HDRPでプロジェクトを進めるなら、一度触ってみてください。

おまけ

サンプルをアセットストアに出品しました。アニソトロピー表現を詰め込んだマテリアル集となっております。手早くアニソトロピーを体験したい方は、どうぞお買い求めください。

Anisotropy Material Pack

また、上のアセットをストアに出品した手順を記事にし、同日別のアドベントカレンダーで公開しました。アセットストア出品に興味のある方はご覧になってください。

参考文献

https://learn.foundry.com/modo/901/content/help/pages/shading_lighting/shader_items/anisotropic_direction.html
https://help.sketchfab.com/hc/en-us/articles/360020687812-Anisotropy

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

お蔵入りをお金に換える!!UnityAssetStoreに出店する手順まとめ

この記事は「Unityアセット冬のアドベントカレンダー 2019 Winter!」の12/3の記事となります。
Unityアセット冬のアドベントカレンダー 2019 Winter!

みなさん、「お蔵入り」って誰しもが経験しますよね? (...私も今年5本お蔵に送りました、アーメン)しかし、お蔵入りにしてしまうには惜しい素材やスクリプトって少なからずありません?

それ、アセットにしちゃいましょう!!

アセットストアに出品するのってなんだが難しそう、なんて思っているあなた!! 実は簡単に出品できるんですよ。本稿では、アセットストアへの出品プロセスを、実際に出品している様子とともにまとめてみました。

①パブリッシャーアカウントを登録する

Unityアカウントとは別物です。以下の公式ページからアカウント作成に進めます。(こちら)

②新規パッケージを作成する

パブリッシャーアカウントを作成後、自身のストアページにログインします。ログインの入口は、先ほどのアカウント作成手順を示した公式ページ右段にあります。

ログインしたら、packagesの項目に進み、Create New Packageを押して新しいパッケージテンプレートを作成します。
create.jpg

③MetadataとArtworkを編集する

Metadata&Artwarkの欄よりストアページの外観を編集します。ここに書いた内容が、アセット名やストアページの紹介文に使用されます。

アセットの説明を書く

まずはアセットタイトルとアセット説明文を編集します。これらはmetadataより編集できます。htmlのタグ(<em>, <strong>, <a>, <p>, <br>)が使えるので、web記事を書くぐらいの気持ちで内容を練ります。ページを離れる際には、右下のSaveボタンを押し忘れないようにしましょう。

英語の説明文は絶対に入れます。Unityのユーザー数のうち、ジャパニーズなんてたかが知れています。google翻訳でもいいので、英語で書きましょう。説明文に入れたほうがいい情報としては、

  • パッケージ構成(モデル数、テクスチャ数、プレハブ数)
  • 動作環境(LWRPでは動きません、2019.1.0f2以降は動作確認済み......等)
  • テクスチャサイズ(1k、2k, 4k)
  • 自作シェーダーの有無
  • 最新アップデートによる変更点
  • バンドル販売によるアセットの重複
  • 別売りアセットが必要かどうか

今回はこんな説明文にしました。
metadata.jpg

Key Imagesにアセットの紹介画像を追加する

アセット出品において一番面倒なのはここです。用意する画像は以下の5つです。テンプレートファイルを公式サイトよりダウンロードできます。(こちら)

  • アイコン (160x160 pixel) ・・・ 1枚
  • カード画像 (420x280 pixel) ・・・ 1枚
  • カバー画像 (1950x1300 pixel) ・・・ 1枚
  • SNSプロモーション画像 (1200x630 pixel、テキストおよびロゴは不可) ・・・ 1枚
  • スクリーンショット (2048x1152 pixel) ・・・ 好きなだけ

SNSプロモーション画像にgifは使えません。アップロード容量が8メガもあるうえに、gifファイルをアップロード出来てしまうので、一見大丈夫なように見えますが、審査で弾かれます(1敗)。

公式が配布するテンプレートには、ロゴやテキストのセーフゾーンが指示されています。テキストを載せてはいけない等の制限を守って商品イメージを作成します。今回作ったカバー画像はこちら。

Unity Recorderでキャプチャすると楽

画像サンプルの撮影方法がわからない人は、Unity Recorderを使ってみましょう。Unity Recorderはゲームビューを直接キャプチャしてくれるプラグインです。デフォルトでは入っていないので、Window > Package Managerより追加します。

使い方は簡単。画像を撮るかムービーを撮るかを選択して、インスペクターの赤い再生ボタンを押すだけ。撮影が終わると、指定したフォルダにファイルを出力してくれます。撮影したファイルが見当たらない場合はctl + R でフォルダを更新してください。
 
レコーダーの設定で注意すべき点は、撮影フレームを0にしないことです。ポストプロセッシングの中には前フレームを利用するタイプのエフェクトがあり、0フレーム目はうまく出力されません。フレームは余裕をもって60ぐらいに設定します。CaptureはGame View、ResolutionはMatch Window Sizeに指定します。
recorder.jpg
ゲームビュー上段より解像度のテンプレートを追加できます。ここに、あらかじめ必要な画像の解像度を設定しておくと、のちのちの撮影が楽です。こうやってRecorderを利用することで、photoshop等に取り込んだ際に縮尺をいじる必要がなくなります。ただし、アイコンとカード画像は、Unity Recorderではなく、スクリーンショットやカバー画像から切り取った方が綺麗に仕上がります。
screenshot.jpg
 

アセットの紹介動画を追加する

面倒なら動画は無くてもいいかもしれません。しかし、ページのアクセス数が動画の再生数より推測できるので、できるだけ作成することをおすすめします。映像作品である必要はまったくないので、1分程度の紹介動画で結構です。

動画の撮影もUnity Recorderを使用します。CinemachineやTimelineがあると撮影が捗りますが、Cinemachineの使い方や動画の編集等は本筋から外れるので、割愛します。 動画はyoutubeにアップすることがほとんどなので、youtubeに合わせた解像度とアスペクト比に設定します。
画像ではHD -720p になっていますが、FHD - 1080pの解像度がおすすめです。
recorder2.jpg

今回作った動画はこちら。

④パッケージをアップロード

パッケージ構成

パッケージはDemoシーンとReadmeがないと審査に落ちるようです。スクリプト系のアセットであれば、デモシーンはとても重要です。スクリプトをセットアップ済みのデモシーンを作りましょう。CGモデル系のアセットであれば、プレハブの一覧を置いたシーンで問題ないと思います。CGモデルがメインのアセットであれば、Prefabフォルダも準備します。
directory.jpg

イケてるReadmeの作り方

Readmeの書き方がイマイチわからない、って方多いと思います。簡単なtextファイルでも審査は通るのですが、どうせなら見栄えを良くしませんか? ver.2018以降、LWRPやHDRPのプロジェクトを作成すると、デフォルトでReadmeが作成されます。こいつのレイアウトがなかなか見やすいので、これをテンプレートに作成します。以下の記事がとても参考になります。

nakamura001, アプリ内に Unity 式の Readme を置く方法, 強火で進め, 2018-09-05

上の記事のように、編集のたびに[CustomEditor(typeof(Readme))]をコメントアウトするのは面倒なので、Readmeを作成したら、メモ帳からファイルを開いて編集しましょう。パラグラフの差し込みなどもテキストエディターの方がやりやすいです。アイコンを設定する場合は、Readmeと同じ階層に配置しましょう(フォルダをパッケージにするため、フォルダ外の画像はアップロード後に参照できなくなります)。今回作成したreadmeはこちら。
readme.jpg

Asset Store Toolsを使ってパッケージをアップロード

Asset Store Toolsはストア内からダウンロードできます。一応webページも貼っておきます。(こちら
toolをプロジェクトに導入すると、ツールバーにAsset Store Toolsが追加されます。Asset Store Tools > Package Upload よりツールを起動すると、ログイン画面に進めるので、先ほど作成したパブリッシャーアカウントでログインします。

  • 項目1で、アップロードするアセットストアページを選びます。
  • 項目2で、パッケージにするフォルダを選択します。
  • 項目3で、アセットに含まれるプラグインがあれば、チェックを入れます。できるだけ不必要なプラグインは抜くことをおすすめします。
  • 項目4は、簡易的なチェッカーとなっています。ガイドラインに則っていない部分を自動検知してくれます。ただし精度が良くありません、パス外のプレハブとか平気で誤検知します。
  • 不備がないか再度チェックしてアップロードします。

注意点として、審査が通ったストアページに更新したアセットをアップロードするには、ストアの編集ページの「Create New Draft to edit Package」より新規ドラフトを作らなければなりません。

 upload2.jpg

⑤動作環境を設定

platform.jpg

パッケージをアップロードしたら、動作環境を設定します。Editを押して入力画面に入ります。上から、

  • 動作するハード環境
  • 動作するUnityのバージョン
  • 動作するパイプライン
  • 別売りで必要なアセットの有無

を設定します。審査にしかこの項目は使わないようです。選択によって検索から外れるようなことはないみたいなので、審査してくれる人のためにも、できるだけ正確に入力しましょう。

platform2.jpg

⑥値段を決める

4.99ドル以下の値付けはできなくなりました。似たようなアセットが出品されているはずですので、相場から離れすぎないように値段を決める方がいいでしょう。トイザらス風な値段が、外国人受けがいいと聞いたことがあります(e.g. 14.99$, 49.99$...etc)。

⑦Submit

以上の項目をすべて設定し終えると、ようやくパッケージを提出できます。コメントを何かしら入れて、提出します。コメントは英語であればなんでもいいと思います。
submit.jpg
お疲れさまでした。1週間ほどで審査結果がメールで送られてきますので、気長に待ちましょう。

やってはいけないこと

画像、動画の見栄えを過度に編集で加工する

商品イメージが商品そのものとは別物になるような加工はやめましょう。それは詐欺です。ダメ、ぜったい。商品アセットとUnityだけで実現できるルックをサンプル画像にしましょう。もし別売りのプラグインやシェーダーを使用しているのであれば、必ずその旨を明確に記さなければなりません。

許可のない二次配布

Unityの環境では、意図しないアセットがパッケージに入りこむ可能性が十分にあります。権利関係は十分確認したうえでアセットを出品しましょう。意外と知られていないかもしれませんが、ユニティーちゃんトゥーンシェーダーは二次配布可能です(ライセンスについて)。UTSを使った作ったキャラクターでも、アセットストアに出品できます。

アダルト系

ダメというか、審査で弾かれるようです。ストアでもエッチなグッズは見たことないので、多分本当だと思います。Unityはエロに厳しい。水着衣装のキャラクターはストアで見たことがあるので、水着ぐらいなら大丈夫だと思います。

まとめ

今回アセットにして出品したマテリアルは、UnityForProアドベントカレンダーの記事用に作成したものです。そちらの記事も同日公開いたしました。High-Definition Render Pipelineを使った中級者向けの内容ですが、興味がある方はぜひご覧になってください。

最後に、この記事を読んで、アセット販売へのハードルが一段二段下がってもらえれば、幸いです。自分なりの手順なので、もっと効率のよいやり方や売り方等あるかと思います。アドバイスがありましたら、ぜひコメント欄よりご意見お聞かせください。

これまでに作成したアセット

買ってってよ、ねえ。ねえってば。買っt商品イメージやアセット作りの参考になれば幸いです。画像クリックでストアに飛べます。

Anisotropy Material Pack(今回出品したアセット) - 4.99$

※審査で一回却下を食らってしまったので、記事公開時点でリンクが未通かもしれません。

Japanese Common Pool - 9.99$

Food stall ver.Oden - 14.99$

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】2D Spine Animation ムカデとかの動き。

ムカデっぽい動きを作りたかった

今作っているゲームのボスに画面上を縦横無尽に動き回るムカデのボスを実装しようと思ったのがこの記事を書くきっかけでした。

どんな感じの物を想定していたか

ss_bad30e7c79c66b1717b1259042bf9566762931d4.1920x1080.jpg
これはRisk of Rainというゲームに出てくるワームなのですがこのような感じの物を想定していました。
Risk of Rain

どうやって作ろう?

頭と体が別々で頭がPlayerに向かってい飛んで行って体が同じ速度で頭の方向に飛んでいけばそれっぽくみえるんじゃないかな?

まずは頭だけ飛んでいく処理

bu7tu-zttvs.gif

できた!

次は体も追従するように

qwijg-3xr3j.gif

サンプルコード

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

public class test :MonoBehaviour
{
    Vector2 A, C, AB, AC;               // ベクトル

    public GameObject target;           // 追いかける対象

    public Sprite headSprite;           // 頭の画像
    GameObject head;                    // 頭オブジェクト

    public int bodyLength=10;           // 胴体の数
    public Sprite bodySprite;           // 胴体のPrefab
    public GameObject[] bodys;          // 胴体の配列

    public Rigidbody2D[] rigidbody2Ds;          //Rigidbody2Dの配列

    public float speed;                 // 移動スピード
    public float maxRot;                // 曲がる最大角度
    public float maxRot2;                // 曲がる最大角度
    void Start()
    {
        //頭を生やす
        head = new GameObject();
        head.transform.position = transform.position;
        head.transform.SetParent(transform);
        head.name = "Head";
        head.AddComponent<SpriteRenderer>();
        head.GetComponent<SpriteRenderer>().sprite = headSprite;
        head.AddComponent<BoxCollider2D>().isTrigger = true;
        head.AddComponent<Rigidbody2D>();
        head.GetComponent<SpriteRenderer>().sortingOrder = bodyLength;

        //胴体の長さ分配列を用意
        bodys = new GameObject[bodyLength];
        rigidbody2Ds = new Rigidbody2D[bodyLength+1];
        rigidbody2Ds[0] = head.GetComponent<Rigidbody2D>();
        //胴体を生成
        for (int i = 0; i < bodys.Length; i++)
        {
            bodys[i] = new GameObject();
            bodys[i].name = "body" + i.ToString();
            bodys[i].transform.SetParent(transform);
            bodys[i].AddComponent<SpriteRenderer>();
            bodys[i].GetComponent<SpriteRenderer>().sprite = bodySprite;
            bodys[i].AddComponent<BoxCollider2D>().isTrigger=true;
            bodys[i].transform.position=new Vector3(head.transform.position.x, head.transform.position.y-bodys[i].GetComponent<BoxCollider2D>().size.y*(i+1),head.transform.position.z);
            bodys[i].AddComponent<Rigidbody2D>();
            bodys[i].GetComponent<SpriteRenderer>().sortingOrder = bodys.Length - 1 - i;

            rigidbody2Ds[i + 1] = bodys[i].GetComponent<Rigidbody2D>();
        }
    }

    // Update is called once per frame
    void Update()
    {

        HeadMove(Theta(head,target));

        for (int i = 0; i < bodys.Length; i++)
        {
            BodyMove(Theta(bodys[i],(i==0)?head:bodys[i-1]),i);
        }
    }

    // θを求める
    float Theta(GameObject _myObject,GameObject _target)
    {

        A = _myObject.transform.position;                           // 自身の座標
        C = _target.transform.position;                 // ターゲットの座標

        AB = _myObject.transform.up;                                // 自身の上方向ベクトル
        AC = C - A;                                     // ターゲットの方向ベクトル

        float dot = Vector3.Dot(AB, AC);                        // 内積
        float rot = Acosf(dot / (Length(AB) * Length(AC)));     // アークコサインからθを求める

        // 外積から回転方向を求める
        if (AB.x * AC.y - AB.y * AC.x < 0)
        {
            rot = -rot;
        }

        return rot * 180f / Mathf.PI;                           // ラジアンからデグリーに変換して角度を返す
    }

    // 移動処理
    void BodyMove(float rot,int _i)
    {
        // 求めた角度が曲がる最大角度より大きかった場合に戻す処理
        if (rot > maxRot2)
        {
            rot = maxRot2;
        }

        else if (rot < -maxRot2)
        {
            rot = -maxRot2;
        }

        bodys[_i].transform.eulerAngles += new Vector3(0, 0, rot); // 回転
        bodys[_i].GetComponent<Rigidbody2D>().velocity = AB.normalized * speed; // 上に移動
    }
    // 移動処理
    void HeadMove(float rot)
    {
        // 求めた角度が曲がる最大角度より大きかった場合に戻す処理
        if (rot > maxRot)
        {
            rot = maxRot;
        }

        else if (rot < -maxRot)
        {
            rot = -maxRot;
        }

        head.transform.eulerAngles += new Vector3(0, 0, rot); // 回転
        head.GetComponent<Rigidbody2D>().velocity = AB.normalized * speed; // 上に移動
    }

    /// <summary>
    /// ベクトルの長さを求める
    /// </summary>
    /// <param name="vec">2点間のベクトル</param>
    /// <returns></returns>
    float Length(Vector2 vec)
    {
        return Mathf.Sqrt(vec.x * vec.x + vec.y * vec.y);
    }

    /// <summary>
    /// Acosの引数の値が+-1を越えたとき1に戻すAcos関数
    /// </summary>
    /// <param name="a">内積 / (ベクトルの長さ * ベクトルの長さ)</param>
    /// <returns></returns>
    float Acosf(float a)
    {
        if (a < -1) a = -1;
        if (a > 1) a = 1;

        return (float)Mathf.Acos(a);
    }
}

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

UnityのTilemapで有名ゲームのステージを作成してみる(見た目だけ)

Tilemapという面白そうなものを試すために、有名ゲームのステージを作成してみました。
※今回は見た目だけの作成です

Tile Palletを開くときの詳しい流れについての記事が少なかったため、そちらのまとめでもあります。

画像の設定

まず、Tilemapで使用したい画像を用意します。今回はこちらの画像を使用します。もうどのゲームかお分かりですよね。
Tile.png

画像をインポートし、設定を以下のように変更します。
TileImportSettings.png

そして、Sprite Editorをクリック。ここから画像を区切っていきます。
上の方にあるSliceをクリックし、TypeとPixel Sizeを変更します。今回の画像は一つ一つを32×32で作成しているため、Pixel Sizeも32にしています。
SpriteEditor.png

Sprite Editorをクリックしてエラーが出た場合
以下の方法で解消されます。
1. Window > Package Managerを開く
2. 2D Spriteを選択し、右下のInstallをクリック
PackageManager_2DSprite.png

これで画像の設定は完了です。

Tile Palletの作成

まず、Window > 2DからTile Palletを開きます。

Tile Palletが表示されていない場合
Window > 2DにTile Palletが表示されていない場合には、以下の方法で開くことができます。
1. Window > Package Managerを開く
2. 2D Tilemap Editorを選択し、右下のInstallをクリック
PackageManager_2DTilemapEditor.png

これでもう一度Window > 2Dを確認するとTile Palletが追加されています。

Tile Palletを開くと、以下のようなウインドウが開きます。
TilePalet.png

Create New Paletteをクリックすると、新しいTile Palletを作成できます。
CreateNewPalette.png

Tile Assetの作成

画像をTile Palletウインドウにドラッグ&ドロップすることで作成できます。
TileAsset.png

Tilemapを使ってみる

では、実際にTilemapを使って、ステージを作ってみます。

タイルの塗り方は、Tile Palletウインドウでブラシを選択し、塗るタイルを選択。
そして、塗りたい場所でクリックするだけです。ドラッグでも塗ることができます。
Paint.png

これで、有名ゲームのステージが完成しました!
complete.png

おわりに

今回はステージの見た目だけを作成しましたが、Tilemapでは当たり判定もできるということで、色々試してみたいと思います。そして、可能であればこのステージを完成させていきたいです!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】ファイル名とクラス名が合ってるのにコンパイルエラーで動かなくなる現象の対策法

やらかした

急にUnityが言うことをきかなくなってしまいました。
コンパイルが全く通らなくなりました。
思い当たる直接の原因はUnityを起動した状態のまま、その開いているプロジェクトをコピーしたことです。

検索してもファイル名とクラス名が違う話ばかり…そういうことではないんです。
Unityを再インストールしても症状は変わらず。そんな場合の対策記事です。

環境

  • OS:Windows10 Pro
  • Unityバージョン:2019.2.12.f1

解決策

  • Unityをアンインストールします。
  • Unityのアンインストーラーが削除しないファイルを消します。
  • 新しくWindowsのユーザーを作ります。
  • 再度Unityをインストールします。

出た症状や詳細

起こったことや情報をまとめます。

症状

  • 急にコンポーネントをアタッチできなくなりました。

    • アタッチしようとするとエラーメッセージが出てつけられなくなります。メッセージは Can't add script component 'コンポーネント名' because the script class cannot be found. Make sure that there are no compile errors and that the file name and class name match.
    • image.png
    • スクリプトを選択すると No MonoBehaviour script in the file, or their names do not match the file name. と言うメッセージがインスペクタに表示されます。
    • image.png
  • 空白メッセージの謎のコンパイルエラーが常に発生します。

  • image.png

  • Visual Studio上でも作りたてのスクリプトが using UnityEngineMonoBehaviour の参照を認識しません。

  • アンインストールしようがアプリケーションデータを消そうが再インストールしたら症状が続きます。

もしかして

  • 権限関係が破壊されたのでは、という情報をもらいました。権限が破壊され操作不能になったファイルを含む部分の権限を正しく再設定すればよいのは道理です。ただし検証や修復には莫大な時間がかかるでしょう。私は新しいユーザーを作ることで済ませることにしました。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RustでUnityプラグインを作って敗北する

なんか最近流行ってるらしい

浮世の変化には疎いのですが、なんか流行ってるらしいですねRust
実はUnityのネイティブプラグインを作ってみたかったのですが、CC++もやったことない上に勉強する気もないため踏み切れないでいました。
いい機会なのでRustで作ってみます。

目的

  • Rustを使ってみる
  • Unityのネイティブプラグインを作ってみる
  • 自分の学習軌道をメモしておく

書いている人

スマホ開発がメイン
CC++は未経験
値渡しと参照渡しはわかるけどぽいんた? とかいうのはわからん

情報取得

とりあえずインプットします。

Rust

Rustの日本語ドキュメント/Japanese Docs for Rust
プログラミング言語 Rust, 2nd Edition/ The Rust Programming Language, Second Edition
プログラミング言語Rust
The Rust Programming Language

必修言語Rustの他己紹介
Rust についてのメモ
Rustのポインタ(所有権・参照)・可変性についての簡単なまとめ
Rustはこうやって勉強するといいんじゃないか、という一例
Rustのクレート・ツールを探すための情報源

コンパイル通るまで殴ればいい! かんたん!
こんなにやさしいコンパイラ初めて見ました。cargoによる依存関係の管理もよく練れていていい感じです。
Kotlinなんかもそうですが近代の言語はユーザ獲得のため、チュートリアルがおそろしく充実していていいですね。
あとプロジェクト新規作成したらgit作ってくれるのすごい。

NativePlugin

Rustで実装したアルゴリズムをUnityから使う
C#からC++(DLL)に配列を渡す
How do I get Rust FFI to return array of structs or update memory?
How to return an array of structs from Rust to C#

プラグイン更新のたびにUnityEditorの再起動が必要とかつらい。

エディタ

intellij製品じゃないともうなにも書けない。
MEET INTELLIJ RUST

Vector3を100倍して返す

ひとまず手始めとして、C#から渡されたVector3Rust側で100倍して返します。

とりあえず作る

Rust側
// こんな感じで構造体を定義
#[repr(C)]
pub struct Vector3 {
    x: f32,
    y: f32,
    z: f32,
}

// 外部公開する関数
#[no_mangle]
pub extern fn size_up(v: &Vector3) -> Vector3 {
    Vector3 { x: v.x * 100.0, y: v.y * 100.0, z: v.z * 100.0 }
}
C#側
[DllImport("libtest")]
private static extern Vector3 size_up(Vector3 moto);

さっそく実行してみましょう。

テストコード
var v3 = UnityEngine.Random.insideUnitSphere;
var sizeUpV3 = size_up(v3);
Debug.Log($"{v3} -> {sizeUpV3}");
実行結果
(-0.1, -0.8, -0.2) -> (254563.7, 0.0, 1402342000000000000000000.0)

……なんか……なんだろう、よくないことが起こっているようですね。

借用をやめる

というわけで、Rust側を修正します。

引数を修正
#[no_mangle]
pub extern fn size_up(v: Vector3) -> Vector3 {
    Vector3 { x: v.x * 100.0, y: v.y * 100.0, z: v.z * 100.0 }
}

引数のv: Vector3をうっかり拝借していましたが、Rust内で完結するならばともかくC#から借りてくるのはあまりにも無茶な話でした。というわけでそのまま渡してみます。

実行結果
(-0.3, -0.7, 0.2) -> (-26.1, -66.8, 21.3)

……またよくないことが……いや、よく見たら四捨五入してそうな数字です。

nicely formatted

Rider先生にデコンパイルしてもらってVector3ToStringを覗いてみます。

Vector3.ToString
/// <summary>
///   <para>Returns a nicely formatted string for this vector.</para>
/// </summary>
/// <param name="format"></param>
public override string ToString()
{
  return UnityString.Format("({0:F1}, {1:F1}, {2:F1})", (object) this.x, (object) this.y, (object) this.z);
}

nicely formatted string

${\Large なに言うとるがじゃ!!!!!}$

しょうがないのでこんな感じの拡張メソッド定義してありのままの姿を見せてもらうことにします。

ToStringFloat
static class Extensions
{
    public static string ToStringFloat(this Vector3 vector3)
    {
        return $"({vector3.x}, {vector3.y}, {vector3.z})";
    }
}
テストコード
var v3 = UnityEngine.Random.insideUnitSphere;
var sizeUpV3 = size_up(v3);
Debug.Log($"{v3.ToStringFloat()} -> {sizeUpV3.ToStringFloat()}");
実行結果
(-0.01608862, 0.5905958, 0.7266953) -> (-1.608862, 59.05958, 72.66953)

できました。

気になるのは借用をやめた修正です。
C#側では「もともとのVector3」「引数としてコピーされたVector3」の2つがあります。
「もともとのVector3」はC#が管理しているからいいとして、Rustの借用ではないということは「引数としてコピーされたVector3」をRust側で開放しちゃってそうな気がしますが、これってC#側の扱いはどうなっているのでしょうか。externだとそのあたり忖度されるんでしょうか。もしくはstructなのでC#から渡すときに値をコピーしてるから大丈夫なのか。まあいいか。

計測

ようやくNative Pluginを使いたい理由に入ります。
Meshの頂点座標を基準点からの相対位置に変換する処理ですが、この処理がやや重い……ような気がします。そう頻繁に行う処理でもないので無理に高速化する必要もないのですが、今回はやってみることそのものが目的です。
というわけで、以下のC#で書かれた関数をRust側へ計算処理を逃がす関数にするのが今回のゴールです。

TransWithCsharp
public static Vector3[] TransWithCsharp(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
    var ret = new Vector3[points.Count];
    for (var count = 0; count < points.Count; count++)
    {
        ret[count] = matrix.MultiplyPoint(points[count]);
    }
    return ret;

    // LINQでこう書くと実際オサレ
    // return points.Select(matrix.MultiplyPoint).ToArray();
}

matrix4x4

C#からMatrix4x4を受け取るため、Rust側で同じ構造体を定義します。
本来ならありものを使うのではなく、C#側でも自分でちゃんと受け渡すための構造体を定義するべきですが、Vector3もそのまま渡せたんだからMatrix4x4も行けるやろ! の精神です。

Matrix4x4
#[repr(C)]
pub struct Matrix4x4 {
    m00: f32,
    m01: f32,
    m02: f32,
    m03: f32,
    m10: f32,
    m11: f32,
    m12: f32,
    m13: f32,
    m20: f32,
    m21: f32,
    m22: f32,
    m23: f32,
    m30: f32,
    m31: f32,
    m32: f32,
    m33: f32,
}

ちゃんとRust側で受け取れているか試すために、以下の関数を作ってC#と突き合わせてみます。

#[no_mangle]
pub extern fn matrix_add(matrix: Matrix4x4) -> Vector3 {
    Vector3 { x: matrix.m00, y: matrix.m01, z: matrix.m02 }
}
テストコード
var Anchor = new GameObject().transform;
Anchor.position = UnityEngine.Random.insideUnitSphere;
Anchor.rotation = UnityEngine.Random.rotation;
Anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;
var matrix = Anchor.transform.localToWorldMatrix;

var a = matrix_add(matrix);
Debug.Log(a.ToStringFloat());
var b = new Vector3(matrix.m00, matrix.m01, matrix.m02);
Debug.Log(b.ToStringFloat());
実行結果
(-0.9590587, -0.6367525, -0.6988028)
(-0.9590587, -0.2962521, 0.6547196)

最初だけ合っている。ということはつまり構造体のメンバの定義されている順番が違うのでしょう。
再びRider先生にデコンパイルしてもらいます。

  public struct Matrix4x4 : IEquatable<Matrix4x4>
  {
    [NativeName("m_Data[0]")]
    public float m00;
    [NativeName("m_Data[1]")]
    public float m10;
    [NativeName("m_Data[2]")]
    public float m20;
    [NativeName("m_Data[3]")]
    public float m30;
    [NativeName("m_Data[4]")]
    public float m01;
    [NativeName("m_Data[5]")]
    public float m11;
    [NativeName("m_Data[6]")]
    public float m21;
    [NativeName("m_Data[7]")]
    public float m31;
    [NativeName("m_Data[8]")]
    public float m02;
    [NativeName("m_Data[9]")]
    public float m12;
    [NativeName("m_Data[10]")]
    public float m22;
    [NativeName("m_Data[11]")]
    public float m32;
    [NativeName("m_Data[12]")]
    public float m03;
    [NativeName("m_Data[13]")]
    public float m13;
    [NativeName("m_Data[14]")]
    public float m23;
    [NativeName("m_Data[15]")]
    public float m33;
  }

十の位から増えてるの……?
なんか感覚と違いますが、そう定義されている以上はしょうがありません。Rust側の構造体の定義の順番を変えます。

#[repr(C)]
pub struct Matrix4x4 {
    m00: f32,
    m10: f32,
    m20: f32,
    m30: f32,
    m01: f32,
    m11: f32,
    m21: f32,
    m31: f32,
    m02: f32,
    m12: f32,
    m22: f32,
    m32: f32,
    m03: f32,
    m13: f32,
    m23: f32,
    m33: f32,
}
実行結果
(-0.04724042, -0.6401328, 0.3618424)
(-0.04724042, -0.6401328, 0.3618424)

一致しました。Matrix4x4はちゃんとC#からRustに渡せています。

ダブルキャスト

/// <summary>
///   <para>Transforms a position by this matrix (generic).</para>
/// </summary>
/// <param name="point"></param>
public Vector3 MultiplyPoint(Vector3 point)
{
    Vector3 vector3;
    vector3.x = (float) ((double) this.m00 * (double) point.x + (double) this.m01 * (double) point.y + (double) this.m02 * (double) point.z) + this.m03;
    vector3.y = (float) ((double) this.m10 * (double) point.x + (double) this.m11 * (double) point.y + (double) this.m12 * (double) point.z) + this.m13;
    vector3.z = (float) ((double) this.m20 * (double) point.x + (double) this.m21 * (double) point.y + (double) this.m22 * (double) point.z) + this.m23;
    float num = 1f / ((float) ((double) this.m30 * (double) point.x + (double) this.m31 * (double) point.y + (double) this.m32 * (double) point.z) + this.m33);
    vector3.x *= num;
    vector3.y *= num;
    vector3.z *= num;
    return vector3;
}

肝心のMultiplyPointの処理です。
デコンパイラの結果というのもあると思いますが、なかなかにカオスな計算処理。
float -> double -> floatとキャストしている部分をRustでも再現するかは悩みどころですが、いったんは心を無にしてRustでも同様の処理を書きます。

#[no_mangle]
pub extern fn multiply_point(m: Matrix4x4, v: Vector3) -> Vector3 {
    let x = ((m.m00 as f64 * v.x as f64 + m.m01 as f64 * v.y as f64 + m.m02 as f64 * v.z as f64) + m.m03 as f64) as f32;
    let y = ((m.m10 as f64 * v.x as f64 + m.m11 as f64 * v.y as f64 + m.m12 as f64 * v.z as f64) + m.m13 as f64) as f32;
    let z = ((m.m20 as f64 * v.x as f64 + m.m21 as f64 * v.y as f64 + m.m22 as f64 * v.z as f64) + m.m23 as f64) as f32;
    let num = (1.0 / (m.m30 as f64 * v.x as f64 + m.m31 as f64 * v.y as f64 + m.m32 as f64 * v.z as f64) + m.m33 as f64) as f32;
    Vector3 { x: (x * num), y: (y * num), z: (z * num) }
}
テストコード
var v3 = UnityEngine.Random.insideUnitSphere;
Anchor = new GameObject().transform;
Anchor.position = UnityEngine.Random.insideUnitSphere;
Anchor.rotation = UnityEngine.Random.rotation;
Anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;
var matrix = Anchor.transform.localToWorldMatrix;

var withU = matrix.MultiplyPoint(v3);
Debug.Log(withU.ToStringFloat());

var withR = multiply_point(matrix, v3);
Debug.Log(withR.ToStringFloat());

実行結果
(-0.02336239, 0.5018276, 0.7009525)
(-Infinity, Infinity, Infinity)

Infinity...

数回繰り返したところ正負は合っているので、キャストに失敗して無限の彼方に辿り着いているようです。
こんなもんの原因追求する気はさらさらないのでRustのコードをきれいに書き直します。

fn multiple_float(a: f32, b: f32) -> f64 {
    ((a as f64) * (b as f64))
}

#[no_mangle]
pub extern fn multiply_point(m: Matrix4x4, v: Vector3) -> Vector3 {
    let x = multiple_float(m.m00, v.x) + multiple_float(m.m01, v.y) + multiple_float(m.m02, v.z) + m.m03 as f64;
    let y = multiple_float(m.m10, v.x) + multiple_float(m.m11, v.y) + multiple_float(m.m12, v.z) + m.m13 as f64;
    let z = multiple_float(m.m20, v.x) + multiple_float(m.m21, v.y) + multiple_float(m.m22, v.z) + m.m23 as f64;
    let a = multiple_float(m.m30, v.x) + multiple_float(m.m31, v.y) + multiple_float(m.m32, v.z) + m.m33 as f64;
    let num = 1.0 / a;
    Vector3 { x: (x * num) as f32, y: (y * num) as f32, z: (z * num) as f32 }
}

なんかもっときれいに書けるような、そうでもないような。
ともあれこれを実行してみます。

実行結果
(-0.1820646, -0.6444009, 0.6140736)
(-0.1820646, -0.6444009, 0.6140736)

一致しました。これでようやく完成です。

実験

さっそくC#と比べて早いのか遅いのか実験してみます。

テストコード
private const int PointCount = 1000000;

private async void Check(CancellationToken token)
{
    var anchor = new GameObject().transform;

    while (true)
    {
        anchor.position = UnityEngine.Random.insideUnitSphere;
        anchor.rotation = UnityEngine.Random.rotation;
        anchor.localScale = UnityEngine.Random.insideUnitSphere + Vector3.one;

        var matrix = anchor.transform.localToWorldMatrix;
        var randomVectors = await Task.Run(() => GenerateRandomVectorAsync(_cancellationTokenSource.Token, PointCount), token);

        // Rustによる変換
        Profiler.BeginSample("#ByRust");
        var r = TransByRust(matrix, randomVectors);
        Profiler.EndSample();

        // Rust(Releaseビルド)による変換
        Profiler.BeginSample("#ByRustR");
        var rr = TransByRustRelease(matrix, randomVectors);
        Profiler.EndSample();

        // C#による変換
        Profiler.BeginSample("#ByCSharp");
        var c = TransByCsharp(matrix, randomVectors);
        Profiler.EndSample();

        Debug.Log(r.Length + " - " + rr.Length + " - " + c.Length);
    }
}

// C#による変換
private static Vector3[] TransByCsharp(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
    var ret = new Vector3[points.Count];
    for (var count = 0; count < points.Count; count++)
    {
        ret[count] = matrix.MultiplyPoint(points[count]);
    }

    return ret;
}

// RustDebugビルドによる変換
private static Vector3[] TransByRust(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
    var ret = new Vector3[points.Count];
    for (var count = 0; count < points.Count; count++)
    {
        ret[count] = multiply_point(matrix, points[count]);
    }

    return ret;
}

// RustReleaseビルドによる変換
private static Vector3[] TransByRustRelease(Matrix4x4 matrix, IReadOnlyList<Vector3> points)
{
    var ret = new Vector3[points.Count];
    for (var count = 0; count < points.Count; count++)
    {
        ret[count] = multiply_point_r(matrix, points[count]);
    }

    return ret;
}

// ランダムなVector3の配列を生成
private static Task<Vector3[]> GenerateRandomVectorAsync(CancellationToken cancellationToken, int length)
{
    var random = new System.Random();
    var points = new Vector3[length];
    for (var count = 0; count < points.Length; count++)
    {
        cancellationToken.ThrowIfCancellationRequested();
        // UnityEngine.RandomのAPIはメインスレッドからしか呼べない...
        // なので無理矢理ランダムなVector3を生成する
        points[count].x = (float) (random.NextDouble() * random.Next(-100, 100));
        points[count].y = (float) (random.NextDouble() * random.Next(-100, 100));
        points[count].z = (float) (random.NextDouble() * random.Next(-100, 100));
    }

    return Task.FromResult(points);
}

これがプロファイラの結果です。

スクリーンショット 2019-11-27 3.56.35.png

C#が一番速い……。
Rustのリリースビルドとデバッグビルドで差が出ている以上、NativePluginだからプロファイラがおかしくなっているわけでもないようです。

f32が溢れるようなことはまずないので、Rust側でキャストを止めてみます。

multiply_point_without_cast
#[no_mangle]
pub extern fn multiply_point_without_cast(m: Matrix4x4, v: Vector3) -> Vector3 {
    let x = m.m00 * v.x + m.m01 * v.y + m.m02 * v.z + m.m03;
    let y = m.m10 * v.x + m.m11 * v.y + m.m12 * v.z + m.m13;
    let z = m.m20 * v.x + m.m21 * v.y + m.m22 * v.z + m.m23;
    let a = 1.0 / (m.m30 * v.x + m.m31 * v.y + m.m32 * v.z + m.m33);
    Vector3 { x: (x * a), y: (y * a), z: (z * a) }
}

スクリーンショット 2019-11-29 3.33.15.png

ちょっとはやくなってる。

仮説

1. UnityのMatrix4x4.MultiplyPointはC++層で実行されている
デコンパイルするってことはコンパイルされてるんだよねこれ
2. C#がmatrix4x4をキャッシュしているのに対し、Rustは毎回受け渡しているため非効率
これは確実にあるはず
3. 言語間で受け渡すコスト > Rustによる恩恵
単純な計算処理では意味がなかった

仮説1が一番大きいと思います。わたしが戦っていたのはキャストしまくりのC#ではなくバチバチにチューニングされたC++だったのです……たぶん。なので自分で実装した重い処理とかだったら違う結果が出るかもしれません。

仮説2の解決としてVector3[]の配列を受け渡しできればいいのですが、ポインタがわからないからマーシャリングもわからないので諦めました。Rust側でポインタを復元する方法もわからないです。

仮説3もわりとありそうな気がしています。いちいち変換している分のコストはかなり大きい……はず。

あと、いくらVector3とはいえこの数なら結構なGCを誘発していると思うのですが、プロファイラのGC Allocはみんないっしょです。NativePlugin部分に対するプロファイラの動作もいまいち情報がないのでよくわからん。

まとめ

Rust

今更ですがedition2018です。
Rustの学習ですが、ヤバいと噂の所有権は自分はそんなにひっかかりませんでした。でもライフタイムは微妙にまだよくわかってないかもしれない。
あと、エラー処理と並列プログラミングはちらっと読んだだけで何言ってるかまったく理解してないので改めて読もうと思います。
Rustの学習コストは確かに高いですが、コンパイラさんが徹底的にチェックしてくれることで実行時に吹っ飛ぶのを防止してくれるのはとても好きです。

NativePlugin

Unity+C#+Rustの知識を要求されるのつらい。

敗北

プログラミングぢからは高まった気がしますが、結果が出せていません。
しかし現在の自分ではこれ以上は手が出ない……。ポインタを理解するためにCを諦めてやってみるべきか……。
なにはともあれ今回はここで敗北します。誰かなんか強い人がなんとかしてくれたら嬉しいな! サヨナラ!

とりあえず書いた分は置いておきます。
gist

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む