はじめに
Siv3D を使うと、C++ でゲームやツールを簡単に作成することができます。Siv3D の便利なクラス群を活用して、ドット絵エディタを作成してみましょう。
今回作成するドット絵エディタでは、こんな機能を実装してみます。
- ドット絵を表示する
- ドット絵のグリッドを表示する
- クリックしたドットの色を変える
- ドット絵を保存する
- パレットから色を選ぶ
ドット絵を表示しよう、その前に
まず最初に、エディタの外観を大まかに決めます。幅 16 × 高さ 16 のドット絵を、1 ドットあたり 32 ピクセルのサイズに拡大して表示することにします。
あとはウィンドウの大きさ、背景色を自分好みに設定します。
ウィンドウと背景
#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 クラスがあります。今回はこのクラスを使ってドット絵を編集することにしましょう。
画像編集
幅 16 × 高さ 16 で、中身が真っ白の画像を作成します。
Image image(W, H, Palette::White);
ただ、Image クラスは画像の編集はできても表示はできません。画像を表示するには Texture クラスか、または描き換えが可能な DynamicTexture を使用します。今回は頻繁にテクスチャを書き換える事になりそうので、 DynamicTexture を使うことにします。
テクスチャを描く / スケッチ
以下のようにして、既存の画像からテクスチャを生成します。
DynamicTexture texture(image);
テクスチャの表示には Texture::draw()
関数を使用します。また、今回はドット絵を GridSize
倍に拡大して表示するので、draw()
の前に Texture::scale()
関数で拡大しましょう。
以下のようにして、左から 10px、上から 10px の位置に、拡大されたテクスチャを表示します。
texture.scale(GridSize).draw(10, 10);
テクスチャを拡大表示すると、ぼやけた感じになると思います。これをくっきりさせたい時は、サンプラーステートの変更 をします。
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);
}
}
グリッドを表示しよう
次に、ドット絵エディタらしくグリッドを表示してみましょう。大きさが GridSize
の矩形(Rect = Rectangle クラス)を敷き詰めればよさそうです。
Rectangle::drawFrame()
関数を使って、灰色の枠を描いています。
長方形の枠を描く
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);
}
}
クリックしたドットの色を変えてみよう
Rectangle のような図形のクラスには、クリックされたかどうかを知るためのプロパティ Rectangle::leftClicked
、Rectangle::rightClicked
がありますので、それを利用します。
図形とクリック
クリックの判定には、グリッド表示に使った Rect を流用すると良さそうですね。左クリックされたドットを黒に、右クリックされたドットを白にしてみましょう。
画像のピクセルの色を変更するときは、Image::operator[]
を使用します。
画像のピクセルに色を書き込む
画像を編集したあとは、忘れずに表示用の 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);
}
}
}
ドット絵を保存してみよう
ではここで画像を保存できるようにしてみましょう。「保存」ボタンを表示しておいて、クリックされたら「名前をつけて保存」ダイアログを出し、指定されたファイル名で保存するようにします。
まずは「保存」ボタンを表示します。ボタン本体の表示には Rect を、「保存」の文字の表示には Font クラスを使います。
下記のようにして、画面右上のスペースにボタンを描きます。Font::drawCenter()
関数を使うと、指定した座標が中心になるように文字を描くことができるので、矩形の中心 Rectangle::center
を指定して、ボタンの中央に文字を表示します。
テキストを描く#中央に表示する
//ボタンのラベル用フォント
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);
このボタンがクリックされた時に、「名前をつけて保存」ダイアログを出し、指定されたファイル名で画像を保存するには以下のようにします。
ダイアログ#名前を付けて画像を保存する
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 つしかありません。右側にパレットを表示して、色を選択できるようにしてみましょう。
その前に、現在選択している色を表示することにします。leftColor
、rightColor
という 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);
}
ではパレットを作ります。Array に色をいくつか入れて、右側のスペースに表示し、それらがクリックされたら選択中の色(leftColor
、rightColor
)を変更します。
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];
}
}
完成
今回の記事ではここでひとまず完成とします。最終的にほぼ 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.