- 投稿日:2020-09-28T23:12:09+09:00
【Unity】ボタンが押せないのは大体RaycastTargetの設定ミス
UIが反応しない…
上の画像のようにボタンのUIをしっかり設定しているのに、なぜか押せない時ってありますよね?
そんな時は大体RaycastTarget
の設定ミスです。RaycastTargetとは
まず、
Raycast
というのは指定した場所からRay(光線)
を放ち、光線と接触したオブジェクトの情報を取得する機能になります。
銃を撃って敵を攻撃する時に使用したり、本記事のテーマであるUIの情報を取得したりと様々な場面で使うことのできる機能です。では
RaycastTarget
とは何か?
もうお分かりだと思いますが、その名の通りRaycast
のTarget(対象)とするか否かを設定するパラメータになります。
これをtrue
にするとRaycast
のTarget(対象)となるので、情報を取得することができるということです。ボタンが押せない理由
本記事最初の画像のボタンが押せなかった理由は、以下の二つが同時に発生している時になります。
- 「Helloテキスト」がボタンより手前にある
- 「Helloテキスト」の
RaycastTarget
がtrue
手前かつ
RaycastTarget
がtrue
になっていると、背後のオブジェクトまでRay
が通らないのでボタンが反応しない現象が起きるのです。
テキストボックスは見かけを整えるために、実際に見えてる範囲以上の大きさを持っている場合があり、ボタンと重なってしまうことが多いので要注意です。
見かけよりでかいテキスト解決策
基本的にボタン以外は
RaycastTarget
をfalse
にすることで解決します。
新しくUIを作成する時は注意してみてください。
それでも反応しない場合はボタンのOnClick
が設定されていないか、シーン内にEventSystemがないかだと思われます。まとめ
Raycast
はRay(光線)
を放ち、光線と接触したオブジェクトの情報を取得する機能- 基本的にボタン以外は
RaycastTarget
をfalse
にしよう- それでも反応しない場合は
OnClick
を設定しているか確認しよう
- 投稿日:2020-09-28T17:41:38+09:00
AzureFunctionを使用して定期的にメッセージ送信するだけのLINEBOTを作る。(C#)
背景
どうしても漏らしてはいけない作業を日次で行う必要があるけどどうすればいいか?と友人から相談を受けていたので何とかできないかと考えていたところ、僕たちの生活に深く根ざしているLINEを利用できないかと思い、BOTを作成するに至りました。また筆者はSEとしてWebアプリの開発に携わる仕事をしていますが、業務でAzure等のクラウドサービスを触ることがなく、常日頃から使うタイミングがないか考えていたところでした。
要件
要件は【毎日21時(営業日等の判定は不要)に「その日のタスクが完了しているか?」を通知する】という非常に簡単な内容でした。またAzureFunctionもAzureBot(Botは今回使用しません)も触ったことがない状態ではありましたが、2時間程度で上手く作成できました。
※本投稿内容は2020年8月に作成したものでその後においてportal画面や仕様の変更があり得ますのでご了承ください。作成手順
0 : 前提
・LINEの個人アカウントを持っていること。
・Azureアカウントを持っていること。(サブスクリプションやリソースグループは既に作られているものとします。)1 : LINEBOTの作成
LINE Developersにアクセスし、個人IDでログインします。
普段使用しているLINEアカウントのIDとパスワードで問題ないです。
名前とメールアドレスを入力して開発者アカウントを作成してください。ログイン後は以下の画面より、以下の画面より新規プロバイダーを作成します。
自分の名前や企業名、組織名を入れろとLINEの公式サイトには記載されています。
ここでは「テスト組織」と入れました。
プロバイダーが作成出来たら、MessageAPIを選んでEnterを押下します。
チャネル名やチャネルのアイコンを自由に設定し、規約に同意するにチェックを入れMessageAPIを作成します。一旦ここまででBOTは作成されましたが、まだ確認したい項目が後続の作業で必要になりますのでそのままにしておきます。
3 : AzureFunctionの作成
次に、Azureポータル画面より定期実行するためAzureFunction(関数アプリ)を作成します。
ここではwindows系のタスクスケジューラーやUNIX系のクーロンみたいなものとして利用します。関数名を適当に決めて入力してください。私はC#で処理を書きたかったこともあり、以下のように設定しました。終わればホスティングの設定に移ってください。
AzureFunction作成時には必ずストレージアカウントの作成とプランの設定が必要になります。
プランについては月額の支払いにも大いに関わってくるので、間違いないように「App Service プラン」の「F1 free」プランに設定します。また定期実行するためにテンプレート「TimerTrigger」を使用したいのでOSは「Windows」を選びました。ここでの設定が終われば監視の設定をして「確認および作成」ボタン「作成」ボタンの順に押下するとAzureFunctionが作成されデプロイが完了します。4 : TimeTriggerテンプレートを作成する
AzureFunctionが作成されると、TimerTriggerテンプレートを使用し定期実行する関数を作ります。
「関数」タブから移動し「+追加」ボタンを押下し「TimerTriggerテンプレート」を利用してください。ここでは実行頻度とタイミングを決めます。あくまで入力形式はNCRONTAB式になります。
各単位の間に半角スペースを入れていつ実行するかを表します。
詳細はマイクロソフト公式のDocをご確認下さい。
この場合は毎日21時ということで最初の秒と分は0を、時間は21時なので21を、日と月と何曜日か等の指定はないので*を入力します。{second} {minute} {hour} {day} {month} {day-of-week}
0 0 21 * * *5 : 各種設定値を定義する。
表題の通りLINEへのアクセスキーをコードから分離する目的や時間の定義を東京に合わせる設定などを入れるため、
設定値を定義していきます。定義は3で作ったAzureFunctionアプリの「設定」-「構成」タブから作成できます。①時間設定
デフォルトでは別の国のタイムゾーンで21時に実行されるようになっていますので以下を追加します。
■名前:「WEBSITE_TIME_ZONE」
■値:「Tokyo Standard Time」②LINEBOTチャンネルアクセストークン
1で作成したBOTを指定してメッセージを送信しますので、LINE DevelopersよりLINEBOTのチャンネルアクセストークンを発行し、追加する必要があります。LINE Developersより1で作成したBOTのMessaging API設定を押下する。
発行された文字列を値に設定し追加する。
■名前:「CHANNEL_ACCESS_TOKEN」
■値:LINE Developersで調べた値になります値の設定が終われば保存ボタンを確実に押します。
6 : C#で送信処理プログラムを作成する
設定値が完了したら、TimeTriggerから実際にコードを書いてどのような処理(どんなメッセージを表示)させるかを決めます。先ほど作成したTimerTrigger1をクリックし、「コードとテスト」タブを押下するとプログラムが入力できるテキストエディタが表示されます。
送信メッセージに日にちや時間を加えたいので以下のようなコードに書き換えます。
AzureFunction.cs#r "Newtonsoft.Json" using System; using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; public static async void Run(TimerInfo myTimer, ILogger log) { // チャンネルアクセストークンを変数へ格納 var channelAccessToken = System.Environment.GetEnvironmentVariable("CHANNEL_ACCESS_TOKEN"); DateTime dt = DateTime.Now; var mm = dt.ToString( "MM" ).TrimStart('0'); var dd = dt.ToString( "dd" ).TrimStart('0'); var HH = dt.ToString( "HH" ).TrimStart('0'); //リプライメッセージ作成 ReplyMessage rm = new ReplyMessage { messages = new List<Message>() { new Message(){ type="text", text=mm + "月"+ dd +"日の"+ HH + "時になりました。本日のタスクは完了していますか?" } } }; // リプライメッセージをシリアライズ string json = JsonConvert.SerializeObject(rm, Formatting.Indented); // Line Messaging APIへのリクエストを作成する var apiUrl = "https://api.line.me/v2/bot/message/broadcast"; var req = new HttpRequestMessage(HttpMethod.Post, apiUrl); // ヘッダーにチャンネルアクセストークンを追加する req.Headers.Add(@"Authorization", @"Bearer {" + channelAccessToken + "}"); // リクエストをJSON形式にシリアライズする req.Content = new System.Net.Http.StringContent(json, System.Text.Encoding.UTF8, "application/json"); // リクエストを送信する using (var client = new HttpClient()) { var response = await client.SendAsync(req); log.LogInformation($"{response}"); } } public class ReplyMessage { public List<Message> messages { get; set; } } public class Message { public string type { get; set; } public string text { get; set; } }7 : 実際にデバッグ実行してみる。
実際に自分のLINEアカウントにメッセージが送信されるかテストします。
LINE Developersから作成したLINEBOTのQRコードを読み込んで友達になっておきましょう。先ほどの「コードとテスト」の画面より「テストと実行」を押下し、更に表示されたウィンドウの「実行」ボタンを押します。「202 Accepted」が返ってきていること及びLINEでメッセージが受信されていることが確認できます。
実行時間が16時台でしたので16時とメッセージが出ており設定値やプログラムに問題ないことの確認も取れました。
一応QRコード置いておきます。
以上で簡単な通知BOT作成手順でした。ご指摘やアドバイスございましたら遠慮なく記載ください。
- 投稿日:2020-09-28T16:33:18+09:00
【Unity】Texture2Dを毎フレーム更新する処理を書いたらメモリリークした
概要
ある GameObject の Texture を毎フレーム更新して動画のように描画するコードを書いていたところ、ネイティブ環境で異常にクラッシュする事象が発生した。その時に調査した手法と原因をまとめておく。
調査手法
下記のようなエラーを事前に吐いていたので、メモリ関連であるということは分かっていた。 (WebGL)
Uncaught RangeError : Maximum Call Stack Size Exceeded次に Profiler からメモリ使用量を確認すると、Unity メモリが 3GB を超えていることを確認。(上昇ペースも1秒に数十MBだった)
更に、メモリビューの Detailed から何がメモリを食っているのか確認すると、画像のように Texture2D が毎フレームごとに 2.3MB メモリ確保していることが判明した。
Texture2D をこんなに生成している箇所は 1 箇所しか心当たりが無かったので、該当コードを確認することにした。
原因
該当コードは下記の通り。
// frameはtexture情報をバイナリで保持している独自クラス var texture2D = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false); texture2D.wrapMode = TextureWrapMode.Clamp; texture2D.LoadRawTextureData(frame.Buffer); texture2D.Apply(); material.mainTexture = texture2D;ここで問題になっているのは
new Texture2D()
の部分で、今回確保されたメモリは今フレームではmaterial.mainTexture
が参照するが、material.mainTexture
は次フレームでは別の参照を保持している。そのため、前フレームに確保したTexture2D
のメモリ領域はどこからも参照されなくなり、Unity メモリなので GC も行われずそのままリークするといった事が原因であった。解決策
前フレームで使用していた
Texture2D
のメモリを手動で解放する事で解決。
Unity で利用するアセットのメモリ解放はMonoBehaviour.Destroy()
で出来る。
実際の修正済みコードは下記の通り。var texture2D = new Texture2D(frame.Width, frame.Height, TextureFormat.RGBA32, false); texture2D.wrapMode = TextureWrapMode.Clamp; texture2D.LoadRawTextureData(frame.Buffer); texture2D.Apply(); // これを追加した Destroy(material.mainTexture); material.mainTexture = texture2D;これで無事にメモリリークは解消され、ネイティブ環境でアプリがクラッシュすることも無くなった。
まとめ
「アプリが重い、メモリ関係でクラッシュする」といった場合はまず「Profiler」を確認すること。
闇雲にあたりを付けて修正してもコストが掛かるだけで改善するとは限らない。Profiler を使った最適化は 「【Unite 2017 Tokyo】最適化をする前に覚えておきたい技術」の動画を見たメモ にまとめてあるので、全く縁が無かった方は読んでおくと良いと思われる。
※ 調査方法や解決方法で更に効率的な方法などがあればコメントを頂けますと幸いです。
- 投稿日:2020-09-28T07:48:48+09:00
BlockingCollection の使い方
コードリーディングしている時に
BlockingCollection
というものが出てきてよくわからなかったので、調べてみました。主にBlockingCollection in C# - Introduction and Examplesを参考にさせていただいて、自分のコードを書いてみました。Blocking Collection とは
BlockingCollection
はProducer-Consumer パターンの実装です。IPublisherConsumerCollection<T>
というインターフェイスがありますが、そのスレッドセーフの実装です。つまり、コンカレントな状況においてもちゃん動いてくれるものです。デフォルトでは内部的に
ConcurrentQueue
を用いているようですが、コンストラクタでIProducerConsumerCollection<T>
を実装したクラスを渡すことで、そちらのクラスを使用するように変えることもできるようです。インターフェイスは次のものになっています。IProducerConsumerCollection
public interface IProducerConsumerCollection<T> : IEnumerable<T>, IEnumerable, ICollection { void CopyTo(T[] array, int index); T[] ToArray(); bool TryAdd(T item); bool TryTake([MaybeNullWhen(false)] out T item); }コンストラクタ
実際に挙動を確認してみましょう。
BlockingCollection
クラスを単純にインスタンス生成します。boundedCapacity
を渡すことで、このコレクションの最大値を指定することができます。先ほど述べた通り、ここで、ConcurrentQueue
以外の実装を渡すことも可能です。var blockingCollection = new BlockingCollection<string>(boundedCapacity: 3);Producer
コンカレントOKとの事なので、スレッドを生成して、そこで、
Add
を使って要素を足していきます。ここでは、コンソールから入力したものを渡しています。挙動としてのポイントは、上記のコンストラクタで指定した、boundedCapacity
を超えると、Add
メソッドがブロックして、Consumer がアイテムを取得してくれるのを待ちます。Task producerThread = Task.Factory.StartNew(() => { while (true) { string command = Console.ReadLine(); if (command.Contains("quit")) break; blockingCollection.Add(command); // blocked if it reach the capacity } });この待ち受けの挙動が好きではない場合、
TryAdd
メソッドもあります。このメソッドの場合、一定の時間ブロックされたら、「失敗した」とみなして処理をさせることも可能です。CancellationToken
を持つオーバーロードもあります。if (blockingCollection.TryAdd(command, TimeSpan.FromSeconds(1))) { // it works! } else { Console.WriteLine($"It reached boundedCapacity: {capacity} couldn't add {command}"); }Consumer
Take
メソッドにより、1件のアイテムを取得することができます。もし、BlockingCollection
のインスタンスに1件もなかったら、ここでブロックされます。blockingCollection.IsComplete
メソッドで、BlockingCollection
が終了したことの通知を受け取ることができます。NOTE
ちなみにこのサンプルで、
.GetAwaiter().GetResult()
みたいなクソださなことをしているかというと、Task.Factory.StartNew(async () => {}
にすると、async のため labmda の実行がブロックされて、すぐに終了したとみなされて、後に出てくるWaitAll
メソッドでこのスレッドの終了を待ち受ける処理がうまく動作しなくなるからです。正直もっといいやり方がありそう。本番では、async/await を使うので直接スレッドを起動していないので、問題にはなっていませんが、、ダサさを何とかしたい。Task consumerAThread = Task.Factory.StartNew(() => { while (true) { if (blockingCollection.IsCompleted) break; string command = blockingCollection.Take(); Console.WriteLine($"ConsumerA: Take Received: {command}"); Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult(); } });
TryTake
メソッドは、ブロック中に、一定の時間がたつと失敗させてくれるメソッドです。Task consumerBThread = Task.Factory.StartNew(() => { while (true) { if (blockingCollection.IsCompleted) break; string command; if (blockingCollection.TryTake(out command, TimeSpan.FromSeconds(5))) { Console.WriteLine($"ConsumerB: TryTake Received: {command}"); } else { Console.WriteLine($"consumerB: Can't take now."); } Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult(); } });CancellationToken
TryXXX
メソッドは、CancellationToken
をサポートしているので、活用することもできます。CancellationToken
が発行されたら、OperationCanceledException
がスローされます。CancellationTokenSource source = new CancellationTokenSource();Task consumerBThread = Task.Factory.StartNew(() => { while (true) { if (blockingCollection.IsCompleted) break; string command; try { if (blockingCollection.TryTake(out command, (int)TimeSpan.FromSeconds(5).TotalMilliseconds, source.Token)) { Console.WriteLine($"ConsumerB: TryTake Received: {command}"); } else { Console.WriteLine($"consumerB: Can't take now."); } } catch (OperationCanceledException e) { Console.WriteLine($"ConsumerB: Task is cancelled.: {e.Message}"); break; } Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult(); } });プログラムの全体像をシェアしておきます。実行して、何かを入力すると、Producerが
BlockingCollection
にアイテムを代入していきます。Producer
,ConsumerA
,ConsumerB
のブロッキングの振る舞いを観察することができます。cancel
とタイプすると、CancellationToken
が発行されて終了します。もしくは、quit
で終了します。class Program { static void Main(string[] args) { int capacity = 3; // Blocking Collection var blockingCollection = new BlockingCollection<string>(boundedCapacity: capacity); CancellationTokenSource source = new CancellationTokenSource(); Task producerThread = Task.Factory.StartNew(() => { while (true) { string command = Console.ReadLine(); if (command.Contains("quit")) break; if (command.Contains("cancel")) { Console.WriteLine("Cancelling ..."); source.Cancel(); break; } // blockingCollection.Add(command); // blocked if it reach the capacity if (blockingCollection.TryAdd(command, TimeSpan.FromSeconds(1))) { // it works! } else { Console.WriteLine($"It reached boundedCapacity: {capacity} couldn't add {command}"); } } }); Task consumerAThread = Task.Factory.StartNew(() => { while (true) { if (blockingCollection.IsCompleted) break; string command = blockingCollection.Take(); Console.WriteLine($"ConsumerA: Take Received: {command}"); Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult(); } }); Task consumerBThread = Task.Factory.StartNew(() => { while (true) { if (blockingCollection.IsCompleted) break; string command; try { if (blockingCollection.TryTake(out command, (int)TimeSpan.FromSeconds(5).TotalMilliseconds, source.Token)) { Console.WriteLine($"ConsumerB: TryTake Received: {command}"); } else { Console.WriteLine($"consumerB: Can't take now."); } } catch (OperationCanceledException e) { Console.WriteLine($"ConsumerB: Task is cancelled.: {e.Message}"); break; } Task.Delay(TimeSpan.FromSeconds(10)).GetAwaiter().GetResult(); } }); Task.WaitAll(producerThread, consumerAThread, consumerBThread); } }Source
今回のサンプルです。
* Sample