20200921のC#に関する記事は5件です。

ContextからStartActivityする際の注意

以前KudanARライブラリを使用したKudanARアプリをリリースしました。
Xamarin.FormsでKudan ARを試してみる

そのアプリのテスト環境として使用していたAndroid8.0では発生しなかったが、Android9.0だと発生するエラーがありました。
少し調査したので軽くまとめたいと思います。

先に結論

Contextの実体にはApplication ContextActivity Contextがあります。
実体を把握した上で使用しよう。

現象

まずは、下記が確認できたエラーメッセージの内容です。

Android.Util.AndroidRuntimeException: 'Calling startActivity() from outside of an Activity  context requires the FLAG_ACTIVITY_NEW_TASK flag. Is this really what you want?'

どうやら下記のStartActivity実行時にエラーが発生していることがわかりました。

// Androidバージョンによってはエラーが発生する書き方
var context = Android.App.Application.Context;
using (var intent = new Android.Content.Intent(context, typeof(MarkerARActivity)))
{
    context.StartActivity(intent);
}

原因

Contextと一言に言っても、実体が異なる場合があることを把握しないまま使用したことが原因です。

なぜアプリリリース前に気づけなかった?

KudanARライブラリを使用すると、エミュレータで動作確認ができなくなる仕様があります。
また、私が持っている一番新しいAndroidバージョンは8.0です。
よって、他のAndroidバージョンでの挙動を確認できなかったため、リリース後に気づいた次第です。
言い訳終了。

エラー対処方法

Xamarinのテンプレートソリューションから、空のActivityを呼び出すだけのアプリを作成し、動作を確認しました。
対処方法にはいくつかあるようです。

  • context = Android.App.Application.Context;を使う場合
    • Contextの実体はApplication
    • Intentにフラグを追加して対応
      • intent.SetFlags(ActivityFlags.NewTask);
  • context = (Context)Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity;を使う場合
    • Contextの実体はActivity
    • NuGetからPlugin.CurrentActivityをインストールする必要あり
    • Intentへのフラグ追加は必要なし
  • context = MainActivity.Context;を使う場合
    • Contextの実体はActivity
    • public static Context Context { get; private set; }のような適当な静的プロパティをMainActivityに用意する必要あり(OnCreateでthisの代入を行っておく)
    • Intentへのフラグ追加は必要なし

下記の記事が参考になりました。
Androidの勉強:Contextについて

最終的な実装

フラグ設定処理を追加して行数を増やすのは避けたかったので、対処方法の2番目に挙げた方法で実装しました。

// 正常に動作する書き方
var context = (Context)Plugin.CurrentActivity.CrossCurrentActivity.Current.Activity;
using (var intent = new Android.Content.Intent(context, typeof(MarkerARActivity)))
{
    context.StartActivity(intent);
}

まとめ

Contextの実体にはApplication ContextActivity Contextがあるので、実体を把握した上で使用しよう。
Contextの取得には、Plugin.CurrentActivityを使用するのが安定。

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

【Unity(C#)】ファイルの保存上限を設けて日付の古いファイルから順に消去する実装

はじめに

【参考リンク】:【Unity(C#)】テクスチャを画像データに変換して端末に保存、読み込み
↑前回の応用です。

やりたい事としては下記の画像のように
ファイルの保存上限を設けて、
上限を超えた場合は最も古いファイルから削除するという実装です。
SaveLimitImage.png

デモ

早速ですがデモです。
保存するファイルは画像ファイルとしました。
Paint2DForQiita5.gif

PC内の保存領域を確認したところ、
しっかりと最後の3枚が保存されていました。

SaveImageCheckSS.PNG

コード

下記今回の処理の全文です。

適当なオブジェクトにアタッチ
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UniRx;
using UniRx.Triggers;
using UnityEngine;
using UnityEngine.UI;

/// <summary>
/// テクスチャー ⇔ Png画像 の変換と保存と読み込み
/// </summary>
public class TexturePngConverter : MonoBehaviour
{
    [SerializeField] private Button _saveButton;
    [SerializeField] private Button _loadButton;
    [SerializeField] private Image _paintImage;
    [SerializeField] private Painter _painter;
    [SerializeField] private Transform _loadImagesParentTransform;

    private const string IMAGE_SAVE_FOLDER = "Image";
    private const string PNG = ".png";

    /// <summary>
    /// 限界保存枚数
    /// </summary>
    private const int UPPER_LIMIT_SAVE_PICTURE = 3;

    private void Start()
    {
        //セーブボタン
        _saveButton.OnPointerClickAsObservable()
            .Subscribe(_ =>
            {
                //保存
                ConvertToPngAndSave(GetSaveDirectoryPath(IMAGE_SAVE_FOLDER), GetSaveFilePath(IMAGE_SAVE_FOLDER, PNG));
                //リセット
                _painter.ResetTexture();
            }).AddTo(this);

        //ロードボタン
        _loadButton.OnPointerClickAsObservable()
            .Subscribe(_ => ConvertToTextureAndLoad(GetSaveDirectoryPath(IMAGE_SAVE_FOLDER))).AddTo(this);
    }

    /// <summary>
    /// 保存先のファイルのパス取得
    /// </summary>
    /// <param name="folderName">区切りのフォルダ名</param>
    /// <param name="fileType">拡張子</param>
    /// <returns>保存先のパス</returns>
    private string GetSaveFilePath(string folderName, string fileType)
    {
        return GetSaveDirectoryPath(folderName) + DateTime.Now.ToString("yyyyMMddHHmmss") + fileType;
    }

    /// 保存先のディレクトリのパス取得
    /// </summary>
    /// <param name="folderName">区切りのフォルダ名</param>
    /// <returns>保存先のパス</returns>
    private string GetSaveDirectoryPath(string folderName)
    {
        string directoryPath = Application.persistentDataPath + "/" + folderName + "/";

        if (!Directory.Exists(directoryPath))
        {
            //まだ存在してなかったら作成
            Directory.CreateDirectory(directoryPath);
            return directoryPath;
        }

        return directoryPath;
    }

    /// <summary>
    /// "ディレクトリ配下のファイル"が全て入ったリストを返す
    /// 最も古いファイルが[0]番目
    /// </summary>
    /// <param name="directoryName">取得したいファイル群の親ディレクトリ</param>
    /// <returns>指定したディレクトリ配下のファイルが全て入ったリスト</returns>
    private List<string> GetAllFileFromDirectory(string directoryName)
    {
        //古いものが先頭にくるようにファイルをソート
        List<string> imageFilePathList = Directory
            //Imageディレクトリ内の全ファイルを取得
            .GetFiles(directoryName, "*", SearchOption.AllDirectories)
            //.DS_Storeは除く
            .Where(filePath => Path.GetFileName(filePath) != ".DS_Store")
            //日付順に降順でソート
            .OrderBy(filePath => File.GetLastWriteTime(filePath).Date)
            //同じ日付内で時刻順に降順でソート
            .ThenBy(filePath => File.GetLastWriteTime(filePath).TimeOfDay)
            .ToList();

        return imageFilePathList;
    }

    /// <summary>
    /// 画像に変換&保存
    /// 上限保存数をチェック
    /// </summary>
    /// <param name="directoryPath">保存数をチェックするディレクトリ</param>
    /// <param name="fileSavePath">保存先のパス</param>
    private void ConvertToPngAndSave(string directoryPath, string fileSavePath)
    {
        //指定したディレクトリー配下のファイルが全て入ったリストを取得
        List<string> imageFilePaths = GetAllFileFromDirectory(directoryPath);

        //ファイル数の上限をチェック
        if (imageFilePaths.Count >= UPPER_LIMIT_SAVE_PICTURE)
        {
            //上限に達していた場合、最も古いファイルを削除
            File.Delete(imageFilePaths[0]);
        }

        //Pngに変換
        byte[] bytes = _paintImage.sprite.texture.EncodeToPNG();
        //保存
        File.WriteAllBytes(fileSavePath, bytes);
    }

    /// <summary>
    /// テクスチャに変換&読み込み
    /// </summary>
    /// <param name="directoryPath">ロードしたいファイル群の親ディレクトリ</param>
    private void ConvertToTextureAndLoad(string directoryPath)
    {
        List<Image> imageList = new List<Image>();

        //ロード後、複数枚表示するためのImageリスト作成
        foreach (Transform child in _loadImagesParentTransform)
        {
            Image childImage = child.gameObject.GetComponent<Image>();

            if (childImage != null)
            {
                imageList.Add(childImage);
            }
        }

        //指定したディレクトリー配下のファイルが全て入ったリストを取得
        List<string> imageFilePaths = GetAllFileFromDirectory(directoryPath);

        //インデックス用カウンター
        int count = 0;

        //ファイルのリストから古い順にロードしてImageに適用
        foreach (string imageFilePath in imageFilePaths)
        {
            Debug.Log(imageFilePath);
            //読み込み
            byte[] bytes = File.ReadAllBytes(imageFilePath);
            //画像をテクスチャに変換
            Texture2D loadTexture = new Texture2D(2, 2);
            loadTexture.LoadImage(bytes);
            //テクスチャをスプライトに変換
            imageList[count].sprite = Sprite.Create(loadTexture, new Rect(0, 0, loadTexture.width, loadTexture.height), Vector2.zero);
            //インデックス用カウンターを進める
            count++;
        }
    }
}

ソート

今回は古いフォルダの概念を保存した日時(秒単位)にしています。
そのため、ディレクトリ内のファイルの中から
最も古い保存日時のファイルを取得する必要がありました。

そのために一度全ファイルを取得して
それぞれの保存先のパスを保存順にソートしています。

ロード時も同様に全ファイルのパスを取得し
File.ReadAllBytesの引数に渡しています。

下記リンクを参考にLINQを使って簡単に書くことができました。

【参考リンク】:タイムスタンプ(作成日、変更日、最後に開いた日)を基準にファイルを古い順や新しい順にソート【C#】【LINQ】

DateTime.Now

DateTime.Nowを使えば現在の日付と秒単位までの時刻を取得することができます。

今回の実装においては、
この日付と秒単位までの時刻をファイル名とすることで
ユニークなパスを生成しています。

ただ、そのままファイル名として与えた際に少し厄介なことがありました。

Image/2020/09/21 17:12:43.pngのようにディレクトリの区切りとして解釈される
/(スラッシュ)が含まれてしまいます。

このまま保存処理を行おうとすると
そんなディレクトリは存在しませんと怒られます。

ですので、下記のように引数に"yyyyMMddHHmmss"を渡しています。

DateTime.Now.ToString("yyyyMMddHHmmss")

こうすることでImage/20200921171710.pngとなり
秒数まで全て数字で取得することが可能となります。

【参考リンク】:日付や時刻を文字列に変換するには?


2020/09/21 追記

GetFiles(directoryName, "*", SearchOption.AllDirectories)
の箇所を"*""*.png"等にした方が
想定外のファイルを除外することができるとコメント頂いたので
メモしときます (ありがとうございます!)

おわりに

モバイル端末で一気に5,60枚読み込んでも処理落ちは見られませんでした。
枚数が何百、何千となるといろいろと工夫が必要そうです。

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

C#でUDPブロードキャストパケット受信

参考記事

https://garafu.blogspot.com/2015/08/broadcast.html

実装

UDPブロードキャストパケットを受信してエコーするタスクを常駐させるサンプルです。

using System.Net;
using System.Net.Sockets;

namespace udp
{
    public class udpSample {

        public void start()
        {
            Task.Run(() => BroadcastReceiver()); // UDPブロードキャストパケット受信タスク起動
        }

        private void BroadcastReceiver()
        {
            var endPoint = new IPEndPoint(IPAddress.Any, 4000); // すべてのアドレス監視するためIPAddress.Anyを指定

            using (UdpClient udpClient = new UdpClient(endPoint)) {
                while(true) {
                    var buff = udpClient.Receive(ref endPoint);  // 受信(同期)
                    udpClient.Send(buff, buff.Length, endPoint); // 受信内容をエコー
                }
            }
        }
    }
}

IPEndPointにusing System.Net;が、
UdpClientにusing System.Net.Sockets;が必要になります。

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

C#のLambdaプロジェクトを作成してVisual Studio for Macでデバッグ実行する

はじめに

MacでC#のLambdaプロジェクトを作成し、Visual Studio for Macでテストプロジェクトをデバッグ実行した記録です。

できたこと

  • C#のLambdaプロジェクトを作成
  • AWS Lambdaへのデプロイ
  • AWS Lambdaでのテスト
  • Visual Studio for Macでソリューション化
  • テストプロジェクトのデバッグ実行

AWS Lambda上で動くプログラムをアタッチしてデバッグしたわけではありません。
テストプロジェクトをローカルPC上でデバッグ実行しました。

準備

アクセスキー ID とシークレットアクセスキーを設定

PCからAWSにアクセスできるようにアクセスキー ID とシークレットアクセスキーを設定します。

.NET Core CLI

  • プロジェクトテンプレートの追加
    dotnet new -i Amazon.Lambda.Templatesコマンドを実行して、.NET Core CLIにAWS Lambdaのテンプレートを追加します。

  • Amazon.Lambda.Tools .NET Core Global Tool のインストール
    dotnet tool install -g Amazon.Lambda.Toolsコマンドを実行します。

参考:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/csharp-package-cli.html

プロジェクトの作成

空のLambdaプロジェクト作成するコマンドを実行します。
dotnet new lambda.EmptyFunction --name {メソッド名}
(以下、{メソッド名}に「SelectFunction」と指定した想定で説明します。)

色々とファイルが生成されますが、Lambda関数を実行するとFunction.csのFunction.FunctionHandler()メソッドが呼び出されることになります。
デフォルトでは、引数inputで受け取った文字列を大文字に変換して戻り値に返す処理となっています。

Function.cs
using Amazon.Lambda.Core;

// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class.
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]

namespace SelectFunction
{
    public class Function
    {
        /// <summary>
        /// A simple function that takes a string and does a ToUpper
        /// </summary>
        /// <param name="input"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public string FunctionHandler(string input, ILambdaContext context)
        {
            return input?.ToUpper();
        }
    }
}

参考:https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/csharp-package-cli.html

デプロイ

デプロイしてみます。

  • .csprojファイルがあるディレクトリに移動します。
  • dotnet restoreコマンドを実行します。
  • dotnet lambda deploy-functionを実行します。

    • Enter AWS Region:と表示されるので、デプロイ先のリージョンを入力。
    • Enter Function Name:と表示されるので、作成したいLambda関数名を入力。
    • Select IAM Role that to provide AWS credentials to your code:とIAMロールの一覧が表示されるので、使用するIAMロールを選択。

デプロイに成功すると、AWS Lambdaのコンソールに作成したLambda関数が表示されます。
スクリーンショット 2020-09-21 8.49.12.png

AWS Lambda上でテスト

AWS LambdaコンソールでデプロイしたLambda関数を選択し、「テストイベントの設定」をクリックします。
スクリーンショット 2020-09-21 8.49.59.png

「イベント名」欄に任意の名称を入力し、その下の編集エリアに引数に渡す情報をJsonで入力して保存ボタンをクリックします。
スクリーンショット 2020-09-21 8.50.49.png

先ほど「イベント名」欄に入力した名前を選択してテストボタンをクリックすると、実行結果欄にFunction.csのFunction.FunctionHandler()メソッドの戻り値が表示されます。
スクリーンショット 2020-09-21 8.51.39.png

Visual Studio for Macでソリューション化

Visual Studio for Macでコーディングやデバッグ実行をしたいのでソリューションを作成します。
まずは、空のソリューションを作成します。

スクリーンショット 2020-09-21 8.17.17.png

プロジェクトの作成で作成されたディレクトリ構成に合わせてソリューションの場所を指定します。ディレクトリ構成はお好きにどうぞ。
スクリーンショット 2020-09-21 8.23.37.png

ソリューションウィンドウでソリューション名を右クリック→[追加]→[既存のプロジェクト]を選択し、Lambdaプロジェクトとそのテストプロジェクトを追加します。
スクリーンショット 2020-09-21 9.03.10.png

テストの実行

テストプロジェクトをスタートアッププロジェクトに設定します。

単体テストウィンドウを表示し、テストメソッドを右クリック→[テストのデバッグ]をクリックすると、テストメソッドをデバッグ実行できます。

スクリーンショット 2020-09-21 9.07.48.png

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

【Unity(C#)】Lerpで滑らかな線を描く

はじめに

平面上に線を滑らかに描く方法を調べました。

VR空間でもぬるぬる動いている例を参考にしました(↓すごい)

【参考リンク】:ホワイトボードで線がヌルヌル描ける仕組み

描く実装

"描く部分"のコードは下記リンクからまるまる拝借しました。

【参考リンク】:Unityでテクスチャにお絵描きしよう

デモ

まずは補間することなくそのまま動かしてみたサンプルです。
Paint2DForQiita1.gif

線がかすれてしまって不格好です。


次にLerpによる補間を施したものです。
Paint2DForQiita2.gif

しっかりと線の間を補間してくれました。

コード

下記全文です。

using UnityEngine;

//使い方↓
//https://nn-hokuson.hatenablog.com/entry/2016/12/08/200133
public class SmoothPaint : MonoBehaviour
{
    Texture2D drawTexture;
    Color[] buffer;

    private Vector2 _prevPosition;

    void Start()
    {
        Texture2D mainTexture = (Texture2D) GetComponent<Renderer>().material.mainTexture;
        Color[] pixels = mainTexture.GetPixels();

        buffer = new Color[pixels.Length];
        pixels.CopyTo(buffer, 0);

        drawTexture = new Texture2D(mainTexture.width, mainTexture.height, TextureFormat.RGBA32, false);
        drawTexture.filterMode = FilterMode.Point;
    }

    public void Draw(Vector2 p)
    {
        for (int x = 0; x < 256; x++)
        {
            for (int y = 0; y < 256; y++)
            {
                if ((p - new Vector2(x, y)).magnitude < 5)
                {
                    buffer.SetValue(Color.black, x + 256 * y);
                }
            }
        }
    }

    void Update()
    {
        if (Input.GetMouseButton(0))
        {
            //前回値がまだないなら現在の値を前回値として扱う
            if (_prevPosition == Vector2.zero)
            {
                _prevPosition = Input.mousePosition;
            }

            //線形補間に使う入力の終点座標
            Vector2 endPosition = Input.mousePosition;
            //1フレームの線の距離
            float lineLength = Vector2.Distance(_prevPosition, endPosition);
            //線の長さに応じて変わる補間値 CeilToIntは小数点以下を切り上げ
            int lerpCountAdjustNum = 5;
            int lerpCount = Mathf.CeilToInt(lineLength / lerpCountAdjustNum);

            for (int i = 1; i <= lerpCount; i++)
            {
                //Lerpの割合値を "現在の回数/合計回数" で出す
                float lerpWeight = (float) i / lerpCount;

                //前回の入力座標、現在の入力座標、割合を渡して補間する座標を算出
                Vector3 lerpPosition = Vector2.Lerp(_prevPosition, Input.mousePosition, lerpWeight);

                Ray ray = Camera.main.ScreenPointToRay(lerpPosition);
                RaycastHit hit;
                if (Physics.Raycast(ray, out hit, 100.0f))
                {
                    Draw(hit.textureCoord * 256);
                }

                drawTexture.SetPixels(buffer);
                drawTexture.Apply();
                GetComponent<Renderer>().material.mainTexture = drawTexture;
            }

            //前回の入力座標を記録
            _prevPosition = Input.mousePosition;
        }
        else
        {
            //前回の入力座標をリセット
            _prevPosition = Vector2.zero;
        }
    }
}

ロジックとしては下記です
①前フレームのマウスの入力座標を保持
②次フレームにて①と現在の入力座標の距離を算出
③距離に応じて補間値(補間回数)を算出
④補間値を利用しLerpで補間

線が繋がる

実装していてそこそこ時間を使ってしまったのが、
線が繋がってしまう という現象でした。

下記GIFのように新しく描き始めた箇所と
前回の終了地点が繋がってしまっていました。

Paint2DForQiita3.gif

これは描き終えた際の値を前回値として保持したまま
新しい入力箇所での補間を行っていることが原因でした。

ですので、下記箇所で入力が無い状態になった際に前回値のリセットを行っています。
さらに、入力値のリセットにより前回値が存在しないフレーム、
すなわち描き始めのフレームにおいては補間を行う必要が無いので
前回の値=現在の値として扱うようにしています。

    if(Input.GetMouseButton(0))
    { 
        //前回値がまだないなら現在の値を前回値として扱う
        if (_prevPosition == Vector2.zero)
        {
            _prevPosition = Input.mousePosition;
        }
    }
    else
    {
        //前回の入力座標をリセット
        _prevPosition = Vector2.zero;
    }

これにより描き始めた箇所に線を新しく描画することができました。
Paint2DForQiita4.gif

おわりに

線の太さ(大きさ)も考慮した補間値を算出すれば
もっと最適な補間ができるみたいです。

そのうち書けたら書きます。

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