20200928のC#に関する記事は4件です。

【Unity】ボタンが押せないのは大体RaycastTargetの設定ミス

UIが反応しない…

スクリーンショット 2020-09-28 21.52.46.png
上の画像のようにボタンのUIをしっかり設定しているのに、なぜか押せない時ってありますよね?
そんな時は大体RaycastTargetの設定ミスです。

RaycastTargetとは

まず、Raycastというのは指定した場所からRay(光線)を放ち、光線と接触したオブジェクトの情報を取得する機能になります。
銃を撃って敵を攻撃する時に使用したり、本記事のテーマであるUIの情報を取得したりと様々な場面で使うことのできる機能です。

ではRaycastTargetとは何か?
もうお分かりだと思いますが、その名の通りRaycastTarget(対象)とするか否かを設定するパラメータになります。
これをtrueにするとRaycastTarget(対象)となるので、情報を取得することができるということです。

ボタンが押せない理由

本記事最初の画像のボタンが押せなかった理由は、以下の二つが同時に発生している時になります。

  • 「Helloテキスト」がボタンより手前にある
  • 「Helloテキスト」のRaycastTargettrue

手前かつRaycastTargettrueになっていると、背後のオブジェクトまでRayが通らないのでボタンが反応しない現象が起きるのです。
テキストボックスは見かけを整えるために、実際に見えてる範囲以上の大きさを持っている場合があり、ボタンと重なってしまうことが多いので要注意です。
スクリーンショット 2020-09-28 22.17.29.png
見かけよりでかいテキスト

image.png
trueになってしまっているRaycastTarget

解決策

基本的にボタン以外はRaycastTargetfalseにすることで解決します。
新しくUIを作成する時は注意してみてください。
それでも反応しない場合はボタンのOnClickが設定されていないか、シーン内にEventSystemがないかだと思われます。

まとめ

  • RaycastRay(光線)を放ち、光線と接触したオブジェクトの情報を取得する機能
  • 基本的にボタン以外はRaycastTargetfalseにしよう
  • それでも反応しない場合はOnClickを設定しているか確認しよう
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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の公式サイトには記載されています。
ここでは「テスト組織」と入れました。
image.png

プロバイダーが作成出来たら、MessageAPIを選んでEnterを押下します。
image.png

チャネル名やチャネルのアイコンを自由に設定し、規約に同意するにチェックを入れMessageAPIを作成します。一旦ここまででBOTは作成されましたが、まだ確認したい項目が後続の作業で必要になりますのでそのままにしておきます。

3 : AzureFunctionの作成

次に、Azureポータル画面より定期実行するためAzureFunction(関数アプリ)を作成します。
ここではwindows系のタスクスケジューラーやUNIX系のクーロンみたいなものとして利用します。

image.png

関数名を適当に決めて入力してください。私はC#で処理を書きたかったこともあり、以下のように設定しました。終わればホスティングの設定に移ってください。
image.png

AzureFunction作成時には必ずストレージアカウントの作成とプランの設定が必要になります。
プランについては月額の支払いにも大いに関わってくるので、間違いないように「App Service プラン」の「F1 free」プランに設定します。また定期実行するためにテンプレート「TimerTrigger」を使用したいのでOSは「Windows」を選びました。ここでの設定が終われば監視の設定をして「確認および作成」ボタン「作成」ボタンの順に押下するとAzureFunctionが作成されデプロイが完了します。

image.png

4 : TimeTriggerテンプレートを作成する

AzureFunctionが作成されると、TimerTriggerテンプレートを使用し定期実行する関数を作ります。
「関数」タブから移動し「+追加」ボタンを押下し「TimerTriggerテンプレート」を利用してください。

image.png

ここでは実行頻度とタイミングを決めます。あくまで入力形式はNCRONTAB式になります。
各単位の間に半角スペースを入れていつ実行するかを表します。
詳細はマイクロソフト公式のDocをご確認下さい。
この場合は毎日21時ということで最初の秒と分は0を、時間は21時なので21を、日と月と何曜日か等の指定はないので*を入力します。

{second} {minute} {hour} {day} {month} {day-of-week}
0 0 21 * * *

image.png

5 : 各種設定値を定義する。

表題の通りLINEへのアクセスキーをコードから分離する目的や時間の定義を東京に合わせる設定などを入れるため、
設定値を定義していきます。定義は3で作ったAzureFunctionアプリの「設定」-「構成」タブから作成できます。

image.png

①時間設定
デフォルトでは別の国のタイムゾーンで21時に実行されるようになっていますので以下を追加します。
■名前:「WEBSITE_TIME_ZONE」
■値:「Tokyo Standard Time」

②LINEBOTチャンネルアクセストークン
1で作成したBOTを指定してメッセージを送信しますので、LINE DevelopersよりLINEBOTのチャンネルアクセストークンを発行し、追加する必要があります。

LINE Developersより1で作成したBOTのMessaging API設定を押下する。
image.png

一番右下にある「発行」ボタンを押下する。
image.png

発行された文字列を値に設定し追加する。
■名前:「CHANNEL_ACCESS_TOKEN」
■値:LINE Developersで調べた値になります

値の設定が終われば保存ボタンを確実に押します。

6 : C#で送信処理プログラムを作成する

設定値が完了したら、TimeTriggerから実際にコードを書いてどのような処理(どんなメッセージを表示)させるかを決めます。先ほど作成したTimerTrigger1をクリックし、「コードとテスト」タブを押下するとプログラムが入力できるテキストエディタが表示されます。

image.png

送信メッセージに日にちや時間を加えたいので以下のようなコードに書き換えます。

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コードを読み込んで友達になっておきましょう。

IMG_3537.PNG

先ほどの「コードとテスト」の画面より「テストと実行」を押下し、更に表示されたウィンドウの「実行」ボタンを押します。「202 Accepted」が返ってきていること及びLINEでメッセージが受信されていることが確認できます。
実行時間が16時台でしたので16時とメッセージが出ており設定値やプログラムに問題ないことの確認も取れました。
image.png

IMG_3538.PNG

一応QRコード置いておきます。

image.png

以上で簡単な通知BOT作成手順でした。ご指摘やアドバイスございましたら遠慮なく記載ください。

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

【Unity】Texture2Dを毎フレーム更新する処理を書いたらメモリリークした

概要

ある GameObject の Texture を毎フレーム更新して動画のように描画するコードを書いていたところ、ネイティブ環境で異常にクラッシュする事象が発生した。その時に調査した手法と原因をまとめておく。

調査手法

下記のようなエラーを事前に吐いていたので、メモリ関連であるということは分かっていた。 (WebGL)

Uncaught RangeError : Maximum Call Stack Size Exceeded

次に Profiler からメモリ使用量を確認すると、Unity メモリが 3GB を超えていることを確認。(上昇ペースも1秒に数十MBだった)

image.png

更に、メモリビューの Detailed から何がメモリを食っているのか確認すると、画像のように Texture2D が毎フレームごとに 2.3MB メモリ確保していることが判明した。

image.png

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】最適化をする前に覚えておきたい技術」の動画を見たメモ にまとめてあるので、全く縁が無かった方は読んでおくと良いと思われる。

※ 調査方法や解決方法で更に効率的な方法などがあればコメントを頂けますと幸いです。

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

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

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