以前テトリスを作成しましたが、PictureBoxを使用しています。今回はPictureBoxを使わずにつくります。
主な仕様
落下中のテトリミノはMovingTetriminoクラスで、着地し固定されたテトリミノはFixedTetriminoクラスで扱います。どちらも静的クラスです。
次のミノは3つまで表示されます。また落下中のミノはホールドすることができます。
スーパーローテーションシステム対応です。Tスピントリプルも可能です。
BGMをつけました。
まずデザイナですが、メニューバー以外ありません。
MovingTetriminoクラスが長くなったので、次回示します。ここではそれ以外のクラスを示します。
まず使用する定数に関するクラスを示します。
1 2 3 4 5 6 7 |
static public class Constant { static public int FieldWidth { get { return 10; } } static public int FieldHeight { get { return 20; } } static public int BlockSize { get { return 20; } } static public Point LeftTopBlockPosition { get { return new Point(150, 70); } } } |
次にBlockクラスを示します。これは描画するブロックを管理するためのクラスです。
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 |
public class Block { public Block(int posX, int posY) { Color = Color.Gray; PosX = posX; PosY = posY; } public int PosX { get; protected set; } public int PosY { get; protected set; } public Color Color { get; set; } public int X { get { return Width * PosX + Constant.LeftTopBlockPosition.X; } } public int Y { get { return Height * PosY + Constant.LeftTopBlockPosition.Y; } } public int Width { get { return Constant.BlockSize; } } public int Height { get { return Constant.BlockSize; } } 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)); } } |
TetriminoTypesはテトリミノのタイプを示す列挙型です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
public enum TetriminoTypes { // I - テトリミノ(水色) // O - テトリミノ(黄色) // S - テトリミノ(緑) // Z - テトリミノ(赤) // J - テトリミノ(青) // L - テトリミノ(オレンジ) // T - テトリミノ(紫) I = 0, O = 1, S = 2, Z = 3, J = 4, L = 5, T = 6, None = 7, } |
TetriminoAngleはテトリミノが向いている方向を示す列挙型です。
1 2 3 4 5 6 7 |
public enum TetriminoAngle { Angle0 = 0, Angle90 = 1, Angle180 = 2, Angle270 = 3, } |
ではForm1クラスをみてみましょう。
最初にコンストラクタ内の処理を示します。
タイマーイベントを使用するのでイベントハンドラ Timer_Tickを追加しています。またMovingTetriminoクラスとFixedTetriminoクラスのイベントに対応できるようにしています。
MovingTetrimino.TetriminoFixedはミノが設置したときのイベントでMovingTetrimino.CantPutNewTetriminoは積み重なったミノが最上位まで到達してこれ以上積めなくなったときに発生します。FixedTetrimino.LinesDeletingイベントとFixedTetrimino.LinesDeletedは横一列がそろって消去されるときに発生します。すぐに消えるのではなくちょっとだけタイマーをとめます。
CreateOutsideBlocksメソッドは外枠のブロックを描画するときに必要なブロックの位置を求めるためのものです。
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 |
public partial class Form1 : Form { Timer Timer = new Timer(); int TickCount = 0; public Form1() { InitializeComponent(); Text = Application.ProductName + " Ver." + Application.ProductVersion; Timer.Tick += Timer_Tick; Timer.Interval = 1000 / 60; this.BackColor = Color.Black; // 背景は黒 ResetScore(); this.Paint += Form1_Paint; this.DoubleBuffered = true; OutsideBlocks = CreateOutsideBlocks(); FixedTetrimino.Init(); MovingTetrimino.TetriminoFixed += MovingTetrimino_TetriminoFixed; MovingTetrimino.CantPutNewTetrimino += MovingTetrimino_CantPutNewTetrimino; FixedTetrimino.LinesDeleting += FixedTetrimino_LinesDeleting; FixedTetrimino.LinesDeleted += FixedTetrimino_LinesDeleted; ; } 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; } } |
ゲームスタート以降の処理を示します。
ゲームスタートにおける処理は、
(1) FixedTetriminoクラス内のブロックオブジェクトを削除
(2) 一番最初に落ちてくるミノの取得
です。
isGameOveredフラグがクリアされ、キー操作が可能になります。ホールドされているミノがあればクリアして、得点も0にリセットします。そしてタイマーをスタートさせます。
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 |
public partial class Form1 : Form { bool firstGame = true; // 起動直後は「GameOver」と表示しない bool isGameOvered = true; private void MenuItemGameStart_Click(object sender, EventArgs e) { firstGame = false; GameStart(); } void GameStart() { isGameOvered = false; isAllowHold = true; HoldTetoro = TetriminoTypes.None; ResetScore(); // MovingTetrimino.Init()より先に実行しないと // MovingTetrimino.CantPutNewTetriminoイベントが発生する FixedTetrimino.Init(); MovingTetrimino.Init(); Timer.Start(); Invalidate(); } void ResetScore() { Score = 0; } int _score = 0; int Score { get { return _score; } set { // 得点を表示する処理 _score = value; Invalidate(); } } } |
キー操作とタイマーイベントに関する処理を示します。
ミノが一番下に落ちてもすぐにその場に固定されるわけではありません。ロックダウンには0.5秒間の猶予があり、キー操作によってその時間はリセットされます。ただしこれは何回でもできるわけでなく15回までです。ミノが一段下がればこの15回はリセットされます。
↓キーがおされると押している間、ミノは下に落ち続けます。またスペースキーを押すと瞬時に下に落ちて固定されます。
ミノを左右に移動させたりドロップさせる動作はMovingTetriminoクラスでおこないます。
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 |
public partial class Form1 : Form { bool isGameOvered = true; int keyCount = 0; // ミノが一段落ちてから何回キー操作がされたか? // ロックダウンの猶予0.5秒がリセットするのは15回まで protected override void OnKeyDown(KeyEventArgs e) { if (isGameOvered) return; if (e.KeyCode == Keys.Left) MovingTetrimino.MoveLeft(); else if (e.KeyCode == Keys.Right) MovingTetrimino.MoveRight(); else if (e.KeyCode == Keys.Down) SoftDrop = true; else if (e.KeyCode == Keys.Z || e.KeyCode == Keys.ControlKey) MovingTetrimino.RotateL(); else if (e.KeyCode == Keys.X || e.KeyCode == Keys.Up) MovingTetrimino.RotateR(); else if (e.KeyCode == Keys.Space) MovingTetrimino.HardDrop(); else if (e.KeyCode == Keys.C || e.KeyCode == Keys.ShiftKey) Hold(); else if (e.KeyCode == Keys.Escape || e.KeyCode == Keys.F1) Pause(); if (e.KeyCode == Keys.Left || e.KeyCode == Keys.Right || e.KeyCode == Keys.Down || e.KeyCode == Keys.Up || e.KeyCode == Keys.Z || e.KeyCode == Keys.X || e.KeyCode == Keys.ControlKey) { if (TickCount % 60 > 30 && keyCount < 15) TickCount = 30; // 0.5秒の猶予を作る(ロックダウンの猶予0.5秒がリセットするのは15回まで) keyCount++; } Invalidate(); base.OnKeyDown(e); } protected override void OnKeyUp(KeyEventArgs e) { if (e.KeyCode == Keys.Down) { SoftDrop = false; } } bool SoftDrop { set; get; } private void Timer_Tick(object sender, EventArgs e) { TickCount++; if (isGameOvered) { Timer.Stop(); return; } // TickCount 60回で一段下げる if (!SoftDrop && TickCount % 60 == 0) { MovingTetrimino.MoveDown(); keyCount = 0; TickCount = 0; } // SoftDropのときはTimer.Tickイベントのたびに一段下げる if (SoftDrop) { // SoftDropのときは着地していないのであれば下げ続ける if (!IsRockDowning()) MovingTetrimino.MoveDown(); else { // 着地した場合はロックダウンまで0.5秒の猶予をつくる SoftDrop = false; TickCount = 30; } } Invalidate(); } bool IsRockDowning() { List<Block> gohstBlocks = MovingTetrimino.GetGohstBlocks(); List<Block> blocks = MovingTetrimino.GetBlocks(MovingTetrimino.Angle); foreach (Block block in blocks) { if (!gohstBlocks.Any(x => x.PosX == block.PosX && x.PosY == block.PosY)) return false; } return true; } } |
MovingTetrimino.TetriminoFixedイベントが発生したときの処理を示します。ここでやっていることはホールドを可能にすることと(いったんホールドしてしまうと交換したミノをセットするまで次のホールドができない)と、ドロップポイントを加算することです。
1 2 3 4 5 6 7 8 |
public partial class Form1 : Form { private void MovingTetrimino_TetriminoFixed(object sender, EventArgs e) { isAllowHold = true; Score += MovingTetrimino.DropPoint; } } |
ライン消去時の処理を示します。ここではタイマーを一時的に停止(横一列に揃ったブロックが消え、他のブロックが浮かんでいるように見せる)するとともに、得点を追加しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public partial class Form1 : Form { private void FixedTetrimino_LinesDeleting(object sender, EventArgs e) { Timer.Stop(); } private void FixedTetrimino_LinesDeleted(object sender, LinesDeletedArgs e) { Timer.Start(); if (e.LinesCount == 1) Score += 40; if (e.LinesCount == 2) Score += 100; if (e.LinesCount == 3) Score += 300; if (e.LinesCount == 4) Score += 1200; } } |
ホールドの処理です。ホールドされているミノがあるときはそれと入れ替え、ない場合は落下中のミノをホールドして次のミノを降らせます。
MovingTetrimino.PopNext()は次のミノを取り出し、現在のミノにセットします。MovingTetrimino.Typeがセットされると自動的に最上段からおちてくるようになっています(後述)。
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 |
public partial class Form1 : Form { void Hold() { if (!isAllowHold) return; isAllowHold = false; if (HoldTetoro == TetriminoTypes.None) { HoldTetoro = MovingTetrimino.Type; MovingTetrimino.Type = MovingTetrimino.PopNext(); } else { TetriminoTypes oldTetoro = MovingTetrimino.Type; MovingTetrimino.Type = HoldTetoro; HoldTetoro = oldTetoro; } } bool isAllowHold = true; TetriminoTypes _holdTetoro = TetriminoTypes.None; TetriminoTypes HoldTetoro { get { return _holdTetoro; } set { _holdTetoro = value; Invalidate(); } } } |
ポーズ時の処理です。Escキーが押されるとポーズになります。ポーズのときはタイマーを停止させます。ポーズのときにEscキーを押すとポーズが解除されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
public partial class Form1 : Form { bool isPause = false; void Pause() { if (!isGameOvered) { if (Timer.Enabled) { isPause = true; Timer.Stop(); Invalidate(); } else { isPause = false; Timer.Start(); } } } } |
ゲームオーバー判定の処理です。MovingTetrimino.CantPutNewTetriminoイベントが発生したらゲームオーバー処理を行ないます。タイマーを停止させてisGameOveredフラグをセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public partial class Form1 : Form { private void MovingTetrimino_CantPutNewTetrimino(object sender, EventArgs e) { OnGameOver(); } void OnGameOver() { isGameOvered = true; Timer.Stop(); } } |
描画に関する処理です。MovingTetrimino.Draw(Graphics g)メソッド、FixedTetrimino.Draw(Graphics g)メソッド、MovingTetrimino.DrawGohst(Graphics g)メソッドを呼び出してミノを描画させます。
また次回3回分のミノの種類を取得してShowNextTetoros(Graphics g)メソッドでフィールドの右側に描画します。ホールドされているミノや現在の得点を表示するとともに、現在ゲームオーバーやポーズ状態であるならそのように表示させます。
起動直後はisGameOveredフラグがtrueなので「GameOver」と表示されるのですが、一度もゲームをしていないのに起動直後に「GameOver」と表示されるのはおかしいです。そこで起動直後はfirstGameフラグの効果で「GameOver」と表示されないようにしています。
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 |
public partial class Form1 : Form { bool firstGame = true; // 起動直後は「GameOver」と表示しない private void Form1_Paint(object sender, PaintEventArgs e) { foreach (Block block1 in OutsideBlocks) block1.Draw(e.Graphics); MovingTetrimino.Draw(e.Graphics); FixedTetrimino.Draw(e.Graphics); MovingTetrimino.DrawGohst(e.Graphics); ShowNextTetoros(e.Graphics); ShowScore(e.Graphics); ShowHold(e.Graphics); ShowStringGameOver(e.Graphics); ShowStringPause(e.Graphics); } Font nextFont = new Font("MS ゴシック", 12, FontStyle.Bold); void ShowNextTetoros(Graphics g) { g.DrawString("Next", nextFont, new SolidBrush(Color.White), new Point(405, 57)); if (isGameOvered) return; List<TetriminoTypes> tetoroTypes = MovingTetrimino.GetNext7().Take(3).ToList(); g.DrawImage(GetImageFromTetoroType(tetoroTypes[0]), new Point(408, 93)); g.DrawImage(GetImageFromTetoroType(tetoroTypes[1]), new Point(408, 188)); g.DrawImage(GetImageFromTetoroType(tetoroTypes[2]), new Point(408, 282)); } Font scoreFont = new Font("MS ゴシック", 12, FontStyle.Bold); void ShowScore(Graphics g) { g.DrawString(String.Format("Score {0}", _score), scoreFont, new SolidBrush(Color.White), new Point(18, 169)); } void ShowHold(Graphics g) { if (_holdTetoro != TetriminoTypes.None) g.DrawImage(GetImageFromTetoroType(_holdTetoro), new Point(21, 57)); } Image GetImageFromTetoroType(TetriminoTypes types) { if (types == TetriminoTypes.I) return Properties.Resources.i; if (types == TetriminoTypes.J) return Properties.Resources.j; if (types == TetriminoTypes.L) return Properties.Resources.l; if (types == TetriminoTypes.O) return Properties.Resources.o; if (types == TetriminoTypes.S) return Properties.Resources.s; if (types == TetriminoTypes.T) return Properties.Resources.t; if (types == TetriminoTypes.Z) return Properties.Resources.z; return null; } Font gameOverFont = new Font("MS ゴシック", 30, FontStyle.Bold); Font pauseFont = new Font("MS ゴシック", 30, FontStyle.Bold); void ShowStringGameOver(Graphics g) { if (!firstGame && isGameOvered) { Point point = new Point(155, 169); DrawWhiteString(g, "GameOver", gameOverFont, point); } } void ShowStringPause(Graphics g) { if (isPause) { Point point = new Point(185, 169); DrawWhiteString(g, "Pause", pauseFont, point); } } // 文字が描画される部分の背景を黒くしたかった。それだけです・・・ void DrawWhiteString(Graphics g, string str, Font font, Point pt) { //文字列を描画するときの大きさを計測する Size strSize = TextRenderer.MeasureText(g, str, font); //取得した文字列の大きさを使って四角を描画する g.FillRectangle(new SolidBrush(Color.Black), pt.X - 5, pt.Y-3, strSize.Width + 10, strSize.Height+6); g.DrawString(str, font, new SolidBrush(Color.White), pt); } } |
フィールド上に固定されているミノを管理するためのFixedTetriminoクラスを示します。
Init()メソッドは初期化のためのものであり、フィールド変数 List<Block> Blocksをクリアするためのものです。
ExistsBlock(List<Block> blocks)メソッドはわたされたブロックのリストの座標に固定されたミノが存在するかどうかを返します。
DeleteLines()メソッドは横一列にブロックが並んでいるか調べて並んでいる場合はその行のブロックを消去し、LinesDeletingイベントと LinesDeletedイベントを発生させます。
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 |
static public class FixedTetrimino { static public List<Block> Blocks = new List<Block>(); static public void Init() { Blocks.Clear(); } static public bool ExistsBlock(List<Block> blocks) { foreach (Block block in blocks) { if (Blocks.Any(x => x.PosX == block.PosX && x.PosY == block.PosY)) return true; } return false; } static public event EventHandler LinesDeleting; static public event LinesDeletedHandler LinesDeleted; public delegate void LinesDeletedHandler(object sender, LinesDeletedArgs e); 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()); Timer timer = new Timer(); timer.Interval = 100; timer.Tick += Timer_Tick; timer.Start(); void Timer_Tick(object sender, EventArgs e) { Timer t = (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(); } } static List<int> GetDeleteLineNums() { List<int> lineNums = new List<int>(); for (int row = 0; row < Constant.FieldHeight; row++) { List<Block> lineBlocks = Blocks.Where(x => x.PosY == row).ToList(); if (lineBlocks.Count() == Constant.FieldWidth) lineNums.Add(row); } return lineNums; } 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)); } } } |
イベントハンドラ LinesDeletedHandlerに渡される引数 LinesDeletedArgs を示します。消されるラインの数を送るときに使います。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class LinesDeletedArgs : EventArgs { public LinesDeletedArgs(int linesCount) { LinesCount = linesCount; } public int LinesCount { get; protected set; } } |