- 投稿日:2019-11-15T23:30:39+09:00
競プロ風じゃんけんプログラムを作る
競プロというと、あるお題が出され、テキストベースのテストデータが入力されると、ある結果を出力するように期待される。
今回は、じゃんけんプログラムを普通に作ったところで面白くないから、自分が出す手と勝負する回数をあらかじめ入力し、CPUと勝負した結果を出力するというじゃんけんを作る。問題文
自分が勝負したい回数(N)を最初に入力し、その後、自分が出す予定の手(グー=0、チョキ=1、パー=2)をN回入力する。その後、CPUと対戦をし、その結果(あなたの勝ち=a、負け=b、あいこ=c)を出力せよ。
制約
- 1≦N≦100
入力
入力は以下の形式で標準入力される。
N 012出力
a b c入力例1
5 02102出力例
あなたの勝ち:2回 CPUの勝ち:1回 あいこ:2回なお、あくまでこれは基本回答であるからして、創造力(クリエイティビティ:配点20)が高い出力結果を出してもよい。
回答例
qiita.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Janken { public class Program { public static string[] jankenAA ={"\nグー! __\n /」」」」\n |っ丿\n ∧_∧ / /\n ( ・ω・)/\n (つ ノ\n u-u" , "\nチョキ!\n (V)\n /アE)\n ∧_∧ / /\n ( ・ω・)/\n (つ ノ\n u-u" , "\nパー!\n 「「「h\n C ノ\n ∧_∧ / /\n ( ・ω・)/\n (つ ノ\n u-u" }; public static void Main(string[] args) { Console.WriteLine("※説明:最初に何回じゃんけんをするか回数を入力する。\nその後、回数分の自分の出すじゃんけん数値を入れる。\nグー=0、チョキ=1、パー=2"); try { int n = int.Parse(Console.ReadLine()); int[] jankMe = new int[n]; for (int i = 0; i < n; i++) { string jankme = Console.ReadLine(); if (!(jankme.Equals("0") || jankme.Equals("1") || jankme.Equals("2"))) { Console.WriteLine("じゃんけん番号じゃないです"); i -= 1; continue; } jankMe[i] = int.Parse(jankme); } for (int j = 0; j < n; j++) { //Console.Clear(); Console.WriteLine("最初はグー、じゃんけん…"); int pjank = Player(jankMe[j]); int cpjank = Cpu(); Judge(pjank, cpjank); } Statistics(judgeList); Console.ReadKey(true); } catch (Exception e) { string m = e.Message; Console.WriteLine(m); Console.ReadKey(true); } } /// <summary> /// プレイヤー /// </summary> /// <returns>じゃんけん番号</returns> public static int Player(int jankMeNow) { string janken = null; bool loopFlg = true; //while (loopFlg) //{ //janken = Console.ReadLine(); janken = jankMeNow.ToString(); if (!(janken.Equals("0") || janken.Equals("1") || janken.Equals("2"))) { Console.WriteLine("0か1か2で入れてください。"); loopFlg = true; } else { loopFlg = false; } //} int jank = int.Parse(janken); Display(0, jank); return jank; } /// <summary> /// CPU /// </summary> /// <returns>じゃんけん番号</returns> public static int Cpu() { int randomInt = Janken(); Display(1, randomInt); return randomInt; } /// <summary> /// 画面への表示 /// </summary> /// <param name="isPlayer"></param> /// <param name="jank"></param> public static void Display(int isPlayer, int jank) { if (isPlayer == 0) { Console.Write("あなたは、"); } else { Console.Write("コンピューターは、"); } Console.WriteLine(jankenAA[jank]); } /// <summary> /// ランダム関数 /// </summary> public static Random rand = new Random(); /// <summary> /// CPUがランダムで出すじゃんけんの関数 /// </summary> /// <returns>ランダムで出した数</returns> public static int Janken() { int jank = rand.Next(3); return jank; } /// <summary> /// ジャッジ /// </summary> /// <param name="player"></param> /// <param name="cpu"></param> public static List<int> judgeList = new List<int>(); public static void Judge(int player, int cpu) { int judge = 0; if ((player == 0 && cpu == 1) || (player == 1 && cpu == 2) || (player == 2 && cpu == 0)) { Console.WriteLine("【プレイヤーが勝ち】"); judge = 0; } else if ((cpu == 0 && player == 1) || (cpu == 1 && player == 2) || (cpu == 2 && player == 0)) { Console.WriteLine("【CPUが勝ち】"); judge = 1; } else { Console.WriteLine("【あいこ】"); judge = 2; } judgeList.Add(judge); Console.Write("\n"); } public static void Statistics(List<int> jlist) { int playerWin = jlist.Where(j=>j==0).Count(); int cpuWin = jlist.Where(j=>j==1).Count(); int draw = jlist.Where(j => j == 2).Count(); Console.WriteLine(string.Format("プレイヤーの勝利数:{0}\nCPUの勝利数:{1}\nあいこ数:{2}", playerWin,cpuWin,draw)); } } }入力結果
※説明:最初に何回じゃんけんをするか回数を入力する。 その後、回数分の自分の出すじゃんけん数値を入れる。 グー=0、チョキ=1、パー=2 3 0 1 2出力結果
最初はグー、じゃんけん… あなたは、 グー! __ /」」」」 |っ丿 ∧_∧ / / ( ・ω・)/ (つ ノ u-u コンピューターは、 グー! __ /」」」」 |っ丿 ∧_∧ / / ( ・ω・)/ (つ ノ u-u 【あいこ】 最初はグー、じゃんけん… あなたは、 チョキ! (V) /アE) ∧_∧ / / ( ・ω・)/ (つ ノ u-u コンピューターは、 パー! 「「「h C ノ ∧_∧ / / ( ・ω・)/ (つ ノ u-u 【プレイヤーが勝ち】 最初はグー、じゃんけん… あなたは、 パー! 「「「h C ノ ∧_∧ / / ( ・ω・)/ (つ ノ u-u コンピューターは、 チョキ! (V) /アE) ∧_∧ / / ( ・ω・)/ (つ ノ u-u 【CPUが勝ち】 プレイヤーの勝利数:1 CPUの勝利数:1 あいこ数:1はい。
- 投稿日:2019-11-15T20:29:49+09:00
C# - FontをPixel単位にしたいときはGraphicsUnit.Pixelを指定しよう(兼 フォントメトリクス再調査)
以前投稿した C# - フォントメトリクスを調査してみたらカオスだった件 - 未解決 のイケてなかったとこを修正してみた。
イケてなかったとこ#1 - 単位が合っていなかった
フォントのインスタンスを生成するときに、
new Font(フォント名,フォントサイズ)
で指定していたが、このコンストラクタではフォントサイズ
はピクセル単位ではなくポイント単位であり、解像度に依存する。
ピクセル単位で作りたいときは、
new Font(フォント名,フォントサイズ,GraphicsUnit.Pixel)
のように指定する必要がある。イケてなかったとこ#2 - フォントが代用されていた
代用されるのを避けるには、
StringFormat
のプロパティFormatFlags
にStringFormatFlags.NoFontFallback;
を指定する必要がある。
(前記事でコメント頂いて知りました。感謝!)画面キャプチャ
accent と accent+descent の位置に補助線(それぞれ青の点線と黒の実線)を入れています。
accent+descentで高さ方向が収まるっぽい。
公式には下記の記載があるので、多分あってるはず。Note that the em height (also called size or em size) is not the sum of the ascent and the descent. The sum of the ascent and the descent is called the cell height. The cell height minus the internal leading is equal to the em height. The cell height plus the external leading is equal to the line spacing.
ソースコード
using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Windows.Forms; class FontMetricsTest : Form { TrackBar trackbar; PictureBox pct; ComboBox cmbFonts; TextBox txtContent; FontMetricsTest() { Text = "Font metrics test"; ClientSize = new Size(600, 400); cmbFonts = new ComboBox(); foreach (FontFamily ff in FontFamily.Families) { cmbFonts.Items.Add(ff.Name); } cmbFonts.Text = SystemFonts.DefaultFont.Name; cmbFonts.Location = new Point(0,0); cmbFonts.Width = 300; cmbFonts.DropDownHeight = 500; cmbFonts.DropDownStyle = ComboBoxStyle.DropDownList; cmbFonts.SelectedIndexChanged += (sender,e)=>{MyRedraw();}; Controls.Add(cmbFonts); trackbar = new TrackBar(); trackbar.Location = new Point(300,0); trackbar.Maximum = 100; trackbar.Value = 50; trackbar.Minimum = 1; trackbar.TickFrequency = 33; trackbar.ValueChanged += (sender,e)=>{MyRedraw();}; Controls.Add(trackbar); txtContent = new TextBox(); txtContent.Location = new Point(0, 50); txtContent.Width = 300; txtContent.Text = "g あいう Qiita"; txtContent.TextChanged += (sender,e)=>{MyRedraw();}; Controls.Add(txtContent); pct = new PictureBox(); pct.Location = new Point(0, 80); pct.Size = new Size(600, 300); pct.Image = new Bitmap(600,300); Controls.Add(pct); MyRedraw(); } void MyRedraw() { int w = pct.Image.Width; int h = pct.Image.Height; string fontName = cmbFonts.Text; Font font = new Font(fontName, (float)trackbar.Value, GraphicsUnit.Pixel); string text = txtContent.Text; if (font.Name != fontName) { text = "Font unmatch: \"" + font.Name + "\" is loaded."; } using ( Graphics g = Graphics.FromImage(pct.Image) ) { g.Clear(Color.White); FontFamily ff = font.FontFamily; int ascent = ff.GetCellAscent(font.Style); int descent = ff.GetCellDescent(font.Style); int emHeight = ff.GetEmHeight(font.Style); float fontHeight = font.GetHeight(g); float ascentSize = font.Size * ascent / emHeight; float descentSize = font.Size * descent / emHeight; Console.Write("==== "); Console.Write(font.Name); Console.WriteLine(" ===="); Console.Write("font.Size: "); Console.WriteLine(font.Size); Console.Write("fontHeight: "); Console.WriteLine(fontHeight); Console.Write("sum in pixel: "); Console.WriteLine(ascentSize+descentSize); Console.Write("ascent in pixel: "); Console.WriteLine(ascentSize); Console.Write("descent in pixel: "); Console.WriteLine(descentSize); Console.Write("sum: "); Console.WriteLine(ascent+descent); Console.Write("ascent: "); Console.WriteLine(ascent); Console.Write("descent: "); Console.WriteLine(descent); Console.Write("emHeight: "); Console.WriteLine(emHeight); Pen pen = new Pen(Color.Black, 1.0f); Pen penDash = new Pen(Color.Blue, 1.0f); penDash.DashStyle = DashStyle.Dash; g.DrawLine(pen, 0, ascentSize+descentSize, w, ascentSize+descentSize); g.DrawLine(penDash, 0, ascentSize, w, ascentSize); StringFormat sf = new StringFormat(); sf.FormatFlags = StringFormatFlags.NoFontFallback; g.DrawString(text, font, Brushes.Black, new PointF(0,0), sf); } pct.Refresh(); } [STAThread] static void Main(string[] args) { Application.Run(new FontMetricsTest()); } }
- 投稿日:2019-11-15T14:46:58+09:00
Xamarin.Macにおける、画面遷移に伴う値の受け渡し
パターン1: ダイアログ(モーダル)に値を受け渡す
MainViewController
: アプリのメイン画面を扱うViewControllerとする
ModalViewController
: メイン画面から開くモーダルを扱うViewControllerとするメイン画面の情報をダイアログに受け渡す、例えばメイン画面で保持されているユーザー情報をダイアログに引き渡すみたいな場合。
MainViewController.csprivate UserInfo userInfo; // ユーザー情報を表すフィールド // 画面遷移の前に実行されるメソッド public override void PrepareForSegue(NSStoryboardSegue segue, NSObject sender) { // DestinationControllerプロパティで遷移先を参照できる var modalViewController = segue.DestinationController as ModalViewController; modalViewController.userInfo = userInfo; // 受け渡し方は自由(メソッドなどを使っても良い) }セグエを表すオブジェクトのプロパティに、遷移先のViewControllerを表すものがあるのでそれを利用する。この時キャストを忘れないように注意。
値の受け渡しは遷移先のViewControllerに対応するフィールドを用意しておいて引き渡すでも良いし、メソッドを用意しておくのでも良い。ただしprivate
メソッドだと呼び出せないのでinternal
にしておくこと。パターン2: ダイアログ(モーダル)から呼び出し元に値を渡す
パターン1の逆。例えば設定ダイアログなどを開き、その設定をメイン画面に反映させる場合など。
ModalViewController.csprivate UserSettings settings; // 設定情報を表すフィールド // ダイアログを閉じるボタン(OKボタンなど)のクリックイベント partial void OKButtonClicked(NSObject sender) { // PresentingViewControllerプロパティで呼び出し元を参照できる var mainViewController = PresentingViewController as MainViewController; mainViewController.settings = settings; // ここも受け渡し方は自由 // ダイアログを閉じる DismissController(this); }見た目はパターン1とほぼ同じだが、セグエを使っていない点が大きく違う。
セグエは現在の画面からダイアログやモーダルシートなどの所謂子ウィンドウを開くためのものなので、それによって開かれた画面から親ウィンドウへ情報を渡すような動作の場合、セグエではなく「OK」ボタンなどのクリックイベントに処理を記述するのが自然(だと思う)。
DismissController
は単純にダイアログを閉じるためのメソッド
ダイアログを閉じるだけのボタンを作る場合はXcodeのFirst Responderで設定できるが、上記のように受け渡しのロジックなどを追加する場合はクリックイベントを定義する必要がある。補足: 画面遷移に合わせてUIを操作する場合の注意点
パターン1,2に共通して言えることだが、例えば設定した内容によって画面の文言が変わる、といったように遷移先のUIを操作したい時がある。
が、UIコンポーネントのオブジェクトは基本的に
private
プロパティに設定されているので別のViewControllerからは参照できない。これは自身でprivate
に設定したフィールドやメソッドも勿論同様である。MainViewController.cspublic override void PrepareForSegue(NSStoryboardSegue segue, NSObject sender) { var modalViewController = segue.DestinationController as ModalViewController; modalViewController.SomeLabelTextField.StringValue = "hogehoge" // => UIコンポーネントのオブジェクト(SomeLabelTextField)の参照でエラーが発生する }このアクセスレベルは変更できない(はず)ので、遷移先で
internal
以上のメソッドを作成しUI操作をラップする。ModalViewController.csinternal void ChangeText(string newText) { SomeLabelTextField.StringValue = newText; }MainViewController.cspublic override void PrepareForSegue(NSStoryboardSegue segue, NSObject sender) { var modalViewController = segue.DestinationController as ModalViewController; modalViewController.ChangeText("hogehoge"); // ChangeTextメソッド自体はprivateじゃないので呼び出せる }簡単にですが、値の受け渡しについてでした。ポイントはとにかく遷移先となる画面のViewControllerオブジェクトを取得することです。それさえ出来ればどうにでもなる。
本当はパターン3として通知オブジェクトを使うものを考えていたけど、画面遷移関係ないなってのと通知についての説明含めて別でまとめた方が良いなと思ったので……またその内投稿します。
何かご指摘などありましたらコメントお願いします!
Xamarin.Mac(というかMacOSのネイティブアプリ開発事例)についての情報が国内だと中々少なく、自分も未だ手探り中なので情報提供して頂けると助かります……?
- 投稿日:2019-11-15T13:24:16+09:00
【C#,Unity】Dictionary<GameObject,float>をfloat基準で昇順にSortしようとしたら謎処理を挟んでいた話
追記【2019/11/15】
@albireo 様にアドバイスしていただいた
SortedList
の方が簡易的かつ無駄なく処理できたので
記事の末尾に追加しておきました。この記事の狙い
自分の経験を書き記すとともに、有識者の方々からアドバイスや説明をもらえたらいいなぁ。
という期待。あらすじ
Unityで画面内に映っているTargetを取得&自キャラとTargetの距離を取得できるようになったので、
これを「距離が近い順」で並べ替えたい!と思い、色々調べながら試してみることに。本題
まず「画面内に映っているTarget」と「Targetまでの距離」、この二つの情報を紐付けるには
どうすればいいのかを調べるところから始めた。これは調べると直ぐに出てきた、最初はList<>か配列[ ]でなんとかするしかないのかな?とか考えていたが、
調べてみるとDictionary
という超便利なクラスがあるではないか。
早速、意気揚々と使ってみる。Dictionary<GameObject,float> ProvisionalValue = new Dictionary<GameObject,float>(); ProvisionalValue.Add(hit,target_distance);よし、これで情報の紐付けはできたな...!
後はこれを「距離が近い順」で並べ替えるだけだ...!なんだ楽勝じゃん。勝ったな、風呂入ってくる。
ここからが本当の地獄だ...!
さて、ちゃちゃっとSortして終わらせるか~
ProvisionalValue.Sort();...あれ、できない。なになに?DictionaryにはSortの定義が含まれていない?
あら、そうなのか...。じゃあどうやって並び替えするんだろう、調べてみるか...。ははーん、Linqを使えば出来るのね。試したことが無いけど、やってみるか...。
ここで訳の分からないことをし始める(多分色々調べているうちにいろんな記事の情報が入り混じった)
えーっと、まずはソートする基準がいるから(?)floatだけをList<>で取得してからこれをOrderByで
並べ替えて...、あれ?Valueが定義が含まれていない...?(Listだから)これは何かがおかしい...!という感じで混迷を極めた結果、こうなった。
private static Dictionary<GameObject,float> hitsOB = new Dictionary<GameObject,float>(); private static Dictionary<GameObject,float> ProvisionalValue = new Dictionary<GameObject,float>(); { ProvisionalValue.Add(hit,target_distance); Sort(ProvisionalValue); } public static void Sort(Dictionary<GameObject,float> itemTable) //取得したオブジェクトと距離のデータを距離が近い順でソートするメソッド { IOrderedEnumerable<KeyValuePair<GameObject,float>> _table_1 = itemTable.OrderBy(selector => selector.Value); foreach(KeyValuePair<GameObject,float> pair in _table_1) { hitsOB.Add(pair.Key,pair.Value); } }改めて見ると何やってるんだか、と思いました。
ProvisionalValueに格納したデータをOrderByで並べ替えて、並べ替えたデータを_table_1に格納して、
foreachでpairに一つずつ格納してhitsOBに順に格納....いや、これ普通にProvisionalValueをOrderByでソートすればいいじゃん。
はい、というわけでそうした結果がこちら。
ProvisionalValue.Add(hit,target_distance); ProvisionalValue.OrderBy(x => x.Value);二行で終了。(なぜ俺はあんなムダな時間を...)
ふりかえり
そもそも混乱した理由は「よく理解していなかったから」につきます。
Dictionaryの値の取得方法すら理解していなかったが為に、KeyとValueがなんなのか分からずに使っていた
結果、今回のような無駄な時間を浪費する羽目になりました...。まさしく「知識は身を助ける」を身をもって実感しました。
追加記事【SortedList】
先に紹介したDictionaryクラスは値を格納した後にOrderByでソートしていましたが、
SortedListクラスを使用すればわざわざソートする必要がなくなります。しかし、注意しなければいけない点があります。
それはSortedList <TKey,TValue>
では、キー(TKey)
の値を基準にソートされます。今回の例で行くと、先の紹介したDictionaryは
Dictionary<GameObject,float>
としていましたが、
これだとTKeyの値に代入しているGameObjectのソート基準はNameの頭文字をABCD...順で並び替える為、
ソートの基準は距離で並び替えたかったので、順番を入れ替える必要がありました。ということを踏まえた上で変更したものが以下の流れです。(距離の所得等は省いています)
private SortedList<float,GameObject> ProvisionalValue; void Start() { ProvisionalValue = new SortedList<float,GameObject>(); } public void Sort() { ProvisionalValue.Add(target_distance,hit); foreach(var item in ProvisionalValue) { Debug.Log("Distance : " + item.Key + " / Object : " + item.Value); } }これでコンソールに距離順で距離とオブジェクト名が表示されます。
- 投稿日:2019-11-15T07:10:13+09:00
Unityで物理演算を使いたくなかった話 ~横スクロールアクションゲームの開発~
物理演算を使いたくない理由
Unityの物理演算(Rigidbody周り)は大変便利な機能ですが、時に制御しきれないときもあります。
意図せず、Collisionを突き抜けてしまったりしますよね。
なにより、Unityの便利機能に甘えっぱなしな感じがちょっと嫌なので、今回はRigidbody、Rigidbody2Dを使わず、横スクロールアクションゲームを開発した時のことを書こうと思います。(Collisionは使います。)
不思議の国のアリスが、ハートの女王などが仕掛ける罠をよけるゲームです。Raycastを使う
本来、Rigidbodyを設定しないと、Collision周りのメソッド(OnCollisionEnterメソッドとか)が反応しないため、「何かに当たったときに、ダメージを負う」的な処理ができません。
なので、結局Unityの機能に甘える形にはなりますが、Rayを使用します。Rayを投げ、当たったCollisionの座標との距離を測ったりして、衝突判定の処理を実装します。
これは、Debug.DrawRay()
メソッドでキャラクターやトラップからRayを飛ばす様子をSceneウィンドウから確認したものです。白い線が当たり判定によるアリスの動きの処理をするためのRayで、鎌から伸びている赤い線が、アリスに当たった瞬間アリスにダメージを与える処理をするためのRayです。床判定、壁判定
今回作るゲームは、スマートフォン向けのずっと横に走り続けながら、敵をかわしてスコアを伸ばすゲームなので、床の判定と壁の判定が必要になりました。
なので、まずはキャラクターから横向きと、下向きにRayを飛ばします。(左右に動く場合は、左右どちらにもRayを飛ばす必要がありますが、今回はずっと右向きに走るゲームなので、右向き、Vector3(1,0,0)の方向と下向き、Vector3(0,-1,0)の方向のみにRayを飛ばします。)Alice.csprivate bool IsCollision(Vector2 direction,float distance,float adjast = 0) { var position = transform.position; // Rayを飛ばす初期位置を調整する position.y += adjast; var raycastHit2D = Physics2D.RaycastAll(position, direction, distance); Debug.DrawRay(position, direction); // 何も検知できなかった場合、処理を中断し、falseを返す if (raycastHit2D.Length == 0) return false; foreach (var raycastHit in raycastHit2D) { if (raycastHit.collider && !raycastHit.collider.gameObject.Equals(gameObject)) return true; } return false; }※Rayを飛ばすオブジェクト自体にCollisionを付けている場合、Rayを飛ばす位置は自身のオブジェクトも含んで当たったことを通知します。なので自身と子オブジェクトの当たり判定を外す必要があります。
移動の処理
Rigidbodyを使う場合は、velocityなどを使えば簡単にオブジェクトを動かせます。
しかし、Rigidbodyを設定していないため、Tween系のライブラリを使うか、transform.positionを愚直に操作するしかありません。本来であればTweenなどを使うべきかもしれませんが、面倒くさかったため、transform.positionの値を0.001fずつ動かすというヤバみの深い実装になりました。
(UIとかはTween使うと綺麗に動くので、Tween好きです。)結論
カジュアルゲーム作るぐらいなら、普通にRigidBody使ったほうが良いと思いました。
ちょっと調べると、Rayも普通に処理として重いそうです。(なので、あらかじめ飛ばす距離などはできるだけ短く指定してあげるとましになるとか、ならないとか・・・)あと、見た目が2.5Dの場合は、2D用のRayではなく、3D用のRayを使用したほうが開発しやすいのでは・・・と思いました。
- 投稿日:2019-11-15T00:11:13+09:00
New Input System をふわっと理解する ~大八耐2019 in Tokyoを添えて~
まえがき
大八耐2019 in Tokyoに参加したときに気になっていたNew Input Systemを触ってみたのでふわっとまとめたみました。
*Input System - 1.0.0 時点での記事なので以降のバージョンと差異がある可能性があります。
Input System のインストール
Input SystemはPackage Managerを通じてインストールします。
*2019年11月時点では、まだpreview packageなのでShow preview packagesにチェックを入れないとリストに現れません。
次に、Project Settings > PlayerからActive Input Handlingを設定します。設定を反映させるにはUnity Editor の再起動が必要なので注意しましょう。
Input Settings を作る
次は、Project Settings > Input System Package からCreate settings assetを選択し、
InputSystem.inputsettings
を作成します。
作成したInputSystem.inputsettings からOpen Input Setting Windowを選択し、
Supported Devices
に入力を取りたいデバイスを登録します。
Input Actionを使ってみる
Input Actionは、入力があった際のイベントをデリゲートに登録しておくことで動作します。今回は、キーボードのスペースキーとスクリーンのタップと、マウスとタップの座標を取得してみます。
GettingFromDevice.csusing UnityEngine; using UnityEngine.InputSystem; public class GettingFromDevice : MonoBehaviour { [SerializeField] private InputAction _jumpInput = default; [SerializeField] private InputAction _moveInput = default; private void Awake() { // A事前にイベントを登録しておく _jumpInput.performed += callbackContext => { // Buttonの入力はfloat var value = callbackContext.ReadValue<float>(); if (value > 0) { Debug.Log("On Jump."); } }; _moveInput.performed += callbackContext => { var value = callbackContext.ReadValue<Vector2>(); Debug.Log($"position {value.x},{value.y}"); }; } private void OnEnable() { // Enable()で有効化しないと動作しない _jumpInput.Enable(); _moveInput.Enable(); } private void OnDestroy() { _jumpInput.Dispose(); _moveInput.Dispose(); } }InputActionフィールドを定義し、インスペクタで各InputActionにBindした入力受けたときのイベントをInputAction.performedに記載します。この時InputAction.Enable()で有効かしておかないと動作しないので注意が必要です。
CallbackContext.ReadValueで受け取る値は、インスペクタ上の各InputActionの歯車ボタン内のAction TypeとControl Typeから設定しています。
今回の例では、Jump InputはAction TypeはButton、Move InputはAction TypeはValue、Control TypeをVector2としています。ActionをButtonにした場合は、Control Typeはfloatになっています。
キーボードのWASDをなど複数のキーを入力として取りたい場合、Add Bingindではなく
Add 2D Vector Composite
を使います。上下左右のにそれぞれのキーをBind
することでVector2の入力を作ることができます。ただしMouseのPositionとは異なり座標ではなく-1から1の正規化された入力なので併用する場合は工夫が必要です。ActionMapを生成してみる
先ほどは、InputActionフィールドを用意してデリゲートにイベントを登録しました。今度はActionMapを生成し、自動生成されたコードを継承してコールバックを受け取る方法を試してみます。先ほどと同じくキーボードのスペースキーとスクリーンのタップと、マウスとタップの座標を取得してみます。
Asset > Create > Input ActionsからSampleControles.inputactionsを生成し、SampleMapsの中に先ほどと同じくMoveActionとJumpActionをを設定しました。次にGeberate C# Classにチェックを入れてApplyすると同じフォルダに同名のクラスが生成されます。今度はこれを動かすコードを書いていきます。ActionMapSample.csusing UnityEngine; using UnityEngine.InputSystem; // 生成されたクラスはI[ActionMap]インターフェスを持っているので、これを実装します。 public class ActionMapSample : MonoBehaviour, SampleControls.ISampleMapsActions { private SampleControls.SampleMapsActions _sampleMapsActions = default; private void Awake() { // SampleControlsに登録したActionMapを生成ます。 _sampleMapsActions = new SampleControls.SampleMapsActions(new SampleControls()); // SampleControls.ISampleMapsActionsが実装されたクラスをSetCallbacksに指定します。 _sampleMapsActions.SetCallbacks(this); } // SampleControls.ISampleMapsActionsによって定義されたMoveActionのコールバック public void OnMoveAction(InputAction.CallbackContext callbackContext) { var value = callbackContext.ReadValue<Vector2>(); Debug.Log($"position {value.x},{value.y}"); } // SampleControls.ISampleMapsActionsによって定義されたJumpActionのコールバック public void OnJumpAction(InputAction.CallbackContext callbackContext) { var value = callbackContext.ReadValue<float>(); if (value > 0) { Debug.Log("On Jump."); } } private void OnEnable() { // 忘れずEnabl() _sampleMapsActions.Enable(); } private void OnDestroy() { // こっちはDisposeではなくDisable() _sampleMapsActions.Disable(); } }自動生成されたクラスはファイル名.I[ActionMap名]のインターフェースを持っています。これを実装したクラスをSetCallbacksで指定することで入力を取得できるようになります。この時、ReadValueで取得する型とControl Typeが一致していないとエラーになってしまいます。
入力の検知タイミングを指定する
これまでJumpActionとしてキーボードやタップを入力にしましたが、現在の状態では押した時、離した時の両方で入力を受け取ってしまいます。これを回避するためにInteractionsを設定する必要があります。
上記のようにJumpActionのInteractionsにPressを、そしてTrigger Behavior
nにPress Onlyを設定しました。これによってJumpActionはButtonのPressのみを検知するようになりました。今回はJumpActionに対して設定しましたが、SpaceキーやTouchそれぞれ個別に設定することも可能です。*ここまで実装した方は気づいたかもしれませんがNew Input Systemは1フレームに複数回の入力を受け取ることができます。フレームに依存しないので処理落ち処理落ちした場合でも入力を受け取ることができますが、同じ処理を複数回しないために工夫する必要があります。
大八耐2019 in Tokyo
大八耐2019ではパズルのピースを使ったテトリスっぽいものを
作ってみました(完成しませんでした)。ピースを回転させて横一列に並べるというミニゲームで、New Input Systemを使ってキーボード入力からピースを左右に回転させました。
SpinInputView.csusing System; using papicra.Scripts.Presentation.Presenter; using UniRx; using UnityEngine; using UnityEngine.InputSystem; namespace papicra.Scripts.Presentation.View { public class SpinInputView : MonoBehaviour, PieceRollControls.IPieceRollActionActions, ISpinInputPort { private PieceRollControls.PieceRollActionActions _input = default; private readonly Subject<Unit> _spinClockwise = new Subject<Unit>(); private readonly Subject<Unit> _spinUnClockwise = new Subject<Unit>(); public IObservable<Unit> OnSpinClockwise() => _spinClockwise.Publish().RefCount().ThrottleFrame(1); public IObservable<Unit> OnUnSpinClockwise() => _spinUnClockwise.Publish().RefCount().ThrottleFrame(1); private void Awake() { _input = new PieceRollControls.PieceRollActionActions(new PieceRollControls()); _input.SetCallbacks(this); } public void OnClockwise(InputAction.CallbackContext context) { if (context.ReadValue<float>() > 0) { _spinClockwise.OnNext(Unit.Default); } } public void OnUnClockwise(InputAction.CallbackContext context) { if (context.ReadValue<float>() > 0) { _spinUnClockwise.OnNext(Unit.Default); } } private void OnEnable() { _input.Enable(); } private void OnDestroy() { _spinClockwise?.OnCompleted(); _spinClockwise?.Dispose(); _spinUnClockwise?.OnCompleted(); _spinUnClockwise?.Dispose(); _input.Disable(); } } }その時に使用したコードがこちらです。テラシュールブログ様 *2 を参考に結構さっくりと実装ができました(inputsettingsの項目に気がつかなくて入力が取れず沼ったのは秘密)。今回紹介したInputActionフィールドを定義してデリゲートにイベントを登録する方法、ActionMapをつかったコールバックを取る方法、ここでは紹介していないPlayerInputを使う方法など色々ありますが、個人的にはコールバックを取る方法が一番やりやすいなぁと感じました。インターフェースあると実装漏れないし楽でした。
大八耐の時間内でゲームを完成させることはできませんでしたがNew Input Systemを触るいい機会になったので個人的には大満足です。ゲーム自体はちまちま作っていこうかな。あとがき
大八耐2019 in TokyoにてNew Input Systemを軽く触ってみたのでまとめてみました。BindingsやProcessorsなどなど調べ切れていないことが多々あるのでまたいずれ......。
内容に誤りがありましたら、@sai_maple_にご連絡いただけると幸いです。参考
- 投稿日:2019-11-15T00:09:24+09:00
[C#/WindowsIoT] RaspberryPi3にI2Cサーマルカメラ(サーモグラフィ)をつなげて温度を画像化する
もくじ
→https://qiita.com/tera1707/items/4fda73d86eded283ec4fWinIoT on ラズパイでのI2C通信関連
- [C#/WinIoT/I2C] ラズパイ+WindowsIoTCore+C# で9軸センサ(MPU-9150)の値をとるやりたいこと・やったこと
電子工作で、ラズパイ3にWindows IoT Coreを入れて、サーマルカメラをつなげて、いわゆるサーモグラフィを作ってみたい。
さっと調べたところ、一番安くて(amazonで7000円くらい)手に入りやすそうな「MLX90640」を使おうと思うが、そのサーマルカメラがI2C接続のようなので、以前、9軸センサで練習したI2Cを生かせそう。
と思い立って作ったのが下記のようなもの。作るうえでいろいろ調べた事、やったことをメモしておく。
■動画
https://youtu.be/cPm-WGY2Y8c■コード一式
https://github.com/tera1707/ThermalCamera使った機材
- RaspberryPi3
- ラズパイ用小型モニタQuimat 3.5インチ
- サーマルカメラMLX90640
全体の流れ
- 回路作成
- サーマルカメラとのI2C通信実装
- VisualStudio2019ソリューションの設定
- I2C通信のデータ送受信の準備
- 初期化処理
- EEPROM読み出し
- サーマル生データを取得
- 生データから温度に変換
- 温度データからサーマル画像作成
回路作成
下図のような回路をつくる。
実際の配線
電源は、作成中、デバッグ中はコンセント~USBで。実際動かすときはモバイルバッテリーでの予定。サーマルカメラとのI2C通信実装
以前実験した9軸センサのI2Cをもとに、通信部分の実装を行う。
VisualStudio2019ソリューションの設定
9軸センサのI2Cの方で同じ作業をしているので、そちら参照。
I2C通信のデータ送受信の準備
データシートによると、このサーマルカメラは、2バイトを一単位としてデータのやり取りを行う。
9軸センサ(MPU-9150)の場合は、1バイト単位でデータをやり取りしていたので、そこが違う。
9軸センサの場合は、下記のようにしていた。9軸センサの送受信例.cs// 書き込み:1バイト目に書き込みたいレジスタアドレス、2バイト目に書く内容を載せて送信 WriteBuf = new byte[] { 0x6B, 0x00 }; I2CAccel.Write(WriteBuf); // 読み込み:1バイト目に読み込みたいレジスタアドレスを載せておくると、 // そのアドレスを先頭にした、指定バイト数分のデータが返ってくる // (バイト数の指定は、ReadBufの配列数で行う(下記の場合はnew byte[1]なので1バイト)) WriteBuf = new byte[] { 0x75 }; ReadBuf = new byte[1]; I2CAccel.WriteRead(WriteBuf, ReadBuf);今回のサーマルカメラでは、下記のようなメソッドを作って、ushortでアドレス指定、データ指定を行うようにした。
サーマルカメラの送受信例.cs// 書き込み private void WriteRegisterData(ushort writeAddr, ushort data) { // 書き込むデータ作成(最初の2バイトが書き込み先アドレス、その後の2バイトがそこに書き込むデータ) var writeByteData = new byte[] { (byte)(writeAddr / 0x100), (byte)(writeAddr % 0x100), // 書き込み先アドレス (byte)(data / 0x100), (byte)(data % 0x100), // 書き込みデータ }; // 書き込み実施 I2CThermalCamera.Write(writeByteData); } // 読み込み private ushort[] ReadRegisterData(ushort readAddr, int NumberOfData) { // 返すデータ(受信したbyteデータをushortに直したもの) ushort[] ret = new ushort[NumberOfData]; // アドレスを上位/下位に分解 var destAddr = new byte[] { (byte)(readAddr / 0x100), (byte)(readAddr % 0x100) }; // 受信用バッファを確保(このサーマルカメラのレジスタは1つで2バイト) var readBuf = new byte[NumberOfData * 2]; // 読み込み実施 I2CThermalCamera.WriteRead(destAddr, readBuf); // 読み込んだbyteデータをushortに直す for (int i = 0; i < NumberOfData; i++) { ret[i] = (ushort)(readBuf[2 * i] * 0x100 + readBuf[2 * i + 1]); } return ret; }初期化処理
下記のような流れで初期化を行う。
(というか、I2cDevice.FromIdAsync();
まではWinIotのI2C通信の準備)初期化処理時の通信.cspublic async Task InitThermalCamera() { // すべてのI2Cデバイスを取得するためのセレクタ文字列を取得 string aqs = I2cDevice.GetDeviceSelector(I2cDeviceName); DeviceInformationCollection dis = null; try { // セレクタ文字列を使ってI2Cコントローラデバイスを取得 dis = await DeviceInformation.FindAllAsync(aqs); if (dis.Count == 0) { Debug.WriteLine("I2Cコントローラデバイスが見つかりませんでした"); return; } } catch (Exception ex) { Console.WriteLine(ex.Message); throw; } // I2Cアドレスを指定して、デフォルトのI2C設定を作成する var settings = new I2cConnectionSettings(ThermalCameraI2CAddress); // バス速度を設定(FastMode:400 kHz)(指定しないと、標準設定(StandardMode:100kHz)になる) settings.BusSpeed = I2cBusSpeed.FastMode; // 取得したI2Cデバイスと作成した設定で、I2cDeviceのインスタンスを作成 I2CThermalCamera = await I2cDevice.FromIdAsync(dis[0].Id, settings); if (I2CThermalCamera == null) { Debug.WriteLine(string.Format("スレーブアドレス {0} の I2C コントローラー {1} はほかのアプリで使用されています。他のアプリで使用されていないか、確認してください。", settings.SlaveAddress, dis[0].Id)); return; } // サーマルカメラの設定 try { // コントロールレジスタを取得 var ctrreg = ReadRegisterData(0x800D, 1).FirstOrDefault(); // リフレッシュレートを変更する(現在のコントロールレジスタを読み出して、そいつに対して変更を実施) var ctrregset = (ushort)(ctrreg | 0x0300); WriteRegisterData(0x800D, ctrregset); } catch (Exception ex) { Debug.WriteLine("デバイスとの通信に失敗しました。: " + ex.Message); return; } // EEPROM読み出し this.MLX90640_DumpEE(); }ここで「リフレッシュレートを変更」しているが、今回は32Hzを使用した。(最速の64Hzにしなかったことに特に理由なし。)
試したところ、リフレッシュレートの設定によって、下のデータ取得のところで出てくる「isReady」フラグ(0x8000のbit3)がONになるまでの時間が変わってくるので注意。
※以降の内容について
ここから下は、ほぼサンプルプログラムを基に作成しています。
⇒https://github.com/sparkfun/SparkFun_MLX90640_Arduino_ExampleサンプルはC言語で描かれていますので、それをもとに、C#の処理を作成しました。
また、EEPROMの個別の中身や生データから温度に変換する計算など、データシートをじっくり読めばわかるのかもしれませんが、今回はあまり理解せずに(というか難しくてすぐに理解できなかった)サンプルを基に作らせて頂いてます。
EEPROM読み出し
上の初期化の中の一番下、下記の部分。
// EEPROM読み出し this.MLX90640_DumpEE();EEPROMに保存されているパラメータが、読み出したサーマルデータの生データを温度の値に変換する際に使われるので、EEPROMから各種パラメータを読みだして、所定のクラスに格納しておく。(ここでは
ParamsMLX90640
クラスにいれた。)データシートより、EEPROMはレジスタ0x2400番地から0x273F番地。
※EEPROMの読み出し項目は非常に多数あるので、コードは、githubのこちらを参照。
サーマル(生)データを取得 ~ 生データを温度データに変換
まずは、I2Cでサーマルの生データを取得する。
生データは温度データではないので、EEPROMから読み出したパラメータを使って温度に変換が必要。データ取得は、データシートの「Measurement Flow」の項の流れに沿った処理を行う。
生データ読み出し.cspublic double[] GetTemperatureData() { for (int i = 0; i < 2; i++) { byte isReady = 0; while (isReady == 0) { // ステータスレジスタ取得 isReady = (byte)(ReadRegisterData(0x8000, 1).FirstOrDefault() & 0x0008); } //// ステータスレジスタ書き込み(MeasurementStartをON) WriteRegisterData(0x8000, 0x0030); // アIRデータ取得 // 0x0400~0x06FF:IRデータ // 0x0700~0x070F:Ta_Vbe、CP.GAIN // 0x0720~Ta_PATA,CP,VddPix var frameDataS = ReadRegisterData(0x0400, FrameDataLength); // ステータスレジスタ読み出し(SubPage番号) //ReadRegisterData(0x8000, 1); StatusRegister = (ushort)(ReadRegisterData(0x8000, 1).FirstOrDefault() & 0x0001); // コントロールレジスタ読み出し ControlRegister = ReadRegisterData(0x800D, 1).FirstOrDefault(); ///////////////////////// // データ読み出し終了、データから温度への変換計算実施 ///////////////////////// var ta = this.MLX90640_GetTa(frameDataS, CamParameters); double tr = ta - 8; double[] ret = new double[FrameDataLength]; // 生データを温度データに変換 MLX90640_CalculateTo(frameDataS, CamParameters, 0.95, tr, ret); for (int l = 0; l < frameDataS.Length; l++) { if (ret[l] > 0.0) { TotalFrameData[l] = ret[l]; } } } return TotalFrameData; }生データ取得
メソッド
MLX90640_CalculateTo()
より上は、生データ取得処理。生データを取る際、for分で2回回している。
データを採る際、「Subpage」という番号も一緒にとるのだが、これで全体のどの部分のデータが取れているかがわかる。
具体的には、データは下記の図のようなイメージで2回とって1画面分のデータとなる。
ステータスレジスタの設定により2パターンの取れ方があるが、今回は上のパターン(横一行分のデータが1行飛ばしで取れるパターン)を使った。つまり、subPageが0のデータは奇数行目のデータ、subPageが1のデータが1のデータは偶数行目のデータとなっている。
温度データに変換
そのsubPageの値と、取れてきた1行飛ばしのデータは、生データを温度データに変換するメソッド
MLX90640_CalculateTo()
の中で使っている。(中の計算ロジックは難しいのであまり見ず。)変換した温度データを、最終的に配列に格納。(ここでは
TotalFrameData[]
)この配列の中身が、サーマルカメラに映った画面(32*24)の1ピクセルごとの温度の値となる。
温度データからサーマル画像作成
温度データを画面に表示するための値にさらに変換する。
画面に温度を表す点を打つ方法は、こちらの以前の記事を参照。温度の値を32*24のピクセルの描画データに変換する.csprivate async Task<BitmapImage> DoubleToRaindowColor(double[] totalFrameData)//temp:温度の値 { int width = 32; int height = 24; byte[] data = new byte[width * height * 4]; for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { // 指定の温度下限~上限の値を、0.0~1.0の値に変換する var v = TemperatureTo0to1Double(totalFrameData[i + j * width]); // 0.0~1.0の値を、虹色を表すバイト列に変換する var c = ColorScaleBCGYR(v); data[4 * (i + j * width)] = c.Item4; // Blue data[4 * (i + j * width) + 1] = c.Item3; // Green data[4 * (i + j * width) + 2] = c.Item2; // Red data[4 * (i + j * width) + 3] = c.Item1; // alpha } } // サーマル画像を作成 WriteableBitmap bitmap = new WriteableBitmap(width, height); InMemoryRandomAccessStream inMRAS = new InMemoryRandomAccessStream(); BitmapEncoder encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.BmpEncoderId, inMRAS); encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Ignore, (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight, 96.0, 96.0, data); await encoder.FlushAsync(); BitmapImage bitmapImage = new BitmapImage(); bitmapImage.SetSource(inMRAS); return bitmapImage; }上記の中の、指定の温度下限~上限の値を、0.0~1.0の値に変換するメソッド
ColorScaleBCGYR()
は、こちらのサイトを参考にさせていただいています。ありがとうございます。完成
これで作成したbitmapを画面に表示したら、サーモグラフィの完成。
意外となめらかに動いているが、サーマルカメラの解像度が32*24と結構低いので、モザイク状に見える。最初、解像度、一桁間違えてないか?320*240の間違いでは?と思ったが、32*24であってた。
サーマルカメラは、割と解像度が低いものらしい。(高いものには、もっと高解像度のものもあるが、手が出ない)コード一式
ここまでで挙げてきた初期化やらデータ取得やらのメソッド以外にも、画面だったりC++のサンプルをもとに作ったEEPROM読み込み機能やらが多数ある。下記に一式置いているので参照ください。
https://github.com/tera1707/ThermalCamera
■191116 追記
コンセントから離れても動かせるよう、携帯の乾電池式充電器に接続。
あとサーマルカメラもブランブランならないよう、手抜きだが形だけユニバーサル基盤にくっつけて一旦のできあがり。
電気電子やらメカのプロの方からしたら怒られそうだが、個人的には手作り感満載で、落としたら一撃死しそうな感じがたまらなく良い。↓↓↓敬礼する、メガネをかけた嫁
※真ん中の四角の中の、3*3マスの平均温度を画面下に表示しているのだが、
体温が異常に低く見えてる。放射率?とかパラメータ調整必要なのかも。参考
公式ページ
https://shop.pimoroni.com/products/mlx90640-thermal-camera-breakoutデータシート
https://cdn.sparkfun.com/assets/7/b/f/2/d/MLX90640-Datasheet-Melexis.pdf値の大きさをサーモグラフィのような色に変換する
https://qiita.com/krsak/items/94fad1d3fffa997cb651サンプルプログラム(arduino)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/tree/master/Firmware通信手順(どういうデータが取れるか、とか通信のお作法の参考になる)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/blob/master/Firmware/Example1_BasicReadings/MLX90640_API.cpp通信ドライバ(データの送り方の参考になる)
https://github.com/sparkfun/SparkFun_MLX90640_Arduino_Example/blob/master/Firmware/Example1_BasicReadings/MLX90640_I2C_Driver.cpp
- 投稿日:2019-11-15T00:05:33+09:00
【AWS-SES】C#でメール一括送付するツールを作成してみた
概要
以前にGASでQRCodeを一括でメール送付する仕組みを作成しました。これはこれで便利なのですが1日に送信できるメールが限られているのが弱点でした。そこで今回は社内でAWSをお試しで使っているので、SESを使ってメールを一括送付する仕組みを作成しました。
■GoogleSpread(GAS)でQRCodeを一括で送付する
構成
主にC#のフォームで作成しました。C#でのSESの利用についてはSDKを使う方法とSMTPを使用した方法がサンプルとしてあるので、今回はSMTPを使用する方法を採用しました。C#のフォームアプリでSMTPインターフェイスからEメールを送信する仕組みを作成します。フォームアプリについてはSMTPの設定値や送信アドレスなどを設定できるようにし、SESの設定ができていればアプリから一括でメール送信が可能です。
■AWS SDK for .NETを使用してEメールを送信する
まずは Amazon SES の設定
送信するだけなら結構シンプルに設定できます。今回は送付用アドレスにGmailのメールを使用しました。ちなみに外部へのメール送信についてはサンドボックスの外への移動する必要があります。
いきなりイミワカメで困りますが、何もしていないと不正使用や悪用を防止し、送信者としての評判を保つため、新しい Amazon SES アカウントには一定の制限が適用されている状態になっています。それらの制限の解除をサポートセンターにお願いして解除してもらいます。SESの設定については以下に記載します。
AWS-SESの設定方法
このツールを使用するにあたり、AWS側の設定が必要になります。要はAWS-SESで送信用のメールアドレスやSMTPの設定などを行います。以降の手順の参考としてスクリーンショットを取得しました。ただし画像の一部にモザイクなどの加工する部分がありますのでご了承ください。
【AWS-SES】マネジメントコンソールを開く
まずはAWSマネジメントコンソールを開きSESと入力してSimple Email Serviceの画面を開きます。
【AWS-SES】送信用のアドレスを設定する
まずは送信用のメールアドレスを設定します。こちらに設定するメールアドレスは基本的には何でも大丈夫なはずです。メールアドレスを設定すると設定したメールアドレスにAWSから認証のメールが届きます。AWSからメールを承認(Verify)することで送信用のメールアドレスとして設定できます。
■ 設定したメールアドレスはC#フォームアプリの「送信アドレス」に設定します
【AWS-SES】サンドボックスの外への移動
SESの画面からSending Statisticsの画面を開きます。
SESはデフォルトだとサンドボックスと呼ばれる試用環境?の状態で制限が掛かっています。以下のスクリーンショットのように「Request a Sending Limit increase」と表示されている場合は、まだサンドボックスの状態で外部へのメール送信ができません。表示されている「Request a Sending Limit increase」をクリックしてサポートセンターへのページに遷移しサンドボックスの制限解除を依頼します。
ここで個人的に詰まったのがオレゴン以外のリージョンだといくら待っても制限が解除されませんでした。オレゴンのリージョンに選択しなおし制限解除の依頼をするとすぐに対応してくれました。まだ、アジアのリージョンもそこまで用意されていないのでオレゴンを選択した方がイイかもしれません。
なお、サンドボックスの制限解除はリージョンごとに設定する必要があるため、他のリージョンに切り替えているとRequest a Sending Limit increase」のボタンが表示されます。
【AWS-SES】SMTPの設定
最後にSMTPの設定を行います。SMTPの「Server Name」などは同じ設定だと思うので、サンプルのコードと同じであることを確認してください。確認後「Create My SMTP Credentials」をクリックするとIAMの画面に切り替わります。
IAMの画面に切り替わりIAMのユーザーを作成します。
SMTPでメールを送付する場合はIAM Userにメール送付用の専用のユーザーを作成します。作成されると以下のような「認証情報のダウンロード」ボタンからSMTP USERとSMTP PASSWORDが記載されたCSVをダウンロードできます。
もしこの画面を閉じてしまってもIAMにはユーザーが登録されており、そのユーザーの認証情報から発行し直すことができます。
最悪、もう一度「Create My SMTP Credentials」で作り直しても大丈夫です。作り直した場合はIAMで必要のないIAMのユーザーは整理しましょう。
■ ダウンロードしたCSVの情報を元にC#フォームアプリの「SMTP_USERNAME」と「SMTP_PASSWORD」を設定します
IAMの画面では先ほど作成したSMTPのユーザーが表示されているはずです。実は認証情報のアクセスキーがSMTPのユーザーに該当します。ここではパスワードを確認することができません。ただしアクセスキーを発行し直しSMTPのユーザーとパスワードを取得しなおすことはできそうです。
C#によるフォームアプリの作成
上記のAWS-SESが完了したらC#フォームで送る準備をします。ツールを使う場合は上記のSMTPユーザーなどを設定すれば一括でメールを送付することができます。
なおAWSの設定情報のどの項目を設定すべきかはAWS手順の画像の下に■の記号で補足している部分を参考にしていただけると助かります。
それでは実装した機能を記載していこうと思います。【C#フォーム】プロパティによる設定値の保存
入力した設定値についてはプロパティに保存され次回起動したときにも設定値を維持するようにします。
保存する値がセキュアなモノである場合は、暗号化して保存するようにしています。これは以前にプロパティではなくリソースを使っていたときにEXEのバイナリから読み取れてしまうことがあったので、念のために暗号化の処理を挟むことにしました。
【C#フォーム】Excelから送付リストの取り込み
DataGridViewに表示する内容についてはExcelファイルから取り込めるようにしました。外部のデータベースに接続して利用することも可能ですが、データベース自体が決まっていないので、とりあえずClosedXMLを使用してExcelファイルを読み込むようにしました。
ClosedXMLを使用すればExcelがインストールされてなくても処理できるので便利ですね。Office Interopでも読み込めますが後処理とかちゃんとしないとイケないんで少し面倒です。
取り込んだ後のDataGridViewでは列項目で絞り込めるようにしています。
【C#フォーム】テキストの置換
3つの項目だけですが自分で設定したテキストの内容と取り込んだExcelファイルの列項目に対応して置換するようにしました。文章にするとわかりづらいため設定画面も日本語でわかりやすくしてみたつもりです。
【C#フォーム】AWSの設定
AWSで設定したSMTPなどを設定できます。AWS側の設定については後段で説明します。
【C#フォーム】メールの設定
メールの件名や本文の設定ができます。上記のテキストの置換に合わせて本文を置換することができます。
【C#フォーム】ログ出力
送信後に本当に送信されたかどうか不安だと思うのでログを出力するようにしました。
送信結果
捨てメアド【メルアドぽいぽい】というサービスでメールアドレスを作成して試しに送ってみました。
テキストの置換も上手く機能しています。
リンク先を確認するとちゃんと社員番号のQRコードが生成されています。
総評
AWS-SESとC#のフォームアプリで一括で送付する仕組みができました。別にメールで送付するならGASでもイイじゃんとなりそうですが、GASはGASで送付エラーのときが若干怖かったりします。
作成したC#のフォームアプリでは送付後にログも出力するので正しく送付されたかどうか確認できます。もし送付できていなかった場合でもログから拾うことができ、もう一度再送してあげることもできます。
今回、作成したC#フォームアプリは以下の場所に格納しましたのでAWS-SESの設定をした上でご試用いただければ幸いです。