Siv3D での 2D カメラ操作をサポートする Camera2D 

[Siv3D AdventCalendar 2016](http://qiita.com/advent-calendar/2016/siv3d) の 3 日目の記事です．

# 概要
Siv3D に付属されている HamFramework は，2D カメラ操作を簡単に実装できるクラス `ham::Camera2D` を提供しています．以下のコードでマウス操作や W/A/S/D/上下キー入力による 2D カメラ移動が可能になります．

![Siv3DCamera002.gif](https://qiita-image-store.s3.amazonaws.com/0/34295/56f03860-33f3-089a-956d-4681ad1d9ca8.gif)

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

void Main()
{
	const Font font(30);

	Camera2D camera;

	while (System::Update())
	{
		camera.update();
		{
			const auto t1 = camera.createTransformer();

			font(L"ようこそ、Siv3D の世界へ！").draw();

			Circle(Mouse::PosF(), 50).draw({ 255, 0, 0, 127 });
		}
		camera.draw(Palette::Orange);
	}
}

```

# このクラスを作った理由
マウス操作でのカメラ移動が可能な状況で四角形を描画すると以下のようになります．

![Siv3DCamera001.gif](https://qiita-image-store.s3.amazonaws.com/0/34295/cc47028f-0c51-0106-2861-876ec5649662.gif)


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

void Main()
{
	Vec2 cameraPos(0.0, 0.0);

	const Vec2 rectPos = Window::Size() / 2.0;

	const Vec2 rectSize(100.0, 100.0);

	Vec2 mouseClickStartPos;

	while (System::Update())
	{
		if (Input::MouseR.clicked)
		{
			mouseClickStartPos = Mouse::Pos();
		}

		if (Input::MouseR.pressed)
		{
			cameraPos += (Mouse::Pos() - mouseClickStartPos) * 0.1;

			Circle(mouseClickStartPos, 100.0).drawFrame(2.0, 2.0);
			Circle(Mouse::Pos(), 30.0).draw();
		}

		RectF(rectSize).setCenter(rectPos - cameraPos).draw();
	}
}

```

このプログラムでは

1. カメラの位置 `cameraPos` を用意しその座標をマウスの操作により動かす
2. 四角形の描画時に位置 `rectPos` からカメラの位置を引く

ことにより，マウス操作によるカメラ移動を実現しています．ここで，カメラ移動だけでなく拡大縮小やキーボード入力への対応などの機能を追加したい場合にはどうすればよいでしょうか．描画対象すべての位置や拡大率を個別に操作することが必要になりそうでが，可能であれば描画とカメラ操作は分離して実装したいと思いました．他にも同じ思いをしている人がいるのではないかと考え，Siv3D 上での2Dカメラ移動を簡単に実装できる Camera2D を作成しました．

# 2Dカメラ操作をサポートする Camera2D

Siv3D には描画とマウスの座標に任意の行列を使って移動やスケーリングを一律に適用できる `Graphics2D::PushTransform()` と `Graphics2D::PopTransform()` があります．`Graphics2D::PushTransform()` には移動やスケーリングを行列形式で表現する `Mat3x2` 型の値を渡します．`Graphics2D::PopTransform()` が呼ばれると，それまで呼ばれた行列を最新の順から解除していきます．さらに，スコープから外れた際に Push した行列が自動的に Pop してくれる，`Transformer2D` クラスが Siv3D に存在します．HamFramework の Camera2D はこの Transformer2D を作成する関数 `createTransformer()` を使うことにより，特定のスコープ内の描画オブジェクトの移動・スケーリングを行います．


# 基本的な使い方

![Siv3DCamera003.gif](https://qiita-image-store.s3.amazonaws.com/0/34295/9d7f87b5-e394-241f-c306-5b79f7245056.gif)

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

void Main()
{
	Camera2D camera;

	while (System::Update())
	{
		camera.update();
		{
			const auto t1 = camera.createTransformer();

			Circle(100, 100, 100).draw(Palette::Red);
			Rect(150, 100, 200, 200).draw(Palette::Orange);
			Line(50, 400, 600, 50).draw(3, Color(128, 255, 255));
			Circle(400, 400, 200).draw(Color(0, 0, 255, 127));
		}
		camera.draw(Palette::Orange);
	}
}

```

1. `HamFramework.hpp` をインクルード
2. `Camera2D` クラスのインスタンスを作成
3. メインループの中で `Camera2D::update()` を呼ぶ
4. `{ }` でスコープを作り、その中で `Camera2D::createTransformer()` を使って `Transformer2D` を作成
5. カメラ操作する対象であるオブジェクトを描画
6. スコープ外で `Camera2D` オブジェクトの `draw()` を呼ぶ（任意）

これにより

- マウスの右クリック・ドラッグまたは W/A/S/D キーでカメラの移動
- マウスのホイールまたは上下キーでカメラのズーム操作

ができるようになります．

# マウス座標

マウス座標の取扱には注意が必要です．

![Siv3DCamera004.gif](https://qiita-image-store.s3.amazonaws.com/0/34295/c998799b-1f08-5db9-d5e8-e0029169317d.gif)

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

void Main()
{
	Camera2D camera;

	while (System::Update())
	{
		camera.update();
		{
			const auto t1 = camera.createTransformer();

			Circle(Mouse::Pos(), 2.0).drawFrame(0.1, 0.1);
			Circle(Mouse::PosF(), 1.0).draw();
		}
		camera.draw(Palette::Orange);
	}
}

```

マウス座標の取得方法には二種類あります．`Point` 型でマウス座標を取得する `Mouse::Pos()` と `Vec2` 型でマウス座標を取得する `Mouse::PosF()` です．`Mouse::Pos()` ではズーム時に実際のマウス位置からは離れた場所を指し示すことがあるため，Camera2D とうまく連携させるには `Mouse::PosF()` を利用してください．

# カメラ操作の調整

キーボードまたはマウスのどちらかのみでカメラを操作したい場合や，カメラの移動速度などをカスタマイズしたいときには，`Camera2D::update()` の引数を変更します．引数は以下の通りです．

1. `speed`: カメラの速度
2. `scaleRatio`: ズーム変更率 [0.0~1.0]
3. `mouseSpeedRatio`: マウス操作の影響率 [0.0~1.0]
4. `lerpRatio`: 補間率 [0.0~1.0]（ぬるっと感）
5. `useKeyControl`: キーボード入力を受け付けるかどうか
6. `useMouseControl`: マウス入力を受け付けるかどうか

例えば，キーボード入力を受け付けずマウス入力のみを受け付けたい場合には，`useKeyControl` を `false` にし，`useMouseControl` を `true` にします．（デフォルトではどちらも `true` です．）

# カメラ位置・ズームの変更

![Siv3DCamera005.gif](https://qiita-image-store.s3.amazonaws.com/0/34295/b08f1993-fd7b-7ee0-65a9-36dcfc5fb5d9.gif)

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

void Main()
{
	Camera2D camera;

	Vec2 playerPos = Window::Center();

	const Rect rect(20, 20, 200, 100);

	const Circle circle(150, 300, 100);

	const Polygon star = Geometry2D::CreateStar(200, 0.0, Vec2(400, 250));

	while (System::Update())
	{
		camera.update(10.0, 1.1, 0.01, 0.2, false, true);
		{
			const auto t1 = camera.createTransformer();

			playerPos += Vec2(Input::KeyRight.pressed - Input::KeyLeft.pressed, Input::KeyDown.pressed - Input::KeyUp.pressed) * 10.0;

			camera.setTargetPos(playerPos);

			const Circle player(playerPos, 30);

			// プレイヤーと重なっていたら赤、そうでなかったら黄色で図形を描く
			rect.draw(player.intersects(rect) ? Palette::Red : Palette::Yellow);
			circle.draw(player.intersects(circle) ? Palette::Red : Palette::Yellow);
			star.draw(player.intersects(star) ? Palette::Red : Palette::Yellow);

			player.draw();
		}
	}
}

```

ここでは，`Camera2D` のカメラ位置をプレイヤーと同じ位置に設定しています．

※`Camera2D` の `setTargetPos()` 次回の Siv3D アップデートで追加される関数です．それ以前のバージョンでは使用できないので，以下のコードを `HamFramework/CameraManager.hpp` の 227 行目に入れてください．

```cpp:CameraManager2D.hpp
		void setTargetPos(const Vec2& targetPos)
		{
			m_targetPos = targetPos;
		}

		void setTargetScale(const double targetScale)
		{
			m_targetScale = targetScale;
		}

```


# キーボード入力時におけるキー割り当て

`Camera2D` のキーボード入力のキー割り当てはデフォルトでは移動が W/A/S/D, ズーム操作が上下キーとなっています．変更をしたい場合には，`setKeyConfig()` を利用してください．たとえば，移動に上下左右キー，ズーム操作に Z・X キーを割り当てるには

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

void Main()
{
	const std::array<s3d::Key, 6> keyConfig =
	{
		s3d::Input::KeyUp, s3d::Input::KeyLeft, s3d::Input::KeyDown, s3d::Input::KeyRight,
		s3d::Input::KeyZ, s3d::Input::KeyX
	};

	Camera2D camera;

	camera.setKeyConfig(keyConfig);

	while (System::Update())
	{
		camera.update();
		{
			const auto t1 = camera.createTransformer();

			Circle(100, 100, 100).draw(Palette::Red);
			Rect(150, 100, 200, 200).draw(Palette::Orange);
			Line(50, 400, 600, 50).draw(3, Color(128, 255, 255));
			Circle(400, 400, 200).draw(Color(0, 0, 255, 127));
		}
		camera.draw(Palette::Orange);
	}
}

```

のように，`std::array<s3d::Key, 6>` で作成されるキー割り当てを `Camera2D::setKeyConfig()` の引数に入れてください．デフォルトでのキー割り当ては以下のようになっています．

```cpp
std::array<s3d::Key, 6> m_keyConfig =
{
	s3d::Input::KeyW, s3d::Input::KeyA, s3d::Input::KeyS, s3d::Input::KeyD,
	s3d::Input::KeyUp, s3d::Input::KeyDown
};

```

# ScalableWindow との併用

`Camera2D` と同様，`Transformer2D` を利用することでウィンドウの大きさを自由に変更可能な機能 `ham::ScalableWindow` を HamFramework で提供しています．Camera2D と ScalableWindow を同時に使いたい場合には以下のように書きます

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

void Main()
{
	Camera2D camera;

	ScalableWindow::Setup();

	while (System::Update())
	{
		camera.update();
		{
			const auto t1 = camera.createTransformer();
			const auto t2 = ScalableWindow::CreateTransformer();

			Circle(100, 100, 100).draw(Palette::Red);
			Rect(150, 100, 200, 200).draw(Palette::Orange);
			Line(50, 400, 600, 50).draw(3, Color(128, 255, 255));
			Circle(400, 400, 200).draw(Color(0, 0, 255, 127));
		}
		camera.draw(Palette::Orange);
	}
}

```

1. `ScalableWindow::Setup()` を呼ぶ
2. `Camera2D::createTransformer()` の後 `ScalableWindow::CreateTransformer()` で`Transformer2D` オブジェクト生成

# カメラ操作をもっとカスタマイズしたい

`Camera2D` の操作をもっとカスタマイズしたい場合は，`BasicCamera2D` もしくは `Camera2D` を継承した独自のカメラクラスを作成してください．例えば，Y軸の正の方向を上向きに調整したカメラクラス `CameraBox2D` は `Camera2D` の一部の関数をオーバーライドすることで作成しました．


# まとめ
Siv3D Advent Calendar 2016 3 日目は Camera2D の使い方について紹介しました．

明日は @agehama_ さんの記事です．よろしくお願いいたします．