20211203のC#に関する記事は11件です。

Blazor with Zeu.js

Zeu.jsは様々なデータをビジュアル化し、アニメーションで表示するJavaScriptライブラリです。 今回はBlazor WebAssemblyアプリで、Zeu.jsを動かしてみたいと思います Zeu.js 今回はバリバリのJavaScriptライブラリを使って、Blazorアプリケーションを作っていきたいと思います Demo サンプルコード canvasの初期化 Zeu.jsはcanvasを使ってますので、それぞれのコントロールを初期化する処理をC#側から呼び出します。 Index.razor <div class="col-lg-8"> <div class="row"> <div class="col-lg-8"> <h1>MY COMMAND CENTER</h1> <div><a href="https://github.com/shzlw/zeu" style="font-size: 18px;">by Zeu.js</a></div> <div style="font-size: 22px; font-weight: bold;">US-EAST-01</div> </div> <div class="col-lg-4"> <h2>TIME</h2> <canvas id="digital-clock" width="200" height="50"></canvas> </div> </div> <canvas id="heartbeat" width="730" height="150"></canvas> ?ここに配置 </div> index.html <script> window.Init = (_) => { /* Heartbeat */ heartbeat = new zeu.Heartbeat('heartbeat', { viewWidth: 730, speed: 2, fontColor: COLOR.lightGrey, maxQueueCapacity: 50 }); heartbeat.scaleByHeight(150); // 中略 </script> canvasの初期化は OnAfterRenderの初回レンダリング時に行うようにします。 Index.razor @code{ protected override void OnAfterRender(bool firstRender) { if (firstRender) { JSRuntime.InvokeVoidAsync("Init", new object[] { }); } } } コントロールへ値のセット コントロールの初期化が行えればあとは簡単です。 injectされたJavaScriptの呼び出し機能を使用して、scriptのファンクションにセットしたい値を渡すだけです。 Index.raozr <script> window.valueChangeHertbeat = (p1, p2) => { heartbeat.beat({ color: p1, space: p2 }); } </script> Index.razor @code{ void OnClick() { var r = new Random(); var ret = @"#" + System.Drawing.Color.FromArgb(r.Next(256), r.Next(256), r.Next(256)).Name.Substring(2); // Heartbeat JSRuntime.InvokeVoidAsync("valueChangeHertbeat", new object[] {ret,r.Next(0,10) }); } } JavaScriptライブラリを使う BlazorはJavaScriptを使わずにC#だけでWebアプリケーションをコーディングできるのが大きな特徴でありますが、逆にJavaScriptとの親和性の高さも大きな特徴です。 UIに関してのユーザービリティやビジュアルなギミックは、純粋にBlazorだけで表現するのはまだまだ難しいですね いつか、すべてをC#でコーディングできる日がやってくるのでしょうか
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#でカノジョをつくる(3日目)

お疲れ様です。たなしょです。 今日は配列などのデータ構造や名前空間などの構造化について学びました。 この辺からC#特有の文法が出てきて面白かったです。 配列 今まで学んだ言語と同じなのでここも記載は割愛します。 配列→多次元配列→ジャグ配列の流れはどの入門書でも定番の流れですね。 クラス、メンバー、enum ここもJavaと同じなので記載は割愛します。 マイクロソフトが作ったJavaと言われているだけあってこの辺の記載はJavaと似ていました。 後一点気になったのが、 enum 年号 { 明治, 大正, 昭和, 平成, 令和 } C#は日本語を正常に読み解てくれるんですね。 2バイト文字だと文字化けが怖いのでNENGOにして中の値もmeijiに変更したほうがいいと思うんですけど…。 タプル 分解と組み合わせるとスワップが簡単にできるのは確かに便利だなと思いました。 static void Main() { int a = 10, b = 20; (a, b) = (b, a); Console.WriteLine($"{a} {b}"); } それ以外は目新しい文法は特になかったです。 if, switch, goto どの言語でも共通している部分が多いので記載は割愛します。 goto文は極力使ってはいけませんよーというのはどの言語でも常套句です。(プロジェクトのコーディング規約でも「gotoは使用禁止」と記載があるんですよね。) 僕がgoto文を使ったのはC言語でOSかコンパイラを作った時ぐらいですかね。 while, do~while, for, foreach どの言語でも共通している部分が多いので記載は割愛します。 foreach文をまともに使えるようになったのはPHPを書き始めた時です。for文でしこしこ配列の値を取得していたらレビューで全て指摘対象になり、泣く泣く修正しました。いい思い出です。 可変長引数(パラメーター配列) これはすごく便利だなと思いました。 普通なら一旦配列に値を代入しないといけないです。 static void Main() { int a = 1, b = 2, c = 3, d = 4, e = 5; int[] tmp = new int[] { a, b, c, d, e }; int max = Max(tmp); } static int Max(int[] a) { int max = a[0]; for(int i=1; i<a.Length; i++) { if (max < a[i]) max = a[i]; } return max; } 可変長引数(パラメーター配列)を使うと配列を宣言しなくて済むので、可読性が上がりそうです。 static void Main() { int a = 1, b = 2, c = 3, d = 4, e = 5; int max = Max(a, b, c, d, e); Console.WriteLine($"{max}"); } static int Max(params int[] a) { int max = a[0]; for (int i = 1; i<a.Length; i++) { if(max < a[i]) max = a[i]; } return max; } 名前付き引数 本来関数の引数を変更するときは、直前で宣言した変数に値を代入します。 int i = 0; // 返り値で4が欲しい i = 2; int sq = Square(i); static int Square(int a) { return a * a; } 名前付き引数を使えば関数の直前で代入しなくても問題ありません。 i: 2のように使います。 ```c# int i = 0; // 返り値で4が欲しい int sq = Square(i: 2); static int Square(int a) { return a * a; } 例外処理 こんな風にcatch文を記載できるんですね。Javaでもできるんですかね? catch (Exception e) when (e is FormatException || e is DivideByZeroException) 上記のように書くとFormatExceptionとDivideByZeroExceptionときにcatch文の処理に入ります。 それ以外は特に目新しい項目がないので記載は割愛します。 久しぶりにtry-catch-finally文を書きました。PHPだとDB処理の部分でしかtry文を書かないんですよね。 名前空間 C#でよくみるnamespaceについてです。 本の最初の方に「ディレクトリのようなもの」と書かれていましたがまさにその通りでした。 namespaceを定義することでこの中にあるclassなどは定義したnamespaceに属します。 エイリアスを付けられるみたいなのですが、いまいち内容がわかりません。 定数 ここも既知などで記載は割愛します。 最近Go言語でCLIツールをよく書くのですが、そこでもがっつり定数を使ってます。大事ですね。 ソースコードが見やすくなります。 最後に 色々とソースコードを写経したことで簡単CLIツールなら作れるぐらいにはコードが書けるようになりました。 条件分岐なども一通り学んだのでこれでAtcoderにも参加できますね! 明日も引き続きよろしくお願いします。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#を使用してExcelで組み合わせグラフを作成する方法

C#を使用してExcelで組み合わせグラフを作成する方法 背景 組み合わせグラフとは、同じグラフに2つ以上のスタイルが含まれているグラフのことです。このようなグラフは、データをより包括的に分析するのに役立ちます。一般的な組み合わせグラフは、縦棒グラフと折れ線グラフの混合グラフです。この記事では、C#と無料バージョンのSpire.XLSを使用してこの種のグラフを作成する方法を紹介します。 Excelドキュメントデータのスクリーンショット: 次のコードを使用する前に、Spire.Xls.dllファイルを参照し、次の名前空間を追加する必要があります: using Spire.Xls; using Spire.Xls.Charts; 詳細な手順: 手順1:ワークブックのインスタンスを作成し、Excelドキュメントを読み込みます。 Workbook workbook = new Workbook(); workbook.LoadFromFile("data.xlsx"); 手順2:最初のワークシートを取得します。 Worksheet sheet = workbook.Worksheets[0]; 手順3:ワークシートにグラフを追加します。 Chart chart = sheet.Charts.Add(); chart.DataRange = sheet.Range["A1:D5"]; chart.SeriesDataFromRange = false; 手順4:グラフの位置を設定します。 chart.LeftColumn = 6; chart.TopRow = 1; chart.RightColumn = 12; chart.BottomRow = 13; 手順5:グラフのデータ系列にさまざまなグラフタイプを設定します。 //チャートのシリーズ1とシリーズ2のチャートタイプを縦棒グラフに設定する var cs1 = (ChartSerie)chart.Series[0]; cs1.SerieType = ExcelChartType.ColumnClustered; var cs2 = (ChartSerie)chart.Series[1]; cs2.SerieType = ExcelChartType.ColumnClustered; //チャートのシリーズ3のチャートタイプを折れ線グラフに設定する var cs3 = (ChartSerie)chart.Series[2]; cs3.SerieType = ExcelChartType.LineMarkers; //チャートに2次軸を追加する chart.SecondaryCategoryAxis.IsMaxCross = true; cs3.UsePrimaryAxis = false; 手順6:ファイルを保存します。 //ファイルを保存する workbook.SaveToFile("result.xlsx", ExcelVersion.Version2010); System.Diagnostics.Process.Start("result.xlsx"); 効果は以下のようになります: コード一覧: using Spire.Xls; using Spire.Xls.Charts; namespace CreateCombinationExcel { class Program { static void Main(string[] args) { Workbook workbook = new Workbook(); //Excelファイルをロードする workbook.LoadFromFile("data.xlsx"); //最初のワークブックを取得する Worksheet sheet = workbook.Worksheets[0]; //ワークシートにグラフを追加する Chart chart = sheet.Charts.Add(); chart.DataRange = sheet.Range["A1:D5"]; chart.SeriesDataFromRange = false; //チャートの位置を設定する chart.LeftColumn = 6; chart.TopRow = 1; chart.RightColumn = 12; chart.BottomRow = 13; //チャートのシリーズ1とシリーズ2のチャートタイプを縦棒グラフに設定する var cs1 = (ChartSerie)chart.Series[0]; cs1.SerieType = ExcelChartType.ColumnClustered; var cs2 = (ChartSerie)chart.Series[1]; cs2.SerieType = ExcelChartType.ColumnClustered; //チャートのシリーズ3のチャートタイプを折れ線グラフに設定する var cs3 = (ChartSerie)chart.Series[2]; cs3.SerieType = ExcelChartType.LineMarkers; //チャートに2次軸を追加する chart.SecondaryCategoryAxis.IsMaxCross = true; cs3.UsePrimaryAxis = false; //ファイルを保存する workbook.SaveToFile("result.xlsx", ExcelVersion.Version2010); System.Diagnostics.Process.Start("result.xlsx"); } } } こうやって組み合わせグラフは作成完了です、最後まで読んでいただきありがとうございます。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

3項演算子

メモです if(i < 1){ /*true時の内容*/ }else{ /*false時の内容*/ } が i < 1 ? /*true時の内容*/ : /*false時の内容*/; と書ける i < 1 ? /*true時の内容*/ : /*false時の内容*/; 条件式 if else ;
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

新卒2年目までに学んだ、コーディングで意識すること

本記事はCraft Egg Advent Calendar 2021の12/3の記事です。 12/2の記事は@ishiguro_takuyaさんの[Unity] UniversalRenderPipelineについて調べた覚え書き でした。 はじめに 株式会社Craft EggでUnityクライアントエンジニアをしている豊田です。 今回は私が新卒入社してから現在の2年目までに、プルリクエストでチームからもらったコメントや技術書をなどの学びを元に、コーディングで気をつけていることをまとめてみました。 before/afterのコードの例を出すことで、新卒エンジニアが遭遇しやすいコーディングアンチパターンとその改善例の参考になれば幸いです。 コーディング規約やしきたりはチームによって異なりはしますが、できるだけ一般的な内容を取り上げました。 ※本記事で取り上げるコードはサンプルです。重要でない部分の命名は「hoge」としたり、簡略化しています。 表面上の改善 リーダブルコードには、第1部に「表面上の改善」が紹介されています。 命名規則やコードの体裁など、第三者(未来の自分自身も)がコードを理解するために最初に意識できる観点だと思います。 適切な変数名にする 意味が分かる命名にする boolは「is~」「should~」「can~」「exists~」「has~」など コールバックは「onCloseDialog」など「on~」 略しすぎない。一般的な略称ならOK(× s_pos → ◯ startPosや、startPosition) 英文法に誤りが無いか気をつける(× onClosedDialog → ◯ onCloseDialog) など 適切な関数名にする 基本的に動詞始まりにする 初期化は「Initialize」「Setup」 生成は「Create~」 登録は「Register~」 変換は「ConvertHogeToFoo」 「Add◯◯」なら「Remove◯◯」というように、名前と処理を対にして理解しやすくする 「~IfNeed」「Check~」のような命名の関数を避ける(処理を分けられないか検討する) 英文法に誤りが無いか気をつける など 一次変数でも分かりやすい命名にする 一次変数などスコープが小さくてもできるだけ意味の分かる命名にします。 before UserData u = GetUserData(userId); after UserData userData = GetUserData(userId); ※ラムダ式の中でも同様 before filterUsers = userList .Where(x => x.Id % 2 == 0) .ToList(); after filterUsers = userList .Where(user => user.Id % 2 == 0) .ToList(); ネストを浅くする 例外は先に早期returnすることで思考をクリアにできます。 before private void Hoge() { if (hoge != null) { Foo(); if(foo != null) { // メインの処理が続く } } } after private void Hoge() { if (hoge == null) { return; } Foo(); if (foo == null) { return; } // メインの処理が続く } なお、早期returnは関数の出口が複数でき、利用側の期待する処理が行われず罠にハマることがあるため、何でも早期returnするのではなく、処理を分離できないか検討するなどしてケースバイケースで用いるのが良いと思います。 三項演算子を使う before if (IsHoge()) { foo = bar; } else { foo = null; } after foo = IsHoge() ? bar : null; foreachをLINQに置き換える before private List<ItemData> CreateItemDataList(List<uint> itemIds) { List<ItemData> itemDataList = new List<ItemData>(); foreach (uint id in itemIds) { itemDataList.Add(new ItemData(id)); } return itemDataList; } after private List<ItemData> CreateItemDataList(List<uint> itemIds) { return itemIds .Select(id => new ItemData(id)) .ToList(); } 配列やListに対してforeachで処理する場合の多くはLINQで書けます。 文字列補間を使う C#の6.0以上で使える文字列補間は、書きやすく見やすいです。 https://docs.microsoft.com/ja-jp/dotnet/csharp/language-reference/tokens/interpolated before Debug.LogError("userId : " + userId + ", userName : " + userName + "のユーザでエラー"); after Debug.LogError($"userId : {userId}, userName : {userName}のユーザでエラー"); コメントアウト 自明なコメントは書かない Initialize(); // 初期化 と書かれていてもコメントの有無で情報量が変わりません(むしろノイズになりうる)。 できるだけコードで説明することを心がけます。 処理の内容ではなく理由を書く 特殊な実装をしていたら、なぜそうしているかのコメントがあると、読んだ人をびっくりさせません。 なお、低レベルのコードを要約したり、処理のまとまりを説明する場合など、ケースによって処理の内容は書くことはあると思います。 問題のあるコードにコメントを書く こちらも読んだ人をびっくりさせることを防ぎます。 やむを得ず綺麗じゃない実装をしているのなら、しれっとコードに紛れ込ませるのではなくTODOやHACKなどを添えて問題点を説明します。 安全性、Nullチェック Get関数は失敗を考慮する before UserData userData = GetUserData(userId); userData.HogeMethod(); この例だと、userDataの取得に失敗した時にNullエラーになりかねないので、Nullチェックを挟みます。 after UserData userData = GetUserData(userId); if (userData == null) { Debug.LogError( "UserData is null. userId : " + userId ); // 場合に合わせて例外処理を行う return; } userData.HogeMethod(); Null演算子を使って簡潔にする before if (userData != null && userData.Card != null) { // userData.Cardにアクセスする処理 } after if (userData?.Card != null) { // userData.Cardにアクセスする処理 } ※ リストのNull、空チェックも同様に簡潔にできます before if (userList != null && userList.Count != 0) { // userListにアクセスする処理 } after if ((userList?.Count ?? 0 ) != 0) { // userListにアクセスする処理 } なお、MonoBehaviourなどUnityEngine.Objectを継承したものに対してのnullチェックは以下のような問題もあるので、Null演算子は使わずに== nullを使います。 エラー通知は必要な時にはっきり行う ログは、エラーが起きた時にエラー出力をさせるなど、必要な時に行います。 正常挙動である時はログは出力させません。 なお、追いにくい低レイヤの処理には正常ログを入れる場合もあるかと思います(問題が起きた時に追いやすくする)。 制御の改善 処理の対象を絞ってLoop処理を回す before foreach (var hoge in hogeList) { if(!hoge.IsFoo) { continue; } hoge.HogeMethod(); } 先にIsFooで絞ってからforを回したほうが、どういうデータを処理したいループなのかが分かりやすいです。 after foreach (var hoge in hogeList.Where(hoge => hoge.IsFoo)) { hoge.HogeMethod(); } 状態を保持する変数はenumで定義する フラグを乱立すると状態が分かり辛く、ソースを読み解くのが大変になります。 before private bool isProcessing; private bool isInterrupted; private bool isInitialized; after private enum StateType { None, Processing, Interrupted, Initialized } private StateType currentState; 設計の改善 モジュールを「純粋」にして、モジュール間を「疎遠」にすることを意識します。 凝集度と結合度の話ですが、以下の要素は基本的に凝集度を高く結合度を低くするための実践例だと思います。 長すぎるメソッド、クラスを書かない 一つの関数が長くなっているのであれば、まず分離できないかを検討します。 before public void Setup() { // UI、コンポーネントの初期化が数行に渡って書かれる ... // ロードの準備が数行に渡って書かれる ... // ロード処理が数行に渡って書かれる ... } after public void Setup() { SetupComponents(); PrepareLoad(); Load(); } 必要ないものはpublicにしない クラスが、利用側の知る必要のない内部の詳細部分を隠蔽すれば、やりとりがシンプルになりコード全体の複雑性を下げることができます。利用側からみても、使い方がシンプルになり使い勝手が良くなります。 まずprivateで書いてみて、公開する必要があればpublicにする、がいいかもしれません。 ロジックとデータは近くに置く before(データ) public class PurchaseData { // 単価 public uint Price { get; } // 個数 public uint Count { get; } // コンストラクタは省略 } before(利用側) // 単価100円、5つのデータとする PurchaseData purchaseData = new PurchaseData(100, 5); Debug.Log($"合計金額は{purchaseData.Price * purchaseData.Count}円です") このbeforeの例では、データとなるPurchaseDataクラスと、利用側の2つのコードがあります。 データのクラスは、DB由来のモデルクラスと捉えてもいいです。 改善したいのは、Debug.Log内の金額の計算を利用側で行っている点で、ロジックがデータの外側に実装されてしまっています。 after(データ) public class PurchaseData { // 単価 public uint Price { get; } // 個数 public uint Count { get; } public uint GetAmount() { return Price * Count; } // コンストラクタは省略 } after(利用側) // 単価100円、5つのデータとする PurchaseData purchaseData = new PurchaseData(100, 5); Debug.Log($"合計金額は{purchaseData.GetAmount()}円です") afterでは、合計金額を計算するロジックをデータのクラス側に移動しました。 例えばこれに、「軽減税率を適用するか」という「データ」を増やし、消費税計算の「ロジック」の実装が必要になった時に、改修の対象はPurchaseData内のみですので、利用側はロジックの変更を知らなくて済みます。 引数で処理の内容が変わる関数を避ける 以下のように引数を元に処理が変わる関数だと、利用側が関数のロジックを把握する必要があり、ブラックボックス化できません(制御結合になっている)。 制御結合になっている例 public void hoge(bool flag) { if(flag) { // 処理Aが続く } else { // 処理Bが続く } } ただし、システムによっては、制御結合を避けられない場合もあるので、集まっている機能を精査しながらより結合度を下げられないか検討します。 無駄な引数を送らない before例ではスタンプ結合になっています。関数にはできるだけ使うものだけ送るようにします。 before public void SetUserNameLabel(UserData userData) { userNameLabel.text = userData.Name; } after public void SetUserNameLabel(string userName) { userNameLabel.text = userName; } Unity関連 GetComponent、Find系の関数を使わない パフォーマンスに悪いので、事前にSerializeFieldで参照を持たせます。 Update()は使わない コールバックやコルーチンで実現できないか検討をします Animatiorの引数はhash値を使う Tips的な項目ですが、animatorの引数はhash値を使った方がパフォーマンス的に良いです。 Animator.StringToHash で取得した値を保持しておくのがミソ(先輩の言葉を引用) before private void PlayRunAnimation() { animator.Play("run"); } after private readonly int AnimationHashRun = Animator.StringToHash("run"); private void PlayRunAnimation() { animator.Play(AnimationHashRun); } さいごに コードの可読性、保守性、安全性や柔軟性を上げるための手法、設計の原則は今回記事に取り上げたことの他にも様々あると思います。 業務でのプルリクエストでのコメントで得た学びの他、「リーダブルコード」と「プリンシプルオブプログラミング」からも執筆にあたって参考にしました。これらの書籍は1年目で読んだのですが、とても勉強になりました。 今回は局所的なコードの改善事例を取り上げましたが、今後インタフェースを使った実装の分離や、デザインパターンを用いた実践なども体系的にまとめてアウトプットしてみたいです。 アドベントカレンダーの明日の記事は @kai_yamamoto です。お楽しみに! 参考文献
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【トラブル回避】端末依存のC#処理に注意!

本記事は、サムザップ Advent Calendar 2021の12/7の記事です。 はじめに C#の一部APIには、端末の設定によって結果が変わる挙動をするものがあります。 今回はそんな危うい挙動をする処理と、それを端末の設定に依存させないで行う方法を紹介したいと思います。 端末の言語設定に依存した処理 float n = float.Parse("1.5"); これは1.5という文字列をパースしてfloatへ変換する何の変哲もないコードですが、 端末が特定の言語に設定されている場合、以下の例外が投げられて処理は失敗してしまいます。 FormatException: Input string was not in a correct format. これは小数点の表現がカルチャによって異なることと、デフォルトではカルチャは端末の言語設定を参照していることに依るものです。 [ws][sign] [integral-digits[,]]integral-digits[.[fractional-digits]][e[sign]exponential-digits][ws] . A culture-specific decimal point symbol. Single.Parse Method (System) | Microsoft Docs 例えば端末の言語がフランスに設定されている場合、小数点はカンマ(,)で表現されるため、 1.5を浮動小数点数として正しくパースすることが出来ません。 この言語設定への依存は、明示的にカルチャを指定することで解消出来ます。 以下のコードでは、float.Parseメソッドの第2引数へ特定の国/地域に依存しないカルチャ1を指定しています。 float n = float.Parse("1.5", CultureInfo.InvariantCulture); なお今回はfloatを例に挙げましたが、同じ浮動小数点数のdoubleはもちろん、 DateTime, DateTimeOffsetなどのパースも端末の言語設定の影響を受けます。 Unity特有の事情 ちなみにUnityではAndroid向けにビルドしたアプリで、デフォルトのカルチャがInvariantCultureとなるバグ2がありました。 このバグは修正されており、Unity 2020.1.5f1以降ではちゃんと端末に設定された言語のCultureInfoオブジェクトが返されます。 従って同じAndroid端末・同じ言語設定(例では日本語)で以下のコードを実行した場合、 Debug.Log(CultureInfo.CurrentCulture.DisplayName); Unity 2019.4.15f1では、 Invariant Language (Invariant Country) Unity 2020.3.14f1では、 Japanese (Japan) と異なる結果が出力されます。 もしパース処理などでデフォルトのカルチャがInvariantCultureであることに依存していたコードがある場合、 古いUnityからバージョンアップをする際には注意が必要でしょう。 端末のタイムゾーンに依存した処理 少々わざとらしいコードですが、以下の処理も端末のタイムゾーンによって結果が異なります。 // toの指す日付とfromの差を出力する var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var delta = to.Date - from; // to.Dateでtoの日付部分を取得する Debug.Log(delta.ToString()); 例えば端末のタイムゾーンが日本標準時(+09:00)の場合は、想定した通りに2日と出力されますが、 2.00:00:00 中央ヨーロッパ時間(+01:00)に設定していた場合は、2日と8時間と出力されます。 2.08:00:00 何故この差異が生じるかというと、 DateTimeOffsetのDateプロパティは元々のタイムゾーンが反映されないのと、 The value of the DateTime.Kind property of the returned DateTime object is always DateTimeKind.Unspecified. It is not affected by the value of the Offset property. DateTimeOffset.Date Property (System) | Microsoft Docs DateTimeとDateTimeOffsetの減算は、DateTimeがDateTimeOffsetに変換されてから行われますが、 その変換の際に端末に設定されたタイムゾーンが採用されてしまうことによります。 If the value of the DateTime.Kind property is DateTimeKind.Local or DateTimeKind.Unspecified, the date and time of the DateTimeOffset object is set equal to dateTime, and its Offset property is set equal to the offset of the local system's current time zone. DateTimeOffset.Implicit(DateTime to DateTimeOffset) Operator (System) | Microsoft Docs 端末のタイムゾーンが中央ヨーロッパ時間(+01:00)のときの処理を一つ一つ追っていくと、 var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); DateTime a = to.Date; // 1. 2021-12-03 00:00:00, Kind=Unspecified DateTimeOffset b = a; // 2. 2021-12-03 00:00:00+01:00 TimeSpan delta = b - from; // 3. 2021-12-03 00:00:00+01:00 - 2021-12-01 00:00:00+09:00 Dateプロパティで日付部分を表すDateTimeインスタンスを取得(DateTimeKindはUnspecified) DateTimeインスタンスのDateTimeOffsetへの暗黙の変換、DateTimeKindはUnspecifiedなので、システムのタイムゾーン(+01:00)のオフセットが設定される 差の計算の日時をUTCに換算すると、2021-12-02 23:00:00 - 2021-11-30 15:00:00 となる 結果、差が2日と8時間になってしまっていることが分かります。 端末のタイムゾーン設定に依らず正しい結果を得たい場合は、 日付を取り出す際に元々のオフセットを指定して、明示的にDateTimeOffsetをインスタンス化する必要があります。 var to = DateTimeOffset.ParseExact("2021-12-03T12:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var from = DateTimeOffset.ParseExact("2021-12-01T00:00:00.0000000+09:00", "O", CultureInfo.InvariantCulture); var delta = new DateTimeOffset(to.Date, to.Offset) - from; Debug.Log(delta.ToString()); これで意図した通りに2日と出力されます。 2.00:00:00 最後に 同じ環境で開発と動作確認を行っている場合、見逃されがちな端末の設定に依存した処理を紹介いたしました。 この記事が堅牢なシステムを構築するための助けになれば幸いです。 参考 .NET のカルチャー依存 API 問題 | ++C++; // 未確認飛行 C ブログ .NET での数値文字列の解析 | Microsoft Docs DateTime と DateTimeOffset 間の変換 | Microsoft Docs CultureInfo.CurrentCulture always returning Invariant Language (Invariant Country) - Unity Forum CultureInfo.InvariantCulture プロパティ (System.Globalization) | Microsoft Docs ↩ Unity Issue Tracker - [Android] System.Globalization.CultureInfo.CurrentCulture returns Culture Invariant when is being built on Android device ↩
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity】後回しにすると大変なことになった事例集

はじめに 本記事は、サムザップ Advent Calendar 2021 の12/8の記事です。 経験上 私は、Unityを使ったゲーム開発をしております。 ゲーム開発では、モック開発中にゲームをプレーできるように優先して開発してしまいますが 開発しているときから常に意識してほしいことを説明したいと思います 後回しにすると大変になった事例をいくつか紹介します 意識してほしいこと 画面遷移 モック中に複数の画面の機能を量産して動く形に仕上げ、 デザインも適応させ見た目の確認をすることが多いと思います。 量産ばかりしていていざ本開発にむけて仕上げる状態になったときに やりたい挙動ができなくなることがしばし見受けられます。 画面遷移中にインタラクションを入れたいけど入れにくい ダイアログを出す仕組みが各々違うからインタラクションが入れにくい Androidのバックキーを入れたいけど戻るボタンの処理が各々違うからいれにくいなどなど 上記のようなことをおろそかにすると、本開発中にチームメンバーが増えて さらにページ数が増えてくると修正が困難になりがちです。 最初にページの遷移やダイアログの遷移のベースを先に作ってしまうことをおすすめします 設計のポイント ページの遷移やダイアログを(Prefab単位で実装するか複数のシーンを使って実装するか) Unityだとどちらかの方法を使って実装することが多く見受けられます。 私の経験上だと、Prefabを使って構成することが多い印象ですが どちらの方式でつくるか決めます 次に挙動を決める。 設計によって、細かく挙動がかわると思いますが。ここでは例として列挙します ページに移動する 1. 初期化する 2. アニメーションを入れる 3. 画面を表示する ページから離れる 1. アニメーションを入れる 2. 画面から離れる 3. 後始末(メモリー等やアセットの解放など) 上記のような処理で設計した場合にそれぞれをベースコードとして実装しておくことで すべてのページが同じ挙動になります。 アセット管理 モック開発中に乱雑に配置してしまったアセットですが、 これらは大体が使われない素材になりがちです。 ここも適当に配置して放置しまっていると本開発するというフェーズの時に アプリサイズが肥大化してしまうという問題をよく拝見します。 すべての素材をResources上に配置してしまっている Atlasデータと素材がResources上に一緒に置かれてしまっている 圧縮形式をきめていない モバイル以外のプラットフォームにも考慮しておく(Windows)など ポイント 素材類は、Resources上に配置せずに別フォルダーに入れておく。 Atlasデータは、Resources上に配置しておく。 Assets/  ├ Sandbox/  │ └ Mock/  │   └ UI/ << 開発中はこちらに配置  │ └ Release/  │   └ UI/ << 本開発になったらこちらに配置  ├ Resources/  │ └ Atlas/ << こちらにAtlasデータを配置して、AtlasのPackingフォルダーを適宜変更 今回の例は、Atlas例にしていますが、他の素材も同様にわけておくことをおすすめします 圧縮形式について 圧縮形式をきめておかないと、見た目の印象から変わってしまいますし アプリサイズにも影響してしまうのでここも抑えておきます 圧縮形式(モバイル) 用途 注意点 ASTC 4x4 UIパーツ(Atlas) 2048 x 2048に収める ASTC 4x4 他のテクスチャ PCでも考慮してDXT5にする予定なので4の倍数にする 上記は一例ですが、あらゆる素材で構成されますので チームで相談しつつ適切に設定することをおすすめします アセットバンドル管理 ゲーム開発において、アセットバンドルにして外部で管理することが多くなると思います。 乱雑に配置してしまっていると肥大化してしまう傾向になりがちです。 肥大化するとダウンロード時間の配慮も必要になってきてしまいます ポイント アセットは、フォルダパック管理で管理する アセットサイズが大きい素材は、ファイルパックにして分割しておく 並列ダウンロードの実装もしておく この3点を抑えておくだけ、効果が違います。 Assets/  ├ AssetBundles/  │ └ character/  │   └ 1/  │    │ head  │    │ body  │    │ hair  │    │ background  │    │ icon  │ └ movie/  │   └ 1/  │    │ sample1  │    │ sample2  │    │ sample3  │    │ sample4  │    │ sample5 上記のような構成でアセットバンドルに管理されているという例で説明します キャラクターの素材を下記のように管理した場合 パック形式 AssetBundle Label 補足 フォルダパック character/1 一つで5つの素材を管理(一つの素材が数K Byte) ファイルパック character/1/headcharacter/1/bodycharacter/1/haircharacter/1/backgroundcharacter/1/icon 5つをそれぞれ管理 ムービーの素材を下記のように管理した場合 パック形式 AssetBundle Label 補足 フォルダパック movie/1 一つで5つの素材を管理(一つの素材が数M Byte) ファイルパック movie/1/sample1movie/1/sample2movie/1/sample3movie/1/sample4movie/1/sample5 5つをそれぞれ管理 例として、それぞれを10万台の端末が一斉にダウンロードを開始した場合 パック形式 素材 補足 フォルダパック キャラクター 10万台 × 1コネクション ファイルパック キャラクター 10万台 × 5コネクション フォルダパック ムービー 10万台 × 1コネクション ファイルパック ムービー 10万台 × 5コネクション フォルダパックのほうがコネクション数が少なく済むので ダウンロードが高速になりそうですが ムービーは数M Byte × 5のファイルサイズになるので ファイルサイズが数M Byte以上あると一つのダウンロード時間が 遅くなる傾向が経験上体験しましたので 並列ダウンロードできる仕組みを用意しておくをおすすめします ポイントまとめ アセット管理は小さいサイズでまとめて管理できるならフォルダパック 大きいサイズになるなら、ファイルパックして分割ダウンロード アセットバンドルでは一つの素材でもフォルダパックで作成しておき、 大きいサイズになるものはファイルパックで管理することをおすすめします フォルダパックにしておくことで、後で追加しやすくなります。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET Web API2 Owin ・ ASP.NET Core でミドルウェアを作成する

概要 2021年11月 Visual Studio 2022 のリリースにより、.NET Core は .NET6 (LTS) になりました。 C# の Webアプリケーション開発は、ASP.NET Core で開発するケースが増えてきたと思います。 ASP.NET Core は、ASP.NET MVC / ASP.NET Web API2 と比較して様々な点が改良されているかと思いますが、 本件では、その中でも ミドルウェア を題材に挙げたいと思います。 MS公式ドキュメントには、Owinミドルウェア や ASP.NET Coreミドルウェア に関する記事が多く存在しています。 本記事では、ドキュメントを見ながら、実際にコードを書きつつ、ミドルウェアの挙動を見ていきたいと思います! 入門的な内容となっており、既に業務で バリバリ とミドルウェアを利用している方には、 やや物足りない可能性がありますが、ご容赦ください お品書き 1. ミドルウェアについておさらい 2. ASP.NET Web API2 Owin のミドルウェア 3. ASP.NET Core のミドルウェア 環境 Windows10 Visual Studio 2022 ASP.NET Core ASP.NET Web API 2 Owin サンプルコード サンプルコードを Github にアップしています。 ご興味のある方は、是非手元で動かしながら見ていただけると理解が深まると思います https://github.com/tYoshiyuki/dotnet-middleware-sample ミドルウェアについておさらい ミドルウェアに関しては、MS公式ドキュメントの図が参考になるかと思います。 https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/middleware/?view=aspnetcore-6.0#create-a-middleware-pipeline-with-webapplication アプリケーションに到達した HTTPリクエスト に対して、各ミドルウェアが順次、処理を行う形になります。 ドキュメントの図の場合、Middleware1 > Middleware2 > Middleware3 の順序でリクエスト情報が到達します。 そして、Middleware3 > Middleware2 > Middleware1 の順番で各ミドルウェアの処理が実施、解決されます。 (※) 実際は、Middleware3 の後続で、MVCコントローラのような終端処理が存在しています。 ミドルウェアの処理ロジック (ドキュメントでは デリゲート と記載されています) は、 HttpContext を共有する形で、各処理を実施していきます。 ベルトコンベアの流れ作業 のようなイメージを持っていただくと、解りやすいかと思います。 同様にMS公式ドキュメントからの引用になりますが、 ASP.NET Core MVC のミドルウェア パイプライン に関するイメージ図があります。 例外処理を行うミドルウェア ExceptionHandler や 静的ファイルをサポートするミドルウェア Static Files、認証を行うミドルウェア Authentication 等、 Webアプリケーションを利用するにあたり、必要となる基本機能はミドルウェアで実装されていることが判るかと思います。 独自のミドルウェアは、Custom middlewares で導入し、最終的には Endpoint が呼び出される・・・という感じです。 普段、何となく利用している ASP.NET Core の機能 と ミドルウェアが、密接に関わっていることが何となくイメージ出来るかなと思います! 尚、ミドルウェアという概念は、ASP.NET MVC (.NET Framework) 時代からも存在しており、 その際は、Owin (Open Web Interface for .NET) と呼ばれる類似の仕組みで、ミドルウェアを利用することが出来ました。 (※) 細かい話になりますが、Owin は、インターフェースの仕様を定義したものであり、Microsoft.Owin (Katana) がその実装になります。 Owinのミドルウェアについては、neueccさん の記事が非常に参考になります。 https://neue.cc/2014/01/06_442.html なんと 2014年 の記事です!! 歴史を感じます 円形の部分がアプリケーションを表現しており、 外側のミドルウェアの層 (複数) と 内側のコア機能で構成されているイメージです。 リクエストがアプリケーションに到達すると、外側のミドルウェアからリクエストが伝達されていきます。 その後、内側のコア機能から順次処理が解決されていき、最終的にレスポンスが返却される という感じです。 ASP.NET Web API2 Owin のミドルウェア まずは、ASP.NET Web API 2 Owin の ミドルウェア サンプルコードを見ていきます。 StopwatchMiddleware.cs namespace DotNetMiddlewareSample.WebApi.Middleware { /// <summary> /// 実行時間を記録するミドルウェアです。 /// </summary> public class StopwatchMiddleware : OwinMiddleware { public StopwatchMiddleware(OwinMiddleware next) : base(next) { } public override async Task Invoke(IOwinContext context) { var sw = new Stopwatch(); sw.Start(); context.Response.OnSendingHeaders(x => { // リクエスト実行時間をレスポンスヘッダーに記載します。 context.Response.Headers.Add("X-Execution-Time", new[] { sw.ElapsedMilliseconds.ToString() }); }, context); await Next.Invoke(context); sw.Stop(); } } /// <summary> /// <see cref="StopwatchMiddleware"/> 用の拡張メソッドです。 /// </summary> public static class StopwatchMiddlewareExtensions { /// <summary> /// <see cref="StopwatchMiddleware"/> をパイプラインに登録します。 /// </summary> /// <param name="app"><see cref="IAppBuilder"/>></param> /// <returns><see cref="IAppBuilder"/></returns> public static IAppBuilder UseStopwatch(this IAppBuilder app) { return app.Use<StopwatchMiddleware>(); } } } 非常に単純なサンプルになりますが、リクエスト処理時間を 独自のレスポンスヘッダ に出力するというものです。 以下、Swagger UI で実行した結果になります。x-execution-time というレスポンスヘッダに処理時間 (ミリ秒) が設定されていることが判るかと思います。 (※) サンプルコードには、NSwag を導入していますので、デバッグ実行で確認出来ます。 StopwatchMiddleware クラス は OwinMiddleware クラス を継承します。 Owin の場合、IOwinContext に HTTP処理 に関する要素が集約されており、 リクエスト や レスポンス に関する情報は、IOwinContext を参照し、そこから取得・更新を行うことが出来ます。 await Next.Invoke(context); の部分は、後続のミドルウェアを呼び出している部分になります。 StopwatchMiddlewareExtensions は、StopwatchMiddleware をパイプラインに登録するためのヘルパークラスになります。 ミドルウェアは、このようなヘルパークラスを作成し、UseStopwatch という形でパイプラインに登録するのが、通例になります。 ミドルウェアの登録は、Startup クラスで実施します。 以下、コードサンプルを引用します。 Startup.cs [assembly: OwinStartup(typeof(Startup))] namespace DotNetMiddlewareSample.WebApi { public class Startup { public void Configuration(IAppBuilder app) { var configuration = new HttpConfiguration(); configuration.MapHttpAttributeRoutes(); app.UseSwaggerUi3(typeof(Startup).Assembly, settings => { }); app.UseStopwatch(); app.UseWebApi(configuration); } } } [assembly: OwinStartup(typeof(Startup))] のアノテーションにより、 当該クラスが Owin のスタートアップクラス として認識されています。 app.UseStopwatch(); の部分が、ミドルウェアを登録している部分になります。 ASP.NET Core のミドルウェア いよいよ真打ち、ASP.NET Core のミドルウェアを見ていきましょう。 折角なので、.NET6・Visual Studio 2022 で作成してみました StopwatchMiddleware.cs namespace DotNetMiddlewareSample.NetCoreApi.Middleware { /// <summary> /// 実行時間を記録するミドルウェアです。 /// </summary> public class StopwatchMiddleware { private readonly RequestDelegate next; public StopwatchMiddleware(RequestDelegate next) { this.next = next; } public async Task Invoke(HttpContext context) { var sw = new Stopwatch(); sw.Start(); // リクエスト実行時間をレスポンスヘッダーに記載します。 context.Response.OnStarting(_ => { context.Response.Headers.Add("X-Execution-Time", new[] { sw.ElapsedMilliseconds.ToString() }); return Task.CompletedTask; }, context); await next(context); sw.Stop(); } } /// <summary> /// <see cref="StopwatchMiddleware"/> 用の拡張メソッドです。 /// </summary> public static class StopwatchMiddlewareExtensions { /// <summary> /// <see cref="StopwatchMiddleware"/> をパイプラインに登録します。 /// </summary> /// <param name="builder"><see cref="IApplicationBuilder"/></param> /// <returns><see cref="IApplicationBuilder"/></returns> public static IApplicationBuilder UseLogging(this IApplicationBuilder builder) { return builder.UseMiddleware<StopwatchMiddleware>(); } } } 大まかな構造は、ほぼ同じような感じです。 ミドルウェアをパイプラインに登録するための拡張メソッド StopwatchMiddlewareExtensions を準備している点も同様ですね。 ASP.NET Web API 2 Owin と比較して、StopwatchMiddleware クラス は 何も継承していないピュアなC#のクラス となっています。 Owin で利用していた IOwinContext は無くなっており、 代わりに HttpContext を用いて、パイプラインの処理を行っていきます。 await next(context); の部分は、後続のミドルウェアを呼び出している部分になります。 続いて、ミドルウェアの登録部分を見ていきましょう。 以下、コードサンプルを引用します。 Program.cs using DotNetMiddlewareSample.NetCoreApi.Middleware; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Host.ConfigureServices(services => { services.AddSwaggerDocument(); services.AddRouting(options => options.LowercaseUrls = true); services.AddControllers(); }); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseAuthorization(); app.MapControllers(); app.UseOpenApi(); app.UseSwaggerUi3(); app.UseStopwatch(); app.Run(); ASP.NET Core のアプリケーション起点処理 (ホスティングモデル) は、.NET5 から .NET6 でかなり簡素化されています! https://docs.microsoft.com/ja-jp/aspnet/core/migration/50-to-60?view=aspnetcore-6.0&tabs=visual-studio#new-hosting-model Startup.cs は不要となり、Program.cs でアプリケーションの起動処理が完結するようになっています。 app.UseStopwatch(); の部分が、ミドルウェアを登録している部分になります。 トップレベルステートメント (C# 9.0の機能) により、非常にスッキリとしたコードになっていますね! https://docs.microsoft.com/ja-jp/dotnet/csharp/fundamentals/program-structure/top-level-statements 実行した結果は、以下の通りになります。 無事、出力が確認出来ました! まとめ ASP.NET Core の ミドルウェアは、最初は取っつきにくい部分もあるのですが、 パイプラインのフローをイメージしつつ、少しずつ慣れていくことで、理解を深めることが出来るかなと思います。 アプリケーションの 共通処理 や 基底処理 を実装している方にとっては、強い味方になります。 本記事により、少しでも皆様の理解の助けになれば幸いです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ASP.NET Coreで簡易プロキシサーバーを作ってみる

この記事は 地震情報アプリ界隈 Advent Calendar 2021 の4日目の記事です。 自己紹介 趣味で KyoshinEewViewer for ingen というソフトウェアを作成しています。 https://svs.ingen084.net/kyoshineewviewer/ 興味あったら見ていってください。 モチベーション この1年で地震観測というジャンルが非常に活発になってきたように思います。 主に、強震モニタ関連のソフトを並べてじゃらじゃら音を鳴らしたりソフトが反応する様子を写した動画や生放送から、揺れたことがわかりやすい物体を置いたカメラ映像をあわせて動画や生放送という形にする方向にシフトしてきています。 ところで、強震モニタに対しての負荷については考えたことはありますでしょうか。 みなさん沢山のソフトを起動して比較などを行っていますが、これらは各ソフトごとに個別に強震モニタから画像を取得しています。 そのため、ソフトを起動すればするほど強震モニタへの負荷が増えてゆき、他の利用者やNIEDに迷惑がかかっています。(現在進行系) そこで、中間のソフトを噛ませることにより負荷をある程度軽減させよう、というのが今回の趣旨です。 使用技術 利便性 主なターゲットとしては自力で証明書の作成やキャッシュサーバーを建てることができないユーザーなので、証明書の生成や登録、hostsの書き換え、キャッシュの内容などをすべてアプリ側で実装することとしました。 フレームワーク・ライブラリ 仕事ではPHPとかGoとかScalaとかを書いていますが、命とお金と諸々の次ぐらいにC#が好きなので ASP.NET Core の Minimal APIを使ってみることにしました。 (結局はミドルウェア1つで完結したせいであんまり使ってない) ほかにもWindowsサービス化などでもフル活用しています。 通常の強震モニタはHTTPですが、長周期地震動モニタはHTTPSです。なので証明書を作成する必要がありました。 こちらはBouncyCastleを使用して証明書を作成し登録することで対応しました。 ポイント オリジンへのリクエスト 普通のサーバーだとリクエスト元とプロキシサーバーが別だったりするのでなんとかるのですが、これはローカルサーバーのプロキシなのでちょっと工夫が必要でした。 まずDNSのレコードが引けませんから、GoogleのDNSのAPIを使用しIPアドレスを取得、HttpClientへは生のIPアドレスを取得しつつ明示的にHostヘッダーを設定しリクエストを送信しました。 オリジンがSNIとか使ってるともっと工夫が必要になります。。。 ちなみに、オリジンの証明書の検証は現状していません。ご注意ください! キャッシュ 今回は IMemoryCache を使用しました。が、完成当初メモリリークがすごく半日起動で1GB消費したり散々でした・・・。 .NET には昔から続く問題として、LargeObjectHeap(LOH)の断片化という問題があります。 プラットフォームによっても異なるためWindowsの話をしますが、具体的にはオブジェクト単体のサイズが85kbを超えると専用の場所にインスタンスが配置されるのですが、オブジェクトの開放など色々あってデータが配置できない空間というのが発生してしまい、メモリ使用量が増えていってしまうという問題です。 今回の実装では80kb以上のレスポンスはキャッシュしないようにすることで対処を行いました。 利用方法 インストール 証明書のインストール・hostsファイルの書き換えを行うため起動には管理者権限が必要です 起動し、上下カーソルキーで 1. サービスのインストール を選択しEnterキーを押すことでサービスがインストールされ、インストールしたPCでの強震モニタへのアクセスがプロキシによって統合されます。 なお、インストール後は実行ファイルのフォルダを変更しないでください。変更したい場合はアンインストールを行ってから変更してください。 アンインストール 起動し、上下カーソルキーで 2. サービスのアンインストール を選択しEnterキーを押すことでアンインストールすることができます。 このままでは証明書がインストールされっぱなしになるので、もう一度起動し 3. 証明書の削除 を選択しEnterキーを押してください。 これでアンインストールは完了です。 トラブルシューティング ソフトの終了時にhostsファイルを戻す動作を行いますが、異常終了した場合などは強震モニタへアクセスできなくなることがあります。 その場合は再度アプリを起動するか、管理者権限でhostsファイルを開き、強震モニタのドメイン名の設定行を削除してください。 さいごに リポジトリはこちらです: https://github.com/ingen084/KyoshinMonitorProxy ごめんなさい、忙しくてほぼプロトタイプのまま公開する運びになってしまいました…。 やる気のある方、PRお待ちしております。。。。 会社の方のアドベントカレンダーにも参加する予定なのでお楽しみに!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

1文字違いの文字列を検出してParty Parrotを騒がせたい(準備編)

Party Parrot動画を見たことはありますか? 一時Twitterなどで流行った こんな感じの図で右上のParrotが喋った内容に対して下4名のParrotが反応し 大抵下ネタの場合に虹色に発光しながら高速回転する例のアレです。ここで勢いのある音楽もよく流れますね。 最近ゲーム仲間の間で何故かParty Parrotが謎の流行をしているので これに近いものをC#とUnityで作れないかと考えました。 まずはC#のコードでそれらしい挙動が出来るか作ってみたのが今回の記事です。 JavaScriptあたりで書いてWeb上で動かせた方が共有しやすかったとコード書いてから気付いた 機能 考える動作としては 何らかの方法で文字列を入力する (とりあえずコンソールからの標準入力を使用、Unity時は黒板への入力としたい) 入力された文字列に、あらかじめ用意した反応する単語が含まれているかチェックする。 含まれていた場合、Parrotたちが高速回転する。 といった感じです。 ところで、Parrotたちは単語そのものだけに反応しているだけでは限りません。 パンチ → パンツのように、類似の文字列にも反応します。 むしろこっちがParrot動画的には多いのではないでしょうか。 そのため 1文字違いまでの単語には反応する。 機能も必要となるのですね。 単純に一致を比較するだけならIndexOf等で一瞬なので ここをどうするかがポイントになります。 さらにさらに、Parrot動画では 完全に一致した場合 → 同時に全員が騒ぐ 一文字違いの場合 → 下4名の誰かが言い換えた下ネタを放って、それに便乗して全員が騒ぐ 騒がない単語の場合は復唱するだけで騒がない。 傾向にあるので、それぞれのパターンを分ける必要がありました。 動作確認 本来の(?)Parrot動画であれば下ネタで騒がせるところですが さすがにおおっぴらに貼るソースに「うんこ」とか列挙するのもアレなので tokyo, osaka, nagoyaの3つをParrotたちの反応する文字列としました。 完全一致する場合 Console.WriteLineとアスキーアートのコピペで表現しているので ズレまくりで大変不細工なカカポになっていますが、とりあえず反応を見られました 1文字違いの場合 デバッグ用の数字が出たまんま index = 3で文字列を発見して、 tokoo を tokyo に置き換えて発言してくれてます。 全く違う場合 復唱しているのは完全一致の場合と変わりませんがカカポが1匹になっています。 完全一致の場合と結果が区別できていますね! 1文字違い判定の実装について C#はRegexクラスを利用すれば正規表現で比較ができるので 例えば「tokyo」であれば「.okyo」から「toky.」までを正規表現に放り込めば1文字違いの文字列を発見してくれます。 それなら、指定した位置を「.」に変える関数作ってString.Length分だけループすればいけますね! changeDot private string changeDot(int replacePosition, string targetString){ if (replacePosition == 0){ return "." + targetString.Substring(1); } else if (replacePosition == targetString.Length){ return targetString.Substring(0,targetString.Length - 1) + "."; } return targetString.Substring(0,replacePosition) + "." + targetString.Substring(replacePosition + 1); } というわけでこんな関数で実装してみました。 ソースコード全体 kakapo.cs using System; using System.Text.RegularExpressions; //固定値(仮) public static class ConstNum{ public const int FULL_CORRECT = 3; public const int ONE_DIFFERENCE = 2; public const int DIFFERENCE_STRING = 1; } public class Libraly{ //Parrotたちが反応する単語の配列 private string[] Lib = { "tokyo", "osaka", "nagoya" }; public string compareString(string targetString){ foreach (string libString in Lib){ if (targetString.IndexOf(libString) != -1){ return targetString; } else if (searchOneDiffString(libString, targetString) != null){ return searchOneDiffString(libString, targetString); } } return null; } private string searchOneDiffString(string libString, string targetString){ int i; string pattern; for (i = 0; i < libString.Length; i++){ pattern = changeDot(i, libString); Regex reg = new Regex(pattern); Match match = reg.Match(targetString); if (match.Success) { Console.WriteLine(match.Index); return targetString.Replace(targetString.Substring(match.Index), libString); } } return null; } //与えられた文字列のn番目の文字を.に置き換える private string changeDot(int replacePosition, string targetString){ if (replacePosition == 0){ return "." + targetString.Substring(1); } else if (replacePosition == targetString.Length){ return targetString.Substring(0,targetString.Length - 1) + "."; } return targetString.Substring(0,replacePosition) + "." + targetString.Substring(replacePosition + 1); } } public class TeacherParrot{ public void sayWord(string word){ Console.WriteLine("   _,.、-‐冖‐- .,_"); Console.WriteLine(" ‐=ニ ,_:i:i:i:i:i:i:_,ニ=‐"); Console.WriteLine("    | {ニ=‐ ‐=ニ }_"); Console.WriteLine("    ∧ ●i⌒Y ● `、    " + word); Console.WriteLine("    `¨,′ {  ‘     `、"); Console.WriteLine("    {  `、/       }"); Console.WriteLine("  ‘          /\"); Console.WriteLine("  }       -‐ '゛    ` 、"); Console.WriteLine(" /              `、"); Console.WriteLine("  ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄"); } } public class Parrot { public void doParty(int count, string word){ int i; for(i = 0; i < count; i++){ Console.WriteLine("          / ̄~\"'+、"); Console.WriteLine("       / ● i⌒Y●"); Console.WriteLine("       /    {  ‘  ゚,   " + word); Console.WriteLine(".     {     ‘,/  :}"); Console.WriteLine("     /\     ′  ,′"); Console.WriteLine(". _、-'゙     ` ‐-      {"); Console.WriteLine("/                ‘,"); Console.WriteLine(" ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄ ̄"); } Console.ReadLine(); } } public static class main{ static Parrot Parrot1 = new Parrot(); static TeacherParrot Teacher = new TeacherParrot(); static Libraly SimLib = new Libraly(); public static void Main(){ string inputString; string partyString; inputString = Console.ReadLine(); Teacher.sayWord(inputString); partyString = SimLib.compareString(inputString); if (partyString == null){ Parrot1.doParty(ConstNum.DIFFERENCE_STRING, inputString); } else if(inputString == partyString){ Parrot1.doParty(ConstNum.FULL_CORRECT, partyString); } else{ Parrot1.doParty(ConstNum.ONE_DIFFERENCE, partyString); } } } とりあえず「文字列との比較結果によって処理を変える」部分は実現できたので あとはUnityに落とし込んで、それぞれのイベントをどう作るか、となりました。 しょうもない内容でもやっぱりコード書くのって楽しいですね。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

似た顔のFANZA女優を顔写真から検索出来るUI/UXを考えるのに苦労した

この記事は 驚異のFANZA女優検索 Advent Calendar 2021 の 3 日目の記事です。 似た顔のFANZA女優を顔写真から検索出来るUI/UXを作る 似た顔のFANZA女優を顔写真から検索出来るようにしたが、顔認識はAzure Cognitive ServicesのFace APIを使うことでそれ程苦労せずに出来た。ただこれを実際に機能としてサービスに乗せる時にどういうUI/UXにするか、これは非常に悩んだ。 顔写真を指定するUI/UXは2種類が多い 最初のポイントは顔写真を指定してもらう方法だ。顔認識が出来るWEBサービスを色々見てみると、URLを指定する方法とローカルファイルをアップロードする方法の2つを採用している所が多かった。ただ方法が2つあると、ユーザーはどちらの方法を使うかを選ぶ必要がある。2つの方法から1つを選ぶというUI/UXは決してシンプルとはいえないと思う。また1つのUIを操作している時もう1つのUIは使わないにも関わらずレイアウトは占拠される。個人的に使っていないUIが表示されるのは嫌だった。それらを考慮するとGoogleの画像検索のように選択方法がタブになっているのが一番しっくりきた。タブなら選んでいるレイアウトだけが表示されるところも良いと思った。 タブの場合デフォルトはどっちにするか タブ式のUIにするのであればデフォルト表示を何にするかは極めて重要だと思う。今回はURL指定をデフォルトにすべきだと思った。まずローカルファイルに顔写真があるかは人に依る所が大きいと思う。また殆どの人(特に男性)は自分の顔で検索したいわけではないと思う。つまりローカルファイルをアップロードする方法を使うケースは基本少ないと思う。(ただ少ないが確実に0ではないと思うので無くすという選択肢は考えなかった)。またURL指定ならサンプルを用意できるメリットがあると思った。サンプルをクリックしたらURL指定する所に入るようにすればユーザーへの操作説明にもなる。 採用しなかった3つ目の方法 余談だが、WEBサービスを見ている時、顔写真を選ぶ方法がもう一つあった。それはカメラを起動してその場で写真を撮ってアップするという方法だ。面白そうと思ったので、実験でカメラを起動させて写真を撮る所までは作った。ただ作ってみて実際にサービスとして採用するならカメラが複数あった場合の対応なども考えなければいけないし、そもそも自分の写真と似た顔のFANZA女優を検索したい状況はあまりないだろうと思って結局この機能の実装は止めた。(3つあってもレイアウトにさほど悩まなくて済むのもタブの良さだと思った) さらにUXを詰める タブで顔写真を選べるようにした。最初は選んだらその画像が有効な画像か判定して、その後検索ボタンを押すことで検索出来るようなUIを作った。しかし、実際に作ってみて選択と検索で2回操作をするのは面倒だと思った。顔写真を選択したら、そのまま似た顔のFANZA女優が表示されるようにした。ただこの流れで画面切り替えが入ってしまうとやっぱりUX的にはイマイチと感じたので、AJAXを使って顔写真を選択して検索されるところまで画面切り替えなしにしたくなった。(AJAXに関しては明日詳細書く)。これで自分的には大分良い感じになってきた。そんな時にたまたま自分の参考に出来そうな面白いサービスが話題になっていた。 イライザダイジェクトのセーフティーネット参考に そのサービスはイライザダイジェクトという文章を入力させて、要約を作るサービスだ。当たり前の話だが、文章を入力しなければ何も起きない。そうすると多くの人は既に書かれている文章をどこかからコピーして持ってくることが多いと思う。要約スタートを押す前に利用規約に同意する必要があり、そこには以下の文章が書いてあった。 本サービスの利用により、第三者の著作権その他の権利を侵害しないよう十分ご留意ください。詳細は利用規約をご確認ください 正直このアプリに入れられる文章が小説の一説だったとしても、それは使用者の自己責任でありアプリ自体の問題ではないよう気がする。それでもここまでの文章を用意しているは多分セーフティーネットだろうなと思った。同じように自分のアプリで、URLを指定させる方法は、言い換えれば他のページから画像情報を取得することになる。人によっては女優やアイドルの顔写真を使う人もいるだろうと思うと自分のアプリでもこれと同等のことをした方が安全だろうと思った。 イライザダイジェクトのSNS投稿機能で気づいたこと またイライザダイジェクトは結果をSNSに投げる機能もあった。最初はこれも実装しようと思った。しかしその時に気づいた。あーこれをSNSに投げた時点でさらに著作権が関わってきそうだ。つまり結果だけを貼るより元がどうであって、それがどうなったかを表示しようとすると、どうしても元文章が必要になる。そこで有料で出版されている本の文章が丸々のっていたらやっぱりそれは問題になるだろう。だからここまでやっているんだなと気づいた。 SNS投稿機能は止めた で、SNSへの投稿機能は悩んだが、結局作らなかった。一番大きい理由は顔検索を機能として使っている分には面白いとしても、それをSNSに投稿出来るようにすると、本来面白く使ってほしくて作った機能を別の用途で使う人もいる可能性があるからだ。つまりFANZA女優の誰々に似ていると言われて気分を害する人もいるだろうし、似ていることで揶揄されたり、酷いケースだといじめにつながる可能性すらもある。サービスを作る側がどこまでそれを踏まえるべきかは正直分からない。機能としてはむしろ入れたい。ただ、これはやっぱり凶器になりうる可能性もあると思ってしまったので、あくまで自分のサービスの中だけで完結する機能とした。だからそれであればイライザダイジェクトのようにボタンを押すときのチェックボックスによる同意まで取るのは冗長かと思って一言書くだけに留めた。アップロードしたデータも最初は何か使えるので取っておいても良いかと思ったが、元々Azureが24時間でデータが消える仕組みだったので、自分のサービスでもアップされたデータは保存しない仕組みにした。 顔写真から似た顔のFANZA女優を検索出来るUI/UX というわけで画像を選んで、顔検索が出来る仕組みは、色々悩んだ末にシンプルに作ったつもりだ。UI/UXには絶対的な正解はないので、気になる所があれば是非教えて欲しい。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む