- 投稿日:2021-10-31T17:16:50+09:00
C#9.0 配列入りのレコードってどうだろう 【不変型配列レコード】
配列入りのレコードってどうだろう 【不変型配列レコード】 はじめに この記事では、レコードと配列の共存を図る方法について書いていきます。 「レコードの中に配列を入れたい」という好奇心と「レコードが不変型であることを守りたい」という頑固な心が葛藤する人のための記事ですが、単にレコードのカスタマイズに興味がある人にも参考になるかもしれません。 C#9.0のrecordを想定しております。 C#10.0で予定されているrecord structについてはまた違った考え方が必要になると思います。 記事内では、下記の名前空間を定義しています。 using System; using System.Collections.Immutable; using System.Collections.Generic; using System.Linq; using System.Text; 位置指定パラメータの型の選択 データ列を扱う型がC#(.NET)にはいくつかあります。 まずは今回位置指定パラメータに指定する型を考えていきます。 制限がないのだから通常の配列でも良い? 案:Arrayバージョン record MyArrayRecord(string Name, int[] Values); 単純に考えれば、配列を使ってはいけない理由はないですし、 何も気にしない場合は上のコードのまま使っても構わないのですが、 「不変型」を指向しているレコードに、配列という「可変型」を含めるのは気が引けるのです。 問題点:Arrayは不変ではない var rec = new MyArrayRecord("田中", new int[] { 1, 3, 5, 7 }); // rec.Values = new int[] { 2, 4, 6 }; /* プロパティ自体は書き換えられない */ rec.Values[0] = 100; /* プロパティの中身は書き換えられる */ だったらIEnumerableでいいのでは? 案:IEnumerableバージョン record MyEnumerableRecord(string Name, IEnumerable<int> Values); これでValuesプロパティが書き換えられる心配は無くなりました。 大きな改善です。 ちなみに、Microsoftのレコード紹介記事ではIEnumerable<T>を使う方法が書かれています。 Create record types これで十分な状況であれば、間違った対応ではないと思います。 めでたしめでたし……と言いたいところですが、まだ少しだけ問題があります。 問題点:IEnumerableでも、渡した側は書き換えられる var nums = new int[] { 1, 3, 5, 7 }; var rec = new MyEnumerableRecord("田中", nums); // rec.Values[0] = 100; /* プロパティを経由した値の変更はできない。 */ nums[0] = 123; /* ←元の配列の中身を書き換えることはできる。 */ この方法では、元の配列の中身を書き換えることはできてしまいます。 IEnumerableを使う場合の緊急回避策 案:IEnumerable改 record MyEnumerableRecord(string Name, IEnumerable<int> Values) { public IEnumerable<int> Values { get; } = Values.ToArray(); } 一回コピーしてしまうのが作戦の一つです。 もしIEnumerable<T>型で良ければ、この対応策で問題ないでしょう。 こだわり:インデクサは使いたい var nums = new int[] { 1, 3, 5, 7 }; var rec = new MyEnumerableRecord("田中", nums); Console.WriteLine(rec.Values[1]); /* ←これを書きたい */ ですが、今回は配列のようにインデクサを使って値を取り出したいとします。 ImmutableArrayの出番 こういう時のためのSystem.Collections.Immutable名前空間です。 ImmutableArray<T>が「不変型」を作るためのカギになります。 案:Immutableバージョン record MyImmutableArrayRecord(string Name, ImmutableArray<int> Values); System.Collections.Immutable名前空間のクラスは、一度作成したインスタンスの値を変更したい場合、新たなインスタンスを作成するような仕組みになっています。 つまり、レコードと同じような考え方のクラスになっています。 参考:ImmutableArrayの使い方(値の操作) ImmutableArray<int> array = ImmutableArray.Create(1, 3, 5); //Builderを使って操作する方法 ImmutableArray<int>.Builder builder = array.ToBuilder(); for (int i = 7; i <= 13; i+=2) { builder.Add(i); } array = builder.ToImmutable(); //Addメソッドを使って操作する方法 array = array.Add(15); 今回はこのImmutableArray<int>を選ぶことにして、 レコードの本体に必要な本体を追加していきます。 不変配列レコードの本体の肉付け 以下の順で本体にメンバーを追加します。 コンストラクタをオーバーロードする ToString()の結果に配列の中身も反映する 等値比較を整備する コンストラクタをオーバーロードする 不満:インスタンス生成にいちいちImmutableArrayと書きたくないかも var rec = new MyImmutableArrayRecord("田中", ImmutableArray.Create(1, 3, 5, 7)); このままではインスタンス生成が多少面倒なので、int[]を受け付けるコンストラクタも用意しておきましょう。 実装:配列を受け入れるコンストラクタ record MyImmutableArrayRecord(string Name, ImmutableArray<int> Values) { public MyImmutableArrayRecord(string name, params int[] values) : this(name, ImmutableArray.Create(values)) { } } 実装後:書きなれた方法でインスタンス生成できる var rec1 = new MyImmutableArrayRecord("田中", ImmutableArray.Create(1, 3, 5, 7)); var rec2 = new MyImmutableArrayRecord("田中", new int[] { 1, 3, 5, 7 }); var rec3 = new MyImmutableArrayRecord("田中", 1, 3, 5, 7); レコードにコンストラクタを追加する場合、thisによるプライマリコンストラクタによる初期化が必須となります。 ※位置指定パラメータ有りのレコードに限る。 個人的にはこの制約はお気に入りで、下記のQiitaの意見交換(クローズ済)を思い出しました。 コンストラクタのオーバーロードが複数ある場合のメンバーの初期化について ToString()の結果に配列の中身も反映する インスタンスの中身を表示 Console.WriteLine(rec1) してみると、こんな結果になってしまいます。 不満:ToStringの出力が不便 MyImmutableArrayRecord { Name = 田中, Values = System.Collections.Immutable.ImmutableArray`1[System.Int32] } 配列の中身まではToString()されない、あるある話ですね。 ここでは、配列の中身まで表示できるように手を加えます。 レコード型ではToString()のoverrideより先に、PrintMembers()メソッドを書くことを優先します。 実装:PrintMembers /// 内容を出力する protected virtual bool PrintMembers(StringBuilder builder) { builder.Append(nameof(Name)); builder.Append(" = "); builder.Append(Name); builder.Append(", "); builder.Append(nameof(Values)); if (!Values.IsDefaultOrEmpty) { builder.Append(" = { "); builder.Append(string.Join(", ", Values)); builder.Append(" }"); } else { builder.Append(" = { }"); } return true; } 実装後:出力 MyImmutableArrayRecord { Name = 田中, Values = { 1, 3, 5, 7 } } このPrintMembers()メソッドはToString()から呼び出されています。 表示すべき中身が無いときには以下のように書きましょう。 参考:表示すべき中身がない場合 protected virtual bool PrintMembers(StringBuilder builder) { return false; } 参考:出力 MyImmutableArrayRecord { } 型名すら表示させたくないのであれば、ToString()をoverrideします。 この場合も、ToString()からPrintMembers()の呼び出しの流れを意識して書くとより良いと思います。 等値比較を整備する 等値比較すると…… var rec1 = new MyImmutableArrayRecord("田中", 1, 3, 5, 7); var rec2 = new MyImmutableArrayRecord("田中", 1, 3, 5, 7); Console.WriteLine(rec1 == rec2); 欠陥:配列の中身が等しくても等値にならない False 現状の実装だと、配列内の値は比較されません。 内部的にはNameプロパティの比較とValuesプロパティの比較はしてくれているのですが、Valuesの部分の比較が常にFalseを返してしまいます。 等値性関連メソッド Equals(), GetHashCode() を追加します。 実装:Equals,GetHashCode // 全ての要素について等値かどうか確認する public virtual bool Equals(MyImmutableArrayRecord? other) { if (ReferenceEquals(this, other)) { return true; } if (other is null) { return false; } if (Values.IsDefaultOrEmpty && other.Values.IsDefaultOrEmpty) { return true; } if (Values.IsDefaultOrEmpty ^ other.Values.IsDefaultOrEmpty) { return false; } return EqualityContract == other.EqualityContract && Name == other.Name && Values.SequenceEqual(other.Values); } public override int GetHashCode() { HashCode hash = new(); hash.Add(EqualityContract); hash.Add(Name); if (!Values.IsDefaultOrEmpty) { //要素が多い場合、末尾8つの要素を用いて生成する int start = Values.Length > 8 ? Values.Length - 8 : 0; for (int i = start; i < Values.Length; i++) { hash.Add(Values[i]); } } return hash.ToHashCode(); } 実装後:等値比較成功 True ここで Equals(object? o), ==, != は実装しなくていいの? と思った方は察しが良いです。 レコード型では、上記のメソッドや演算子はEquals(T? o)の実装を利用するようになるため、これらを手書きする必要はありません。(上書きできない) 余談:EqualityContractプロパティ EqualityContractという見慣れないプロパティは、等値比較に「型」の一致を要求するためのものです。 派生型と基本型、派生型同士の比較は常にFalseになるように設計されています。 そうです。レコードにも継承があるのです。 レコードの継承については色々考えなければないのですが、この記事では扱いません。 この問題を考えたくない場合は、初めからsealedにしてしまうのも手かもしれません。 まとめ 当初の要求を満たす不変型配列レコードができました。 同じようなコードをclassで書くよりコードの量も減っていて、 recordの機能を生かしているのではないでしょうか。 ご意見、ご感想、ご質問などあれば、是非コメントお願いします。 この実装が実用的かどうかは、状況次第だと思っています。 「不変」にこだわったレコードを実装する時に何をすればいいのか、 少しでも参考になる情報が書き残せていれば幸いです。
- 投稿日:2021-10-31T16:02:47+09:00
IDisposableはどんな時に実装すればいいですか?
PRのコメントでIDisposableを実装してくださいと言われたが、どんな時に実装すればいいのかわからなかったので調べまとめました。 IDisposable実装の目的は参照の破棄なので、参照をいつ、どうやって破棄すべきかについて論じます。 IDisposableインターフェースとは void Dipose()があるだけのシンプルなインターフェース。 ただのインターフェースであり、参照が切れた時にGC(ガベージコレクション)が走るなどということはない。 Dispose()はメモリを破棄するメソッドでありメモリーリークを防いだり、パフォーマンスを維持したりするために用いられる。 GCはマネージリソースを自動で破棄してくれる。 ただし、GCの破棄タイミングは予測できない上にアンマネージリソースについては破棄してくれないので開発者が管理する必要がある。 マネージリソース、アンマネージリソースの定義は文献によって微妙に差異があるのですが、下記記事で詳細調査が行われており大変参考になりました。 対象オブジェクト マネージ・アンマネージ double[],string等(.NET組み込みライブラリ) マネージリソース Stream,Bitmap等(派生クラスはOSリソースをラップしているのでアンマネージリソース) マネージリソース (外部リソースの)ポインター等(ポインターそのものはマネージだが、ポインターが指すリソースはアンマネージ) マネージリソース ポインターが指す外部リソース(C/C++、OSリソース) アンマネージリソース OSリソース云々というのは下記のようなリソースを指します。 ファイルや周辺機器などのリソース(OSが管理している資源)を使用する場合、 まずリソースを使用する権利を取得し、 リソースに対する操作(ファイルの読み書きなど)を行った後、 リソース使用権を破棄する必要があります。 いつIDisposableを実装するか? アンマネージリソースを扱う時 アンマネージリソースはGCが管理してくれないので必ず開発者が破棄処理を実装しなければなりません。 具体的に警戒しなければならないケースは下記 System.IO.Streamの派生クラスやSystem.Drawing.Imaging.Bitmap派生クラス、その他DB接続するためクラスなどのOSリソースをラップしたリソースを扱う時 C/C++などの.NETのメモリ管理対象外のライブラリを扱う時 マネージドリソースを扱う時 マネージドリソースを扱う場合も、GCに任せず任意のタイミングで破棄したい場合は開発者側で明示的に実装する場合があります。 例えば下記のようなケースが考えられます。 画面遷移が多い場合 長い処理(計算の為に巨大な配列を扱う、変換処理を繰り返すなど)を行う場合 こまめにメモリ管理をしてパフォーマンスを維持したい場合などです。 Observaberパターンを扱う時 購読解除する時に利用します。 ReactiveExtensionsを利用する場合は、もともとDisposeを実装してくれているのでDisposeするだけでOKです。 更に、まとめてDisposeするためのCompositeDisposableという型があるのでこれを利用します。 IDiposableをどう実装するか? 基本Disposeパターン マネージドリソースだろうとアンマネージリソースだろうと一意な基本Disposeパターンをというものが存在します。 とりあえずこれを実装しておけばいい。 Disposeパターンは、ファイナライザーとIDisposableインターフェースの使用法と実装の標準化を意図したものです(.NETクラスライブラリ設計 9.4 Disposeパターン より) public class DiposeableResourceHolder : IDisposable { private SafeHandler _resouce; // リソースへのハンドル public DiposeableResourceHolder() { _resouce = ... // リソースへの割り当て } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); // ファイナライザが不要なことをGCに伝える } protected virtual void Dispose(bool disposing) { // disposingはIDisposable.Disposeなのかファイナライザーなのかを判断する if(disposing) { _resource?.Dispose(); } } } あとはこれをDisposeが必要なクラスに継承させればよい。 追加処理(フィールドの購読解除など)を書く場合は、継承先でDispose(bool)をoverrideして追加処理を書けばよいのです。 突然出てきたファイナライザというのは簡単に言うとGCの実行です。 ファイナライザはGCにオブジェクトが回収された時(つまり参照が切れたタイミング)に実行される処理で、Object基底クラスに実装されたFinalize()メソッドが実行されることを指します。 Disposeパターンの拡張(ファイナライズ可能な型) たとえファイナライザーが役に立つ可能性があるとしても、本当に、ファイナライザーは書かないほうがいいでしょう。.... 型にファイナライザーを記述すると、たとえファイナライザーが全く呼び出されなかったとしても、その方を使うためのコストが大きくなります。 (.NETクラスライブラリ設計 9.4.2 ファイナライズ可能な型より) アンマネージリソースの場合自前実装しなければならず、(前提状態を整えることが)非常に難しいそうで、この記事を読んでいる方々は一旦無視してもらっていいと思います。 知っておきたい方は下記記事の「Disposeの実装方法」を読んで頂ければと思います。 Disposeの処理が走るタイミング さてDispose()メソッドに処理書いたはいいものの、これはどこで使えばいいのだろう? 明示的に実行した時 usingを使うことで明示的に実行できます。 アンマネージリソースを利用した処理を行う場合はもちろん、上記の「いつIDisposableを実装するか?/マネージドリソースを扱う場合」で上げた例のようなケースで使用します。 using( myConnection = new SqlConnやection(connString)) { myConnection.Open(); } //上記はCLIにて下記に展開される try { myConnection = new SqlConnection(connString); myConnection.Open(); } finally { myConnection.Dispose(); } まとめ まとめると下記のようになります。 いつIDisposableを実装するか? アンマネージリソースを扱う時 重い、長い処理を行う時 購読解除したい時 IDisposableをどう実装するか? 基本Diposeパターンに従う 何がアンマネージリソースなのかマネージリソースなのか、又GCの対象やタイミングなど完全に理解していないですが いつ警戒するか、どう実装するかがわかったので当面は生きていけそうです。
- 投稿日:2021-10-31T15:42:51+09:00
C# WinFormsで試行錯誤してデザインパターンStrategy Patternを学ぶ
はじめに C#を使い始めて5か月くらいになり、業務でのコードのボリュームが増えてきたため、スパゲッティを卒業してきれいなコードを書いてみたいと思い始めました。 今まで業務とは関係のないコーディングをしていた頃は、自分一人のためのコーディングであったため、誰かに見せるという機会がなくそれでいて保守性や拡張性等を考えたこともありませんでした。 機能を追加したいと考えたときに過去に書いたコードを編集しないといけないようなコードを書き続けていると、時間を食うようになりこれではいけないと思いコーディングのいろは的なものを調べてみました。 調べていたところオブジェクト指向やデザインパターンというものをよく見かけるようになりそれらを調べてみても全くわからないという状況が続いていました。そこで今回はデザインパターンのうちの一つであるStrategy patternを勉強してみたいと思います。 Strategy Patternとは Strategy Pattern自体の説明はいろいろなところでされているので割愛しますが、理解を決定づけた説明は数字のリストのソーティングをする際に、リストを作り直すことなくアルゴリズムのみを変更したい場合にこのパターンを使えば戦略の切り替えが容易になるという説明でした。Template PatternやState Patternも一緒に出てくることが多いようです。 参考 今回作るもの ターミナル上に文字を表示させる例が多いですがwinformsでも同じことをしたいと思ったため、今回は画像をピクチャボックスの上に表示させてボタンで操作して遊べるものを作ります。picボタンを押すと移動させることのできる画像の切り替えができ、右下のボタンで移動方向を変更することができます。 ところどころ気になる箇所はありますが今回の目的はデザインパターンを使ってみたいというものなので多めに見てください。 UML図の書き方がよくわかっていないので適当ですがこのようなものを意識して作ります。 まずはべた書き 書いてみないと始まらないためとりあえず書いてみましょう。ボタンとピクチャボックス等適当に配置します。 初めに、一枚の画像を動かせるようにします。 まずは数値の初期設定をして、ボタンを実装します。上下左右のクリックで移動方向の変更、NumericUpDownで速度の変更をします。 Form1.cs public partial class Form1 : Form { public Form1() { InitializeComponent(); } private Image img1; private float x = 0; private float y = 0; private float dx; private float dy; float speed = 0.4f; private void Form1_Load(object sender, EventArgs e) { Image imgload1 = System.Drawing.Image.FromFile(@"C:\Users\Fridge\Desktop\cat2.jpg"); img1 = ResizeImage(imgload1, 200, 200); numericUpDown1.Value = Convert.ToDecimal(speed); } private void LeftButton_Click(object sender, EventArgs e) { dx = -speed; dy = 0; } private void UpButton_Click(object sender, EventArgs e) { dy = -speed; dx = 0; } private void RightButton_Click(object sender, EventArgs e) { dx = speed; dy = 0; } private void DownButton_Click(object sender, EventArgs e) { dy = speed; dx = 0; } private void StopButton_Click(object sender, EventArgs e) { dx = 0; dy = 0; } private void numericUpDown1_ValueChanged(object sender, EventArgs e) { speed = (float)numericUpDown1.Value; } 画像をpictureBoxに描画します。Bounceにチェックマークが入っている場合は画像がループせず端で跳ね返ります。 Form1.cs private void pictureBox1_Paint(object sender, PaintEventArgs e) { if (img1 != null) { e.Graphics.DrawImage(img1,x, y, img1.Width, img1.Height); if (bounceCheckBox.Checked) { x += dx; if (x >= pictureBox1.Width-img1.Width || x <= 0) { dx = -dx; } y += dy; if (y >= pictureBox1.Height-img1.Height || y<=0) { dy = -dy; } } else { x += dx; if (x >= pictureBox1.Width) { x = -img1.Width; } else if (x <= -img1.Width) { x = pictureBox1.Width; } y += dy; if (y >= pictureBox1.Height) { y = -img1.Height; } else if (y <= -img1.Height) { y = pictureBox1.Height; } } } pictureBox1.Invalidate(); LeftButton.Refresh(); RightButton.Refresh(); UpButton.Refresh(); DownButton.Refresh(); } 画像のリサイズです。以降省きます。 Form1.cs private Bitmap ResizeImage(Image img, float maxWidth, float maxHeight) { double resizeWidth = img.Width; double resizeHeight = img.Height; double aspect = resizeWidth / resizeHeight; if (resizeWidth > maxWidth) { resizeWidth = maxWidth; resizeHeight = resizeWidth / aspect; } if (resizeHeight > maxHeight) { aspect = resizeWidth / resizeHeight; resizeHeight = maxHeight; resizeWidth = resizeHeight * aspect; } Bitmap result = new Bitmap((int)resizeWidth, (int)resizeHeight); Graphics g = Graphics.FromImage(result); g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic; g.DrawImage(img, 0, 0, result.Width, result.Height); return result; } ここまでは特になんてことない短いコードです。 仕様変更 今までは画像を一枚表示できればよかったのに急に画像を二枚一度に表示させてください。かつ、画像を一枚一枚動かせるようにもしてくださいと言われました。二枚くらいならそのまま書いてもよさそうですがもしかすると5枚に増えてしまうかもしれません。そこで私はいままでに聞いたことがあるワード「オブジェクト指向」を使えないかと考えます。 Catというクラスを作ってそれにすべて投げ込んでみましょう。動きを表すメソッドMovementも入れてしまいます。 Form1.cs public class Cat { public Image Img; public float X; public float Y; public float Dx; public float Dy; public int BoundX; public int BoundY; public bool Bounce; public void Movement() { if (this.Bounce) { this.X += this.Dx; if (this.X >= this.BoundX - this.Img.Width || this.X <= 0) { this.Dx = -this.Dx; } this.Y += this.Dy; if (this.Y >= this.BoundY - this.Img.Height || this.Y <= 0) { this.Dy = -this.Dy; } } else { X += Dx; if (X >= this.BoundX) { X = -this.Img.Width; } else if (X <= -this.Img.Width) { X = this.BoundX; } Y += Dy; if (Y >= this.BoundY) { Y = -this.Img.Height; } else if (Y <= -this.Img.Height) { Y = this.BoundY; } } } } クラスができたのでそれを使って猫を作ります。 Form1.cs private void Form1_Load(object sender, EventArgs e) { Image imgload1 = System.Drawing.Image.FromFile(@"C:\Users\Fridge\Desktop\Tofu.jpg"); img1 = ResizeImage(imgload1, picWidth, picHeight); Image imgload2 = System.Drawing.Image.FromFile(@"C:\Users\Fridge\Desktop\cat2.jpg"); img2 = ResizeImage(imgload2, picWidth, picHeight); numericUpDown1.Value = Convert.ToDecimal(speed); cat1 = new Cat(); cat1.Img = img1; cat1.BoundX = pictureBox1.Width; cat1.BoundY = pictureBox1.Height; cat1.Bounce = false; cat2 = new Cat(); cat2.Img = img2; cat2.BoundX = pictureBox1.Width; cat2.BoundY = pictureBox1.Height; cat2.Bounce = false; } 画像が二枚あるのだからcatNumberに値を与え、一つ目の猫が選ばれたときはcat1を動かし、二つ目が選ばれたときはcat2を動かそうと私は考えました。 すべてのボタンでif文が書かれてしまっていますが2枚くらいならこれでも何とかなるでしょう。 Form1.cs int catNumber = 1; private void LeftButton_Click(object sender, EventArgs e) { if (catNumber == 1) { cat1.Dx = -speed; cat1.Dy = 0; } else if (catNumber == 2) { cat2.Dx = -speed; cat2.Dy = 0; } } private void UpButton_Click(object sender, EventArgs e) { if (catNumber == 1) { cat1.Dy = -speed; cat1.Dx = 0; } else if(catNumber == 2) { cat2.Dy = -speed; cat2.Dx = 0; } } private void RightButton_Click(object sender, EventArgs e) { if (catNumber == 1) { cat1.Dx = speed; cat1.Dy = 0; } else if (catNumber == 2) { cat2.Dx = speed; cat2.Dy = 0; } } private void DownButton_Click(object sender, EventArgs e) { if (catNumber == 1) { cat1.Dy = speed; cat1.Dx = 0; } else if (catNumber == 2) { cat2.Dy = speed; cat2.Dx = 0; } } private void StopButton_Click(object sender, EventArgs e) { if (catNumber == 1) { cat1.Dy = 0; cat1.Dx = 0; } else if (catNumber == 2) { cat2.Dy = 0; cat2.Dx = 0; } } private void numericUpDown1_ValueChanged(object sender, EventArgs e) { speed = (float)numericUpDown1.Value; } private void pictureBox1_Paint(object sender, PaintEventArgs e) { e.Graphics.DrawImage(cat1.Img, cat1.X, cat1.Y, cat1.Img.Width, cat1.Img.Height); cat1.Movement(); e.Graphics.DrawImage(cat2.Img, cat2.X, cat2.Y, cat2.Img.Width, cat2.Img.Height); cat2.Movement(); pictureBox1.Invalidate(); LeftButton.Refresh(); RightButton.Refresh(); UpButton.Refresh(); DownButton.Refresh(); } private void picture1Button_Click(object sender, EventArgs e) { catNumber = 1; } private void picture2Button_Click(object sender, EventArgs e) { catNumber = 2; } private void bounceCheckBox_CheckedChanged(object sender, EventArgs e) { if (bounceCheckBox.Checked) { if (catNumber == 1) { cat1.Bounce = true; } else if(catNumber ==2) { cat2.Bounce = true; } } else { if (catNumber == 1) { cat1.Bounce = false; } else if (catNumber == 2) { cat2.Bounce = false; } } } } 再び仕様変更 それでは今回は画像を4枚に増やしてください。 少しこじつけ感はありますが、このままでは分岐が多くなってしまうため以下のような分岐を減らすためにstrategy patternに挑戦します。 Form1.cs private void StopButton_Click(object sender, EventArgs e) { if (catNumber == 1) { cat1.Dy = 0; cat1.Dx = 0; } else if (catNumber == 2) { cat2.Dy = 0; cat2.Dx = 0; } else if (catNumber == 3) { cat3.Dy = 0; cat3.Dx = 0; } else if (catNumber == 4) { cat4.Dy = 0; cat4.Dx = 0; } } インターフェイス まずはインターフェイスに実装するメソッドを定義します。 ここではIStrategyというインターフェイスに上下左右の動き+停止のメソッドと動き続けるのに必要なメソッドMovementを定義しました。 ファイルも分けてCatClassというファイルにインターフェイスとこれから実装するクラスも書いてしまいます。 CatClass.cs interface IStrategy { void MoveLeft(); void MoveRight(); void MoveUp(); void MoveDown(); void Stop(); void Movement(); } 処理の内容をクラスに記載します。IStrategyを継承しています。 CatClass.cs public class Cat : IStrategy { public Image Img { get; set; } public float X { get; set; } public float Y { get; set; } public float Dx { get; set; } public float Dy { get; set; } public int BoundX { get; set; } public int BoundY { get; set; } public bool Bounce { get; set; } public float Speed { get; set; } public void MoveLeft() { this.Dx = -this.Speed; this.Dy = 0; } public void MoveRight() { this.Dx = this.Speed; this.Dy = 0; } public void MoveUp() { this.Dy = -this.Speed; this.Dx = 0; } public void MoveDown() { this.Dy = this.Speed; this.Dx = 0; } public void Stop() { this.Dx = 0; this.Dy = 0; } public void Movement() { if (this.Bounce) { this.X += this.Dx; if (this.X >= this.BoundX - this.Img.Width || this.X <= 0) { this.Dx = -this.Dx; } this.Y += this.Dy; if (this.Y >= this.BoundY - this.Img.Height || this.Y <= 0) { this.Dy = -this.Dy; } } else { this.X += this.Dx; if (this.X >= this.BoundX) { this.X = -this.Img.Width; } else if (this.X <= -this.Img.Width) { this.X = this.BoundX; } this.Y += this.Dy; if (this.Y >= this.BoundY) { this.Y = -this.Img.Height; } else if (this.Y <= -this.Img.Height) { this.Y = this.BoundY; } } } } 実際に処理を行ってくれるクラスStrategistを用意します。このクラスを使い、ボタンにより選択された猫へ切り替えていきます。 クラス名はStrategistにしていますがRunnerでもよいかもしれません。 CatClass.cs class Strategist { private IStrategy strategy; public Strategist(IStrategy strategy) { this.strategy = strategy; } public void ExecuteMovement() { strategy.Movement(); } public void ExecuteMoveLeft() { strategy.MoveLeft(); } public void ExecuteMoveUp() { strategy.MoveUp(); } public void ExecuteMoveDown() { strategy.MoveDown(); } public void ExecuteMoveRight() { strategy.MoveRight(); } public void ExecuteStop() { strategy.Stop(); } public void ChangeStrategy(IStrategy strategy) { this.strategy = strategy; } } Form1に戻り実際に使ってみます。画像のロードと猫の作成です。ベースとなるクラスはできているので同じものをnewして実体を複数作ります。 変数に具体的な数字を入れていますがここは省略できる箇所がいくつかあると思います。 Form1.cs private void Form1_Load(object sender, EventArgs e) { System.Environment.CurrentDirectory = @"..\..\..\Images"; Console.WriteLine(System.Environment.CurrentDirectory); Image imgload1 = System.Drawing.Image.FromFile(@"Tofu.jpg"); img1 = ResizeImage(imgload1, picWidth, picHeight); Image imgload2 = System.Drawing.Image.FromFile(@"cat2.jpg"); img2 = ResizeImage(imgload2, picWidth, picHeight); Image imgload3 = System.Drawing.Image.FromFile(@"cat3.jpg"); img3 = ResizeImage(imgload3, picWidth, picHeight); Image imgload4 = System.Drawing.Image.FromFile(@"cat4.jpg"); img4 = ResizeImage(imgload4, picWidth, picHeight); cat1 = new Cat(); strategist.ChangeStrategy(cat1); currentCat = cat1; cat1.Img = img1; cat1.BoundX = pictureBox1.Width; cat1.BoundY = pictureBox1.Height; cat1.Bounce = false; cat1.Speed = speed; cat2 = new Cat(); cat2.Img = img2; cat2.BoundX = pictureBox1.Width; cat2.BoundY = pictureBox1.Height; cat2.Bounce = false; cat2.Speed = speed; cat3 = new Cat(); cat3.Img = img3; cat3.BoundX = pictureBox1.Width; cat3.BoundY = pictureBox1.Height; cat3.Bounce = false; cat3.Speed = speed; cat4 = new Cat(); cat4.Img = img4; cat4.BoundX = pictureBox1.Width; cat4.BoundY = pictureBox1.Height; cat4.Bounce = false; cat4.Speed = speed; numericUpDown1.Value = Convert.ToDecimal(speed); } ボタンを押すと猫の切り替えができます。ここは分岐をしないといけないので、猫の数に応じて増やさなければいけません。それでも追加のみですでにあるコードを書き換える必要はないので、保守しやすくなったと思います。 Form1.cs private void picture1Button_Click(object sender, EventArgs e) { strategist.ChangeStrategy(cat1); currentCat = cat1; } private void picture2Button_Click(object sender, EventArgs e) { strategist.ChangeStrategy(cat2); currentCat = cat2; } private void picture3Button_Click(object sender, EventArgs e) { strategist.ChangeStrategy(cat3); currentCat = cat3; } private void picture4Button_Click(object sender, EventArgs e) { strategist.ChangeStrategy(cat4); currentCat = cat4; } 一番大事な上下左右の動きの部分です。picボタンを押すだけで猫が入れ替わるのでそのあとに条件分岐をする必要がなくなりました。これで猫が何匹になってもif文を書く必要がなくなりました。 Form1.cs private void LeftButton_Click(object sender, EventArgs e) { strategist.ExecuteMoveLeft(); } private void UpButton_Click(object sender, EventArgs e) { strategist.ExecuteMoveUp(); } private void RightButton_Click(object sender, EventArgs e) { strategist.ExecuteMoveRight(); } private void DownButton_Click(object sender, EventArgs e) { strategist.ExecuteMoveDown(); } private void StopButton_Click(object sender, EventArgs e) { strategist.ExecuteStop(); } 描画部分もすっきりさせることができました。 Form1.cs private void pictureBox1_Paint(object sender, PaintEventArgs e) { e.Graphics.DrawImage(cat1.Img, cat1.X, cat1.Y, cat1.Img.Width, cat1.Img.Height); e.Graphics.DrawImage(cat2.Img, cat2.X, cat2.Y, cat2.Img.Width, cat2.Img.Height); e.Graphics.DrawImage(cat3.Img, cat3.X, cat3.Y, cat3.Img.Width, cat3.Img.Height); e.Graphics.DrawImage(cat4.Img, cat4.X, cat4.Y, cat4.Img.Width, cat4.Img.Height); strategist.ExecuteMovement(); pictureBox1.Invalidate(); LeftButton.Refresh(); RightButton.Refresh(); UpButton.Refresh(); DownButton.Refresh(); } 最終的に出来上がったものは最初に紹介したこちらになります。picボタンで動かしたい画像を選び、その画像上下左右に動かすことができます。 さいごに Strategy Patternを勉強してみました。実際にちゃんと実装できたかはわからないですし、改善点も多いと思います。(この場合においてはStrategy patternを使わなくてもcurrentCatをそのまま使ってしまえばやりたいことができてしまう。) それでも今まで書いていたコードよりはきれいになりオブジェクト指向の理解、実装に役立ったと思います。 すべてのクラス変数にget setしてしまっていますがプロパティはよくわかっていないので今後深く勉強したいです。 ソースコードは需要はないとは思いますが、もしあれば上げようと思います。
- 投稿日:2021-10-31T14:54:13+09:00
【Unity】Cinemachine で即座にバーチャルカメラの位置にリセットする
Unity が所持する強力なカメラシステム Cinemachine ですが、位置の初期化でつまづいてしまったので備忘録です。 確認バージョン Unity 2020.3.13f1 Cinemachine 2.6.10 はじめに Cinemachine はカメラ本体とは別にバーチャルカメラという仮想の概念があり、このバーチャルカメラ間をカメラ本体が渡り歩いていくイメージで、効率よく滑らかなカメラワークを実現できます。 ですのでバーチャルカメラをワープ1させたとしても、カメラ本体はそれに遅れる形で滑らかにバーチャルカメラの位置まで移動していきます。 ですが、配置、リスポーン、テレポートなど、補間などせずすぐにバーチャルカメラの位置に合致してほしいシチュエーションはありますよね。 たとえカメラ本体(CinemachineBrain)の transform を直接設定しても、内部の補間計算は続いているため、結局補間途中の位置に戻されてしまいうまくいきません。 今回はカメラ本体をすぐにバーチャルカメラの位置に飛ばすやり方のメモになります。 方法 要するに補間処理を打ち切ってやれば OK でして、やり方はバーチャルカメラの位置を変える処理の直後で CinemachineVirtualCameraBase.PreviousStateIsValid を false に設定するだけです。 _cinemachineVirtualCameraBase.PreviousStateIsValid = false; 私は CinemachineBrain 側からアプローチしたかったので、次のように使いました。 (_cinemachineBrain.ActiveVirtualCamera as CinemachineVirtualCameraBase).PreviousStateIsValid = false; ちょっと名前が直感的でないため意図が読みにくいコードですが、これでやりたいことが達成できましたので、同じようなことでつまづいている方の参考になれば幸いです。 参考 Unity Forum: Proper way to 'reset' follow camera? CinemachineVirtualCamera の Follow や LookAt のターゲットを変える、もしくは transform を直接設定するなど。 ↩