さらば Format()

(開発メモ)

Print() / Println() / LOG() で導入した Variadic Templates による文字列フォーマットを Font::operator() で使えば、コードの大部分から Format() を削除できることに気付いた。非常に書きやすく読みやすい。図形の .draw() と一貫性がある。

Before

# include <Siv3D.hpp>

void Main()
{
	const Font font{ 20 };

	while (System::Update())
	{
		font.draw(Format(L"frame: ", System::FrameCount()), 50, 50);
	}
}

After

# include <Siv3D.hpp>

void Main()
{
	const Font font{ 20 };

	while (System::Update())
	{
		font(L"frame: ", System::FrameCount()).draw(50, 50);
	}
}

Siv3D March 2014 から導入し、古い記法は問題が無ければ deprecated にしていく。

Siv3D January 2014 をリリースしました

今年最初の Siv3D アップデート January 2014 をリリースしました。December 2013 から 1 か月半ぶりの更新です。ダウンロードはこちら→ Play Siv3D! | Siv3D のダウンロード
更新内容一覧は Play Siv3D! | What's New で確認できます。

主な新機能を 7 つ紹介します。サンプルは Play Siv3D! に今後掲載予定です。

GIF 出力の機能強化



GIF で透過画像の出力と、ディザリングの有無の選択に対応します。

Sprite


頂点座標、UV 座標、頂点色、頂点インデックスを自由に設定できる描画オブジェクトです。従来の TexturedQuad や Polygon ではできなかった効果を実現できます。

楕円


楕円を表現する Ellipse 型を追加しました。画面や画像への描画ができます。あたり判定は現時点では vs Point/Vec2 のみサポートしています。

多角形や星形の作成補助


正多角形や星形のポリゴンを作成するための関数を追加しました。これまでより簡単にいろいろな図形を作れます。

GUI にラジオボタンを追加


選択肢からどれか 1 つだけを選択する GUI ウィジェットです。あるボタンを押すと、それ以外のボタンが押されていない状態に戻ります。

3D プリミティブの描画


サンプルコード
平面、直方体、球、円盤、円錐、円柱といった 3D プリミティブを描画できます。最大 128 個の平行光源・点光源、カメラ、フォグ、環境光といったシーンのオプションを追加し、いくつかのレンダリングオプションを設定できます。

Siv3D 公開版としては初めて 3D 描画を正式機能としてサポートします。ようやく Siv3D という名前に追いつきました。

Siv3D Winter of Code 2013 の解答

昨年末に出題した Siv3D Winter of Code 2013 の解答です。

解答例

# include "Shapes.hpp"

namespace codeIQ
{
	std::vector<Vec2> CreateRect(double x, double y, double w, double h)
	{
		return{ { x, y }, { x + w, y }, { x + w, y + h }, { x, y + h } };
	}

	std::vector<Vec2> CreatePlus(double r, double width, const Vec2& center)
	{
		const double halfWidth = width / 2.0;
		const double x0 = center.x - r;
		const double x1 = center.x - halfWidth;
		const double x2 = center.x + halfWidth;
		const double x3 = center.x + r;
		const double y0 = center.y - r;
		const double y1 = center.y - halfWidth;
		const double y2 = center.y + halfWidth;
		const double y3 = center.y + r;

		return{
			{ x1, y0 }, { x2, y0 }, { x2, y1 },
			{ x3, y1 }, { x3, y2 },	{ x2, y2 },
			{ x2, y3 }, { x1, y3 },	{ x1, y2 },
			{ x0, y2 }, { x0, y1 },	{ x1, y1 }
		};
	}

	std::vector<Vec2> CreatePentagon(double r, double angle, const Vec2& center)
	{
		return CreateNgon(5, r, angle, center);
	}

	std::vector<Vec2> CreateHexagon(double r, double angle, const Vec2& center)
	{
		return CreateNgon(6, r, angle, center);
	}

	std::vector<Vec2> CreateNgon(int n, double r, double angle, const Vec2& center)
	{
		if (n < 3)
		{
			return{};
		}

		std::vector<Vec2> vertices;

		vertices.reserve(n);

		for (int i = 0; i < n; ++i)
		{
			vertices.push_back(center + Circular(r, angle + i*(TwoPi / n)));
		}

		return vertices;
	}

	std::vector<Vec2> CreateStar(double r, double angle, const Vec2& center)
	{
		return CreateNStar(5, r, r / ((3 + ::sqrt(5.0)) / 2), angle, center);
	}

	std::vector<Vec2> CreateNStar(int n, double rOuter, double rInner, double angle, const Vec2& center)
	{
		if (n < 2)
		{
			return{};
		}

		std::vector<Vec2> vertices;

		vertices.reserve(2 * n);

		for (int i = 0; i < n * 2; ++i)
		{
			vertices.push_back(center + Circular(i % 2 ? rInner : rOuter, angle + i*(Pi / n)));
		}

		return vertices;
	}
}

別解(より高速)

std::vector<Vec2> CreateNgon(int n, double r, double angle, const Vec2& center)
{
	if (n < 3)
	{
		return{};
	}

	std::vector<Vec2> vertices(n, center);

	for (int i = 0; i < n; ++i)
	{
		vertices[i] += Circular(r, angle + i*(TwoPi / n));
	}

	return vertices;
}

std::vector<Vec2> CreateStar(double r, double angle, const Vec2& center)
{
	const double innerScale = 0.38196601125010515; // 2/(3 + sqrt(5)) 

	return CreateNStar(5, r, r * innerScale, angle, center);
}

std::vector<Vec2> CreateNStar(int n, double rOuter, double rInner, double angle, const Vec2& center)
{
	if (n < 2)
	{
		return{};
	}

	std::vector<Vec2> vertices(n * 2, center);

	for (int i = 0; i < n * 2; ++i)
	{
		vertices[i] += Circular(i % 2 ? rInner : rOuter, angle + i*(Pi / n));
	}

	return vertices;
}

解説

CreatePlus

単純に実装すると見通しが悪くなるので、登場する値を整理しました。

CreatePentagon / CreateHexagon

DRY 原則で CreateNgon の特殊ケースとします。ソースコードが短くなるだけでなく、命令に使われるメモリを削減し、キャッシュ効率を改善する効果があります。

CreateNgon

空の vector に頂点を push_back() / emplace_back() で追加すると、メモリの再アロケートが生じて性能が低下します。はじめからサイズが N 個と決まっているので、vector::reserve(N) で必要なサイズのメモリをアロケーションしておきましょう。
別解では、N 個の center で vector を初期化しています。これによって計算時の一時オブジェクトを削減するだけでなく、push_back() によるサイズチェックも省略できるので、さらに効率を改善できます。
Circular の使い方については Circular 型 を参照してください。

CreateStar

CreateNStar の特殊ケースです。
rOuter と rInner の比率は 1 : \frac{2}{3+\sqrt{5}} です。
sqrt が実行時に計算されるのを防ぐために、別解のように数値リテラルを埋め込むと効率的です。もちろんコメントで式を示すのを忘れずに。

CreateNStar

CreateNgon と同様、必要なサイズのメモリをアロケーションしてから各要素を追加します。ループは展開して N 回にもできますが、ループごとの命令が大きくなるためか性能が低下しました。手元の環境で計測して選択しましょう。

CodeIQ では 1 週間ほどの募集でしたが、アクセルさん、rumiaqua さん、冬椿さん、ケアルガさん、ciel さん、hisoji_ さんが解答を投稿してくれました。以上の方にはフィードバックコメントをお送りします。
また春か夏ごろに新しい問題を出題予定です。ぜひ挑戦してみてください!

開発中の機能 | Sprite

あけましておめでとうございます。今年の Siv3D の開発作業をスタートしました。

今日は、Siv3D January 2014 で追加される機能 Sprite を紹介します。
Sprite は頂点座標、UV 座標、頂点色、頂点インデックスを自由に設定できる描画オブジェクトで、従来の TexturedQuad や Polygon ではできなかった効果を実現できます。

Sprite のデータ作成は多少手間がかかりますが、単純な 2D 描画では、頂点数とインデックス数の上限以外の制約が無くなることになります。

透明部分が多くを占める画像を描画する際、必要な部分にフィットさせた Sptite を描画することでピクセルフィルレートを節約するといった使い方もできます。

セーブとロード

メリークリスマス、Siv3D Advent Calendar 2013, 25 日目の記事です。
今日は BinaryWriter と BinaryReader を使って、簡単なゲームデータのセーブとロードに挑戦しましょう。

BinaryWriter はデータをテキストではなくバイト列としてファイルに書き込みます。
ファイルに得点をセーブするプログラムで TextWriter との違いを見てみましょう。

# include <Siv3D.hpp>

void Main()
{
	const int score = 1234567;

	TextWriter textWriter(L"save.txt");

	textWriter.write(score);


	BinaryWriter binaryWriter(L"save.bin");
	
	binaryWriter.write(score);
}

save.txt は 1234567 という文字列が保存された 10 バイトのテキストファイルになります。一方 save.bin は int 型の 4 バイトのデータがそのまま保存され、テキストエディタで開くと変な文字列として解釈されます。

次に、今保存した得点をロードするプログラムで TextReader と BinaryReader の違いを見てみましょう。

# include <Siv3D.hpp>

void Main()
{
	TextReader textReader(L"save.txt");	
	
	String text;
	
	textReader.readLine(text);

	const int scoreA = Integer(text);


	BinaryReader binaryReader(L"save.bin");
	
	int scoreB = 0;

	binaryReader.read(&scoreB, sizeof(scoreB));


	Println(L"A: ", scoreA);

	Println(L"B: ", scoreB);

	WaitKey();
}

テキストファイルの場合、読み込んだ文字列を目的のデータ型に変換する必要があります。バイナリデータの場合はデータを変数に直接ロードできます。

セーブデータを人間が読める形式にする必要がないならば、速度やサイズで優れたバイナリ形式でセーブデータを扱いましょう。

構造体の読み書き

構造体を使うと、一度に多くのデータを読み書きできます。

# include <Siv3D.hpp>

struct Result
{
	int stage1Score;
	int stage2Score;
	double time;
};

void Main()
{
	Result result;
	result.stage1Score = 500;
	result.stage2Score = 400;
	result.time = 43.2;

	{
		BinaryWriter writer(L"result.bin");

		writer.write(result);
	}
	//	ここで writer のスコープが外れてファイルがクローズされる
	//	writer.close() でもよい

	// クローズされたファイルでないと正常に開けない
	BinaryReader reader(L"result.bin");

	Result loadData;
	reader.read(&loadData, sizeof(loadData));

	Println(L"stage1Score: ", loadData.stage1Score);
	Println(L"stage2Score: ", loadData.stage2Score);
	Println(L"time: ", loadData.time);

	WaitKey();
}

開いたファイルは、読み書きが終了したらクローズしましょう。クローズしていないファイルは、別の BinaryReader や BinaryWriter で正常に開くことができません。

非 POD クラス型の読み書き

Point, Vec2, Color などは、ポインタとデータのサイズを指定して write できます。

# include <Siv3D.hpp>

void Main()
{
	const Point pos(10, 50);

	{
		BinaryWriter writer(L"pos.bin");

		writer.write(&pos, sizeof(pos));
	}

	BinaryReader reader(L"pos.bin");

	Point loadData(0, 0);
	reader.read(&loadData, sizeof(loadData));

	Println(L"pos: ", loadData);

	WaitKey();
}

String の読み書き

std::vector や String のように、メンバ変数にポインタを持つクラスを、単純に write / read してはいけません。write されるのはポインタだけで、実際のバッファのデータは write されないという問題が生じるためです。

したがって次のように工夫する必要があります。String を文字列の長さと、実際のデータに分けてセーブ・ロードするサンプルです。

# include <Siv3D.hpp>

struct Monster
{
	String name;
	int number;
};

void Main()
{
	Monster pikachu;
	pikachu.name = L"ピカチュウ";
	pikachu.number = 25;

	{
		BinaryWriter writer(L"pikachu.bin");

		writer.write(pikachu.name.length);

		writer.write(pikachu.name.data(), pikachu.name.length * sizeof(wchar_t));

		writer.write(pikachu.number);
	}

	BinaryReader reader(L"pikachu.bin");

	size_t length = 0;

	reader.read(&length, sizeof(length));

	if (length > 6)
	{	// 異常な値(誤ったファイルを読み込んで巨大な値になるのを防ぐ)
		return;
	}

	Monster loadData;
	loadData.name.resize(length);

	reader.read(&loadData.name[0], loadData.name.length * sizeof(wchar_t));

	reader.read(&loadData.number, sizeof(loadData.number));

	Println(L"Name: ", loadData.name);
	Println(L"Number: ", loadData.number);

	WaitKey();
}

保存するデータの内容によっては単純なバイナリではなく INI 形式や CSV 形式、場合によっては画像を使うこともあるでしょう。より高度なアプリケーションでは、ファイルの破損チェックや、データの改造を防ぐ暗号化も必要になります。

将来的にはそういったファイル保存のユーティリティや、String, std::vector などのデータの保存を簡単にする機能を Siv3D に追加する予定です。

Circular 型

Siv3D Advent Calendar 2013, 24 日目の記事です。
今日は平面極座標を表現する Circular 型を紹介します。

Circular 型のメンバ r, theta はそれぞれ平面極座標の動径 r と偏角 θ に対応します。
極座標から直交座標への変換は、Siv3D では次の通り行われます。

Circular::operator Vec2() const
{
	return{ sin(theta)*r, -cos(theta)*r };
}

簡単なプログラムで Circular のはたらきを確かめてみましょう。

# include <Siv3D.hpp>

void Main()
{
	const Vec2 center = Window::Size() / 2;

	while (System::Update())
	{
		Circle(center, 5).draw(Palette::Red);

		for (int i = 0; i < 80; ++i)
		{
			const Vec2 pos = center + Circular(10 + i * 3, Radians(i * 20));

			Circle(pos, (i + 8)*0.2).draw(HSV(i*20));
		}
	}
}

Circular 型を使ったプログラムの例は サンプル | 時計 があります。

CodeIQ の Siv3D 問題 も Circular を使うと楽に実装することができます。

カーソルを変更する

Siv3D Advent Calendar 2013, 23 日目の記事です。
今日はカーソルの表示や画像を切り替える方法を紹介します。

カーソルの表示/非表示


システムカーソルの表示/非表示を切り替えるには Window::ShowCursor(bool) を使います。このプログラムでは、画面の右半分にいるときにカーソル表示をオフにします。

# include <Siv3D.hpp>

void Main()
{
	while (System::Update())
	{
		const bool show = Mouse::Pos().x < Window::Width() / 2;

		Window::ShowCursor(show);
	}
}

カーソルの画像を変更する

Window::SetCursorTexture() を使うと指定したテクスチャを、
Window::SetCursorTextureRegion() を使うと指定したテクスチャの一部領域をカーソル画像にすることができます。
それぞれの第二引数は、カーソル画像と実際のカーソルの中心位置とのオフセットです。
設定したカーソルは常に最前面に描画されます。

# include <Siv3D.hpp>

void Main()
{
	const Texture texture(L"Example/Windmill.png");

	Window::SetCursorTextureRegion(texture(100,100,50,50));

	GUI gui(GUISkin::Default());

	gui.addButton(L"OK", {L"OK"});

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