- 投稿日:2019-11-29T22:26:21+09:00
【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してやればスケールをかけることが出来ます。
このように一文字の横幅が収まるサイズに縮んでくれます。やったね!まとめ
Textの処理内容に直接手をかけることになりましたが一応希望の機能は作ることが出来ました。デフォであっていいような気もするのでUnityさん作ってくれないかな~。もうちょっとスマートな方法を模索したい今日このごろです。
- 投稿日:2019-11-29T21:38:53+09:00
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 さんの記事です。
- 投稿日:2019-11-29T17:21:56+09:00
Azure Functions でファイルをAES暗号化しながらコピーする
Azure Functions でファイルをAES暗号化しながらコピーする
前回投稿から超久しぶりな投稿です。
.NET Core 3.0も出て、ASP.NET Core 2.0系の記事も賞味期限切れなので、今回は全く別の事を書きます。
いくつかの業務で必要になりそうだったので、検証してみた結果です。
タイトル見て実装まで思いついてしまうような実力を持った方は読む必要が無いような記事です。なんでそんなことする必要がある?
Azure Blob Storageで暗号化したいなら Storage Service Encryption を使えばいいじゃん!と思いますが、この機能がリリースされる前から自前で暗号化してファイルを置いておいた為、移行するコストが高すぎる、みたいなケースもあります。
その場合、当然どこかでファイルを暗号化する必要があるのですが、ローカル環境からAzureにファイル転送する場合には、転送前に暗号化したファイルを送れば済む話です。
しかし、Azure Blobにから元ファイルを別のBlobに移動する際に暗号化する、なんて要件があったりする場合もあります。
間に暗号化処理が挟まるとなると、 AzCopy を使えば済む話でもありません。どこで動かそう?
例えばAzure Functionsでテンポラリ領域にダウンロードしてファイルを暗号化し、暗号化済みファイルを転送する、なんてプロセスを想定した場合、Consumption planのAzure Functionsでのテンポラリ領域は500MB程度なので、最大でも250MB未満のファイルの転送しかできないことになります。
App Service planの場合はプランの内容に応じて話は変わりますが、共用であるテンポラリ領域を無尽蔵に使ったりすると、肝心のWebアプリケーション側にも影響を出しかねず、試してみたいとは思えません。
となると、豊富なローカルディスクディスク容量が使えるAzure Batchや、VMを普通に立てる等の対策を考えるかもしれませんが、そもそも常時動くわけでもない、(あえて言いますが)たかがファイルコピー如きに専用のVMを用意したくありません。Azure Functionsで動かしたい
一周回って、Consumption planのAzure Functionsに戻ってきました。
なら、テンポラリやメモリを無駄遣いしないAzure Functionsのコードを書けばいいじゃない、という結論です。
すみません。前置きが長すぎますね。C#コード
この例ではAES 128bit CBCモードで暗号化します。
要は「Blobも当然Streamで扱えるんだから、入力と出力をStreamにしてCryptoStreamを通せばいいや」という単純な話です。
実のところ、別にAzure Blobに限らずStreamさえ取れれば、どんなデータ転送でも使ます。
だからタイトルを見て実装まで思いつく人も多かろうと思います。(結局Streamの話になってしまうので)public static class BlobEncryptionCopy { [FunctionName("BlobEncryptionCopy")] public static void Run([QueueTrigger("copyenc")] string blobPath, [Blob("input/{queueTrigger}", FileAccess.Read, Connection = "AzureWebJobsInputStorage")] Stream inputBlobStream, [Blob("output/{queueTrigger}", FileAccess.Write, Connection = "AzureWebJobsOutputStorage")] Stream outputBlobStream, ILogger log) { log.LogInformation($"Start C# Queue trigger function processed: {blobPath}"); var encryptionKey = Convert.FromBase64String(Environment.GetEnvironmentVariable("AesKeyBase64String")); EncryptStream(inputBlobStream, outputBlobStream, encryptionKey); log.LogInformation($"End C# Queue trigger function processed: {blobPath}"); } private const int AesBlockByteSize = 16; private const int AesBlockBitSize = 128; private const int BufferSize = 4194304; private static void EncryptStream(Stream inputStream, Stream outputStream, byte[] encryptKey) { if (encryptKey == null) throw new ArgumentNullException(nameof(encryptKey)); if (encryptKey.Length < AesBlockByteSize) throw new ArgumentException("AES 128bit encryption key is too short.", nameof(encryptKey)); // AES暗号化キーの長さを強制的に16バイトで切る Array.Resize<byte>(ref encryptKey, AesBlockByteSize); using (var aes = new AesManaged()) { // AES暗号化の設定 aes.BlockSize = AesBlockBitSize; aes.KeySize = AesBlockBitSize; aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; aes.Key = encryptKey; // ランダムなIVを生成 var iv = new byte[AesBlockByteSize]; using (var rng = new RNGCryptoServiceProvider()) { rng.GetNonZeroBytes(iv); aes.IV = iv; } using (var encryptor = aes.CreateEncryptor()) using (var cs = new CryptoStream(outputStream, encryptor, CryptoStreamMode.Write)) { // 復号用にIVの値をファイル先頭に出力する outputStream.Write(iv, 0, AesBlockByteSize); int len; var buffer = new byte[BufferSize]; while ((len = inputStream.Read(buffer, 0, BufferSize)) > 0) { cs.Write(buffer, 0, len); } cs.FlushFinalBlock(); } } }コアとなる部分を抜き出して書いており、エラー処理は削っているので適宜加えてください。
下手にMicrosoft.Azure.Storage.Blobのnugetパッケージを参照していると動作しなくなるのもいつもの事なので、バインドのみで済ませています。コード説明
見ての通り、Queueトリガーで、Queueに書かれたBlobパスを入力元から出力先のBlobにAES暗号化しながらコピーします。
入出力のBlob StorageもFunctionsにバインドして、最初からStreamを受け取る形にしています。
最初からStreamを受け取っているので、出力用のStreamをCryptoStreamでラップして、暗号化したデータを転送します。入出力のBlob StorageはBlobAttributeでバインドを設定し、接続情報(Connection)はAppServiceのアプリケーション設定から読み込む形です。(ここではあえて省略せずに [AzureWebJobs(In|Out)putStorage] と記載していますが、 "AzureWebJobs" の部分は省略してもアプリケーション設定を取得してくれます)
同様に暗号化キーもアプリケーション設定にBase64で指定しているので、 "Environment.GetEnvironmentVariable" メソッドで取得しています。
また、IV (Initial Vector)については、今回はランダム生成してファイルの先頭に書き込む形をとっています。
動作確認
これで数KB、200MB、800MB、4GBと4種類のファイルを暗号化しながら転送したところ、4GBの物を除いて転送が成功しました。
4GBの物はタイムアウトで失敗していたので、タイムアウト時間を延長したところ、正常に転送できました。
host.jsonに追記して、現状Consumption planの最大値である10分になるように指定しました。host.json{ "version": "2.0", "functionTimeout": "00:10:00" }勿論、ダウンロードして復号化すれば、暗号化前のファイルと同じになります。
終わりに
Consumption planでも大き目なファイルの暗号化転送ができました。
ただし、あくまで検証レベルの内容なので、その点にはご留意ください。やはり、Streamなんだから当然だろという突っ込みが入りそうですね。
普通にできそうなことを実際にやってみて、やっぱりできたという記録だと思っていただければ幸いです。
- 投稿日:2019-11-29T12:28:55+09:00
C#で固定値とリストを含むjsonをデータベースに登録する
固定値とリストを含むjsonをデータベースに直接登録したい。
何言ってんの?って感じですよね、ごめんなさい。
ちょっと日本語で説明すると伝わりづらいですが、要はこんなようなjsonを直接データベースに登録したい。SKILLTABLE.json{"NAME": "Akira", "SKILL": [ { "LANGUAGE": "C++", "EX_YEAR": "4" }, { "LANGUAGE": "Java", "EX_YEAR": "3" }, { "LANGUAGE": "C#", "EX_YEAR": "2" } ] }登録後のイメージはこんな感じ。
一つの値は固定で、他のリスト部分は順々に登録したい。
NAME LANGUAGE EX_YEAR Akira C++ 4 Akira Java 3 Akira C# 2 やってみた
まずはプログラム上でJSONファイルを作成する。
(職場で書いたコードを思い出しながら編集しつつ書いてるので、細かいミスなどあるかもしれませんすいません)
engineer.cs//クラス定義 [JsonObject("skill")] class Skill{ [JsonProperty("language")] public string language {get; set;} [JsonProperty("ex_year")] public int exyear {get; set;} public Skill(string lang,int year){ this.language = lang; this.exyear = year; } } [JsonObject("engineer")] class Engineer{ [JsonProperty("name")] public string name {get; set;} [JsonProperty("skilllist")] public List<Skill> skill {get; set;} public Engineer(string Name,List<Skill> skilllist){ this.name = Name; this.skill = skilllist; } } //JSONファイルを作る List<Skill> skillList= new List<Skill>(); Skill cplus = new Skill("C++",4) Skill java = new Skill("Java",3) Skill csharp = new Skill("C#",2) skillList.Add(cplus); skillList.Add(java); skillList.Add(csharp); Engineer engineer = new Engineer("Akira",skillList); string json = Newtonsoft.Json.JsonConvert.SerializeObject(engineer);なんで最初から"Name"のメンバ変数加えとかないの?って思われるかもしれないですが
「Skillのリストが引数として与えられて、そこに名前だけ後付したい」
という要求があるものとしてみてください。jsonをデータベースに登録する
engineer.csString sql; sql = $"DECLARE @json NVARCHAR(MAX) = '{json}';" + $"DECLARE @name = 'SELECT name FROM OPENJSON(@json)WITH(name varchar(10) '$.name');'" + $"INSERT INTO table SELECT @name, language, ex_year FROM OPENJSON(@json,'$.engineer')" + $"WITH(language varchar(10) '$.language', ex_year varchar(2) '$.ex_year)"データベースに接続する部分だったり、SQL実行する部分は書いてないですが
それは他の記事やブログにも色々書いてあるので割愛します。
このSQLを叩いてあげれば登録できるはずです。おわり
SQLど素人だったのでこの方法を見つけるまでに大変時間がかかりました...
同じように困ってる人のヒントになれば幸いです。
冗長な書き方してる可能性があるので、その場合はご指摘お願いします。
- 投稿日:2019-11-29T12:05:30+09:00
コールバック
知らなかったんだけど、Authするときってコールバック必要でしょ?
それって、コールバック先のWebAPIを指定しないといけないと思ってたけど、ブラウザにコールバックでもよかったんだね。
つまり、サーバーを立てる必要がない。こんな感じで、WinForm単品で、WebViewのNavigatedイベントでAuthCodeが取れるんだね。
引数のクラスを見れば、即取り方は分かると思います。VisualStudioだもの。知らなかったわー。
今までは、自分のクラウドアプリにWebAPI用意して、xx/callbackみたいな口を用意してコールバック受けてって処理を進めていたわけです。
- 投稿日:2019-11-29T10:01:47+09:00
【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.
- スクリプトを選択すると
No MonoBehaviour script in the file, or their names do not match the file name.
と言うメッセージがインスペクタに表示されます。空白メッセージの謎のコンパイルエラーが常に発生します。
Visual Studio上でも作りたてのスクリプトが
using UnityEngine
やMonoBehaviour
の参照を認識しません。アンインストールしようがアプリケーションデータを消そうが再インストールしたら症状が続きます。
もしかして
- 権限関係が破壊されたのでは、という情報をもらいました。権限が破壊され操作不能になったファイルを含む部分の権限を正しく再設定すればよいのは道理です。ただし検証や修復には莫大な時間がかかるでしょう。私は新しいユーザーを作ることで済ませることにしました。
- 投稿日:2019-11-29T09:47:57+09:00
Windows フォーム 雑感
夕べ、優吾ちゃんにWindows FormじゃなくてWindows FormsっていわれてVisualStudio2015で確認したら、こいつのテンブレードは、Windows フォームだった。念のため、2019 revも見たらこっちも日本語はフォームだった
ふーん…
でも、.net FWのネームスペースは、System.Windows.Forms
Docsも含めて、カタカナはフォーム、英語はForms
カタカナ表記は基本は単数形という一時期の文科省の指示に従っているのでしょうここまでは、枕で
WindowsFormsが良くないのがDataSetと組み合わせて使うのが便利なんだけど、これだとデータベースのスキーマとUIがべったりと依存しちゃうんだよねぇ
DIも使いづらい
改めて触ったら、確かに、コリャ駄目だ感の百貨店だぁ
- 投稿日:2019-11-29T06:43:31+09:00
RustでUnityプラグインを作って敗北する
なんか最近流行ってるらしい
浮世の変化には疎いのですが、なんか流行ってるらしいですね
Rust
。
実はUnity
のネイティブプラグインを作ってみたかったのですが、C
もC++
もやったことない上に勉強する気もないため踏み切れないでいました。
いい機会なのでRust
で作ってみます。目的
Rust
を使ってみるUnity
のネイティブプラグインを作ってみる- 自分の学習軌道をメモしておく
書いている人
スマホ開発がメイン
C
とC++
は未経験
値渡しと参照渡しはわかるけどぽいんた? とかいうのはわからん情報取得
とりあえずインプットします。
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#
から渡されたVector3
をRust
側で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先生にデコンパイルしてもらって
Vector3
のToString
を覗いてみます。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 なに言うとるがじゃ!!!!!}$
しょうがないのでこんな感じの拡張メソッド定義してありのままの姿を見せてもらうことにします。
ToStringFloatstatic 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
側へ計算処理を逃がす関数にするのが今回のゴールです。TransWithCsharppublic 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); }これがプロファイラの結果です。
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) } }ちょっとはやくなってる。
仮説
- 1. UnityのMatrix4x4.MultiplyPointはC++層で実行されている
- デコンパイルするってことはコンパイルされてるんだよねこれ
- 2. C#がmatrix4x4をキャッシュしているのに対し、Rustは毎回受け渡しているため非効率
- これは確実にあるはず
- 3. 言語間で受け渡すコスト > Rustによる恩恵
- 単純な計算処理では意味がなかった
仮説1が一番大きいと思います。わたしが戦っていたのはキャストしまくりの
C#
ではなくバチバチにチューニングされたC++
だったのです……たぶん。なので自分で実装した重い処理とかだったら違う結果が出るかもしれません。仮説2の解決として
Vector3[]
の配列を受け渡しできればいいのですが、ポインタがわからないからマーシャリングもわからないので諦めました。Rust
側でポインタを復元する方法もわからないです。仮説3もわりとありそうな気がしています。いちいち変換している分のコストはかなり大きい……はず。
あと、いくら
Vector3
とはいえこの数なら結構なGCを誘発していると思うのですが、プロファイラのGC Alloc
はみんないっしょです。NativePlugin
部分に対するプロファイラの動作もいまいち情報がないのでよくわからん。まとめ
Rust
今更ですが
edition
は2018
です。
Rust
の学習ですが、ヤバいと噂の所有権は自分はそんなにひっかかりませんでした。でもライフタイムは微妙にまだよくわかってないかもしれない。
あと、エラー処理と並列プログラミングはちらっと読んだだけで何言ってるかまったく理解してないので改めて読もうと思います。
Rust
の学習コストは確かに高いですが、コンパイラさんが徹底的にチェックしてくれることで実行時に吹っ飛ぶのを防止してくれるのはとても好きです。NativePlugin
Unity
+C#
+Rust
の知識を要求されるのつらい。敗北
プログラミングぢからは高まった気がしますが、結果が出せていません。
しかし現在の自分ではこれ以上は手が出ない……。ポインタを理解するためにC
を諦めてやってみるべきか……。
なにはともあれ今回はここで敗北します。誰かなんか強い人がなんとかしてくれたら嬉しいな! サヨナラ!とりあえず書いた分は置いておきます。
gist