LoginSignup
4
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-23

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

前回記事、クリスマス感溢れる "Snow Catch" という簡易ゲーム (下図) を、Blazor WebAssembly として再実装してみる「Blazor WebAssembly で "snow catch" ゲームを作ってみる (前編)」の続きです。

movie-001.gif

前回までで、

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

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

movie-003.gif

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

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

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

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

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

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

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

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;
    }
}

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

GameContext.cs
...
public class GameContext
{
    ...
    public GameContext()
    {
        this.ResetSnowFlakePos();

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

雪だるま画像を表示する

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

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

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

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

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

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

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

$ dotnet add package Toolbelt.Blazor.HotKeys

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

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 構文で同名のプロパティ変数として取得します。

App.razor
@* 👇 以下の2行を追加 *@
@using Toolbelt.Blazor.HotKeys
@inject HotKeys HotKeys
...

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

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

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 インターフェースの実装を宣言します。

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

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

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

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

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

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

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

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

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 オブジェクトに登録したキーボード打鍵によるコールバックにて、雪だるま画像の左右表示位置を変更するメソッドを呼び出すようにします。

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

スコアと当たり判定

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

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

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

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

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

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

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 上に表示するようマークアップします。

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
以上でゴールとなります。
おつかれさまでした! 🎉

ソースコード一式

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

また実装した "snow catch" ゲームを、過去記事「Blazor WebAssembly アプリの GitHub Pages への発行を、より楽にする」 の方式で GitHub Pages 上に発行してあるので、下記 URL から遊べます。

おわりに

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

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

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

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

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

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

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

Happy Holiday, and Learn, Practice, Share!

4
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
2