前回まででベクトルや行列を使った動きに関する説明を行いました。ただ、Program.cs にベタ書きされているので、このままプログラムの規模が大きくなっていくと、可動性が著しく劣ることが考えられます。また、今後の拡張も面倒なことになりそうですので、まだ規模か小さい今のうちに、プログラムの構造を見直すことにします


  • Player クラス
現在はゲーム画面内にプレイヤーのみ表示されています。そこで、そのプレイヤーをクラスで管理することにします。プレイヤーと言うぐらいですから、クラス名は Player で良いと思います。ソリューションエクスプローラーのツリー元(今回の例では GameTest と表示されている箇所)にて、右クリック > 追加 > 新しい項目を選択します。
新しい項目の追加
新しい項目の追加ダイアログが開きますので、クラスを選択します。名前を Player.cs として、[追加] ボタンを押します。これで編集エリアに Player.cs が追加されます。
プレイヤークラスが追加された
このクラスではプレイヤーを扱いますので、そのプレイヤー関するプロパティは、全てこちら側に移動させます。まずはそのまま Program.cs 側から移動します。
プレイヤーのプロパティを移動
続いてメソッドも移動します。関係していたのは MovePlayer() と DrawPlayer() ですので、そのまま移動します。あちこちエラーが出ますが、今は無視します。
プレイヤーのメソッドを移動
さて、ここから色々修正していきます。まず、クラスメソッド名ですが、現在は xxxPlayer となっています。今回は Player クラスに移動させましたので、既に対象は Player と分かっています。そのため、メソッド名に関しては動作名だけでよくなります。例えば、MovePlayer() であれば、Move() だけで良いわけですね。リネームしたい範囲を指定して右クリックから名前の変更を選択します。
名称を単純化する
リネームダイアログが表示されますので、MovePlayer を Move と変更して enter キーを押します。また、クラスの動作になりますので static は外します。※後述
ASRock ベアボーンPC DESKMINI B660/B/BB/BOX/JP Intel B660 チップセット 搭載 Intel 第12世代CPU( LGA1700 )対応 【国内正規代理店品】
【楽天】
DeskMini B660/B/BB/BOX/JP
2022-07-15
※ 今、この原稿を書いているPCのベアボーンがこれ。ようやく値段が落ち着いてきてオススメしやすくなりました。超安定してて半年以上使っていますが、一度もフリーズしていません。


  • メンバの静的指定
Main 関数は静的な配置でしたので、そこで使用する全ての関数や変数は、同様に静的指定をする必要がありました。それを Player クラスに移動させたことで、この静的指定は少し考えなければならなくなっています。

クラス内で静的指定すると、それはインスタンス(newで実体化させること)をしなくても、メモリ内に一つだけ常に存在するようになります。静的指定がない場合は、インスタンスされたオブジェクト側に、そのメンバを持つことになります。

例えば、

internal class Test { public static int CntGlobal = 0; public int CntLocal = 0; }
このように静的なメンバと、動的なメンバを持つ Test クラスがあったとします。この時、

Test.CntGlobal = 100;
いきなり静的メンバ変数に値を入れても、これは全く問題ありません。ですが、

Test.CntLocal = 100;
これはエラーになります。理由は CntLocal プロパティはまだ実体化されていないためです。static の指定がないメンバは、new によりインスタンスされないと、実体を持たないので使用できません。

Test sample = new(); sample.CntLocal = 100;
このように sample というオブジェクトとして実体化すれば、そこに含まれるメンバとして、CntLocal プロパティは使用可能になるのです。class 定義は型や構造体の定義と似ています。int = 100; と記述したらエラーになるのと同じです。この場合は int n = 100; とすれば、int の型を持つ n という変数が実体化されますから、その n に対して値を入れることが出来るようになっています。

この使い分けですが、静的メンバは定数定義や全てのオブジェクトから共通で使用する値などに指定すればよいでしょう。たとえば、animeCounter は全て同じタイミングで動かしたければ静的に、それぞれ個別のタイミングで動かしたければ動的に指定すれば良いと思います。


  • アクセサー
プロパティの中には、その時の値を自由に読み出したいが、勝手に書き換えられたら困るみたいな事はよくあります。例えば、座標なんかがそうです。当たり判定を行うためには座標の取得が必須出すから。今までだと、そんな場合は GetLocate() なんていう読み取り専用のメソッドを実装したのですが、C# ではプロパティにアクセサーという便利な指定子があったりしますので活用します。

public PointF Locate;
これだけだと、勝手に位置を書き換えられるので、保守メンテという観点からは大変宜しくありません。そこでアクセサーです。いきなりドンッ!

public PointF Locate { private set; get; }
set;、つまり値を変更する場合は private 指定ですから、外から値の書き換えはできません。ですが、get;、値の参照は特に指定子がないので、全体設定の public が有効ですので、自由に覗き見ることが出来ます。このような使い方は私は多用しています。覚えていて損はないと思います。

あと、プロパティ名は私は基本的には private は小文字から、public は大文字から名前を付けています。例外として、Public に設定したプロパティと同等の重要度である private メンバも大文字から名付けたりしています。ということで、最終的な Player クラスのプロパティがこちら。
プロパティを整形
インテル INTEL CPU RPL-S CoreI5-13500 14/20 4.80GHz 6xx/7xxChipset 国内正規代理店品
【楽天】
【国内正規品】INTEL インテル / Core i5 13500 BOX / 動作クロック周波数:2.5GHz / ソケット形状:LGA1700 / [Corei513500BOX] / 735858528290
2023-01-03
※ 第13世代のインテルCPUです。20コアもあると並列動作がとても強いですね。今、このCPUを私が使っていますが、全く不満がありません。これまた価格がようやく落ち着いてきてて良い感じになっています。


  • エラーを修正する
さて、ここでちょっとあれ?っと思ったエラーがこちら。
プロパティにしたことでエラーが発生
クラスのプロパティとして登録すると、PointF はそれで一つの塊として認識されるので、X,Y それぞれ個別のメンバだけを変更できなくなっちゃうんですよね。ここは正直不便だなあと思ってるところですが、言語仕様なので仕方がありません。書き換えます。
プロパティのエラーを修正する
まとめて代入しか出来ないため、メンバを指定し直した新しい PointF オブジェクトを代入するようにしています。

あと、Draw ですが、表示先は Program.cs が管理していますので、引数として、どこに描画するのかを指定する必要があります。あと、ついでに色も外部から指定できるようにしようかなと。ということで、こんな感じに変更してみました。
表示先と色を外から指定できるように
色の指定がないときは白色をデフォルトにしようと思ったのですが、Color 定数は本当の意味で定数ではなかったらしいので、デフォルト引数ではなく、オーバーロードで対応しています。

あと、終了時のリソースの破棄ですが、プレイヤー画像も Player クラス側に移動してしまったので、そちらで破棄するように書き換える必要があります…が、ここでちょっと一工夫が必要に。単純にデストラクタでリソースの破棄を記述すると、複数インスタンスした後で破棄しようとすると、最初のデストラクタでリソースの破棄が完了しているので、2つ目以降ではエラーになっちゃうのです。

では、破棄したら null にして ?.Dispose(); とすればと思うかもですが、例えば 5個インスタンスして、その後 2個だけオブジェクトを破棄するような使い方をされると、残っているオブジェクトが画像を使おうとして落ちてしまうのです。

そこで静的メンバでインスタンス数をカウントして、ゼロになったら初めて画像を破棄する処理を実装します。まずはプロパティです。

private static int numInstances = 0; // インスタンス数
コンストラクタで値を加算します。

public Player() { ++numInstances; }
さて、データの破棄が必要なクラスですので、クラス定義を IDisposable に指定します。

internal class Player : IDisposable
そして、Dispose() メンバを追加して、そこに破棄の処理を実装するようにします。

public void Dispose() { --numInstances; if (numInstances > 0) return; foreach (Bitmap bm in bmps) bm.Dispose(); }
KIOXIA EXCERIA PLUS G3 NVMe M.2 PCIe4x4 NVMe Type2280 1TB
※まあ安い。とにかく安い。あの高品質なキオクシアの Gen4 SSD で1万円。もはやこれを選ばない理由がないぐらいに素晴らしい!


  • Program 側から Player を使う
Player クラスの構築ができましたので、あとは Program 側からどう使うかです。まずはプロパティとして Player クラスのオブジェクトを追加します。

private static Player player;
この player オブジェクトはコンストラクタで実体化します。Player クラスのコンストラクタには初期位置を指定する変更をしています。こうしないと、画面のどこに最初は登場させればよいかわからないですからね…

static Program() { player = new(new(SCR_WIDTH / 2.0f, SCR_HEIGHT / 2.0f)); }
オブジェクトを生成したら最後は破棄が必要ですので、先に Dispose にプレイヤーオブジェクトの破棄を記述しておきます。

static void Dispose() { foreach (Bitmap bm in bmBuffer) bm.Dispose(); player.Dispose(); }
Program クラスは static 指定なので、Dispose も static なんです。これで、player を使う準備ができました。あとはメインループに処理を記述します。

// プレイヤーを移動する ClearBackBuffer(); player.Move(); player.Draw(BackBuffer, GetPlayerColor(player)); if (player.IsMove) { Player.UpdateAnimation(); }
まず最初に、BackBuffer を初期化します。BackBuffer は Program 側が管理していますので、こちら側で消す必要があります。続いて player.Move(); を呼び出して、プレイヤーを移動させます。続いて player.Draw(BackBuffer, GetPlayerColor(player)); として表示させています。第一引数はどこに表示させるかです。第二引数のプレイヤー色は GetPlayerColor(player) の返却値として受け取っています。このメソッドは以下の様になっています。

private static Color GetPlayerColor(Player player) { Color newColor = Color.FromArgb( red: (int)(MathF.Sin(drawCounter / 5f) * 127 + 128), green: 64, blue: 255); return player.IsMove ? Color.White : newColor; }
以前ベタ書きにしていた部分を、メソッドとして分離させただけです。


  • player を複数登場させる
これで Player クラスの作成と使用は完了です。クラス化したので、いとも簡単に複数同時表示に変更することが出来ます。試してみますね。まずはプロパティ…

private static Player[] players;
複数インスタンスのため配列にしてみました。次にコンストラクタで実体化します。

players = new Player[] { new(new PointF(SCR_WIDTH / 2.0f, SCR_HEIGHT / 2.0f)), new(new PointF(SCR_WIDTH * 2 / 3.0f, SCR_HEIGHT / 2.0f)), new(new PointF(SCR_WIDTH * 1 / 3.0f, SCR_HEIGHT / 2.0f)), };
とりあえず3つ。初期位置は横並びとしてみました。リソース開放も変更します。

static void Dispose() { foreach (Bitmap bm in bmBuffer) { bm.Dispose(); } foreach (Player player in players) { player.Dispose(); } }
最後にメインループ。

foreach (Player player in players) { player.Move(); player.Draw(BackBuffer, GetPlayerColor(player)); } if (players[0].IsMove) { Player.UpdateAnimation(); }
ループ毎に UpdateAnimation は一度だけの呼び出しになりますので、そこでちょっと注意ですね。この結果、以下のように動作します。



クラスでオブジェクトを表現する記法は、オブジェクト指向と呼ばれています。オブジェクト指向については下記の記事を参照してください。



今回説明したプログラムリストは下記からダウンロードできます。

2023.11.30.2200

※ 1TB SSD を使った USBメモリがこれまた安い。これくらいのサイズだと無くしにくいので、大事なデータ入れるならやっぱりある程度のサイズは欲しいところ…ですが、これ、十分小さいと思いますよね?