- 投稿日:2019-12-09T23:56:24+09:00
Grasshopperコンポーネントをダブルクリックした際にWindowsFormを呼ぶ方法
はじめに
この記事と同様にGalápagosのようなコンポーネントを作ろうとした際に、GrasshopperのコンポーネントからWindowsFormで作ったものを表示させたかったので、やり方を示します。
ちなみに、RhinoはMac版もあるので、どちらも対応したものを作成したい場合は、WindowsFormではなくRhinoが対応しているクロスプラットフォームインターフェースのEtoを使ってください。
なおこの記事は、RhinocerosForumのこの質問の内容を参考に作っています。完成品のイメージ
今日のもくもく会の成果
— hiron/6cores (@hiron_rgkr) June 29, 2019
GrasshopperのNumberSliderを、コンポーネントをダブルクリックして呼び出せるWindowsフォームのsliderから操作できました~ pic.twitter.com/tDEtHmxPA1作り方
作成にはGH_ComponentAttributes クラスを作り、コンポーネントをダブルクリックした際にウインドウが出てきて欲しいので、GH_AttributesのRespondToMouseDoubleClick メソッドを使います。これを使うことで、ダブルクリックした際にDisplayForm(別途作成しているWinForm)を呼び出しています。
public class Attributes_Custom : GH_ComponentAttributes { public Attributes_Custom(IGH_Component ChangeNumSlider) : base(ChangeNumSlider) { } public override GH_ObjectResponse RespondToMouseDoubleClick(GH_Canvas sender, GH_CanvasMouseEvent e) { (Owner as ChangeNumSlider)?.DisplayForm(); return GH_ObjectResponse.Handled; } }上で作成したアトリビュートをコンポーネントに適用したいので、GH_Component クラス の CreateAttributes メソッドをオーバーライドします。
public override void CreateAttributes() { m_attributes = new Attributes_Custom(this); }WindowsFormの使い方などはほかの記事の方が詳しいと思うので、ここでは割愛します。
ビルトした動くものを作りたい場合
ここで説明した内容はホタルアルゴリズムによる最適化コンポーネントを目指して作り出したホタルコンポーネントの一部です(モチベーションなくなってエタッてしまいました…)。ホタルコンポーネントのリンク先のgithubのDevelopeブランチをクローンしてビルトしてもらえば動くghコンポーネントが作成されると思います。直接の部分はChangeNumSlider.cs の部分で、他はホタルアルゴリズムの実装を行っている箇所です。
Grasshopper内のC#スクリプトコンポーネントではWindowsFormを作るのが面倒なので、Visual Studio Community 2019 を使っています。
ちなみに
カスタムアトリビュートについては、公式の説明もあるので詳しく知りたい方はそちらも参照どうぞ。
- 投稿日:2019-12-09T20:59:28+09:00
C#でJavaのenumを再現する
この記事は StudioZ Tech Advent Calendar 2019 の11日目の記事です。
はじめに
5年ぐらい前からUnityを使うようになり、JavaからC#に転向しました。
似たところの多い言語なのでほとんど違和感なく使うことができたのですが、唯一困ったのがenumの仕様。
そこでJavaのenumをC#で再現できないか考えてみました。Javaのenumの特性と実装方法
- 列挙子は列挙型のインスタンスを持つ
- 普通にクラスを使えば満たせます
- 外部からインスタンスの生成ができない
- コンストラクタをprivateにすることで対応可能です
- 列挙型.列挙子でアクセスできる
- 列挙子のインスタンスをstaticフィールドで管理すればアクセス可能になります
- 同名の列挙子は定義できない
- 同名のフィールドは定義できないので、staticフィールドで管理する時点で満たされます
ここまでの条件をコードにするとこんな感じになります
public class EnumSample { public static EnumSample Enum1 = new EnumSample(); public static EnumSample Enum2 = new EnumSample(); public static EnumSample Enum3 = new EnumSample(); public static EnumSample Enum4 = new EnumSample(); // 外部からのインスタンス生成不可 private EnumSample() {} }
- 列挙子は固有の値を持つことができる
- コンストラクタで値を受け取ってフィールドに持つようにします
- ただし静的に重複のチェックをすることはできません(実行時のチェックは可能)
public class EnumSample2 { public static EnumSample2 Enum1 = new EnumSample2( 1 ); public static EnumSample2 Enum2 = new EnumSample2( 2 ); public static EnumSample2 Enum3 = new EnumSample2( 3 ); public static EnumSample2 Enum4 = new EnumSample2( 4 ); public int Num; // 外部からのインスタンス生成不可 private EnumSample2( int num ) { Num = num; // 重複チェックを入れるとしたらこの辺でやる // Dictionaryに詰めていってContainsKeyでチェックするのが楽そう } }
- インスタンスごとにメソッドを実装できる
- 列挙体クラスを抽象クラスにし、列挙子ごとにクラスを継承してメソッドを実装します
- 列挙子ごとに実装必須にしたい場合はabstract、デフォルト動作を定義したい場合はvirtualにします
- 列挙子間で共通化したい処理は親クラス側、差別化したい処理は子クラス側に実装することで重複処理をなくすことができます
- 列挙体はインタフェースを実装できる
- 列挙型にインタフェースを実装する宣言をするだけです
// ドロップアイテムのインタフェース public interface IDropItem { string Name { get; } int Price { get; } string Description { get; } } // ドロップアイテムとして扱える武器の列挙型 public abstract class Weapon : IDropItem { public static Weapon Sword = new WeaponSword(); private class WeaponSword : Weapon { // コンストラクタがpublicでもクラス自体はprivateなので外部からはインスタンス生成不可 public WeaponSword() {} public override string Name { get { return "剣"; } } public override int Price { get { return 100; } } public override string Description { get { return "いい感じの説明"; } } public override void Attack() { base.Slash(); } } public static Weapon Spear = new WeaponSpear(); private class WeaponSpear : Weapon { public WeaponSpear() {} public override string Name { get { return "槍"; } } public override int Price { get { return 50; } } public override string Description { get { return "そんな感じの説明"; } } public override void Attack() { base.Pierce(); } } public static Weapon Club = new WeaponClub(); private class WeaponClub : Weapon { public WeaponClub() {} public override string Name { get { return "棍棒"; } } public override int Price { get { return 10; } } public override string Description { get { return "とてもアレな説明"; } } // Attack()の実装は省略可能 } protected Weapon() {} // 名前、価格、説明は必ず列挙子で定義する public abstract string Name { get; } public abstract int Price { get; } public abstract string Description { get; } // 攻撃処理の実装は省略可能 public virtual void Attack() { // オーバーライドしない場合はデフォルトとして殴る処理 Strike(); } // protectedで宣言したプロパティやメソッドは列挙子側からアクセスできる protected void Slash() { // 切る武器の処理 } protected void Pierce() { // 刺す武器の処理 } protected void Strike() { // 殴る武器の処理 } }C#ではできないこと
- 列挙子をswitchの条件に使う
- caseには定数しか使えないので、実態がクラスである列挙子を使うことはできません
- とはいえ列挙子側に処理を実装できるので、switchは基本的に使う必要はないと思います
おわりに
ぱっと見面倒そうに見えますが、Java式enumを使うことによって、ポリモーフィズムを実現しやすくなります
機会があったらぜひ試してみてください参考
- 投稿日:2019-12-09T18:38:19+09:00
遺伝的アルゴリズム的なものを作ってみたお話
はじめに
最近AI/アルゴリズムの講演を聞きました。そこで、遺伝的アルゴリズムに興味を持ったので「作ってみたい!」と思い、作成したので、何かの参考になればと思いご紹介します。
実際の挙動
![]()
これらは毎フレームランダムに動かしているだけですが、とても人工知能ぽくみえます。
実践
ソースコードはこちら
※3D空間上でXZ平面を走っているのを上から撮影していますcar.csfloat rotVelo = Random.Range(-rotateSpeed, rotateSpeed); transform.eulerAngles = transform.eulerAngles + new Vector3(0, rotVelo, 0); float posVelo = Random.Range(0, moveSpeed); transform.position += transform.forward * posVelo;このObjectを
20体用意して、
100mごとに障害物を置きます。
移動データの記録
それから動いた記録を保存するデータを作成します。
data.cs[Serializable] public struct MoveData { public float pos; public float rot; public MoveData(float _pos, float _rot) { pos = _pos; rot = _rot; } }これのリストを作ってランダムに動いた結果を保存しておきます。
具体的な保存の値はそれぞれ、上記のrotVelo
とposVelo
です。全員が壁にぶつかったら集計をします。
その際にどのObjectが一番進んだかを確かめて次の世代を生成します。
遺伝子の受け継ぎ
marry.csfor (int i = 0; i < 一,二番優秀移動データリスト.Count; i++) { bool l = Random.Range(0,2) == 0; 次世代.移動データリスト.Add(l ? 一番優秀.移動データリスト[i] : 二番優秀.移動データリスト[i]); }こうすることで二人の親の遺伝子(移動データ)を持った子供が生まれます。
parent1+parent2->child1
parent1+parent3->child2
parent2+parent3->child3
parent2+parent4->child4
parent3+parent4->child5
parent3+parent5->child6と子供を作っていきます
child7は一番優秀のデータをそのまま受け継ぎます。(最高記録が下がらないように)
chaild8,9,10はランダムに選んだ二人の遺伝子を組み合わせます。ですがそこで突然変異を起こします。
突然変異
一定の確率で値をおかしくしてほしいので、
totuzennhenni.csif(突然変異する) { for (int i = 0; i < 突然変異.移動データリスト.Count; i++) { 突然変異.移動データリスト.Add(l ? 一番優秀.移動データリスト[i] : 二番優秀.移動データリスト[i]); foreach(var 移動データ in 突然変異.移動データリスト) { if(Random.Range(0 , 突然変異でどれくらい変わるか) != 0) 移動データ.pos *= Random.Range(0,2); 移動データ.rot *= Random.Range(-2,2); } } }こんな感じで突然変異させます。
これらの出来上がったデータをObjectに記録させて、
第2世代が始まります。第2世代
第2世代は遺伝で受け継いだデータのとおりに動きます。
データがなくなったらランダムに動き始めます。これらを繰り返して行くとこんな感じになりました。
終わりに
講演とかYoutubeの動画を見ながらこんな感じで作るのかなーって作りきりました。
いいお勉強になって楽しかった。
- 投稿日:2019-12-09T17:45:34+09:00
ASP.NET CRUD
まだまだ改善したい点はあるが、一通りコードを書けたのでここに記す。
DTOはFruitクラスを使う。ここは特にいうことはなし。
DTO
Fruit.csusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace asp_smp.Models { public class Fruit { public override string ToString() { return "Id = " + Id + ", Name = " + Name + ", Number = " + Number + ", Price = " + Price; } public int Id { get; set; } public string Name { get; set; } public int Number { get; set; } public int Price { get; set; } } }Controller
Controllerに関して。
- [Route("xxxx")]でルーティングを追加。
- [HttpPost]でPostメソッドを受け付ける。
- [Route("xxx/{id}")]でidの部分にidが入る。
- ApplicationScope?SessionScope?のViewBag。あまり使わない方がいいのか。
- Redirect時に値を渡すことができるDictionary型のTempData
- Redirectを行うことができるRedirectメソッド。
- ShowメソッドではView()の中にFruit型の変数を入れているが、cshtmlで@model Fruitと宣言することによってcshtml側でView()の()の中に入れたものを使用できる。
TopController.csusing asp_smp.Models; using asp_smp.Service; using Microsoft.AspNetCore.Mvc; // For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 namespace asp_smp.Controllers { [Route("")] public class TopController : Controller { private FruitSqlService service = new FruitSqlService(); [Route("")] public IActionResult Index() { ViewBag.list = service.All(); return View(); } [Route("/add")] public IActionResult Add() { return View(); } [HttpPost] [Route("/add")] public IActionResult AddPost(int Id, string Name, int Number, int Price) { var fruit = new Fruit(); fruit.Id = Id; fruit.Name = Name; fruit.Number = Number; fruit.Price = Price; service.INSERT(fruit); TempData.Add("result", "追加に成功しました。"); return Redirect("/"); } [HttpPost] [Route("/delete")] public IActionResult DeletePost(int Id) { service.DELETE(Id); TempData.Add("result", "削除に成功しました。"); return Redirect("/"); } [Route("/show/{id}")] public IActionResult Show(int Id) { var fruit = service.FindById(Id); return View(fruit); } [HttpPost] [Route("/update")] public IActionResult Update(int Id, string Name, int Number, int Price) { var fruit = new Fruit(); fruit.Id = Id; fruit.Name = Name; fruit.Number = Number; fruit.Price = Price; service.UPDATE(fruit); TempData.Add("result", "更新に成功しました。"); return Redirect("/"); } } }Repository
SQLiteを使用。とりあえず全件、1件、追加、更新、削除を実装。
FruitSqlService.csusing asp_smp.Models; using System; using System.Collections.Generic; using System.Data.SQLite; using System.Linq; using System.Threading.Tasks; namespace asp_smp.Service { public class FruitSqlService { /// <summary> /// テーブル作成 /// </summary> public void Create() { var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" }; using (var Connection = new SQLiteConnection(ConnectionStr.ToString())) { Connection.Open(); using (var Command = new SQLiteCommand(Connection)) { Command.CommandText = "CREATE TABLE IF NOT EXISTS fruit(id INTEGER, name TEXT, number INTEGER, price INTEGER)"; Command.ExecuteNonQuery(); } } } /// <summary> /// 削除 /// </summary> public void DELETE(int Id) { var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" }; using (var Connection = new SQLiteConnection(ConnectionStr.ToString())) { Connection.Open(); using (var Command = new SQLiteCommand(Connection)) { Command.CommandText = "DELETE FROM fruit WHERE id = :id"; Command.Parameters.Add(new SQLiteParameter("id", Id)); Command.ExecuteNonQuery(); } } } /// <summary> /// データ追加 /// </summary> /// <param name="fruit"></param> public void INSERT(Fruit fruit) { var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" }; using (var Connection = new SQLiteConnection(ConnectionStr.ToString())) { Connection.Open(); using (var Command = new SQLiteCommand(Connection)) { Command.CommandText = "INSERT INTO fruit(id, name, number, price)VALUES(:id, :name, :number, :price)"; Command.Parameters.Add(new SQLiteParameter("id", fruit.Id)); Command.Parameters.Add(new SQLiteParameter("name", fruit.Name)); Command.Parameters.Add(new SQLiteParameter("number", fruit.Number)); Command.Parameters.Add(new SQLiteParameter("price", fruit.Price)); Command.ExecuteNonQuery(); } } } public void UPDATE(Fruit fruit) { var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" }; using(var Connection = new SQLiteConnection(ConnectionStr.ToString())) { Connection.Open(); using (var Command = new SQLiteCommand(Connection)) { Command.CommandText = "UPDATE fruit SET name = :name, number = :number, price = :price WHERE id = :id"; Command.Parameters.Add(new SQLiteParameter("id", fruit.Id)); Command.Parameters.Add(new SQLiteParameter("name", fruit.Name)); Command.Parameters.Add(new SQLiteParameter("number", fruit.Number)); Command.Parameters.Add(new SQLiteParameter("price", fruit.Price)); Command.ExecuteNonQuery(); } } } public Fruit FindById(int Id) { var fruit = new Fruit(); var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" }; using(var Connection = new SQLiteConnection(ConnectionStr.ToString())) { Connection.Open(); using(var Command = new SQLiteCommand(Connection)) { Command.CommandText = "SELECT id, name, number, price FROM fruit WHERE id = :id"; Command.Parameters.Add(new SQLiteParameter("id", Id)); using (var Reader = Command.ExecuteReader()) { if (Reader.Read()) { fruit.Id = Reader.GetInt32(0); fruit.Name = Reader.GetString(1); fruit.Number = Reader.GetInt32(2); fruit.Price = Reader.GetInt32(3); } else { return null; } } } } return fruit; } public List<Fruit> All() { var list = new List<Fruit>(); var ConnectionStr = new SQLiteConnectionStringBuilder() { DataSource = "test.db" }; using (var Connection = new SQLiteConnection(ConnectionStr.ToString())) { Connection.Open(); using (var Command = new SQLiteCommand(Connection)) { Command.CommandText = "SELECT id, name, number, price FROM fruit"; using (var Reader = Command.ExecuteReader()) { while (Reader.Read()) { var fruit = new Fruit(); fruit.Id = Reader.GetInt32(0); fruit.Name = Reader.GetString(1); fruit.Number = Reader.GetInt32(2); fruit.Price = Reader.GetInt32(3); list.Add(fruit); } } } } return list; } } }View
基本的にコントローラーでreturn View()で返された時、/Folder名/〇〇.cshtmlとなるが、これは/class名/メソッド名+.cshtmlに対応する。
例えば、TopControllerのIndexメソッドであれば/Top/Index.cshtmlがブラウザ側に返される。
bootstrap使用。Index.cshtml@* For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 *@ @{ } <h1>This is Top Page</h1> @{ if(TempData.TryGetValue("result", out var val)) { <h3 class="text-success">@val</h3> } } <a href="/add" class="btn btn-primary">新規追加</a> <table class="table table-bordered"> <thead> <tr> <th>Id</th><th>Name</th><th>Number</th><th>Price</th> </tr> </thead> <tbody> @{ foreach(var fruit in ViewBag.list) { <tr> <td>@fruit.Id</td> <td><a href="/show/@fruit.Id">@fruit.Name</a></td> <td>@fruit.Number</td> <td>@fruit.Price</td> <td> <form action="/delete" method="post"> <input type="hidden" value="@fruit.Id" name="Id"/> <input type="submit" value="削除" class="btn btn-danger"/> </form> </td> </tr> } } </tbody> </table>Add.cshtml@* For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 *@ @{ } <h1>This is Add Page</h1> <div class="container"> <form action="/add" method="post"> <table class="table table-bordered"> <tbody> <tr> <th>Id</th> <td><input type="text" name="Id" /></td> </tr> <tr> <th>Name</th> <td><input type="text" name="Name"></td> </tr> <tr> <th>Number</th> <td><input type="text" name="Number"></td> </tr> <tr> <th>Price</th> <td><input type="text" name="Price"></td> </tr> <tr> <td colspan="2"><input type="submit" value="追加" class="btn btn-info" /></td> </tr> </tbody> </table> </form> </div>Show.cshtml@* For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 *@ @{ } @model Fruit <h1>This is Update Page</h1> <form action="/update" method="post"> <table class="table table-bordered"> <tbody> <tr> <th>Id</th> <td><input type="text" value="@Model.Id" name="Id"/></td> </tr> <tr> <th>Name</th> <td><input type="text" value="@Model.Name" name="Name"/></td> </tr> <tr> <th>Number</th> <td><input type="text" value="@Model.Number" name="Number"/></td> </tr> <tr> <th>Price</th> <td><input type="text" value="@Model.Price" name="Price"/></td> </tr> <tr> <td colspan="2"><input type="submit" value="更新する" /></td> </tr> </tbody> </table> </form>
- 投稿日:2019-12-09T14:48:01+09:00
長いTextの終わりに "..." をつけ、かつ改行させない方法
Textの幅が枠内に収まりきらないとき、私たちのチームでは、以下の記事の
(4) 余談
の部分を参考に、溢れない箇所でカットして、...
を末尾に付与しています。[Unity] Textコンポーネントでテキストを切り捨てずに全文表示させる方法
以下に一部引用させていただきます。
public static void SetTextWithEllipsis(this Text textComponent, string value) { // create generator with value and current Rect var generator = new TextGenerator(); var rectTransform = textComponent.GetComponent<RectTransform>(); var settings = textComponent.GetGenerationSettings(rectTransform.rect.size); generator.Populate(value, settings); // trncate visible value and add ellipsis var characterCountVisible = generator.characterCountVisible; var updatedText = value; if (value.Length > characterCountVisible) { updatedText = value.Substring(0, characterCountVisible - 3); updatedText += "..."; } // update text textComponent.text = updatedText; }しかし、あるとき問題が発覚しました。長い文字列に半角スペースが含まれている場合、
そのスペースで改行されてしまい、↑のコードはその1行目の終わりで可視文字列の終わりと判定されて、右にまだスペースがあるのに早々に...
が打たれて残りが全てカットされてしまうのです。(↓の中身は、「あああああ\nあああああ\nあああああ」のようになっていますが、1行目の最後の文字がリミットと判断されるようです)かといって、TextのHorizontal OverflowをOverflowにすると、今度は全て表示可能とみなされ、
...
を付与されず全部そのまま出てしまいます…。(ただし、この問題は英語のみでは起きないようです。)
そこで、私たちがとった対策は、半角スペースをnbsp(non-breaking space)に置き換えることです。
このnbsp、htmlなどでは
としてご存知の方もいるかと思いますが、文字コードとしても存在します(\u00a0
)。
改行を誘発する半角スペースをnbspに置き換えてしまうことで、改行を抑制するわけです。
上記のコードを書き換えるなら、以下のようになります。public static void SetTextWithEllipsis(this Text textComponent, string value, bool spaceToNbsp) { // create generator with value and current Rect var generator = new TextGenerator(); var rectTransform = textComponent.GetComponent<RectTransform>(); var settings = textComponent.GetGenerationSettings(rectTransform.rect.size); if (spaceToNbsp) { value = value.Replace(" ", "\u00a0"); } generator.Populate(value, settings); // trncate visible value and add ellipsis var characterCountVisible = generator.characterCountVisible; var updatedText = value; if (value.Length > characterCountVisible) { updatedText = value.Substring(0, characterCountVisible - 3); updatedText += "..."; } // update text textComponent.text = updatedText; }やってることとしては単純で、
Populate()
の前に文字列をReplace()
しているだけです。
これで、半角スペースが含まれていても単語で区切らずに...
を付与することができました。英語では問題が起きないところを見るとUnityの処理の不具合のような気もしますが、ともかくこれで想定どおりの挙動になりました。
- 投稿日:2019-12-09T13:03:25+09:00
CsvHelperの簡単な日付変換
CsvHelperの簡単な日付変換で読み込む
CsvHelper 12.2.1
読み込むCSVの日付列が"20190101"のような形式だった時、属性で変換を指定して読み込む方法。
書き込みもたぶん同じ。試してない。最初はClassMapを書いて読み込みをしていたが、日付を形式変換するためにClassMapを書くのは面倒だなと思って調べてたら、AttributesにFormatがあるなと気づいた話。
CsvHelper.Configuration.Attributes Namespace
100列とかあったとき、数列のためにClassMapを書くのはつかれるのでちょっとは楽になった。
ただ、何かに影響するかは知らない。最初に書いていたソース
//File,Encoding,HasHeaderRecordはどっかで設定 public IEnumerable<CSV_Format> GetRecords() { try { using (var reader = new StreamReader(File, Encoding.GetEncoding(EncodingName))) using (var csv = new CsvReader(reader)) { csv.Configuration.HasHeaderRecord = HasHeaderRecord; csv.Configuration.RegisterClassMap<CSV_Format_Mapper>(); return csv.GetRecords<CSV_Format>(); } } catch (Exception ex) { //..例外処理.. } }class CSV_Format_Mapper : ClassMap<CSV_Format> { public CSV_Format_Mapper() { Map(x => x.col1).Index(1); Map(x => x.col2).Index(2); Map(x => x.col4).Index(4).TypeConverterOption.Format("yyyyMMdd"); Map(x => x.col6).Index(6); } } class CSV_Format { public string col1 { get; set; } public int col2 { get; set; } public DateTime col4 { get; set; } public decimal col6 { get; set; } }Attributesを使ったソース
CSV_Format_Mapper
を書かなくていい!//File,Encoding,HasHeaderRecordはどっかで設定 public IEnumerable<T> GetRecords() { try { using (var reader = new StreamReader(File, Encoding.GetEncoding(EncodingName))) using (var csv = new CsvReader(reader)) { csv.Configuration.HasHeaderRecord = HasHeaderRecord; //csv.Configuration.RegisterClassMap<CSV_Format_Mapper>(); //いらなくなった return csv.GetRecords<T>(); } } catch (Exception ex) { //..例外処理.. } }class CSV_Format { [Index(1)] public string col1 { get; set; } [Index(2)] public int col2 { get; set; } [Index(3), Format("yyyyMMdd")] public DateTime col4 { get; set; } [Index(4)] public decimal col6 { get; set; } }参考
- 投稿日:2019-12-09T10:19:07+09:00
音声解析リップシンクミドルウェア「CRI ADX LipSync」を使ってみた
本記事は、サムザップ Advent Calendar 2019 #2 の12/7の記事です。
CRI ADX LipSyncとは
2019年11月末に深層学習を用いた音声解析によるリップシンクをするためのミドルウェア『CRI ADX LipSync』がCRI・ミドルウェア社からリリースされました。2019年12月頭現在、対応しているゲームエンジンはUnityのみですが、いずれ他のゲームエンジンにも対応していく予定とのことです。
今回リリースされた音声解析機能には、再生した音声を入力としてリアルタイムに口の形状情報を出力するものと、予め音声から口の形状情報をテキストデータとして出力するものとがあります。
前者は、CRIWARE SDKに含まれており同社が出しているサウンド演出を手掛ける『CRI ADX2』というミドルウェアと組み合わせて手軽に実装することができます。こちらは、CRIWAREユーザーであればタイトルリリース後も無料で使用することができるようです。
後者は、専用のコマンドツール(Windows版のみ)を使うことになり、出力されたデータはADX2のツール上で編集することも可能です。こちらは、CRIWAREユーザーであってもタイトルリリース後は料金が発生するようです。本記事では、Unityで『CRI ADX2』と『CRI ADX LipSync』を組み合わせて音声データから口の形状情報をリアルタイムに得る方法を紹介したいと思います。
フォルマントベースの音声解析
音声からリアルタイムに口形状情報を得るために用いられる特徴量は、主に次の2つがあります。
・音声の振幅値(音量)
・音声のフォルマント(周波数のピーク)振幅値ベースのリップシンクは、実装が割と楽な反面、得られるデータが1次元であるため単純な口の開閉運動しか表現できず不自然さを感じやすくなります。
一方で、フォルマントベースのものでは、2次元データ(F1, F2周波数)から母音(a, i, u, e, o)を推定することができるため、口の上下の開閉だけでなく左右の伸縮にもマッピングすることで、より自然な表現が可能となります。『CRI ADX LipSync』を使うと、フォルマントベースのリップシンクを手軽に実現することができます。
唇音の判定もできる
自然なリップシンクを実現する上では、母音が推定できただけでは実はまだ足りないようです。口を閉じるところでちゃんと閉じさせることができないケースがあります。
「ま」や「ば」などの唇音と呼ばれる音です。唇音は、口を一度閉じてから発音します。
例えば、母音の推定だけで「ママ」という単語の発音をリップシンクさせると、「aa」と口が開きっぱなしになってしまうのです。この唇音の判定まで自前で実装するとなると、結構大変そうです。
『CRI ADX LipSync』では、なんと唇音であるかどうかも判定してくれるようです。実際に使ってみる
リアルタイムに再生される音声を解析する方式と、予め解析して口パクデータを作っておくオフライン方式とが用意されているが、
今回は、リアルタイムでの解析を試してみました。使用したツールのバージョンは、以下の通りです。
Unity: 2019.2.5f1
CRIWARE SDK for Unity: 3.00.00リアルタイム解析による口形状情報取得までの流れ
1.アナライザーの初期化
ADX2のAtomライブラリを用いて再生した音声から口形状情報を取得するための解析器であるCriLipsAtomAnalyzerを初期化します。// インスタンス生成 var atomAnalyzer = new CriLipsAtomAnalyzer(); // 無音と判定する最大音量(0以下のdB値)の設定 atomAnalyzer.SetSilenceThreshold(-40); // 解析対象の音声データのサンプリング周波数を設定(16000Hz以上) atomAnalyzer.SetSamplingRate(48000);2.アナライザーのプレイヤーへのアタッチ
AtomライブラリのプレイヤーであるCriAtomExPlayerにCriLipsAtomAnalyzerのインスタンスをアタッチします。var player = new CriAtomExPlayer(); atomAnalyzer.AttachToAtomExPlayer(player);3.アナライザーから口形状情報の取得
CriLipsAtomAnalyzerからCriLipMouthクラスの口形状情報を格納する構造体CriLipsMouth.InfoおよびCriLipsMouth.MorphTargetBlendAmountAsJapaneseを取得します。// 縦横形状の組み合わせ情報の取得 var info = new CriLipsMouth.Info(); atomAnalyzer.GetInfo(out info); // 日本語5母音の組み合わせ情報の取得 var blendAmount = new CriLipsMouth.MorphTargetBlendAmountAsJapanese(); atomAnalyzer.GetMorphTargetBlendAmountAsJapanese(out blendAmount);取得できる口形状情報の種類
CriLipsAtomAnalyzerからは、次の2種類の口形状情報を取得することができます。
・縦横形状の組み合わせ情報
構造体: CriLipsMouth.Info
構造体のフィールド:
フィールド名 取得できる値 lipWidth 口の幅 (0.0f~1.0f) lipHeight 口の高さ (0.0f~1.0f) tonguePosition 舌の位置 (0.0f~1.0f) isLipWidthReleased 口の幅が閉じ状態に遷移中かどうか isLipHeightReleased 口の高さが閉じ状態に遷移中かどうか isLipToungueReleased 舌の位置が閉じ状態に遷移中かどうか ・日本語5母音の組み合わせ情報
構造体: CriLipsMouth.MorphTargetBlendAmountAsJapanese
構造体のフィールド:
フィールド名 取得できる値 a 「あ」のブレンド量 (0.0f~1.0f) i 「い」のブレンド量 (0.0f~1.0f) u 「う」のブレンド量 (0.0f~1.0f) e 「え」のブレンド量 (0.0f~1.0f) o 「お」のブレンド量 (0.0f~1.0f) ※ 同時刻においては、5母音の内2母音のブレンド量が0より大きい値として取得できます。
結果
『CRI ADX LipSync』のサンプルプロジェクトについてくる音声を解析してどのようなデータが得られるか調べてみました。サンプル音声は、女性の声で「私が死んだら、代わりはいるのでしょうか。」と話しているものを使いました。
まずは、分かりやすいので日本語5母音の組み合わせ情報の時間推移を示します。
図を見てみると、「あ」「い」「う」については比較的精度高く推定できていそうですが、この音声では「え」と「お」はほとんど検出されなかったようで、子音や前後の音との組み合わせによっては推定が難しいケースもあるようです。そもそも、フォルマント的にも各母音のフォルマントの分布は完全に分離されているという訳ではなく、一部が重複した状態で分布していることも、推定を誤ってしまう原因として考えられます。次に、縦横形状の組み合わせ情報の時間推移を見てみます。
口の高さの推移の図を見てみると、口を縦に大きく開く「a」や「o」の音で極大となっており、全体的に良く追従できているのが分かります。さらに、図の赤丸で示した「だ」と「ら」の間では、一瞬口を閉じている状態のデータを返しており、唇音ではないが間で口を閉じる発音にも対応できていることが伺えます。最後に、CriLipsAtomAnalyzerクラスのGetRmsメソッドから取得した音圧の強さを表すRMS(Root Means Square)値の時間推移です。
こちらのデータだけを使ってリップシンクをさせるのは難しそうですが、口形状情報と組み合わせて使うと音量による口の開き具合の影響度を大きくしたい場合などに役立つかもしれないです。CriLipsAtomAnalyzerクラスのGetVolumeメソッドでも音量(dB値)が取得できるようなので、今後そちらも検証してみたいと思います。まとめ
自然なリップシンクを自前で実装しようとすると、実装も難しく計算負荷も高くなってしまいがちですが、『CRI ADX2』による再生機構と『CRI ADX LipSync』が提供する解析機構を組み合わせて使うことで手軽に品質の高いリップシンクを実現することができそうだということが分かりました。
実機でのプロファイリングで問題がなければ、プロジェクトでの導入も検討していきたいです。参考
・キャラクターをより魅力的に!ゲーム向けリップシンクミドルウェア CRIWARE公式ページより
・音声解析リップシンクミドルウェアCRI ADX LipSync明日は @kojima_akira さんの記事です。
- 投稿日:2019-12-09T03:21:28+09:00
Unity セーブ・ロードの仕組み
ゲームをする側でいる時、前回プレイした時に獲得したアイテムがまだ手元に残っているのを当たり前のように思っていましたが、いざ自分自身がゲームを作る側になると、どういう仕組みでそれが成立しているのか不思議なります。今回はアイテムなどのオブジェクトのセーブ/ロードに関しての話です。(今回話すのはローカルセーブについてです。)
実現したいこと
- セーブ / ロード機能の実装
現状
- セーブ/ロードがどういう仕組みか分からない
- セーブ/ロード機能の実装方法が分からない
仕組みの大まかな説明
セーブするとはどのような仕組みによって成立しているのでしょうか?
ざっくり言うとセーブ対象となるオブジェクトの状態(クラスのフィールドと思って大丈夫かと)を保存することです。オブジェクトの状態を保存をする際にシリアル化と言う重要なプロセスがあります。
シリアル化についての説明は、Microsoft Documentによると
シリアル化は、オブジェクトを格納するか、メモリ、データベース、またはファイルに転送するためにバイト ストリームに変換するプロセスです。 その主な目的は、必要なときに再作成できるように、オブジェクトの状態を保存しておくことです。 逆のプロセスは、逆シリアル化と呼ばれます。
引用したドキュメントにはわかりやすい図もあったので載せておきます
Unityではセーブ機能を実装するための様々な方法があります。
- PlayerPrefs
- ScriptableObjects
- json / xml
- custom binary file
それぞれ長所、短所があり一概にどれが一番良いと言うことはできませんし、自分にあった方法を見つけていただければと思います。
以下の表にざっくりとまとめました。
実装方法 概要 長所 短所 備考 PlayerPrefs Player Preferences(略してPlayerPrefsかな)を保存、アクセスできる UnityEngineが提供しているため手軽に扱える。 使えるデータ型の制限(Int, Float, Stringのみ)がある。
単純な記録(スイッチのオンオフを覚えておくとか)には向いているが、複雑な処理には向いていないUnity Document ScriptableObjects クラスのインスタンスとは独立し、データを大量に格納できるコンテナ アセットとして扱える。
メモリ消費量が小さい。主に開発側のためのものであり、プレイヤーのプレイ中におけるセーブのための機能ではない(つまりプレイヤーの進捗とか保存できない)。 Unity Documentation external files(json/xml) よく使われるマークアップ言語 簡単に修正できる 簡単に修正できる custom binary file バイナリ形式でシリアライズ、デシリアライズを行う 自由度が高い。
バイナリ形式であるため安全性が高い。コードを書く量が多い。 サンプル
githubにサンプルを上げていますので、興味があればご自由に見てください。
本サンプルでは、Custom Binary Fileでセーブ機能を実装しています。実行環境
- macOS Catalina ver10.15.1
- Unity 2019.2.10f1
概要
シーン1: 新しくゲームを始めるか、登録済みのプレイヤーをロードするかの選択。(ロードを選択するとシーン3へ行きます)
シーン3: ゲームシーン。セーブデータが正しく保存されているか確かめるためにセーブデータをロードしてプレイヤーに反映します。
全部を紹介すると長くなるのでセーブ、ロードに関するところをピックアップして以下に紹介します。
シーン2
シーン1はシーン遷移のみなので飛ばします。
まずセーブするプレイヤーのデータ
SavePlayerData.cs[System.Serializable] public class SavePlayerData { public string name; public int age; public string color; }セーブするクラスにはSystem.Serializableと言う属性をつけることでシリアル化を可能にします。
Monobehaviourを継承しているとシリアル化できないの注意しましょう。次にユーザーの入力をもとにセーブデータを作りシリアル化する箇所です。
SaveManager.csusing System.Collections.Generic; using System.IO; using System.Runtime.Serialization.Formatters.Binary; using UnityEngine; using UnityEngine.UI; public class SaveManager : MonoBehaviour { // ----- 一部抜粋 ----- // public void OnSaveNewPlayer() { // セーブデータ作成 SavePlayerData player = CreateSavePlayerData(); // バイナリ形式でシリアル化 BinaryFormatter bf = new BinaryFormatter(); // 指定したパスにファイルを作成 FileStream file = File.Create(SaveFilePath); // Closeが確実に呼ばれるように例外処理を用いる try { // 指定したオブジェクトを上で作成したストリームにシリアル化する bf.Serialize(file, player); } finally { // ファイル操作には明示的な破棄が必要です。Closeを忘れないように。 if (file != null) file.Close(); } } // 入力された情報をもとにセーブデータを作成 private SavePlayerData CreateSavePlayerData() { SavePlayerData player = new SavePlayerData(); player.name = nameInput.text; player.age = int.Parse(ageDropdown.options[ageDropdown.value].text); player.color = colorDropdown.options[colorDropdown.value].text; return player; } // ----- 一部抜粋 ----- // }シーン3
セーブしたファイルをロードする部分はこちらです。
LoacManager.cspublic class LoadManager : MonoBehaviour { // ----- 一部抜粋 ----- // private void LoadPlayer() { if (File.Exists(SaveFilePath)) { // バイナリ形式でデシリアライズ BinaryFormatter bf = new BinaryFormatter(); // 指定したパスのファイルストリームを開く FileStream file = File.Open(SaveFilePath, FileMode.Open); try { // 指定したファイルストリームをオブジェクトにデシリアライズ。 SavePlayerData player = (SavePlayerData)bf.Deserialize(file); // 読み込んだデータを反映。 var playerObject = Instantiate(playerPrefab) as GameObject; playerObject.GetComponent<PlayerController>().Init(player.name, player.age, player.color); } finally { // ファイル操作には明示的な破棄が必要です。Closeを忘れないように。 if (file != null) file.Close(); } } else { Debug.Log("no load file"); } } // ----- 一部抜粋 ----- // }それほど難しくないですね!
シリアライズなど使えるようになると、ランキングやら簡単なログイン機能の実装やら色々できることの幅が広がりそうですね!
Next Step
- 他のPlayerPrefs/ScriptableObjects/JSON/XMLとの違いを試してみる
- クラウドセーブ
- セーブファイルと暗号化について
- オートセーブ機能について
参考文献
Unite 2016 - Best Practices in Persisting Player Data on mobile
Unite Europe 2017 - How Unity's Serialization system works
Saving Game Data in Unity
How to Save and Load a Game in Unity
シリアル化
Script Serialization
オブジェクトのシリアル化コメント
以下の観点でコメントいただけると嬉しく思います!
- 間違い、不備
- 「こういう状況では、このセーブ/ロード方法が良いよ!」
- 投稿日:2019-12-09T01:23:43+09:00
【Unity】「機動戦士ガンダムVS.」シリーズっぽいカメラの動きを作ってみる
はじめに
本記事ではUnityで「機動戦士ガンダムVS.」シリーズにあるような、操作キャラクターを視界に入れつつ、ターゲットに向き続けるTPSカメラを作ってみます。
近い動きで「ディシディア ファイナルファンタジー」や「Fate/Grand Order Arcade」のようなカメラにも応用できるんじゃないかと思います。完成イメージ
デモプロジェクト
GitHubにアップロードしています。
unity-tps-lock-on-camera位置と向きのロジック
カメラの動きを作るときは先に位置を考え、その後に向きを考えるのが進めやすいと思います。
位置
まずカメラの位置ですが、下図のようにターゲットと操作キャラを結んだ線上、操作キャラ後方の少し上辺りにいる感じがしますね。
なので、操作キャラ→ターゲットのベクトル$\vec{V_{tgt}}$を基準に、後ろ距離と高さを示すベクトル$\vec{V_{ofsLocal}}$分ずらせば欲しい位置が得られそうです。ただそれは$\vec{V_{tgt}}$を前方とするローカル空間での話なので、ワールド座標に直す必要があります。
あるベクトルを座標系(の一部)とみなしてそれを基準に位置を得る方法ですが、クォータニオンを掛けることでベクトルを回転することができるという性質を利用します。
$\vec{V_{tgt}}$を前方とするクォータニオン$q_{tgt}$を得ることができれば、ワールド座標でずらすべき位置ベクトル$\vec{V_{ofs}}$は、\vec{V_{ofs}} = q_{tgt} \times \vec{V_{ofsLocal}}となります。
幸いUnityにはQuaternion.LookRotationという便利なAPIが用意されていますので、$q_{tgt}$を得るのは難しくありません。
カメラの位置を算出するまでのコードイメージはこんな感じになります。// ターゲットへのベクトル Vector3 vTgt = target.position - player.position; // ターゲットへのベクトルを前方とするクォータニオン // 第二引数はワールド空間的な上(Vector3.up)でいいので省略 Quaternion qTgt = Quaternion.LookRotation(vTgt); // ずらすべき位置ベクトル // ずらしたい量をここでは後方5、高さ2とした場合 Vector3 vOfs = qTgt * new Vector3(0f, 2f, -5f); // 最終的なカメラ位置(ワールド座標) Vector3 cameraPosition = player.position + vOfs;向き
次に向きですが、こちらはシンプルです。
常にターゲットに向くだけなので、カメラの位置が決まればQuaternion.LookRotationにカメラ→ターゲットのベクトルを前方ベクトルとして渡してあげれば得られます。// ターゲットへの向き Quaternion cameraRotation = Quaternion.LookRotation(target.position - cameraPosition);プロトタイプ完成
変数名を分かりやすくして、MonoBehaviourにのっけたコードに直すとこんな感じです。
ここまでで操作キャラの後方を維持しつつ、ターゲットを見続けるカメラができました。TpsLockOnCameraPrototype.csusing UnityEngine; public class TpsLockOnCameraPrototype : MonoBehaviour { /// <summary> /// 取りつくキャラクター /// </summary> [SerializeField] private Transform _attachTarget = null; /// <summary> /// 取りつくキャラクターからのカメラオフセット位置 /// </summary> [SerializeField] private Vector3 _attachOffset = new Vector3(0f, 2f, -5f); /// <summary> /// 注視ターゲット /// </summary> [SerializeField] private Transform _lookTarget = null; /// <summary> /// 現在の注視点 /// </summary> private Vector3 _lookTargetPosition = Vector3.zero; private void LateUpdate() { _lookTargetPosition = _lookTarget.position; // ターゲットへのベクトル Vector3 targetVector = _lookTargetPosition - _attachTarget.position; // ターゲットへのベクトルを前方とするクォータニオン Quaternion targetRotation = targetVector != Vector3.zero ? Quaternion.LookRotation(targetVector) : transform.rotation; // 位置と向き Vector3 position = _attachTarget.position + targetRotation * _attachOffset; Quaternion rotation = Quaternion.LookRotation(_lookTargetPosition - position); transform.SetPositionAndRotation(position, rotation); } }ターゲット切り替えのロジック
続いてターゲット切り替えの動きを考えます。
最終的にはターゲットとして指定しているtransformを変えればいいわけですが、単純に変えるとカメラが位置と向きをがっつりワープすることになるので、プレイヤーは切り替わり前後で混乱してしまいますよね。滑らかに繋ぎたいところです。ターゲット切り替え
処理の流れを考えます。
まず、元々見ていたターゲットは、切り替わり動作中も含めて無視としたいので、切り替え開始の瞬間の位置$P_{old}$だけ覚えておけばよさそうです。新しく見るターゲットは切り替わり動作中も含めてその位置$P_{new}$を追従し続けたいです。
なので$P_{old}$は固定位置、$P_{new}$はTransformのpositionとして、$P_{old}$から$P_{new}$へ補間しつつ一定時間かけて移動すればよさそうです。
(見る位置を滑らかに変えることで、カメラの位置と向きもスムーズに変化するよねという考え方です。)位置の補間にはVector3.Lerpのメソッドが使えます。(好みでイージングしてもいいかもしれません。)
よって、先程のコードにターゲット変更の処理を組み込んだ最終版はこのようになります。TpsLockOnCamera.csusing UnityEngine; public class TpsLockOnCamera : MonoBehaviour { /// <summary> /// 取りつくキャラクター /// </summary> [SerializeField] private Transform _attachTarget = null; /// <summary> /// 取りつくキャラクターからのカメラオフセット位置 /// </summary> [SerializeField] private Vector3 _attachOffset = new Vector3(0f, 2f, -5f); /// <summary> /// 注視ターゲット /// </summary> [SerializeField] private Transform _lookTarget = null; /// <summary> /// ターゲットがいないときの注視点 /// </summary> [SerializeField] private Vector3 _defaultLookPosition = Vector3.zero; /// <summary> /// ロック切り替え時間 /// </summary> [SerializeField] private float _changeDuration = 0.1f; /// <summary> /// ロック切り替えタイマー /// </summary> private float _timer = 0f; /// <summary> /// 現在の注視点 /// </summary> private Vector3 _lookTargetPosition = Vector3.zero; /// <summary> /// ロックを移すときの最後の注視点 /// </summary> private Vector3 _latestTargetPosition = Vector3.zero; /// <summary> /// ターゲット切り替え /// </summary> /// <param name="target"></param> public void ChangeTarget(Transform target) { _latestTargetPosition = _lookTargetPosition; _lookTarget = target; _timer = 0f; } private void LateUpdate() { var targetPosition = _lookTarget != null ? _lookTarget.position : _defaultLookPosition; // 現在の注視点を更新 if (_timer < _changeDuration) { _timer += Time.deltaTime; _lookTargetPosition = Vector3.Lerp(_latestTargetPosition, targetPosition, _timer / _changeDuration); } else { _lookTargetPosition = targetPosition; } // ターゲットへのベクトル Vector3 targetVector = _lookTargetPosition - _attachTarget.position; // ターゲットへのベクトルを前方とするクォータニオン Quaternion targetRotation = targetVector != Vector3.zero ? Quaternion.LookRotation(targetVector) : transform.rotation; // 位置と向き Vector3 position = _attachTarget.position + targetRotation * _attachOffset; Quaternion rotation = Quaternion.LookRotation(_lookTargetPosition - position); transform.SetPositionAndRotation(position, rotation); } }おわりに
このタイプのカメラ挙動はカメラ操作がシンプルになるというメリットがありますね。
拙い説明であることに加え、細かくは再現しきれておらず申し訳ないですが、少しでも何かの参考になれば幸いです。
また、説明の間違いやソースコードの不具合などあるかもしれません。その場合は是非ご指摘いただけますと幸いです。
- 投稿日:2019-12-09T00:41:30+09:00
.NETCoreのWebAPIをDockerで実行する
はじめに
昔使ってた.NETCoreを使って、WebAPIでDockerを動かしてみたいと思いました。ちなみに私、.NET環境触るの久しぶりだったり、Docker初心者なので、間違ってる〜とか今はこんなのが主流だよ〜とかあったらご指摘いただけると嬉しいです
また、Docker良く分からないけど、なんか動かしてみたい〜っていうMSフレンズも実際にやってもらえたら嬉しいです。
TL;DR
- C#やWindowsServerを使っている方、.NETCoreやDockerを検討している方向けです。
- この記事を読むと、.NETCoreのWebAPIをDockerコンテナで実行できるようになります。
準備
- 環境
- macOS Mojave バージョン 10.14.6
※Windows でも適宜読み替えればできると思います。.NETCoreをインストール
.NETCore SDKからダウンロードします。
バージョンを確認すると...
dotnet --version --version3.1.100
正常に.NETCore SDKのインストールが完了しました。
WebAPIのサンプルプロジェクトを作成
続いて、今回試すAPIのプロジェクトを作成します。
# プロジェクト作成 (my_docker_api は任意のプロジェクト名) dotnet new webapi -o my_docker_api動作確認
# プロジェクトに移動してlocalhost起動 cd my_docker_api/ dotnet runサンプルとして用意されている
WeatherForecast
に
https://localhost:5001/WeatherForecast
でアクセスすると...
Command + C
でlocalhostシャットダウンDockerイメージをビルド
準備
※Dockerをインストール(この記事では割愛)
docker --version Docker version 19.03.5, build 633a0ea
VSCode拡張機能インストール(任意)
Dockerfile作成
Command + Shift + P
でコマンドパレットを開き
Docker: Add Docker Files to Workspace...
を選択
ASP.NET Core
>Linux
を選択すると自動でDockerfile
が作成されます。
Dockerイメージをビルド
my_docker_api_image
は任意のイメージ名でOKです。docker build -t my_docker_api_image -f Dockerfile .
数分かかりますが、最終的に
Successfully
が出ていればOKです。
Dockerイメージの確認
docker images
コンテナーを実行する
docker run --rm -p 5000:80 my_docker_api_image my_docker_api
--rm
: このコンテナー実行後に自動的にコンテナを削除してくれます。不要なコンテナが残り続けるとストレージを圧迫します。
-p 5000:80
: ローカル端末の5000番ポートをDockerコンテナー内の80番ポートに接続します。実行後に
http://localhost:5000/WeatherForecast
にアクセスすると、Docker内のAPIを実行できることを確認できました。先程、
dotnet run
でローカル実行したときと同様のことがDockerコンテナで実行できることが分かります。感想
これだけではDockerの旨味は感じられにくいが、Dockerで開発することができれば、そのままAWS ECS/Fargateを使ってデプロイできるみたい。(でも今は、.NETCore2系まで対応で3系はまだかな?) マイクロサービスやサーバーレスアプリケーションを作成するなら使いたい技術だと感じました。
〜後日談〜
Dockerには様々な使い方があります。例えば、開発環境をメンバー全員で揃えたいとか、サービス運用を楽にしたいとか。つまり、解決したい課題を明確にして自分がどのようにDockerを使いたいか把握することが大事なんだと気づきました。参考にした記事
- Docker で ASP.NET Core ウェブアプリケーションを動かす @Nossa さん
- チュートリアル: NET Core アプリのコンテナー化
- チュートリアル: ASP.NET Core で Web API を作成する
私たちのチームで働きませんか?
エイチームは、インターネットを使った多様な技術を駆使し、幅広いビジネスの領域に挑戦し続ける名古屋の総合IT企業です。
そのグループ会社である株式会社エイチームブライズでは、一緒に働く仲間を募集しています!上記求人をご覧いただき、少しでも興味を持っていただけた方は、まずはチャットでざっくばらんに話をしましょう。
技術的な話だけでなく、私たちが大切にしていることや、お任せしたいお仕事についてなどを詳しくお伝えいたします!Qiita Jobsよりメッセージお待ちしております!
- 投稿日:2019-12-09T00:30:17+09:00
WPF オセロ
出張で無事死んでいました...
記事いっぱい書いていかないと...オセロって使うもの自体は少ないんだけど、満たす要件だったりロジックが複雑だったりするのでわりと頭の体操によくやったりします。(書くことないのでこれを書きます。)
オセロの説明は言うまでもないと思うので省略
オセロの要件
- 8×8の緑に黒線のボードで行う。
- ターンがあり、●と〇が交互に打っていく。
- すでに石があるところに石はおけず、またひっくり返せない所には石はおけない。
- 自分の色で相手の色を挟んだらひっくり返す。
- 最後に多い方の勝ち。
- 途中で打てなくなった場合Passをする。
- 途中で石がなくなった場合その時点で負け
実装
まずは、xaml側から。
こちらは8×8の緑に黒線のボードを用意してあげて、Passボタンと現在のターン表示と一応黒と白の現在の数を用意してあげましょう。MainWindow.xaml<Window x:Class="osero_wpf.MainWindow" 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:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:osero_wpf" mc:Ignorable="d" Title="MainWindow" Height="500" Width="600"> <Window.Resources> <Style TargetType="Button"> <Setter Property="Width" Value="50"></Setter> <Setter Property="Height" Value="50"></Setter> <Setter Property="Background" Value="Green"></Setter> <Setter Property="BorderBrush" Value="Black"></Setter> </Style> </Window.Resources> <Grid> <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" Name="Board"> <StackPanel Orientation="Horizontal" Tag="1"> <Button Content="" Click="Button_Click" Tag="1"></Button> <Button Content="" Click="Button_Click" Tag="2"></Button> <Button Content="" Click="Button_Click" Tag="3"></Button> <Button Content="" Click="Button_Click" Tag="4"></Button> <Button Content="" Click="Button_Click" Tag="5"></Button> <Button Content="" Click="Button_Click" Tag="6"></Button> <Button Content="" Click="Button_Click" Tag="7"></Button> <Button Content="" Click="Button_Click" Tag="8"></Button> </StackPanel> <StackPanel Orientation="Horizontal" Tag="2"> <Button Content="" Click="Button_Click" Tag="1"></Button> <Button Content="" Click="Button_Click" Tag="2"></Button> <Button Content="" Click="Button_Click" Tag="3"></Button> <Button Content="" Click="Button_Click" Tag="4"></Button> <Button Content="" Click="Button_Click" Tag="5"></Button> <Button Content="" Click="Button_Click" Tag="6"></Button> <Button Content="" Click="Button_Click" Tag="7"></Button> <Button Content="" Click="Button_Click" Tag="8"></Button> </StackPanel> <StackPanel Orientation="Horizontal" Tag="3"> <Button Content="" Click="Button_Click" Tag="1"></Button> <Button Content="" Click="Button_Click" Tag="2"></Button> <Button Content="" Click="Button_Click" Tag="3"></Button> <Button Content="" Click="Button_Click" Tag="4"></Button> <Button Content="" Click="Button_Click" Tag="5"></Button> <Button Content="" Click="Button_Click" Tag="6"></Button> <Button Content="" Click="Button_Click" Tag="7"></Button> <Button Content="" Click="Button_Click" Tag="8"></Button> </StackPanel> <StackPanel Orientation="Horizontal" Tag="4"> <Button Content="" Click="Button_Click" Tag="1"></Button> <Button Content="" Click="Button_Click" Tag="2"></Button> <Button Content="" Click="Button_Click" Tag="3"></Button> <Button Content="" Click="Button_Click" Tag="4"></Button> <Button Content="" Click="Button_Click" Tag="5"></Button> <Button Content="" Click="Button_Click" Tag="6"></Button> <Button Content="" Click="Button_Click" Tag="7"></Button> <Button Content="" Click="Button_Click" Tag="8"></Button> </StackPanel> <StackPanel Orientation="Horizontal" Tag="5"> <Button Content="" Click="Button_Click" Tag="1"></Button> <Button Content="" Click="Button_Click" Tag="2"></Button> <Button Content="" Click="Button_Click" Tag="3"></Button> <Button Content="" Click="Button_Click" Tag="4"></Button> <Button Content="" Click="Button_Click" Tag="5"></Button> <Button Content="" Click="Button_Click" Tag="6"></Button> <Button Content="" Click="Button_Click" Tag="7"></Button> <Button Content="" Click="Button_Click" Tag="8"></Button> </StackPanel> <StackPanel Orientation="Horizontal" Tag="6"> <Button Content="" Click="Button_Click" Tag="1"></Button> <Button Content="" Click="Button_Click" Tag="2"></Button> <Button Content="" Click="Button_Click" Tag="3"></Button> <Button Content="" Click="Button_Click" Tag="4"></Button> <Button Content="" Click="Button_Click" Tag="5"></Button> <Button Content="" Click="Button_Click" Tag="6"></Button> <Button Content="" Click="Button_Click" Tag="7"></Button> <Button Content="" Click="Button_Click" Tag="8"></Button> </StackPanel> <StackPanel Orientation="Horizontal" Tag="7"> <Button Content="" Click="Button_Click" Tag="1"></Button> <Button Content="" Click="Button_Click" Tag="2"></Button> <Button Content="" Click="Button_Click" Tag="3"></Button> <Button Content="" Click="Button_Click" Tag="4"></Button> <Button Content="" Click="Button_Click" Tag="5"></Button> <Button Content="" Click="Button_Click" Tag="6"></Button> <Button Content="" Click="Button_Click" Tag="7"></Button> <Button Content="" Click="Button_Click" Tag="8"></Button> </StackPanel> <StackPanel Orientation="Horizontal" Tag="8"> <Button Content="" Click="Button_Click" Tag="1"></Button> <Button Content="" Click="Button_Click" Tag="2"></Button> <Button Content="" Click="Button_Click" Tag="3"></Button> <Button Content="" Click="Button_Click" Tag="4"></Button> <Button Content="" Click="Button_Click" Tag="5"></Button> <Button Content="" Click="Button_Click" Tag="6"></Button> <Button Content="" Click="Button_Click" Tag="7"></Button> <Button Content="" Click="Button_Click" Tag="8"></Button> </StackPanel> </StackPanel> <StackPanel Orientation="Vertical" VerticalAlignment="Top" HorizontalAlignment="Left"> <StackPanel Orientation="Horizontal"> <TextBlock Text="黒:"></TextBlock> <TextBlock Text="" Name="ViewBlackCount"></TextBlock> </StackPanel> <StackPanel Orientation="Horizontal"> <TextBlock Text="白:"></TextBlock> <TextBlock Text="" Name="ViewWhiteCount"></TextBlock> </StackPanel> </StackPanel> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Left" Orientation="Vertical"> <StackPanel Orientation="Horizontal"> <TextBlock Text="" Name="TurnText"></TextBlock> <TextBlock Text="のターンです。"></TextBlock> </StackPanel> <Button Content="Pass" Click="Button_Click_1" Background="AliceBlue"></Button> </StackPanel> </Grid> </Window>コード側でやってあげたほうがよかったかもしれないですが、面倒なのでxaml側でばーっとやっちゃいました。
コード側MainWindow.xaml.cspublic partial class MainWindow : Window { private bool IsBlackTurn = true; private int[,] BoardInfo = new int[8, 8]; public MainWindow() { InitializeComponent(); Init(); } }ターンの状態と、ボードの情報をMainWindowクラスで保持。ボードの情報に関してはint型で保持し、
1は黒、0はなし、-1は白とする。InitではBoardInfoにすべて0を入れてから、4,4に黒、4,5に白、5,4に白、5,5に黒を入れてやる。
MainWindow.xaml.csprivate void Init() { for (var i = 0; i < 8; i++) { for (var j = 0; j < 8; j++) { BoardInfo[i, j] = 0; } } SetBoardInfo(4, 4, 1); SetBoardInfo(4, 5, -1); SetBoardInfo(5, 4, -1); SetBoardInfo(5, 5, 1); ReflectBoardInfoToXaml(); }SetBoardInfoは4,4,1であれば[4,4]に黒を入れてやる、の意。
わざわざメソッド化をしてあげる理由は配列が0から始まるが、見かけ上は1からはじまるため。
(ここは別にTag付けを0から始めればよかったか)
ReflectBoardInfoToXamlメソッドはBoardInfoの情報をxaml側に反映させてあげるメソッド。MainWindow.xaml.csprivate void ReflectBoardInfoToXaml() { int row = 0; int col = 0; int BlackCount = 0; int WhiteCount = 0; int NoCount = 0; foreach (var Children in Board.Children) { var Panel = Children as StackPanel; foreach (var But in Panel.Children) { var Butt = But as Button; Butt.Content = ConvertIntInfoToStringInfo(BoardInfo[row, col]); if (BoardInfo[row, col] == 1) BlackCount++; else if (BoardInfo[row, col] == -1) WhiteCount++; else NoCount++; col++; } col = 0; row++; } if (BlackCount == 0) { MessageBox.Show("白の勝ち"); Init(); } else if(WhiteCount == 0) { MessageBox.Show("黒の勝ち"); Init(); } else if(NoCount == 0) { if(WhiteCount < BlackCount) { MessageBox.Show("黒の勝ち"); Init(); } else if(WhiteCount > BlackCount) { MessageBox.Show("白の勝ち"); Init(); } else { MessageBox.Show("引き分け"); Init(); } } ViewBlackCount.Text = BlackCount.ToString(); ViewWhiteCount.Text = WhiteCount.ToString(); TurnText.Text = IsBlackTurn ? "黒" : "白"; }基本的に最初のforeachの終わりまでが反映作業。
foreachのところのChildrenに関しては子要素を取得。
なので、全体のStackPanelの子要素を取得して、8枚のStackPanelを取り出す。
その後、各StackPanelからButtonを取り出してそこのContentに情報を入れていく。
その後の勝ち負けの処理を入れるのはforeachですべての情報を回してるここでやると便利なため。オセロのボタンをクリックしたときの処理。
MainWindow.xaml.csprivate void Button_Click(object sender, RoutedEventArgs e) { var button = sender as Button; var stackPanel = button.Parent as StackPanel; int Col = int.Parse(button.Tag.ToString()); int Row = int.Parse(stackPanel.Tag.ToString()); if(BoardInfo[Row - 1, Col - 1] != 0) { MessageBox.Show("そこにはすでに置かれています。"); return; } var IsTurnChange = CheckValidBoardInfo(Row, Col); if (!IsTurnChange) { MessageBox.Show("そこには置けません。"); return; } SetBoardInfo(Row, Col, ConvertTurnToIntInfo(IsBlackTurn)); ReflectBoardInfoToXaml(); IsBlackTurn = !IsBlackTurn; }CheckValidBoardInfoは挟めることができたらtrueを返し、できなければfalseを返す。
まぁなので名前としてはTryAndReverseみたいなの方が妥当だろうか。
そして、無事ひっくり返すことができたあとに、Row,Colに自分の石を置き、ターンを変える。そしてオセロを実装する上で一番の面倒なところが石を挟んだらひっくり返す処理だろう。
石を挟むという言葉もなかなか抽象的である。なので、より具体的な言葉で言ってあげるならば座標(x, y)がzであり、(x + a * 1, y + b * 1)から(x + a * (n - 1), y + b * (n - 1))が-zで(x + a * n, y + b * n)がzである場合(x + a * 1, y + b * 1)から(x + a * (n - 1), y + b * (n - 1))をzに変更する。(0 <= x <= 8, 0 <= y <= 8, a,b は1,0,-1のいずれか。2 <= n <= 7)
という言葉になる。
まぁ簡単に言えば方向と挟む数と挟む回数の順番でfor文を回してやり適宜チェックを行ってやればよい。
それをコードで表すとMainWindow.xaml.csprivate bool CheckValidBoardInfo(int Row, int Col) { int Info = ConvertTurnToIntInfo(IsBlackTurn); int RowDirection; int ColDirection; var IsTurnChange = false; //方向 for(var i = 1; i <= 8; i++) { //石の数 for(var j = 7; j >= 2; j--) { (RowDirection, ColDirection) = GetDirection(i); //ひとつでも変更があればturnChangeFlgをtrueにする。 if (!CheckRangeValid(Row + j * RowDirection, Col + j * ColDirection) || !CheckReverse(Row, Col, Info, i, j)) continue; else IsTurnChange = true; //チェックが通ったものの場合j - 1個ひっくり返す。 for(var k = 1; k < j; k++) { SetBoardInfo(Row + k * RowDirection, Col + k * ColDirection, Info); } } } return IsTurnChange; }という風になる。
全体のコードを示すとMainWindow.xaml.csusing System.Windows; using System.Windows.Controls; namespace osero_wpf { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private bool IsBlackTurn = true; private int[,] BoardInfo = new int[8, 8]; public MainWindow() { InitializeComponent(); Init(); } /// <summary> /// 初期化 /// </summary> private void Init() { for (var i = 0; i < 8; i++) { for (var j = 0; j < 8; j++) { BoardInfo[i, j] = 0; } } SetBoardInfo(4, 4, 1); SetBoardInfo(4, 5, -1); SetBoardInfo(5, 4, -1); SetBoardInfo(5, 5, 1); ReflectBoardInfoToXaml(); } /// <summary> /// オセロの諸々の処理実行 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Button_Click(object sender, RoutedEventArgs e) { var button = sender as Button; var stackPanel = button.Parent as StackPanel; int Col = int.Parse(button.Tag.ToString()); int Row = int.Parse(stackPanel.Tag.ToString()); if(BoardInfo[Row - 1, Col - 1] != 0) { MessageBox.Show("そこにはすでに置かれています。"); return; } var IsTurnChange = CheckValidBoardInfo(Row, Col); if (!IsTurnChange) { MessageBox.Show("そこには置けません。"); return; } SetBoardInfo(Row, Col, ConvertTurnToIntInfo(IsBlackTurn)); ReflectBoardInfoToXaml(); IsBlackTurn = !IsBlackTurn; } private bool CheckValidBoardInfo(int Row, int Col) { int Info = ConvertTurnToIntInfo(IsBlackTurn); int RowDirection; int ColDirection; var IsTurnChange = false; //方向 for(var i = 1; i <= 8; i++) { //石の数 for(var j = 7; j >= 2; j--) { (RowDirection, ColDirection) = GetDirection(i); //ひとつでも変更があればturnChangeFlgをtrueにする。 if (!CheckRangeValid(Row + j * RowDirection, Col + j * ColDirection) || !CheckReverse(Row, Col, Info, i, j)) continue; else IsTurnChange = true; //チェックが通ったものの場合j - 1個ひっくり返す。 for(var k = 1; k < j; k++) { SetBoardInfo(Row + k * RowDirection, Col + k * ColDirection, Info); } } } return IsTurnChange; } /// <summary> /// 配列の範囲をチェック /// </summary> /// <param name="RowLimit">Row</param> /// <param name="ColLimit">Col</param> /// <returns>配列の範囲を越えなければtrue超えればfalse</returns> private bool CheckRangeValid(int RowLimit = 0, int ColLimit = 0) { RowLimit--; ColLimit--; return RowLimit < 8 && ColLimit < 8 && 0 <= RowLimit && 0 <= ColLimit; } /// <summary> /// /// </summary> /// <param name="Row"></param> /// <param name="Col"></param> /// <param name="Info">現在のtrunをintで表したもの</param> /// <param name="Direction">チェックする方向</param> /// <param name="Length">チェックの範囲</param> /// <returns>ひっくり返すことができればtrueそうでなければfalse</returns> private bool CheckReverse(int Row, int Col, int Info, int Direction, int Length) { int RowDirection; int ColDirection; (RowDirection, ColDirection) = GetDirection(Direction); var IsOneReverse = false; for(var i = 1; i < Length; i++) { IsOneReverse = BoardInfo[Row + RowDirection * i - 1, Col + ColDirection * i - 1] == -Info; if (!IsOneReverse) return false; } return BoardInfo[Row + RowDirection * Length - 1, Col + ColDirection * Length - 1] == Info; } /// <summary> /// /// </summary> /// <param name="Direction">方向</param> /// <returns>方向をRow,Colの二つに分ける</returns> private (int, int) GetDirection(int Direction) { int RowDirection; int ColDirection; if (Direction == 1) { RowDirection = 0; ColDirection = 1; } else if (Direction == 2) { RowDirection = 1; ColDirection = 1; } else if (Direction == 3) { RowDirection = 1; ColDirection = 0; } else if (Direction == 4) { RowDirection = 1; ColDirection = -1; } else if (Direction == 5) { RowDirection = 0; ColDirection = -1; } else if (Direction == 6) { RowDirection = -1; ColDirection = -1; } else if (Direction == 7) { RowDirection = -1; ColDirection = 0; } else { RowDirection = -1; ColDirection = 1; } return (RowDirection, ColDirection); } /// <summary> /// boolのターン情報をintに変換(intにしといたほうがいいかも?) /// </summary> /// <param name="Turn">現在のターン黒か白か</param> /// <returns></returns> private int ConvertTurnToIntInfo(bool Turn) { if (Turn) { return 1; } else { return -1; } } /// <summary> /// 黒か白かをintの情報からstringに変換 /// </summary> /// <param name="info">1ならば黒,-1ならば白,0ならばなし</param> /// <returns></returns> private string ConvertIntInfoToStringInfo(int info) { if(info == 1) { return "●"; } else if(info == -1) { return "〇"; } else { return ""; } } /// <summary> /// BoardInfoにsetするとき-1のずれが発生するのでメソッド化する。 /// </summary> /// <param name="row">縦</param> /// <param name="col">横</param> /// <param name="info">黒か白かを1か-1かで表したもの</param> private void SetBoardInfo(int row, int col, int info) { BoardInfo[row - 1, col - 1] = info; } /// <summary> /// ボードの情報をxamlに反映 /// </summary> private void ReflectBoardInfoToXaml() { int row = 0; int col = 0; int BlackCount = 0; int WhiteCount = 0; int NoCount = 0; foreach (var Children in Board.Children) { var Panel = Children as StackPanel; foreach (var But in Panel.Children) { var Butt = But as Button; Butt.Content = ConvertIntInfoToStringInfo(BoardInfo[row, col]); if (BoardInfo[row, col] == 1) BlackCount++; else if (BoardInfo[row, col] == -1) WhiteCount++; else NoCount++; col++; } col = 0; row++; } if (BlackCount == 0) { MessageBox.Show("白の勝ち"); Init(); } else if(WhiteCount == 0) { MessageBox.Show("黒の勝ち"); Init(); } else if(NoCount == 0) { if(WhiteCount < BlackCount) { MessageBox.Show("黒の勝ち"); Init(); } else if(WhiteCount > BlackCount) { MessageBox.Show("白の勝ち"); Init(); } else { MessageBox.Show("引き分け"); Init(); } } ViewBlackCount.Text = BlackCount.ToString(); ViewWhiteCount.Text = WhiteCount.ToString(); TurnText.Text = IsBlackTurn ? "黒" : "白"; } /// <summary> /// 置けない時にパスするためのボタン。 /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void Button_Click_1(object sender, RoutedEventArgs e) { IsBlackTurn = !IsBlackTurn; } } }という感じになる。