⇒ Blazor WebAssemblyで作成したテトリスはこちら とても重いです。
ではBlazor WebAssemblyでテトリスを作ります。使用するクラスは以下で作成したものを基本的にそのまま使います。ただBlazor WebAssemblyにあわない部分は変更します。
PictureBoxに頼らずC#でテトリスをつくる (その1)
MovingTetriminoクラス PictureBoxに頼らずC#でテトリスをつくる (その2)
ではさっそくBlazor WebAssemblyでテトリスを作ってみましょう。
基本的にそのままでいいのですが、先頭に
1 |
using System.Drawing; |
をつけてください。
変更が必要な部分だけ示します。
Blockクラス
Draw(Graphics g)メソッドを変更します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Block { //public void Draw(Graphics g) //{ // g.FillRectangle(new SolidBrush(Color), new Rectangle(X, Y, Width, Height)); // g.DrawRectangle(new Pen(Color.Black), new Rectangle(X, Y, Width, Height)); //} public async Task Draw(Blazor.Extensions.Canvas.Canvas2D.Canvas2DContext context) { await context.SetFillStyleAsync(Color.Name); await context.FillRectAsync(X, Y, Width, Height); await context.SetStrokeStyleAsync(Color.Black.Name); await context.StrokeRectAsync(X, Y, Width, Height); } } |
FixedTetriminoクラス
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
static public class FixedTetrimino { //static public void Draw(Graphics g) //{ // foreach (Block block in Blocks) // { // g.FillRectangle(new SolidBrush(block.Color), new Rectangle(block.X, block.Y, block.Width, block.Height)); // g.DrawRectangle(new Pen(Color.Black), new Rectangle(block.X, block.Y, block.Width, block.Height)); // } //} static public async Task Draw(Blazor.Extensions.Canvas.Canvas2D.Canvas2DContext context) { foreach (Block block in Blocks) { await block.Draw(context); } } static public void DeleteLines() { List<int> lineNums = GetDeleteLineNums(); if (lineNums.Count == 0) return; lineNums = lineNums.OrderBy(x => x).ToList(); foreach (int num in lineNums) { List<Block> lineBlocks = Blocks.Where(x => x.PosY == num).ToList(); foreach (Block block in lineBlocks) Blocks.Remove(block); } LinesDeleting?.Invoke(null, new EventArgs()); // System.Windows.FormsのTimerは使わない //Timer timer = new Timer(); //timer.Interval = 100; //timer.Tick += Timer_Tick; //timer.Start(); System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = 100; timer.Elapsed += Timer_Tick; timer.AutoReset = false; timer.Start(); void Timer_Tick(object sender, EventArgs e) { //Timer t = (Timer)sender; System.Timers.Timer t = (System.Timers.Timer)sender; t.Stop(); t.Dispose(); foreach (int num in lineNums) { List<Block> upperBlocks = Blocks.Where(x => x.PosY < num).ToList(); foreach (Block block in upperBlocks) { Blocks.Remove(block); Block block1 = new Block(block.PosX, block.PosY + 1); block1.Color = block.Color; Blocks.Add(block1); } } LinesDeleted?.Invoke(null, new LinesDeletedArgs(lineNums.Count)); //PlaySoundeffect.DeleteLine(); ここではこのメソッドは使えない } } } |
MovingTetriminoクラス
PlaySoundeffect.Rotate()メソッドが使えないのでコメントアウトしています。また移動系のメソッドは実際に移動処理ができたらtrue、できなかった場合はfalseを返すように変更しました。
あとミノが着地したときにイベントが発生するのですが、そのときドロップポイントを渡せるようにしました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 |
static public class MovingTetrimino { static public bool MoveLeft() { List<Block> blocks = GetMoveLeft(); if (blocks.Min(x => x.PosX) >= 0 && !FixedTetrimino.ExistsBlock(blocks)) { PosX--; return true; } return false; } static public bool MoveRight() { List<Block> blocks = GetMoveRight(); if (blocks.Max(x => x.PosX) < Constant.FieldWidth && !FixedTetrimino.ExistsBlock(blocks)) { PosX++; return true; } return false; } static public bool MoveDown() { List<Block> blocks = GetMoveDown(); if (blocks.Max(x => x.PosY) < Constant.FieldHeight && !FixedTetrimino.ExistsBlock(blocks)) { PosY++; return true; } else { FixTetrimino(); return false; } } static public bool RotateL() { SuperRotationResult result = null; if (Type != TetriminoTypes.I && Type != TetriminoTypes.O) result = GetSuperRotationResultLeftTSZLJ(); if (Type == TetriminoTypes.I) result = GetSuperRotationResultLeftI(); if (result != null) { Angle = GetAngleRotateL(); PosX += result.MoveX; PosY += result.MoveY; //PlaySoundeffect.Rotate(); return true; } return false; } static public bool RotateR() { SuperRotationResult result = null; if (Type != TetriminoTypes.I && Type != TetriminoTypes.O) result = GetSuperRotationResultRightTSZLJ(); if (Type == TetriminoTypes.I) result = GetSuperRotationResultRightI(); if (result != null) { Angle = GetAngleRotateR(); PosX += result.MoveX; PosY += result.MoveY; //PlaySoundeffect.Rotate(); return true; } return false; } public delegate void TetriminoFixedHandler(object sender, FixTetriminoArgs args); static public event TetriminoFixedHandler TetriminoFixed; static void FixTetrimino() { List<Block> blocks = GetBlocks(Angle); FixedTetrimino.Blocks.AddRange(blocks); // PlaySoundeffect.Drop(); FixedTetrimino.DeleteLines(); Type = PopNext(); // TetriminoFixed?.Invoke(null, new EventArgs()); TetriminoFixed?.Invoke(null, new FixTetriminoArgs(DropPoint)); DropPoint = 0; } // static public void Draw(Graphics g) // { // if (Type == TetriminoTypes.None) // return; // List<Block> blocks = GetBlocks(Angle); // foreach (Block block in blocks) // { // g.FillRectangle(new SolidBrush(block.Color), new Rectangle(block.X, block.Y, block.Width, block.Height)); // g.DrawRectangle(new Pen(Color.Black), new Rectangle(block.X, block.Y, block.Width, block.Height)); // } // } static public async Task Draw(Blazor.Extensions.Canvas.Canvas2D.Canvas2DContext context) { if (Type == TetriminoTypes.None) return; List<Block> blocks = GetBlocks(Angle); foreach (Block block in blocks) { await block.Draw(context); } } // static public void DrawGohst(Graphics g) // { // if (Type == TetriminoTypes.None) // return; // List<Block> blocks = GetGohstBlocks(); // foreach (Block block in blocks) // { // Color color = Color.FromArgb(80, block.Color); // g.FillRectangle(new SolidBrush(color), new Rectangle(block.X, block.Y, block.Width, block.Height)); // g.DrawRectangle(new Pen(Color.Black), new Rectangle(block.X, block.Y, block.Width, block.Height)); // } // } static public async Task DrawGohst(Blazor.Extensions.Canvas.Canvas2D.Canvas2DContext context) { if (Type == TetriminoTypes.None) return; List<Block> blocks = GetGohstBlocks(); await context.SetGlobalAlphaAsync(0.2f); foreach (Block block in blocks) { string str = String.Format("rgb([{0},{1},{2})", block.Color.R, block.Color.G, block.Color.B); await context.SetFillStyleAsync(str); await context.FillRectAsync(block.X, block.Y, block.Width, block.Height); await context.SetStrokeStyleAsync(Color.Black.Name); await context.StrokeRectAsync(block.X, block.Y, block.Width, block.Height); } await context.SetGlobalAlphaAsync(1.0f); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 追加されたイベントハンドラの引数 public class FixTetriminoArgs : EventArgs { public FixTetriminoArgs(int dropPoint) { DropPoint = dropPoint; } public int DropPoint { get; protected set; } } |
ではこれらのクラスをつかってBlazor WebAssemblyでテトリスをつくることにします。
パッケージマネージャーからBlazor.Extensions.CanvasとToolbelt.Blazor.HotKeysをインストールしておいてください。
wwwroot/index.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>ちょっと動作が重いTETRIS</title> <link href="css/bootstrap/bootstrap.min.css" rel="stylesheet" /> <link href="css/app.css" rel="stylesheet" /> </head> <body> <app>Loading...</app> <div id="blazor-error-ui"> An unhandled error has occurred. <a href="" class="reload">Reload</a> <a class="dismiss">??</a> </div> <script src="_framework/blazor.webassembly.js"></script> <script src="_content/Blazor.Extensions.Canvas/blazor.extensions.canvas.js"></script> <audio id="rotate0" src="./audio/rotate.mp3" /> <audio id="rotate1" src="./audio/rotate.mp3" /> <audio id="rotate2" src="./audio/rotate.mp3" /> <audio id="rotate3" src="./audio/rotate.mp3" /> <audio id="rotate4" src="./audio/rotate.mp3" /> <audio id="rotate5" src="./audio/rotate.mp3" /> <audio id="rotate6" src="./audio/rotate.mp3" /> <audio id="rotate7" src="./audio/rotate.mp3" /> <audio id="delete0" src="./audio/delete.mp3" /> <audio id="delete1" src="./audio/delete.mp3" /> <audio id="drop0" src="./audio/drop.mp3" /> <audio id="drop1" src="./audio/drop.mp3" /> <script> function mysound(str) { document.getElementById(str).play(); } </script> </body> </html > |
Pages/Index.razor
1 2 3 4 5 6 7 8 9 10 11 |
@page "/" <BECanvas Width="500" Height="530" @ref="_canvasReference"></BECanvas> <img @ref="i_png" hidden id="i" src="./images/i.png" /> <img @ref="j_png" hidden id="j" src="./images/j.png" /> <img @ref="l_png" hidden id="l" src="./images/l.png" /> <img @ref="o_png" hidden id="o" src="./images/o.png" /> <img @ref="s_png" hidden id="s" src="./images/s.png" /> <img @ref="t_png" hidden id="t" src="./images/t.png" /> <img @ref="z_png" hidden id="z" src="./images/z.png" /> |
まず音を鳴らすためのメソッドを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
@code { @inject IJSRuntime JS; int counter = 0; async void PlaySeRotate() { counter++; string str = "rotate" + (counter % 8).ToString(); await JS.InvokeAsync<string>("mysound", str); } async void PlaySeDrop() { counter++; string str = "drop" + (counter % 2).ToString(); await JS.InvokeAsync<string>("mysound", str); } async void PlaySeDelete() { counter++; string str = "delete" + (counter % 2).ToString(); await JS.InvokeAsync<string>("mysound", str); } } |
次にキー入力に対応できるようにします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 |
@code { protected override void OnInitialized() { RegistKey(); SetEventHandlers(); // 外枠を描画する準備 OutsideBlocks = CreateOutsideBlocks(); FixedTetrimino.Init(); MovingTetrimino.Init(); Timer.Interval = 500; Timer.Elapsed += Timer_Tick; Timer.Start(); } @using Toolbelt.Blazor.HotKeys @inject HotKeys HotKeys @implements IDisposable private HotKeysContext HotKeysContext; void RegistKey() { this.HotKeysContext = this.HotKeys.CreateContext(); this.HotKeysContext.Add(ModKeys.None, Keys.Left, () => { this.MoveLeft(); }); this.HotKeysContext.Add(ModKeys.None, Keys.Right, () => { MoveRight(); }); this.HotKeysContext.Add(ModKeys.None, Keys.Down, () => { MoveDown(); }); this.HotKeysContext.Add(ModKeys.None, Keys.Space, () => { HardDrop(); }); this.HotKeysContext.Add(ModKeys.None, Keys.Z, () => { RotateL(); }); this.HotKeysContext.Add(ModKeys.None, Keys.X, () => { RotateR(); }); this.HotKeysContext.Add(ModKeys.None, Keys.S, () => { GameStart(); }); this.HotKeysContext.Add(ModKeys.None, Keys.C, () => { Hold(); }); this.HotKeysContext.Add(ModKeys.None, Keys.ESC, () => { Pause(); }); } public void Dispose() { // イベントの捕捉を切り離す Timer.Elapsed -= Timer_Tick; MovingTetrimino.TetriminoFixed -= MovingTetrimino_TetriminoFixed; FixedTetrimino.LinesDeleting -= FixedTetrimino_LinesDeleting; FixedTetrimino.LinesDeleted -= FixedTetrimino_LinesDeleted; // HotKeysContext オブジェクトを破棄する this.HotKeysContext.Dispose(); } } |
これはイベントハンドラをセットするためのメソッドです。Dispose()でイベントの捕捉を切り離す必要があります。これをしないと再び同じページが読み込まれたときに処理が二重三重におこなわれてしまいます。
1 2 3 4 5 6 7 8 9 |
@code { void SetEventHandlers() { MovingTetrimino.TetriminoFixed += MovingTetrimino_TetriminoFixed; MovingTetrimino.CantPutNewTetrimino += MovingTetrimino_CantPutNewTetrimino; FixedTetrimino.LinesDeleting += FixedTetrimino_LinesDeleting; FixedTetrimino.LinesDeleted += FixedTetrimino_LinesDeleted; } } |
CreateOutsideBlocks()メソッドはテトリスの外枠のブロックの位置を取得するためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
@code { List<Block> OutsideBlocks = new List<Block>(); List<Block> CreateOutsideBlocks() { List<Block> outsideBlocks = new List<Block>(); for (int row = 0; row < Constant.FieldHeight + 1; row++) { outsideBlocks.Add(new Block(-1, row)); outsideBlocks.Add(new Block(Constant.FieldWidth, row)); } for (int colum = -1; colum < Constant.FieldWidth + 1; colum++) { outsideBlocks.Add(new Block(colum, Constant.FieldHeight)); } return outsideBlocks; } } |
以下は描画処理に関するコードです。
現在落下してくるミノやすでにフィールドに固定されたミノ、次に降ってくるミノ、ホールドされているミノを表示するためのものです。ゲームオーバーになったりPauseしているときはそれを示す文字列を描画します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 |
@code { @using System.Timers; Timer Timer = new Timer(); @using Blazor.Extensions @using Blazor.Extensions.Canvas @using Blazor.Extensions.Canvas.Canvas2D private Canvas2DContext _context; protected BECanvasComponent _canvasReference; bool isGameOvered = false; bool isAllowHold = true; bool isPause = false; int Score = 0; ElementReference i_png; ElementReference j_png; ElementReference l_png; ElementReference o_png; ElementReference s_png; ElementReference t_png; ElementReference z_png; protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { this._context = await _canvasReference.CreateCanvas2DAsync(); } } private void Timer_Tick(object sender, EventArgs e) { if (!isPause && !isGameOvered) MovingTetrimino.MoveDown(); Update(); } async void Update() { if (isGameOvered) { await _context.SetFontAsync("bold 40px 'MS ゴシック'"); await _context.SetFillStyleAsync(System.Drawing.Color.White.Name); await _context.FillTextAsync("GAME OVER", 160, 180); await _context.SetFontAsync("bold 20px 'MS ゴシック'"); await _context.FillTextAsync("RESTART S KEY", 180, 220); return; } if (isPause) { await _context.SetFontAsync("bold 40px 'MS ゴシック"); await _context.SetFillStyleAsync(System.Drawing.Color.White.Name); await _context.FillTextAsync("PAUSE", 200, 180); return; } await _context.SetFillStyleAsync(System.Drawing.Color.Black.Name); await _context.FillRectAsync(0, 0, _canvasReference.Width, _canvasReference.Height); foreach (Block block1 in OutsideBlocks) { await block1.Draw(this._context); } await MovingTetrimino.Draw(this._context); await MovingTetrimino.DrawGohst(this._context); await FixedTetrimino.Draw(this._context); await _context.SetFillStyleAsync(System.Drawing.Color.White.Name); await _context.SetFontAsync("24px 'MS ゴシック'"); await _context.FillTextAsync("Score " + Score.ToString(), 10, 50); var nextTypes = MovingTetrimino.GetNext7(); if (nextTypes.Count != 0) { await _context.DrawImageAsync(GetImageFromTetrimino(nextTypes[0]), 400, 50); await _context.DrawImageAsync(GetImageFromTetrimino(nextTypes[1]), 400, 150); await _context.DrawImageAsync(GetImageFromTetrimino(nextTypes[2]), 400, 250); } if (holdTetriminoType != TetriminoTypes.None) { await _context.DrawImageAsync(GetImageFromTetrimino(holdTetriminoType), 30, 100); } } ElementReference GetImageFromTetrimino(TetriminoTypes tetrimino) { if (tetrimino == TetriminoTypes.I) return i_png; else if (tetrimino == TetriminoTypes.J) return j_png; else if (tetrimino == TetriminoTypes.L) return l_png; else if (tetrimino == TetriminoTypes.O) return o_png; else if (tetrimino == TetriminoTypes.S) return s_png; else if (tetrimino == TetriminoTypes.T) return t_png; else //if (tetrimino == TetriminoTypes.Z) return z_png; } } |
以下はキー入力されたときに呼び出されるメソッドに関するコードです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
@code { void MoveLeft() { MovingTetrimino.MoveLeft(); Update(); } void MoveRight() { MovingTetrimino.MoveRight(); Update(); } void MoveDown() { MovingTetrimino.MoveDown(); Update(); } void HardDrop() { MovingTetrimino.HardDrop(); Update(); } void RotateL() { // 回転できた場合だけ音を鳴らす if (MovingTetrimino.RotateL()) { PlaySeRotate(); Update(); } } void RotateR() { // 回転できた場合だけ音を鳴らす if (MovingTetrimino.RotateR()) { PlaySeRotate(); Update(); } } TetriminoTypes holdTetriminoType = TetriminoTypes.None; void Hold() { if (!isAllowHold) return; if (holdTetriminoType == TetriminoTypes.None) { holdTetriminoType = MovingTetrimino.Type; MovingTetrimino.Type = MovingTetrimino.PopNext(); } else { TetriminoTypes old = holdTetriminoType; holdTetriminoType = MovingTetrimino.Type; MovingTetrimino.Type = old; } isAllowHold = false; } void Pause() { if (!isGameOvered) isPause = isPause ? false : true; } void GameStart() { isGameOvered = false; isAllowHold = true; holdTetriminoType = TetriminoTypes.None; Score = 0; // MovingTetrimino.Init()より先に実行しないと // MovingTetrimino.CantPutNewTetriminoイベントが発生する FixedTetrimino.Init(); MovingTetrimino.Init(); Timer.Start(); Update(); } } |
最後にイベントハンドラに関するコードを示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
@code{ void MovingTetrimino_TetriminoFixed(object sender, FixTetriminoArgs args) { isAllowHold = true; PlaySeDrop(); Score += args.DropPoint; } void FixedTetrimino_LinesDeleting(object sender, EventArgs args) { Timer.Stop(); } void FixedTetrimino_LinesDeleted(object sender, LinesDeletedArgs args) { PlaySeDelete(); if (args.LinesCount == 1) Score += 40; else if (args.LinesCount == 2) Score += 100; else if (args.LinesCount == 3) Score += 300; else if (args.LinesCount == 4) Score += 1200; Timer.Start(); } void MovingTetrimino_CantPutNewTetrimino(object sender, EventArgs args) { isGameOvered = true; } } |
さてこれで一応完成なのですが、実際に動かしてみると動作がとても重いです。やっぱりこういうものをつくるのであればJavaScriptあたりがいいのかな?