この記事では「Siv3Dで15パズルを作って、極限までハイクオリティーにしてみる」を目標としてます。
極限といってもキリがないので僕の技術力と発想力の限りを尽くしていきたいと思います。
完成版のソースコードは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)
とりあえずクリックか矢印キーで操作できるようにして、クリア画面とかまだない状態です。
ここでさっとプログラムの説明。
この15パズルでは数字をTile
クラスにして盤面をBoard
クラスにしました。
Tile
クラスの宣言だけここに載せておきます。
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
クラスの宣言を紹介したいと思います。
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
があるのは壁のぶんのカウントです。
壁とは、以下のようなプログラムがあるとします。
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
が含まれない場合はどうなるでしょう?**
もちろん無限ループにハマります。
そこで壁の出番です。
先程のプログラムに壁を作ってあげると以下のようになります。
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
配列にランダムに数字を入れていくのではなくシャッフルをするかというと、絶対に解けないパズルが出てきてしまうからです。
最後の最後で以下のような盤面になってしまうのが一つの例です。
なので、正解の状態からランダムにシャッフルしていくことで必ずクリアできるパズルを生成しています。
とりえあずここまでがこの15パズルのプログラムの解説です。
ここから見た目や機能を僕の技術力と発想力(笑)の限りを尽くしてハイクオリティーにしていきたいと思います。
アニメーションをつけてみる
今度はSiv3Dのeasing機能を使ってアニメーションをつけてみたいと思います。
ソースコード(2/4)
とりあえずシャッフルと動かすときにアニメーションをつけてみました。
結構気に入ってます。
実際にeasingを利用している部分は以下のとおりです。
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)
です。
先程のプログラムだと自分自身が動いていたら、イージングで目標位置まで移動する感じです。
見た目を調整してみる
アニメーションもついたところで今のままでは味気ないので見た目を調整してみます。
実行するたびに色が違います。
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;
}
の
.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(宣伝)
乱数はRandom()
関数で生成しています。
引数はRandom(最小値,最大値)
です。
そして実行してみるとわかりますが、タイルにグラデーションをかけています。
これは
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);
}
の
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 完成)
まずSceneManager
の定義からです。
SceneManager
では複数のScene
を管理します。
定義には以下の2つの情報が必要です。
Scene
の識別子
共通データ
実際のコードを見てみましょう。
#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
のコードを見てみましょう。
#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
をオーバーライドして定義すれば完成です。
共通データは以下のように使います。
count(m_data->moveCount).drawCenter({Window::Center().x,200});
m_data
でアクセスできます。もちろん読み書き可能です。
まとめ
ずいぶん長い記事になってしまいました。
今回はハイクオリティーには程遠いものができましたが、自分もプログラミングしててすごく楽でした。
あとはデザインと発想が足らない・・・
また時間があれば続編も作ります。それまでもうちょっと勉強してみます。
つぎはOyakodonさんです。よろしくお願いします。
ありがとうございました。