OpenSiv3Dでカーソル中心のカメラを作ってみた

今回はOpenSiv3Dで使える視点移動クラス``CursorCamera2D``を作ってみたので、紹介していきたいと思います。

これは、もともと以前から自分の作るアプリで長くに渡り利用していたものですが、ほかの方にも役立つものなのでは？と思いまとめたものです。

こういった形で配布するのは初めてですので、不具合や改善点などありましたらコメント等で教えていただけると幸いです。

# CursorCamera2Dの特徴

![Siv3D-App-2018_07_01-14_24_26.gif](https://qiita-image-store.s3.amazonaws.com/0/213924/06a0a02e-5e51-e197-5723-adf18693562c.gif)

OpenSiv3Dに付属しているHamFrameworkには、すでに``Camera2D``という手軽にカメラ操作が可能なクラスが導入されています。
しかしながら、``Camera2D``はカメラ操作に「マウス右ボタン」がすでに割り当てられていて、既存のプログラムに導入する場合に競合してしまうことが多々あります。
また、画面中心で拡大縮小をする方式を取っているので、「画面の特定の場所をズームしたい！」といったケースに対応するのが難しいです。

この問題に対処するために、視点移動を「画面端にカーソルを持っていく or WASDキー」とし、拡大縮小を「マウスカーソル中心」で行うようにしたのが``CursorCamera2D``です。
さらに、追加の機能として「拡大縮小の範囲制限、カメラの移動可能な範囲制限」を用意したので、テスト的に使うのではなくユーザーが使う場合の制限も簡単に作ることができます。

# CursorCamera2Dの導入
``CursorCamera2D``の導入は至ってシンプルで、``CursorCamera2D.hpp``をダウンロードして``#include"CursorCamera2D.hpp"``と記述するだけです。

## ダウンロード
[こちら](https://github.com/sknjpn/SKNPlus/releases/tag/1.00)から最新のものをダウンロードしてください。
現時点でサポートしているOpenSiv3Dは``OpenSiv3D v0.2.6``ですが、基本的にそれ以降でも動作可能だと思われます。

## プロジェクトに追加
既存のプロジェクトに導入するよりも、新規のプロジェクトに追加したほうが簡単で導入する方法を早く習得できるので、新しいプロジェクトを作って導入していきます。
また、既存のプロジェクトに導入する場合も同様の手順で行われます。

まずは新規でOpenSiv3Dのプロジェクトを作成します。
OpenSiv3Dの前のバージョンであるSiv3Dとは互換性がないので、OpenSiv3Dで作成してください。


次に、追加したプロジェクトの``Main.cpp``と同じフォルダに、ダウンロードした``CursorCamera2D.hpp``をコピーしてください。

次に、以下のコードを``Main.cpp``にコピー&ペーストしてください。

``` cpp:Main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.2.6
# include "CursorCamera2D.hpp"

void Main()
{
	Graphics::SetBackground(ColorF(0.8, 0.9, 1.0));

	const Font font(50);

	const Texture textureCat(Emoji(U"🐈"), TextureDesc::Mipped);

	CursorCamera2D cursorCamera2D;

	while (System::Update())
	{
		cursorCamera2D.update();

		{
			const auto t = cursorCamera2D.createTransformer();

			font(U"Hello, Siv3D!🐣").drawAt(Window::Center(), Palette::Black);

			font(Cursor::Pos()).draw(20, 400, ColorF(0.6));

			textureCat.resized(80).draw(540, 380);

			Circle(Cursor::Pos(), 60).draw(ColorF(1, 0, 0, 0.5));
		}
	}
}
```

最後に、F5キーを押して実行すれば``CursorCamera2D``を機能を試すことができます。
試しに、マウスホイールを回転させるとマウス座標を中心に拡大縮小されるのがわかると思います。
![Siv3D-App-2018_07_01-14_34_11.gif](https://qiita-image-store.s3.amazonaws.com/0/213924/c0483284-9c7d-80d0-f1fb-6e0c81c0dc6b.gif)

# 使い方
ここでは基本的なCursorCamera2Dの使い方を紹介していきます。
HamFrameworkの``Camera2D``よりも機能が多く、複雑に感じるかもしれませんが、使わなくても動作するので使いたいときに参照していただけると良いかと思います。

## 描画範囲
描画範囲は``GetCameraRect()``より取得できます。
例えばミニマップに描画範囲を表示させたい場合などに有用な機能です。
または、広大なマップで表示範囲外の描画をすると重く、見えないところの処理を簡略化させたい場合にも有用です。

## 拡大縮小と上下左右移動
``CursorCamera2D``は「中心座標」と「倍率」によって制御されています。
視点移動自体は``update()``を毎フレーム呼び出すことで自動的に実行されるので、特に制御する必要はありませんが、

デフォルトの「感度」に不満がある場合や、ユーザーの好みで設定させたい場合は

```c++
 SetSensitivity(double magnifyingSensitivity, double movingSensitivity)
 ```
を呼び出してください。

``magnifyingSensitivity``によって拡大縮小の感度を、
``movingSensitivity``によって上下左右移動の感度を調節できます。

## 瞬間移動
マップが広大なゲームで起こるイベントは「イベントが起こった場所まで行く」のがある種の不満を生み出します。
もしくは、ユーザーがイベントの発生地点を正確に把握できない場合、探すのに苦労させることになります。

そういった場合に活躍するのが、この瞬間移動です。
瞬間移動を利用するのは非常に簡単で、「移動先の``RectF``をセット」するだけです。

```c++
SetCameraRect(RectF cameraRect, SettingMode settingMode);
```

``SettingMode``は現在座標から間を補完して移動するかどうかの設定で、デフォルトで``TargetOnly``となっています。
他の設定内容は大雑把に言えば以下の通りです。
``BaseOnly`` : 一瞬だけcameraRectに視点移動させ、現在座標まで戻す。（あまり使わない、特殊演出向け）
``TargetOnly`` : 現在座標からcameraRectまで間を補完して移動する（高速移動。優しい動き）
``BaseAndTarget`` : 間を補完せずにcameraRectを座標として設定する（瞬間移動。初期位置設定向け）

## 制限範囲
おそらく、名の知れたゲームのカメラ操作で制限範囲がないものはほとんどないと言っても過言ではないでしょう。
たいていの場合、ユーザーに見せてはいけない範囲外領域が存在し、移動を制限し、
拡大させすぎない、縮小させすぎないのが制限範囲のシステムです。

デフォルトでは範囲をウィンドウサイズで制限し、拡大縮小はそれぞれ8.0倍、1.0倍に制限しています。
もし、導入予定のゲームのサイズがウィンドウよりも大きかったり、小さい場合、

``` C++
setRestrictedRect(RectF restrictedRect);
```
を利用して制限範囲を変更することが可能です。

ただし、制限範囲内に現在のカメラ表示範囲が収まっていない場合、予期せぬ動作が発生する場合があるので、
以下のように制限範囲内に収まるようにカメラ表示範囲を再設定することを推奨します。

``` C++
RectF rect(100, 200, 320, 240);

setRestrictedRect(rect);
SetCameraRect(rect, SettingMode::BaseAndTarget);	//この場合はBaseAndTargetを使ってください。
```

また、倍率設定は

``` C++
setMaxMagnification(double maxMagnification);
setMinMagnification(double minMagnification);
```
を利用してください。
ただし、``maxMagnification``よりも``minMagnification``のほうが大きい場合、予期せぬ動作が発生する場合があるので、``maxMagnification``のほうが大きな値になるように設定してください。

また、``minMagnification``については``restrictedRect``が小さいと設定する意味はないです。
（基本的に広大なマップで、全体を表示させるケースがない場合に有用です。）


# 使用例
実際の使用例を見たほうが使い道を早く習得できると思うので、簡単なものではありますがいくつか提示しておこうと思います。

## 簡易導入
![Siv3D-App-2018_07_01-14_34_11.gif](https://qiita-image-store.s3.amazonaws.com/0/213924/c0483284-9c7d-80d0-f1fb-6e0c81c0dc6b.gif)
既存のプログラムに導入する場合のサンプルです。

``` cpp:Main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.2.6
# include "CursorCamera2D.hpp"

void Main()
{
	Graphics::SetBackground(ColorF(0.8, 0.9, 1.0));

	const Font font(50);

	const Texture textureCat(Emoji(U"🐈"), TextureDesc::Mipped);

	CursorCamera2D cursorCamera2D;

	while (System::Update())
	{
		cursorCamera2D.update();

		{
			const auto t = cursorCamera2D.createTransformer();

			font(U"Hello, Siv3D!🐣").drawAt(Window::Center(), Palette::Black);

			font(Cursor::Pos()).draw(20, 400, ColorF(0.6));

			textureCat.resized(80).draw(540, 380);

			Circle(Cursor::Pos(), 60).draw(ColorF(1, 0, 0, 0.5));
		}
	}
}
```

## ミニマップ
![Siv3D-App-2018_07_01-14_24_26.gif](https://qiita-image-store.s3.amazonaws.com/0/213924/06a0a02e-5e51-e197-5723-adf18693562c.gif)
ミニマップは``Transformer2D``を利用することで非常に簡単に導入することができます。
``getCameraRect()``を使うことでカメラ領域を取得できます。

``` cpp:Main.cpp
# include <Siv3D.hpp> // OpenSiv3D v0.2.6
# include "CursorCamera2D.hpp"

struct Unit
{
	Vec2 pos;
	Vec2 target;
	bool selected;

	Unit()
		: pos(RandomVec2(Window::ClientRect()))
		, target(pos)
		, selected(false)
	{ }

	void update()
	{
		pos = pos.lerp(target, 0.1);

		if (selected)
		{
			if (MouseL.down()) { selected = false; }
			if (MouseR.down()) { target = Cursor::PosF(); }
		}
		else if (RandomBool(0.01))
		{
			auto f = pos.movedBy(RandomVec2(64.0));

			if (Window::ClientRect().contains(f))
			{
				target = f;
			}
		}

		if (Circle(pos, 8).leftClicked()) { selected = true; }
	}

	void draw()
	{
		Line(pos, target).drawArrow(2.0, Vec2(8.0, 8.0), Palette::Black);

		Circle(pos, 8)
			.draw(selected ? Palette::Yellow : (Circle(pos, 8).mouseOver() ? Palette::Pink : Palette::Red))
			.drawFrame(1.0, Palette::Black);
	}
};

void Main()
{
	Graphics::SetBackground(ColorF(0.8, 0.9, 1.0));

	Array<Unit> units;

	for (int i = 0; i < 10; i++) { units.emplace_back(); }

	CursorCamera2D cursorCamera2D;

	while (System::Update())
	{
		cursorCamera2D.update();

		{
			const auto t = cursorCamera2D.createTransformer();

			for (auto& u : units) { u.update(); }
			for (auto& u : units) { u.draw(); }
		}

		// Mini map
		{
			const auto t = Transformer2D(Mat3x2::Scale(0.25).translated(Window::BaseSize() * 0.1));

			Window::ClientRect()
				.draw(ColorF(Palette::Green, 0.5))
				.drawFrame(8.0, Palette::Black);

			for (auto& u : units) { u.draw(); }

			cursorCamera2D
				.getCameraRect()
				.draw(ColorF(Palette::Orange, 0.25))
				.drawFrame(4.0, Palette::Black);
		}
	}
}
```

# 最後に

ここまで見ていただきありがとうございました。
もしよろしければ実際にプロジェクトに追加して試していただけると嬉しいです。
今後は他のクラスも追加していき、一つのライブラリのようなものを作っていきたいとも考えています。

では、またどこかでお会いしましょう。