Blazor WebAssembly で "snow catch" ゲームを作ってみる (後編)

# "Snow Catch" ゲームを作ろう - 前回までのあらまし

前回記事、**クリスマス感溢れる** "Snow Catch" という簡易ゲーム (下図) を、Blazor WebAssembly として再実装してみる[「Blazor WebAssembly で "snow catch" ゲームを作ってみる (前編)」](https://qiita.com/jsakamoto/items/8214211c1fd2ff91f46b)の続きです。

![movie-001.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/c25ee244-b322-c691-c5ea-1f2180e1ff11.gif)

前回までで、

- ゲームコンテキストクラスの作成
- ゲームエリアのサイズ情報の作成と表示
- 雪の画像の位置情報の作成と表示
- タイマーによる表示の更新

を実装し、雪の画像がランダムな位置から降ってくるところまで出来上がったのでした。

![movie-003.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/1167f257-3474-ec33-79b2-fe87e2d67a79.gif)

引き続き後編にて、"Snow Catch" ゲームを完成に向けて進めていきます。

# 雪だるまの、サイズと位置を実装する

前編で雪が降ってくるところまでは実装できたので、後編は、まずは雪だるまにとりかかります。

ゲームコンテキスト上で雪だるまサイズとの表示位置を表現し、それを HTML 上に表示反映するようにします。

ゲームコンテキストクラスに、雪だるま画像の表示位置とサイズを示す、Rectangle 型のフィールド変数を、SnowManRectangle という名前で追加します。
雪だるま画像のサイズは、幅 64 x 高さ 64 としました。
フィールド変数定義時点では、とりあえず表示位置は x 座標 = 0, y 座標 = 0 としておきます。

```csharp:GameContext.cs
...
public class GameContext
{
    ...
    // 👇 下記の行を追加
    public Rectangle SnowManRect = new(x: 0, y: 0, width: 64, height: 64);
    ...
```

そして、雪だるま画像を、ゲームエリアの底辺、左右は中央に初期表示されるよう、座標値をリセットするメソッドを、ゲームコンテキストクラスに書き足します。

```csharp:GameContext.cs
...
public class GameContext
{
    ...
    // 👇 このメソッドを追加
    private void ResetSnowManPos()
    {
        // 左右方向はゲームエリアの中央に、
        this.SnowManRect.X = (this.GameAreaSize.Width - this.SnowManRect.Width) / 2;
        // 上下方向はゲームエリア底辺に、雪だるまの座標を設定する
        this.SnowManRect.Y = this.GameAreaSize.Height - this.SnowManRect.Height;
    }
}
```

雪だるま画像の表示位置をリセットするメソッドを追記したら、それをゲームコンテキストクラスのコンストラクターで実行して、ゲーム開始時に雪だるま画像の表示位置が下辺中央に初期化されるようにします。

```csharp:GameContext.cs
...
public class GameContext
{
    ...
    public GameContext()
    {
        this.ResetSnowFlakePos();

        // 👇 ゲームコンテキストのコンストラクタ内に、下記行を追加
        this.ResetSnowManPos();
        ...
}
```

# 雪だるま画像を表示する

ゲームコンテキスト上における、雪だるま画像のサイズと位置の表現は実装できましたので、次はこの雪だるま画像を HTML 上に表示反映させます。

雪の画像のときと同じく、雪だるま画像も作成して、`wwwroot` フォルダ以下に配置します。
自分は `wwwroot/assets/snowman.svg` という SVG 画像を作成・配置しました。

そしてこの雪だるまの画像を、雪の画像のときと同じく、ゲームエリアの `<div>` 要素内に `<img>` 要素としてマークアップします。
このときに、ゲームコンテキストのフィールドを参照して、雪だるま画像の表示位置とサイズを CSS スタイル指定にバインドします。

```html:App.razor
<div... >
    ...
    <!-- 👇 ゲームエリアの div 要素内に、下記 img 要素を追記する -->
    <!-- Snow Man -->
    <img src="assets/snowman.svg"
        style="left:@(Context.SnowManRect.X)px; top:@(Context.SnowManRect.Y)px; width:@(Context.SnowManRect.Width)px; height:@(Context.SnowManRect.Height); position:absolute;" />
    ...
```
以上で Web ブラウザで再読込を実行すると、雪だるま画像も表示されるようになりました。
![movie-004.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/2ecc9185-09c6-0ec4-dc34-490e2eeb7b5b.gif)

# キーボード操作を可能にする

引き続き、雪だるまの画像を、キーボードの左右のカーソルキーで左右に動かせるようにする必要があります。
そのため、この Blazor アプリケーションに ["Blazor HotKeys"](https://www.nuget.org/packages/Toolbelt.Blazor.HotKeys/) NuGet パッケージ参照を追加し、"Blazor HotKeys" サービスを利用して、キーボードの左右のカーソルキーの打鍵をハンドルできるようにします。

プロジェクトファイル (`SnowCatch.csproj`) があるフォルダをカレントフォルダとして、ターミナル (コマンドプロンプト) から下記のとおり NuGet パッケージ参照の追加のコマンドを実行します。

```shell
$ dotnet add package Toolbelt.Blazor.HotKeys
```

"Blazor HotKeys" NuGet パッケージ参照を追加したら、`HotKeys` サービスをこの Blazor アプリに登録します。
プロジェクトフォルダにある `Program.cs` ファイルをテキストエディタで開き、以下のように `HotKeys` サービスの登録処理を追加します。

```csharp:Program.cs
...
// 👇 この名前空間の利用宣言を追加
using Toolbelt.Blazor.Extensions.DependencyInjection;

namespace SnowCatch
{
    public class Program
    {
        public static async Task Main(string[] args)
        {
            ....
            builder.Services.AddScoped(sp => new HttpClient {...});
            // 👇 この行を追加して、"HotKeys" サービスを登録
            builder.Services.AddHotKeys();
            ...
```

`HotKeys` サービスの登録を済ませたら、次は、`App.razor` 側で `HotKeys` サービスを入手します。
`App.razor` を編集し、`App.razor` の先頭行に以下のように追記し、HotKeys サービスを `@inject` 構文で同名のプロパティ変数として取得します。

```csharp:App.razor
@* 👇 以下の2行を追加 *@
@using Toolbelt.Blazor.HotKeys
@inject HotKeys HotKeys
...
```
`HotKeys` サービスオブジェクトが入手できましたので、`App.razor` コンポーネント初期化のタイミング (`OnInitialized()` メソッド内) で `HotKeys` サービスを介して、キーボード入力を捕捉する `HotKeysContext` を作成します。
そしてその作成された `HotKeysContect` オブジェクトに対して、打鍵されたキーに対応するコールバック関数を登録します。

作成した `HotKeysContext` オブジェクトは明示的に破棄する必要があるため、`App.razor` コンポーネントのフィールド変数を追加して収録するようにします。

```csharp:App.razor
...
@code {
    ...
    // 👇 このフィールド変数を追加
    private HotKeysContext HotKeysContext;

    protected override void OnInitialized()
    {
        ...
        // 👇 以下、HotKeysContext の作成と、
        this.HotKeysContext = this.HotKeys.CreateContext();

        // 👇 左右のカーソルキーの打鍵で呼び出すコールバックの登録
        this.HotKeysContext.Add(ModKeys.None, Keys.Left, () => {
          /* 左カーソルキーが押されたらここが実行される */
        });
        this.HotKeysContext.Add(ModKeys.None, Keys.Right, () => {
          /* 右カーソルキーが押されたらここが実行される */
        });
    }
    ...
```

今回の Blazor アプリでは、`App.razor` コンポーネントがひとたび表示されたら、以後は表示内容は更新していきますが、`App.razor` コンポーネントそのものが (ブラウザを閉じるまで) 破棄されることはありません。

とはいえ、今後の応用のこともありますので、ここは丁寧に、`App.razor` コンポーネントが破棄されるときには、作成した `HotKeysContext` も破棄するように実装しておきます。

Blazor のコンポーネントが破棄されるときの処理を実装するには、そのコンポーネントで `IDisposable` インターフェースの実装を宣言し、同インターフェースで宣言される `void Dispose()` メソッドを実装します。

`App.razor` ファイルの先頭に戻り、`IDisposable` インターフェースの実装を宣言します。

```csharp:App.razor
@using Toolbelt.Blazor.HotKeys
@inject HotKeys HotKeys
@* 👇 下記行を追加 *@
@implements IDisposable
...
```

続けてコードブロックに戻り、`void Dispose()` メソッドを実装します。
この `Dispose()` メソッドの中で、作成した `HotKeysContext` オブジェクト (実はこの `HotKeysContext` クラスも `IDisposable` インターフェースを実装しています) の破棄を行ないます。

また、実は今までサボっていたのですが、本来であれば、ゲームコンテキストクラスのタイマイベントの捕捉も、コンポーネント破棄のタイミングで切り離すべきでした。
`Dispose()` メソッドを実装するこの機会に、タイマイベント捕捉の切り離しもついでに書き足しておきます。

```csharp:App.razor
@code {
    ...
    // 👇 IDisposable インターフェースで宣言される、Dispose() メソッドを追加する
    public void Dispose()
    {
        // 👇 ゲームコンテキストのタイマイベントの捕捉を切り離し、
        this.Context.GameLoopTimer.Elapsed -= GameLoopTimer_Elapsed;
        // 👇 HotKeysContext オブジェクトも破棄する
        this.HotKeysContext.Dispose();
    }
}
```

# 雪だるまの画像を左右に操作できるようにする

ここまでで、ゲームのプレイヤーによるキーボード打鍵に対して処理を行えるように仕込みはできました。

続けて、雪だるまの画像を左右に操作できるようにつなげていきます。

まずはゲームコンテキストクラスにて、雪だるまの左右の座標値を変動させるためのメソッドを追加します。
左方向への移動用と、右方向への移動用に、それぞれ別々のメソッドで作ってみます。
一回のメソッド呼び出しで、雪だるまの画像の左右位置が 5 ずつ変動するようにしてみました。

```csharp:GameContext.cs
...
public class GameContext
{
    ...
    // 👇 雪だるまを左へ 5 移動するメソッドを追加
    public void MoveSnowManToLeft()
    {
        // 但しゲームエリアの左端を越えないようにする 👇
        this.SnowManRect.X = Math.Max(0, this.SnowManRect.X - 5);
    }

    // 👇 雪だるまを右へ 5 移動するメソッドを追加
    public void MoveSnowManToRight()
    {
        // 但しゲームエリアの右端を越えないようにする 👇
        var maxSnowManX = this.GameAreaSize.Width - this.SnowManRect.Width;
        this.SnowManRect.X = Math.Min(this.SnowManRect.X + 5, maxSnowManX);
    }
}
```

こうして、雪だるま画像の左右表示位置を変更するメソッドができましたので、`App.razor` の編集に戻り、先ほど `HotKeysContext` オブジェクトに登録したキーボード打鍵によるコールバックにて、雪だるま画像の左右表示位置を変更するメソッドを呼び出すようにします。

```csharp:App.razor
...
@code {
    ...
    protected override void OnInitialized()
    {
        ...
        this.HotKeysContext.Add(ModKeys.None, Keys.Left, () => {
          /* 左カーソルキーが押されたらここが実行される */
          // 👇 雪だるまを左に移動させるメソッド呼び出しを追加
          this.Context.MoveSnowManToLeft();
        });
        this.HotKeysContext.Add(ModKeys.None, Keys.Right, () => {
          /* 右カーソルキーが押されたらここが実行される */
          // 👇 雪だるまを右に移動させるメソッド呼び出しを追加
          this.Context.MoveSnowManToRight();
        });
    }
    ...
```
以上で Web ブラウザにて再読み込みを実行すると、キーボードの左右のカーソルキーの打鍵で、雪だるまの画像が左右に操作できるようになりました。
![movie-005.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/681b2b89-3ef4-a4fb-3929-7b13977aa567.gif)

# スコアと当たり判定

仕上げとして、スコア (点数) を表示するようにして、雪だるまが雪をキャッチしたらスコアに 1 点ずつ加点するようにします。

はじめに、ゲームコンテキストクラスにスコア (点数) を覚えておくためのフィールド変数を追加します。

```csharp:GameContext.cs
...
public class GameContext
{
    ...
    // 👇 下記フィールド変数を追加
    public int Score;
    ...
```

次に、ゲームコンテキストクラス内の、タイマー発火ごとの処理にて、雪の画像と雪だるまの画像との当たり判定を実装します。

雪の画像、および雪だるま画像の表示位置は、ゲームコンテキストクラスのフィールド変数 `SnowFlakeRect` と `SnowManRect` で把握できています。
これら 2 つの `Rectangle` 構造体に収録の X, Y 座標値から、雪と雪だるまの距離が割り出せます。
この両画像間の距離が、両画像のサイズを勘案した一定数より近くなったら "当たり" と判断します。

"当たり" と判定されたら、スコアに 1 加算し、その時点で雪の画像の位置はリセットします。


```csharp:GameContext.cs
    ...
public class GameContext
{
    ...
    private void GameLoopTimer_Elapsed(object sender, EventArgs e)
    {
        ...
        // 👇 雪と雪だるまとの距離を求める計算を追加
        var deltaH = this.SnowManRect.X - this.SnowFlakeRect.X;
        var deltaV = this.SnowManRect.Y - this.SnowFlakeRect.Y;
        var distance = Math.Sqrt(Math.Pow(deltaH, 2) + Math.Pow(deltaV, 2));

        // 👇 雪と雪だるまとの距離が一定数より近くなったら "当たり" とする判断を追加
        if (distance < (this.SnowManRect.Height + this.SnowFlakeRect.Height) / 2 * 0.6)
        {
            // 当たりと判定されたら、雪の位置をリセットして、
            ResetSnowFlakePos();
            // スコアに 1 加算
            this.Score++;
        }
    }
}
```

最後に `App.razor` の編集に戻り、ゲームコンテキストクラスに追加された、スコアのフィールド変数 (`Score`) を HTML 上に表示するようマークアップします。

```html:App.razor
...
<div ...>
    <!-- 👇 ゲームエリアの div 要素内に下記スコア表示用の div 要素をマークアップ -->
    <!-- Score -->
    <div style="color:#0094ff; position:absolute; top:10px; right:10px; font-size:24px; font-weight:bold;">
        <!-- 👇 ゲームコンテキストのスコアのフィールド変数をバインド -->
        @Context.Score
    </div>
    ...
``` 
これでブラウザを再読込してみると、当たり判定とスコア加算も動作するようになりました。
![movie-001.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/c25ee244-b322-c691-c5ea-1f2180e1ff11.gif)
以上でゴールとなります。
おつかれさまでした! 🎉

# ソースコード一式

以上、今回の "snow catch" ゲームを実装したソースコード一式は、下記 GitHub リポジトリに公開してあります。

- https://github.com/sample-by-jsakamoto/SnowCatch-on-BlazorWasm

また実装した "snow catch" ゲームを、過去記事[「Blazor WebAssembly アプリの GitHub Pages への発行を、より楽にする」](https://qiita.com/jsakamoto/items/0be0005d9b30acbfe1e4) の方式で GitHub Pages 上に発行してあるので、下記 URL から遊べます。

- https://sample-by-jsakamoto.github.io/SnowCatch-on-BlazorWasm/

# おわりに

この "Snow Catch" ゲームを Blazor WebAssembly で再実装したのには、深い理由や利点があったわけではありません。 😅
実際のところ、このような "ゲーム" を作るなら、Blazor よりも、[Scratch](https://scratch.mit.edu/) などのほうが向いていることでしょう。
ただ、**Blazor によるプログラミングの楽しみ・息抜き**として、こんなペライチの簡易ゲームを作ってみた次第です。

なお、ご覧のとおり、本当に基本的な作り込みしかしていません。
ゲームオーバーの定義もありませんし、ゲームの開始・中止もありません。
ブラウザでロードしたらいきなり動き始めるていたらくです。

**いろいろと改善・拡張のしがいがある**と思いますので、皆さんオリジナルの "Snow Catch" ゲームに作り替えるのもよいかもです。
はじめは、画像を差し替えたり、座標やサイズなどの数値をいじってみるだけでも面白いかも知れませんね。

また、この "Snow Catch" ゲームの開発にあたっては、簡易に・素早く実装することが主要目標にありました。
そのため、実装のスタイルとしては、いろいろと**ベストプラクティスに反して**ばかりいます。

フィールド変数にこまめに `readonly` 付けてなかったり、null 許容参照型の機能を有効にしていなかったり、CSS スタイル指定をすべてインラインで指定していたり、単体テストがなくテスト駆動開発をしていなかったり...

そもそも Blazor で組んでいるのに、コンポーネントは App.razor ひとつだけですw
いちおうゲームコンテキストこそ設けましたが、雪や雪だるまのモデリングは只の座標情報でしかなく、ゲームコンテキストがすべてを仕切る "神" クラスになっているのも、本当はよくないことでしょう。

ようするに、**近代プログラミングとしては、かなり雑な造り**ですので、その点はくれぐれも承知ください。
とはいえ、こんな感じで、手抜きもしつつ勢いでミニゲームを作ってみるのも、それはそれで**プログラミングの楽しみ**がありますよね! 😊

_**Happy Holiday**, and Learn, Practice, Share!_