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

【ElasticSearch】bulkAPIを用いたデータ登録および検索をC#で試してみた

はじめに ElasticSearchを使ってみたい!という興味方位からElasticSearch導入~Webアプリケーションにて検索までを実施してみた。 ほぼほぼ初心者なのでお手柔らかにお願いします。 ※今回はIndex定義やMapping定義などをせず、とりあえず検索したいというレベルで開発しています。 Elastic Search導入編 下記URLを参考にElasticSearchとKibanaをインストールする https://qiita.com/azumabashi/items/a6d989f02bf0e369023a Elasticsearch と Kibana 7.5.1 Windows をインストールしてみる とりあえず使いたいレベルであればデータ登録時にIndexが自動生成されるっぽい データ登録をKibanaで一つずつ作るのは面倒くさいので、C#のConsoleアプリケーションをVisual Studioで開発する。 データ登録は下記URLにあるbulkAPIを用いる https://qiita.com/mino_s2000/items/191817f9d1d16320c478 NEST Tips - Elasticsearch .NET Client 開発方針 下記方針で開発を行った。 前提:タイトルからURLを取得するといったツールを作成する ①Consoleアプリケーションにて登録したいデータをcsvからデータ取得し、bulkAPIを用いてIndexに投入する ②MVC WebアプリケーションにてElasticSearchを用いて検索を行い、画面に検索結果を表示させる。 実際に作成したコードを下記に記載する バッチ Program.cs // 検索用(登録用のバッチのため不要) //ElasticSearch.SearchData("API"); // 登録用 // CSVからデータ取得 var list = FileOperationService.GetCsvFileForElasticSearchData(); ElasticSearch.RegistData(list); ElasticSearch.cs static class ElasticSearch { public static string ApiUrl = "http://localhost:9200"; public static string IndexName = "testindex"; public static ConnectionSettings settings = new ConnectionSettings(new Uri(ApiUrl)); public static ElasticClient client = new ElasticClient(settings.DefaultIndex(IndexName)); public static void SearchData(string searchWord) { IList<IndexModel> hits = new List<IndexModel>(); try { var response = client.Search<IndexModel>(s => s.Size(10) .Query(q => q.MatchPhrasePrefix(m => m.Field(f => f.title).Query(searchWord)))); foreach (var hit in response.Hits) { hits.Add(hit.Source); Console.WriteLine("id:" + hit.Source.id); } } catch (Exception ex) { Console.WriteLine(ex.Message); } Console.WriteLine("検索結果:" + hits.Count); Console.WriteLine("何かキーを押してください"); Console.ReadKey(); } public static void RegistData(List<IndexModel> indexModel) { var docList = new List<IndexModel>(); foreach (var index in indexModel) { docList.Add(new IndexModel { id = index.id, title = index.title, path = index.path, description = index.description } ); } var res = client.Bulk(e => e .Index(IndexName) .IndexMany(docList)); } } IndexModel.cs namespace ConsoleElasticSearchSample.Model { class IndexModel { public string id { get; set; } public string title { get; set; } public string path { get; set; } public string description { get; set; } } } FileOperationService.cs static class FileOperationService { public static string FilePath = @"C:\work\elasticsearch\"; public static List<IndexModel> GetCsvFileForElasticSearchData() { // 配列からリストに格納する var indexList = new List<IndexModel>(); // 読み込みたいCSVファイルのパスを指定して開く using (var sr = new StreamReader(FilePath + "elasticsearch.csv")) { // 末尾まで繰り返す while (!sr.EndOfStream) { // CSVファイルの一行を読み込む string line = sr.ReadLine(); // 読み込んだ一行をカンマ毎に分けて配列に格納する string[] values = line.Split(','); var index = new IndexModel(); index.id = values[0]; index.title = values[1]; index.path = values[2]; index.description = values[3]; indexList.Add(index); } } return indexList; } } オンライン 共通処理はバッチを参照 HomeController.cs public ActionResult About() { ViewBag.Message = "ElasticSearchの結果を表示します"; var list = ElasticSearch.SearchData("ela"); ViewBag.list = list; return View(); } About.cshtml @{ ViewBag.Title = "About"; } <h2>@ViewBag.Title.</h2> <h3>@ViewBag.Message</h3> <p> <table class="table table-responsive"> <tr> <th>id</th> <th>title</th> <th>path</th> <th>description</th> </tr> @for (var i = 0; i < ViewBag.list.Count; i++) { <tr> @{ <th>@ViewBag.list[i].id</th> <th>@ViewBag.list[i].title</th> <th><a href="@ViewBag.list[i].path" target="_blank">@ViewBag.list[i].path</th> <th>@ViewBag.list[i].description</th> } </tr> } </table> </p> CSV elasticsearch.csv id,title,path,description 1,Elasticsearch と Kibana 7.5.1 Windows をインストールしてみる,https://qiita.com/azumabashi/items/a6d989f02bf0e369023a,https://db-engines.com/en/ranking によると、サーチエンジンのElasticsearchはかなりいいところ(2019年12月付で全体の8位、サーチエンジンではトップ)につけています。 2,NEST Tips - Elasticsearch .NET Client,https://qiita.com/mino_s2000/items/191817f9d1d16320c478,川崎フロンターレ、J1初優勝おめでとうございます!等々力に行きたかったです。 3,Python Elasticsearch 基本的な使い方まとめ,https://qiita.com/satto_sann/items/8a63761bbfd6542bb9a2,PythonでElasticsearchを使う機会があったため情報を収集していましたが、サイトで使われているElasticsearchのバージョンが古かったり、そもそも、情報が少なかったりしたので、今回、メモとして簡単な例と共に基本的な使い方をまとめました。 開発メモ bulkAPIには「NEST」を使用しました。 NESTをVisual Studioで使用するためには、Nugetパッケージから「NEST」で検索を行い、最新バージョンをインストールすることで使用することができます。 暫定版画面 titleフィールドに登録されている「ela」を検索した結果が下記となる 入力エリアを用意して、AjaxにてElasticSearchを呼び出す仕組みを用意するとよりよいだろう。 ElasticSearchのメモ 今回検索にて「elastic」などで検索してもヒットしない現象が起きました。 GET testindex/_search { "query" : { "match": { "title": "elastic" } } } ElasticSearchでは"match"は完全一致検索となるため、部分一致検索をしたい場合は"match_phrase_prefix"を使用しないといけないみたいです。 知らなかった。。。 GET testindex/_search { "query" : { "match_phrase_prefix": { "title": "elastic" } } } 上記であれば検索可能。 最後に 今後はIndex、Mapping定義をしっかりと実施したいのと、ElasticSearchの性能検証など試してみる。 あとは検索クエリのまとめとかやってみようかなーと。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity入門】ゲーム開発ができるUnityをインストール

はじめに ゲーム開発ができるUnityを使い始めるにあたって行った内容メモ Unityの説明 ゲームやツールを開発できるC++で作成されたゲームエンジン 非常に幅広いプラットフォームに使用できる iOSやAndroidのようなスマホ向けアプリ Windows、MacOS、Linux向けのデスクトップアプリ PlayStationやXbox、WiiUなどの家庭用ゲーム機 ゲームエンジンであると同時に、開発用プラットフォームでもある。 ゲーム制作用の補助ツールやビジュアルプログラミングツールなどが含まれる。 2Dゲーム, 3Dゲームどちらも作ることができる。 直感的にゲームを作ることができる。 AssetStore(アセットストア)が提供されている。 ◆ ゲームエンジン ゲーム開発における一般的な処理(映像処理や音響処理など)を効率よく実装できるツール よく使われる処理が共通化されているため、実装作業の工数を減らすことができる。 プログラミング言語の知識が浅いクリエイターでもゲーム開発が進められるように作られている ◆ ビジュアルプログラミングツール 直感的な操作を用いてコーディングレス開発を行うツール ◆ AssetStore(アセットストア) 3Dモデルなどのゲームデータを購入できる場所(有料販売だけでなく、無料配布されている) 下記例 3Dモデル(人型、ロボット、クリーチャー) キャラクターのイラスト アイコン画像 音楽 エフェクト プログラム処理 無料で利用する条件 個人利用(ただし過去 12 か月の収益や調達した資金が 10 万米ドルを下回る方が対象) 無料のUnityプラン「Personal」 ダウンロードとインストール Unityを公式ページ(https://unity3d.com/jp/get-unity/download) から「Unity Hub をダウンロード」を押してダウンロード。 ↓ インストール ↓ (UnityHubを日本語化する) Preferences > Appearance > Languages 使い始めと試しにテンプレートで新しいプロジェクトを作ってみる UnityID作成 ↓ 「Personal」を選択 ↓ インストール > 「エディタをインストール」 ※結構時間かかる ↓ 新しいプロジェクト作成 ↓ テンプレートを選ぶ ↓ テンプレートをダウンロード ↓ プロジェクト作成 ↓ しばらく経過するとUnityが起動し 作成したプロジェクトを開く ※ ここでUnityとUnityHubは別アプリなのかと気がつく Unityを日本語化する 日本語化は「UnityHub」から行う。 UnityHubを起動し、「インストール」 > 「アイコン」をクリック > 「モジュールを加える」> 言語パック「日本語」を選んでチェックを入れ > 「インストール」 ↓ Unity再起動 ※再起動しないと表示されない ↓ Unity > Preferences > Languages > 「English」→「Japanese」に変更 ※ Unityを再起動する ※ 上記はMacの場合 Unityの説明 画面(枠の色) 説明 シーン(赤) オブジェクトなどを配置する画面。マウスのドラッグなどで画面を回転することができる。 インスペクタ(黄色) シーンやオブジェクトのプロパティを表示 ヒエラルキー(緑) 左側の画面:現在編集しているシーンに存在しているオブジェクトの一覧 プロジェクト(水色) コンソール 下部の画面:今開いているプロジェクトの構成。 シーンやパッケージ等のフォルダが表示される。 役に立つカメラ視点の操作方法 上の方のボタンを教えてもできそうだけど、効率を考えると覚えたが良さそう。 (Blenderを調べたときも誰かが似たようなこと言っていた) ※たまたま見つけただけなのでオブジェクトのアップのショートカットは不明 ※ 2点ドラッグも目のアイコンになるけどよくわからん 操作 方法 中心を支点に回転 option(手から目のアイコンに変化する) + ドラッグ 拡大 と 縮小 2点で上下スライド(スクロールの時のやつ) カメラ視点移動 option + command + 1点ドラッグ オブジェクトのアップ ヒエラルキーのオブジェクトダブルクリック 最後に 所感、Blenderのときは実行できないのでつまらなかったが、こっちは実行できるので面白かった
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

kintone のセキュアアクセス環境で Windows アプリからのデータ取得

kintoneセキュアアクセス環境でクライアント証明書を使ったWindowsアプリからのデータ取得を試しました。自己メモとして、参考情報のリンクとコード例をこちらに残しておきます。 kintoneのセキュアアクセスについて セキュアアクセスは、クライアント証明書を使って接続元の端末を認証する方法で、kintoneをはじめとした cybouz 社のクラウドサービス(cybouz.com)で利用可能です。 cybouz.com でセキュアサイトは、kintoneの接続にIPアドレス制限を行った場合に外部からの接続手段として用いられます。詳細は以下に説明を参照ください。 cybouz.com セキュアアクセスとは https://jp.cybozu.help/general/ja/admin/list_security/list_access/secureaccess.html cybouz.com IPアドレス制限とは https://jp.cybozu.help/general/ja/admin/list_security/list_access/ip_restrictions.html セキュアアクセスはクライアント証明書をブラウザに登録することで、安全に外部から接続できる仕組みです。クライアント証明書のセキュリティ的なメリットを知りたい方はこちらを参照すると良いでしょう。 クライアント証明書 総合解説サイト https://jp.globalsign.com/lp/epki/ 【図解】クライアント証明書(https,eap-tls)の仕組み https://milestone-of-se.nesuke.com/sv-advanced/digicert/client-cert/ 実際にクライアント証明書を発行し、ブラウザに登録して利用するまでは以下を参照すると良いでしょう。 セキュアアクセスを設定する https://jp.cybozu.help/general/ja/admin/list_security/list_secureaccess/secureaccess.html クライアント証明書の登録方法 https://jp.cybozu.help/ja/settings/browser/certificate.html 今回の試験実装 前置きが長くなりましたが、今回はブラウザではなくWindows環境で動くアプリを開発して、外部API経由でkintoneのセキュアアクセスする方法を実際に試してみました。 試験環境 今回の試験環境はOS Windows 10 Professional のPCを用いて、開発ツールはVisual Studio 2017、開発言語はC#で .NET Freamework 4.8のwindows formsプロジェクトを準備しました。kintone外部APIでのデータ受け渡しはJSON形式になりますので、以下のパッケージをnugetで追加しています。 Newtonsoft.Json.13.0.1 https://www.nuget.org/packages/Newtonsoft.Json/ 実装について Windowsのフォームに配置したボタンをクリックすると、kintoneのセキュアアクセス経由で指定したアプリのレコードを読み込み、テキストフィールドに結果をJSONフォーマットで表示するテスト用のアプリを実装しました。 今回の試験実装のポイントはHttpWebRequestでクライアント証明書を使う方法でした。情報は少なかったものの、しつこく検索した結果X509Certificate2でクライアント証明書を参照し、HttpWebRequestのClientCertificates.Add()で追加するだけで良いことがわかり、以下のように以外と簡単に実装可能でした。 KintoneApp.CS using System; using System.IO; using System.Net; using System.Security.Cryptography.X509Certificates; using System.Text; namespace kintoneSecureAccsessTest { public static class KintoneApp { private static readonly string apiUrl = "https://<サブドメイン名>.s.cybozu.com/k/v1/records.json"; private static readonly string apiToken = "<アプリのtoken>"; private static readonly string appId = "<アプリID>"; private static readonly string query = "<クエリ文>"; private static readonly string certPath = @"<証明書ファイルのパス>"; private static readonly string certPasswd = @"<証明書パスワード>"; public static string DataGet() { // TLS1.1、TLS1.2 対応 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12; // token を利用して kintone のレコードを取得 string url = apiUrl + Uri.EscapeUriString("?app=" + appId + "&query=" + query + "&totalCount=true"); HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url); // kintone クライアント証明書を追加 X509Certificate2 Cert = new X509Certificate2(certPath, certPasswd, X509KeyStorageFlags.PersistKeySet); req.ClientCertificates.Add(Cert); // ヘッダーにTokenを追加 req.Headers.Add("X-Cybozu-API-Token:" + apiToken); // kintone からのレスポンスをテキストで返す using (var respons = req.GetResponse()) { using (var stream = respons.GetResponseStream()) { var sr = new StreamReader(stream, Encoding.GetEncoding("UTF-8")); string text = sr.ReadToEnd(); sr.Close(); stream.Close(); return text; } } } } } 蛇足までにフォームのコードは以下です。 Form1.CS using Newtonsoft.Json; using System; using System.Windows.Forms; namespace kintoneSecureAccsessTest { public partial class Form1 : Form { public Form1() { InitializeComponent(); } private void button1_Click(object sender, EventArgs e) { // kintoneクライアント証明書を使って取得したアプリのレコードをJSONで表示 var respons = KintoneApp.DataGet(); this.textBox1.Text = JsonConvert.SerializeObject( JsonConvert.DeserializeObject(respons), Formatting.Indented); } } } 参考資料 HttpClient でクライアント証明書を設定する方法 https://qiita.com/volpe28v/items/95efad9e8816b00b348c X509Certificate2 クラス https://docs.microsoft.com/ja-jp/dotnet/api/system.security.cryptography.x509certificates.x509certificate2?view=net-6.0
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# の Json.NET を理解する

C# を使うときに、今までふわっと理解して適当に使っていた Json.NETだけど、一度体系的にしっかり理解してみようと思った。Introductionやそのドキュメントを読んで、サンプルを書いて理解してみよう。 現在では、C# の JSON を操作するライブラリとしては、一択感のある Json.NET だが、大きく分けると2つの機能を有する。 Json を C#のオブジェクトにシリアライズ、デシリアライズする。 Json を 手動で書いたり、読んだり、クエリーしたりする。LINQ to JSON という名前で呼ばれている。 の大きく2つに分類される。 シリアライズとデシリアライズ SerializeObject と DeserializeObject<T> が最も頻繁に使うメソッドになります。C# のオブジェクトに対して、JSONをシリアライズしたり、デシリアライズします。 public static void Execute() { var product = new Product() { Name = "Apple", ExpiryDate = DateTimeOffset.Parse("2008/12/28"), Price = 3.99M, Sizes = new string[] { "Small", "Medium", "Large" } }; string output = JsonConvert.SerializeObject(product); Console.WriteLine(output); var deserializedProduct = JsonConvert.DeserializeObject<Product>(output); } 実行結果 {"Name":"Apple","ExpiryDate":"2008-12-28T00:00:00-08:00","Price":3.99,"Sizes":["Small","Medium","Large"]} おそらく 90% ぐらいのユースケースでは、この文法を知っていると対応できると思います。しかし、たまに対応できないケースがあります。 例えば私は durabletaskにコントリビュートしたときに、次の3つのシリアライズの設定をしています。この TraceContextBase というオブジェクトはサブクラスがあり、自分の子のオブジェクトのリファレンスを持っています。このオブジェクトをシリアライズして、デシリアライズするためには、どのTraceContextBase のサブクラスがシリアライズされたのかという情報を持つ必要があります。それは、TypeNameHandling という設定で実現されています。具体的には、型名がシリアライズされて保存されます。また、PreserveReferenceHandling によって、参照を持てるようになります。また、ReferenceLoopHandling によって、リファレンスが循環するようなケースも対応されます。それぞれについて解説してみましょう。 TraceContextBase.cs static TraceContextBase() { CustomJsonSerializerSettings = new JsonSerializerSettings() { TypeNameHandling = TypeNameHandling.Objects, PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Serialize, }; } TypeNameHandling Json にシリアライズを行うときに型情報を追加します。例えば、このようなクラスを作成します。インターフェイスとそのサブクラスがあります。 Node.cs public interface Node { string Name { get; } void Print(); } public class NodeLeaf : Node { public string Name { get; set; } public void Print() { Console.WriteLine($"I am {this.GetType()}"); } } public class NodeComposite : Node { public string Name { get; set;} public void Print() { Console.WriteLine($"I am {this.GetType()}"); } } このオブジェクトをシリアライズしてみましょう。同じインターフェイスですが実装の型が違います。 var nodes = new List<Node> { new NodeLeaf { Name = "I'm file."}, new NodeComposite { Name = "I'm directory."} }; これをTypeNameHandlingをつけてシリアライズしてみます。 var typeNameList = JsonConvert.SerializeObject(nodes, Formatting.Indented, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Objects }); Console.WriteLine("typeNameHandling: " + typeNameList); この箇所はつぎのように表示されます。きれいにシリアライズされています。オブジェクトに $typeが追加されていて、型情報が追加されているのがわかると思います。このシリアライズの設定もTypeNameAssemblyFormatによって実施することが可能です。これをFull に設定すると、アセンブリ名やバージョン名などを付加することも可能です。 実行結果 typeNameHandling: [ { "$type": "JTokenSPike.NodeLeaf, JTokenSPike", "Name": "I'm file." }, { "$type": "JTokenSPike.NodeComposite, JTokenSPike", "Name": "I'm directory." } ] デシリアライズするときも設定を追加します。JsonSerializerSettingsと、TypeNameHandling を設定しないと、この場合は、Exception になります。型情報を読み取らなければ、Node はインターフェイスなのでデシリアライズできないからです。 var deserializedNodeList = JsonConvert.DeserializeObject<IList<Node>>(typeNameList, new JsonSerializerSettings { TypeNameHandling = TypeNameHandling.Objects }); foreach (var node in deserializedNodeList) { node.Print(); } 実行結果も思った通りですね。 実行結果 I am JTokenSPike.NodeLeaf I am JTokenSPike.NodeComposite PreserveReferenceHandling 次に、参照を保持したいときにどうしたらいいかを見てみましょう。 DirectoryAndFile public class Directory { public string Name { get; set; } public Directory Parent { get; set; } public IList<File>? Files { get; set; } } public class File { public string Name { get; set; } public Directory Parent { get; set; } } Directory と File は参照を持っています。これを普通にシリアライズすると、Exception になります。Directory が File を持っていて、Fileが、Directoryを持っているからです。 try { JsonConvert.SerializeObject(document, Formatting.Indented); } catch (JsonSerializationException ex) { Console.WriteLine("Expected: " + ex.ToString()); } 実行結果 Expected: Newtonsoft.Json.JsonSerializationException: Self referencing loop detected for property 'Parent' with type 'JTokenSPike.Directory'. Path 'Files[0]'. at Newtonsoft.Json.Serialization.JsonSerializerInternalWriter.CheckForCircularReference(JsonWriter writer, Object value, JsonProperty property, JsonContract contract, JsonContainerContract containerContract, JsonProperty containerProperty) 次のように設定します。 PreserveRerferenceHandling.All var preserveReferenceAll = JsonConvert.SerializeObject(document, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.All }); Console.WriteLine("All: " + preserveReferenceAll); $id が作成され、参照されているケースはリファレンスとして表現されています。 実行結果 All: { "$id": "1", "Name": "My Documents", "Parent": { "$id": "2", "Name": "Root", "Parent": null, "Files": null }, "Files": { "$id": "3", "$values": [ { "$id": "4", "Name": "Important Legal Document.docs", "Parent": { "$ref": "1" } } ] } } シリアライズの方法は、All以外にもあります。Object は、オブジェクトしてシリアライズを行います。 Object var preserveReferenceObject = JsonConvert.SerializeObject(document, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects }); Console.WriteLine("Object: " + preserveReferenceObject); 先ほどと異なって、Files 部分の下が Array として表現されています。 実行結果 Object: { "$id": "1", "Name": "My Documents", "Parent": { "$id": "2", "Name": "Root", "Parent": null, "Files": null }, "Files": [ { "$id": "3", "Name": "Important Legal Document.docs", "Parent": { "$ref": "1" } } ] } 他にも Array というのがあります。実行結果は、Allと同じように見えます。ただし、Allと違って、後で解説するReferenceLoopHandling を設定してあげないと、正しく動作しません。 Array var preserveReferenceArrays = JsonConvert.SerializeObject(document, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Arrays, ReferenceLoopHandling = ReferenceLoopHandling.Serialize }); Console.WriteLine("Array: " + preserveReferenceArrays); 実行結果 Array: { "Name": "My Documents", "Parent": { "Name": "Root", "Parent": null, "Files": null }, "Files": { "$id": "1", "$values": [ { "Name": "Important Legal Document.docs", "Parent": { "Name": "My Documents", "Parent": { "Name": "Root", "Parent": null, "Files": null }, "Files": { "$ref": "1" } } } ] } } Object と、Arrayの違いは何なのでしょう?私の推測でしかないのですが、小さいコードを書いてみます。 ObjectAndArray var files = new List<File> { new File {Name = "doc 1"}, new File {Name = "doc 2"} }; var filesObject = JsonConvert.SerializeObject(files, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects }); Console.WriteLine("Object: " + filesObject); var arrayObject = JsonConvert.SerializeObject(files, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Arrays }); Console.WriteLine("Array: " + arrayObject); List だけを作ってシンプルにしました。実行結果の違いは、配列の中としてオブジェクトが表現されているか、オブジェクトの中のValueとして、配列が表現されているかの違いです。ここからはマニュアルから読み取れないので私の推測ですが、JToken という最上位の概念の下に、JObject, JArray という概念が存在します。それと対比していると考えると、Object としてシリアライズするとは、配列をオブジェクトとして解釈しないので、オブジェクトの配列として認識し、Arrayとしてシリアライズするなら、もし Arrayがあったとしたら、JArrayのオブジェクトつまり、配列を表現できるオブジェクトとしてシリアライズするために下記のような仕様になっているのではないでしょうか? 実行結果 Object: [ { "$id": "1", "Name": "doc 1", "Parent": null }, { "$id": "2", "Name": "doc 2", "Parent": null } ] Array: { "$id": "1", "$values": [ { "Name": "doc 1", "Parent": null }, { "Name": "doc 2", "Parent": null } ] } ReferenceLoopHandling 既に出てきていますが、循環参照をどのように解決するかです。この設定を入れないと、永遠にシリアライズし続けてしまいます。 Employee.cs public class Employee { public string Name { get; set; } public Employee Manager { get; set; } } ReferenceLoopHandling の設定付きでシリアライズすると下記のような循環参照もシリアライズ可能です。その際は、PreserveReferenceHandlingと併用します。 ReferenceLoopHandling var joe = new Employee { Name = "Joe User" }; var mike = new Employee { Name = "Mike Manager" }; joe.Manager = mike; mike.Manager = mike; var loopReferenceHandling = JsonConvert.SerializeObject(joe, Formatting.Indented, new JsonSerializerSettings { PreserveReferencesHandling = PreserveReferencesHandling.Objects, ReferenceLoopHandling = ReferenceLoopHandling.Serialize }); Console.WriteLine("Loop: " + loopReferenceHandling); きれいにシリアライズされていますね。 実行結果 Loop: { "$id": "1", "Name": "Joe User", "Manager": { "$id": "2", "Name": "Mike Manager", "Manager": { "$ref": "2" } } } Linq to Json いくつかのケースで、自分で Json のオブジェクトを作って、フィルタをかけたいケースがあります。その場合は、Linq to Json という機能を使います。Json のオブジェクトは主要なものには次のものがあります。 JTokenとサブクラス JToken - abstract base class JContainer - abstract base class of JTokens that can contain other JTokens JArray - represents a JSON array (contains an ordered list of JTokens) JObject - represents a JSON object (contains a collection of JProperties) JProperty - represents a JSON property (a name/JToken pair inside a JObject) JValue - represents a primitive JSON value (string, number, boolean, null) 上記のクラス階層になっていますので、Json をパースすると、上記のオブジェクトのツリーが作られます。サンプルを見てみましょう。Newtonsoft.Json.Linq namespace を using すると使用できるようになります。JObject.Parseメソッドを使うと、オブジェクトがパースできるようになります。 LinqToJson JObject o = JObject.Parse(@"{ 'CPU': 'Intel', 'Drives': [ 'DVD read/writer', '500 gigabyte hard drive' ] }"); string cpu = (string)o["CPU"]; Console.WriteLine($"CPU: {cpu}"); string firstDrive = (string)o["Drives"][0]; Console.WriteLine($"First Drive: {firstDrive}"); IList<string> allDrives = o["Drives"].Select(t => (string)t).ToList(); foreach (var drive in allDrives) { Console.WriteLine("Drive: " + drive); } Arrayの場合、JArrayでパースすることもできます。 Array string json = @"[ 'Small', 'Medium', 'Large' ]"; JArray a = JArray.Parse(json); Console.WriteLine($"0: {a[0]}, 1: {a[1]}, 2: {a[2]}"); 実行結果 0: Small, 1: Medium, 2: Large StreamReader の ReadFrom メソッドを直接読むことも可能です。computer.json のファイル名で、先ほどの、CPUと同じJsonを格納しています。インテリセンスによると、Async メソッドも用意されていました。 using(StreamReader reader = System.IO.File.OpenText(@"computer.json")) { JObject obj = (JObject)JToken.ReadFrom(new JsonTextReader(reader)); Console.WriteLine($"CPU: {obj["CPU"]}"); } 実行結果 CPU: Intel Linq を使用する 普通に、JObject を使う次のようなコードになり、煩雑ですね。 string json = @"{ 'channel': { 'title': 'James Newton-King', 'link': 'http://james.newtonking.com', 'description': 'James Newton-King\'s blog.', 'item': [ { 'title': 'Json.NET 1.3 + New license + Now on CodePlex', 'description': 'Announcing the release of Json.NET 1.3, the MIT license and the source on CodePlex', 'link': 'http://james.newtonking.com/projects/json-net.aspx', 'categories': [ 'Json.NET', 'CodePlex' ] }, { 'title': 'LINQ to JSON beta', 'description': 'Announcing LINQ to JSON', 'link': 'http://james.newtonking.com/projects/json-net.aspx', 'categories': [ 'Json.NET', 'LINQ' ] } ] } }"; JObject rss = JObject.Parse(json); string rssTitle = (string)rss["channel"]["title"]; // James Newton-King string itemTitle = (string)rss["channel"]["item"][0]["title"]; // Json.NET 1.3 + New license + Now on CodePlex JArray categories = (JArray)rss["channel"]["item"][0]["categories"]; // ["Json.NET", "CodePlex"] IList<string> categoriesText = categories.Select(c => (string)c).ToList(); // Json.NET // CodePlex Linq を使った例 上記のものを Linq を使ってクエリーしてみます。 シンプルに Select で、タイトルを抽出してみます。素直に実行できていますね。 JObject rss = JObject.Parse(channel); var postTitles = rss["channel"]["item"].Select(p => p["title"]); foreach(var title in postTitles) { Console.WriteLine($"Title: {title}"); }; 実行結果 Title: Json.NET 1.3 + New license + Now on CodePlex Title: LINQ to JSON beta もう少し複雑なケースで、上記の Json にある、category の部分をクエリーします。二重配列になってしまうので、SelectManyを使って、flatten します。その後、GroupByを使って、グループ化した値を使って新しいオブジェクトを作ります。GroupBy の第一引数が、グループ化したい対象を表すための Function で、2つ目の Function は、Functionを実行した結果が、各グループのリストとして、3つ目の Function に渡されます。ここでは、その値を使って、Count() でグループに属するメンバーの数を数えています。最後に降順ソートを実施して終了。 var categories = rss["channel"]["item"].SelectMany(p => p["categories"]).Values<string>() .GroupBy(k => k, v => v, (k, vs) => new { Key = k, Value = vs.Count() }).OrderByDescending(p => p.Value); foreach(var c in categories) { Console.WriteLine($"{c.Key}: {c.Value}"); } これは楽ちんですね。 実行結果 Json.NET: 2 CodePlex: 1 LINQ: 1 手動で強引にデシリアライズ 本来は、下記の Parse はうまく動作しません。なぜかというと、.NET Ojbect のプロパティと、JSON が一致していないからです。ケースセンシティブだからです。ところが、Parseすると、lower case ではアクセスできるのでそれを利用して強引にデシリアライズを実行しているサンプルです。 Shortie public class Shortie { public string Original { get; set; } public string Shortened { get; set; } public string Short { get; set; } public ShortieException Error { get; set; } } public class ShortieException { public int Code { get; set; } public string ErrorMessage { get; set; } } string jsonText = @"{ 'short': { 'original': 'http://www.foo.com/', 'short': 'krehqk', 'error': { 'code': 0, 'msg': 'No action taken' } } }"; JObject json = JObject.Parse(jsonText); Shortie shortie = new Shortie { Original = (string)json["short"]["original"], Short = (string)json["short"]["short"], Error = new ShortieException { Code = (int)json["short"]["error"]["code"], ErrorMessage = (string)json["short"]["error"]["msg"] } }; Console.WriteLine(shortie.Original); // http://www.foo.com/ Console.WriteLine(shortie.Error.ErrorMessage); // No action taken SelectToken SelectToken を書くと、クエリをダイナミックに定義することも可能です。ちなみに、SeelctTokenは、JsonPathもサポートしていますので、かなり複雑なことが出来そうですが、それはそれで頭が混乱しそうなので、SelectTokenと、Linq の組み合わせで十分な気がしますので、あまり調べないことにします。LinqToJsonの最初のサンプルを使ってクエリしてみましょう。 SelectToken var link = (string)rss.SelectToken("channel.item[1].link"); Console.WriteLine($"SelectToken: Link: {link}"); 実行結果 SelectToken: Link: http://james.newtonking.com/projects/json-net.aspx まとめ これでざっくり知りたかったことはわかったので、このブログは一旦終了です。Json.NET のページに行くと Performance Tips とか SerializationAttributesとか面白そうなところは残っていますが、今回のブログは一旦ここまで。また次のお楽しみにしておきます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

MetricsEventSourceを利用する

はじめに System.Diagnostics.Metricsの記事で軽く触れた、MetricsEventSourceなるものについて、こちらで解説を入れる。 System.Diagnostics.Metricsについての基本的なことについては上記記事を参照のこと。 MetricsEventSourceとは 簡単に言うと、 Metricsで各種Instrumentが出している値を、EventSourceのイベントとして出力しているクラス である。 EventSourceについてはdocs.microsoft.comの記事を参照。 普通InstrumentはMetricsListenerで拾うのが筋だが、外部ツール等で情報を一時的に収集したいときなどに使う。 dotnet-traceでEventSourceのイベントをEventPipe経由で収集できるので、MetricsEventSourceを指定して収集するというわけである。 使い方 サンプルプログラム 今回は例として以下のようなプログラムを走らせるとする。 このプログラムでは、 Meterの名前"appmeter"を作成 Counter<int>のカウンター"counter1"を作成 "counter1"の値2で、tag1=1とtag1=2となるイベントを交互に500msecの感覚で発生させる ということを行っている。 using System.Diagnostics.Metrics; { Console.WriteLine(System.Diagnostics.Process.GetCurrentProcess().Id); using var meter = new Meter("appmeter"); var counter = meter.CreateCounter<int>("counter1", "unit", "desc"); async Task CounterTask(CancellationToken ct) { int count = 2; int tagval = 1; while(!ct.IsCancellationRequested) { await Task.Delay(500).ConfigureAwait(false); if(counter.Enabled) { counter.Add(count, new KeyValuePair<string, object?>("tag1", tagval) ); } tagval = tagval ^ 3; } } using var cts = new CancellationTokenSource(); await Task.WhenAll(CounterTask(cts.Token), Task.Run(() => { // エンターキーを押したら終了 Console.ReadLine(); cts.Cancel(); })); } 収集の開始 収集の開始はdotnet-trace collectを使用する。ここで指定するプロバイダはSystem.Diagnostics.Metricsで、簡単な例として以下のように指定する。 dotnet-trace collect -p [対象プロセスのPID] --providers "System.Diagnostics.Metrics:::Metrics=appmeter" このコマンドの--providersの引数でMetricsEventSourceの監視対象や監視間隔を指定できる。この--providersの引数の構造だが、 [EventSource名]:[監視フラグ(デフォルト0xffffffff=全て]:[レベル(デフォルト5=Verbose)]:[EventSource固有の引数(';'区切りのA=B形式)]のようになっている。 つまり、上記コマンドの意味は、 System.Diagnostics.Metricsという名前のEventSource名の イベントの監視フラグを全て立て 監視レベルを5にして System.Diagnostics.Metricsへの引数としてMetrics=appmeterというキーと値を渡す という意味になる。 System.Diagnostics.Metricsへ渡せるパラメーターとして、以下のようなものがある SessionId トレースセッションを判別するためのID 一つのトレース開始から終了まで保持され、各MetricsEventSourceで発生するイベントに値が付与される デフォルトはランダムなGUID Metrics 何のInstrumentを監視するか指定する 形式は[Meter名]あるいは[Meter名]\[カウンタ名]の二つがある 複数監視する場合は";"で区切る dotnet-traceの引数リストは;で分ける仕様なので、値部分を二重引用符(")で囲むこと RefreshInterval カウンタの値をとる間隔(sec) doubleなので、0.5秒のような指定も可 MetricsEventSourceは記録された値を保持する処理をするものがあるので、メモリ制約にも気を付けること MaxTimeSeries 一つのセッションで記録されるInstrumentの数 早い者勝ち デフォルトは1000 超えた場合、 ここで保持のためのメモリを消費するのはMeterを保持している側なので注意 MaxHistograms 一つのセッションで記録されるHistograms/ObservableGaugeの数 デフォルトは20 超えた場合、HistogramLimitReachedイベントが起こり、それ以降Histogram系メトリックが作成されても新規追加されなくなる イベントキーワード EventSourceには、キーワードというフラグのようなものがあり、これを指定することによってカテゴリごとに収集するかしないかを指定できる。実際はenumなので、指定するときは数値型に直して指定することになる。 MetricsEventSourceで指定できるキーワードは以下 Messages(0x1) : 主にデバッグ用途で発生されるイベントのキーワード TimeSeriesValues(0x2) : 実際の値が収集されたイベントのキーワード InstrumentPublishing(0x4) : Instrumentが作成されたときに発生するイベントのキーワード 発生しうるイベント Message(1) Keyword=Messages 主にデバッグ用途で使われるその他のメッセージ。 パラメーターはstring messageのみ CollectionStart(2) Keyword=TimeSeriesValue RefreshIntervalで設定された秒数ごとに発生する、メトリックの収集開始イベント 渡されてくるパラメーターは、 string sessionId : セッションID(イベントパラメーターSessionIdの値) DateTime intervalStartTime : 収集開始時刻 DateTime intervalEndTime : 次の収集開始時刻 の三つ CollectionStop(3) Keyword=TimeSeriesValue メトリック収集が終わったら発生するイベント。渡される引数はCollectionStartと同一 CounterRateValuePublished(4) Keyword=TimeSeriesValue Counter<T> と、 ObservableCounter<T> の値を収集したときに発生するイベント。 パラメーターは以下 string sessionId : セッションID string meterName : Meter 作成時に指定した名前 string meterVersion : Meter 作成時に指定した名前 string instrumentName : Instrumentを作成時に指定した名前 string unit : Instrument作成時に指定した単位 string tags : 収集時に追加されたタグの、 , 区切りのキーと値のペア(なければ空文字列) string rate : 増分(少数と整数両方の場合が存在) rateには Counter<T> は、前回収集された時点からAddされた値の総計値が渡される。 ObservableCounter<T> は、 [その時収集した値] - [前回収集した値] の値が渡される。 実際の出力(CSV) GaugeValuePublished(5) Keyword=TimeSeriesValue ObservableGauge<T> の値を収集したときに発生するイベント。 パラメーターは以下 string sessionId : セッションID string meterName : Meter 作成時に指定した名前 string meterVersion : Meter 作成時に指定した名前 string instrumentName : Instrumentを作成時に指定した名前 string unit : Instrument作成時に指定した単位 string tags : 収集時に追加されたタグの、 , 区切りのキーと値のペア(なければ空文字列) string lastValue : その時収集された値 実際の出力(CSV) HistogramValuePublished(6) Keyword=TimeSeriesValue Histogram<T> の値を収集したときに発生するイベント パラメーターは以下 string sessionId : セッションID string meterName : Meter 作成時に指定した名前 string meterVersion : Meter 作成時に指定した名前 string instrumentName : Instrumentを作成時に指定した名前 string unit : Instrument作成時に指定した単位 string tags : 収集時に追加されたタグの、 , 区切りのキーと値のペア(なければ空文字列) string quantiles : 前回収集された後に発行されたRecord(value)の値の50%ile、95%ile、99%ileの値を、以下の形式でフォーマットしたもの(高い値の方が1に近くなる) 0.5=[50%ileの値];0.95=[95%ileの値];0.99=[99%ileの値] 実際の出力(CSV) BeginInstrumentReporting(7)/EndInstrumentReporting(8) Keyword=TimeSeriesValue Instrumentの監視を開始/終了したときに発生する。 パラメーターは以下 string sessionId : セッションID string meterName : Meter 作成時に指定した名前 string meterVersion : Meter 作成時に指定した名前 string instrumentName : Instrumentを作成時に指定した名前 string instrumentType : Instrumentの型 string unit : Instrument作成時に指定した単位 string description : Instrument作成時に指定した概要 Error(9) Keyword=TimeSeriesValues | Messages | InstrumentPublishing なにがしかの例外が、Instrument監視中に内部で発生したときに発生するイベント パラメーターは以下 string sessionId : セッションID string errorMessage : エラーメッセージ InitialInstrumentEnumerationComplete(10) Keyword=TimeSeriesValues | InstrumentPublishing セッションの初期化が終わって、監視を開始する時に発生するイベント パラメーターは以下 string sessionId : セッションID InstrumentPublished(11) Keyword=InstrumentPublishing Instrumentが作成されて、セッションに捕捉されたときに発生するイベント。 セッションの開始以前に作られたInstrumentは初期化後にこのイベントが発生し、監視対象に入れられる TimeSeriesLimitReached(12) Keyword=TimeSeriesValues 監視されるInstrumentの数がMaxTimeSeriesに到達したときに発生するイベント。 パラメーターは string sessionId のみ。 これ以後新規に発生するメトリックに関しては、イベントパラメーターMetricsに合致しても無視される。 なお、タグの値が別でも別物として扱われることに注意。 (tag1=aとtag1=bのメトリックがあると、それぞれTimeSeriesとしては別物として扱われる) HistogramLimitReached(13) Keyword=TimeSeriesValues 監視されるHistogram<T>の数が上限に達したときに発生するイベント。 パラメーターはstring sessionIdのみ。 これ以後新規に発生するHistogram<T>のメトリックに関しては無視される。 Histogramのカウントに現在バグがあるので、MaxHistogramsの値は必ず超えないようにすること。 なお、修正PRはマージされているので遅くともnet7.0にはこの問題は解消する。 ObservableInstrumentCallbackError(14) Keyword=TimeSeriesValues ObservableInstrument<T> のコールバックの中で例外が発生したときに呼ばれる。 パラメーターは以下。 string sessionId : セッションID string errorMessage : エラーメッセージ MultipleSessionsNotSupportedError(15) Keyword=TimeSeriesValues | Messages | InstrumentPublishing MetricsEventSourceは 現在二つ以上のセッションを同時展開することはできない仕様となっている。 このイベントは、セッションを追加しようとしたときに発生するエラーイベントとなる。 引数はstring sessionIdのみ 注意点 執筆時点の最新版リリースでは以下の問題がある dotnet-traceがEventSourceに関するパラメーターをうまくパースできない 二つ以上のEventSourceパラメーターがあると、最後のもの以外正常に解釈できない v6.0.257301で解消 MaxHistogramsを超えた状態でさらにHistogramsのRecordを追加しようとすると、MaxCountersの制限に引っかかる場合がある MaxHistogramsを超えない分には問題ないが、超えてから更に超えた分のHistograms<T>.Recordが発生すると、なぜかMaxCountersの制限に引っかかって、それ以後Counterが作成されても監視対象にならなくなる 遅くともdotnet7では解消される予定 終わりに Metricsの監視をアプリに組み込む場合は普通にMetricsListenerで処理すればいい話なので、MetricsEventSourceを使う機会は少ないかもしれない。 しかし、こういうものが存在するということがわかっていれば、例えば開発中や、あるいは緊急対応の時などにアドホックに値を監視することによって、トラブルシューティングの役に立つかもしれない。 注意点を最後に挙げたが、これは修正され次第また記事を更新することにする。 参考リンク 実験ソース 6.0時点のソース
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

WPFでMaterialDesign導入したからMessageBoxもおしゃれにしたい!

はじめに  WPFでMaterialDesignThemes導入してせっかく見た目をかっこよくしても、 MessageBoxを表示させてみたら標準のままでダサかった…。 MaterialDesignThemesでダイアログを出す方法はなんとなくわかったけど、 標準のMessageBoxライクに簡単に表示させたい。 こちらは、そんな方に向けた記事です。 環境 .NET 5.0 C# Prism.Unity MaterialDesignThemes ダイアログView まずはUserControlとして、さくっとダイアログ本体となるものを用意します。 これさえ作っちゃえば、あとはサクサクッとできちゃいます。 MyMessageBox.xaml <UserControl x:Class="TestApp.Views.MyMessageBox" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"> <Grid Margin="16"> <Grid.RowDefinitions> <RowDefinition /> <RowDefinition /> </Grid.RowDefinitions> <StackPanel Grid.Row="0" Orientation="Vertical"> <TextBlock FontSize="20" FontWeight="Bold" Style="{StaticResource MaterialDesignBody1TextBlock}" Text="{Binding DialogTitle}" /> <TextBlock FontSize="16" FontWeight="Medium" Style="{StaticResource MaterialDesignBody1TextBlock}" Text="{Binding DialogText}" /> </StackPanel> <StackPanel Grid.Row="1" Margin="0,16,0,0" HorizontalAlignment="Right" Orientation="Horizontal"> <Button Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" CommandParameter="キャンセル" Content="キャンセル" IsCancel="True" Style="{StaticResource MaterialDesignFlatButton}" /> <Button Margin="8,0,0,0" Command="{x:Static materialDesign:DialogHost.CloseDialogCommand}" CommandParameter="OK" Content="OK" IsDefault="True" Style="{StaticResource MaterialDesignFlatButton}" /> </StackPanel> </Grid> </UserControl> 縦並びのダイアログのタイトル・テキストエリア、 横並びのキャンセル・OKボタンエリアに分かれてます。 文字の大きさやボタンの配置などはこのあたりを参考に。 CommandParameterはダイアログのボタンを押したときに返ってくる値ですね。 文字列じゃなくても、boolにしたり、intにしたり、お好きなように。 ダイアログを表示させるViewとViewModel ManWindow.xaml <Window x:Class="TestApp.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes" xmlns:prism="http://prismlibrary.com/" Title="{Binding Title}" Width="525" Height="350" prism:ViewModelLocator.AutoWireViewModel="True"> <materialDesign:DialogHost CloseOnClickAway="True" DialogTheme="Inherit"> <Grid> <Button Width="100" Command="{Binding ClickCommand}" Content="ボタン" Style="{StaticResource MaterialDesignRaisedButton}" /> <ContentControl prism:RegionManager.RegionName="ContentRegion" /> </Grid> </materialDesign:DialogHost> </Window> DialogHostで囲んだ部分を覆うようにダイアログのグレー背景、ダイアログが表示されるようになります。 なので画面全体に対して表示させたい場合は上記のようにします。 CloseOnClickAwayプロパティはダイアログの外側をクリックで閉じられるようにするかどうかのプロパティです。 あとはボタン等のコントロールにCommandを設定してやり、ViewModelで呼ぶだけ。 MainWindowViewModel.cs using MaterialDesignThemes.Wpf; using Prism.Commands; using Prism.Mvvm; using System.Diagnostics; using TestApp.Views; namespace TestApp.ViewModels { public class MainWindowViewModel : BindableBase { public MainWindowViewModel() { } // プロパティ private string _title = "MessageBoxApplication"; public string Title { get { return _title; } set { SetProperty(ref _title, value); } } private string _dialogTitle; public string DialogTitle { get { return _dialogTitle; } set { SetProperty(ref _dialogTitle, value); } } private string _dialogText; public string DialogText { get { return _dialogText; } set { SetProperty(ref _dialogText, value); } } // コマンド private DelegateCommand<string> _clickCommand; public DelegateCommand<string> ClickCommand => _clickCommand ?? (_clickCommand = new DelegateCommand<string>(ExecuteCommandName)); // コマンド実行メソッド private async void ExecuteCommandName(string parameter) { DialogTitle = "確認"; DialogText = "ダイアログの説明文です。よろしいですか?"; var result = await DialogHost.Show(new MyMessageBox()); if ((string)result == "OK") { Debug.Print("OKが押されました。"); } else if ((string)result == "キャンセル") { Debug.Print("キャンセルが押されました。"); } else { Debug.Print("ダイアログの外が押されました。"); } } } } ダイアログのタイトル・テキスト設定する部分が不格好なので改善の余地ありですが…。 とりあえず var result = await DialogHost.Show(new MyMessageBox()); で標準のMessageBox.Showライクにダイアログが出せるようになります。 最後に MaterialDesignThemesでのダイアログの出し方はほかにもいくつかありますが、 自分はこの形が一番しっくりきました。 Viewだけでもできるけど、Viewのコード量が増えるのが微妙だったり…。 やりやすい方法はいろいろ探せば出てくると思うので、この記事は参考程度で、自分に合ったやり方を見つけるのがいいと思います! 参考サイト 【Material Design Toolkit】 Dialog
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Beta版のUnity Gaming Servicesを導入して軽く触ってみる

本記事はCraft Egg Advent Calendar 2021の12/4の記事です。 12/3の記事は@Tomy_0331さんの「新卒2年目までに学んだ、コーディングで意識すること」でした。 はじめに 株式会社Craft EggでUnityクライアントエンジニアをしている山元です。 アドベントカレンダーを書くということで何かいいネタがないかと探していたところ、最近 Unity Gaming Services のBeta版がリリースされていたので触ってみた記事になっております。 Unity Gaming Services とは 「バックエンド、マネタイズ、ユーザー獲得、プレイヤーエンゲージメントなど、ゲームクリエイターが必要とするすべての機能を提供する新しいプラットフォーム」のようです。 11/22にBeta版がリリースされた新機能になっておりマルチプレイ機能、Vivoxよる音声通話など様々な機能が入っています。 早速触って試してみる 今回はUnity Gaming Services を使用してプレイヤーが Lobby に参加するところまでを試してみようと思います。 検証環境 Mac Catalina Unity 2021.1.0b12 Unity Authentication 1.0.0-pre.6 Unity Lobby 1.0.0-pre.6 UniTask 2.2.5 Gaming Servicesの準備 PackageManager, Unity Editor, Unity DashBoard にてそれぞれ準備を行います。 PackageManager Unity Editor で Gaming Services の機能を利用するため manifest.json に以下のPackageを追加します。 "dependencies": { "com.unity.services.authentication": "1.0.0-pre.6", "com.unity.services.lobby": "1.0.0-pre.6", } Unity Editor Gaming Services の Unity Project ID を作成します。 手順: ・ UnityEditor で Project Settings > Services を開く ・ Organizations から自分のアカウントを選択する ・ Create Project ID ボタンをクリック ・ 一番下の項目にある13歳以下の子供を対象にしているかの設定で Yes No どちらかを選ぶ ・ 右下にあるSaveボタンを押して完了 Gaming Services を利用するための Unity Editor 側の準備は以上になるので Unity DashBoard 側の準備に移ります。 Unity DashBoard ダッシュボードでは Lobby 内でプレイヤーがマッチングするために必要な機能を有効にする設定を行います。 手順: ・ Unity Dashboard 側で先ほど作成した Unity Project を選択する ・ 今回はプレイヤーのマッチング機能を試すので左タブ下の方にある Multiplayer を選択する ・「Lobby」「Relay」機能それぞれ Get Started から手順通りに進み機能を ON にする これでダッシュボード側の準備も完了しました。 これから実際にコードを書いてロビーを作成、参加していこうと思います。 実装 サインイン Unity Gaming Services は何をするにしても Unity Authentication を使用して認証を通す必要があります。 なのでまずはUnity Authentication で匿名サインイン処理を行っていきましょう。 private async void Start() { // Unity Gaming Services の初期化処理 await UnityServices.InitializeAsync(); Debug.Log(UnityServices.State); await SignInAnonymouslyAsync(); } /// <summary>匿名サインインする処理</summary> private async UniTask SignInAnonymouslyAsync() { try { await AuthenticationService.Instance.SignInAnonymouslyAsync(); Debug.Log("匿名サインイン成功!"); } catch (AuthenticationException ex) { // 認証エラー時の例外 Debug.LogException(ex); } catch (RequestFailedException exception) { // リクエストエラー時の例外 Debug.LogException(exception); } } サインイン成功,サインイン失敗,サインアウト時のコールバックをそれぞれ登録することもできます。 AuthenticationService.Instance.SignedIn AuthenticationService.Instance.SignInFailed AuthenticationService.Instance.SignedOut ロビーの作成 private async void Start() { // Unity Gaming Services の初期化処理 await UnityServices.InitializeAsync(); Debug.Log(UnityServices.State); await SignInAnonymouslyAsync(); var lobby = await CreateLobbyAsync("test lobby", 2); if (lobby == null) return; Debug.Log($"ロビー作成成功! Id:{lobby.Id}, Name:{lobby.Name}"); } /// <summary>ロビーを作成する処理</summary> /// <param name="lobbyName">ロビー名</param> /// <param name="maxPlayers">ロビーの最大プレイヤー数</param> /// <returns>作成したロビー</returns> private async UniTask<Lobby> CreateLobbyAsync(string lobbyName, int maxPlayers) { try { return await Lobbies.Instance.CreateLobbyAsync(lobbyName, maxPlayers); } catch (LobbyServiceException ex) { Debug.LogException(ex); } return null; } 今回はお試しなので指定していませんが Lobbies.Instance.CreateLobbyAsync の第三引数に CreateLobbyOptions という細かいオプションを渡すこともできます。 public class CreateLobbyOptions { // プライベート用のフラグ public bool? IsPrivate { get; set; } // ロビー製作者の情報 public Player Player { get; set; } // ゲーム固有のプロパティも設定できる public Dictionary<string, DataObject> Data { get; set; } } ロビーの入室 Unity Editorだけでは検証することができないのでアプリケーションをビルドしてロビーを作成した状態で検証します。 private async void Start() { // Unity Gaming Services の初期化処理 await UnityServices.InitializeAsync(); Debug.Log(UnityServices.State); await SignInAnonymouslyAsync(); var lobby = await QuickJoinLobbyAsync(); if (lobby != null) { Debug.Log($"ロビー入室成功! Id:{lobby.Id}, Name:{lobby.Name}"); return; } // ロビーに参加できなかったので作成する lobby = await CreateLobbyAsync("test lobby", 2); if (lobby == null) return; Debug.Log($"ロビー作成成功! Id:{lobby.Id}, Name:{lobby.Name}"); } /// <summary>条件に応じた適当な部屋に素早く入室する処理</summary> /// <returns>入室したロビー</returns> private async UniTask<Lobby> QuickJoinLobbyAsync(QuickJoinLobbyOptions options = null) { try { return await Lobbies.Instance.QuickJoinLobbyAsync(options); } catch (LobbyServiceException ex) { Debug.LogException(ex); } return null; } ロビーに入室できた時 ロビーに入室できなかった時 無事ロビーに入室するところまで実装することができました。 まとめ 今回は Unity Gaming Services を使って、ロビー作成、参加というマルチプレイの基本的な部分の実装を行ってみました。 Unity Gaming Services はマネタイズ、プレイヤーエンゲージメントなど広い領域をサポートするようなので今後のアップデートに注目していこうと思います。 今回のLobby実装はUnity公式にサンプルプロジェクトがあるので気になった方は触ってみると良いかもしれません! ※サンプルプロジェクトでは Vivox が使用されているのでダッシュボードにて「Vivox」を ON にする必要があります ここまで読んで頂きありがとうございました! Craft Egg Advent Calendar 2021 明日は@ryousangataの記事になります!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityでセーブ:ScriptableObjectもDictionaryもシリアライズしたい

やりたいこと セーブデータをファイルとして保存したい。 そのために、下記をシリアライズしたい。 Vector3などのUnity組み込みクラス Dictionary ※今回はシリアライズに関してなので、暗号化やファイル保存、逆アセンブル対策などは扱いません こまったこと Unityで利用できるシリアライズ方法は、主に2通り .Netで一般的な方法(例:System.Text.Json等)は、 Vector3やScriptableObjectなど、Unity組み込みクラスに対応しない UnityEngine.JsonUtility Unity組み込みクラスに対応しているが、Dictionaryを保存できない でもUnity組み込みクラスとDictionaryを両立したい! 解決方法 Dictionaryを含んだクラスは別に切り分け、byte[]としてシリアライズする ※JsonとかでもOK ※サンプルではNugetとか不要なBinaryFormatterを使用 サンプルコード using System.Collections.Generic; using System.Runtime.Serialization.Formatters.Binary; using UnityEngine; [Serializable] public class SaveData { public Vector3 position; public byte[] dictionaryClassBinary; } [Serializable] public class DictionarySaveData { // フラグとかを想定 public Dictionary<string, bool> data1; } public class SaveTest : MonoBehaviour { public void Save() { var saveData = new SaveData(); saveData.position = transform.position; var dictionarySaveData = new DictionarySaveData(); // 適当にデータセット dictionarySaveData.data1= new Dictionary<string, bool>(); dictionarySaveData.data1["test"] = true; using (var ms = new MemoryStream()) { var bf = new BinaryFormatter(); bf.Serialize(ms, dictionarySaveData); saveData.dictionaryClassBinary = ms.ToArray(); } string saveDataJson = JsonUtility.ToJson(saveData); byte[] saveDataBinary = Encoding.UTF8.GetBytes(saveDataJson); // saveDataBinaryを暗号化してファイル保存 } public void Load() { string saveDataJson = /* ファイルを開いて復号、jsonを読み取ってくる */; SaveData saveData = JsonUtility.FromJson<SaveData>(saveDataJson); using (var ms = new MemoryStream(saveData.dictionaryClassBinary)) { var bf = new BinaryFormatter(); var dictionarySaveData = (DictionarySaveData)bf.Deserialize(ms); // trueと表示される! Debug.Log(dictionarySaveData.data1["test"]); } // Unity組み込みクラスも読み込めている transform.position = saveData.position; } } まとめ EasySaveみたいな優良有料アセットだと、こういう悩みとは無縁なのかなぁ… でもプログラム系のアセットは自分でなんとかしたい よきゲーム制作を!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む