20191002のC#に関する記事は12件です。

C# のClass とは 参照型である 

プロダクトで業務ばかりやっていると、基本的なところを全く気にせずに時間がたってしまいます。

先日C#チョットできるヒト(社内ではない)と会話する機会という名の
サンドバック面談 1on1(社内ではない) がありまして

C#のパフォーマンスと本当の基礎の基礎を学ばないといけないなという気になりました。

9月末に .NET Core 3.0 がリリースされました
個人的にはblazor がとても気になってます。

結局のところ、公式ガイドを読むのが一番いいのですが、


Class (C#)

  • 参照型
  • new することで object となる
  • new を使って明示的に作成されるまで class内部の変数にはNullが入る
Myclass.cs
//これが MyClass という class
public class MyClass
{

}

Classを実際に使うには new を使って実体化させます→Objectになります。
Classから作られたObjectはインスタンスとも言います。

Program.cs
class Program
    {
        static void Main(string[] args)
        {
           //ここでインスタンスになる(Objectになる)
           MyClass mc = new MyClass();
        }
    }

Class は 参照型 なので クラスの型を参照することができます。
mc2 という MyClassを用意してそれの実態を参照します

Program.cs
class Program
    {
        static void Main(string[] args)
        {
           //ここでインスタンスになる(Objectになる)
           MyClass mc = new MyClass();
           MyClass mc2 = mc;
        }
    }

上記2つを合わせます Myclassも同一ファイルにかけるので、
下記のようになります

Program.cs
//classの中に プロパティとコンストラクタを定義
public class MyClass
{
    public string ClassName{ get; set; }
    public int ClassId { get; set; }

    public MyClass(string name, int id)
    {
        ClassName= name;
        ClassId = id;
    }
}

class Program
    {
        static void Main(string[] args)
        {
           //ここでインスタンスになる(Objectになる)
           MyClass mc = new MyClass("hoge",1); //Classname = hoge, ClassId = 1 のMyClassインスタンス 
           MyClass mc2 = mc;

           //MyClassは 読み書きできるので mc2を使って変数を変えてみます
           mc2.Classname = "hogehoge";
           mc2.ClassId = 2;

           //参照型なので、mc  mc2のプロパティは下記のようになる
           //Output
           // mc.Classname is "hogehoge" 
           // mc.ClassId is 2
           // mc2.Classname is "hogehoge" 
           // mc2.ClassId is 2

        }
    }


尚 1on1(ではない)で下記を含んだいろいろま聞かれて 頭ぺっしゃんこになってたので

以下の疑問が解決できるまで記事を書き続けます

  • class と構造体の違いは?
  • それぞれのメリットとデメリットは?
  • class内のコンストラクタ、変数、プロパティ、メソッドはどう書いたら違いになる?
  • 〇〇の修飾子はどういう意味?
  • LINQはどういう処理と等価?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

超初心者向け C# のClass とは 参照型である 

プロダクトで業務ばかりやっていると、基本的なところを全く気にせずに時間がたってしまいます。

先日C#チョットできるヒト(社内ではない)と会話する機会という名の
サンドバック面談 1on1(社内ではない) がありまして

C#のパフォーマンスと本当の基礎の基礎を学ばないといけないなという気になりました。

9月末に .NET Core 3.0 がリリースされました
個人的にはblazor がとても気になってます。

結局のところ、公式ガイドを読むのが一番いいのですが、


Class (C#)

  • 参照型
  • new することで object となる
  • new を使って明示的に作成されるまで class内部の変数にはNullが入る
Myclass.cs
//これが MyClass という class
public class MyClass
{

}

Classを実際に使うには new を使って実体化させます→Objectになります。
Classから作られたObjectはインスタンスとも言います。

Program.cs
class Program
    {
        static void Main(string[] args)
        {
           //ここでインスタンスになる(Objectになる)
           MyClass mc = new MyClass();
        }
    }

Class は 参照型 なので クラスの型を参照することができます。
mc2 という MyClassを用意してそれの実態を参照します

Program.cs
class Program
    {
        static void Main(string[] args)
        {
           //ここでインスタンスになる(Objectになる)
           MyClass mc = new MyClass();
           MyClass mc2 = mc;
        }
    }

上記2つを合わせます Myclassも同一ファイルにかけるので、
下記のようになります

Program.cs
//classの中に プロパティとコンストラクタを定義
public class MyClass
{
    public string ClassName{ get; set; }
    public int ClassId { get; set; }

    public MyClass(string name, int id)
    {
        ClassName= name;
        ClassId = id;
    }
}

class Program
    {
        static void Main(string[] args)
        {
           //ここでインスタンスになる(Objectになる)
           MyClass mc = new MyClass("hoge",1); //Classname = hoge, ClassId = 1 のMyClassインスタンス 
           MyClass mc2 = mc;

           //MyClassは 読み書きできるので mc2を使って変数を変えてみます
           mc2.Classname = "hogehoge";
           mc2.ClassId = 2;

           //参照型なので、mc  mc2のプロパティは下記のようになる
           //Output
           // mc.Classname is "hogehoge" 
           // mc.ClassId is 2
           // mc2.Classname is "hogehoge" 
           // mc2.ClassId is 2

        }
    }


尚 1on1(ではない)で下記を含んだいろいろま聞かれて 頭ぺっしゃんこになってたので

以下の疑問が解決できるまで記事を書き続けます

  • class と構造体の違いは?
  • それぞれのメリットとデメリットは?
  • class内のコンストラクタ、変数、プロパティ、メソッドはどう書いたら違いになる?
  • 〇〇の修飾子はどういう意味?
  • LINQはどういう処理と等価?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Unity]NavMeshを改めておさらい。

UnityのNavMeshを改めておさらいするということで簡単なEnemyもどきが目的地へ行くものを実装していきます。

1,移動できる範囲を設定しよう

地面となる床を配置していきましょう。HierarchyのCreateから3DObject→Planeです。
1.PNG

とりあえず床が実装できましたね。
2.PNG

では早速移動できる範囲、NavMeshを実装していきましょう。
まず、Planeを選択してInspector(詳細)を表示します。
Inspectorにオブジェクトの名前が書かれている部分があります。その右にstaticと書かれている部分があり、staticの近くにある逆三角形をクリックしNavigation Staticと書かれている部分を選択します。
3.PNG

次に、上部にあるWindowタブからWindow→AI→NavigaitonでNavigationタブを出します。
4.PNG
すると、新しくNavigationタブが出てきます。
あとは簡単です。Navigationタブの4つある項目のうちBakeというものを選択します。
そして、下部分にあるBakeというボタンを押すだけです。
すると、Planeに薄い青の四角形が出てきます。
Bake.png

この青い部分が移動できる範囲となります。
これでNavMeshの設定が終了しました。
では次に、実際に動かしてみる、前に・・・。
カメラを少しいじります。
わかりやすいように上から観察するようにしましょう。
HierarchyのMainCameraを選択して以下のTransform情報を記入してください。
12.PNG
こうすると上から観察しているような視点になります。

2,Enemyを目的地まで移動させてみる

まず、動いてくれるEnemyを作ります。
先ほどPlaneを生成したときと同じ要領でCapsuleを生成します。
Hierarchy→3DObject→Capsuleで生成します。
わかりやすくするためにCapsuleの名前をEnemyに変えておきましょう。
Capsuleを選択してInspectorを表示します。Inspectorの上部にCapsuleと書いてある部分があるのでそこをEnemyに書き換えます。
6.PNG

・・・まだ少しわかりずらいですね。色も変えてあげましょう。
Projectというタブを選択してファイルのある部分で右クリックをし、Create→Materialと選択します。
7.PNG

作ったマテリアル選択してInspectorからAlbedoという項目を探します。
その右側にある白い四角形をクリックして好きな色を選択してあげます。僕は赤を設定します。

色を変えることができたら作ったマテリアルを押しながらHierarchyにいるEnemyまでもっていきます。Enemyの部分に青い枠が出てきたら離してあげます。すると、Enemyの色が変化しているはずです。
drag.png
こんな感じに。
9.PNG

次に、Enemyを選択してInspectorを開き、一番下にあるAdd Component(要素を追加する)を押します。
Navigationという項目を探し、その中にあるNavMeshAgentを押して追加します。
10.PNG

すると、新しくNavMeshAgentが追加されます。
11.PNG

では次にスクリプトを記入します。
先ほどと同様にEnemyを選択してInspectorを開き、AddComponentからNewScriptを選択します。
名前はNavMeshTestにしてCreateAndAddを押します。
作ったスクリプトを開いたら以下のコードを記入します。
最初から記入されているものに対して追加してある部分はコメントでわかりやすくしています                 。

NavMeshTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AI;                   //☆追加

public class NavMeshTest : MonoBehaviour
{
    public NavMeshAgent agent;          //☆追加
    public GameObject target;           //☆追加  目的地

    // Start is called before the first frame update
    void Start()
    {

    }

    // Update is called once per frame
    void Update()
    {
        //目的地を設定してあげる
        agent.SetDestination(target.transform.position);//☆追加
    }
}

すると、EnemyについているNavMeshTestに以下の項目が新しく追加されます。
キャプチャ.PNG

では、このEnemyの移動先である目的地を作ります。
Create→3DObject→Cubeで四角形を出してあげてScaleをいじってあげます。Positionは床の中ならどこでも大丈夫です。(Yは0が好ましい・・・)
キャプチャ.PNG

色が見にくいので先ほど作ったマテリアルでも適応させてあげましょう。
すると、このような感じになります。
キャプチャ.PNG

最後に、EnemyについているNavMeshTestのNoneとなっている場所にEnemyとCubeを入れてあげます。
矢印を参考に選択しながら入れてみてください。

14.png

あとは再生ボタンを押すとEnemyが目的地となるCubeへ向かっていきます。

終わり

細かく書きすぎてわかりづらくなってしまった部分もありますが、慣れてしまえば実装も簡単になります。
別の機会があったら今度は索敵してくれるEnemyを作っていきたい・・・です。

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

C#

C# 개발

환경설정

1. ini파일 쓰기/읽기

// import
using System.Runtime.InteropServices;

// .ini파일 쓰기함수
[DllImport("kernel32")]
private static extern long WritePrivateProfileString(string section, string key, string val, string filePath);
// .ini파일 읽기함수
[DllImport("kernel32")]
private static extern int GetPrivateProfileString(string section, string key, string def, StringBuilder retVal, int size, string filePath);

2. DB 접속

가상머신 DB

- 가상 머신에서의 MS-SQL 설치와 운영

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

デザインパターン:(23分の1)

過去に教えて頂いたデザインパターンの一つ
テンプレートメソッドパターンをアウトプット
(間違ってるかも)

デザインパターン:TemplateMethod
を参考にJAVAをC#に変えて記述にしております。

Class1.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Class1
{
    public abstract class AbstractDisplay
    {
        public abstract void Open();
        public abstract void Print();
        public abstract void Close();

        public void Display()
        {
            Open();
            for (int i = 0; i < 3; i++)
            {
                Print();
            }
            Close();
        }
    }
    public class CharDisplay : AbstractDisplay
    {
        char ch;
        public CharDisplay(char ch)
        {
            this.ch = ch;
        }

        public override void Open()
        {
            Console.Write("***");
        }

        public override void Print()
        {
            Console.Write(ch);
        }

        public override void Close()
        {
            Console.WriteLine("***");
        }
    }
    public class StringDisplay : AbstractDisplay
    {
        private string str;
        private int width;
        public StringDisplay(string str)
        {
            this.str = str;
            this.width = str.Length;
        }
        void PrintLine()
        {
            Console.Write("+");
            for (int i = 0; i < width; i++)
            {
                Console.Write("-");
            }
            Console.WriteLine("+");
        }

        public override void Open()
        {
            PrintLine();
        }

        public override void Print()
        {
            Console.WriteLine("|" + str + "|");
        }
        public override void Close()
        {
            PrintLine();
        }
    }
}

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            AbstractDisplay cd = new CharDisplay('T');
            cd.Display();
            AbstractDisplay sd = new StringDisplay("Design Pattern");
            sd.Display();

        }
    }
}

23分の1個目

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

デザインパターンC#:TemplateMethod

過去に教えて頂いたデザインパターンの一つ
テンプレートメソッドパターンをアウトプット
(間違ってるかも)

デザインパターン:TemplateMethod
を参考にJAVAをC#に変えて記述にしております。

Class1.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Class1
{
    public abstract class AbstractDisplay
    {
        public abstract void Open();
        public abstract void Print();
        public abstract void Close();

        public void Display()
        {
            Open();
            for (int i = 0; i < 3; i++)
            {
                Print();
            }
            Close();
        }
    }
    public class CharDisplay : AbstractDisplay
    {
        char ch;
        public CharDisplay(char ch)
        {
            this.ch = ch;
        }

        public override void Open()
        {
            Console.Write("***");
        }

        public override void Print()
        {
            Console.Write(ch);
        }

        public override void Close()
        {
            Console.WriteLine("***");
        }
    }
    public class StringDisplay : AbstractDisplay
    {
        private string str;
        private int width;
        public StringDisplay(string str)
        {
            this.str = str;
            this.width = str.Length;
        }
        void PrintLine()
        {
            Console.Write("+");
            for (int i = 0; i < width; i++)
            {
                Console.Write("-");
            }
            Console.WriteLine("+");
        }

        public override void Open()
        {
            PrintLine();
        }

        public override void Print()
        {
            Console.WriteLine("|" + str + "|");
        }
        public override void Close()
        {
            PrintLine();
        }
    }
}

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            AbstractDisplay cd = new CharDisplay('T');
            cd.Display();
            AbstractDisplay sd = new StringDisplay("Design Pattern");
            sd.Display();

        }
    }
}

23/1個目

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

デザインパターン:(23/1)

過去に教えて頂いたデザインパターンの一つ
テンプレートメソッドパターンをアウトプット
(間違ってるかも)

デザインパターン:TemplateMethod
を参考にJAVAをC#に変えて記述にしております。

Class1.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Class1
{
    public abstract class AbstractDisplay
    {
        public abstract void Open();
        public abstract void Print();
        public abstract void Close();

        public void Display()
        {
            Open();
            for (int i = 0; i < 3; i++)
            {
                Print();
            }
            Close();
        }
    }
    public class CharDisplay : AbstractDisplay
    {
        char ch;
        public CharDisplay(char ch)
        {
            this.ch = ch;
        }

        public override void Open()
        {
            Console.Write("***");
        }

        public override void Print()
        {
            Console.Write(ch);
        }

        public override void Close()
        {
            Console.WriteLine("***");
        }
    }
    public class StringDisplay : AbstractDisplay
    {
        private string str;
        private int width;
        public StringDisplay(string str)
        {
            this.str = str;
            this.width = str.Length;
        }
        void PrintLine()
        {
            Console.Write("+");
            for (int i = 0; i < width; i++)
            {
                Console.Write("-");
            }
            Console.WriteLine("+");
        }

        public override void Open()
        {
            PrintLine();
        }

        public override void Print()
        {
            Console.WriteLine("|" + str + "|");
        }
        public override void Close()
        {
            PrintLine();
        }
    }
}

Program.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static void Main(string[] args)
        {
            AbstractDisplay cd = new CharDisplay('T');
            cd.Display();
            AbstractDisplay sd = new StringDisplay("Design Pattern");
            sd.Display();

        }
    }
}

23/1個目

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

【Unity】CSVを読み込む

CSVを読み込むには、行を , でデータを区切って読み取っていくのが最も簡単な方法ですが、
データの中に , や改行が入ると途端にめんどくなります。
そこで、CSVHelper を用いてCSVを読み込みます。
pythonならcsvモジュールで一発なんですけどね...

環境

  • macOS High Sierra
  • Unity 2019.1.8f1

準備

CSVHepler を準備する

NuGet Gallery _ CsvHelper から最新版のバージョンをクリックし、右の Download package をクリックしすると、
csvhelper.XX.X.X.nupkg というファイルがダウンロードされます。

そのファイルを右クリックし、 このアプリケーションで開く > その他... から /System/Library/CoreServices/Applications 内の アーカイブユーティリティ で解凍します。

次に csvhelper.12.1.2/lib/netstandard2.0 内の CsvHelper.dll をUnityのプロジェクトの中の Assets フォルダ内に入れます。

準備はここまで。

CSVを読み込む

ここでは例として、CSVでキャラクターのステータスの管理をしてそれを読み込みます。
用意したCSVは以下です。

Name, Hp, Atk, Def
player, 250, 250, 200
slime, 510, 180, 140

キャラクターのステータスを管理するクラスを作ります。

Character.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace CharacterController
{
    public class Character
    {
        public string Name { get; set; }
        public int Hp { get; set; }
        public int Atk { get; set; }
        public int Def { get; set; }
    }
}

CSVを読み取って、データを読み取ります。

ShowStatus.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ShowStatus : MonoBehaviour
{
    void Start() {
        string path = @"";  // CSVファイルのパス
        var csv = Resources.Load(path) as TextAsset;
        var sr = new System.IO.StringReader(csv.text);
        var cr = new CsvHelper.CsvReader(sr);
        cr.Configuration.RegisterClassMap<CharacterMapper>();
        var records = cr.GetRecords<CharacterController.Character>();

        foreach (var record in records) {
            Debug.Log($"{record.Name}, {record.Hp}, {record.Atk}, {record.Def}");
        }
    }
}

public class CharacterMapper : CsvHelper.Configuration.ClassMap<CharacterController.Character>
{
    public CharacterMapper()
    {
        Map(x => x.Name).Index(0);
        Map(x => x.Hp).Index(1);
        Map(x => x.Atk).Index(2);
        Map(x => x.Def).Index(3);
    }
}

CharacterMapper クラスで、CSVの各フィールドが Character クラスのどのプロパティに当たるのかを指定します。

実行

適当なゲームオブジェクトにこのスクリプトをアタッチして実行してみます。

スクリーンショット 2019-10-02 12.43.32.png

先ほどのCSVと見比べて、問題なく出力できているのが確認できました。

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

[Unity] Spriteを範囲指定付きテクスチャとして使う

経緯

texture_animation.gif
一枚の画像に異なるパーツの画像が入った画像の範囲を指定してテクスチャとして使用したい。どうせならUnity上で SpriteEditor を使って視覚的に範囲指定できると便利。
つまりSpriteをスプライト以外のテクスチャとしても使えれば便利

以前書いた記事 【Unity】 クォータービューのドット絵に深度バッファを適用する でスプライトを3Dモデルに投影する手法を試しましたが、この時 Texture には Sprite 用のものではなく通常のタイプを使っていました。今回は、スプライトのままで3Dオブジェクトに貼り付ける方法を試してみました。

その過程で、通常の3Dモデル用マテリアルのテクスチャにスプライトを使う方法もわかったのでまとめてみます。

Sprite から Texture 範囲を取得(マテリアル汎用)

ググってみたら、わざわざピクセルを書き出す例があったけど、そんな面倒なことしなくても、今回の目的のためには Sprite.textureSprite.textureRect がわかれば十分でした。

マテリアルのテクスチャに範囲を設定する

スプライトの元のテクスチャと範囲矩形がわかるので、それをマテリアルに設定してやります。
Material.mainTextureでテクスチャをセットし、Material.mainTextureOffsetMaterial.mainTextureScaleで範囲指定します。

SpriteAnime.cs
        var renderer = GetComponent<MeshRenderer>();
        var material = renderer.material;
        var texSize = new Vector2(sprite.texture.width, sprite.texture.height);
        var rect = sprite.textureRect;
        material.mainTexture = sprite.texture;
        material.mainTextureOffset = new Vector2(rect.x/ texSize.x, rect.y / texSize.y);
        material.mainTextureScale = new Vector2(rect.width / texSize.x, rect.height / texSize.y);

公式ドキュメントにも書いてありますが、上で使用した Material のプロパティはシェーダーの _MainTex を置き換えます。最後の三行はこう書いても同じです。

        material.SetTexture("_MainTex",sprite.texture);
        material.SetTextureOffset("_MainTex", new Vector2(rect.x, rect.y));
        material.SetTextureScale("_MainTex", new Vector2(1 / rect.width, 1 / rect.height));

メインテクスチャ以外にスプライトを使いたい場合は、上記のように名前指定で置き換えられるでしょう。

ついでにスプライトアニメーションやってみた

ただスプライトを貼るだけでは芸がないので、スプライトの配列を設定してアニメーションができるようにしてみました。

SpriteAnime.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class SpriteAnime : MonoBehaviour
{
    [SerializeField, Header("アニメーションリスト")]
    private Sprite[] sprites = new Sprite[0];

    [SerializeField, Header("アニメーション速度"),Range(1,100)]
    private float speed = 50f;

    // Update is called once per frame
    void Update()
    {
        if (sprites.Length < 1) return;

        var index = (int)Mathf.Repeat(Time.frameCount * speed / 100, sprites.Length);
        var renderer = GetComponent<MeshRenderer>();
        var material = renderer.material;

        var sprite = sprites[index];
        var texSize = new Vector2(sprite.texture.width, sprite.texture.height);
        var rect = sprite.textureRect;
        material.mainTexture = sprite.texture;
        material.mainTextureOffset = new Vector2(rect.x/ texSize.x, rect.y / texSize.y);
        material.mainTextureScale = new Vector2(rect.width / texSize.x, rect.height / texSize.y);
        material.SetTexture("_MainTex",sprite.texture);
        material.SetTextureOffset("_MainTex", new Vector2(rect.x, rect.y));
        material.SetTextureScale("_MainTex", new Vector2(1 / rect.width, 1 / rect.height));

        renderer.material = material;
    }
}

上記をコンポーネントとして適当なゲームオブジェクトに追加します。
spriteAnime.jpgspriteeditor.jpg

結果

texture_animation.gif

Shedクラスで使う

一般的なゲームオブジェクトに対する設定方法がわかったので、今度は Shed クラス(以前の記事で作ったドット絵を直方体メッシュに正射影するクラスです)でやってみます。
もともとクラス内で mesh に UV座標を設定していたので、そこを変えるだけでよいです。
以下は関連する部分だけ抜き出したものです。完全なソースはこちら(GitHub

Shed4Sprite.cs
    public Sprite sprite;

    // ドット絵を保持するマテリアル
    public Material material;

    // マテリアルのメインテクスチャサイズ
    private Vector2Int texSize;

    // Spriteのテクスチャ領域
    private RectInt spriteRect;
    void Start()
    {
        if (!needRestruct) return;
        needRestruct = true;
        heplMessage = null;

        if (fieldsNotReady()) return;
        spriteRect = GetSpriteRect();
        texSize = GetTextureSize();

        Mesh mesh = InitializeCube();
        var newMaterial = Instantiate(material);
        newMaterial.SetTexture("_MainTex", sprite.texture);
        GetComponent<MeshFilter>().sharedMesh = mesh;
        GetComponent<MeshRenderer>().material = newMaterial;
    }

    /// <summary>
    /// テクスチャサイズを取得
    /// </summary>
    /// <returns></returns>
    private Vector2Int GetTextureSize()
    {
        var tex = sprite.texture;

        return new Vector2Int(tex.width, tex.height);
    }

    /// <summary>
    /// Spriteのテクスチャ範囲を取得
    /// </summary>
    /// <returns></returns>
    private RectInt GetSpriteRect()
    {
        var rect = sprite.textureRect;

        return new RectInt((int)rect.x, (int)rect.y, (int)rect.width, (int)rect.height);
    }
    /// <summary>
    /// pivotからの相対位置をUV座標に変換する
    /// </summary>
    /// <param name="offestX"></param>
    /// <param name="offsetY"></param>
    /// <returns></returns>
    private Vector2 ToUV(float offestX, float offsetY)
    {
        var x = pivot.x + offestX + spriteRect.x;
        var y = pivot.y + offsetY + spriteRect.y;
        var pos = new Vector2( x / texSize.x, y / texSize.y);
        //Debug.LogFormat("ToUV:({0},{1})",pos.x, pos.y);
        return pos;
    }

疑似3Dサイズを自動計算させよう

Shed クラスはドット絵の疑似3Dサイズを指定するようになっていますが、スプライトの画像サイズが無駄な余白を含んでいないと仮定すれば、pivot.xと画像サイズから以下のように導き出せます。

       var width = size.x + size.z;
       var height = size.y + width / 2);

せっかく SpriteEditor で視覚的に範囲指定できるので、計算で求められるものは計算して、手入力しなければならないパラメータは極力減らしたいですね。また関連部分だけの抜き出しですが、以下のようにしてみました。

Shed4Sprite.cs
    [SerializeField, Header("スプライトとpivot.xからsize自動計算")]
    public bool autoSizeAdjust = true;

    // マテリアルのメインテクスチャサイズ
    private Vector2Int texSize;

    // Spriteのテクスチャ領域
    private RectInt spriteRect;

    // Inspector 表示用の警告メッセージ
    public string heplMessage;

    private void doAutoSizeAdjust()
    {
        Debug.LogFormat("Texture size:({0},{1})", texSize.x, texSize.y);
        Debug.LogFormat("Sprite rect:({0},{1})-({2},{3})", spriteRect.x, spriteRect.y, spriteRect.width, spriteRect.height);
        pivot.y = 0;
        pivot.x = Mathf.Clamp(pivot.x, 0, spriteRect.width);

        size.z = pivot.x;
        size.x = spriteRect.width - pivot.x;
        size.y = spriteRect.height - spriteRect.width / 2;

        if (size.y < 0)
        {
            heplMessage = "スプライトの高さが足りません。最低でも横幅の半分以上必要です。";
            size.y = 0;
        }
    }

    void Start()
    {
        if (!needRestruct) return;
        needRestruct = true;
        heplMessage = null;

        if (fieldsNotReady()) return;
        spriteRect = GetSpriteRect();
        texSize = GetTextureSize();

        if (autoSizeAdjust)
        {
            doAutoSizeAdjust();
        }
        else if (!verifySize())
        {
            heplMessage = "スプライトのサイズは指定の3Dサイズに必要な大きさがありません";
        }

        //... 中略 ... //
    }

    private bool verifySize()
    {
        var width = size.x + size.z;
        if (spriteRect.width < width) return false;
        return spriteRect.height >= size.y + width / 2;
    }

プレビューにこだわる

これで、面倒な座標入力は pivot.x だけになりました。ここまで来たらこれも WYSIWYG にしたい! エディタ拡張でプレビューをカスタマイズして、pivot 位置を視覚的に確認できるようにしてみました。

Shed4SpriteEditor.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEngine.SceneManagement;
using UnityEditor.SceneManagement;

[CustomEditor(typeof(Shed4Sprite))]
public class Shed4SpriteEditor : Editor
{
    Shed4Sprite shed = null;
    SerializedProperty sprite;
    SerializedProperty material;
    SerializedProperty pivot;
    SerializedProperty size;
    SerializedProperty autoAdjust;

    void OnEnable()
    {
        shed = target as Shed4Sprite;
        sprite = serializedObject.FindProperty("sprite");
        material = serializedObject.FindProperty("material");
        pivot = serializedObject.FindProperty("pivot");
        size = serializedObject.FindProperty("size");
        autoAdjust = serializedObject.FindProperty("autoSizeAdjust");
    }

    public override void OnInspectorGUI()
    {
        // シリアライズオブジェクトの更新
        serializedObject.Update();

        EditorGUI.BeginDisabledGroup(true);
        EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((MonoBehaviour)target), typeof(MonoScript), false);
        EditorGUI.EndDisabledGroup();

        EditorGUI.BeginChangeCheck();

        EditorGUILayout.PropertyField(pivot);
        EditorGUILayout.PropertyField(sprite);
        EditorGUILayout.PropertyField(material);

        EditorGUILayout.PropertyField(autoAdjust);
        if (autoAdjust.boolValue)
        {
            EditorGUI.BeginDisabledGroup(true);
            EditorGUILayout.PropertyField(size);
            EditorGUI.EndDisabledGroup();
        }
        else
        {
            EditorGUILayout.PropertyField(size);
        }
        // シリアライズオブジェクトのプロパティの変更を更新
        serializedObject.ApplyModifiedProperties();

        string help = shed.heplMessage;
        if (help != null && help.Length > 0)
        {
            EditorGUILayout.HelpBox(help, MessageType.Warning);
        }

        if (EditorGUI.EndChangeCheck())
        {
            shed.UpdateMesh();
        }
    }

    // プレビューウィンドウを表示するかどうか
    public override bool HasPreviewGUI()
    {
        return true;
    }

    private bool ZoomAroundPivot
    {
        get { return shed.zoomAroundPivot; }
        set { shed.zoomAroundPivot = value; }
    }

    // プレビューウィンドウのヘッダーバーをカスタムする関数
    public override void OnPreviewSettings()
    {
        ZoomAroundPivot = GUILayout.Toggle(ZoomAroundPivot, "zoom pivot");
    }

    // プレビューウィンドウで描画させたいものはここで書く
    public override void OnPreviewGUI(Rect r, GUIStyle background)
    {
        if (ZoomAroundPivot) _drawAroundPivot(r);
        else _drawEntire(r);
    }

    private void _drawEntire(Rect r)
    {
        Vector2 size2D = shed.Size2D;

        const int border = 2;

        var scale = Mathf.Min((r.width - border * 2)/ size2D.x , (r.height - border * 2)/ size2D.y);
        var centerPos = new Vector2(r.x + r.width / 2, r.y + r.height / 2);
        var drawSize = size2D * scale;
        var size3D = ((Vector3)shed.size) * scale;

        var offsetPos = new Vector2(centerPos.x - border - drawSize.x / 2, centerPos.y -border - drawSize.y / 2);

        // 3D投影サイズ境界のx,zを赤と緑の外殻線で描く
        var maxPos = new Vector2(offsetPos.x + drawSize.x + border * 2, offsetPos.y + drawSize.y + border * 2);
        Handles.DrawSolidRectangleWithOutline(new Rect(offsetPos.x, offsetPos.y, size3D.x + border, border - 1), Color.red, Color.red);
        Handles.DrawSolidRectangleWithOutline(new Rect(offsetPos.x, offsetPos.y, border - 1, size3D.x / 2 + border), Color.red, Color.red);
        Handles.DrawSolidRectangleWithOutline(new Rect(offsetPos.x, maxPos.y, size3D.z + border, border - 1), Color.green, Color.green);
        Handles.DrawSolidRectangleWithOutline(new Rect(offsetPos.x, maxPos.y, border - 1, -size3D.z / 2), Color.green, Color.green);

        Handles.DrawSolidRectangleWithOutline(new Rect(maxPos.x, maxPos.y, -size3D.x - border, border - 1), Color.red, Color.red);
        Handles.DrawSolidRectangleWithOutline(new Rect(maxPos.x, maxPos.y, border - 1, -size3D.x / 2 - border), Color.red, Color.red);
        Handles.DrawSolidRectangleWithOutline(new Rect(maxPos.x, offsetPos.y, -size3D.z - border, border - 1), Color.green, Color.green);
        Handles.DrawSolidRectangleWithOutline(new Rect(maxPos.x, offsetPos.y, border - 1, size3D.z / 2 + border), Color.green, Color.green);

        var texture = shed.sprite.texture;
        var texRect = shed.SpriteRect;
        var texSize = new Vector2(texture.width, texture.height);
        //Debug.LogFormat($"({texSize.x},{texSize.y}) , ({texRect.xMin},{texRect.yMin}) -({texRect.xMax}, {texRect.yMax})");

        // 中央にスプライト領域を描画
        var drawRect = new Rect(offsetPos.x + border, offsetPos.y + border, drawSize.x, drawSize.y);
        Rect texCoord = new Rect(texRect.x / texSize.x, texRect.y / texSize.y, texRect.width / texSize.x, texRect.height / texSize.y);
        GUI.DrawTextureWithTexCoords(drawRect, texture, texCoord);
    }

    private void _drawAroundPivot(Rect r)
    {
        Vector2 size2D = shed.Size2D;

        const int border = 2;
        const int zoom = 3;

        var scale = Mathf.Min(r.width / size2D.x, (r.height - border) / size2D.y);
        scale = Mathf.Max(3, scale);
        var hfWidth = r.width / 2;
        var hfHeight = r.height / 2;

        // 3D投影サイズ境界のx,zを赤と緑の外殻線で描く
        Handles.DrawSolidRectangleWithOutline(new Rect(r.x, r.yMax, hfWidth, border - 1), Color.green, Color.green);
        Handles.DrawSolidRectangleWithOutline(new Rect(r.x + hfWidth, r.yMax, hfWidth, border - 1), Color.red, Color.red);

        var texture = shed.sprite.texture;
        var texRect = shed.SpriteRect;
        var texSize = new Vector2(texture.width, texture.height);
        //Debug.LogFormat($"({texSize.x},{texSize.y}) , ({texRect.xMin},{texRect.yMin}) -({texRect.xMax}, {texRect.yMax})");
        Vector2 pivot = shed.pivot;
        var clipSize = new Vector2(hfWidth / zoom, (r.height - border) / zoom);
        Rect texCoord = new Rect(
            (texRect.x + pivot.x - clipSize.x) / texSize.x, texRect.y / texSize.y,
            clipSize.x * 2 / texSize.x, clipSize.y / texSize.y);


        // 中央にスプライト領域を描画
        var drawRect = new Rect(r.x, r.y, r.width, r.height - border);
        GUI.DrawTextureWithTexCoords(drawRect, texture, texCoord);
    }
}

結果

pivot を変更すると連動してプレビューが動いて、赤と緑の枠がそれぞれ3D投影サイズの x,z の範囲を示すようにサムネイル画像外縁に表示します。さらに 'zoom pivot' にチェックすると pivot 周辺が拡大表示されてピクセル単位での位置あわせが容易にできるようにしました。  
inspector-preview.gif

参考記事

エディタ拡張については下記を参考にさせていただきました。
http://baba-s.hatenablog.com/entry/2019/04/10/181000
https://techblog.kayac.com/unity_advent_calendar_2018_16
https://qiita.com/kyourikey/items/7a5f693d1fe17bde5387

まとめ

  • スプライトの texture と textureRect で元のテクスチャと引用範囲を取得できた
  • マテリアルの mainTexture, mainTextureOffset, mainTextureScale で範囲指定付きでテクスチャを設定できた
  • スプライトを使った3Dモデルのテクスチャアニメーションができた
  • ドット絵正射影用のShedクラスもUV計算時に範囲計算を反映することでスプライトを使用できるようにした
  • Shedクラスの3D投影サイズをスプライト領域から自動計算するようにした
  • エディタ拡張でShedクラスのプレビューに3D投影サイズがわかりやすく表示できた
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

HttpClientをusingで囲わないでください

C#でHTTP通信をするためのコードのサンプルはネットに沢山あり、そのほとんどが以下のような感じです

    using (var client = new HttpClient())
    {
       var response = await client.GetAsync(url);
       ....
    }

これは間違いです。HttpClientオブジェクトは dispose してはいけません! Stackoverflowにも沢山この間違いがあります。

正しい使い方はAPIの公式ドキュメントに書いてある通りです。

https://docs.microsoft.com/ja-jp/dotnet/api/system.net.http.httpclient?view=netframework-4.8

public class GoodController : ApiController
{
    private static readonly HttpClient HttpClient;

    static GoodController()
    {
        HttpClient = new HttpClient();
    }
}

上記の通り、HttpClientオブジェクトは一度作成するだけでそれをずっと使い続けるのが正しい使用法です。知ってる人には当然なんですけども、知らないと結構驚くかも知れません。APIドキュメントにはこう書いてあります。

HttpClientは、1回インスタンス化し、アプリケーションの有効期間全体に再利用することを目的としています。 すべての要求に対して HttpClient クラスをインスタンス化すると、大量の読み込みで使用可能なソケットの数が枯渇します。 これにより、SocketException エラーが発生します。

間違った使い方をしていても普段はあまり問題は起きませんが、高負荷時に突然ダウンすることが起こりえます。

Azure App Service で問題になりやすい理由

間違った実装のアプリをAzure App Serviceで運用している場合、特に問題になりやすいです。どうしてかというとSNATの枯渇につながるからです。

SNATとは

SNATの正確な説明はここにあります(なぜか和訳されてませんが)

https://docs.microsoft.com/ja-jp/azure/load-balancer/load-balancer-outbound-connections

このページの中の重要な部分を引用します。Understanding SNAT and PATの中のTCP SNAT Portsセクションからです。

One SNAT port is consumed per flow to a single destination IP address, port. For multiple TCP flows to the same destination IP address, port, and protocol, each TCP flow consumes a single SNAT port. This ensures that the flows are unique when they originate from the same public IP address and go to the same destination IP address, port, and protocol.

1つの宛先IPアドレスとポートへの通信のためにSNATポートが1つ消費されます。同一の宛先に対する複数の通信の場合、それぞれの通信が一つずつのSNATポートを消費します。これにより、一つのIPアドレスから複数の同じ宛先IPアドレス、ポートへの通信がそれぞれ別の通信となることを保証します。

Multiple flows, each to a different destination IP address, port, and protocol, share a single SNAT port. The destination IP address, port, and protocol make flows unique without the need for additional source ports to distinguish flows in the public IP address space.

反対に、複数の通信がそれぞれ違う宛先の場合は消費せずに一つのSNATポートが共有されます。宛先が違うことで、それぞれの通信が別のものであることが確定しているからです。

直観的に「同じ宛先の通信には同じSNATポートが使われる」と考えがちですが、実際は正反対です。ばらばらの宛先に対して通信している場合はSNATポートは1つしか使いませんが、同じ通信相手に対しては都度SNATポートを消費します。

使い終わったSNATポートはTCP CLOSE_WAITあるいはTIME_WAIT状態に遷移し、4分間再利用できない状態のままポートを占有し続けます(TCPプロトコルの標準動作です)。

HttpClientとSNAT浪費がもたらす問題

ここで最初のHttpClientの問題に戻ります。HttpClientオブジェクトを通信のたびに作成すると、ソケットを再利用せずに新しいTCPポートを作ります。もし通信相手が同じ場合(たとえばAzure ADによる認証や外部SaaSサービスなど)はIPアドレスも同じ可能性がかなり高く、SNATは再利用されることなく1つ消費されます。これが短時間に多数起きるとCLOSE_WAITまたはTIME_WAIT状態のSNATポートが大量にできてしまいます。

Azure AppServiceとSNAT浪費

VMサービスにグローバルIPアドレスを割り当てている場合はあまり問題になりませんが、App Serviceは1つのスケールユニット中のVM群がSNATを共有しているため、1つのVMあたりに使えるSNAT数は最低保証数(128)以上はBest Effortによる割り当てになります。これらのSNATポートがCLOSE_WAITやTIME_WAITで埋まってしまうと、それ以上割り当てることができないことがあります。

たとえばあるSNAT浪費問題を持っているアプリがあると、こういうシナリオが起きることがあります。
1. アプリがSNAT枯渇により通信エラーを起こす
2. 原因が分からないのでとりあえずスケールアウトする
3. スケールアウトしたすべてのVMが同じ挙動をすることで大量のSNATを消費し続ける
4. 新たにSNATを確保することが非常に困難になり、通信エラーの確率が上がる

この手の問題の厄介なところは、手元のテスト環境では発生しにくいことです。クラウドにデプロイしてしばらく運用してから初めて発生し、しかもとりあえず行う問題回避行動(上記の場合スケールアウト)が余計事態を悪化させがちです。

どうすればいいか

一番あきらかで最適な解は、コードを修正することです。公式のAPIドキュメントの記載にしたがって正しく実装すれば上記の問題の発生確率を大幅に下げることができます。ちょっとだけ面倒なのは、認証等の動的なヘッダー情報が必要な際は少し記述量が増えることです。具体的にはHttpRequestMessageオブジェクトとSendAsync()メソッドを使います。なおHttpClientはスレッドセーフなのでロックは不要です。

GET

まずはHTTP GETのサンプルコードです。本質部分だけを書くのでエラー処理は含んでいません。

public class SampleHttp
{
    private static HttpClient _httpClient;

    static SampleHttp()
    {
        _httpClient = new HttpClient();
    }

    public async Task<string> GetAsync(string uri)
    {
        var request = new HttpRequestMessage
        {
            Method = HttpMethod.Get,
            RequestUri = new Uri(uri)
        };

        var response = await _httpClient.SendAsync(request);
        return await response.Content.ReadAsStringAsync();        
    }
}

HTTP VerbはHttpRequestMessageMethodプロパティに設定し、送信先URLはRequestUriプロパティに設定します。responseオブジェクトを受け取った後はGetAsync()の場合と全く同じです。

PUT/POST

PUTとPOSTはbodyの送信が入る部分がGETと異なります。JSONで送信する例を示します。

    public async Task PostAsync(string uri, SomeClass data)
    {
        var request = new HttpRequestMessage
        {
            Method = HttpMethod.Post,
            RequestUri = new Uri(uri)
        };

        var content = JsonConvert.SerializeObject(data);
        request.Content = new StringContent(content, Encoding.UTF8, "application/json");
        var response = await _httpClient.SendAsync(request);
        response.EnsureSuccessStatusCode(); 
    }

認証ヘッダが必要な場合

トークンをヘッダに入れるときはHttpRequestMessage.Headers.Authorizationプロパティにセットします。GETでの例を示します(他のVerbでもまったく同じ)

    public async Task<string> GetAsync(string uri, string token)
    {
        var request = new HttpRequestMessage
        {
            Method = HttpMethod.Get,
            RequestUri = new Uri(uri)
        };

        request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        var response = await _httpClient.SendAsync(request);
        return await response.Content.ReadAsStringAsync();        
    }

HttpClientオブジェクトをusingで囲って使っている場合DefaultRequestHeaders.Authorizationに入れるのが楽ですが、staticオブジェクトの初期化時にはまだトークンが得られていないことが多いです。なので上記のようにHttpRequestMessageHeaders.Authorizationにセットします。

さいごに

コマンドラインアプリなどのインタラクティブ and/or 寿命の短いコードと、サーバ上で動かすコードとでは、気を付ける部分がだいぶ違います。手元でやってみて動いたことと、クラウドで動かすことは必ずしも一致しません。思わぬトラブルを避けるためには公式ドキュメントをよく読むのが大事だと思います。つまりこの記事も鵜呑みにしないでくださいね! Happy Hacking!!

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

UnityInjectorのHelloWorld

COM3D2でお馴染みUnityInjector。
適当にDLL形式のプラグインを落としてきてフォルダに突っ込めば、標準機能ではできないあんなことやこんなことができる優れものです。しかし、当然のことながら他人の作ったプラグインだけでは辛抱ならないのが男の子です。というわけで、パパっと作ってしまいたいところですが、アングラなものなこともあってかあまり情報がありません。エンドユーザー向けの情報は結構Wikiとかブログとかにまとまってるんですけどね…

ということで、COM3D2でHelloWorldするUnityInjectorプラグインのチュートリアルを書き残したいと思います。COM3D2以外もパスを変えれば、多分同じ手順で行けると思います。

環境構築

dotnetがビルドできるようになればなんでもいいのですが、手っ取り早くかつ最低限の機能はそろっている.NET Coreで開発を行いたいと思います。

.NET Coreはコマンドラインツールでdotnetのプロジェクトビルドをすることができます。GUIがお好みの方はVS入れてください。同じDLLを参照させて、同じコードを書けば同じ結果になると思います。

まずは以下のリンク(Microsoft)から、.NET Core SDKをDLしインストールします。
https://dotnet.microsoft.com/download

するとdotnetコマンドが使えるようになります。cmdかPowerShellで叩いてみてください。うまくいかない場合はパスが通っていない可能性があるのでパスを通しましょう。

次に、COM3D2+UnityInjectorがインストールされていない場合はインストールしてください。UnityInjectorはSybarisを入れればフォルダ内にUnityInjector.dllがあるはずです。

プロジェクト生成

適当なプロジェクトフォルダを作ります。ここではCom3d2HelloWorldとします。そしてdotnetコマンドでプロジェクトを生成します。プロジェクトタイプはclasslib(直接実行しないライブラリのプロジェクト)

mkdir Com3d2HelloWorld
cd Com3d2HelloWorld
dotnet classlib

すると、Com3d2HelloWorldフォルダ内にCom3d2HelloWorld.csproj及び、Class1.csというファイルが生成されていることが確認できると思います。

参照の追加

プラグインがUnityInjector、UnityEngine、そしてCOM3D2のクラスを呼び出すために、プラグインプロジェクトに参照を追加します。尤も、今回はHelloWorldをするだけなのでCOM3D2のクラスは使用しませんが。csprojに追記します。

Com3d2HelloWorld.csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <!-- ここから追記 -->
  <ItemGroup>
    <Reference Include="UnityInjector">
      <HintPath>C:\KISS\COM3D2\Sybaris\UnityInjector.dll</HintPath>
    </Reference>
    <Reference Include="UnityEngine">
      <HintPath>C:\KISS\COM3D2\COM3D2x64_Data\Managed\UnityEngine.dll</HintPath>
    </Reference>
    <Reference Include="Assembly-CSharp"><!-- COM3D2クラスが詰まってるDLL -->
      <HintPath>C:\KISS\COM3D2\COM3D2x64_Data\Managed\Assembly-CSharp.dll</HintPath>
    </Reference>
  </ItemGroup>
  <!-- ここまで-->

</Project>

パスは各自の環境に合わせてください。

csprojを反映させます。(要らないかも)

dotnet restore

コーディング

Class1.csの方にコードを書いていきます。ファイル名、クラス名に制約はありません。(衝突しなければ)
Class1というのはあまりにもなので、Com3d2HelloWorld.csにリネームしておきます。そして、次のようなコードを書きます。

Com3d2HelloWorld.cs
using UnityEngine;
using UnityInjector;
using UnityInjector.Attributes;

namespace Com3d2HelloWorld
{
    /// <summary>
    /// これはMonoBehaviourで、UnityInjectorによりInstansiateされてシーンに放り出されます。
    /// 実際にゲーム内と相互作用をするにはObject.FindObjectOfTypeで所望のGemeObject(メイドさん等)を捕まえて
    /// そこからGameObject.GetComponentでコンポーネントを捕まえてメンバを書き換えたり実行するというフローになります。
    /// PluginFilterは対象とするExeです。この場合64bit版と32bit版を指定しています。ほかにもVR版などもあります。
    /// </summary>
    [PluginFilter("COM3D2x64"), PluginFilter("CM3D2x86"),PluginName("Com3d2HelloWorld"), PluginVersion("0.0.0.0")]
    public class Com3d2HelloWorld : PluginBase
    {
        void OnGUI()
        {
            // とりあえず画面にHello world!出しておく
            // MonoBehaviourなのでOnGUIやUpdate等、Unityのコールバック関数が自由に使えます
            GUI.Label(new Rect(100, 100, 500, 100), "Hello world! [8846]");
        }
    }
}

コンパイル&デプロイ

以下のコマンドを実行してビルドします。

dotnet build

すると、Com3d2HelloWorld\bin\Debug\Com3d2HelloWorld.dllが出来上がっている筈です。これをCOM3D2のプラグインとして配置してあげればOKです。おそらくSybaris\UnityInjector\配下ですね。

確認

COM3D2を起動します。コンソールに次のように表示されていることを確認します。

Loading Assembly: 'C:\KISS\COM3D2\Sybaris\UnityInjector\Com3d2HelloWorld.dll'

ゲーム画面を見ると…

コメント 2019-10-02 032412.png

まとめ

要するに

  • UnityEngine.dll
  • UnityInjector.dll
  • Assembly-CSharp.dll

を参照し

  • PluginFilterAttribute
  • PluginNameAttribute
  • PluginVersionAttribute

のついたPluginBase派生のMonoBehaviourなクラスを含むDLLを作れば大丈夫です。

参考になりそうなの

https://umaiumeunion.github.io/guides_unityinjector/03_writing_plugins
多分これがすべて(書き始めて気づいた)

https://gist.github.com/neguse11/1951c3625ee7aa153a2a

https://github.com/asyetriec/COM3D2.AddYotogiSlider.Plugin/blob/master/COM3D2.AddYotogiSlider.Plugin/AddYotogiSlider.cs

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

C# 8 Pattern-matching

C# 8 でとうとうパターンマッチングが実装されたので、早速 rust っぽく Result<T> 型を実装してみました。

    public struct Result<T>
    {
        public readonly bool IsSuccess;
        public readonly T Value;
        public readonly string? Message;

        private Result(bool success, T value, string? message) =>
            (IsSuccess, Value, Message) = (success, value, message);

        public void Deconstruct(out bool success, out T value, out string? message) =>
            (success, value, message) = (IsSuccess, Value, Message);

        public static Result<T> Fail(string message) => new Result<T>(false, default, message);
        public static Result<T> Success(T value) => new Result<T>(true, value, null);
    }

こんな感じに使います。

    var tmp = SomeMethod() switch
    {
        (false, _, _) => throw new NotSupportedException(),
        (true, var value, _) => value 
    };

ただメッセージが欲しいが為につけてる message のせいであまり綺麗になってないのが難点…。Enum に対して対して値を持たせられないのでこうなるのは仕方ないかんじでしょうか。

ただ {} で指定すると必要のない値は無視できるんですが、如何せんメンバ名がごちゃごちゃして見づらい。

    // 位置指定
    switch (TryGetValue(ref input, ref length))
    {
        case (false, _, var e):
            return Result<JsonNode>.Fail(e);
        case (true, var value, _):
            values.Add(value);
            break;
    }

    // メンバ指定
    switch (TryGetValue(ref input, ref length))
    {
        case {IsSuccess:false, Message: var e}:
            return Result<JsonNode>.Fail(e);
        case {IsSuccess:true, Value: var value}:
            values.Add(value);
            break;
    }
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む