Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

この記事では「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クラスの宣言だけここに載せておきます。

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メンバ変数は自身の番号です。
このnumber0だと空白のタイルになって-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();
};

widthheight+2があるのは壁のぶんのカウントです。

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

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

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

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

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

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というのがあります。これはarrayi番目が壁じゃない間繰り返すという意味になります。

この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配列にランダムに数字を入れていくのではなくシャッフルをするかというと、絶対に解けないパズルが出てきてしまうからです。

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


wiki参照。

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

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

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

アニメーションをつけてみる

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

ソースコード(2/4)
とりあえずシャッフルと動かすときにアニメーションをつけてみました。

結構気に入ってます。

実際に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)

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

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(宣伝)

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

そして実行してみるとわかりますが、タイルにグラデーションをかけています。
スクリーンショット 2017-12-03 時刻 21.41.26.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
    });

でかけています。

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

RectだけではなくLineTriangleでも使うことができます。

スタート画面とクリア画面を作る(製作中・・・)

ここで肝心のスタート画面と結果画面を作っていきます。
画面遷移にはHamFrameworkのSceneManagerを使います。

HamFrameworkはSiv3Dに付属されているためすぐに使うことができます。
ソースコード(4/4 完成)

まず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;
};

先程のGameSceneManagerSceneを継承しています。
そしてupdatedrawをオーバーライドして定義すれば完成です。

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

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

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

まとめ

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

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

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

つぎはOyakodonさんです。よろしくお願いします。

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

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away