はじめに
この記事では 「たった1時間」 で Webゲーム作りを体験できます。
🎯こんな方へお勧め
- 普段はC#を書いているけど Webフロントはちょっと苦手…
- Webアプリを作ったことはあるけど Blazorは触ったことがない
- JavaScriptやAJAXを最小限に と思っている
HTMLやCSSは最低限だけ触れますが、深い知識は不要です。
C#が書ければOK! 👍
🔷 ちなみに Blazor は Microsoft が開発する最新の Web フレームワークです。
「サーバーサイドC#」だけでリッチなWeb UIを構築できます。
これから作るゲームのイメージ
実際に遊んでみるのが一番わかりやすいです。
是非ご自分で操作してみてください!
1.Visual Studioで新しいプロジェクトを作成する
プロジェクト名を入力します( ここでは HakoiriMusume とします )
重要な設定 | 設定値 | 備考 |
---|---|---|
認証の種類 | なし | 今回はログイン不要 |
インタラクティビティ型 | サーバ | ★ めちゃくちゃ重要 |
インタラクティビティ場所 | グローバル |
2.プロジェクトの構成を知る
標準的なプロジェクト構成
ディレクトリ | 内容 |
---|---|
wwwroot | cssなど |
Components/Pages | 各Webページ |
沢山のフォルダやファイルがありますが、これから触るのは
「Components/Pages/Home.razor」のみです。
Home.razor
最初に実行したときに表示されたページのソースです
@page "/"
<PageTitle>Home</PageTitle>
<h1>Hello, world!</h1>
Welcome to your new app.
3.HTMLの中にC#が書けるBlazor
Home.razorを修正します
@page "/"
@for (int i = 1; i < 4; i++)
{
<p>ループの @i 回目</p>
}
htmlの中に
@for (int i = 1; i < 4; i++)
と、突然C#のコードが出てきました。
※ HTMLの中に「@」でC#を自由に記述することができます。
(GO言語やJSP、PHPなどと同じですね)
🔷 htmlの<p>
タグの中の @i
でC#の変数を利用しているのも注目です。
<p>ループの @i 回目</p>
4.ゲームの盤面を作る
まずは駒の背景の盤面を作ります
<style>
でゲーム盤の見た目を定義してます。
(一般的にはCSSファイルへ分離しますがここでは見やすさを優先します。)
HTMLは「何を表示するか」、<style>
が「装飾」を担当するイメージです。
CSS(<style>
)の詳しい知識はなくても大丈夫!
見た目を装飾しているんだと思ってください。
@page "/"
<style>
.board {
display: grid;
grid-template-rows: repeat(7, 50px);
grid-template-columns: repeat(8, 50px);
position: relative;
}
</style>
<div class="board">
@for (int row = 0; row < 7; row++)
{
for (int col = 0; col < 8; col++)
{
// CSSのグリッドは1オリジン
var cssRow = row + 1;
var cssCol = col + 1;
<div class="cell" style="grid-area:@(cssRow) / @(cssCol);">
□
</div>
}
}
</div>
簡素過ぎるので、各セルを四角形で描画するように cellのstyleを設定します。
.cell {
border: 1px solid #444;
display: flex;
justify-content: center;
align-items: center;
}
7×8を変数で宣言するように変更します。
<div class="board">
@for (int row = 0; row < MaxRows; row++)
{
for (int col = 0; col < MaxCols; col++)
{
// CSSのグリッドは1オリジン
var cssRow = row + 1;
var cssCol = col + 1;
<div class="cell" style="grid-area:@(cssRow) / @(cssCol);">
□
</div>
}
}
</div>
@code {
private const int MaxRows = 7;
private const int MaxCols = 8;
}
-
@code
でC#のコードをまとめて書くことができます -
@code
で宣言した変数MaxRows
を htmlの中のfor
文で利用してます
5.ゲームの盤面を作る(その2)
単純な7×8から、壁や出口を定義できるようにします。
@code {
// 盤面の定義
private const int MaxRows = 7;
private const int MaxCols = 8;
private int[,] Board = new int[MaxRows, MaxCols]
{
// 盤面定義 (0=空, 1=壁, 9=出口)
{1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,1,1,9,9,1,1,1},
};
}
htmlの表示を定義に対応させます
<div class="board">
@for (int row = 0; row < MaxRows; row++)
{
for (int col = 0; col < MaxCols; col++)
{
// CSSのグリッドは1オリジン
var cssRow = row + 1;
var cssCol = col + 1;
int cell = Board[row, col];
switch (cell)
{
case 0: // 空
<div class="cell" style="grid-area:@(cssRow) / @(cssCol);">
□
</div>
break;
case 1: // 壁
<div class="cell" style="grid-area:@(cssRow) / @(cssCol);">
■
</div>
break;
case 9: // 玄関
<div class="cell" style="grid-area:@(cssRow) / @(cssCol);">
玄関
</div>
break;
}
}
}
</div>
(まあまあ盤面ぽく成ってきました)
styleの定義を強化してもっと見た目を良くします
セルの背景を塗りつぶして盤面を表現します
<style>
/* ゲーム盤全体 */
.board {
display: grid;
grid-template-rows: repeat(7, 50px);
grid-template-columns: repeat(8, 50px);
position: relative;
}
/* 1つ1つのセルの書式 */
.cell {
border: 1px solid #444;
display: flex;
justify-content: center;
align-items: center;
}
/* 壁 */
.wall {
background: #444;
}
/* 盤面 */
.empty {
background: #aaa;
}
/* 玄関 */
.exit {
background: #eee;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
</style>
switch (cell)
{
case 0: // 空
<div class="cell empty" style="grid-area:@(cssRow) / @(cssCol);">
</div>
break;
case 1: // 壁
<div class="cell wall" style="grid-area:@(cssRow) / @(cssCol);">
</div>
break;
case 9: // 玄関
<div class="cell exit" style="grid-area:@(cssRow) / @(cssCol);">
玄関
</div>
break;
}
これで盤面が完成しました!
次はいよいよ駒を置いていきます
6.駒を置く - C#クラスをHTMLに描画してみよう
盤面はhtmlの中に固定で定義しました。
ページを表示するたびに必ず描画されます。
駒は固定では困るので盤面とは別に描画します。
イベントで駒を置く
まずはページ表示時の「イベント OnInitialized
で駒を置きます。
@code {
// 駒の定義
private class Piece
{
public int Row { get; set; }
public int Col { get; set; }
public string? Label { get; set; }
}
// 駒のリスト(行・列・表示文字)
private List<Piece> Pieces
= new List<Piece>();
protected override void OnInitialized()
{
// 初期配置
Pieces.Add(new Piece { Row = 2, Col = 3, Label = "娘" });
}
}
C#で駒のクラスを定義したので、HTMLで描画します。
@foreach (Piece p in Pieces)
{
var cssPieceRow = p.Row + 1;
var cssPieceCol = p.Col + 1;
<div class="piece"
style="
grid-row:@cssPieceRow;
grid-column:@cssPieceCol;">
@p.Label
</div>
}
スタイルで駒の見た目を装飾します
/* 駒 */
.piece {
background: red;
color: white;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
行2,列3(0オリジン)に駒「娘」が配置されました。
少しソースが複雑になってきたので、ここで全体像を一度確認します。
今、ソース全体はこうなってます。
============================
ソースコード全体を表示(折りたたみ)
=============================
@page "/"
<style>
/* ゲーム盤全体 */
.board {
display: grid;
grid-template-rows: repeat(7, 50px);
grid-template-columns: repeat(8, 50px);
position: relative;
}
/* 1つ1つのセルの書式 */
.cell {
border: 1px solid #444;
display: flex;
justify-content: center;
align-items: center;
}
/* 壁 */
.wall {
background: #444;
}
/* 盤面 */
.empty {
background: #aaa;
}
/* 玄関 */
.exit {
background: #eee;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
/* 駒 */
.piece {
background: red;
color: white;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
}
</style>
<div class="board">
@for (int row = 0; row < MaxRows; row++)
{
for (int col = 0; col < MaxCols; col++)
{
// CSSのグリッドは1オリジン
var cssRow = row + 1;
var cssCol = col + 1;
int cell = Board[row, col];
switch (cell)
{
case 0: // 空
<div class="cell empty" style="grid-area:@(cssRow) / @(cssCol);">
</div>
break;
case 1: // 壁
<div class="cell wall" style="grid-area:@(cssRow) / @(cssCol);">
</div>
break;
case 9: // 玄関
<div class="cell exit" style="grid-area:@(cssRow) / @(cssCol);">
玄関
</div>
break;
}
}
}
@foreach (Piece p in Pieces)
{
var cssPieceRow = p.Row + 1;
var cssPieceCol = p.Col + 1;
<div class="piece"
style="
grid-row:@cssPieceRow;
grid-column:@cssPieceCol;">
@p.Label
</div>
}
</div>
@code {
// 盤面の定義
private const int MaxRows = 7;
private const int MaxCols = 8;
private int[,] Board = new int[MaxRows, MaxCols]
{
// 盤面定義 (0=空, 1=壁, 9=出口)
{1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,1,1,9,9,1,1,1},
};
// 駒の定義
private class Piece
{
public int Row { get; set; }
public int Col { get; set; }
public string? Label { get; set; }
}
// 駒のリスト(行・列・表示文字)
private List<Piece> Pieces
= new List<Piece>();
protected override void OnInitialized()
{
// 初期配置
Pieces.Add(new Piece { Row = 2, Col = 3, Label = "娘" });
}
}
7.駒を選択する - Blazorでイベント処理を体験
いよいよここからがBlazorの本領発揮です!
駒を動かすためには、まずは駒を選択する必要があります。
また盤面の中で選択中の駒は1つだけじゃないと、どの駒を動かすのか困ります。
@code
の中に 下の2つを追加します。
- 選択中の駒を示す
Selected
変数 - 選択された時のイベント
SelectPiece(p)
// 選択中の駒
private Piece? Selected;
// 駒の選択
private void SelectPiece(Piece p) => Selected = p;
駒のエレメントにクリックされた時のアクション @onclick
と、自身が選択された駒の場合だけ"selected"クラスを指定します
<div class="piece @(p == Selected ? "selected" : "")"
style="
grid-row:@cssPieceRow;
grid-column:@cssPieceCol;"
@onclick="() => SelectPiece(p)">
@p.Label
</div>
最後に <style>
へ選択された時の見た目の書式を追加します。
.piece.selected {
outline: 5px solid orange;
}
「父」をクリックします
( 「娘」の選択が自動で解除されました )
C#を書きなれている方には自然に見えたと思います。
でもこれは、通常のWeb開発では絶対にありえないことなんです。
本来なら、セルの見た目を変えるだけでも
- JavaScriptでページを直接書き換える
- もしくはAJAXでサーバーと通信し、ページの一部分を書き換える
といった手間が必要でした。
それがBlazorでは @onclick
を1行追加するだけ。
JavaScriptもAJAXも不要で、C#のコードだけでページが動的に変化します。
これこそがBlazorのすごさであり、最大の魅力です!
8.Blazor Serverの通信(技術的な背景)
かなり通信技術に踏み込んだ話なので、興味のある方だけどうぞ
=======================
技術的な背景を表示(折りたたみ)
========================
HTTPリクエストもjavascriptも無しに、いったいどうやってページの内容が書き換わったんでしょう?
実は、Blazor Serverでは WebSocket という仕組みで通信しています。
❓このWebSocketとはなんでしょう?信頼できる技術なんでしょうか?
WebSockets(ウェブソケット)は、WebブラウザとWebサーバー間でリアルタイムに双方向通信を可能にする通信プロトコルです。一度接続が確立されると、HTTPのような都度のリクエスト・レスポンスを必要とせず、データのやり取りが継続的に行われるため、チャット、オンラインゲーム、データストリーミングなどのリアルタイム性が求められるアプリケーションで効率的に利用されます。
https://ja.wikipedia.org/wiki/WebSocket
📶 通信内容を確認しよう
実行してブラウザの開発者ツールを表示します。(Chromeなら「F12」です)
ネットワークタブを選択して「F5」でページをGETし直します。
Name=_blazor?id=.... Type=websocketの通信があります
「娘」をクリックした瞬間(時刻 06:03:02.017)にサーバーへ送信されたメッセージが確認できます。
バイナリですが BeginInvokeDotNetFromJS... という文字列が含まれており、
Blazorの @onclick
イベントがサーバーへ送られていることがわかります。
👉 つまり Blazor Server では、ブラウザでのクリックが オンラインゲームと同じ技術 WebSocketでサーバーに即時送信され、サーバーで処理された結果が再びWebSocket経由で返ってきて、画面が更新される仕組みになっています。
これこそが「AJAXの記述なしで動的にページが変化する」秘密です。
9.選択した駒を動かす
駒のアクション @onclick
をドラッグ用に @onmousedown
へ変更します。
ドラッグの移動検出用に座標も同時に取得します。
<div class="piece @(p == Selected ? "selected" : "")"
style="
grid-row:@cssPieceRow;
grid-column:@cssPieceCol;"
@onmousedown="(e) => StartDrag(p, e.ClientX, e.ClientY)">
@p.Label
</div>
ドラッグ開始イベントを記述します。
// ドラッグ開始
private void StartDrag(Piece p, double clientX, double clientY)
{
Selected = p;
startX = (int)clientX;
startY = (int)clientY;
}
ドラッグ移動イベントを記述します。
// ドラッグ移動
private void DragMove(MouseEventArgs e)
{
if (Selected == null) return;
// マウス移動量(px単位)
int currentX = (int)e.ClientX;
int currentY = (int)e.ClientY;
const int cellSize = 50; // 1駒のサイズ
int threshold = cellSize / 2; // 50pxの半分
// 1駒の半分以上ドラッグさせたら、移動させる
int rowStep = 0, colStep = 0;
if (Math.Abs(currentX - startX) > threshold)
{
colStep = (currentX > startX) ? 1 : -1;
startX += colStep * cellSize;
}
if (Math.Abs(currentY - startY) > threshold)
{
rowStep = (currentY > startY) ? 1 : -1;
startY += rowStep * cellSize;
}
// 次の位置
int newRow = Selected.Row + rowStep;
int newCol = Selected.Col + colStep;
// 駒位置の移動
Selected.Row = newRow;
Selected.Col = newCol;
}
ドラッグの終了イベントを記述します。
// ドラッグ終了
private void EndDrag()
{
Selected = null;
}
駒をドラッグしやすいようにスタイルを追加します
.piece {
background: red;
color: white;
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
+ cursor: pointer;
+ user-select: none;
+ touch-action: none;
}
ドラッグ移動イベントは、ドラッグ開始と同様に「駒」に書きたいところですがそれだとうまくいきません。
「駒」の範囲を超えてマウスを移動させたときに「駒」を移動させるので、「駒」の範囲外でもイベントを取れる必要があります。
そこで、盤面 <div class="board">
エレメントへ、ドラッグ移動イベントの呼び出しを記述します。
(ドラッグの終了も同様です)
<div class="board"
@onmousemove="(e) => DragMove(e)"
@onmouseup="EndDrag">
動作確認します。
無事に駒が動きました!
10.仕上げる
駒は動くようになりましたがまだまだ不完全です。
❌ 駒が壁に突入できてしまう
❌ 駒同士が重なる移動もできてしまう
❌ 駒のサイズが 1×1 固定
❌ 駒の背景色が全部同じ
Blazorの体験からは少し離れるので一気に仕上げます。
駒Classを拡張します
- 幅と高さ、背景色のクラス名を追加します
- コンストラクタも追加しておきます
// 駒の定義
private class Piece
{
public int Row { get; set; }
public int Col { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string Label { get; set; } = string.Empty;
public string ColorClass { get; set; } = string.Empty;
public Piece(string label, int col, int row, int w, int h, string colorClass = "piece-default")
{
Label = label;
Row = row;
Col = col;
Width = w;
Height = h;
ColorClass = colorClass;
}
}
全ての駒を実装します
private void InitPieces()
{
Pieces.Clear();
// 駒名、駒の左上の 列、行、幅、高さ、色のクラス名
Pieces.Add(new Piece("父", 2, 1, 1, 2, "Color_父"));
Pieces.Add(new Piece("娘", 3, 1, 2, 2, "Color_娘"));
Pieces.Add(new Piece("母", 5, 1, 1, 2, "Color_母"));
Pieces.Add(new Piece("手代", 1, 3, 1, 1, "Color_手代"));
Pieces.Add(new Piece("大番頭", 2, 3, 4, 1, "Color_大番頭"));
Pieces.Add(new Piece("兄嫁", 6, 3, 1, 1, "Color_兄嫁"));
Pieces.Add(new Piece("丁稚", 1, 4, 1, 1, "Color_丁稚"));
Pieces.Add(new Piece("女中", 2, 4, 2, 1, "Color_女中"));
Pieces.Add(new Piece("番頭", 4, 4, 2, 1, "Color_番頭"));
Pieces.Add(new Piece("丁稚", 6, 4, 1, 1, "Color_丁稚"));
Pieces.Add(new Piece("番犬", 1, 5, 1, 1, "Color_番犬"));
Pieces.Add(new Piece("祖父", 2, 5, 2, 1, "Color_祖父"));
Pieces.Add(new Piece("祖母", 4, 5, 2, 1, "Color_祖母"));
Pieces.Add(new Piece("丁稚", 6, 5, 1, 1, "Color_丁稚"));
}
駒色のスタイルを追加します
/* 駒色 */
.Color_父 {
background: #b9bbdd;
}
.Color_娘 {
background: #e4bdc3;
}
.Color_母 {
background: #e9cccc;
}
.Color_手代 {
background: #f7dcb4;
}
.Color_大番頭 {
background: #dfbfaa;
}
.Color_兄嫁 {
background: #d2e7d4;
}
.Color_丁稚 {
background: #e9e3cc;
}
.Color_女中 {
background: #f3e0e3;
}
.Color_番頭 {
background: #ddcdc1;
}
.Color_番犬 {
background: #f7f0f1;
}
.Color_祖父 {
background: #adafdf;
}
.Color_祖母 {
background: #daadad;
}
駒のエレメントで背景色、幅、高さを描画できるようにします
@foreach (Piece p in Pieces)
{
var cssPieceRow = p.Row + 1;
var cssPieceCol = p.Col + 1;
<div class="piece @(p.ColorClass) @(p == Selected ? "selected" : "")"
style="
grid-row:@cssPieceRow;
grid-column:@cssPieceCol;
width:@(p.Width * 50)px;
height:@(p.Height * 50)px;"
@onmousedown="(e) => StartDrag(p, e.ClientX, e.ClientY)">
@p.Label
</div>
}
駒の移動判定を追加します
//------------------------------------------------
// 移動して良いか判定する
//------------------------------------------------
private bool CanMove(Piece p, int newRow, int newCol)
{
// ----- 盤外は禁止 -----
if (newRow < 0 || newRow + p.Height > MaxRows) return false;
if (newCol < 0 || newCol + p.Width > MaxCols) return false;
// ----- 壁セルとの衝突 -----
bool blocked = Enumerable.Range(newRow, p.Height).Any(r =>
Enumerable.Range(newCol, p.Width).Any(c =>
Board[r, c] == 1));
if (blocked) return false;
// 出口判定(娘だけOK)
bool inExit = Enumerable.Range(newRow, p.Height).Any(r =>
Enumerable.Range(newCol, p.Width).Any(c =>
Board[r, c] == 9));
if (inExit)
{
if (p.Label == "娘")
{
GameClear();
return true;
}
else
{
ShowWarning();
return false;
}
}
// 他駒との衝突
foreach (var o in Pieces.Where(o => o != p))
{
bool overlapX = newCol < o.Col + o.Width && newCol + p.Width > o.Col;
bool overlapY = newRow < o.Row + o.Height && newRow + p.Height > o.Row;
if (overlapX && overlapY) return false;
}
return true;
}
駒が出口に来た時の処理を追加します
- 娘が出口に来たらゲームクリアー
- 娘以外が出口に来たら警告を表示
private async void GameClear()
{
isCleared = true;
StateHasChanged();
await Task.Delay(5000);
isCleared = false;
StateHasChanged();
}
private async void ShowWarning()
{
isWarning = true;
StateHasChanged();
await Task.Delay(3000);
isWarning = false;
StateHasChanged();
}
htmlへゲームクリアなどのメッセージ表示を追加します
@if (isCleared)
{
<div class="game-message">🎉クリア🎉</div>
}
@if (isWarning)
{
<div class="game-message">玄関を出てよいのは娘だけ</div>
}
メッセージをあらかじめ準備しておき、C#のフラグ制御で表示をON/OFFするのも簡単制御で良いです😄
メッセージの書式を設定します。
(ちょっと凝ってアニメーションさせてます)
/*メッセージ */
.game-message {
position: fixed;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 72px;
font-weight: bold;
color: #ff4081;
text-shadow: 0 0 10px #fff, 0 0 20px #ff80ab, 0 0 30px #ff4081;
animation: flash 1s infinite alternate;
z-index: 9999;
pointer-events: none;
}
@@keyframes flash {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1.0);
}
to {
opacity: 0.5;
transform: translate(-50%, -50%) scale(1.1);
}
}
11.もう少しだけゲームらしく
最後に、タイマー表示とリセットボタンぐらいは欲しいですね
経過時間とリセットボタンの表示スタイルを追加します
/* 経過時間とリセット */
.status {
margin: 15px 0;
user-select: none;
}
#reset-btn {
margin-left: 10px;
background-color: #dc3545;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
#reset-btn:hover {
background-color: #b52a37;
}
htmlの先頭に経過時間とリセットボタンを表示します
<div class="status">
@if (isCleared)
{
<span id="elapsed-time">🎉クリア🎉</span>
}
<span id="elapsed-time">経過時間: @Elapsed 秒</span>
<button id="reset-btn" @onclick="ResetGame">リセット</button>
</div>
1秒ずつカウントアップする経過時間を @Elapsed
だけで表現できます!
C#でタイマーのカウントアップ処理を追加します
protected override void OnInitialized()
{
// 盤面初期化
InitPieces();
// タイマー開始
StartTimer();
}
// --------------------------------------------------
// タイマー
// --------------------------------------------------
private System.Threading.Timer? timer;
private int Elapsed = 0;
// タイマー開始
private void StartTimer()
{
timer?.Dispose();
timer = new System.Threading.Timer(_ =>
{
InvokeAsync(() =>
{
Elapsed++;
StateHasChanged();
});
}, null, 1000, 1000);
}
リセットボタンの処理とゲームクリアー時のタイマー停止を組み込みます
// リセット
private void ResetGame()
{
Elapsed = 0;
isCleared = false;
isWarning = false;
InitPieces();
StartTimer();
StateHasChanged();
}
private async void GameClear()
{
// タイマー停止
timer?.Dispose();
timer = null;
isCleared = true;
StateHasChanged();
await Task.Delay(5000);
isCleared = false;
StateHasChanged();
}
修正されたコードの全体像
=======================
コード全体を表示(折りたたみ)
========================
@page "/"
<style>
/* 経過時間とリセット */
.status {
margin: 15px 0;
user-select: none;
}
#reset-btn {
margin-left: 10px;
background-color: #dc3545;
color: white;
border: none;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
user-select: none;
}
#reset-btn:hover {
background-color: #b52a37;
}
/* ゲーム盤全体 */
.board {
display: grid;
grid-template-rows: repeat(7, 50px);
grid-template-columns: repeat(8, 50px);
position: relative;
}
/* 1つ1つのセルの書式 */
.cell {
display: flex;
justify-content: center;
align-items: center;
padding: 4px;
}
/* 壁 */
.wall {
background: #444;
}
/* 盤面 */
.empty {
background: #aaa;
}
/* 玄関 */
.exit {
background: #eee;
color: #333;
display: flex;
align-items: center;
justify-content: center;
}
/* 駒 */
.piece {
font-weight: bold;
display: flex;
align-items: center;
justify-content: center;
z-index: 1;
cursor: pointer;
user-select: none;
touch-action: none;
border: 1px solid #555;
box-sizing: border-box;
transform: scale(0.97);
}
.piece:active {
cursor: grabbing;
}
.piece.selected {
outline: 5px solid orange;
}
/* 駒色 */
.Color_父 {
background: #b9bbdd;
}
.Color_娘 {
background: #e4bdc3;
}
.Color_母 {
background: #e9cccc;
}
.Color_手代 {
background: #f7dcb4;
}
.Color_大番頭 {
background: #dfbfaa;
}
.Color_兄嫁 {
background: #d2e7d4;
}
.Color_丁稚 {
background: #e9e3cc;
}
.Color_女中 {
background: #f3e0e3;
}
.Color_番頭 {
background: #ddcdc1;
}
.Color_番犬 {
background: #f7f0f1;
}
.Color_祖父 {
background: #adafdf;
}
.Color_祖母 {
background: #daadad;
}
/*メッセージ */
.game-message {
position: fixed;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 72px;
font-weight: bold;
color: #ff4081;
text-shadow: 0 0 10px #fff, 0 0 20px #ff80ab, 0 0 30px #ff4081;
animation: flash 1s infinite alternate;
z-index: 9999;
pointer-events: none;
}
@@keyframes flash {
from {
opacity: 1;
transform: translate(-50%, -50%) scale(1.0);
}
to {
opacity: 0.5;
transform: translate(-50%, -50%) scale(1.1);
}
}
</style>
<div class="status">
@if (isCleared)
{
<span id="elapsed-time">🎉クリア🎉</span>
}
<span id="elapsed-time">経過時間: @Elapsed 秒</span>
<button id="reset-btn" @onclick="ResetGame">リセット</button>
</div>
<div class="board"
@onmousemove="(e) => DragMove(e)"
@onmouseup="EndDrag">
@for (int row = 0; row < MaxRows; row++)
{
for (int col = 0; col < MaxCols; col++)
{
// CSSのグリッドは1オリジン
var cssRow = row + 1;
var cssCol = col + 1;
int cell = Board[row, col];
switch (cell)
{
case 0: // 空
<div class="cell empty" style="grid-area:@(cssRow) / @(cssCol);">
</div>
break;
case 1: // 壁
<div class="cell wall" style="grid-area:@(cssRow) / @(cssCol);">
</div>
break;
case 9: // 玄関
<div class="cell exit" style="grid-area:@(cssRow) / @(cssCol);">
玄関
</div>
break;
}
}
}
@foreach (Piece p in Pieces)
{
var cssPieceRow = p.Row + 1;
var cssPieceCol = p.Col + 1;
<div class="piece @(p.ColorClass) @(p == Selected ? "selected" : "")"
style="
grid-row:@cssPieceRow;
grid-column:@cssPieceCol;
width:@(p.Width * 50)px;
height:@(p.Height * 50)px;"
@onmousedown="(e) => StartDrag(p, e.ClientX, e.ClientY)">
@p.Label
</div>
}
</div>
@if (isCleared)
{
<div class="game-message">🎉クリア🎉</div>
}
@if (isWarning)
{
<div class="game-message">玄関を出てよいのは娘だけ</div>
}
@code {
// 盤面の定義
private const int MaxRows = 7;
private const int MaxCols = 8;
private int[,] Board = new int[MaxRows, MaxCols]
{
// 盤面定義 (0=空, 1=壁, 9=出口)
{1,1,1,1,1,1,1,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,0,0,0,0,0,0,1},
{1,1,1,9,9,1,1,1},
};
// 駒の定義
private class Piece
{
public int Row { get; set; }
public int Col { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string Label { get; set; } = string.Empty;
public string ColorClass { get; set; } = string.Empty;
public Piece(string label, int col, int row, int w, int h, string colorClass = "piece-default")
{
Label = label;
Row = row;
Col = col;
Width = w;
Height = h;
ColorClass = colorClass;
}
}
// 駒のリスト
private List<Piece> Pieces = new List<Piece>();
private void InitPieces()
{
Pieces.Clear();
// 駒名、駒の左上の 列、行、幅、高さ、色のクラス名
Pieces.Add(new Piece("父", 2, 1, 1, 2, "Color_父"));
Pieces.Add(new Piece("娘", 3, 1, 2, 2, "Color_娘"));
Pieces.Add(new Piece("母", 5, 1, 1, 2, "Color_母"));
Pieces.Add(new Piece("手代", 1, 3, 1, 1, "Color_手代"));
Pieces.Add(new Piece("大番頭", 2, 3, 4, 1, "Color_大番頭"));
Pieces.Add(new Piece("兄嫁", 6, 3, 1, 1, "Color_兄嫁"));
Pieces.Add(new Piece("丁稚", 1, 4, 1, 1, "Color_丁稚"));
Pieces.Add(new Piece("女中", 2, 4, 2, 1, "Color_女中"));
Pieces.Add(new Piece("番頭", 4, 4, 2, 1, "Color_番頭"));
Pieces.Add(new Piece("丁稚", 6, 4, 1, 1, "Color_丁稚"));
Pieces.Add(new Piece("番犬", 1, 5, 1, 1, "Color_番犬"));
Pieces.Add(new Piece("祖父", 2, 5, 2, 1, "Color_祖父"));
Pieces.Add(new Piece("祖母", 4, 5, 2, 1, "Color_祖母"));
Pieces.Add(new Piece("丁稚", 6, 5, 1, 1, "Color_丁稚"));
}
protected override void OnInitialized()
{
// 盤面初期化
InitPieces();
// タイマー開始
StartTimer();
}
// --------------------------------------------------
// タイマー
// --------------------------------------------------
private System.Threading.Timer? timer;
private int Elapsed = 0;
// タイマー開始
private void StartTimer()
{
timer?.Dispose();
timer = new System.Threading.Timer(_ =>
{
InvokeAsync(() =>
{
Elapsed++;
StateHasChanged();
});
}, null, 1000, 1000);
}
// リセット
private void ResetGame()
{
Elapsed = 0;
isCleared = false;
isWarning = false;
InitPieces();
StartTimer();
StateHasChanged();
}
// 選択中の駒
private Piece? Selected;
private int startX, startY;
// ゲームクリアと警告フラグ
private bool isCleared = false;
private bool isWarning = false;
// ドラッグ開始
private void StartDrag(Piece p, double clientX, double clientY)
{
Selected = p;
startX = (int)clientX;
startY = (int)clientY;
}
// ドラッグ移動
private void DragMove(MouseEventArgs e)
{
if (Selected == null) return;
// マウス移動量(px単位)
int currentX = (int)e.ClientX;
int currentY = (int)e.ClientY;
const int cellSize = 50; // 1駒のサイズ
int threshold = cellSize / 2; // 50pxの半分
// 1駒の半分以上ドラッグさせたら、移動させる
int rowStep = 0, colStep = 0;
if (Math.Abs(currentX - startX) > threshold)
{
colStep = (currentX > startX) ? 1 : -1;
startX += colStep * cellSize;
}
if (Math.Abs(currentY - startY) > threshold)
{
rowStep = (currentY > startY) ? 1 : -1;
startY += rowStep * cellSize;
}
// 次の位置
int newRow = Selected.Row + rowStep;
int newCol = Selected.Col + colStep;
// 移動して良いか判定
if(CanMove(Selected, newRow, newCol))
{
// 駒位置の移動
Selected.Row = newRow;
Selected.Col = newCol;
}
}
//------------------------------------------------
// 移動して良いか判定する
//------------------------------------------------
private bool CanMove(Piece p, int newRow, int newCol)
{
// ----- 盤外は禁止 -----
if (newRow < 0 || newRow + p.Height > MaxRows) return false;
if (newCol < 0 || newCol + p.Width > MaxCols) return false;
// ----- 壁セルとの衝突 -----
bool blocked = Enumerable.Range(newRow, p.Height).Any(r =>
Enumerable.Range(newCol, p.Width).Any(c =>
Board[r, c] == 1));
if (blocked) return false;
// 出口判定(娘だけOK)
bool inExit = Enumerable.Range(newRow, p.Height).Any(r =>
Enumerable.Range(newCol, p.Width).Any(c =>
Board[r, c] == 9));
if (inExit)
{
if (p.Label == "娘")
{
GameClear();
return true;
}
else
{
ShowWarning();
return false;
}
}
// 他駒との衝突
foreach (var o in Pieces.Where(o => o != p))
{
bool overlapX = newCol < o.Col + o.Width && newCol + p.Width > o.Col;
bool overlapY = newRow < o.Row + o.Height && newRow + p.Height > o.Row;
if (overlapX && overlapY) return false;
}
return true;
}
private async void GameClear()
{
// タイマー停止
timer?.Dispose();
timer = null;
isCleared = true;
StateHasChanged();
await Task.Delay(5000);
isCleared = false;
StateHasChanged();
}
private async void ShowWarning()
{
isWarning = true;
StateHasChanged();
await Task.Delay(3000);
isWarning = false;
StateHasChanged();
}
// ドラッグ終了
private void EndDrag()
{
Selected = null;
}
}
修正後のイメージ
本格的な駒が実装されました。
メッセージ表示も良い感じですね。
クリアメッセージは是非ご自分で確認してください😄
念のためにここまでのソリューション全体をGitHubへアップしました
12.もっと拡張したい方へ
いかがだったでしょうか?
Webゲームが簡単に作成できました。
さらにゲームを発展させたい方もいると思います。
🔼 マウスだけじゃなくてスマホでタッチ操作したい
🔼 見た目をもっと強化したい
この記事の趣旨である 「初心者向けのBlazorのご紹介から」 外れますので、こちら公開しているデモ版のソースを参考にしてみてください。
13.最後に
Blazor ServerはWebSockets(ウェブソケット)を利用したフレームワークなのでゲーム作成が得意なだけなんでしょうか?
この記事で利用してきた技術を流用すれば、相当にリッチなユーザーインターフェースを簡単に作成できることが想像できると思います。
ブラウザ上の単純なボタンクリックだけでなく、必要に応じマウスドラッグなど任意のイベントを拾うことができます。
Blazorは業務アプリ開発でも優秀!
こちらの記事ではBlazorを利用した業務アプリケーションの開発事例を紹介しています。
気になった方は是非ご覧ください。