Siv3DでRPG風のメニューをつくる

Siv3D Advent Calendar 2016 16日目の記事です。

***

本記事では画像のようなメニューをつくります。
![完成形](https://qiita-image-store.s3.amazonaws.com/0/151567/cd5b0497-ac5d-7391-309c-e72901ff00a7.png)

本記事でのSiv3DのバージョンはAugust2016v2です。
ウィンドウの画像として、ねくらさんの画像素材を使わせていただきました。

#ウィンドウ画像をサイズを変えて表示する
ウィンドウの画像として枠に装飾のあるかっこいい素材を使いたいのですが、使いたい大きさに拡大すると装飾が崩れてしまい台無しです。

![Siv3D App 2016_12_14 21_05_26.png](https://qiita-image-store.s3.amazonaws.com/0/151567/99700db1-0d03-5d79-f309-0175b4804a7b.png)

元の画像を9分割して、画像の2,5,8の部分を横方向に拡大、4,5,6の部分を縦方向に拡大することで枠の形を崩すことなく拡大できます。

![9分割して拡大](https://qiita-image-store.s3.amazonaws.com/0/151567/f99657b3-6443-dfc9-fdab-c406d1baf7a5.png)

次のプログラムで、ウィンドウ画像を装飾を崩さないように大きさを変えて表示することを実装します。
上の図の5の部分の領域を取得できると、ウィンドウの中に何かを表示する際に便利なので、この領域を取得する関数も作っておきます。

```Main.cpp
# include <Siv3D.hpp>

class VariableWindow
{
private:
	Texture texture_m;	// 画像素材のテクスチャ
	Point pos_m;		// 左上の座標
	Size size_m;		// 描画するサイズ		
public:
	VariableWindow(Texture texture, Point pos, Size size)
		: texture_m(texture)
		, pos_m(pos)
		, size_m(size)
	{};
	// 描画
	void draw()const {
		const int w = texture_m.width / 3;
		const int h = texture_m.height / 3;
		const std::array<int, 3> width = { w, size_m.x - 2 * w, w };
		const std::array<int, 3> height = { h, size_m.y - 2 * h, h };
		Point p(pos_m);
		for (int y = 0; y < 3; y++) {
			for (int x = 0; x < 3; x++) {
				texture_m({ w*x,h*y }, { w, h }).resize(width[x], height[y]).draw(p);
				p.x += width[x];
			}
			p.x = pos_m.x;
			p.y += height[y];
		}
	};
	// 真ん中の部分を四角形として取得する
	Rect getCenterRect()const {
		const Point p(texture_m.size / 3);
		const Size size(size_m - texture_m.size * 2 / 3);
		return Rect(pos_m + p, size);
	};
};

void Main() {
	// 画像素材が黒っぽいので背景色を変える
	Graphics::SetBackground(Palette::Blue);

	 //素材画像のテクスチャ、位置、サイズを引数として渡す
	VariableWindow variableWindow(Texture(L"WindowBase_b_02.png"), { 10,10 }, { 200,300 }); 

	while (System::Update())
	{
		variableWindow.draw();
	}
}
```

#選択肢をつくる

![Siv3D App 2016_12_15 18_48_08.png](https://qiita-image-store.s3.amazonaws.com/0/151567/798f4ad8-43c4-1b6d-38ed-5979752176a9.png)

選択肢の一つ一つが持っていると良い要素は、選択肢を表示するときの「文字列」と選ばれたとき実行する「関数」です。これらのメンバを持つクラスを作っても良いのですが、STLのpairを使うとお手軽です。

```cpp
// 文字列と関数のペアを作る
std::pair< String,std::function<void(void)> > pair = std::make_pair( L"選択肢１", []() {Println(L"選択肢１が選ばれました"); });

// ペアの１つ目の文字列を表示
Println(pair.first);

// ペアの２つ目の関数を実行
pair.second();
```
どの選択肢を選んでるかを示すカーソルを用意し、↑↓キーでカーソルを動かし、ZキーかEnterキーでカーソルが指している選択肢を実行するようにします。

```Main.cpp

# include <Siv3D.hpp>

class Menu {
private:	
	const Point pos_m;			// メニューの左上の座標	
	const Font font_m;			// 選択肢表示のフォント
	const int width_m;			// カーソルの幅
	const int height_m;			// 選択肢一つ一つの高さ
	int cursor_m;				// カーソル
	std::vector<std::pair<String, std::function<void(void)>>> choices_m;	// 選択肢の可変長配列
public:
	Menu(Point pos, Font font,int width,int height)
		: pos_m(pos)
		, font_m(font)	
		, width_m(width)
		, height_m(height)
		, cursor_m(0)
	{}

	// 選択肢を追加
	void add(String text, std::function<void()> function) {
		choices_m.push_back(std::make_pair(text, function));
	}

	void update() {

		// ↑↓キーでカーソルを動かす
		cursor_m += Input::KeyDown.clicked - Input::KeyUp.clicked;

		// カーソルを上下でループさせる
		cursor_m = (cursor_m < 0)? choices_m.size() - 1 : cursor_m % choices_m.size();

		// ZキーかEnterキーで、カーソルが指す選択肢の関数を実行する
		if ( (Input::KeyZ | Input::KeyEnter).clicked ){
			choices_m[cursor_m].second();
		}
	}

	void draw()const {
		// 選択肢の表示
		for (unsigned i = 0; i < choices_m.size(); i++) {
			font_m.draw(choices_m[i].first, pos_m.movedBy(0,i*height_m));
		}
		// カーソルの表示
		Rect(pos_m.movedBy(0, height_m * cursor_m), width_m,height_m).draw(Color(Palette::White).setAlpha(64));
	}
};

void Main() {

	Menu menu({ 200,100 }, Font(15), 150, 30);

	menu.add(L"選択肢１", []() {Println(L"選択肢１が選ばれました"); });
	menu.add(L"選択肢２", []() {Println(L"選択肢２が選ばれたよ～"); });
	menu.add(L"選択肢３", []() {Println(L"選ばれたのは選択肢３！！"); });
	menu.add(L"表示を消す", ClearPrint);

	while (System::Update())
	{
		menu.update();
		menu.draw();
	}
}
```

#キーリピートに対応する
現状のメニューは↑↓キーを押した瞬間に１つカーソルが動きますが、↑↓キーの押しっぱなしでカーソルを幾つか先の選択肢に動かせたほうが便利です。しかし、キーを押してる間、常にカーソルが動くようにするとカーソルが早すぎて制御できなくなってしまいます。理想的なカーソルの動作は

 1.　押した瞬間にカーソルが一つ動く
 2.　押し始めてから0.5~1秒は更にカーソルが動くことはない
 3.　それ以降は0.1秒ごとに一つ動く

という感じで動いてくれることです。
このようにカーソルが動くようにしてみましょう。

```Main.cpp
# include <Siv3D.hpp>

class RepeatableKey {
private:
	Key key_m;
	int pressedTime_m;					// キーが押され続けている時間
	const int delayUntilRepeat_m;		// リピート入力までの時間
	const int keyRepeat_m;				// キーリピートの間隔
public:
	RepeatableKey(Key key, int delayUntilRepeat = 30, int keyRepeat = 6)
		: key_m(key)
		, delayUntilRepeat_m(delayUntilRepeat)
		, keyRepeat_m(keyRepeat)
	{}
	// キーが押され続けているかの情報を更新する
	void update() {
		pressedTime_m = (key_m.pressed)? (pressedTime_m + 1) % INT_MAX : 0;
	}
	// キーの入力を受け付けるときtrueを返す
	bool get()const {
		return (pressedTime_m > delayUntilRepeat_m)? pressedTime_m % keyRepeat_m == 0 : pressedTime_m == 1;
	}
};

void Main() {

	// 「文字列　と　関数　のペア」　の可変配列
	std::vector< std::pair< String , std::function<void(void)> > > choices;

	// 可変配列に作ったペアを追加する
	choices.push_back( std::make_pair( L"選択肢１", []() {Println(L"選択肢１が選ばれました"); }));
	choices.push_back( std::make_pair( L"選択肢２", []() {Println(L"選択肢２が選ばれたよ～"); }));
	choices.push_back( std::make_pair( L"選択肢３", []() {Println(L"選ばれたのは選択肢３！！"); }));
	choices.push_back( std::make_pair( L"表示を消す", ClearPrint));

	Font font(15);		// 選択肢を表示するためのフォント	
	int cursor = 0;		// 今何を選んでるかのカーソル
	RepeatableKey up(Input::KeyUp), down(Input::KeyDown);	//キーリピートに対応したキー

	while (System::Update()) {

		// 選択肢を描画
		for (unsigned i = 0; i < choices.size(); i++) {
			font.draw(choices[i].first, { 200,30 * i });
		}

		// ↑キーと↓キーでカーソルを動かす(キーリピート対応)
		up.update();
		down.update();
		cursor += up.get() ? -1 : down.get() ? 1 : 0;
		//cursor += Input::KeyDown.clicked - Input::KeyUp.clicked;	

		// カーソルを上下でループさせる
		if (cursor < 0) { 
			cursor = choices.size() - 1;
		}			
		cursor %= choices.size();									

		//カーソルを半透明な四角で描画
		Rect({ 200 ,cursor * 30 }, { 150,30 }).draw(Color(Palette::Wheat).setAlpha(64));

		// Enterキー　か　Zキー　を押すと選択した関数を実行
		if ((Input::KeyEnter | Input::KeyZ).clicked) {
			choices[cursor].second();
		}
	}
}
```
これでいい感じにカーソルが動かせるようになりました。

#完成形
ウィンドウ画像の真ん中の領域を取得し、その座標と幅に合わせて選択肢を表示すればRPG風のメニューの完成です。

```Main.cpp
# include <Siv3D.hpp>

class VariableWindow
{
private:
	Texture texture_m;	// 画像素材のテクスチャ
	Point pos_m;		// 左上の座標
	Size size_m;		// 描画するサイズ
public:
	VariableWindow(Texture texture, Point pos, Size size)
		: texture_m(texture)
		, pos_m(pos)
		, size_m(size)
	{};
	void draw()const {
		const int w = texture_m.width / 3;
		const int h = texture_m.height / 3;	
		const std::array<int, 3> width = { w, size_m.x - 2 * w, w };
		const std::array<int, 3> height = { h, size_m.y - 2 * h, h };
		Point p(pos_m);							// テクスチャの一部を表示する座標(表示するたびにずらしていく)
		for (int y = 0; y < 3; y++) {
			for (int x = 0; x < 3; x++) {
				texture_m({ w*x,h*y }, { w, h }).resize(width[x], height[y]).draw(p);
				p.x += width[x];
			}
			p.x = pos_m.x;
			p.y += height[y];
		}
	}
	// 真ん中の部分を四角形として取得する
	Rect getCenterRect()const {
		const Point p(texture_m.size / 3);
		const Size size(size_m - texture_m.size * 2 / 3);
		return Rect(pos_m + p, size);
	}
};

class RepeatableKey {
private:
	Key key_m;
	int pressedTime_m;					// キーが押され続けている時間
	const int delayUntilRepeat_m;		// リピート入力までの時間
	const int keyRepeat_m;				// キーリピートの間隔
public:
	RepeatableKey(Key key, int delayUntilRepeat = 30, int keyRepeat = 6)
		: key_m(key)
		, delayUntilRepeat_m(delayUntilRepeat)
		, keyRepeat_m(keyRepeat)
	{}
	// キーが押され続けているかの情報を更新する
	void update() {
		pressedTime_m = (key_m.pressed)? (pressedTime_m + 1) % INT_MAX : 0;
	}
	// キーの入力を受け付けるときtrueを返す
	bool get()const {
		return (pressedTime_m > delayUntilRepeat_m)? pressedTime_m % keyRepeat_m == 0 : pressedTime_m == 1;
	}
};

class Menu {
private:	
	const Point pos_m;			// メニューの左上の座標	
	const Font font_m;			// 選択肢表示のフォント
	const int width_m;			// カーソルの幅
	const int height_m;			// 選択肢一つ一つの高さ
	int cursor_m;				// カーソル
	std::vector<std::pair<String, std::function<void(void)>>> choices_m;	// 選択肢の配列
	RepeatableKey up_m, down_m;	// キーリピートに対応したキー
public:
	Menu(Point pos, Font font = Font(20),int width=300,int height=30)
		: pos_m(pos)
		, font_m(font)	
		, width_m(width)
		, height_m(height)
		, cursor_m(0)
		, up_m(Input::KeyUp)
		, down_m(Input::KeyDown)
	{}

	void update() {

		// ↑↓キーでカーソルを動かす
		down_m.update();
		up_m.update();
		cursor_m += down_m.get() ? 1 : up_m.get() ? -1 : 0;

		// カーソルを上下でループさせる
		cursor_m = (cursor_m < 0)? choices_m.size() - 1 : cursor_m % choices_m.size();

		// ZキーかEnterキーで、カーソルが指す選択肢の関数を実行する
		if ( (Input::KeyZ | Input::KeyEnter).clicked ){
			choices_m[cursor_m].second();
		}
	}

	void draw()const {
		// 選択肢の表示
		for (unsigned i = 0; i < choices_m.size(); i++) {
			font_m.draw(choices_m[i].first, pos_m.movedBy(0,i*height_m));
		}
		// カーソルの表示
		Rect(pos_m.movedBy(0, height_m * cursor_m), width_m,height_m).draw(Color(Palette::White).setAlpha(64));
	}

	// 選択肢を追加
	void add(String text, std::function<void()> function) {
		choices_m.push_back(std::make_pair(text, function));
	}
};

void Main()
{
	Graphics::SetBackground(Palette::Blue);

	VariableWindow variableWindow(Texture(L"WindowBase_b_02.png"), { 200,50 }, { 200,210 });
	Rect r(variableWindow.getCenterRect());			// ウィンドウ画像の真ん中の領域
	Menu menu(r.tl, Font(15), r.w, 30);				// ↑に合わせて選択肢を作る

	menu.add(L"選択肢１",	[]() {Println(L"選択肢１が選ばれました"); });
	menu.add(L"選択肢２",	[]() {Println(L"選択肢２が選ばれたよ～"); });
	menu.add(L"選択肢３",	[]() {Println(L"選ばれたのは選択肢３！！"); });
	menu.add(L"表示を消す", ClearPrint);
	menu.add(L"ゲーム終了", System::Exit);

	while (System::Update())
	{
		variableWindow.draw();
		menu.update();
		menu.draw();
	}
}
```

![完成形](https://qiita-image-store.s3.amazonaws.com/0/151567/cd5b0497-ac5d-7391-309c-e72901ff00a7.png)