Unityで複数のオブジェクトがカメラの描画範囲内に収まるように自動調整できるカメラワークを模索してみた。

この記事は [SLP KBIT Advent Calendar 2017](https://adventar.org/calendars/2492) の11日目の記事です。

3Dで作るゲームの利点ってカメラワークの自由度にあるんじゃないかなーって思ったり。

# はじめに
Unityを触ってるときにわりとカメラワークに悩んだりするんですが、調べてみるとけっこうTPS方式のカメラワークについて書かれてることが多いんですよね。まあそのカメラワークにもお世話になってるんですが、ゲームをゲームらしく見せるにはそれだけじゃ足りないことってありますし。

記事にまとめる時に改めてコード組みなおしましたが割と~~やっつけ仕事~~拙いコードになってるかもしれません。

# コンセプト
タイトルそのままです。

* 注視すべきオブジェクト(キャラクター、モンスター等)が常にカメラ範囲内に収まるようにカメラの距離を自動調整

これにつきます。
ちなみに当初の私は某ファイ○ルファンタジー13の戦闘シーンをイメージしてました。

# 仕様
自由度の高い3次元上のカメラワークのことですし、やり方は色々あると思うのですが、今回は以下の通りで考えてます。

* カメラの視点の中心はオブジェクトたちのポジション値の平均地点
* カメラの範囲の基準になる長さは中心から最も離れているオブジェクトとの距離
* 基準となる長さが範囲内に収まるカメラの距離を計算して動かす

# 考え方
上の仕様についての補足といいますか、本題がほぼここかもしれないですが。

まず、上から見たフィールド上に複数のオブジェクト(キャラクター、モンスター等)が配置されています。
![図その1.png](https://qiita-image-store.s3.amazonaws.com/0/151989/b62fedda-6c2c-3ac1-c582-1942aaf3f612.png)

ここで各オブジェクトのワールド座標値の平均値を出すと、まぁだいたいこのへんになると思います。
![図その2.png](https://qiita-image-store.s3.amazonaws.com/0/151989/1de151a7-6c0d-64b9-57c3-0c06bb312c68.png)

そうしたら、中心点と各オブジェクトとの距離を計算しまして、中心点から最も離れているオブジェクトを探します。
![図その3.png](https://qiita-image-store.s3.amazonaws.com/0/151989/c03c122c-9f1b-c90b-a35b-7d7890f27823.png)

次に、中心点をもとに中心点から最も離れているオブジェクトまでの距離を半径として円を描きますと、必ず複数のオブジェクトたちは円の内部に配置された状態になるわけですね。…なるよね？(不安)
![図その4.png](https://qiita-image-store.s3.amazonaws.com/0/151989/33683bce-5cd8-21df-3b84-5befb8df533a.png)

全オブジェクトが常にカメラの描画範囲内に入っているためには、この円形部分をカメラの描画領域内におさめればいいわけです。
ここで、カメラの描画領域は錐台の形(図は上から見た2次元平面としてとらえているので三角形)となっているので、ある距離における四角形のサイズ(図では線分の長さ)は計算によって求めることができます。

ということでカメラから中心点までの距離で円の直径だけの長さが描画されればいい、と最初は思ったんですがちょっと違います。
![図その6.png](https://qiita-image-store.s3.amazonaws.com/0/151989/04bacd92-83df-2839-e225-cc2f68f1d5e7.png)
図のようにちょうど中心点で直径だけの長さを描画できるようにすると円の内部に若干の描画範囲外の部分ができてしまうのです。些細な問題に見えてこれが意外とオブジェクトが見えなくなっちゃうんで注意が必要。

というわけでこうします。
![図その7.png](https://qiita-image-store.s3.amazonaws.com/0/151989/a32a5d10-5139-8f51-6642-ca5555a92a9f.png)

高校あたりで見かけそうな数学ですね。
カメラの描画角度はFOV角度として取得することができ、かつ円の半径は分かっているので、求めたい距離はわりと簡単に求められるわけです。
![図その8.png](https://qiita-image-store.s3.amazonaws.com/0/151989/5607cadd-b7a6-c7ed-e315-481a97d02fdb.png)

ただしこれはあくまで二次元上におけるものでしかありません。
プラスしていえばFOV角度は垂直視野であり、水平視野ではないので上の図のθにそのまま使うことはできません。

ということでがんばって3次元に描きなおしてみました。
![図その9.png](https://qiita-image-store.s3.amazonaws.com/0/151989/ca51ff50-8d08-19f1-2805-4cd915944927.png)
※cameraコンポーネントにあるfieldOfViewがFOV角度、垂直視野のことです。
**うん、わかりづらいですね**

ともあれ、実際のところは円が球になっただけですし、計算方法も変わりません。
角度ですがそのまま垂直視野の角度を用いて計算します。
こうすることで、複数のオブジェクトが上下に離れていても描画範囲内におさまるようになるわけですね。
アスペクト比が横長である限りは水平視野内にもしっかりおさまるようになっています。

ちなみに水平視野の角度はアスペクト比によって変化するそうなので計算で求められないかと思ったんですが、いまいち上手くいきませんでした…。

長かったですが考え方は以上です。

# 使用したもの

* Unity 5.6.3p1
* [iTween](https://www.assetstore.unity3d.com/jp/#!/content/84)
* [SDこはくちゃんズ・小碓学園夏＆冬制服モデル](http://unity-chan.com/download/releaseNote.php?id=SDKohakuchanz)

iTweenはカメラワークに自然な感じを持たせるのには必要不可欠です。扱い方は他の人が色々と記事書いてくれてるんでそちらを参考にしてください。
オブジェクトはUnity Technologies Japanが提供しているSDのユニティちゃんたちを使わせてもらいました。まぁオブジェクトに関していえば最悪ただのブロックでもいいんですが、そこはこう…モチベーションの上がる方がいいかなって。
なお、キャラクターの動かし方に関しては触れてないのであしからず。

# 前準備
用意したオブジェクトは次の通りです。
```CenterPoint```と```CameraPosition```は空のゲームオブジェクトになってます。
フィールドを```Terrain```で作ってありますが特に意味はありません。土台になればなんでも問題ないです。
![20f27dbbf3bc37fb86f7ec9cb24bdc2b.png](https://qiita-image-store.s3.amazonaws.com/0/151989/ee98d3f2-7361-4077-674e-24041bb9ee9d.png)


基本的な処理フローは以下の通りです。

1. 全オブジェクトのポジション値の中心点となる平均値を算出
2. 中心地点に```CenterPoint```を移動
3. ```CenterPoint```と各オブジェクトとの距離を算出し、最も遠い距離の長さを算出
4. 求めた長さをもとにカメラの距離を算出
5. ```CenterPoint```から求めたカメラの距離だけ離れたところに```CameraPosition```を配置
6. ```CameraPosition```を目標にカメラをiTweenで動かす

基本的には先ほどの考え方の通りです。
```CenterPoint```を中心地点に配置しているのはカメラをy軸上で回転させる時に面倒な計算をしなくて済むためだったりします。```CameraPosition```はそのままカメラに追従させるための目標地点です。

# コード部分
まずはカメラのコードから。
といっても```Target```を追従するだけのコードですが。

```csharp:ItweenCamera.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ItweenCamera : MonoBehaviour {

	public Transform Target;
	
	void Update () {
		iTween.MoveUpdate(this.gameObject, iTween.Hash(
			"position", Target.position,
			"time", 3.0f)
		);
		iTween.RotateUpdate(this.gameObject, iTween.Hash(
			"rotation", Target.rotation.eulerAngles,
			"time", 3.0f)
		);
	}
}

```

これを書いたソースファイルをカメラオブジェクトにアタッチしてやります。
inspecter上で```Target```に```CenterPoint```をドラッグ＆ドロップしてやります。
![4bbd501566bc5810422bfb3e06d347db.png](https://qiita-image-store.s3.amazonaws.com/0/151989/971f8191-11ed-b569-f086-954e38aa000c.png)

続いて中心点となる```CenterPoint```にアタッチするコードです。

```csharp:GetCenterPoint
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GetCenterPoint : MonoBehaviour {
	public List<Transform> transList = new List<Transform>();	//カメラ範囲内におさめたいオブジェクトのリスト
	private Transform cameraPos;
	public Camera usingCamera;
	private Vector3 pos = new Vector3();
	private Vector3 center = new Vector3();
	private float radius;
	private float margin = 1.0f;		//半径を少し余分にとるための値
	private float distance;
	private float cameraHeight = 1.5f;	//カメラが地面にめり込まないようにカメラを浮かせる高さ

	void Start () {
		cameraPos = GameObject.Find("CameraPosition").GetComponent<Transform>();
	}

	void Update () {
		pos = new Vector3(0,0,0);
		radius = 0.0f;
		foreach (Transform trans in transList){		//オブジェクトのポジションの平均値を算出
			pos += trans.position;
		}
		center = pos/transList.Count;
		this.transform.position = center;			//CenterPointのポジションを中心に配置
		foreach (Transform trans in transList){		//中心から最も遠いオブジェクトとの距離を算出
			radius = Mathf.Max(radius, Vector3.Distance(center,trans.position));
		}
		distance = (radius + margin) / Mathf.Sin(usingCamera.fieldOfView*0.5f*Mathf.Deg2Rad);	//カメラの距離を算出
		cameraPos.localPosition = new Vector3(0,cameraHeight,-distance);	//CameraPositionをカメラの距離をもとに配置
		cameraPos.LookAt(this.transform);			//CameraPositionを中心の方向に向かせる
	}
}

```

今回は~~細かいことを考えるが面倒だったので~~カメラ領域内にとらえるオブジェクトをpublicにしてinspecter上であらかじめ設定できるようにしてあります。本来の実装の際にはList型に各オブジェクトを入れる関数なんかを用意して作ると思いますが、そのあたりはやり方自由ですよね。

変数の```margin```と```cameraHeight```の値はわりと自由な値でいいと思います。```cameraHeight```に関しては後で書きますが、値が実行中に乱数で増減するようにするとなんかそれっぽさが増したりもします。

先ほどのソースファイルを```CenterPoint```にアタッチしましたらとりあえずこんな感じにまずオブジェクトを2つでやってみます。
![0615d51cad059a6706caa70ad5247bfb.png](https://qiita-image-store.s3.amazonaws.com/0/151989/a2d29d6b-5bc9-4b4a-14cf-85151a83fd0f.png)

するとCenterPoint自身のrotation.yの値にもよりますが、こんな感じになります。
![camera8.gif](https://qiita-image-store.s3.amazonaws.com/0/151989/68f82c5f-a777-da11-39ae-76a9d196d0fa.gif)

いいかんじ。

オブジェクト同士が離れると自動的にカメラが離れて、近づけば自動的に寄ってくる感じになってます。
カメラの前後動作だけではありますが、これだけでも格闘ゲームとか某テイ○ズシリーズみたいな動きを再現できそうな感じがしてきます。

ではちょっとここからはなんとなく感満載でカメラの```Center```を中心としたy軸上の回転を加えてみましょう。
なんとなく感満載というのも、特にこれといった考え方を持っているわけではなく、乱数をはじいていい感じに動かそうとしてるだけです。

```csharp:GetCenterPoint
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class GetCenterPoint : MonoBehaviour {
	public List<Transform> transList = new List<Transform>();	//カメラ範囲内におさめたいオブジェクトのリスト
	private Transform cameraPos;
	public Camera usingCamera;
	private Vector3 pos = new Vector3();
	private Vector3 center = new Vector3();
	private float radius;
	private float margin = 1.0f;		//半径を少し余分にとるための値
	private float distance;
	private float cameraHeight = 1.5f;	//カメラが地面にめり込まないようにカメラを浮かせる高さ

	private float angle;
	private float timer = 0.0f;
	private float interval = 5.0f;

	void Start () {
		cameraPos = GameObject.Find("CameraPosition").GetComponent<Transform>();
	}
	
	void Update () {
		pos = new Vector3(0,0,0);
		radius = 0.0f;
		foreach (Transform trans in transList){		//オブジェクトのポジションの平均値を算出
			pos += trans.position;
		}
		center = pos/transList.Count;
		this.transform.position = center;			//CenterPointのポジションを中心に配置
		foreach (Transform trans in transList){		//中心から最も遠いオブジェクトとの距離を算出
			radius = Mathf.Max(radius, Vector3.Distance(center,trans.position));
		}
		distance = (radius + margin) / Mathf.Sin(usingCamera.fieldOfView*0.5f*Mathf.Deg2Rad);	//カメラの距離を算出
		cameraPos.localPosition = new Vector3(0,Mathf.MoveTowards(cameraPos.transform.position.y,cameraHeight,1.0f*Time.deltaTime),-distance);	//CameraPositionをカメラの距離をもとに配置
		cameraPos.LookAt(this.transform);			//CameraPositionを中心の方向に向かせる

		timer += Time.deltaTime;
		if ( timer > interval ) {
			angle += Random.Range(-30.0f,30.0f);
			cameraHeight = Random.Range(1.5f,2.5f);
			timer = 0.0f;
		}
		this.transform.eulerAngles = new Vector3(0.0f,Mathf.MoveTowardsAngle(this.transform.eulerAngles.y,angle,10.0f*Time.deltaTime),0.0f);
	}
}
```

変数```timer```を使って一定間隔ごとに乱数をはじきまして、はじいた乱数の値目指して少しずつ動いてもらっています。```Mathf.MoveTowards```というのと```Mathf.MoveTowardsAngle```というのが少しずつ値を動かす関数です。統一してiTween使えって話な気もしますがそこはそれ。iTween使った方がよかったかもしれませんが気にしません。
そもそもカメラの追尾にiTweenを使っているので角度の変更、位置の変更はパっと変えてしまってもいいんですけどね。ただ、思ったよりも急に動かれちゃうので少しずつ動いてもらうことにしました。

UIつけるとそれっぽさがましますね。
![camera10.gif](https://qiita-image-store.s3.amazonaws.com/0/151989/2d0cec4e-598c-8eb7-1d44-4863d6ff0eff.gif)
といっても雰囲気だけ出すためにUIつけて適当にオブジェクトを配置しただけですが。(おかげで味方のステータスが見えなかったり、そもそも全員動かないので距離が変わらない)
~~敵モブ？探すのが面倒だったのでたまたま拾ってきてた草に代理していただきました。~~

雰囲気出てますかね？
それっぽいなーって思っていただければ幸いです。
最後に今回のカメラワークをふんだんに活用するために(?)ユニティちゃんたちに走り回ってもらいました。
<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ユニティちゃんたちがただ走り回るだけ。自動で寄ったり離れたりしてみんなを映し続けるカメラワークさんの図。 <a href="https://t.co/etM0LyIMk3">pic.twitter.com/etM0LyIMk3</a></p>&mdash; おりばー。@週2でブルスク日和 (@OliverNotepad) <a href="https://twitter.com/OliverNotepad/status/939729866257133575?ref_src=twsrc%5Etfw">2017年12月10日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ちなみに```NavMeshAgent```を使って動いてもらってるのですが、これを使うとなぜか```cameraHeight```を使った```Mathf.MoveTowards```の挙動が変になっちゃうらしいのでカメラの高さは乱数で変えるだけにしてます。

# おわりに
長々と書きましたが実際のところ高校数学レベルのことですし、それほど難しい話でもないですね。
途中からのなんとなく感満載な部分に関しては前述のとおり某ファイ○ルファンタジー13をイメージしたゆえの結果です。
ゲームで使われるのは基本的にカメラの自動接近機能みたいなものなので、実際はそれ以前の部分で事足りますしね。もっと効率的なやり方もあるのかもしれないですが、今の私が模索した結果はこんな感じになりました。

ちなみになにか3Dゲームを作ろうとして、さきほどみたいな感じでRPGでも作れないものかと考えたりはしてたんですが、思った以上に無料で扱えるアニメーションやら3Dモデルにも限界があるものですね。自分で作成することも視野に入れたりはしてますが、しばらくはRPGに手を出すのは控えようかなって思ってます。

# 参考

* [カメラからの距離で求める錐台のサイズ](https://docs.unity3d.com/jp/530/Manual/FrustumSizeAtDistance.html)
* [Camera.fieldOfView](https://docs.unity3d.com/jp/540/ScriptReference/Camera-fieldOfView.html)