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

Siv3D でドット絵エディタを作ろう

More than 3 years have passed since last update.

siv3d_dotedit.gif

はじめに

Siv3D を使うと、C++ でゲームやツールを簡単に作成することができます。Siv3D の便利なクラス群を活用して、ドット絵エディタを作成してみましょう。

今回作成するドット絵エディタでは、こんな機能を実装してみます。

  • ドット絵を表示する
  • ドット絵のグリッドを表示する
  • クリックしたドットの色を変える
  • ドット絵を保存する
  • パレットから色を選ぶ

ドット絵を表示しよう、その前に

まず最初に、エディタの外観を大まかに決めます。幅 16 × 高さ 16 のドット絵を、1 ドットあたり 32 ピクセルのサイズに拡大して表示することにします。

あとはウィンドウの大きさ、背景色を自分好みに設定します。
:information_source: ウィンドウと背景

Main.cpp
#include <Siv3D.hpp>

void Main()
{
    Window::Resize(640, 530);

    Graphics::SetBackground(Palette::Peachpuff);

    // 1マスの大きさ
    const int GridSize = 32;

    // 横方向のマスの数
    const int W = 16;

    // 縦方向のマスの数
    const int H = 16;

    while (System::Update())
    {
    }
}

ドット絵を表示しよう

Siv3D には、画像を編集するための Image クラスがあります。今回はこのクラスを使ってドット絵を編集することにしましょう。
:information_source: 画像編集

幅 16 × 高さ 16 で、中身が真っ白の画像を作成します。

    Image image(W, H, Palette::White);

ただ、Image クラスは画像の編集はできても表示はできません。画像を表示するには Texture クラスか、または描き換えが可能な DynamicTexture を使用します。今回は頻繁にテクスチャを書き換える事になりそうので、 DynamicTexture を使うことにします。
:information_source: テクスチャを描くスケッチ

以下のようにして、既存の画像からテクスチャを生成します。

    DynamicTexture texture(image);

テクスチャの表示には Texture::draw() 関数を使用します。また、今回はドット絵を GridSize 倍に拡大して表示するので、draw() の前に Texture::scale() 関数で拡大しましょう。

以下のようにして、左から 10px、上から 10px の位置に、拡大されたテクスチャを表示します。

    texture.scale(GridSize).draw(10, 10);

:warning: テクスチャを拡大表示すると、ぼやけた感じになると思います。これをくっきりさせたい時は、サンプラーステートの変更 をします。

    Graphics2D::SetSamplerState(SamplerState::WrapPoint);

ここまでをまとめると、ソースコードは以下のようになります。真っ白な画像が表示されたでしょうか。Palette::White のところを編集して、画像の色が変わることを確認してみましょう。

#include <Siv3D.hpp>

void Main()
{
    Window::Resize(640, 530);

    Graphics::SetBackground(Palette::Peachpuff);

    Graphics2D::SetSamplerState(SamplerState::WrapPoint);

    // 1マスの大きさ
    const int GridSize = 32;

    // 横方向のマスの数
    const int W = 16;

    // 縦方向のマスの数
    const int H = 16;

    // ドット絵
    Image image(W, H, Palette::White);

    // ドット絵表示用のテクスチャ
    DynamicTexture texture(image);

    while (System::Update())
    {
        // ドット絵を描画
        texture.scale(GridSize).draw(10, 10);
    }
}

00.png

グリッドを表示しよう

次に、ドット絵エディタらしくグリッドを表示してみましょう。大きさが GridSize の矩形(Rect = Rectangle クラス)を敷き詰めればよさそうです。

Rectangle::drawFrame() 関数を使って、灰色の枠を描いています。
:information_source: 長方形の枠を描く

    for (int i : step(H))
    {
        for (int j : step(W))
        {
            // グリッドを描画
            Rect dot(10 + j*GridSize, 10 + i*GridSize, GridSize);
            dot.drawFrame(1.0, 1.0, Palette::Gray);
        }
    }

01.png

クリックしたドットの色を変えてみよう

Rectangle のような図形のクラスには、クリックされたかどうかを知るためのプロパティ Rectangle::leftClickedRectangle::rightClicked がありますので、それを利用します。
:information_source: 図形とクリック

クリックの判定には、グリッド表示に使った Rect を流用すると良さそうですね。左クリックされたドットを黒に、右クリックされたドットを白にしてみましょう。

画像のピクセルの色を変更するときは、Image::operator[] を使用します。
:information_source: 画像のピクセルに色を書き込む

:warning: 画像を編集したあとは、忘れずに表示用の texture 変数を描き換えましょう。テクスチャに画像を描き込むには、DynamicTexture::fill() 関数を使用します。

    for (int i : step(H))
    {
        for (int j : step(W))
        {
            // グリッドを描画
            Rect dot(10 + j*GridSize, 10 + i*GridSize, GridSize);
            dot.drawFrame(1.0, 1.0, Palette::Gray);

            // ドットがクリックされていたら、ドットの色を変更する
            if (dot.leftClicked)
            {
                image[i][j] = Palette::Black;
                texture.fill(image);
            }
            else if (dot.rightClicked)
            {
                image[i][j] = Palette::White;
                texture.fill(image);
            }
        }
    }

bw.gif

ドット絵を保存してみよう

ではここで画像を保存できるようにしてみましょう。「保存」ボタンを表示しておいて、クリックされたら「名前をつけて保存」ダイアログを出し、指定されたファイル名で保存するようにします。

まずは「保存」ボタンを表示します。ボタン本体の表示には Rect を、「保存」の文字の表示には Font クラスを使います。

下記のようにして、画面右上のスペースにボタンを描きます。Font::drawCenter() 関数を使うと、指定した座標が中心になるように文字を描くことができるので、矩形の中心 Rectangle::center を指定して、ボタンの中央に文字を表示します。
:information_source: テキストを描く#中央に表示する

    //ボタンのラベル用フォント
    Font font(12);

    // ・・・途中省略・・・

        // 保存ボタン
        Rect saveButton(531, 10, 100, 30);
        saveButton.draw(Palette::Whitesmoke).drawFrame(1.0, 1.0, Palette::Black);
        font(L"保存").drawCenter(saveButton.center, Palette::Black);

このボタンがクリックされた時に、「名前をつけて保存」ダイアログを出し、指定されたファイル名で画像を保存するには以下のようにします。
:information_source: ダイアログ#名前を付けて画像を保存する

        if (saveButton.leftClicked)
        {
            const auto save = Dialog::GetSaveImage();
            if (save)
            {
                image.save(save.value());
            }
        }

ここまでをまとめると、ソースコードは以下のようになります。

#include <Siv3D.hpp>

void Main()
{
    Window::Resize(640, 530);

    Graphics::SetBackground(Palette::Peachpuff);

    Graphics2D::SetSamplerState(SamplerState::WrapPoint);

    // 1マスの大きさ
    const int GridSize = 32;

    // 横方向のマスの数
    const int W = 16;

    // 縦方向のマスの数
    const int H = 16;

    // ドット絵
    Image image(W, H, Palette::White);

    // ドット絵表示用のテクスチャ
    DynamicTexture texture(image);

    // ボタンのラベル用フォント
    Font font(12);

    while (System::Update())
    {
        // ドット絵を描画
        texture.scale(GridSize).draw(10, 10);

        for (int i : step(H))
        {
            for (int j : step(W))
            {
                // グリッドを描画
                Rect dot(10 + j*GridSize, 10 + i*GridSize, GridSize);
                dot.drawFrame(1.0, 1.0, Palette::Gray);

                // ドットがクリックされていたら、ドットの色を変更する
                if (dot.leftClicked)
                {
                    image[i][j] = Palette::Black;
                    texture.fill(image);
                }
                else if (dot.rightClicked)
                {
                    image[i][j] = Palette::White;
                    texture.fill(image);
                }
            }
        }

        // 保存ボタン
        Rect saveButton(531, 10, 100, 30);
        saveButton.draw(Palette::Whitesmoke).drawFrame(1.0, 1.0, Palette::Black);
        font(L"保存").drawCenter(saveButton.center, Palette::Black);

        if (saveButton.leftClicked)
        {
            const auto save = Dialog::GetSaveImage();
            if (save)
            {
                image.save(save.value());
            }
        }

    }
}

パレットから色を選べるようにしてみよう

さて、この段階では使える色が白と黒の 2 つしかありません。右側にパレットを表示して、色を選択できるようにしてみましょう。

その前に、現在選択している色を表示することにします。leftColorrightColor という 2 つの変数を用意し、現在選択している色をここに格納するようにします。

    Color leftColor = Palette::Black;
    Color rightColor = Palette::White;

    // ・・・途中省略・・・

        // 現在選択中の色
        Rect(531, 470, 50, 50).draw(leftColor).drawFrame(1.0, 1.0, Palette::Black);
        Rect(581, 470, 50, 50).draw(rightColor).drawFrame(1.0, 1.0, Palette::Black);

そしてグリッドがクリックされたとき、これらの変数を使うようにします。

                if (dot.leftClicked)
                {
                    image[i][j] = leftColor;
                    texture.fill(image);
                }
                else if (dot.rightClicked)
                {
                    image[i][j] = rightColor;
                    texture.fill(image);
                }

02.png

ではパレットを作ります。Array に色をいくつか入れて、右側のスペースに表示し、それらがクリックされたら選択中の色(leftColorrightColor)を変更します。

    Array<Color> palette{ Palette::White, Palette::Black, Palette::Red, Palette::Blue, Palette::Green };

    // ・・・途中省略・・・

        // パレット
        for (int i = 0; i < palette.size(); i++)
        {
            // パレットのボタン
            Rect palButton(531, 100 + i * 30, 100, 30);
            palButton.draw(palette[i]).drawFrame(1.0, 1.0, Palette::Black);

            // パレットのボタンがクリックされたら、現在の色を変える
            if (palButton.leftClicked)
            {
                leftColor = palette[i];
            }
            else if (palButton.rightClicked)
            {
                rightColor = palette[i];
            }
        }

03.png

完成

今回の記事ではここでひとまず完成とします。最終的にほぼ 100 行に収まりましたね。マウスやダイアログを使うような複雑な処理がありましたが、Siv3D の便利なクラス群のおかげで、短いソースコードで記述することができました。

#include <Siv3D.hpp>

void Main()
{
    Window::Resize(640, 530);

    Graphics::SetBackground(Palette::Peachpuff);

    Graphics2D::SetSamplerState(SamplerState::WrapPoint);

    // 1マスの大きさ
    const int GridSize = 32;

    // 横方向のマスの数
    const int W = 16;

    // 縦方向のマスの数
    const int H = 16;

    // ドット絵
    Image image(W, H, Palette::White);

    // ドット絵表示用のテクスチャ
    DynamicTexture texture(image);

    // ボタンのラベル用フォント
    Font font(12);

    // 選択中の色
    Color leftColor = Palette::Black;
    Color rightColor = Palette::White;

    // パレット
    Array<Color> palette{ Palette::White, Palette::Black, Palette::Red, Palette::Blue, Palette::Green };

    while (System::Update())
    {
        // ドット絵を描画
        texture.scale(GridSize).draw(10, 10);

        for (int i : step(H))
        {
            for (int j : step(W))
            {
                // グリッドを描画
                Rect dot(10 + j*GridSize, 10 + i*GridSize, GridSize);
                dot.drawFrame(1.0, 1.0, Palette::Gray);

                // ドットがクリックされていたら、ドットの色を変更する
                if (dot.leftClicked)
                {
                    image[i][j] = leftColor;
                    texture.fill(image);
                }
                else if (dot.rightClicked)
                {
                    image[i][j] = rightColor;
                    texture.fill(image);
                }
            }
        }

        // 保存ボタン
        Rect saveButton(531, 10, 100, 30);
        saveButton.draw(Palette::Whitesmoke).drawFrame(1.0, 1.0, Palette::Black);
        font(L"保存").drawCenter(saveButton.center, Palette::Black);

        if (saveButton.leftClicked)
        {
            const auto save = Dialog::GetSaveImage();
            if (save)
            {
                image.save(save.value());
            }
        }

        // 現在選択中の色
        Rect(531, 470, 50, 50).draw(leftColor).drawFrame(1.0, 1.0, Palette::Black);
        Rect(581, 470, 50, 50).draw(rightColor).drawFrame(1.0, 1.0, Palette::Black);

        // パレット
        for (int i = 0; i < palette.size(); i++)
        {
            // パレットのボタン
            Rect palButton(531, 100 + i * 30, 100, 30);
            palButton.draw(palette[i]).drawFrame(1.0, 1.0, Palette::Black);

            // パレットのボタンがクリックされたら、現在の色を変える
            if (palButton.leftClicked)
            {
                leftColor = palette[i];
            }
            else if (palButton.rightClicked)
            {
                rightColor = palette[i];
            }
        }
    }
}

今後の課題

このプログラムにさらに機能を追加するとしたら、どんな機能が考えられるでしょうか。

  • 画像を読み込めるようにする
  • マウスが乗っているグリッドを強調する
  • グリッドのサイズを変えられるようにする
  • 画像のサイズを変えられるようにする
  • マウスをドラッグして連続して線を引けるようにする
  • etc.
voidProc
たまにゲームを作っています。
https://voidproc.com/
Why not register and get more from Qiita?
  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