Siv3Dで15パズルを極限までハイクオリティーにしてみる

この記事では「Siv3Dで15パズルを作って、極限までハイクオリティーにしてみる」を目標としてます。
極限といってもキリがないので僕の技術力と発想力の限りを尽くしていきたいと思います。

[完成版のソースコード](https://github.com/hota1024/15Puzzle_with_scene_for_siv3d_ac)はgithubで公開しています。
ソースコードがかなりスパゲッティなので見ると気分を悪くするかもしれませんのでご注意ください。
プロジェクトを開く場合はgithub上のリポジトリのなかのslnファイルをvisual studioで開いてください。

githubへのリンクはところどころに書いています。

# 15パズルとは
4✕4のマスにランダム並べられた1～15までの数字を順番に並び替えるパズルゲームです。

# 使うもの
| 名前 | 説明 |
|:-:|:-:|
| Visual studio Express 2015 for Windows Desktop | MS製のIDE  |
| C++  | 僕の中では一番好きな言語  |
| Siv3D | 僕の中では一番好きなC++ライブラリ。 |

# 最低限作ってみる
とりあえず動かせる状態のものを作ってみました。

コード量が多いいのでgithubに置きました。
[ソースコード(1/4)](https://github.com/hota1024/15Puzzle_for_siv3d_ac)

とりあえずクリックか矢印キーで操作できるようにして、クリア画面とかまだない状態です。

ここでさっとプログラムの説明。

この15パズルでは数字を`Tile`クラスにして盤面を`Board`クラスにしました。

`Tile`クラスの宣言だけここに載せておきます。

```Tile.h
class Tile
{
	int32 number;
	Font font{ Size / 2 };
	Vec2 drawPos;
	Vec2 pos;
public:
	Tile();
	Tile(int32 number);
	~Tile();

	const static int32 Size;

	void draw(Vec2 start);
	Rect rect(Vec2 start);

	int32 getNumber();
	Tile &setNumber(int32 number);

	Vec2 getPos();
	Tile &setPos(Vec2 pos);

	bool isEmpty();
	bool isNumber();
};
```

ここで主要メンバ変数の`number`の説明だけします。
`number`メンバ変数は自身の番号です。
この`number`が`0`だと空白のタイルになって`-1`だと壁（なんの役割のないタイル）で、それ以外は普通の番号タイルです。

次は`Board`クラスの宣言を紹介したいと思います。

```Board.cpp
class Board
{
	const static uint16 width = 4 + 2;
	const static uint16 height = 4 + 2;
	const static uint16 size = width * height;

	Tile tiles[size];
public:
	Board();
	~Board();

	Board &init();
	Board &shuffle(uint32 count);

	void update();
	void draw();

	void keyInput();

	Tile &findTile(uint32 number);
	Tile &getPointTile(Vec2 pos);
	Tile &emptyTile();

	bool MoveTile(Vec2 pos);


	static Vec2 GetDrawStartPos();
};
```

`width`と`height`に`+2`があるのは壁のぶんのカウントです。

壁とは、以下のようなプログラムがあるとします。

```C++
int32 array = {0,1,2,3,4,5,6,7,8,9};

size_t i = 0;
while(array[i] != 8){
    i++;
}
```

このプログラムは`array`の中の`8`のインデックスを取得するプログラムです。
しかし、もしこの`array`に**`8`が含まれない場合はどうなるでしょう？**
もちろん無限ループにハマります。

そこで**壁**の出番です。
先程のプログラムに壁を作ってあげると以下のようになります。

```C++
int32 array = {-1,0,1,2,3,4,5,6,7,8,9,-1};

size_t i = 0;
while(array[i] != 8 || array[i] != -1){
    i++;
}
```

壁は`array`の中の`-1`に当たります。
`while`ループ中に`array[i] == -1`というのがあります。これは`array`の`i`番目が壁じゃない間繰り返すという意味になります。

この`Board`クラスでは`tiles`配列にタイルを管理します。
この`tiles`配列は1次元配列のため色々と不便です。

そこで`Board`クラス内ではかなり重要な`getPointTile(Vec2 pos)`について説明します。

`getPointTile(Vec2 pos)`は`tiles`配列の`pos`にあるタイルの**参照**を返します。

次に主要メソッドの`MoveTile(Vec2 pos)`関数について説明します。

`MoveTile(Vec2 pos)`関数は引数の`pos`を動かせるなら動かして`true`を返し、動かせないなら`false`を返すようになっています。

先程の`getPointTile`関数でposの上下左右に空白のタイルがあるなら、動かせるという判定プログラムです。

次に紹介したいのが`shuffle(uint32 count)`関数です。
この関数は`tiles`配列を`count`回シャッフルします。

なぜ`tiles`配列にランダムに数字を入れていくのではなくシャッフルをするかというと、**絶対に解けないパズルが出てきてしまう**からです。

最後の最後で以下のような盤面になってしまうのが一つの例です。

![](https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/15-puzzle-loyd.svg/920px-15-puzzle-loyd.svg.png)
wiki参照。

なので、正解の状態からランダムにシャッフルしていくことで必ずクリアできるパズルを生成しています。

とりえあずここまでがこの15パズルのプログラムの解説です。

ここから見た目や機能を僕の技術力と発想力(笑)の限りを尽くしてハイクオリティーにしていきたいと思います。

# アニメーションをつけてみる
今度はSiv3Dのeasing機能を使ってアニメーションをつけてみたいと思います。

[ソースコード(2/4)](https://github.com/hota1024/15Puzzle_with_animations_for_siv3d_ac)
とりあえずシャッフルと動かすときにアニメーションをつけてみました。

結構気に入ってます。

実際にeasingを利用している部分は以下のとおりです。

```Tile(36行目).cpp
void Tile::update(float addtime) {
	nowDrawPos = EaseIn(deltaDrawPos, drawPos, Easing::Quad, time);
	if (!isAnimation()) return;
	time += addtime;
	if (time >= 1.0) {
		time = 1;
		m_isAnimation = false;
	}
}
```

`EaseIn`関数がそうです。

`EaseIn`の引数は`開始位置,終点位置,イージング関数,時間(0-1)`です。

先程のプログラムだと自分自身が動いていたら、イージングで目標位置まで移動する感じです。

# 見た目を調整してみる
アニメーションもついたところで今のままでは味気ないので見た目を調整してみます。

[ソースコード(3/4)](https://github.com/hota1024/15Puzzle_with_colors_for_siv3d_ac)

実行するたびに色が違います。

```Board.cpp
Board &Board::init() {
	state = State::Shuffle;
	shuffleCount = 0;
	uint32 number = 1;
	uint32 index = 0;
	for (size_t y = 0; y < height; y++)
	{
		for (size_t x = 0; x < width; x++)
		{
			if (y == 0 || y == height - 1 || x == 0 || x == width - 1) {
				tiles[index] = Tile()
					.setNumber(-1)
					.setPos({ x,y }, true);
			}
			else {
				tiles[index] = Tile()
					.setNumber(number == (width - 2) * (height - 2) ? 0 : number)
					.setPos({ x, y }, Vec2(Tile::Size * width / 2, Tile::Size * height / 2))
					.setColor(MaterialPalette::List[Random(0, (int)MaterialPalette::List.size() - 1)][5], MaterialPalette::List[Random(0, (int)MaterialPalette::List.size() - 1)][5]);
				number++;
			}
			index++;
		}
	}
	return *this;
}
```
の

```Board(42行目).cpp
.setColor(MaterialPalette::List[Random(0, (int)MaterialPalette::List.size() - 1)][5], MaterialPalette::List[Random(0, (int)MaterialPalette::List.size() - 1)][5]);
```
で色をセットしています。
`MaterialPalette`は僕の作ったSiv3DでMaterialDesignの色を扱えるようにするライブラリです。良ければ使ってみてください[MaterialPalette for Siv3D](https://github.com/hota1024/MaterialPalette-for-Siv3D)（宣伝）

乱数は`Random()`関数で生成しています。
引数は`Random(最小値,最大値)`です。

そして実行してみるとわかりますが、タイルにグラデーションをかけています。
<img width="640" alt="スクリーンショット 2017-12-03 時刻 21.41.26.png" src="https://qiita-image-store.s3.amazonaws.com/0/154299/1469cb3f-8c92-606e-06ce-9f8167b5d8f6.png">

これは

```Tile.cpp
void Tile::draw(Vec2 start,int index) {
	if (number < 0) return;
	if (number == 0) return;
	Rect((start + nowDrawPos).asPoint(), Size).draw({
		c_start,c_end,
		c_start,c_end
	});

	Vec2 fontpos = start + nowDrawPos + Vec2(Size, Size) / 2;
	font(number).drawAt(start + nowDrawPos + Vec2(Size, Size) / 2, MaterialPalette::White);
}
```

の

```Tile(31~34行目).cpp
Rect((start + nowDrawPos).asPoint(), Size).draw({
		c_start,c_end,
		c_start,c_end
	});
```

でかけています。

`Rect`の`draw`の第一引数に波括弧で色を4つ指定するとグラデーションをかけることができます。

`Rect`だけではなく`Line`や`Triangle`でも使うことができます。

# スタート画面とクリア画面を作る（製作中・・・）
ここで肝心のスタート画面と結果画面を作っていきます。
画面遷移にはHamFrameworkの`SceneManager`を使います。

HamFrameworkはSiv3Dに付属されているためすぐに使うことができます。
[ソースコード(4/4 完成)](https://github.com/hota1024/15Puzzle_with_scene_for_siv3d_ac)

まず`SceneManager`の定義からです。
`SceneManager`では複数の`Scene`を管理します。
定義には以下の2つの情報が必要です。
`Scene`の識別子
`共通データ`

実際のコードを見てみましょう。

```GameSceneManager.h
#pragma once
#include <Siv3D.hpp>
#include <HamFramework.hpp>
#include "Board.h"

enum class SceneCode {
	Title,
	Game,
	Result
};

struct SceneData {
	int moveCount = 0;
	Board board;
};

using GameSceneManager = SceneManager<SceneCode, SceneData>;
```

まず`Scene`の識別子（ここでは`enum class`）を定義します。
次に`共通データ（ここでは`struct SceneData{...}`）`です。

最後に`using GameSceneManager = SceneManager<SceneCode, SceneData>`で完成です。

次に`Scene`の定義です。

`ResultScene`のコードを見てみましょう。

```ResultScene.h
#pragma once
#include "GameSceneManager.h"
#include "MaterialPalette.h"
#include "BackgroundEffect.h"
#include <Siv3D.hpp>

class ResultScene : public GameSceneManager::Scene
{
	Font font{ 64 };
	Font count{ 48 };
	Font title{ 32 };
public:
	ResultScene();
	~ResultScene();

	void update() override;
	void draw() const override;
};
```

先程の`GameSceneManager`の`Scene`を継承しています。
そして`update`と`draw`をオーバーライドして定義すれば完成です。

共通データは以下のように使います。

```ResultScene(24行目).cpp
count(m_data->moveCount).drawCenter({Window::Center().x,200});
```

`m_data`でアクセスできます。もちろん読み書き可能です。

# まとめ
ずいぶん長い記事になってしまいました。
今回はハイクオリティーには程遠いものができましたが、自分もプログラミングしててすごく楽でした。

あとはデザインと発想が足らない・・・

また時間があれば続編も作ります。それまでもうちょっと勉強してみます。

つぎは[Oyakodon](https://qiita.com/Oyakodon)さんです。よろしくお願いします。

ありがとうございました。