20200705のC#に関する記事は8件です。

LINQ とっかかりと自分が書く時に考えていることと注意点

LINQ っていいですよね。2007 年に登場してるので、13 年前の技術です。枯れに枯れてる技術です。

LINQ to XXXX という感じで色々なものに LINQ 出来たりしますが、今回は LINQ to Objects についてのみ書きます。配列やリストに対してやる LINQ のことです。

今回のコードは全て Try .NET にコピペして試せる感じのコードを書くつもりです。

ということで徒然なるままに書いて行きます!

LINQ の前に

LINQ に入る前に以下のコードを見てください。

var array = new[] { 1, 2, 3, 4, 5 };
foreach (var x in array)
{
    if (x % 2 == 0)
    {
        Console.WriteLine(x);
    }
}

説明するまでもないとは思いますが 1 〜 5 までの数字の入った配列から偶数だけ抜き出してから表示しています

これはデータを加工するという処理(偶数のみ抜き出す)と、表示するという処理が 1 つのループの中に入っています。混ざってる。

今回の例では上のようなコードで十分で、これから書くコードは過剰な分離にはなるのですが、理想的には以下のように処理と表示はわけたほうがいいです。

var array = new[] { 1, 2, 3, 4, 5 };

// 偶数のみにフィルタリング
var evenNumbers = new List<int>();
foreach (var x in array)
{
    if (x % 2 == 0)
    {
        evenNumbers.Add(x);
    }
}

// 表示
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

何度も言いますが最初のコードと比べると性能面でも悪い(2回ループしてる)し、今回のようなシンプルな例で処理と表示を分離したおかげでコード量も増えてしまいました。

でも、実際のそこそこの大きさのコードを書くときは、上記のようになるべくデータに対する処理と、その処理結果をどのように表示するのかといった部分は分離するのがいいです。

この点は、頭に入れておいてください。

LINQ とは

配列やリストに対してフィルターや射影(変換)や集計やグルーピングなどを簡単に行えるようにしてくれる便利なメソッドの集まりです。

これを覚えるだけで、プログラムを書く上でかなりの部分を占める配列やリストに対して何かするという処理が非常にシンプルに書けるようになります。

例えばフィルタリングは Where というメソッドを使って書くことができます。

var array = new[] { 1, 2, 3, 4, 5 };

// 偶数のみにフィルタリング
var evenNumbers = array.Where(x => x % 2 == 0);

// 表示
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

Where が配列やリストから条件に合致するデータだけにフィルタリングしてくれるメソッドになります。Where を知っている人から見ると

// foreach
var evenNumbers = new List<int>();
foreach (var x in array)
{
    if (x % 2 == 0)
    {
        evenNumbers.Add(x);
    }
}

// Where
var evenNumbers = array.Where(x => x % 2 == 0);

上と下のどっちが読みやすいか?というのは個人的に重要なところだと思ってます。
LINQ のメソッド数は決して少なくはないけどよく使うものは、そんなに多くありません。ぶっちゃけ WhereSelect を覚えてるだけでも、かなりイケます。

ということで LINQ とは「配列やリストに対して皆が知ってる共通言語で簡単に、わかりやすくデータの加工処理などを行えるようになるもの」ということになります。個人的には LINQ で書いた方がシンプルそうなものを、あえて foreach などで書いてると「ここは LINQ が使えないような理由があるのか??何か元になるデータが特殊なのか?どういう意図があるんだろう?」と深読みをしてしまいます。

みんな LINQ を覚えて、素直な LINQ はバンバン書こう!!というか書いてください、お願いします。

フィルタリングと射影(変換)

さて、先ほど Where については説明しました。データのフィルタリングに使います。
次に射影です。射影ってなんでこんな難しい言い方が使われるのかわかりませんが変換と思ってもらえればいいです。変換には Select というメソッドを使います。

例えば先ほどの偶数のみにフィルタリングした結果に対して 2 乗するといったような変換処理を追加したい場合は Where の結果に対して Select を呼び出してやります。

var array = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = array
    .Where(x => x % 2 == 0) // 偶数のみにして
    .Select(x => x * x); // 2 乗する

// 表示
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

結果は 4 と 16 が表示されます。
さて、LINQ を使うことで自然とデータのフィルタリングや加工と、それの表示が分離されるような書き方になりますね。素晴らしい。

効率悪いんじゃないの??

さて、この WhereSelect ですが普通に内部の実装をしようと考えると Where メソッドの中で foreach して、さらに Select メソッドの中で foreach して…と言ったようになると思います。
なので WhereSelect を連打するとパフォーマンスが最高に劣化するのではないか?という気持ちになりますが、そこら辺は WhereSelect を呼ぶだけでは実は処理が行われてなくて、こう言ったフィルタリングや変換をやるという予定だけが登録された状態になります。

そして、それらは実際に本当に必要になったタイミングで実行されます。今回の例では foreach ループで表示しているところまで実際の処理が行われません。

それがわかるように以下のようにコードを書き換えてみましょう。

var array = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = array
    .Where(x => 
    {
        Console.WriteLine($"{x} についてフィルタリングしています");
        return x % 2 == 0;
    }) // 偶数のみにして
    .Select(x => 
    {
        Console.WriteLine($"{x} について変換しています");
        return x * x;
    }); // 2 乗する

// 表示
Console.WriteLine("結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

WhereSelect 内部でログを出すようにしました。また結果を表示する foreach ループの前でもログを出すようにしました。順当に行くと結果を表示します!!の前に WhereSelect 内のログが出て欲しいところですが、先ほど説明したように実際に必要になるまで処理が行われないので、以下のような結果になります。

結果を表示します!!
1 についてフィルタリングしています
2 についてフィルタリングしています
2 について変換しています
4
3 についてフィルタリングしています
4 についてフィルタリングしています
4 について変換しています
16

結果を表示します!!の後にフィルタリングと変換処理が行われているのがわかります。しかも foreach の前にフィルタリングと変換が一気に行われるのではなく、データを表示しつつ本当に必要になる直前まで処理が保留されていることがわかります。

なので、LINQ を使うと最初に処理を分離しようぜ!!って言って foreach でフィルタリングした時の処理よりも効率がいいということになりますね。foreach でのフィルタリングは 2 回ループが走ったのに、LINQ を使うと 1 回だけのループになるので。最高

注意すること

さて、LINQ のメソッドは本当に必要になるまで処理が行われないと言いましたが、注意すべき点があります。それは場合によっては、この動作のせいで予期せぬエラーになったりパフォーマンスの問題にぶち当たることがあるという点です。

例えば、複数回 LINQ の結果に対してループを行うと、都度都度全部の処理が走ります。見てみましょう。以下のように 2 回結果を表示するようにコードを書き換えました。

var array = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = array
    .Where(x => 
    {
        Console.WriteLine($"{x} についてフィルタリングしています");
        return x % 2 == 0;
    }) // 偶数のみにして
    .Select(x => 
    {
        Console.WriteLine($"{x} について変換しています");
        return x * x;
    }); // 2 乗する

// 表示
Console.WriteLine("結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

Console.WriteLine("再度結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}
結果を表示します!!
1 についてフィルタリングしています
2 についてフィルタリングしています
2 について変換しています
4
3 についてフィルタリングしています
4 についてフィルタリングしています
4 について変換しています
16
5 についてフィルタリングしています
再度結果を表示します!!
1 についてフィルタリングしています
2 についてフィルタリングしています
2 について変換しています
4
3 についてフィルタリングしています
4 についてフィルタリングしています
4 について変換しています
16
5 についてフィルタリングしています

今回のようなデータ量で、こんな軽い処理ならあまり問題になりませんが、データの量とフィルタとか変換処理が重たい場合は、この処理を遅らせるというのが原因で無駄に何回も処理を行うような問題が起きます。気をつけてね。

解決方法としては、LINQ の結果に対して ToArray などを呼び出して明示的に配列に変換をしてしまい、配列に対して foreach をするとフィルタリングや変換処理は foreach では走らなくなります。

var array = new[] { 1, 2, 3, 4, 5 };

var evenNumbers = array
    .Where(x => 
    {
        Console.WriteLine($"{x} についてフィルタリングしています");
        return x % 2 == 0;
    }) // 偶数のみにして
    .Select(x => 
    {
        Console.WriteLine($"{x} について変換しています");
        return x * x;
    }) // 2 乗する
    .ToArray(); // ここで明示的に配列にすることで LINQ の処理をやってしまう

// 表示
Console.WriteLine("結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

Console.WriteLine("再度結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}
1 についてフィルタリングしています
2 についてフィルタリングしています
2 について変換しています
3 についてフィルタリングしています
4 についてフィルタリングしています
4 について変換しています
5 についてフィルタリングしています
結果を表示します!!
4
16
再度結果を表示します!!
4
16

配列にするところで余分に一回ループが走ってしまいますが LINQ の処理を再度やるのと、配列を再利用するのでどっちが早いかというトレードオフになります。
例えば、10000 件のデータから 3 件だけとってくるような Where があると考えます。これを後で何回かループで処理するような場合は ToArray をして配列にしてしまったほうが、毎回 10000 ループするのではなく 3 回のループになるので効率がいいですよね。
そのほかに、Select での変換処理でちょっと重ための計算とかが入ってる場合なんかも ToArray などで一回処理した結果を保持しておいたほうが、毎回計算処理が走るより早くなったりします。ケースバイケースですが、このようなことがあるということを頭に入れておいてください。きっと LINQ を使っていてパフォーマンスの問題にぶちあたったときに助けになってくれると思います。

あとは LINQ の元になるデータが途中で書き換わった時とかは思いもよらない結果になることもあります。

var list = new List<int>(new[] { 1, 2, 3, 4, 5 });

var evenNumbers = list
    .Where(x => x % 2 == 0) // 偶数のみにして
    .Select(x => x * x); // 2 乗する

// 表示
Console.WriteLine("結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

// 元データかわちゃった
list.Add(100);
list.Add(9999);
list.Add(1000);

Console.WriteLine("再度結果を表示します!!");
foreach (var x in evenNumbers)
{
    Console.WriteLine(x);
}

最初の結果表示と2回目の結果表示の間でリストのデータが変わっています。LINQ は、その都度処理をやるので2回目のループでは結果が最初と変わります。

結果を表示します!!
4
16
再度結果を表示します!!
4
16
10000
1000000

今回のようにわかりやすい時はいいのですが、これがデータの元を管理しているクラスと結果を表示するクラスが離れていると、気づくのに時間がかかるので気をつけましょう。

因みに、LINQ のメソッド全てがこのような動きをするわけではなく、処理を遅らせることが可能なものだけがこのような動きをします。例えば Aggregate という集計処理を行う関数はその場で実行されます。集計結果を返すには全部舐めないといけないですからね。

まとめ

この記事で言いたかったこと。

  1. データに対する加工などの処理と表示は分離して書こう。ごちゃごちゃして大変になるので
  2. データに対する加工などの処理は LINQ で書こう。多くの場合スッキリ書けるし、LINQ を知ってる人の中ではループでやるよりわかりやすくなります。
  3. LINQ で書いた処理が実行されるのは本当に実行しないといけなくなったタイミングで行われる。一般的には性能面で有利になるんだけど、場合によっては性能劣化やわかりにくいバグに繋がることもあるので気をつけよう。

LINQ には沢山のメソッドがあります。これらは覚えるしかないので一度くらいメソッドのリストを眺めたりドキュメントをさらっとみておくことをお勧めします。

追記:クエリ構文について

LINQ には、ここまで説明してきたメソッド呼び出しの形式で書けるメソッド構文の他に以下のように SQL 文っぽく書けるクエリ構文があります。

var evenNumbers = from x in array where x % 2 == 0 select x;

これについては、存在について知ってるだけでいいと思います。
クエリ構文では使えない LINQ のメソッドもあるので、それなら最初からメソッド構文で書いたほうがいいなぁと個人的には思ってます。(個人の感想)

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

VsCode + C# + MySqlConnector から mariadb10.5 にアクセスしてみる

目的

・VsCode + C# + MySqlConnector から mariadb10.5 にアクセスしてみる
・Windows 10 と Ubuntu 18.04 でコードをコピペして確認

MySqlConnectorを追加する

※事前にvscode-nuget-package-managerが追加されてることを確認
表示メニュー
 -> コマンドパレット
  -> nuget
   -> Nuget package Manager:Add Pckage
    -> MySqlConnector を入力後 enter
     -> MySqlConnector を選択
      -> 1.0.0-beta.5 を選択

サンプルコード

using System;
using MySqlConnector;
using System.Data;

namespace my0001
{
    class Program
    {
        // 接続情報
        private static readonly string Server = "192.168.5.xxx";    // ホスト名
        private static readonly int Port = 3306;                    // ポート番号
        private static readonly string Database = "nation";         // データベース名
        private static readonly string Uid = "demo";                // ユーザ名
        private static readonly string Pwd = "passwd";              // パスワード

        // 接続文字列
        private static readonly string ConnectionString = $"Server={Server}; Port={Port}; Database={Database}; Uid={Uid}; Pwd={Pwd}";

        static void Main(string[] args)
        {
            using (MySqlConnection conn = new MySqlConnection(ConnectionString))
            {
                conn.Open();

                MySqlDataAdapter da = new MySqlDataAdapter("SELECT * FROM guests", conn);
                DataSet ds = new DataSet();
                da.Fill(ds, "guests");
                DataTable tbl = ds.Tables["guests"];

                foreach(DataRow row in tbl.Rows)
                {
                    Console.WriteLine("Column 0: {0} Column 1: {1}", row[0].ToString(), row[1].ToString());
                }
            }
        }
    }
}

参考にしたサイトはこちら

Ubuntu 18.04 + VsCode + Pytho3 + mysql-connector-python から mariadb10.5 にアクセスしてみる

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

VsCode + C# + .NETCore 3.1 + MySqlConnector から mariadb10.5 にアクセスしてみる

目的

・VsCode + C# + .NETCore 3.1 + MySqlConnector から mariadb10.5 にアクセスしてみる
・Windows 10 と Ubuntu 18.04 でコードをコピペして確認

サンプルを試してみる

・VSCode を立ちあげて適当なフォルダを選択する
・コンソールから作成したフォルダ以下で dotnet new console
~/code/my0001$ ls
Program.cs bin my0001.csproj obj

MySqlConnectorを追加する

※事前にvscode-nuget-package-managerが追加されてることを確認
表示メニュー
 -> コマンドパレット
  -> nuget
   -> Nuget package Manager:Add Pckage
    -> MySqlConnector を入力後 enter
     -> MySqlConnector を選択
      -> 1.0.0-beta.5 を選択

サンプルコード

using System;
using MySqlConnector;
using System.Data;

namespace my0001
{
    class Program
    {
        // 接続情報
        private static readonly string Server = "192.168.5.xxx";    // ホスト名
        private static readonly int Port = 3306;                    // ポート番号
        private static readonly string Database = "nation";         // データベース名
        private static readonly string Uid = "demo";                // ユーザ名
        private static readonly string Pwd = "passwd";              // パスワード

        // 接続文字列
        private static readonly string ConnectionString = $"Server={Server}; Port={Port}; Database={Database}; Uid={Uid}; Pwd={Pwd}";

        static void Main(string[] args)
        {
            using (MySqlConnection conn = new MySqlConnection(ConnectionString))
            {
                conn.Open();

                MySqlDataAdapter da = new MySqlDataAdapter("SELECT * FROM guests", conn);
                DataSet ds = new DataSet();
                da.Fill(ds, "guests");
                DataTable tbl = ds.Tables["guests"];

                foreach(DataRow row in tbl.Rows)
                {
                    Console.WriteLine("Column 0: {0} Column 1: {1}", row[0].ToString(), row[1].ToString());
                }
            }
        }
    }
}

参考にしたサイトはこちら

Ubuntu 18.04 + VsCode + Pytho3 + mysql-connector-python から mariadb10.5 にアクセスしてみる
Ubuntu 20.04 に Posgtgres12 をインストール後 C# + Npgsql でアクセスしてみる

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

【Unity(C#),PUN2】ルームの入退室に対応したプレイヤー生成位置、色の振り分け

やりたかったこと

まず前提として、ネットワーク経由で同期し、
ルームに好きなタイミングで出入りできるアプリケーション

作成を行っていました。

その中で、プレーヤーごとの生成位置及び色をどうきめるか
という問題に直面しました。

位置も色も被ることなくプレイヤーに反映したい! 
というのが今回の記事の本筋です。

試行錯誤

最初に思いついたのは
ルームの人数に応じたプレイヤーの生成位置、色の振り分けです。

ルーム入室時の人数が
0人なら1番目の位置、色
1人なら2番目の位置、色
2人なら3番目の位置、色
3人なら4番目の位置、色 といった具合です。

ただし、このやり方だと再入室時に位置被り、色被りが発生してしまいます。


既に部屋に3人いる場合

部屋には3人.png


4人目の場所と色を割り当てる

4人目の場所と色.png


3人目のプレイヤーが退出

抜けます.png


再入室(もしくは別プレイヤーの入室)

再入室部屋には3人.png


部屋の人数は4人なので既に存在する4番目の位置、色に割り当てられたプレイヤーと被る

4人目の場所と色?.png

そこで別のやり方を考えました。

プレイヤーごとに番号を割り当てる

入室時にプレイヤーごとに番号を割り当てるという方法を思いつきました。
まずはルーム内で使われていない番号をチェックします。

使われていない番号が
1なら番号1をプレイヤーに割り当て
2なら番号2をプレイヤーに割り当て
3なら番号3をプレイヤーに割り当て
4なら番号4をプレイヤーに割り当て といった具合です。

あとは各々のクライアントが各自のローカルで生成された
同期オブジェクトの所有権を持つプレイヤーを取得し、
番号をチェックして処理を行います。

プレイヤー番号振り.png

番号ごとに各々のクライアントが自分のローカル環境で処理を行うので、
位置情報や色を送る必要がなくなります。

つまり、この方法を使えば通信に必要な情報は
プレイヤーごとに割り振られた番号のみ
で済みます。

カスタムプロパティ

入室時にプレイヤーごとに番号を割り当てるという実装を行う上で便利そうな
カスタムプロパティというPUN2の機能に関する記事を発見しました。

【参考リンク】:PUN2で始めるオンラインゲーム開発入門【その5】

今回はカスタムプロパティを利用して、
ルームの入退室に対応したプレイヤー生成位置、色の振り分けを
実装していきます。

カスタムプロパティはプレイヤー単位で設定できて、
必要な時に通信して呼び出す
形で利用できるので
今回の目的との親和性は高いと思います。

拡張メソッド

カスタムプロパティを使う上で拡張メソッドは必須かなと個人的には思ってます。

理由としては、カスタムプロパティの定義には文字列のキーが必要だからです。

とある文字列Xに対して、値を紐づけていくようなイメージです。

コードに落とし込むとこんな感じになります。

using Photon.Realtime;
using UnityEngine;
using Hashtable = ExitGames.Client.Photon.Hashtable;

public static class PlayerPropertyExtensions
{
    private const string PLAYER_ASSIGN_NUMBER = "n"; 

    private static Hashtable _hashtable = new Hashtable();

    //=========================================
    // プレイヤーの番号
    //=========================================

    //Hashtableにプレイヤーに割り振られた番号があれば取得する
    private static bool TryAndGetPlayerNum(this Hashtable hashtable, out int playerAssignNumber)
    {
        if (hashtable[PLAYER_ASSIGN_NUMBER] is int value)
        {
            playerAssignNumber = value;
            return true;
        }

        playerAssignNumber = 0;
        return false;
    }

    //プレイヤー番号を取得する
    public static int GetPlayerNum(this Player player)
    {
        player.CustomProperties.TryAndGetPlayerNum(out int playerNum);
        return playerNum;
    }

    //プレイヤーの割り当て番号のカスタムプロパティを更新する
    public static void UpdatePlayerNum(this Player player, int assignNum)
    {
        _hashtable[PLAYER_ASSIGN_NUMBER] = assignNum;
        player.SetCustomProperties(_hashtable);
        _hashtable.Clear();
    }
}

任意の文字列キーのカスタムプロパティを取得、更新する拡張メソッドを用意することで、
利用側は文字列キーの誤りを気にする必要性がなくなります。

使用済みの番号を特定し、未使用の番号でプレイヤーの番号を更新

拡張メソッドで利用側はかなり楽になったので、
プレイヤーごとの番号割り当ての実装に移ります。

順番としては見出し通り下記です。
①使用済みの番号を特定
②未使用の番号でプレイヤーの番号を更新

コードは以下です。

private const int _PLAYER_UPPER_LIMIT = 4;

/// <summary>
/// プレイヤーに番号を与える
/// </summary>
private void SetMyCustomProperties()
{ 
    //自分のクライアントの同期オブジェクトにのみ
    if (photonView.IsMine)
    {
        List<int> playerSetableCountList = new List<int>();

        //制限人数までの数字のリストを作成
        //例) 制限人数 = 4 の場合、{0,1,2,3}
        int count = 0;
        for (int i = 0; i < _PLAYER_UPPER_LIMIT; i++)
        {
            playerSetableCountList.Add(count);
            count++;
        }

        //他の全プレイヤー取得
        Player[] otherPlayers = PhotonNetwork.PlayerListOthers;

        //他のプレイヤーがいなければカスタムプロパティの値を"0"に設定
        if (otherPlayers.Length <= 0)
        {
            //ローカルのプレイヤーのカスタムプロパティを設定
            int playerAssignNum = otherPlayers.Length;
            PhotonNetwork.LocalPlayer.UpdatePlayerNum(playerAssignNum);
            return;
        }

        //他のプレイヤーのカスタムプロパティー取得してリスト作成
        List<int> playerAssignNums = new List<int>();
        for (int i = 0; i < otherPlayers.Length; i++)
        {
            playerAssignNums.Add(otherPlayers[i].GetPlayerNum());
        }

        //リスト同士を比較し、未使用の数字のリストを作成
        //例) 0,1にプレーヤーが存在する場合、返すリストは2,3
        playerSetableCountList.RemoveAll(playerAssignNums.Contains);

        //ローカルのプレイヤーのカスタムプロパティを設定
        //空いている場所のうち、一番若い数字の箇所を利用
        PhotonNetwork.LocalPlayer.UpdatePlayerNum(playerSetableCountList[0]);
    }
} 

コメントの通りの処理を行っています。

番号に応じた生成位置と色を割り当て

未使用の番号でプレイヤーの番号を更新 することができたので、
割り当てられた番号を利用した処理を追加していきます。

PhotonViewがアタッチされたアバターオブジェクトにアタッチ
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using ExitGames.Client.Photon;
using Photon.Pun;
using Photon.Realtime;
using UnityEngine;

/// <summary>
/// プレーヤー生成時に行う初期設定処理
/// </summary>
public class PlayerInitializeSetting : MonoBehaviourPunCallbacks
{
    [SerializeField] private MeshRenderer _avatarObjectMeshRenderer;
    [SerializeField] private Material[] _playerMaterials;
    [SerializeField] private Transfrom[] _playerInitTransform;

    private bool _isUpdateCustomProperty;

    private async void Start()
    {
        //プレーヤーのカスタムプロパティ更新
        SetMyCustomProperties();

        //通信完了(カスタムプロパティ更新)まで待つ
        await UniTask.WaitUntil(() => _isUpdateCustomProperty);

        //自分のクライアントの同期オブジェクトの設定
        if (photonView.IsMine)
        {
            this.gameObject.transform.rotation = _playerInitTransform[PhotonNetwork.LocalPlayer.GetPlayerNum()].rotation;
            this.gameObject.transform.position = _playerInitTransform[PhotonNetwork.LocalPlayer.GetPlayerNum()].position;
            _avatarObjectMeshRenderer.sharedMaterial = _playerMaterials[PhotonNetwork.LocalPlayer.GetPlayerNum()];
        }
        //他のクライアントの同期オブジェクトの設定
        else
        {
            this.gameObject.transform.rotation = _playerInitTransform[photonView.Owner.GetPlayerNum()].rotation;
            this.gameObject.transform.position = _playerInitTransform[photonView.Owner.GetPlayerNum()].position;
            _avatarObjectMeshRenderer.sharedMaterial = _playerMaterials[photonView.Owner.GetPlayerNum()];
        }
    }

    /// <summary>
    /// カスタムプロパティ更新時のコールバック
    /// </summary>
    /// <param name="target">更新されたカスタムプロパティを持つプレーヤー</param>
    /// <param name="changedProps">更新されたカスタムプロパティ</param>
    public override void OnPlayerPropertiesUpdate(Player target, Hashtable changedProps)
    {
        _isUpdateCustomProperty = true;
    }
}

OnPlayerPropertiesUpdateという
通信完了後、カスタムプロパティが更新し終えたことを
通知するコールバック
内でフラグを立てています。

このフラグを利用し、Start関数内でasync/awaitして完了待ちを実装しています。

デモ

手話VRSS2.PNG

実際に今回メモした実装方法でプレイヤーの生成位置、色の設定を
入退室に対応させたアプリのデモムービーです。

【参考リンク】:Sign Language in VR

オンライン上でVR空間内での手話トレーニングを実施し、
動画やビデオ通話以上のトレーニング効果を期待したVRアプリです。

最後に

カスタムプロパティで初期設定を行う方法を使用しましたが、
完全に同じタイミングでカスタムプロパティを
更新した場合のエラーハンドリングなどが未実装です。

私が把握している問題は現状これだけですが、
まだまだ対策しないといけないことが多そうです。

引き続き深みにはまっていこうと思います。。。

参考リンク

PUN2で始めるオンラインゲーム開発入門【その4】

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

C# でシングルバイナリアプリケーションを作成する

.NET Core 3.0 でシングルバイナリを作れるようになるオプションが追加されました。自分的には、シングルバイナリを作りたいときは今までは常にGoでプログラムを書いてきました。それはリリースやインストールが圧倒的に楽だからです。これがC#でできるようになることはうれしいことです。以前から存在は知っていましたが、試したことはなかったので、実際に使ってアプリを作ってみました。

image.png

Self Contained アプリケーション

最初は名前からしてこれが該当設定と思っていました。.NET Core のコンソールアプリケーションを Self-Containedにしてみます。csproj ファイルに次の設定を入れてみましょう。ちなみに Self Contained は DotNet のランタイムを成果物に含むという意味で1つのバイナリファイルという意味ではありません。Self Contained を1つのファイルにパッケージすることで、シングルバイナリのアプリケーションが出来上がります。

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <RuntimeIdentifier>win10-x64</RuntimeIdentifier>
    <PublishReadyToRun>true</PublishReadyToRun>
    <PublishSingleFile>true</PublishSingleFile>
  </PropertyGroup>
Property Description Possible Value Default
OutputType 出力タイプのフォーマット Library, Exe, Module, Winexe Library
TargetFramework アプリケーションを実行するのに必要な .NET のフレームワーク netcore3.1 等 詳細 N/A
RuntimeIdentifier Publish で使われるランタイム識別子、OSの種類やアーキテクチャを指定する win10-x64 詳細 N/A
PublishReadyToRun ILに加えて、ネイティブにコンパイルされたコードを含む。現在のところクロスプラットフォームビルドはサポートされていない。詳細 true/false false
PublishSingleFile アプリケーションを1つのExeにまとめる true/false false

この設定をしてdotnet publishコマンドを実行するとwin10-x64用のアプリケーションができます。

$ dotnet publish

PublishReadyToRun をするとすでにネイティブにコンパイルされているバイナリを含むので起動時間が早くなります。ただし、現在のところクロスプラットフォームビルドはサポートされていません。具体的にはWindows10で、Linuxのビルドをするときにこのオプションは現在のバージョン(3.1.301)では使えないようです。CIでビルドする場合は、ビルドしたい対象のプラットフォームのビルドを使うとよさげです。ただ、このオプションの指定はサイズを増加させるようです。リリースのコマンドでオプションを指定して試してみましょう。

PS > dotnet publish -c Release -r win10-x64 -p:PublishSingleFile=true -p:PublishReadyToRun=true
PS > ls C:\Users\tsushi\source\repos\KafkaSpike\KafkaSpike\bin\Release\netcoreapp3.1\win10-x64\publish\
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----          7/4/2020   1:32 PM       76303080 ProducerSample.exe
-a----          7/4/2020   1:32 PM           1588 ProducerSample.pdb

PS > dotnet publish -c Release -r win10-x64 -p:PublishSingleFile=true -p:PublishReadyToRun=false
PS > ls C:\Users\tsushi\source\repos\KafkaSpike\KafkaSpike\bin\Release\netcoreapp3.1\win10-x64\publish\
Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----          7/4/2020   1:32 PM       75643254 ProducerSample.exe
-a----          7/4/2020   1:32 PM           1588 ProducerSample.pdb

思ったより大したことなさそうですね。ちなみに、2つのコマンドの間には、Remove-Item -Path .\bin -Recurse などでbinを削除してからやるほうがよさそうです。しないと同じ結果になりました。

Linuxもビルドしてみましょう。ただし、この場合は、PublishReadyToRun Windows10だと使えませんので、

PS > dotnet publish -c Release -r linux-x64 -p:PublishSingleFile=true -p:PublishReadyToRun=false
PS  > ls C:\Users\tsushi\source\repos\KafkaSpike\KafkaSpike\bin\Release\netcoreapp3.1\linux-x64\publish\

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----          7/4/2020   1:38 PM      137016076 ProducerSample
-a----          7/4/2020   1:38 PM           1588 ProducerSample.pdb

サイズでけぇなぁw でも、Linux 用のビルドもできるのはありがたい。137MB で、Windows の 75MB の倍ぐらいですね。

Visual Studio

最後に、Visual Studio でも設定ができますので、見てみましょう。Project ファイル右クリック > Publish を選択します。

image.png

これで、一通りのシングルバイナリ系の設定が理解できたと思います。次が、シングルバイナリにほしくなる、コマンドラインパーサーの調査をしてみたいと思います。

参考までにリソースのパートに私が実際に使ったサンプルのURLを張っておきますね。

リソース

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

WPFで滑らかマウス操作を実現

WPFとMainWindowで立方体を作成

以下のような立方体を作成し、マウス操作で空間回転できるようにしました。
キャプチャ.PNG

WPFのソースコード

・以下のようになりました。

MainWindow.xaml
<Window x:Class="RotateTheCube.Rotate"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="" Height="600" Width="900" Background="Black" MouseLeftButtonUp="MouseUp3D">
    <Viewport3D>
        <Viewport3D.Camera>
            <PerspectiveCamera LookDirection="0,0,-1" Position="0,0,5" />
        </Viewport3D.Camera>
        <ModelVisual3D>
            <ModelUIElement3D>
                <DirectionalLight Color="Red" Direction="-0,-0.5,-0.6" />
            </ModelUIElement3D>
            <ModelUIElement3D>
                <DirectionalLight Color="White" Direction="0.612372,-0.5,-0.612372" />
            </ModelUIElement3D>
            <ModelUIElement3D MouseMove="MouseMove3D" MouseLeftButtonDown="MouseDown3D" >
                <GeometryModel3D>
                    <GeometryModel3D.Geometry>
                        <MeshGeometry3D
                                Positions="-0.5  0.5  0.5,  0.5  0.5  0.5,
                                           -0.5 -0.5  0.5,  0.5 -0.5  0.5,
                                            0.5  0.5 -0.5, -0.5  0.5 -0.5,
                                            0.5 -0.5 -0.5, -0.5 -0.5 -0.5,
                                           -0.5  0.5 -0.5, -0.5  0.5  0.5,
                                           -0.5 -0.5 -0.5, -0.5 -0.5  0.5,
                                            0.5  0.5  0.5,  0.5  0.5 -0.5,
                                            0.5 -0.5  0.5,  0.5 -0.5 -0.5,
                                           -0.5  0.5 -0.5,  0.5  0.5 -0.5,
                                           -0.5  0.5  0.5,  0.5  0.5  0.5,
                                            0.5 -0.5 -0.5, -0.5 -0.5 -0.5,
                                            0.5 -0.5  0.5, -0.5 -0.5  0.5"
                                TriangleIndices=" 0  2  1,  1  2  3
                                                  4  6  5,  5  6  7,
                                                  8 10  9,  9 10 11,
                                                 12 14 13, 13 14 15
                                                 16 18 17, 17 18 19
                                                 20 22 21, 21 22 23"/>
                    </GeometryModel3D.Geometry>
                    <GeometryModel3D.Material>
                        <DiffuseMaterial Brush="White"/>
                    </GeometryModel3D.Material>
                    <GeometryModel3D.Transform>
                        <MatrixTransform3D   x:Name="myTransform"/>
                    </GeometryModel3D.Transform>
                </GeometryModel3D>
            </ModelUIElement3D>
        </ModelVisual3D>
    </Viewport3D>
</Window>

C#のソースコード

・続いてC#のコードです。

MainWindow.xaml.cs
// ***************************
// * 立方体を空間で回転させる
// *   2020.07.05  ProOJI
// ***************************
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Media3D; //3D行列使用
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace RotateTheCube
{
    public partial class Rotate : Window
    {
        Matrix3D m = Matrix3D.Identity;
        private bool _isDrag = false;
        private Point _Offset;
        private void MouseDown3D(object sender, MouseButtonEventArgs e)
        {
            _isDrag = true;
            _Offset = e.GetPosition(this);
        }
        private void MouseUp3D(object sender, MouseButtonEventArgs e)
        {
            _isDrag = false;
        }
        private void MouseMove3D(object sender, MouseEventArgs e)
        {
            if (_isDrag == true)
            {
                Point pt = e.GetPosition(this);
                m.Rotate(new Quaternion(new Vector3D(0, 1, 0), (pt.X - _Offset.X) / 50));
                m.Rotate(new Quaternion(new Vector3D(1, 0, 0), (pt.Y - _Offset.Y) / 50));
                myTransform.Matrix = m;
            }
        }
    }
}

まとめ

線形代数がこんなところで役立つとは…
数学をもっと学ぼうと思いました。

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

とあるAPIをたたいて日付の値を取得したら、フォーマットが変でバグかと思ったけどISO 8601でサポートされているフォーマットだった話

ある日の出来事

PiWeb-APIで測定を取得し、日付の値(K4)をしたら、フォーマットが変だったけど、そういう仕様だったお話し。

本当の日付
2020/06/18 21:45:14

取得した日付の値
2020-06-18T12:45:14Z

以下のように変換したら問題ありませんでした。

test
var datetime = "2020-06-18T12:45:14Z";
var convertedDatetime = DateTime.ParseExact(datetime, "yyyy-MM-ddTHH:mm:ssZ", CultureInfo.InvariantCulture);

Console.Writeline(convertedDatetime.ToString("yyyyMMdd HH:mm:ss"));
output
20200618 21:45:14

web サービスでよく使用される
とのことです。

この辺を確認しました。
日時のフォーマット(ISO 8601) - Qiita
C# で日付をフォーマットされた文字列に変換する - C# 便利メモ - C# 入門
DateTime 構造体 (System) | Microsoft Docs

-

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

PowerShell で C# コードを実行

前提

こちら で紹介されているように PowerShell で Add-Type コマンドレットを介して C# のコードが実行できます。

問題点

上記の記事でもふれられているように C# コードを修正して Add-Type を再実行すると

Console
Add-Type : 型を追加できません。型名 'Example' は既に存在しています。

なるエラーがでます。

対策

ワーク アラウンドとしては
* 別プロセスの powershell.exe を起動して Add-Type しなおす。
* Start-Job をつかう。ただし、Console.Write() などのコンソールへの直接的出力はすてられるという制限あり。

別プロセスの powershell.exe を起動して Add-Type しなおす

別プロセスの powershell.exe を起動して Add-Type しなおすのはめんどうなので、多少なりともの自動化として、ラッパー関数をかいてみました。ここ のサンプル コードもエラーなしで実行できるように culture にもきをくばってみました。

PowerShell
function Invoke-CSharp {
    [CmdletBinding(DefaultParameterSetName = 'Code')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'Code', Position = 0)]
        [String] $Code,

        [Parameter(Mandatory = $true, ParameterSetName = 'Path')]
        [ValidateScript({ Test-Path -Path $_ })]
        [String] $FilePath,

        [String] $ClassName,

        [String] $MethodName = 'Main',

        [String[]] $ArgumentList,

        [String] $CultureName = 'en-US'
    )

    if ($FilePath) {
        $Code = Get-Content -Path $FilePath -Raw
    }

    if (! $ClassName) {
        $ClassName = $Code -split '\r?\n' |
            Select-String -Pattern '^\s*public(?:\s\w+)*?\s+class\s+(\w+)' |
            Select-Object -First 1 |
            ForEach-Object -Process { $_.Matches.Groups[1].Value }
    }

    $ArgumentList = $ArgumentList |
        ForEach-Object -Process {
            if ($_ -is [String]) {
                '"' + $_.Replace('"', '`"') + '"'
            } else {
                [String] $_
            }
        }
    [String] $ArgumentList = $ArgumentList -join ', '

    $command = [System.Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes(@"
[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('$CultureName')
Add-Type -TypeDefinition @'
$Code
'@ -Language CSharp
[$ClassName]::$MethodName($ArgumentList)  # Qiita の記事の ``` のなかでも \[.*\]:.*\( をふつうにかくと非表示になり、 # をうしろにおくと復活します。なにかバギーです。
"@))

    Start-Process -FilePath powershell.exe -ArgumentList '-NoLogo', '-NoExit', "-EncodedCommand $command"
}

使用例

上記の記事よりおかりしまして、

PowerShell
Invoke-CSharp -Code @'
using System;
public static class TestClass1
{
    public static string TestMethod(string name, string greet)
    {
        string msg = name + "さん、" + greet;

        Console.WriteLine(msg);
        return msg;
    }
}
'@ -MethodName 'TestMethod' -ArgumentList '太郎', 'おはよう' -CultureName 'ja-JP'

自分の用途としては Visual Studio などいれられない、いれたくない環境でもサンプル コードをきがるに「写経」できることです。よって、 -FilePath パラメーターを多用します。

Start-Job をつかう

Console.Write() などのコンソールへの直接的出力はすてられるという制限があります。

PowerShell
$j = Start-Job -ScriptBlock {
    Add-Type -TypeDefinition @'
using System;
public static class TestClass1
{
    public static string TestMethod(string name, string greet)
    {
        string msg = name + "さん、" + greet;

        Console.WriteLine(msg);
        return msg;
    }
}
'@ -Language CSharp

    [TestClass1]::TestMethod('太郎', 'おはよう')
}

Wait-Job -Job $j | Out-Null
Receive-Job -Job $j

この C# コードを「ふつうに」実行すると「太郎さん、おはよう」が二行でます。一行目は Console.WriteLine(msg); から、二行目は return msg; からのです。しかし、この Start-Job をかませた実行では return msg; からのぶんしかでてきません。

補足

コードを実行させたいだけなら

Console
C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe .\example.cs

とすれば .\example.exe が生成されるのでした (2020 年 7 月現在) 。

ただ、このばあい ここ のサンプル コードは Main() の最初に

C#
System.Threading.Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("en-US");

を追加することでしか正常に実行させられませんでした。 PS のコマンドラインで

PowerShell
[System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo]::GetCultureInfo('en-US')
.\example.exe

とするのは効果がありませんでした。

参考文献

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