20191223のC#に関する記事は16件です。

C# を使って爆速で Web Developer in 2019 を学ぶ

初心者用の記事ですがあまり深堀はしないので、どちらかと言うと他言語から C# を見に来た人向けかもしれません。

Web 界隈では有名なこれを C# で学んでいきます。Back-end の部分をやっていきます。爆速なので外部アプリケーションが必要な部分はスキップします。
IDE は Visual Studio 2019 を使います。 SDK は .NET Core 3.1 を使います。

kamranahmedse/developer-roadmap: Roadmap to becoming a web developer in 2019

日本語訳は Web Developer Roadmap 2018 が 2019 年版になっていたので比較してみる - Qiita にあります。

リンク先を見なくてもわかるように画像だけ持ってきました。

Image from Gyazo

Roadmap

1. Pick a Language

  • 説明: C# (.NET) です。

2. Practice what you learnt

  • 説明: テンプレートが用意されているので、マウスクリックだけで学べます。

Visual Studio 2019 を起動して Create a new project を選択します。

Image from Gyazo

Console App (.NET Core) を選択します。

Image from Gyazo

適当な名前をつけて Create を選択します。

Image from Gyazo

こんな感じのプロジェクトが出来たら F5緑色の再生ボタン を押して実行します。

Image from Gyazo

無事に Hello World! が表示されました。コマンドラインアプリケーションは学び終わりました。爆速です。

Image from Gyazo

3. Learn Package Manager

  • 説明: GUI があるので、マウスクリックだけで学べます。

Tools > NuGet Package Manger > Manage NuGet Packages for Solution を選択します。

Image from Gyazo

Browse タブから Install したい Package を選んで Install を選択します。

Image from Gyazo

Package Install が完了したらすぐに使えるようになります。 Uninstall や Update も GUI から可能です。パッケージマネージャーは学び終わりました。爆速です。

Image from Gyazo

4. Standards and Best Practices

  • 説明: C# は Microsoft が出している言語なので Microsoft 公式ドキュメント C# のコーディング規則 - C# プログラミング ガイド | Microsoft Docs が存在します。凄く長くて眠たくなるし、どちらかというと実践しながら学んでいくものなのでスキップします。
  • 補足: なんか怪しいなと思ったら電球マークか Ctrl + . を押せば、これが正しいよということを教えてくれます。

Image from Gyazo

5. Make and Distribute Some Package/Library

6. Learn about Testing

  • 説明: Unit Test を書きます。ようやくキーボードの出番です。

Solution menu > Add > New Project を選択します。

Image from Gyazo

xUnit Test Project (.NET Core) を選択します。

Image from Gyazo

適当な名前をつけて Create を選択します。

Image from Gyazo

Unit Test 用のプロジェクトとコードが作成されます。

Image from Gyazo

Test1 method の中身を下のコードに書き換えます。 a と b が同じだったら成功というコードです。

[Fact]
public void Test1()
{
    var a = 123;
    var b = 123;
    Assert.Equal(a, b);
}

右クリックメニューから Run Test(s) を選択します。

Image from Gyazo

Test Explorer が開いてテストが実行されます。無事に成功しました。 Unit Test は学び終わりました。爆速です。

Image from Gyazo

7. Write Tests for the practical steps above

  • 説明: 爆速すぎてテスト対象のコードがないのでスキップします。

8. Learn Relational Databases

9. Practical Time

  • 説明: ログインや CRUD を学ぶには RDBMS があったほうがよいのでスキップします。

10. Learn a Framework

  • 説明: C# は .NET が全てなので使っていくうちに学んで行くとよいです。スキップします。
  • 補足: .NET Core を学んでください。 .NET Framework は必要になったら学ぶ程度で OK です。今度の予定では .NET Framework は廃止になり、 .NET Core から引き継いだ .NET 5 というものが誕生します。

11. Practical Time

  • 説明: #10 と同じなのでスキップします。

12. Learn a NoSQL Database

  • 説明: NoSQL を学ぶには NoSQL が必要なのでスキップします。

13. Caching

  • 説明: Cache を学ぶには Cache が必要なのでスキップします。

14. Creating RESTful APIs

  • 説明: テンプレートが用意されているので、マウスクリックだけで学べます。

新しいプロジェクトを作る画面から ASP.NET Core Web Application を選択します。

Image from Gyazo

適当な名前をつけて Create を選択します。

Image from Gyazo

どのテンプレートにするか再度聞かれるので API を選択します。

Image from Gyazo

WeatherForecastController を開くと、ランダムな天気予報を GET で返してくれる Method が出来ています。 F5緑色の再生ボタン を押して実行します。

Image from Gyazo

ブラウザが開いて無事に json 形式の天気予報が表示されました。 RESTful APIs は学び終わりました。爆速です。

Image from Gyazo

補足: GET 以外もテンプレートがあります。

Solution menu > Add > Controller を選択します。

Image from Gyazo

API Controller with read/write actions を選択します。

Image from Gyazo

適当な名前をつけて Add を選択します。

Image from Gyazo

GET / POST / PUT / DELETE が出来上がっています。爆速です。

Image from Gyazo

15-23

  • 説明: 外部アプリケーションが必要だったり、概念説明が難しいのでスキップします。

Note

スキップします。 という Word を検索したら 12 件 Hit しました。 2019 年はもう終わってしまうので、 2020 年は爆速じゃないバージョンを一つづつ丁寧に解説したいです。

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

Printer SpoolerのSPLファイルの構造と情報取得

プリンタジョブはSPLファイル

Windowsのプリンタのジョブの実態のファイルは、5桁の数字の番号のファイルが2つ、拡張子のみ異なるファイルが2つあります。
1つはこのタイトルのSPLファイル、もう1つはプリンタに実際に送り届けられるデータ(プリンタ専用のデータ)のSHDファイルです。
プリンタを開いて、プリントキューとしてプリンタへの送信待ち一覧に表示される情報は、このSPLファイルのデータのようです。
ですが一般には、利用価値が無いので構造がWindows2003までしかネットに載っていませんでした。
なので、とりあえず簡易にですが、クラスとして取得できるようにしたものを作りました。

    public class SPL_data_class
    {
        public bool is_false = false;

        private long VERSION_number;
        private long HeaderSize;
        private long Status;
        private long JobID;
        private long dwPriority;
        private long offUserName;
        private long offNotifyName;
        private long offDocumentName;
        private long Unknown_0;
        private long offPort;
        private long offPrinterName;
        private long offDriverName;
        private long offDevMode;
        private long offPortMode;

        private long offOnPrinter_MachineName;
        private long offUniqIdNumber;



        public string VERSION;

        public string UserName;
        public string NotifyName;
        public string DocumentName;
        public string Port;
        public string PrinterName;
        public string DriverName;
        public string DevMode;
        public string PortMode;
        public string OnPrinter_MachineName;
        public string UniqIdNumber;

        public SPL_data_class()
        {
        }
        public SPL_data_class(string in_spl_filepath)
        {
            bool is_bool = set_proc(in_spl_filepath);

            if (is_bool == false)
                is_false = true;
        }

        private bool set_proc(string in_spl_filepath)
        {
            ByteReader br = new ByteReader(in_spl_filepath);
            if (br.is_false == true)
            {
                return false;
            }

            VERSION_number = br.GetDWord(0);
            switch (VERSION_number)
            {
                default:
                    {
                        is_false = true;
                        return false;
                    }
                    break;
                case 20771:
                    {
                        //
                        // Windows10 64bit (Version 10.0.18362.535)のケース
                        //
                        if (br.length < 192)    // ファイルがヘッダよりも短いケース
                        {
                            is_false = true;
                            return false;
                        }

                        VERSION = "WINDOWS10_64BIT";
                        HeaderSize = br.GetDWord(4);
                        Status = br.GetDWord(8);
                        JobID = br.GetDWord(12);
                        dwPriority = br.GetULONGLONG(16);
                        offUserName = br.GetULONGLONG(24);
                        offNotifyName = br.GetULONGLONG(32);
                        offDocumentName = br.GetULONGLONG(40);
                        long Unknown_0 = br.GetULONGLONG(48);
                        offPort = br.GetULONGLONG(56);
                        offPrinterName = br.GetULONGLONG(64);
                        offDriverName = br.GetULONGLONG(72);
                        offDevMode = br.GetULONGLONG(80);
                        offPortMode = br.GetULONGLONG(88); // RAW
                        long offTEST2 = br.GetULONGLONG(96);
                        long offTEST3 = br.GetULONGLONG(104);
                        long offTEST4 = br.GetULONGLONG(112);
                        long offTEST5 = br.GetULONGLONG(120);
                        long offTEST6 = br.GetULONGLONG(128);
                        long offTEST7 = br.GetULONGLONG(136);
                        long offTEST8 = br.GetULONGLONG(144);
                        long offTEST9 = br.GetULONGLONG(152);
                        long offTEST10 = br.GetULONGLONG(160);
                        offOnPrinter_MachineName = br.GetULONGLONG(168);  // \PC-NJS1
                        long offTEST12 = br.GetULONGLONG(176);
                        offUniqIdNumber = br.GetULONGLONG(184);  // S-1-5-
                        /*long offTEST14 = br.GetULONGLONG(192);
                        long offTEST15 = br.GetULONGLONG(200);
                        long offTEST16 = br.GetULONGLONG(208);
                        long offTEST17 = br.GetULONGLONG(216);
                        long offTEST18 = br.GetULONGLONG(224);
                        long offTEST19 = br.GetULONGLONG(232);
                        long offTEST20 = br.GetULONGLONG(240);*/
                        UserName = br.GetText_asUTF16(offUserName);
                        NotifyName = br.GetText_asUTF16(offNotifyName);
                        DocumentName = br.GetText_asUTF16(offDocumentName);
                        Port = br.GetText_asUTF16(offPort);
                        PrinterName = br.GetText_asUTF16(offPrinterName);
                        DriverName = br.GetText_asUTF16(offDriverName);
                        DevMode = br.GetText_asUTF16(offDevMode);
                        PortMode = br.GetText_asUTF16(offPortMode);

                        OnPrinter_MachineName = br.GetText_asUTF16(offOnPrinter_MachineName);
                        UniqIdNumber = br.GetText_asUTF16(offUniqIdNumber);
                    }
                    break;
                case 18792:
                    {
                        //
                        // Windows7 32bit のケース
                        //
                        if (br.length < 124)    // ファイルがヘッダよりも短いケース
                        {
                            is_false = true;
                            return false;
                        }

                        VERSION = "WINDOWS7_32BIT";
                        HeaderSize = br.GetDWord(4);
                        Status = br.GetWord(8);
                        long Unknown = br.GetWord(10);
                        JobID = br.GetDWord(12);
                        dwPriority = br.GetDWord(16);
                        offUserName = br.GetDWord(20);
                        offNotifyName = br.GetDWord(24);
                        offDocumentName = br.GetDWord(28);
                        long Unknown_0 = br.GetDWord(32);
                        offPort = br.GetDWord(36);
                        offPrinterName = br.GetDWord(40);
                        offDriverName = br.GetDWord(44);
                        offDevMode = br.GetDWord(48);
                        offPortMode = br.GetDWord(52); // RAW
                        long offTEST2 = br.GetDWord(54);
                        long offTEST3 = br.GetDWord(58);
                        long offTEST4 = br.GetDWord(62);
                        long offTEST5 = br.GetDWord(66);
                        long offTEST6 = br.GetDWord(70);
                        long offTEST7 = br.GetDWord(74);
                        long offTEST8 = br.GetDWord(78);
                        long offTEST9 = br.GetDWord(82);
                        long offTEST10 = br.GetDWord(86);
                        long offTEST11 = br.GetDWord(90);
                        long offTEST12 = br.GetDWord(94);
                        long offTEST13 = br.GetDWord(98);
                        long offTEST14 = br.GetDWord(102);
                        long offTEST15 = br.GetDWord(106);
                        long offTEST16 = br.GetDWord(108);
                        offOnPrinter_MachineName = br.GetDWord(112);  // \PC-NJS1
                        long offTEST18 = br.GetDWord(116);
                        offUniqIdNumber = br.GetDWord(120);  // S-1-5-
                        /*long offTEST20 = br.GetDWord(124);*/
                        UserName = br.GetText_asUTF16(offUserName);
                        NotifyName = br.GetText_asUTF16(offNotifyName);
                        DocumentName = br.GetText_asUTF16(offDocumentName);
                        Port = br.GetText_asUTF16(offPort);
                        PrinterName = br.GetText_asUTF16(offPrinterName);
                        DriverName = br.GetText_asUTF16(offDriverName);
                        DevMode = br.GetText_asUTF16(offDevMode);
                        PortMode = br.GetText_asUTF16(offPortMode);

                        OnPrinter_MachineName = br.GetText_asUTF16(offOnPrinter_MachineName);
                        UniqIdNumber = br.GetText_asUTF16(offUniqIdNumber);
                    }
                    break;

            }

            br.dispose();
            return true;
        }
    }



    public class ByteReader
    {
        private FileStream _fs;
        public bool is_false = false;
        public long length
        {
            get
            {
                if (_fs == null)
                    return 0;
                return _fs.Length;
            }
        }
        public ByteReader(string in_filepath)
        {
            if(File.Exists(in_filepath)==false)
            {
                is_false = true;
                return;
            }

            _fs = new FileStream(in_filepath, FileMode.Open);
        }
        public void dispose()
        {
            if (_fs == null)
                return;

            _fs.Close();
        }
        public long GetULONGLONG(long in_file_pos, bool is_unsigned = true)
        {
            byte[] buf = new byte[8];
            _fs.Position = in_file_pos;
            _fs.Read(buf, 0, 8);

            //
            // 標準で、リトルエンディアンとして
            long ret_value = (is_unsigned) ? (int)BitConverter.ToUInt64(buf, 0) : (int)BitConverter.ToInt64(buf, 0);

            return ret_value;
        }
        public long GetDWord(long in_file_pos, bool is_unsigned = true)
        {
            byte[] buf = new byte[4];
            _fs.Position = in_file_pos;
            _fs.Read(buf, 0, 4);

            //
            // 標準で、リトルエンディアンとして
            long ret_value = (is_unsigned) ? (int)BitConverter.ToUInt32(buf, 0) : (int)BitConverter.ToInt32(buf, 0);

            return ret_value;
        }
        public int GetWord(long in_file_pos, bool is_unsigned = true)
        {
            byte[] buf = new byte[2];
            _fs.Position = in_file_pos;
            _fs.Read(buf, 0, 2);
            int ret_value = (is_unsigned) ? (int)BitConverter.ToUInt16(buf, 0) : (int)BitConverter.ToInt16(buf, 0);

            return ret_value;
        }
        /// <summary>1Bytes読み込み、intにして返します。</summary>
        public int GetByte(long in_file_pos)
        {
            byte[] buf = new byte[1];
            _fs.Position = in_file_pos;
            _fs.Read(buf, 0, 1);
            int ret_value = buf[0];

            return ret_value;
        }
        public string GetUTF16Text(long in_file_pos, int in_length)
        {
            byte[] buf = new byte[in_length];
            _fs.Position = in_file_pos;
            _fs.Read(buf, 0, in_length);

            string ret_text = System.Text.Encoding.Unicode.GetString(buf);  // Unicodeは .Net では UTF-16 と同義です。

            return ret_text;
        }
        public string GetText_asUTF16(long in_file_pos_at_start, long in_file_pos_at_next_ByEnd)
        {
            int tmp_length = (int)(in_file_pos_at_next_ByEnd - in_file_pos_at_start);

            return GetUTF16Text(in_file_pos_at_start, in_length: tmp_length);
        }
        private long get_Length_DoubleNullまで(long in_file_start_pos)
        {
            long ret_length = 0;

            bool is_before_NULL = false;
            while (true)
            {
                if (_fs.Length == in_file_start_pos + ret_length)   // ファイル終端のケース
                    return ret_length;

                byte[] buf = new byte[1];
                _fs.Position = in_file_start_pos + ret_length;
                _fs.Read(buf, 0, 1);
                switch (buf[0])
                {
                    case 0x00:  // Null的な・・・
                        if (is_before_NULL == true)
                        {
                            //
                            // ここでの現在地そのものは終端ではない
                            //
                            _fs.Position = in_file_start_pos + ret_length + 1;
                            _fs.Read(buf, 0, 1);
                            if (buf[0] == 0x00 || ret_length <= 1)
                            {
                                // 対象文字 [00] 00 00 次の文字 のケース
                                // [00] は含む(半角英字のケースなど)
                                return ret_length;    // 0x00 が3個続いているケース
                            }
                            //
                            // 対象文字 [00] 00 次の文字 のケース
                            // [00] は含まない(漢字のケースなど)
                            return ret_length - 1;
                        }
                        is_before_NULL = true;
                        break;
                    default:
                        is_before_NULL = false;
                        break;
                }
                ret_length++;
            }
        }
        public string GetText_asUTF16(long in_file_pos_at_start)
        {
            long tmp_length = get_Length_DoubleNullまで(in_file_pos_at_start);

            byte[] buf = new byte[tmp_length];
            _fs.Position = in_file_pos_at_start;
            _fs.Read(buf, 0, (int)tmp_length);

            string ret_text = System.Text.Encoding.Unicode.GetString(buf);  // Unicodeは .Net では UTF-16 と同義です。

            return ret_text.TrimEnd();
        }
    }

このSPLファイルをWindows Spoolerがプリンタに送信する前に扱いたいのですが、それにはプリンタを一時停止にするなどして、ジョブのプリンタへの送信をしない状態を作るひつようがあります。
また、外部操作でSPLとSHDファイルを削除しても、PrintSpoolerは何か操作をされないと消えたという情報は反映されないようです。
PrintSpoolerはサービスとして動作するもので、敢えて互換ソフトを作るようなものでもないので、この時点で詰まっていますが、やってみたいことがあるので作っています。
印刷データをプリンタ専用データに変換されたものがSHDファイルですので、プリントサーバーがPDF等を共有フォルダに設置されると、プリントサーバーで既設定のプリンタドライバでSHDファイルにして、そのSHDファイルをプリンタに送信するなり、ストックして後で再印刷する、というものです。
この場合、クライアント側では共有プリンタとする作業が不要なので、PCのセットアップが楽というメリットがあります。
印刷ファイル(DOCXなりPDF)を保存する、ということが必要なので、実用性は微妙かもですが・・・
ダミープリンタドライバのみ登録して、印刷時プロパティの方でプリンタ毎の既設定のボタンをクリック、そして設定に従ってプリンタ・設定の振り分け・・・
ここまでできれば、実務では
1.角2封筒で、印刷は○○○プリンタ
2.A3用紙に中綴じで、フィニッシャ付の複合機○○○
3.普段の印刷は最寄りの小型プリンタ○○○。そしてプロパティ設定をしてから印刷

こんな無駄な野望の第一歩です(笑)

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

Unity内で取得したデータをcsvに書き出す方法

1. はじめに

Unityのゲーム内で取得したデータをcsvに書き出す方法についての記事を書きます。

具体的には、2つのGameObjectの操作記録をcsvに時系列順で書き出すスクリプトを作成します。

2. 準備

2.1. 用意するGameObject

用意するGameObjectは3つです。

  • F … 赤色のGameObject
  • J … 水色のGameObject
  • SaveCsv … EmptyGameObject

aaaaa.PNG

2.2. 用意するスクリプト

用意するはスクリプトも3つです。

  • SampleFScript … F用のスクリプト
  • SampleJScript … J用のスクリプト
  • SampleSaveCsvScript … SaveCsv用のスクリプト

j.PNG

3. スクリプトに記述

3.1. SampleSaveCsvScripのコードの解説

まずは、csvに保存するためのコードをSampleSaveCsvScripに記述します。

3.1.1. void Start()

引用元サイト①で詳しく解説されています。
ざっくりとした説明をすると、新しくcsvファイルを作成して、{}の中の要素分csvに追記をするコードです。

3.1.2. public void SaveData(string txt1, string txt2, string txt3)

「s1」で記述したヘッダーの数分「string txt」を用意してください。
「public」をつけることで他のスクリプトでも「SaveData(~)」が使用できるようになります。
その他については、上記と同じです。

3.1.2. void Update()

Enterキーが押されたらcsvへの書き込みを終了するコードを記述しています。

SampleSaveCsvScrip
using System.IO;
using System.Text;

public class SampleSaveCsvScript : MonoBehaviour
{
    private StreamWriter sw;

    void Start()
    {
        sw = new StreamWriter(@"SaveData.csv", true, Encoding.GetEncoding("Shift_JIS"));
        string[] s1 = { "F", "J", "time" };
        string s2 = string.Join(",", s1);
        sw.WriteLine(s2);
    }

    public void SaveData(string txt1, string txt2, string txt3)
    {
        string[] s1 = { txt1, txt2, txt3 };
        string s2 = string.Join(",", s1);
        sw.WriteLine(s2);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Return))
        {
            sw.Close();
        }

    }
}

3.2. SampleFScriptのコードの解説

SampleFScriptには、Fキーを検出するコードを記述します。

3.2.1. void Start()

引用元サイト②で詳しく解説されています。
ざっくりとした説明をすると、他のスクリプトを参照するコードを記述しています。

3.2.2. void Update()

Fキーが押されたら、csvに「F」と「いつ押されたか」という情報が書き加えられます。

SampleFScrip
using UnityEngine;
using System.IO;

public class SampleFScript : MonoBehaviour
{
    private float time;
    private StreamWriter sw;

    GameObject SaveCsv;
    SampleSaveCsvScript SampleSaveCsvScript;

    void Start()
    {
        SaveCsv = GameObject.Find("SaveCsv");
        SampleSaveCsvScript = SaveCsv.GetComponent<SampleSaveCsvScript>();
    }

    void Update()
    {
        time += Time.deltaTime;

        if (Input.GetKeyDown(KeyCode.F))
        {
            SampleSaveCsvScript.SaveData("F", " ", time.ToString());
        }
    }
}


3.3. SampleJScriptのコードの解説

SampleJScriptには、Jキーを検出するコードを記述します。

3.3.1. void Start()

上記と同じです。

3.2.2. void Update()

Jキーが押されたら、csvに「J」と「いつ押されたか」という情報が書き加えられます。

SampleJScrip
using UnityEngine;
using System.IO;

public class SampleFScript : MonoBehaviour
{
    private float time;
    private StreamWriter sw;

    GameObject SaveCsv;
    SampleSaveCsvScript SampleSaveCsvScript;

    void Start()
    {
        SaveCsv = GameObject.Find("SaveCsv");
        SampleSaveCsvScript = SaveCsv.GetComponent<SampleSaveCsvScript>();
    }

    void Update()
    {
        time += Time.deltaTime;

        if (Input.GetKeyDown(KeyCode.J))
        {
            SampleSaveCsvScript.SaveData(" ", "J", time.ToString());
        }
    }
}


4. 確認

実際にゲームを動かして確認をしてみます(ここは省略)。

すると、下記のようなcsvが出てきます。
asd.PNG
「F」や「J」そして「いつ押されたか」という情報がきちんと書き出されていることが確認できると思います。

5. 最後に

Unityのゲーム内で取得したデータをcsvに書き出す方法について書いてみましたが、いかかだったでしょうか。
分かりづらい点や間違っている点があれば、ご指摘いただけると幸いです。

参考URL

① Unity 2D] データを保存する(外部ファイルCSV)
https://high-programmer.com/2017/12/10/unity-savedata-otherfile/

② [Unity]他のオブジェクトについているスクリプトの変数を参照したり関数を実行したりする。
https://qiita.com/tsukasa_wear_parker/items/09d4bcc5af3556b9bb3a

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

C# をメイン言語で開発している僕が使っている開発環境を紹介する

たぶんみんな同じようなものが多いと思いますが、何か参考になるものがあるかもしれないのでよかったら見ていってください。

開発環境

Visual Studio

Visual Studio 2019 IDE - Programming Software for Windows

  • 最強の IDE. ほぼ全ての開発はこれ。
  • Version ごとに共存可能なので、職場ではプロジェクトによって変えている。
  • 家では何も気にせず最新版の 2019.
  • Project に対しての Git の操作も殆どこれ。
  • Plugin はこんな感じ。元々が最強だからあまり多く入れていない。リンクを張っておく。
    • ReSharper - Visual Studio Marketplace
      • Visual Studio がイケてない時代から使っている。有料。
      • とりあえず Alt + Enter を押せば大体いい感じにしてくれる。
      • たしか 30 Days は Free なので、使ってみて価格分の価値を見出だせたら購入するのがよいかも。
    • Power Commands for Visual Studio - Visual Studio Marketplace
      • Format document on save と Remove and Sort Usings on save くらいしか使っていない。
      • 100% ではないけど、この 2 つがあれば大体 Style guide は保てる。イケてない Prettier みたいなイメージ。

Visual Studio Code

Visual Studio Code - Code Editing. Redefined

  • 最強の Editor. 最強なのに軽い。 Visual Studio で作る Solution 以外は全部これ。
  • Qiita の記事とか Markdown を書くときも使っている。
  • Front-end の開発もこれ。
  • Git も殆どこれ。
  • Linux Server への SSH 接続とか Docker 接続もこれ。
  • たぶん一番触っている時間が長い。
  • 最近は Visual Studio XXXX で Google 検索すると、こっちが出てくるくらいみんな使っている。
    • Visual Studio が検索しづらいので、正直に言うと名前を変えてほしい。
  • C# と同じく、死ぬまでに一度は Contribute したい。
    • どうすればよいかよい案を持っている人いたら教えて下さい。がんばります。
  • Plugin が結構インストールしているからいくつかオススメをピックアップする。殆ど有名なやつかも。
    • Code Spell Checker - Visual Studio Marketplace
      • Spell が怪しいやつに波線をつけてくれる。日本語はチェックしてくれない。
      • 余談だが Qiita という Word にも波線がつく。
    • EditorConfig for VS Code - Visual Studio Marketplace
      • EditorConfig という有名な設定ファイルを動かすやつ。
      • タブのインデントや最終行の改行、空白のトリミングなどをしてくれる。
    • GitLens — Git supercharged - Visual Studio Marketplace
      • Git を見やすくしてくれる。ファイルの履歴や行の履歴などもすぐ出せる。
      • とりあえず入れておいたほうがよい。
    • Live Server - Visual Studio Marketplace
      • 右下のアイコンをクリックするだけでローカルサーバーが立ち上がる。
      • 素の HTML とか JS を書いているときに、ブラウザで見たいときに使う。
    • Live Share - Visual Studio Marketplace
      • リモート先の相手の画面を見ながら一緒にコーディングできる、たぶん。
      • Screen share をしながらペアプロをするときに使う、予定。
      • 開発フレンドがいないので使ったことはない。
    • Paste JSON as Code - Visual Studio Marketplace
      • JSON を色んな言語の Schema に変換してくれる。
      • TypeScript で開発しているときに一度だけ使った。
    • Prettier - Code formatter - Visual Studio Marketplace
      • Prettier という有名なコードキレイキレイツールを動かすやつ。
      • 一番よく使う Plugin. というより、保存時に勝手に動くようにしている。
      • Markdown も Format してくれて、一番よいのが日本語と英単語の間にスペースを入れてくれるところ。
      • キチンと言語ごとに設定しないと、既存のソースコードをキレイキレイして Git Diff がやばいことになるから気をつけて。
    • Remote Development - Visual Studio Marketplace
      • Linux に接続したりするのに使っている。
      • Remote SSH, Containers など全部入りのやつ。
    • Settings Sync - Visual Studio Marketplace
      • Visual Studio Code の設定ファイルを GitHub Gist で管理できて、複数端末で設定を同期できる。
      • Windows と Mac を使っている僕でも正しく使えている。

IntelliJ IDEA

IntelliJ IDEA:JetBrains によるプロ開発者向け Java IDE

  • 職場から Java や PHP を強いられたときに使っている。これも最強だけど C# では使わない。

DataGrip

DataGrip:データベースおよび SQL 用の JetBrains 製クロスプラットフォーム IDE

  • Database 管理用アプリケーション。 IntelliJ についてきたから使っている。
  • 色んな種類の Database に接続できるから便利。

Management Studio

SQL Server Management Studio (SSMS) のダウンロード - SQL Server Management Studio (SSMS) | Microsoft Docs

  • SQL Server を Monitoring したいときとか、管理系の特別なことをしたいときに使っている。

Git Bash

Git for Windows

  • Windows はコマンドプロンプトが死んでいるので、これがないと生きていけない。
  • Power shell がいらないときの CLI は殆どこれ。

Postman

Postman | The Collaboration Platform for API Development

Sourcetree

Sourcetree | Free Git GUI for Mac and Windows

  • Git History が見やすいので使っている。 Commit などは Visual Studio を使っているので、ほぼ History を見る専門。

Adobe XD

Adobe XD 体験版ダウンロード | UI/UX デザインと共同作業ツール

  • 画面デザインやワイヤーフレームを作るときに使っている。色々出来て今どきっぽくてイケてて便利。
  • 共同作業などをしなければ Free で使える。

draw.io

draw.io

  • 構成図を描くときに使っている。アイコンもたくさんあるし、線も繋げやすい。
  • 今まで紹介してきたやつと違い、ブラウザで動く。

Excel

リンクなし。

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

UniRxとUniTask 相互変換の変わったパターン紹介

はじめに

UniRxのObservableと、UniTaskはそれぞれ相互変換することができます。
今回はその中でも少し変わった変換パターンを紹介したいと思います。

System.Linq UniRx UniRx.Async のusingを忘れずに

IEnumerable<UniTask<T>> -> IObservable<T>

複数のUniTask<T>をまとめて、1つのIObservable<T>にする方法です。
やり方が何パターンかあります。

// 対象
IEnumerable<UniTask<string>> tasks = CreateSample();

// ---

// 並列にまとめる(要素の順序を無視して、終わったものから結果を返す)
IObservable<string> parallel = tasks
    .Select(x => x.ToObservable()) // IE<IO<T>>
    .Merge(); // IO<T>

// 直列にまとめる(要素の先頭から順番に結果を返す)
IObservable<string> sequential = tasks
    .Select(x => x.ToObservable()) // IE<IO<T>>
    .Concat(); // IO<T>


// 全部終わってからまとめて結果をとるなら(IO<IE<T>>)
IObservable<IList<string>> whenAll = tasks
    .Select(x => x.ToObservable()) // IE<IO<T>>
    .Zip(); // IO<IList<T>>

// でも、全部まとめてとるならUniTask.WhenAllでいいのでは?
UniTask<string[]> whenAll2 = UniTask.WhenAll(tasks);

UniTask<IEnumerable<T>> -> IObservable<T>

さっきとネストの仕方が逆のパターン。
UniTask<IE<T>>を分解して1つのIObservable<T>にする方法です。
2パターンあるけど結果は同じです。

// 対象
UniTask<IEnumerable<string>> task = CreateSample();

// パターン1
IObservable<string> p1 = task
    .ToObservable() // IO<IE<T>>
    .Select(x => x.ToObservable()) // IO<IO<T>>
    .Merge(); // IO<T>

// パターン2
IObservable<string> p2 = task
    .ToObservable() // IO<IE<T>>
    .SelectMany(x => x.ToObservable()); // IO<T>

IObservable<T> -> UniTask<IEnumerable<T>>

IObservable<T>が発行するすべてのメッセージを「まとめて」待ち受けるUniTask<T>を作りたい場合。

// 対象
IObservable<string> observable = CreateSample();

// ToArray()してからToUniTask()でOK
UniTask<string[]> task = observable.ToArray().ToUniTask();

IObservable<UniTask<T>> -> IObservable<T>

ObservableUniTaskを扱う場合に、それを1つのObservableにまとめる。

// 対象
IObservable<UniTask<string>> observable = CreateSample();

// 並列(終わった順に結果を出すなら)
IObservable<string> parallel = observable.SelectMany(x => x.ToObservable());

// 直列(もとのIO<T>から発行された順序を維持するなら)
IObservable<string> sequential = observable.Select(x => x.ToObservable()).Concat();

IObservable<UniTask<T>> -> UniTask<IEnumerable<T>>

ObservableUniTaskを扱う場合に、それをUniTask側にまとめる。

IObservable<UniTask<string>> observable = CreateSample();

// 結果は先に終わったUniTaskの順番になる
UniTask<string[]> task = observable
    .SelectMany(x => x.ToObservable()) // IO<IO<T>>
    .Merge() // IO<T>
    .ToArray() // IO<T[]>
    .ToUniTask(); // UniTask<T[]>

UniTask<IObservable<T>> -> IObservable<T>

UniTaskの中にObservableが入り込んじゃった場合。

// 対象
UniTask<IObservable<string>> task = CreateSample();

// taskをIO<IO<T>>に変換してからMerge()
IObservable<string> observable = task.ToObservable().Merge();

UniTask<IObservable<T>> -> UniTask<IEnumerable<T>>

UniTaskの中にObservableが入り込んじゃったものを、今度はUniTask側にまとめる場合。

// async/await使っちゃうのが楽
private async UniTask<IEnumerable<string>> Unwrap(UniTask<IObservable<string>> task)
{
    var observable = await task;
    return await observable.ToArray();
}

IObservable<IObservable<T>> -> UniTask<IEnumerable<T>>

こんなシチュエーションあるのかよくわからないけど。

IObservable<IObservable<string>> observable = CreateSample();

UniTask<string[]> task = observable
    .SelectMany(x => x) // IO<T>
    .ToArray() // IO<T[]>
    .ToUniTask(); // UniTask<T[]>

まとめ

UniRxUniTaskはだいたいどんなパターンでもそれぞれに変換することができます。
両者を組み合わせて使い、必要に応じて変換をかけるとよいでしょう。

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

C# 別のフォームのコントロールを操作する InvaildOperationExceptionはなぜ発生するのか?

前置き

「別のフォームから、メインフォーム(他)のコントロールを操作したい!」
ってことありませんか?

例えば、Form2のボタンをクリックするとForm1のラベルの文字を変更するようにしたい時。
image.png

ここで「ラベルのアクセス修飾子をPublicにして、インスタンス生成して変更してやればいいんじゃないの?」と考えるかもしれません。
ですが、それではダメです。
実際に、やってみると...

sld0k-ax4m2.gif

変わりません。※注1
なぜでしょうか?

ここで、Form1の非同期メソッドから、ラベルの文字を変更してみます。

private async void button1_Click(object sender, EventArgs e)
{
    await Task.Run(() => 
    {
        label1.Text = "非同期実行中";
    });
}

ewne2-ccfxm.gif

落ちてしまいました。
発生している例外は、InvaildOperationExceptionですね。


コメント欄よりご指摘より追記:

※注1... インスタンスは存在していますが、Show()していないゴーストフォーラムに対してラベルの変更命令を送りつけているのが原因です。
Form1のインスタンスを受け取り、そのインスタンスに対してラベルの変更命令を送るのが正しい例です。


原因

上記2つの例
・ラベルをインスタンスから参照
・非同期関数から参照
どちらにも共通している点があります。

それぞれ別のスレッドで動作しているということです。
別スレッド同士で無効な参照を行っていたのが原因です。

crossthread.png

解決方法

ここでは、2つの解決方法を紹介します。

・デリゲートの定義/呼び出し
・メソッドのInvoke

デリゲートを定義して呼び出し

Form1にデリゲートを定義して、該当デリゲートをCallする方法です。

Form1.cs
public partial class Form1 : Form
{
    //デリゲートの定義
    public delegate void ChangeTextDelegate(String text);
    public ChangeTextDelegate del;

    //コンストラクタ
    public Form1()
    {
        InitializeComponent();
        //ハンドルが作成されていなければ作成。Invokeする際に必要です。
        if (!IsHandleCreated) CreateHandle();
        //デリゲートに委任する処理(関数)を定義。
        del = ChangeText;
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //Form2のインスタンスを定義し、自身のインスタンスを渡します。
        Form2 form2 = new Form2(this);
        form2.Show();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Run(() => 
        {
            //label1.Text = "非同期実行中"; だめ
            //del("sometext"); これもだめ
        });
    }

    public void ChangeText(String text)
    {
        label1.Text = (text);
    }
}
Form2.cs
public partial class Form2 : Form
{
    Form1 form1;
    public Form2(Form1 __form1)
    {
        InitializeComponent();
        if (!IsHandleCreated) CreateHandle();
        form1 = __form1;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        //form1.label1.Text = "something"; ok

        //form1.Invoke(form1.del, "sometext"); ok | Invokeでもデリゲートを呼べる
        form1.del("sometext"); //ok
    }
}

メソッドをInvoke

Form1のインスタンスから単純に関数をInvokeする方法です。

public partial class Form1 : Form
{
    //コンストラクタ
    public Form1()
    {
        InitializeComponent();
        //ハンドルが作成されていなければ作成。Invokeする際に必要です。
        if (!IsHandleCreated) CreateHandle();
    }

    private void Form1_Load(object sender, EventArgs e)
    {
        //Form2のインスタンスを定義し、自身のインスタンスを渡します。
        Form2 form2 = new Form2(this);
        form2.Show();
    }

    private async void button1_Click(object sender, EventArgs e)
    {
        await Task.Run(() => 
        {
            //label1.Text = "非同期実行中"; だめ
            //del("sometext"); これもだめ
        });
    }

    public void ChangeText(String text)
    {
        label1.Text = (text);
    }
}
public partial class Form2 : Form
{
    Form1 form1;
    public Form2(Form1 __form1)
    {
        InitializeComponent();
        if (!IsHandleCreated) CreateHandle();
        form1 = __form1;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        //form1.label1.Text = "something"; ok

        //関数をInvoke
        form1?.Invoke(new Action<String>(form1.ChangeTextA), "sometext");
    }
}

Asyncからの呼び出し

asyncからの呼び出しも、デリゲートをInvokeするか、関数を直接Invokeすることで
ラベルの文字列を変更できます。
注意すべき点は、デリゲートを直接呼ぶことはできないということです。
asyncは別スレッドで実行される為、Form1で定義されている変数delにアクセスできないからです。

private async void button1_Click(object sender, EventArgs e)
{
    await Task.Run(() => 
    {
        //label1.Text = "非同期実行中"; だめ
        //del("sometext"); これもだめ | デリゲートは直接呼べない
        this.Invoke(del, "sometext"); //ok
        this.Invoke(new Action<String>(ChangeText), "sometext2"); //ok
    });
}

メリット・デメリット

・デリゲート経由での呼び出し
→冗長なコードになりがち。

・Invoke
→最小限のコードで済む。

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

【C#】バグを生みづらくする値型、列挙型のルール付け

グレンジ Advent Calendar 2019 22日目を担当させていただきます ぐっち と申します。
新卒一年目で現在グレンジでUnityを用いてゲーム開発を行っています。

◆ 背景

エラーも出ず意図したメソッドもちゃんと通っているのになぜか挙動がおかしくなる。。。
とコードを追う時間を要してしまうことが多々ありました。
そこで原因の1つとなっているのが デフォルト値であったり初期化時のミス です。
今回はそういったことを回避するための自分のいるチームでもあるルールの1つを紹介します。

◆ 0は有効な値にし、不正値は -1 と統一する

.NETによるデフォルトの初期化は 全て「0」に設定 されます。
明示的に初期値を設定していない限り基本的に0スタートとなるので、例えばカウント処理を行いたい場合などインスタンス化しちゃえばそこから意図通りカウントアップしてくれます。
列挙型も同じで、値を明示的に指定しなければ0から始まり次の項目に行く毎にインクリメントされます。

では、以下の場合はどうでしょう?

Weekday.cs
public enum Weekday {
    Sunday = 1,
    Monday = 2,
    Tuesday = 3,
    Wednesday = 4,
    Thursday = 5,
    Friday = 6,
    Saturday = 7,
}

private Weekday weekday = new Weekday();

このような書き方をした場合、weekdayの初期値はどうなるでしょうか?
答えは、 無効な値(バグの原因) となります。理由は簡単、0の定義がないからです。
この場合は極端な例で、無効な値なのですぐに気づくことができます。
単純な話、これを解決する方法として 0を未初期化の値(入っていても問題ない更新可能な値) としていれておきます。

Weekday.cs
public enum Weekday {
    None = 0,
    Sunday = 1,
    Monday = 2,
    Tuesday = 3,
    Wednesday = 4,
    Thursday = 5,
    Friday = 6,
    Saturday = 7,
}

private Weekday weekday = new Weekday();

無事、weekdayには0が入り、有効な値となります。
ここで問題となってくるのが、 例外処理 です。
何かが問題で正常に処理が行われなかった時は例外の値を入れエラーを出したいです。
ではWeekdayの例外の値とはどれでしょうか?
Noneにしますか?した場合これは 未初期化の値(入っていても問題ない更新可能な値) として入れているため、エラーとはならずただただ値を待ち続け意図しない結果になるでしょう(関係ない場所でnullエラーとなり根本原因が解決できなかったり...など)

そこで、新たな値「 不正値 」を用意します。

Weekday.cs
public enum Weekday {
    Invalid = -1,
    None = 0,
    Sunday = 1,
    Monday = 2,
    Tuesday = 3,
    Wednesday = 4,
    Thursday = 5,
    Friday = 6,
    Saturday = 7,
}

private Weekday weekday = new Weekday();

こうすることで、
・-1なら例外処理をする
・0なら初期値もしくは初期化されてない値だから初期化されるのを待つ
・それ以外なら正常に使用できる値

として分類することができ、それぞれに適した処理を行うことができます。

◆ まとめ

・0はデフォルトで設定される値のため、有効な値として使用する
・不正値、初期値(または未初期化値)、正常値と分ける(混ぜて使わない)
・可能な限り全てのオブジェクトに対する0をデフォルト値(未初期化値)として扱う

これをするだけで初期値による不具合や設定された値によってバグが追いづらくなるといったことはかなり減らすことができると思います。
しかし、完全ではありません。enumへのフラグ付けやビット演算などさらに色々な工夫をすることもできるので、クライアントエンジニアとしてこれからもエラーのリスクを最小化できる方法を追求していきたいと思います。

参考

More Effective C# 6.0/7.0

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

30分で作る初めてのUnity

はじめに

普段はWeb周りの技術を扱ってばかりなので、
今回は以前から興味があったUnityを触ってみました。

この記事は、

なんでもいいから一度Unityを使ってプロジェクトを作ってみたい。

という方向けに、プロジェクトの作成からBuildまでの流れを掴んでもらう為に書いたものです。
ですので、数あるUnity機能の中からほんの数個ピックアップして追加したのみにすぎません。
ご了承ください。

目次

  1. イントール
  2. プロジェクトの作成
  3. レイアウトの変更
  4. オブジェクトの作成
  5. オブジェクトに装飾
  6. オブジェクトに動きを与える
  7. プロジェクトをBuild

1. イントール

まずはUnityをダウンロードしましょう。
個人開発であれば無料です。
→ ダウンロード画面へ
(*途中のアカウント作成等は割愛させていただきます。)

スクリーンショット 2019-12-22 16.02.23.png

2. プロジェクトの作成

インストールしたApplicationを開くと以下の画面が現れるので、Newを選択します。

スクリーンショット 2019-12-22 16.06.02.png

Template3D にしてプロジェクトを作成します。
スクリーンショット 2019-12-22 16.18.34.png

3. レイアウトの変更

こちらがプロジェクトのデフォルト画面です。

スクリーンショット 2019-12-22 16.22.30.png

自分の作業しやすいレイアウトに変更してください。
私は2by3にしています。

スクリーンショット 2019-12-22 16.27.32.png

4. オブジェクトを作成

ゲームオブジェクト(以下オブジェクト)を作成します。
HierarchyCreate3DオブジェクトCube の順で選択してください。

すると画面左のSceneCubeが現れます。

スクリーンショット 2019-12-22 16.31.55.png

オブジェクトの形を変えるために画面右上のInspectorを下記のように編集します。

スクリーンショット 2019-12-22 16.41.28.png

オブジェクトの形が変わりました。
スクリーンショット 2019-12-22 16.42.28.png

この調子で Squareも追加してみます。

スクリーンショット 2019-12-22 16.44.05.png

5. オブジェクトに装飾

CubeSquareに色を与えましょう

ProjectMaterialsを選択

スクリーンショット 2019-12-22 16.46.38.png

画面を見やすくするために 右上設定(三本の棒線)からOne Column Layoutを選択します。

スクリーンショット 2019-12-22 16.47.11.png

Material内のスポイトのようなアイコンから色を選択します。

スクリーンショット 2019-12-22 16.49.34.png

この調子でもう1色作成します。

そして、今作成したMaterialは対象のオブジェクトにドラッグ&ドロップできます。
(Scene, Hierarchyどちらでも可)

スクリーンショット 2019-12-22 16.51.43.png

6. オブジェクトに動きを加える

次はSquareに動きをつけられるようにします。

まず、SquareInspectorからRigidBody(オブジェクトに重力を付与)を追加します。

スクリーンショット 2019-12-22 17.09.41.png

次に、Add Componentを選択します。
最下部にあるNew Scripを選択、名前を決めてScriptを作成します。

スクリーンショット 2019-12-22 16.55.56.png

スクリーンショット 2019-12-22 16.56.21.png

作成されたScriptをダブルクリックするとEditorが開くはずです。

スクリーンショット 2019-12-22 16.58.56.png

以下、カーソルを押した方向にSquareを動かすようにするScriptです。

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class moveScriot : MonoBehaviour
{
    Rigidbody rigidbody;

    // Start is called before the first frame update
    void Start()
    {
        rigidbody = GetComponent<Rigidbody>();
    }

    // Update is called once per frame
    void Update()
    {
        float moveH = Input.GetAxis("Horizontal");
        float moveV = Input.GetAxis("Vertical");
        Vector3 move = new Vector3(moveH, 0, moveV);
        rigidbody.AddForce(move);
    }
}

これでSquareが動くようになったと思います。

ezgif.com-video-to-gif.gif

画面左下に見えているGameが実際にユーザーがゲームを遊ぶときの視点です。
わかりやすくするために、Main CameraTransFormを調整してください。

スクリーンショット 2019-12-22 18.07.33.png

7. プロジェクトをBuildする

最後にこのプロジェクトをBuildしてみましょう。

FileBuild Settingsを選択

スクリーンショット 2019-12-22 18.11.57.png

こちらでプラットフォームを選択することができます。
今回はMacOSのまま進むのでPlatformはそのままで問題ありません。
右上のAdd Open Souceから現在のScenesを追加して、
最後にBuildを選択してください。
スクリーンショット 2019-12-22 20.54.56.png

デスクトップに作成したゲームが出現します。

スクリーンショット 2019-12-22 18.14.06.png

これでいつでも先程のGameを開くことができるようになりました。

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

【C#】Linqを活用したオブジェクト指向+関数型プログラミングの融合と調和

初めての方は初めまして。以前からご覧頂いている方はお久しぶり。nqdiorことDELLです。
[C# その2 Advent Calendar 2019]-23日目の投稿です。

今回はもはやC#になくてはならない相棒「Linq」を最大限活用するにはどう用いるか、という自分なりの答えのひとつ(令和1stジェネレーション)を書いてみたいと思います。関数型プログラミングの定義は広義ですので、今回は一旦「高階関数(Linq)を使うプログラミング」という前提でお話します。

1. どんな目的のプログラム?

データベーステーブルからデータをインプットし、縦横結合・検索・集計を60ステップほど行って、50近いフィールドを持つテーブルにアウトプットするプログラムを書いていました。

C#上でのINPUTとOUTPUTのイメージは以下の通り。
どちらもデータの持ち方はIEnumerable<T>(T...ClassType)です。

  • INPUT ... オブジェクトにマッピングしたデータ(ORマッパーはDapperを採用)
  • OUTPUT ... 出力データが格納されたテーブル書き込み用のオブジェクト

構築にあたり自分が目指したのは、以下の3点です。

  • (当然ですが)出力結果が正しいこと
  • IOを除くメモリ上での処理をできるだけ0secに近付けること
  • 長期運用される恐れを考慮し、メンテナンスしやすいコードを書くこと

2. なにが課題なの?

普通にコードを組むならば、以下の3点に集約されると思います。

  • Linqを用いて検索や集計を行う
  • 面倒な処理はループさせた中でゴリゴリ記述
  • 処理単位で目的ごとにクラス・メソッドを切り分け

ただ、この方法だと以下の課題があると考えていました。

  • 各クラスでオブジェクトのプロパティに対して変更を行うため、どこのクラスがどのプロパティを担当しているか不明瞭
  • 高階関数(Linq)と通常関数(foreach)が混じってしまうため、foreachの中でLinqを使用した日にはn*nのループが発生
  • メンテナンスによってクラス・メソッドが肥大化

3. それでどうしたの?

1. オブジェクト側

複数ステップのまとまりごとに出力結果を保持する中間エンティティを作成しました。
マッピングオブジェクト → [中間エンティティ(複数)] → 結果オブジェクト

中間エンティティに作成するプロパティは以下の2種類です。
1. 入力側プロパティ ... 出力側の値算出に必要な、入力側のプロパティ。readonly属性を付与し、コンストラクタで値設定。
2. 出力用プロパティ ... 入力側になく、出力側にのみある項目のプロパティ。getterのみ保持。

出力用にプロパティ(フィールド)に関しては、ほぼすべてカプセル化を行い、入力側プロパティが変更されたら自動的にgetter側で出力用フィールドの値が書き換わるように実装の隠蔽を行います。

2. コード側

上記オブジェクトに対してLinq側でUNION, JOIN, Where, GroupByを行い、入力側の値を変更します。
出力側の値をLinqの射影(Select)で、次ステップの中間エンティティの入力側にマッピングします。

各プロパティの設定に必要な細かい計算は、全てオブジェクト側に委任します。

3. イメージ図

image.png
※図では処理ごとに中間エンティティを持っていますが、複数処理を跨いで中間エンティティを触っても問題ありません。

4. なにが嬉しい?

  • オブジェクトそのものがカプセル化された状態となる
  • 実装の隠蔽を用いたオブジェクト指向のメリットが使える
  • Linqの射影を用いて一括で入力側プロパティを設定し、必要なだけ出力側プロパティが計算される遅延評価と相性がいい
  • コードの総量が相当に減り、可読性が向上する

つまり、関数型プログラミングでオブジェクト指向のオブジェクトを用いるという感じでしょうか。

5. 実装例見せてよ

商品購入履歴と各割引率が入力されたとき、顧客ごとの税込請求金額合計を求めるコードを書きました。

オブジェクト側は入力値が設定されたら割引後価格が算出され、割引後価格が算出されたら税込み価格が算出されます。
入力値によって各プロパティが連鎖的に自動計算される形です。オブジェクト指向のカプセル化ですねー。

コード側は、計算クラス見てもらって分かる通り、処理単位であるLinq単位で1メソッドとなっています。「関数型ぽい」ですね!

1. 入出力管理クラス

namespace ConsoleApp2
{
    // 入力用クラス
    internal class InputClass
    {
        internal int CustomerID { get; set; }

        internal long ProductID { get; set; }

        internal double DiscountRate { get; set; }
    }

    // 出力用クラス
    internal class OutputClass
    {
        internal int CustomerID { get; set; }

        internal int TotalPrice { get; set; }
    }
}

2. 商品マスタクラス

using System.Collections.Generic;

namespace ConsoleApp2
{
    // 商品クラス
    internal class Product
    {
        internal long ProductID { get; set; }

        internal int Price { get; set; }
    }

    // 商品マスタ
    internal class Products : List<Product>
    {
        internal Products()
        {
            Add(new Product() { ProductID = 10000000001, Price = 100 });
            Add(new Product() { ProductID = 10000000002, Price = 200 });
            Add(new Product() { ProductID = 10000000003, Price = 300 });
            Add(new Product() { ProductID = 20000000004, Price = 1000 });
            Add(new Product() { ProductID = 20000000005, Price = 2000 });
            Add(new Product() { ProductID = 20000000006, Price = 3000 });
        }
    }
}

3. スタートアップオブジェクト

using System.Collections.Generic;
using System.Linq;

namespace ConsoleApp2
{
    class Program
    {
        static void Main(string[] args)
        {
            // 入力
            var inputs = getData();

            // 出力
            var outputs = new Calculator().Calculation(inputs);
        }

        // DBからデータを取得。今回は手打ち。
        static IEnumerable<InputClass> getData()
        {
            var input = new List<InputClass>();

            input.Add(new InputClass { CustomerID = 1, ProductID = 10000000001, DiscountRate = 0.88 });
            input.Add(new InputClass { CustomerID = 1, ProductID = 10000000002, DiscountRate = 0.19 });
            input.Add(new InputClass { CustomerID = 1, ProductID = 10000000003, DiscountRate = 0.52 });
            input.Add(new InputClass { CustomerID = 2, ProductID = 20000000004, DiscountRate = 0.33 });
            input.Add(new InputClass { CustomerID = 2, ProductID = 20000000005, DiscountRate = 0.56 });
            input.Add(new InputClass { CustomerID = 3, ProductID = 20000000006, DiscountRate = 0.75 });

            return input;
        }
    }

    // 計算クラス
    internal class Calculator
    {
        // 計算を実施する。
        internal IEnumerable<OutputClass> Calculation(IEnumerable<InputClass> inputs)
        {
            // 商品マスタの取得
            var products = new Products();

            // 中間エンティティを用いて1レコードごとの価格を計算する。
            var middles = JoinAndCalcProductPrice(inputs, products);

            // 顧客ごとの集計と同時に結果オブジェクトに変換する。
            var results = SumPriceAndConvertToOutputs(middles);

            return results;
        }

        // 商品マスタから価格を取得する。計算はオブジェクト側で行う。
        privete IEnumerable<MiddleClass> JoinAndCalcProductPrice(IEnumerable<InputClass> inputs, IEnumerable<Product> products) => inputs
        .Join(
            products,
            i => i.ProductID,
            p => p.ProductID,
            (input, product) => new MiddleClass
            {
                CustomerID = input.CustomerID,
                DiscountRate = input.DiscountRate,
                Price = product.Price
            }
        );

        // 顧客ごとの金額合計を取得して出力クラスにマッピングする。
        private IEnumerable<OutputClass> SumPriceAndConvertToOutputs(IEnumerable<MiddleClass> middles) => middles
        .GroupBy(
            c => c.CustomerID
        )
        .Select(c => new OutputClass
        {
            CustomerID = c.Key,
            TotalPrice = c.Sum(s => s.Price)
        });
    }

}

4. 中間クラス

namespace ConsoleApp2
{
    internal class MiddleClass
    {
        // 税率
        internal const double _tax = 0.1;

        // 入力側プロパティ ▽
        internal int CustomerID;

        internal int Price;

        internal double DiscountRate;

        // 出力用プロパティ ▽
        // 入力値が設定されていれば参照時にこの値は決定する。大きいプログラムだとこのプロパティが増えていく。

        // 後々使う可能性があるので割引後価格は単独で持つ。
        private int DiscountPrice => Price - (int)(Price * DiscountRate);

        // 計算結果を持つプロパティ。入力が変われば割引後価格が変わり、計算結果も連動して変わる。
        internal int CalculatedPrice => DiscountPrice + (int)(discountPrice * _tax);
    }
}

6. おわりに

プログラムを長期メンテナンスしていくことが想定される場合は、いかに短いコード量で把握しやすいようシンプルに書けるか。
また、カプセル化による切り分けで楽にメンテナンスできるかを想定して書くことが必要になるかと思います。

今回のコードが完成形とは思っていませんが、ひとつの着地点として引き続き考えていきたいと思います。
ご意見ご感想等あればお気軽にコメントお願い致します!

7. 参考

平凡なプログラマにとっての関数型プログラミング - anopara

LINQでの外部結合の方法が載っています。ぜひご参照ください。
LINQでの内部結合・外部結合

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

OculusQuest ハンドトラッキングSDKから、指Boneの情報を取得し分析する

最近のOculusQuestさんちょっと攻めすぎですよね。
OculusLinkのβが出たのもつい最近な気がしているのに、さらにハンドトラッキングまで!!!

そして、2019/12/20にはUnityのAssetStoreにハンドトラッキング対応のSDKが配布されました。 高速感!

導入方法なんかについては他の方の記事にお任せして・・・。

肝はOVRSkelton

OVRHand には GetFingerIsPinching()GetFingerPinchStrength() があり、それぞれ、ピンチ(つまんでいる)状態かどうかと
つまんでいる力(距離?)が取得できるメソッドが入っています。
参考:OculusQuestのハンドトラッキングについて色々調べてみた

しかし、ぱっと見たところそれ以外の情報取得がそんなにありません。

そこで上記記事でも言及していますが、もっと詳しい情報を知りたい場合は OVRSkelton.Bones にボーン情報が入っている のでそれを使うことにします。

というのも

僕がやりたかったのは以前作った 「空中に図形を描いて物体を生成するアプリを作ってみた」 のハンドトラッキング版だったので、空中に絵を描くイメージです。
みなさんが空中に絵を描く(例えば、誰かに「**って漢字どう書くんだったっけ?」と聞かれた)時に指をどんな形にするかっていうと

十中八九:point_up:←これですよね

この「人差し指だけを伸ばして、他曲げている」をGetFingerIsPinching()GetFingerPinchStrength()でやるのはちょっと無理があります。(できなくはないと思いますが)

そこで、 OVRSkelton.Bones から情報を抜き出して簡単に分析することにしました。

Bone情報取得

OVRSkelton.Bones は IList(Readonly)で、何番目にどの情報が入っているかは以下のようにenumで宣言されています。

    public enum BoneId
    {
        Invalid                 = OVRPlugin.BoneId.Invalid,

        Hand_Start              = OVRPlugin.BoneId.Hand_Start,
        Hand_WristRoot          = OVRPlugin.BoneId.Hand_WristRoot,          // root frame of the hand, where the wrist is located
        Hand_ForearmStub        = OVRPlugin.BoneId.Hand_ForearmStub,        // frame for user's forearm
        Hand_Thumb0             = OVRPlugin.BoneId.Hand_Thumb0,             // thumb trapezium bone
        Hand_Thumb1             = OVRPlugin.BoneId.Hand_Thumb1,             // thumb metacarpal bone
        Hand_Thumb2             = OVRPlugin.BoneId.Hand_Thumb2,             // thumb proximal phalange bone
        Hand_Thumb3             = OVRPlugin.BoneId.Hand_Thumb3,             // thumb distal phalange bone
        Hand_Index1             = OVRPlugin.BoneId.Hand_Index1,             // index proximal phalange bone
        Hand_Index2             = OVRPlugin.BoneId.Hand_Index2,             // index intermediate phalange bone
        Hand_Index3             = OVRPlugin.BoneId.Hand_Index3,             // index distal phalange bone
        Hand_Middle1            = OVRPlugin.BoneId.Hand_Middle1,            // middle proximal phalange bone
        Hand_Middle2            = OVRPlugin.BoneId.Hand_Middle2,            // middle intermediate phalange bone
        Hand_Middle3            = OVRPlugin.BoneId.Hand_Middle3,            // middle distal phalange bone
        Hand_Ring1              = OVRPlugin.BoneId.Hand_Ring1,              // ring proximal phalange bone
        Hand_Ring2              = OVRPlugin.BoneId.Hand_Ring2,              // ring intermediate phalange bone
        Hand_Ring3              = OVRPlugin.BoneId.Hand_Ring3,              // ring distal phalange bone
        Hand_Pinky0             = OVRPlugin.BoneId.Hand_Pinky0,             // pinky metacarpal bone
        Hand_Pinky1             = OVRPlugin.BoneId.Hand_Pinky1,             // pinky proximal phalange bone
        Hand_Pinky2             = OVRPlugin.BoneId.Hand_Pinky2,             // pinky intermediate phalange bone
        Hand_Pinky3             = OVRPlugin.BoneId.Hand_Pinky3,             // pinky distal phalange bone
        Hand_MaxSkinnable       = OVRPlugin.BoneId.Hand_MaxSkinnable,
        // Bone tips are position only. They are not used for skinning but are useful for hit-testing.
        // NOTE: Hand_ThumbTip == Hand_MaxSkinnable since the extended tips need to be contiguous
        Hand_ThumbTip           = OVRPlugin.BoneId.Hand_ThumbTip,           // tip of the thumb
        Hand_IndexTip           = OVRPlugin.BoneId.Hand_IndexTip,           // tip of the index finger
        Hand_MiddleTip          = OVRPlugin.BoneId.Hand_MiddleTip,          // tip of the middle finger
        Hand_RingTip            = OVRPlugin.BoneId.Hand_RingTip,            // tip of the ring finger
        Hand_PinkyTip           = OVRPlugin.BoneId.Hand_PinkyTip,           // tip of the pinky
        Hand_End                = OVRPlugin.BoneId.Hand_End,

        // add new bones here

        Max                     = OVRPlugin.BoneId.Max
    }

親指:Thumb
人差し指:Index
中指:Middle
薬指:Ring
小指:Pinky

で、1,2,3 は関節を表しており、数字が大きくなるほど指先に近づいていく ようです。
僕は英語をロクに読まず 「第一関節」= 1 だと思い込んで処理を書いていたらさっぱり正しい値が返ってこなくてハテ? となりました。ご注意を・・・。
そして、 Tip とついているのは指先です。

分かりやすくマッピングしてみると、どうもこんな感じ・・・?
image.png

なお、Pinky と Thumb だけ 0番があります。

そして、この Bone には Transform が入っており、位置や回転が取れそうです。
例えば「人差し指の先端の位置」を取るには

var indexTipPos = skeleton.Bones[(int) OVRSkeleton.BoneId.Hand_IndexTip].Transform.position;

こうなります(注:enumなのでintキャストが必要)

直線判定

以上を踏まえ、:point_up:この「人差し指がまっすぐになっていて、中指、薬指、小指は曲がっている」を判定します。(親指は除きました。)

この「人差し指がまっすぐになっている」というのは人差し指の「第三関節→第二関節」の方向(ベクトル)と、「第二関節→第一関節」の方向(ベクトル)と「第一関節→指先」の方向(ベクトル)が大体同じ方向を向いている ということです。

この「二つのベクトルが大体同じ方向を向いている」=「どれぐらい似通っているか」を表すのは。そう内積(Vector3.Dot)です。

ベクトルの内積は直角(一番似通っていない)な場合は0
全く同じ場合は+1
全く逆方向の場合は-1です

これを人差し指だけではなく他の指の分もベタ書きするとそこそこ長くなってしまうので、以下のようなメソッドを用意すると便利だと思います。

        [SerializeField]
        private OVRSkeleton _skeleton; //右手、もしくは左手の Bone情報

        /// <summary>
        /// 指定した全てのBoneIDが直線状にあるかどうか調べる
        /// </summary>
        /// <param name="threshold">閾値 1に近いほど厳しい</param>
        /// <param name="boneids"></param>
        /// <returns></returns>
        private bool IsStraight(float threshold, params OVRSkeleton.BoneId[] boneids)
        {
            if (boneids.Length < 3) return false;   //調べようがない
            Vector3? oldVec = null;
            var dot = 1.0f;
            for (var index = 0; index < boneids.Length-1; index++)
            {
                var v = (_skeleton.Bones[(int)boneids[index+1]].Transform.position - _skeleton.Bones[(int)boneids[index]].Transform.position).normalized;
                if (oldVec.HasValue)
                {
                    dot *= Vector3.Dot(v, oldVec.Value); //内積の値を総乗していく
                }
                oldVec = v;//ひとつ前の指ベクトル
            }
            return dot >= threshold; //指定したBoneIDの内積の総乗が閾値を超えていたら直線とみなす
        }

可変長配列でBoneIDを複数(3個以上)受けとり、一つ前のBoneIDが示す関節から関節のベクトルと、今のBoneIDが示す関節と次の関節のベクトルの内積を計算して、 dot にどんどん乗算していっています。(別に平均でも良い気はしますが)

これを使うと、人差し指がまっすぐかどうかを判定してLogに表示する場合、このようになります。

var isIndexStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip);
Debug.Log($"人差し指は{isIndexStraight?"まっすぐ":"曲がってる"}");

おなじく、他の指も調べていけば、:point_up:「人差し指がまっすぐになっていて、中指、薬指、小指は曲がっている」は判定できそうです。

var isIndexStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Index1, OVRSkeleton.BoneId.Hand_Index2, OVRSkeleton.BoneId.Hand_Index3, OVRSkeleton.BoneId.Hand_IndexTip);
var isMiddleStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Middle1, OVRSkeleton.BoneId.Hand_Middle2, OVRSkeleton.BoneId.Hand_Middle3, OVRSkeleton.BoneId.Hand_MiddleTip);
var isRingStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Ring1, OVRSkeleton.BoneId.Hand_Ring2, OVRSkeleton.BoneId.Hand_Ring3, OVRSkeleton.BoneId.Hand_RingTip);
var isPinkyStraight = IsStraight(0.8f, OVRSkeleton.BoneId.Hand_Pinky0, OVRSkeleton.BoneId.Hand_Pinky1, OVRSkeleton.BoneId.Hand_Pinky2, OVRSkeleton.BoneId.Hand_Pinky3, OVRSkeleton.BoneId.Hand_PinkyTip);

Debug.Log($"人差し指は{isIndexStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"中指は{isMiddleStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"薬指は{isRingStraight?"まっすぐ":"曲がってる"}");
Debug.Log($"小指は{isPinkyStraight?"まっすぐ":"曲がってる"}");

if(isIndexStraight && !isMiddleStraight  && !isRingStraight  && !isPinkyStraight ){ //人差し指だけまっすぐで、その他が曲がっている
    Debug.Log($"お前がナンバーワンだ!");
}

そして、人差し指の先端の位置 (IndexTip) で線を描くとこうなりました

うーん。 ノイズなのかなんなのか。 まったく直線が描けてないですね。

とりあえずの目標(:point_up:で線を描く)はできているので、よしとします。
この問題はまた後日・・・。

まとめ

OVRHandからとれるピンチ情報に加え、この「各指が曲がっているか(false)伸びているか(true)」が加わるだけでもいろんな事が出来るんじゃないかなと思います。
<例>
- 全部falseならグー、人差し指と中指だけtrueならチョキ、全部trueならパー、でじゃんけん
- 中指と小指がfalse、そのほかがtrueで「グワシ!」
- 中指だけtrue そのほかfalse で 「F******!」で、プログラム強制終了。
などなど。

しかし・・。ちゃんとリファレンス見てないので、こんなことしなくても情報は取れるよ!もっといい方法あるよ! などあったらコメント教えてください。

ではでは、よきOculusQuestライフを。

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

Prism WPF で サブウィンドウを全画面表示

プロジェクタ投影用サブウィンドウを生成したい。Prismで!

簡単そうだけど意外とやり方が分かりませんでした。

メインウィンドウは普通のGUIなので枠が有ったりして良いんですが、
プロジェクタ投影に際して

  • 枠無し
  • 最大化
  • タスクバー無し

が必要になりました。
WPFで直接いじる分には↓の様なWindowを作れば良さそうです。

<Window 
 WindowStyle="None" WindowState="Maximized"/>
     <WindowChrome.WindowChrome>
        <WindowChrome CaptionHeight="{x:Static SystemParameters.CaptionHeight}"
                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" 
                      UseAeroCaptionButtons="False"/>
    </WindowChrome.WindowChrome>

元々のGUI画面は必要なのでこれをサブWindowとして表示したいのですが、
あれ?っと躓いてしまいました。

とりあえず今回の対処は以下のようにしました。

前提

Prism templete pack導入済み VisualStudioを使って、テンプレートを「Prism Blank App(WPF)」でプロジェクト新規作成した状態から始めます。

.Net Framework 4.8、Select ContainerでUnityを選びました。

空のプロジェクトはNugetパッケージが無いのでビルドを通さないとPrism上手く認識してくれません。
とりあえずビルドしてみましょう。

この時点でPrismは7.2.0.1422でした。

サブWindowを出す変更

サブWindowの作成

Viewsに新規追加でPrismWindow(WPF)を追加

image.png

PrismWindow1.xamlを全画面表示に設定しました。

PrismWindow1.xaml
<Window x:Class="BlankApp1.Views.PrismWindow1"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"             
             prism:ViewModelLocator.AutoWireViewModel="True"
             WindowStyle="None" WindowState="Maximized">
    <WindowChrome.WindowChrome>
        <WindowChrome CaptionHeight="{x:Static SystemParameters.CaptionHeight}"
                      ResizeBorderThickness="{x:Static SystemParameters.WindowResizeBorderThickness}" 
                      UseAeroCaptionButtons="False"/>
    </WindowChrome.WindowChrome>
    <Grid>

    </Grid>
</Window>

App.xaml.csの変更

今回は起動時から終了時までずっと表示したいので、App.xaml.csのCreateShellで生成しました。

App.xaml.cs
using BlankApp1.Views;
using Prism.Ioc;
using Prism.Modularity;
using Prism.Regions;
using System.Windows;

namespace BlankApp1
{
    /// <summary>
    /// Interaction logic for App.xaml
    /// </summary>
    public partial class App
    {
        protected override Window CreateShell()
        {
            var subwindow = new PrismWindow1();
            subwindow.Show();
            RegionManager.SetRegionManager(subwindow, Container.Resolve<IRegionManager>());
            return Container.Resolve<MainWindow>();
        }

        protected override void RegisterTypes(IContainerRegistry containerRegistry)
        {

        }
    }
}


App.xamlの変更

MainWindowが閉じたら全て終了したいのでShutdownMode="OnMainWindowClose"を設定します。

App.xaml
<prism:PrismApplication x:Class="BlankApp1.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:prism="http://prismlibrary.com/"
             xmlns:local="clr-namespace:BlankApp1"
             ShutdownMode="OnMainWindowClose">
    <Application.Resources>

    </Application.Resources>
</prism:PrismApplication>

とりあえず目的は達成

Debug実行とかすれば真っ白枠無しタスクバーまで隠すサブウィンドウとメイン画面が表示されます。

image.png

果たしてこれでいいのか?と言う疑念はありますが・・・。

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

c# 参考サイト メモ

個人メモですみません。

★Uipath-C#

猫の気ままなC#日記
【C#,LINQ】Select,SelectMany~配列やリスト内の要素の形を変形したいとき~うら干物書き
【C#,LINQ】ToDictionary~配列やリストからDictionaryを作りたいとき~
C#でExcelを読み込んで操作する方法を公開
未確認飛行 C OfTypeメソッドとCastメソッド neue cc
未確認飛行 C 日付や時刻を文字列に変換するには?
C#でJSONパーサ

御勉強、アンチョコサイト例
C# と VB.NET のサンプル:Wankumaさんの処
http://jeanne.wankuma.com/tips/
DOBON.NET プログラミング道:Dobonさんの処
http://dobon.net/vb/index.html
++C++; //未確認飛行 C:C#について粗網羅
http://ufcpp.net/study/csharp/
nonsoft :通信系サンプルで困ったら
http://homepage2.nifty.com/nonnon/
すぐ使えるADO.NET と、関連サイト:DB系含めサンプルが、C#用に書直ししやすい。お勉強用ネタ
http://park5.wakwak.com/~weblab/
MSDN:情報隠しの上手なMSなので、検索にはコツがあります。
https://msdn.microsoft.com/ja-jp/dn308572.aspx
TechNet:情報隠しの上手なMSなので、検索にはコツがあります。
https://technet.microsoft.com/ja-jp/
Visual Basic と Visual C#
https://msdn.microsoft.com/ja-jp/library/hh334523.aspx
https://msdn.microsoft.com/en-us/library/hh334523.aspx
ja-JP と en-us 同じ番号で公開されている事も多いです。

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

Blazor(client-side版)で本の情報取得サイトを作成する

概要

Blazorを使ってカメラから本のバーコードを読み取り、情報を取得して表示する簡単なWEBアプリを作成したので、簡単に紹介します。

Demo
(※注意:iOS13のSafariで開くとLoading画面で固まってしまう場合がありました。iOS12環境だと動いたので、最新のBlazorでiOS13の対応が入っているはずですが、まだ未完全なのかもしれません。)

アプリのイメージ

下記のように、カメラで本のバーコードをスキャンすることで本の情報と中古価格を調べることができます。

book.gif

前提

.NET Core SDK 3.1.100
Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2
Visual Studio 2019 16.4.0

実装

実装していく中で、苦労した部分のメモです。

カメラの起動とバーコード認識

カメラの動画からバーコードを認識させる方法として、デバイスを操作するためにJavascriptを使っています。

当初は、videoタグを使って1から実装しようと思っていましたが、QuaggaJSといった、今回の実装したい目的に合致するライブラリが見つかったのでそちらを利用しました。

実装においては下記の記事が参考になりました。
https://qiita.com/kira_puka/items/03dc5c01bbbaffdb6e83

BlazorのレイヤとしてはJavascirptの相互連携機能を使用して、

  1. JS側でのカメラの起動とバーコードの検知のための処理を呼び出す。(引数ではBlazorコンポーネントの参照を渡す)
  2. バーコードが検知されたら、引数で渡したBlazorコンポーネントの参照を経由して検知時のメソッドを呼び出す。(JSInvokable属性の付与されたC#メソッドをJS側から呼び出す)
@inject IJSRuntime jsRuntime;

    public async Task OpenDialogAsync()
    {
        try
        {
            // カメラを起動して画面のキャプチャを開始するJS側の関数を呼び出し
            await jsRuntime.InvokeVoidAsync("barcodeScan.startCapture", DotNetObjectReference.Create(this));
        }
        catch (Exception e)
        {
            // 例外発生時にはカメラを止めて失敗イベントを発火
            await StopCapture();
            await OnFailed.InvokeAsync(e);
        }
    }


    [JSInvokable]
    public async Task CodeDetected(string code)
    {
        // バーコード検出時の処理
    }


window.barcodeScan = {
    startCapture: function(dotNetObj) {
      //ここでdotNetObjの参照の保持とQuaggaJSによるキャプチャを行う
    },
    // QuaggaJSでバーコードが検知された時に呼び出される処理 
    onDetected: function (success) {
    const isbn = success.codeResult.code;
    if (isbn.startsWith("978")) {
        if (dotNetObj) {
            // ISBNバーコードが検知されたので検知したことをBlazor側に通知
            dotNetObj.invokeMethod('CodeDetected', isbn);
        }
    }
}

WEBスクレイピング

ISBNコードから書籍情報や中古価格を取得する際に、WEB-APIが提供されているものはHTTPClientを使ってJSON情報などを取得してパースすればよいですが、無いものに関してはHTMLから情報を取得する必要があります。

一般的には、サーバー側の処理としてスクレイピングを行いますが、今回はBlzorのクライアント上から実施を行いました。

Blazorのクライアントサイド版はブラウザ上で動くため、セキュリティ的な制限なども多く、サーバ側でスクレイピングするのと比較して下記の点で苦労しました。

CORS対策

ブラウザからHTTPをリクエストを投げる際に、WEB-APIではないWEBサイトに対して投げるとそのままでは通らないため、プロキシが必要となります。
今回は下記のようなCORS用のサービスを使用しました。
https://cors-anywhere.herokuapp.com/

https://cors-anywhere.herokuapp.com/{本来のリクエストしたいURL}

とする事で、アクセスが可能になります。

S-JISエンコードのWEBサイト

S-JISエンコードのWEBサイトをHTTPClientで読み込むと、下記のようなエラーが発生しました。
Windows-31J(S-JIS)のエンコードに対応していないため発生しているエラーのようです。

System.InvalidOperationException:
 The character set provided in ContentType is invalid. Cannot read content as string using an invalid character set.
 ---> System.ArgumentException: 'Windows-31J' is not a supported encoding name.
 For information on defining a custom encoding,
 see the documentation for the Encoding.RegisterProvider method. 

.NET CoreではデフォルトではS-JISに対応していないようなので、下記の記事などを参照して、対応するパッケージを入れてエンコード指定で読み込んでみましたが、残念ながら解消できませんでした。
https://qiita.com/sugasaki/items/0639ea9ca07f1ba7a9e0

最終的にはJavascript側のFetchAPIを呼び出して、S-JISエンコードした文字列をBlazorに返す方法で対応しました。

JSでの実装は下記を参照しました。
http://var.blog.jp/archives/79094563.html

window.fetchHttp = {
    getHtmlAsync: async function (url, isEncodeSJIS) {
        const res = await fetch(url, {
            method: "GET",
            mode: "cors",
            headers: {
                "Content-Type": "text/html",
            },
        });
        if (!isEncodeSJIS) {
            // S-JIS出ない場合はそのまま返す
            return await res.text();
        } else {
            // S-JISの場合エンコードしてから返す
            const blob = await res.blob()
            const fr = new FileReader();
            return await new Promise((resolve, reject) => {
                fr.onload = eve => {
                    resolve(fr.result);
                }
                fr.onerror = err => reject(err);
                fr.readAsText(blob, "Shift_JIS");
            });
        }
    }
};

上記のJS関数をBlazorから呼び出すことでHTMLの読み込みができました。
JS側のasync/await関数を普通に呼び出せるのは便利ですね。

    public abstract class FetchClient
    {
        private readonly IJSRuntime _JSRuntime;
        public FetchClient(IJSRuntime jSRuntime)
        {
            _JSRuntime = jSRuntime;
        }

        protected virtual async Task<string> GetHtmlAsync(string url, bool isEncodeSJIS = false)
        {
            return await _JSRuntime.InvokeAsync<string>("fetchHttp.getHtmlAsync", new object[] { url, isEncodeSJIS }).ConfigureAwait(false);
        }
    }

HTMLの読み込みは、AngleSharpを使って実装しました。.NET Core対応のライブラリが普通に使えるのがBlazorの良いところですね。

        private async Task<ProductInfo> GetProductInfoFromHtml(string html)
        {
            var pinfo = new ProductInfo();
            var parser = new HtmlParser();
            var htmlDocument = await parser.ParseDocumentAsync(html);

            // get elem
            var elem = htmlDocument.QuerySelector("div.hoge");
           // do something
        }

コンポーネントにおけるスコープ

APIなどの外部から取得した情報を表示するUIコンポーネントに対して、下記のどちらの手法を取るか迷いました。

  • UIコンポーネント内に、APIのコール処理を持たせるか?
  • UIコンポーネントはデータの入れ物として扱い、配置する上位コンポーネントでAPIをコールして、結果を渡すか?

今回は結局、コンポーネントが外部と連携する事無く独立するため、そちらのほうが良いと判断して、前者を採用しました。
(この辺りはNuxtやVueにおいても、Vuexにとコンポーネント側のどちらに持たせるかで迷ってしまう点と似ていますね。)

<MatCard class="mat-card">
    @if (IsLoading)
    {
        <MatCardContent>
            //API実行中のスピナーなどを表示
        </MatCardContent>
    }
    else if (BookInfo != null)
    {
        <MatCardContent>
            //BookInfo の情報を表示
        </MatCardContent>
    }
</MatCard>
@{
    [Inject]
    IBookGet BookInfoClient { get; set; }
    bool IsLoading { get; set; } = false;
    string ErrorMessage { get; set; }
    BookInfo BookInfo { get; set; }

    public async Task DisplayInfoAsync(string isbn13)
    {
        try
        {
            BookInfo = null;
            IsLoading = true;
            base.StateHasChanged();
            // APIを呼び出して情報を取得
            BookInfo = await BookInfoClient.GetBookInfoAsync(isbn13);
        }
        catch (Exception)
        {
            ErrorMessage = "情報取得に失敗しました。";
        }
        finally
        {
            IsLoading = false;
            base.StateHasChanged();
        }
    }
}

呼び出し元

<BookInfoBox @ref="@bookInfoBox"></BookInfoBox>
@{
    BookInfoBox bookInfoBox;

    public async Task OpenDialogAsync(string isbn13)
    {
        await bookInfoBox.DisplayInfoAsync(isbn13);
    }
}

所感

ということで簡単にですが、簡単なBlazorを使ったサービスを作った際の話を紹介しました。
Blazor以外の話が多くなってしまいましたが、改めてUIやロジック周りはC#を使って書くことができるのは楽で良いと感じました。
前述のAngleSharpのように.NETのライブラリをブラウザ上で使用できる点もBlazorならでは便利な点だと思います。

ただ、VisualStudio上でデバッグができないのは少し不便でした。
現時点でも一応下記に書かれた手順で可能なようでうが、近い将来はもっとお手軽に実行できるようになるみたいなので期待しています。
https://devblogs.microsoft.com/aspnet/asp-net-core-updates-in-net-core-3-1/

少し変わったことをやろうとすると、Javascriptを使わないといけない部分が多く、色々と苦戦しましたが、JSを使えば何とかなるといった事もわかりました。

Blazored.LocalStorageなど、便利なOSSのライブラリなども内部ではJSを使っていたりするので、JSコードをラップしたライブラリが色々と増えて普及すれば、Blazorを使う敷居も下がりそうですね。

@jsakamotoさんが公開している下記のようなBlazorの記事を参考に、何か良いアイディアがあればいずれ個人的にも何かしらの便利なライブラリの作成も試してみたいと思います。
https://qiita.com/jsakamoto/items/a68a62c5e0c13a827da0
https://qiita.com/jsakamoto/items/4c4520a0b73d3f30d95a

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

Unityでシューティングゲームを作る(3)

ここまでの進捗

  • 背景がループするようにした。
  • 普通の敵の動作を作成し、その敵が3秒ごとに生成される。
  • 瞬間移動する敵の動作を作成し、5秒ごとに生成する。
  • プレイヤーが画面の範囲外に行かないようにした。
  • 敵とプレイヤーが衝突したらプレイヤーが消滅する。
  • 分散攻撃の敵を実装する。

今後やること

  • タイトルシーンとエンディングシーンを追加する。
  • ボスキャラの動作を実装する。
  • エフェクトとBGMを追加する。
  • 様々な敵の出現方法を考える。

この記事で書くのは赤文字の部分
前回の最後に「次はボスかぁー」って書いたけどシーン遷移とかやりたかったから先にタイトルとエンディングをやる

タイトルシーンの追加

最終的にどんな感じになったかというと

コメント 2019-12-22 211531.png

シーン作成

File→New Sceneから新しいシーンを作成して名前を「Title Scene」とした
コメント 2019-12-22 232318.png

以下のようにオブジェクトを追加する

コメント 2019-12-22 232248.png

背景

background1,2は背景のオブジェクトで、画面をスクロールするために2つ作った。
コメント 2019-12-22 233630_LI.jpg
オブジェクト1と2で真ん中の赤い部分が中心。
背景オブジェクト1と背景オブジェクト2をY軸に対してマイナスの方向に移動させ、もし中心がある座標までいったらオブジェクト2の位置まで移動するようにした。
具体的なスクリプトとしては以下のようになる。

transform.Translate(0, -0.05f, 0);
if(transform.position.y <= -20.44f) {
    transform.position = new Vector3(0, 18.44f, 0);
        }

これはゲーム内の背景にも適用した。

タイトル名のTextとButton

タイトル画面にタイトル名のTextとゲームを開始するためのボタンを設置した。
これらをそのまま画面に出すのは面白くないので、アニメーションをつけた。
ButtonはScaleを0.3秒毎に変更し続けるようにして、Textのアニメーションは以下のサイトを参考にした。

参考:uGUIのTextで1文字単位のアニメーションを実装できる「Text Juice」紹介
参考:badawe/Text-Juicer

今回はこの中のY Modifierというアニメーションを使用した。

最後のNormalEnemyGeneratorは背景として敵を生成している。
完成したのが下になる。
ezgif.com-video-to-gif (1).gif

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

AAAクオリティのエフェクトアセット「Magic Effects Pack 1」の使い方

この記事について

この記事はUnityアセット冬のアドベントカレンダー 2019 Winter!23日目の記事です。

Magic Effects Pack 1」とは?

アセットストアにてkripto289氏より販売されている、実写(AAA)クオリティ魔法のエフェクトがたくさん詰まったアセットです。
は他にもMesh EffectRealistic Effects Pack 34等の魔法系エフェクト以外にも、様々な美麗エフェクト等のアセットを販売しているので、気になった方は是非チェックしてみてください。

Magic Effects Pack 1

エフェクトのサンプル

C1C4DC5E-F27B-4235-822A-DFB7ED3F54A6.jpeg
火花を撒き散らす雷を飛ばしたり…
D267A8D3-4967-4E3E-BAC3-6AE49B76B2BC.jpeg
水の中に相手を閉じ込めたり…
F3645A55-E88D-4CD9-98DB-99F0BB36CEA5.jpeg
炎の盾で敵の弾を防いだり…

これらを含めた実に計33個ものエフェクトが詰まった大容量パックとなっております。

Magic Effects Pack 1」の特徴

本アセットの大きな特徴として、飛び道具等のエフェクトにはヒットエフェクトもついていることです。
それらも含めると実際のエフェクトの数は50程はあると思います!
かつ用意されているPrefabにはヒット時にそれらのエフェクトを発動させるスクリプトが搭載されているので、Prefabを置くことですぐに使えるのも特徴の一つです。
ほかのエフェクトや動画で見たい方はこちらからご覧下さい。

エフェクトの使い方

プロジェクトを作成し、本アセットをインポートした前提で進めます。

本アセットをインポートしたら、ProjectタブのAssetsフォルダ内に「KriptoFX」という名前のフォルダがあるはずなのでそれを開き、さらに「Realistic Effects Pack 1」というフォルダの中に、本アセットの内容物が入っています。
86F9889E-C1EA-4760-987A-BC89B7D26AE5.jpeg

エフェクトのPrefab名がエフェクト名ではなく、番号が振られているだけなので少々分かりにくいかもしれませんので、そんな時は「PC_DEMO」という名前のサンプルシーンを開けば、全てのエフェクトを番号ごとに見ることができます。
D7DE23CA-CAD8-4699-B668-A4129BBC8F8A.jpeg
使いたいエフェクトを見つけたら、「Prefab」フォルダ内の、「Character」フォルダを開けばキャラクターとセットのエフェクトが、
4BF4E9B0-7C54-4B20-BBF3-B8F06F76B5C0.jpeg
同じく「Prefab」フォルダ内の、「Effects」フォルダを開き「PC」フォルダを開けば単体エフェクトを取り出すことができます。
3B9A4120-C50D-4CEE-90C9-51291157D687.jpeg
また、エフェクトによっては親オブジェクトに「RFX1_Target」というスクリプトがついている場合があり、そのスクリプトの「Target」という変数にオブジェクトを入れると、そのエフェクトが「Target」に入れたオブジェクトに向かって飛ぶようになります!
5D50BF94-E8AE-4BF7-809C-48667193A6B1.jpeg
これらを上手く組み合わせて、貴方の作品に溶け込ませましょう!

最後に

kripto289氏が作成しているアセットはどれもが高品質でリアリティのあるものばかりなので、貴方が作成している(する)ゲームによっては合わないこともあるかもしれませんが、もしリアル味のあるゲームを作成している(する)のであればのアセットはどれもとても重宝することになると思います!
最後まで読んで頂き、ありがとうございました!

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

Unityプログラマにオススメの新しいC#の機能

Unityでも新しいC#!

長い歴史を持つプログラミング言語、C#。C#は着実に進化し、便利な言語機能を追加してきました。ところがゲームエンジンUnityでは少し前まで、古いC#しか使うことができませんでした。

2017年夏 Unity 2017.1がリリースし、「.NET 3.5 Equivalent」に加えて、「.NET 4.6 Equivalent」がExperimentalとして選べるようになりました。
2018年初夏 Unity 2018.1がリリースし、「.NET 4.x Equivalent」がExperimentalでなく、安定版になりました。
2018年冬 Unity 2018.3がリリースし、「.NET 4.x Equivalent」がデフォルトになり、「.NET 3.5 Equivalent」が非推奨になりました。

Unityも、現在は特に工夫をせずに比較的新しいC#であるC# 7.3を使うことができます。(投稿執筆時の最新C#は8.0)

ところで、Unityプログラマの方の中には「こんなC#の機能があるのか!」と驚く人や、「新しいC#の機能、わからない」と困っている人もいるのではないでしょうか?

この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介します。

プロパティの書き方いろいろ

次のコードはUnityでよく使うプロパティの例です。

ゲッターオンリーのプロパティで、SerializeFieldがついたフィールドをバッキングフィールドとしてもっています。

using System;
using System.Collections.Generic;
using UnityEngine;

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 古いC#でのゲッターオンリーのプロパティ
    public int Hp { get { return hp; }}
}

新しいC#では次のように、=>を使って短く書けます。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 新しいC#では短く書けるゲッターオンリーのプロパティ
    public int Hp => hp;
}

冗長な部分のコードがなくなり、コードが短く簡潔になったことに注目してください。


次のコードは、古いC#におけるセッター・ゲッター両方をもつプロパティの例です。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 古いC#でのセッター・ゲッタープロパティ
    public int Hp
    {
        get { return hp; }
        set { hp = value; }
    }
}

これらも=>を使って冗長な部分を取り除き、簡潔に記述することができます。

[Serializable]
public class Monster
{
    [SerializeField] private int hp;

    // 新しいC#でのセッター・ゲッタープロパティ
    public int Hp
    {
        get => hp;
        set => hp = value;
    }
}

C#にはもともと自動実装プロパティという機能がありました。
自動実装プロパティは、バッキングフィールドを自分で書かなくてよいプロパティです。

using System;

public class Player
{
    // 自動実装プロパティ
    public int Name { get; private set; }

    public Player (string name) {
        this.Name = name;
    }
}

古いC#では自動実装プロパティが使えない場面がいくつかありました。新しいC#では、自動実装プロパティが使える場面が増えています。


次のコードではreadonlyなフィールドをバッキングフィールドとしてもつNameプロパティです。
古いC#では「コンストラクタで値 or 参照を設定しそれを書き換えない」というプロパティを実現するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。

public class Player
{
    // 古いC#では、readonlyのために自動実装プロパティでなく
    // バッキングフィールドを使う
    private readonly string name;
    public string Name { get { return name; } }

    public Player(string name)
    {
        this.name = name;
    }
}

新しいC#では、このようにreadonlyなプロパティを自動実装プロパティのみで簡潔に実現できます。

public class Player
{
    // 新しいC#では、readonlyの自動実装プロパティが使える
    public string Name { get; }

    public Player(string name)
    {
        Name = name;
    }
}

次のコードは、バッキングフィールドに初期値をフィールド初期化子で設定しているプロパティです。
古いC#ではプロパティの初期値を設定するためには、このように書く必要がありました。
自動実装プロパティは使えなかったのです。

public class Player
{
    // 古いC#では初期値を設定するために、バッキングフィールドを使う
    // 自動実装プロパティは使えない
    private  string name = "No Name";
    public string Name {
        get { return name; }
        set { name = value; }
    }
}

新しいC#では、初期値の設定とともに自動実装プロパティが使える。

public class Player
{
    // 新しいC#では初期値の設定とともに
    // 自動実装プロパティを使える
    public string Name { get; set; } = "No Name";
}

新しく加わった機能は便利機能ばかりですが、注意しないといけない機能もあります。

新しいC#では、自動実装プロパティのバッキングフィールドに属性をつけられるようになりました。この機能を使い、SerializeFieldをプロパティのバッキングフィールドに付けたくなります。

残念ながらこれは期待する挙動になりません。(フィールドの名前が変 or インスペクターに出てこない)

「自動実装プロパティのバッキングフィールドに属性付与」と「SerializeField」は合わせて使わないようにしてください。

[Serializable]
public class Monster
{

    // Unityでは使ってはいけない
    [field:SerializeField]
    public int Hp { get; }
}

新しいプロパティは、コードの設計が劇的に変わるわけではありませんが、コードが簡潔になります。ぜひ試してみてください。

複数の値を返したい時・まとめたい時はValueTuple

メソッドで複数の値を返したい時、どうすればいいでしょうか?クラスか構造体を作ればいいでしょうか?

ValueTupleは、クラスや構造体などの型を定義しなくても、複数の値をまとめることができるデータ型です。これを使えば、メソッドで複数の値を簡単に返すことができます。

ToStringや、HashCode、Equals、==での比較も実装されており、データ処理時にとても活躍します。

新しいC#では、ぜひValueTupleを使ってみてください。


ValueTupleは、非常に扱いやすい形で複数の値をまとめることができる構造体です。

ValueTupleは、つぎのように()や要素名を記述し、生成することができます。(これ以外の書き方も存在します)

var person0 = (name: "Ryota", level: 31);

上で作ったValueTupleには、namelevelというメンバがあります。

Debug.Log($"{person0.name} {person0.level}");

メソッドの返値型としてValueTupleを使う時は、このように書きます。

public static (string name, int level) LoadNameAndLevel() => (name: "Ryota", level: 31);

ToStringやHashCode、Equalsや==も実装されています。

var person0 = (name: "Ryota", level: 31);
var person1 = (name: "Ryosuke", level: 30);

Debug.Log(person0 == person1);
Debug.Log(person0.name);
Debug.Log(person0.level);
Debug.Log(person0.ToString());

ValueTupleを扱う際分解を使うと、非常に簡潔にかけます。

// ValueTupleを返すLoadNameAndLevel
public static (string name, int level) LoadNameAndLevel() => (name: "Ryota", level: 31);

public static void Main(string[] args)
{
    // 分解で返値を受け取る
    // stringのnameとintのlevel
    var (name, level) = LoadNameAndLevel();
}

今までの古いC#でも、匿名型という便利な言語機能がありました
匿名型もクラスや構造体を定義しなくても、名前のない型を作れる機能です。
詳しくはこちら「C#の匿名型について調べてみた」。
匿名型は、LINQやRxなどの処理の中間データとしては非常に便利だったのですが、メソッドの返り値型にできませんでした。
ValueTupleはメソッドの返値型にできます。

また、ValueTuple構造体よりも前、クラス型のTupleがありました。
Tupleを使えば複数の値をまとめることはできました。
しかし、メンバの名前がItem1やItem2となっていること、構造体ではなくクラスであったことなど、あまり使い安くありませんでした。


ダメージ計算・特典計算などのロジックにおいて、

「privateメソッドで複数の値をまとめて返したい。しかし型を作るほどではない」

という場面があると思います。

そのような時は、ぜひValueTupleを活用してください。

※ ValueTupleは便利ですが、型を作るべき場面もあります。使いすぎに注意してください。
※ ValueTupleを活用したライブラリ、ImportedLinqもみてみてください。

アセンブリを意識したい時のinternalとprivate protected

今までのC#のアクセスレベルは次のものがありました。

  • private
  • protected
  • internal
  • protected internal
  • public

それに加えて新しいC#では、

  • private protected

が加わりました。


UnityではAssembly Definition Filesが使えるようになり、アセンブリを意識して開発する機会が増えました。

今までのUnityにおけるアクセスレベルでは、次の3個を使うことが多かったです。

  • private
  • protected
  • public

Assembly Definition Filesにより、Unityでも簡単にアセンブリを分割できるようになりました。これにより、「アセンブリ内に閉じる」ということが大事になりました。

internalアクセス修飾子を使えば、同一アセンブリ内のみにアクセスを制限できるようになりました。Assembly Definition Filesとともに活用してください。

また、protected internalは「同一アセンブリ」もしくは「その型とその派生型」のどちらかであればアクセスできるアクセスレベルです。

新しく加わったprivate protectedは「同一アセンブリ」かつ「その型とその派生型」がアクセスできるアクセスレベルです。


新しいUnityではAssembly Definition Filesが使えるようになり、アセンブリを意識して開発する機会が増えました。

そこで、internalアクセスレベルとprivate protectedアクセスレベルを活用してください。

合わせて、「C#のアクセス修飾子 2019 〜protectedは 結構でかい〜」も参照してください。

nullの扱いもやりやすく

「null参照の発明は10億ドルにも相当する誤りだった」という言葉もありますが、C#にはnullがあります。nullと上手につきあっていかないといけません。

新しいC#では、そんなnullを上手に扱える記法が追加されています。


次のようなMonsterクラスとPlayerクラスがあります。

public class Monster
{
    public string Name { get; set; }
}

public class Player
{
    public Monster Target { get; set; }
}

MonsterのNameプロパティもPlayerのTargetプロパティもnullになりえます。

そこで次のように三項演算子とnull判定を使って、次のようなコードを書く必要があります。

本当にやりたいことは、メンバへのアクセスだけなのに、非常に冗長です。

// 古いC#では冗長
Player player = LoadPlayer();
var targetMonsterName = player != null && player.Target != null ? player.Target.Name : null;

新しいC#ではこのように?.を使って非常に簡潔に記述できます。

// 新しいC#ではこんな感じに簡潔に書ける
var targetMonsterName = player?.Target?.Name;

「もし対象がnullだったら指定した既定の値を設定したい」という状況があると思います。

古いC#では次のような書き方をする必要がありました。

// 古いC#の書き方
Player player = LoadPlayer();
var targetMonsterName = player != null && player.Target != null ? player?.Target?.Name : "Default Target Name";

新しいC#ではこのように??を使って非常に簡潔に記述できます。

// 新しいC#ではこんな感じに簡潔に書ける
var targetMonsterName = player?.Target?.Name ?? "Default Target Name";

内部的な話をすると、「player?.Target」と「player == null ? null : player.Target」は等価ではありません。==をその型が実装している時は注意してください。?.??を使う場合、==は呼ばれません。

?.??は非常に便利ですが、UnityのGameObjectやMonoBehaviourの中で使うには注意が必要です。

Unityにおいて、GameObjectやコンポーネントでは、?.??には注意が必要です。GameObjectやコンポーネントでは==が実装されています。

?.??を使った際に、何が起こるか考えてみてください。

進化したSwitch

プログラミング言語C#を学び始めた時、ほとんど全ての人はswitchを勉強したと思います。

新しいC#では、switchはとても強化されています。


今までのC#でのswitch文では、列挙型の値、数値の値、文字列の値で分岐するだけでした。

例えば次のコードのようにです。

public enum Shape
{
    Circle,
    Triangle,
    Polygon
}
public static void SwitchExample0(Shape shape)
{
    switch (shape)
    {
        case Shape.Circle:
            Debug.Log("Circleだよ");
            break;
        case Shape.Triangle:
            Debug.Log("Triangleだよ");
            break;
        case Shape.Polygon:
            Debug.Log("Polygonだよ");
            break;
        default:
            throw new ArgumentOutOfRangeException(nameof(shape), shape, "Un expected shape.");
    }
}

新しいC#では型で分岐できるようになりました。次のようなことができるようになったのです。

// objはどんな型がくるかわからない
public static void SwitchExample0(object obj)
{
    switch (obj)
    {
        case int n when n < 0:
            Debug.Log("負の数だよ!");
            break;
        case 7:
            Debug.Log("ラッキーセブンだよ!");
            break;
        case int n:
            Debug.Log($"整数だよ! {n}");
            break;
        case string s:
            Debug.Log($"文字列だよ : {s}");
            break;
        case null:
            Debug.Log("nullだよ");
            break;
        default:
            Debug.Log("それ意外だよ");
            break;
    }
}

より具体的で実用的なコードだとこのようなことができるようになりました。

public abstract class Shape
{
    public abstract double Area { get; }
}

public class Rect : Shape
{
    public int Height { get; set; }
    public int Width { get; set; }
    public override double Area => Width * Height;
}

public class Circle : Shape
{
    public int Radius { get; set; }
    public override double Area => Radius * Radius * Math.PI;
}

Shape型を継承したRect型とCircle型があります。これとswitchを使って、次のようなコードを書くことができます。

// 抽象型のShape。列挙型じゃないよ!
public static void SwitchExample0(Shape shape)
{
    switch (shape)
    {
        case Rect r when r.Width == r.Height:
            Debug.Log($"正方形だよ! 面積: {r.Area}");
            break;
        case Rect r:
            Debug.Log($"長方形だよ! 面積 : {r.Area}");
            break;
        case Circle c:
            Debug.Log($"円だよ! {c.Area}");
            break;
    }
}

ダメージ計算やポイント計算で活用できそうですね!


switchはC# 7.3のさらに先、C# 8.0でさらに進化しています。また今後のC#でさらに強くなっていくでしょう。

ダメージ計算、特定計算などで活躍すること間違いなしです。今後の強化にも期待しましょう。

構造体をより効率よく扱う

C#は、Unityそして.NET Coreの躍進により、よりいろいろな領域で活躍するようになりました。

領域が広がったことにより、パフォーマンスを求められることも増えてきました。

新しいC#では、パフォーマンス改善で活躍する多くの機能が追加されました。一例をあげると、

  • 参照ローカル変数
  • 参照戻り値
  • 読み取り専用参照
  • readonly 構造体
  • ref 構造体

などです。

これらの機能に関して、neueccさんのUnite 2019の公演、「Understanding C# Struct All Things」というとても素晴らしい公演を参照してください。

まとめ

C#は着実に進化し便利な言語機能を追加してきました。
今までUnityでは古いC#しか使えませんでしたが、最近新しいC#が使えるようになりました。
Unityプログラマの方に使って欲しい新しいC#の機能がたくさんあります!

この投稿では、Unityでのロジック記述でオススメの新しいC#の言語機能を、筆者の独断と偏見で紹介しました。

この投稿で紹介していない、便利な新しいC#の機能もたくさんあります。
次の公式ドキュメントや、ufcppさんのとてもわかりやすいブログでぜひ調べてみてください。

MSDN

ufcppさんのC# によるプログラミング入門

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