見出し画像

テトリスでプログラミングを試そう(13)

すみません、新年度に入ってちょっと忙しくなってきましたので、この連載はしばらくの間更新が遅くなります。
1,2週間、間が空くかもしれませんが、どうぞ気長によろしくお願いします。


さて、プログラムが長くなってきて、そろそろ疲れてきましたか??
せっかくここまで来ましたから、是非一緒に最後まで作ってみてもらえると嬉しいです!
私の方も、思いつくままにコードを書いてきたので、果たして最後までできるかな?と思っていたのですが、最後までいけそうです^^。
実は、あの部分はこうしておけば良かったなぁ、と思うところが出てきているのですが、とりあえずこのままいっちゃいましょうw。
今の感覚では、連載が 20回になる前には終わるかな?と思っています。

それでは、今日は平行移動したときの衝突判定をしてみましょう!


衝突判定は Px_8int を用いても簡単にできるのですが、練習のために ColorField に対応しやすいデータ型を1つ用意します。

MainPage.xaml.cs の適当な場所に追加してください。

これは、ColorField と同様に、左上から右下に向かって 0,1,2,3,… という形で場所を記録するものとします。
ColorField の横方向は 12マスであるので、例えば 14 であるなら、上から2マス目、左から3マス目、を表すことになります。

class Mino に、この情報を追加しましょう。

L170, L171 の2行を追加してください。

L170 の _fpos は、現在表示するミノが、ColorField で見てどの場所にあるのかを記録することにします。
ミノは4マス×4マスに収まるため、_fpos_4int_cur に記録される値は 0以上 39以下になります。
class Mino の Init() で、_fpos_4int_cur に値を設定しましょう。

L191 - 196 を追加してください。

では、この情報を利用して、衝突判定を行ってみましょう。
現在、class は MainPage, ColorField, Mino, Game の4つありますが、
新しい機能を追加する場合、その機能を誰が担当すると良いか、というのを考えてみてほしいです。


衝突判定は、ゲーム画面全体のブロックの情報を持っている ColorField に担当させるのが良いでしょう。
では、ColorField に以下の関数 CheckToPut() を追加します。

class ColorField に追加してください。

まず、bool CheckToPut() と最初に書いてあるので、CheckToPut() は bool値(true または false)を返すのだな、と分かって下さい。

CheckToPut() には、現在のミノの場所を示す fpos と、ミノの4つの四角形の位置を fpos_4int で渡して、ミノを置けるかどうか判定してもらいます。ミノを置こうとしている場所に1ヶ所でも Black 以外の色があったら置くことができない、と判定します。
ミノを置けるならば true を、置けないならば false を返すことにします。


次に考えたいことは、CheckToPut() をどこで呼び出すか、ですね。
これは、移動をするときに、移動する前に移動できるかどうか判定する必要がある ので、移動を実行している場所に割り込ませましょう。

例えば、class Mino の中にある下の関数に手を入れましょう。

class Mino の中にある関数です。

左に動く前に、左に動けるかどうか判定させましょう。

左に動きたいので、_fpos の値を1つ減らしたものを CheckToPut() に渡して、その場所に移動できるかどうか判定してもらいたい のですが、CheckToPut() は class ColorField の関数です。
上図の「???」に当てはめられるものが必要になるため、class Mino に、もう1つ情報を追加します。

L176 に1行追加しました。

◆ 上図の L176 の static について
class Mino を利用して、T ミノや J ミノなど5つのミノを new によって作成しました。
これら5つのミノは、すべて同じ ColorField を利用するので、ColorField の情報は全員で共有できます。
このように、全員で 共有できる情報 には static と印を付けておくと良いですよ。もちろん static にしなくてもプログラムを動作させることはできますので、そうなんだ、程度の気楽な気持ちで聞いてください。
static と付けるメリット の1つは、new によって Mino を生成しなくても
Mino.s_color_field(クラス名.s_color_field)と書いて、その情報にアクセスできるようになる点です。
言葉の説明では分かりにくいと思うので、以下のように書ける、ということで理解してほしいです。

MainPage() に、L18 の1行を追加します。

このようにして ColorField の情報を設定することにより、先程の「???」のところに s_color_field と書くことができるようになります。

class Mino の Move_L() は、最終的にこのようになります。

Move_L() は、移動に成功したら true を返し、移動できなかったら false を返すようにしました。
これは、移動できなかった場合、画面の書き換えが必要ない ので、
class MainPage の OnClicked_MoveL() を以下のようにしたかったからです。

移動できた場合のみ、画面の書き換えを行うようにしました。

F5 キーを押して実行してみて、左に移動させたとき、左の壁で移動できなくなることを確認してみて下さい。
回転のときの処理を入れてないので、回転ボタンは押さないでくださいね!

左の壁に当たると左に動けなくなりました!

あとは、面倒ですが、右と下に移動したときにも同じような処理を入れましょう。
まず、class Mino の Move_R() と Move_D() を以下のように変更します。

Move_D() で、1つ下への移動を考えるため 12 を足していますが、本当はこういう数字は ColorField からもらってきた方が良いです。すみません、手抜きしてます、、、。
最後に、class MainPage の OnClicked_… も以下のように書き換えて出来上がりです!

F5 キーを押して、下や右の壁にもぶつかるようになったことを是非確かめてみてほしいです^^。
まだ、回転はさせないでくださいね。
あと、今の状態だったら、Down To Bottom を押すとミノを変更できるので、いろいろなミノで試してみてもらえませんか?

下や右の壁との衝突判定もできるようになりました!

ここまで作っただけも、オブジェクト指向のエッセンスが伝わり始めたかと思います。
オブジェクト指向では、他の class のものに何かを伝えたい場合、その相手の情報が必要になり、少し手間がかかります。
しかし、誰が誰に情報を伝えているのか明確になるため、バグなどが発生したときに対処しやすくなりますよ。
完成まであともう少しかかりますが、是非一緒にやり遂げましょうね!

いろいろとプログラムに変更があったので、今現在の状態のコードを貼り付けておきますね。
それでは。

namespace MauiTetris;

public partial class MainPage : ContentPage
{
	readonly ColorField _colorField;
	readonly Game _game;
	Mino _mino_cur;

	public MainPage()
	{
		InitializeComponent();

		X_ColorField.WidthRequest = ColorField.Px_WIDTH_FIELD;
		X_ColorField.HeightRequest = ColorField.Px_HEIGHT_FIELD;

		_colorField = Resources.TryGetValue("R_ColorField", out object x) ? x as ColorField : null;
		_colorField.Init(this);
		Mino.s_color_field = _colorField;

		_game = new Game();

		_mino_cur = _game.GetNextMino();
	}

	public void NotifyDraw(ICanvas canvas)
	{
		_mino_cur.Draw(canvas);
	}

	public void OnClicked_MoveL(object sender, EventArgs e)
	{
		if (_mino_cur.Move_L() == true) { X_ColorField.Invalidate(); }
	}

	public void OnClicked_MoveR(object sender, EventArgs e)
	{
		if (_mino_cur.Move_R() == true) { X_ColorField.Invalidate(); }
	}

	public void OnClicked_MoveD(object sender, EventArgs e)
	{
		if (_mino_cur.Move_D() == true) { X_ColorField.Invalidate(); }
	}

	public void OnClicked_RotateL(object sender, EventArgs e)
	{
		_mino_cur.Rotate_L();
		X_ColorField.Invalidate();
	}

	public void OnClicked_RotateR(object sender, EventArgs e)
	{
		_mino_cur.Rotate_R();
		X_ColorField.Invalidate();
	}

	public void OnClicked_DownToBtm(object sender, EventArgs e)
	{
		_mino_cur = _game.GetNextMino();
		X_ColorField.Invalidate();
	}
}


////////////////////////////////////////////////////////////////////////////////
public class ColorField : IDrawable
{
	const int PCS_COLUMN = 10;
	const int PCS_ROW = 20;

	const int PCS_COLOR_FIELD = (PCS_COLUMN + 2) * (PCS_ROW + 1);
	Color[] _colorField = new Color[PCS_COLOR_FIELD];

	public const int Px_WIDTH_FIELD = Game.Px_BLOCK * (PCS_COLUMN + 2);
	public const int Px_HEIGHT_FIELD = Game.Px_BLOCK * (PCS_ROW + 1);

	MainPage _main_page = null;

	public void Init(MainPage main_page)
	{
		_main_page = main_page;
	}

	public ColorField()
	{
		for (int i = 0; i < PCS_COLOR_FIELD; i++)
		{ _colorField[i] = Colors.Black; }

		for (int idx = 0; idx < PCS_COLOR_FIELD; idx += PCS_COLUMN + 2)
		{
			_colorField[idx] = Colors.LightGray;
			_colorField[idx + PCS_COLUMN + 1] = Colors.LightGray;
		}

		for (int idx = PCS_COLOR_FIELD - 2, cnt = PCS_COLUMN; cnt > 0; idx--, cnt--)
		{
			_colorField[idx] = Colors.LightGray;
		}
	}

	public void Draw(ICanvas canvas, RectF dirtyRect)
	{
		canvas.FillColor = Colors.Black;
		canvas.FillRectangle(0, 0, Px_WIDTH_FIELD, Px_HEIGHT_FIELD);

		int idx = 0;
		for (int y = 1; y < Px_HEIGHT_FIELD; y += Game.Px_BLOCK)
		{
			for (int x = 1; x < Px_WIDTH_FIELD; x += Game.Px_BLOCK, idx++)
			{
				if (_colorField[idx] == Colors.Black) { continue; }

				canvas.FillColor = _colorField[idx];
				canvas.FillRectangle(x, y, Game.Px_INNER_BLOCK, Game.Px_INNER_BLOCK);
			}
		}

		_main_page.NotifyDraw(canvas);
	}

	public bool CheckToPut(int fpos, FPos_4int fpos_4int)
	{
		int[] _4int = fpos_4int._4int;
		for (int i = 0; i < 4; i++)
		{
			if (_colorField[fpos + _4int[i]] != Colors.Black) { return false; }
		}
		return true;
	}
}


////////////////////////////////////////////////////////////////////////////////
public class Game
{
	public const int Px_BLOCK = 25;
	public const int Px_INNER_BLOCK = Px_BLOCK - 2;

	readonly Mino[] _minos = new Mino[7];
	readonly Random _rand = new Random();

	public Game()
	{
		_minos[0] = new Mino(Colors.Violet, new int[] {1, 0, 0, 1, 1, 1, 2, 1});
		_minos[1] = new Mino(Colors.Blue,   new int[] {0, 0, 0, 1, 1, 1, 2, 1});
		_minos[2] = new Mino(Colors.Orange, new int[] {0, 1, 1, 1, 2, 1, 2, 0});
		_minos[3] = new Mino(Colors.Green,  new int[] {0, 1, 1, 0, 1, 1, 2, 0});
		_minos[4] = new Mino(Colors.Red,    new int[] {0, 0, 1, 0, 1, 1, 2, 1});
		_minos[5] = _minos[0];
		_minos[6] = _minos[1];
	}

	public Mino GetNextMino()
	{
		Mino ret_mino = _minos[_rand.Next(0, 7)];
		ret_mino.Init();
		return ret_mino;
	}
}


////////////////////////////////////////////////////////////////////////////////
public class Px_8int
{
	public int[] _8int = new int[8];
}

public class FPos_4int
{
	public int[] _4int = new int[4];
}

public class Mino
{
	readonly Color _color;
	readonly int[] _init_8int;
	public static ColorField s_color_field;

	int _px_x, _px_y;
	Px_8int _px_8int_cur = new Px_8int();

	int _fpos;
	FPos_4int _fpos_4int_cur = new FPos_4int();

	public Mino(Color color, int[] init_8int)
	{
		_color = color;
		_init_8int = init_8int;
	}

	public void Init()
	{
		_px_x = 4 * Game.Px_BLOCK + 1;
		_px_y = 1;

		int[] dst_8int = _px_8int_cur._8int;
		for (int idx = 0; idx < 8; idx += 2)
		{
			dst_8int[idx] = _init_8int[idx] * Game.Px_BLOCK;
			dst_8int[idx + 1] = _init_8int[idx + 1] * Game.Px_BLOCK;
		}

		_fpos = 4;
		int[] fpos_4int = _fpos_4int_cur._4int;
		for (int i = 0, idx = 0; i < 4; i++, idx += 2)
		{
			fpos_4int[i] = _init_8int[idx + 1] * 12 + _init_8int[idx];
		}
	}

	public void Draw(ICanvas canvas)
	{
		canvas.FillColor = _color;

		int[] px_8int = _px_8int_cur._8int;
		for (int idx = 0; idx < 8; idx += 2)
		{
			canvas.FillRectangle(_px_x + px_8int[idx], _px_y + px_8int[idx + 1]
										, Game.Px_INNER_BLOCK, Game.Px_INNER_BLOCK);
		}
	}

	public bool Move_L()
	{
		if(s_color_field.CheckToPut(_fpos - 1, _fpos_4int_cur) == true)
		{
			_fpos--;
			_px_x -= Game.Px_BLOCK;
			return true;
		}
		return false;
	}

	public bool Move_R()
	{

		if(s_color_field.CheckToPut(_fpos + 1, _fpos_4int_cur) == true)
		{
			_fpos++;
			_px_x += Game.Px_BLOCK;
			return true;
		}
		return false;
	}

	public bool Move_D()
	{
		if(s_color_field.CheckToPut(_fpos + 12, _fpos_4int_cur) == true)
		{
			_fpos += 12;
			_px_y += Game.Px_BLOCK;
			return true;
		}
		return false;
	}

	public void Rotate_L()
	{
		int[] _8int = _px_8int_cur._8int;
		for (int idx = 0; idx < 8; idx += 2)
		{
			int old_x = _8int[idx];
			_8int[idx] = _8int[idx + 1];
			_8int[idx + 1] = Game.Px_BLOCK * 2 - old_x;
		}
	}

	public void Rotate_R()
	{
		int[] _8int = _px_8int_cur._8int;
		for (int idx = 0; idx < 8; idx += 2)
		{
			int old_x = _8int[idx];
			_8int[idx] = Game.Px_BLOCK * 2 - _8int[idx + 1];
			_8int[idx + 1] = old_x;
		}
	}
}

この記事が気に入ったらサポートをしてみませんか?