はじめに
前回はステージを作成しました。今回は、ぷよぷよを自由落下をさせてみます。
今回により「子コンポーネントのメソッドを直接呼びたい」や「SKCanvasViewを再描画したい」といった要望が出てきたため、@ref属性を付けるなどを前記事に追記しています。
もう一度プログラミングの楽しみを取り戻すつもりで、リハビリも兼ねてこの題材にしています。よってペースは遅いです。
利用教材
当該記事の内容は、ぷよぷよプログラミングの利用条件を理解したうえで学習にご利用ください。
画像は教材から取得しています。
※「すぐわかる! ぷよぷよプログラミング SEGA公式ガイドブック」の本のぷよぷよの番号と教材のぷよぷよの番号が違います。
| 番号 | 教材 | 本 |
|---|---|---|
| 1 | 緑色 | 緑色 |
| 2 | 青色 | 赤色 |
| 3 | 紫色 | 黄色 |
| 4 | 赤色 | 青色 |
| 5 | 黄色 | 紫色 |
結果
書籍の「STAGE 05 自由落下さえてみよう! 状態遷移によるアニメーションの実現」です。
ソースコード
現段階で載せられる長さの状態まではここに記載していきます。
そのうち、GitHubに移行する予定です。
Content/Config.cs
前回から追加部分のみ
対戦用は左右反転したデータにしています。
今後のおじゃまぷよを考慮して、ぷよの最大値は6にしています。
namespace BlazorPuyoPuyo.Content;
public static class Config
{
public const int FallingSpeed = 6; // 自由落下のスピード
public const int PuyoColorMax = 6; // ぷよの最大値
// 初期状態のステージ
public static readonly int[,,] InitialBoard = {
{
{ 0, 1, 2, 3, 4, 5 },
{ 1, 2, 3, 4, 5, 0 },
{ 2, 3, 4, 5, 0, 1 },
{ 0, 0, 0, 0, 0, 0 },
{ 0, 1, 1, 1, 2, 0 },
{ 0, 4, 5, 5, 2, 0 },
{ 0, 4, 5, 5, 2, 0 },
{ 0, 4, 3, 3, 3, 0 },
{ 0, 0, 0, 0, 0, 0 },
{ 1, 1, 0, 0, 2, 2 },
{ 0, 0, 2, 2, 0, 0 },
{ 4, 4, 0, 0, 5, 5 },
},
{
{ 5, 4, 3, 2, 1, 0 },
{ 0, 5, 4, 3, 2, 1 },
{ 1, 0, 5, 4, 3, 2 },
{ 0, 0, 0, 0, 0, 0 },
{ 0, 2, 1, 1, 1, 0 },
{ 0, 2, 5, 5, 4, 0 },
{ 0, 2, 5, 5, 4, 0 },
{ 0, 3, 3, 3, 4, 0 },
{ 0, 0, 0, 0, 0, 0 },
{ 2, 2, 0, 0, 1, 1 },
{ 0, 0, 2, 2, 0, 0 },
{ 5, 5, 0, 0, 4, 4 },
}
};
Content/Game.razor.cs
ゲームループ用として、System.Timersを使用するのは何か嫌だなと思い、下記サイトを参考にrequestAnimationFrameを使用しました。
https://mstack.nl/blogs/20210630-blazor-game-development-1-2/
ところが、いざ使用してみるとぷよぷよの自由落下の途中経過が間引かれて最終結果のみ表示されました。
ChatGPTに質問したところ、SKCanvasは、下記によってタイミングがズレるため、途中経過が間引かれてしまうとのこと。
- .NET 側で描画命令を組み立て
- Blazor の再レンダリングを経由
- JS の描画パイプラインに直接乗らない
System.Timersより安全ということで、PeriodicTimerを使用することにしました。
ゲームループ用のGameLoopメソッド名は、Unityエンジンに倣ってUpdateメソッド名に変更しています。
前回から追記分のみ記載
// ゲームの現在の状況
private enum GameState
{
None,
Start,
CheckFallingPuyo,
FallingPuyo
}
private GameState[] _gameState = { GameState.Start, GameState.Start };
private PeriodicTimer? _timer;
private int _frameCount = 0;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
DevicePixelRatio = await JS.InvokeAsync<double>("eval", "window.devicePixelRatio");
IsLoaded = true;
// 定期処理 約60fps
_timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1000.0 / 60));
while (await _timer!.WaitForNextTickAsync())
Update();
}
}
private void Update()
{
Stage?[] stageRef = { _stageRef1, _stageRef2 };
for (int index = 0; index < stageRef.Length; index++)
{
var stage = stageRef[index];
Dispatch(ref _gameState[index], stage);
stage?.Redraw();
_frameCount++;
}
}
private void Dispatch(ref GameState state, Stage? stage)
{
switch (state)
{
case GameState.Start:
// ゲーム開始直後の状態
state = GameState.CheckFallingPuyo;
break;
case GameState.CheckFallingPuyo:
// 落ちるかどうか判定する状態
if (stage != null && stage.CheckFallingPuyo())
{
state = GameState.FallingPuyo;
}
else
{
state = GameState.None;
}
break;
case GameState.FallingPuyo:
// ぷよが自由落下しているアニメーション状態
if (stage != null && !stage.FallPuyo())
{
state = GameState.None;
}
break;
}
}
Content/Stage.razor.cs
CheckFallingPuyoメソッドにて、_fallingPuyoInfoList変数に自由落下する対象を格納しておきます。_puyoBoardには自由落下の最終結果を格納します。
FallPuyoメソッドで、_fallingPuyoInfoListの格納分だけ少しずつ下側(FallingSpeed = 6)に座標を書き換えていきます。
すなわちぷよが自由落下しているアニメーションの状態になります。落ちるぷよが無くなるまで繰り返します。
[SupportedOSPlatform("browser")]
public partial class Stage
{
[CascadingParameter]
public Game Game { get; set; } = default!;
[Parameter]
public int Index { get; set; }
private record PuyoInfo
{
public int PuyoColor; // 画面上のぷよ色
public SKPoint Point; // 画面上のぷよ座標
public PuyoInfo(SKPoint point, int Color)
{
PuyoColor = Color;
Point = point;
}
}
private record FallingPuyoInfo
{
public PuyoInfo? Info; // ぷよ情報
public float Position; // 画面上の現在のY座標の位置
public float Destination; // 画面上の目的値のY座標の位置
public bool Falling; // 今落ちているかどうか
}
private PuyoInfo[,] _puyoBoard = new PuyoInfo[Config.StageRows, Config.StageCols];
private int _puyoCount = 0;
private List<FallingPuyoInfo> _fallingPuyoInfoList = default!;
private SKCanvasView? _canvasView;
protected override void OnInitialized()
{
for (int y = 0; y < Config.StageRows; y++)
{
for (int x = 0; x < Config.StageCols; x++)
{
var point = new SKPoint(x * Config.PuyoImageWidth,
y * Config.PuyoImageHeight);
PuyoInfo info = new(point, Config.InitialBoard[Index - 1, y, x]);
CreatePuyo(x, y, info);
}
}
}
// 再描画
public void Redraw()
{
if (_canvasView != null)
_canvasView?.Invalidate();
}
// 自由落下するぷよがあるかどうかチェックする
public bool CheckFallingPuyo()
{
_fallingPuyoInfoList = new();
// 下の行から上の行を見ていく
for (int y = Config.StageRows - 2; y >= 0; y--)
{
for (int x = 0; x < Config.StageCols; x++)
{
PuyoInfo info = GetPuyoInfo(x, y);
PuyoInfo currentPuyoInfo = new(info.Point, info.PuyoColor);
if (currentPuyoInfo.PuyoColor <= 0)
// このマスにぷよがなければ、次
continue;
PuyoInfo belowPuyoInfo = GetPuyoInfo(x, y + 1);
if (belowPuyoInfo.PuyoColor == 0)
{
// 下が空白なので、このぷよは落ちる
// まず、ぷよぷよ盤からそのぷよを取り去る
RemovePuyoInfo(x, y);
// 自由落下した場合にどこまで落ちるのか調べる
int destination = y;
while (GetPuyoInfo(x, destination + 1).PuyoColor == 0)
destination++;
// 最終目的地に置く
SetPuyoInfo(x, destination, currentPuyoInfo);
// 「落ちるぷよ情報リスト」に「落ちるぷよ情報」を入れる
FallingPuyoInfo puyoinfo = new()
{
Info = GetPuyoInfo(x, destination),
Position = y * Config.PuyoImageHeight,
Destination = destination * Config.PuyoImageHeight,
Falling = true
};
_fallingPuyoInfoList.Add(puyoinfo);
}
}
}
return (_fallingPuyoInfoList.Count > 0);
}
// 自由落下させる
public bool FallPuyo()
{
bool isFalling = false;
foreach (var fallingPuyoInfo in _fallingPuyoInfoList)
{
if (!fallingPuyoInfo.Falling)
{
// すでに自由落下がおわっている
continue;
}
var postion = fallingPuyoInfo.Position;
postion += Config.FallingSpeed;
if (postion >= fallingPuyoInfo.Destination)
{
// 自由落下終了
postion = fallingPuyoInfo.Destination;
fallingPuyoInfo.Falling = false;
}
else
{
// まだ落下しているぷよがあることを記憶する
isFalling = true;
}
// 新しい位置を保存する
fallingPuyoInfo.Position = postion;
// ぷよを動かす
fallingPuyoInfo.Info?.Point.Y = postion;
}
return isFalling;
}
private void OnPaintSurface(SKPaintSurfaceEventArgs e)
{
if (!Game.IsLoaded)
return;
var canvas = e.Surface.Canvas;
canvas.Clear();
float zoom = (float)Game.DevicePixelRatio;
for (int i = 0; i < Config.StageRows; i++)
{
for (int j = 0; j < Config.StageCols; j++)
{
PuyoInfo info = _puyoBoard[i, j];
if (info.PuyoColor >= 1 && info.PuyoColor <= Config.PuyoColorMax)
Draw(canvas, info, zoom);
}
}
}
private void Draw(SKCanvas canvas, PuyoInfo info, float zoom)
{
float width = (float)(Config.PuyoImageWidth * zoom);
float height = (float)(Config.PuyoImageHeight * zoom);
var destRect = SKRect.Create(info.Point.X * zoom, info.Point.Y * zoom, width, height);
canvas.DrawBitmap(Game.PuyoImageList[info.PuyoColor - 1], destRect);
}
private void CreatePuyo(int x, int y, PuyoInfo info)
{
SetPuyoInfo(x, y, info);
// ぷよの総数を追加する
if (info.PuyoColor >= 1 && info.PuyoColor <= Config.PuyoColorMax)
_puyoCount++;
}
// ぷよぷよ盤にぷよ情報をセットする
private void SetPuyoInfo(int x, int y, PuyoInfo info)
{
_puyoBoard[y, x] = info;
}
// ぷよぷよ盤の情報を返す
private PuyoInfo GetPuyoInfo(int x, int y)
{
var point = new SKPoint(0, 0);
// 左右、もしくは底の場合は、ダミーのぷよ情報を返す
if (x < 0 || x >= Config.StageCols || y >= Config.StageRows)
return new(point, -1);
// y座標がマイナスの場合は、そこは空白扱いとする
if (y < 0)
return new(point, 0);
// それ以外の場合はぷよぷよ盤の情報をそのまま返す
return _puyoBoard[y, x];
}
// ぷよぷよ盤からぷよ情報を消す
private void RemovePuyoInfo(int x, int y)
{
_puyoBoard[y, x].PuyoColor = 0;
}
}
最後に
次回は書籍の「STAGE 06 ぷよを消してみよう! 再帰テクニックの有効活用」の処理を行います。
ぷよ画像が動くようになってくるとプログラムも面白くなってきますね。
