横方向の視界を揃えて公平なゲームを作るぞ、という話

[**グレンジ Advent Calendar 2019**](https://qiita.com/advent-calendar/2019/grenge) 9日目担当の、flankids です。
普段はインプット管理、カメラワーク作り、アニメーション制御などなどで遊び心地を作ることを主にやってるエンジニアをしています。

今回は、Unityで3D画面のアスペクト比の管理についてのお話です。よろしくお願いします！

# アス比が違うと起きること

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/b41cdb12-0e89-e8ff-1f8b-4f186c852e57.png" width=300>
プライドをかけたタイマンだ！
と思ってたけどiPadでプレイしたら…

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/1841c224-a350-2d01-056d-fd81a5cc2557.png" height=402>　…　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f15e867f-cd39-09b0-3be1-f6d4b49add56.png" height=402>

なんだねキミたちは！？

と、いうようなことが起きます。
これは **一人用ゲームならアス比の違いでゲームの難易度が変わってしまう** し、**対戦ゲームなら視野の違いで情報格差が出て有利不利が変わってしまいます** 。
ゲーマーならこの不公平、許せねえ…！

# なんでこうなるの？

端末のアスペクト比が変わったとき、**画角** …すなわちCameraコンポーネントの **FieldOfView** （以下、FOV）は変わらないせいです。
UnityのカメラのFOVの値は **垂直視野** であり、水平FOVは画面のアス比によって変わります。（[公式リファレンスより](https://docs.unity3d.com/ja/current/ScriptReference/Camera-fieldOfView.html)）
その結果、アスペクト比が横に伸びた時にFOVが同じ値だと、純粋に視野が横に広がります。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/98bf5c0f-b26e-76d0-c0d5-8c2d388753ae.png" height=402> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/3dffde5d-0abd-a6d8-2548-d042a99d5508.png" width=300>

そもそもiPadのようなタブレットはディスプレイサイズが大きく普通より画面が広く使える分、**より多くの要素を画面に映して快適にアプリが使用できるべき** ではあるのですが、前例のとおり、ことゲームに関してはそれだとマズいときがあります。

それを解決するためには、**アス比によって画角を変える** 必要があります。


# 視野を揃える

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/069b5f91-3e1f-130c-cdea-355ce42968bf.png" height=402>　<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/090c2f72-9ddf-425f-b37d-6fed8f3fb1c7.png" height=402>


揃いました。

# 忙しいあなたに先に手段を説明！

##### ■スクリプトを用意

```csharp:HorizontalFOVFitter.cs
using UnityEngine;

/// <summary>
/// 水平視野を合わせる機能
/// </summary>
public class HorizontalFOVFitter : MonoBehaviour {
	/// <summary>カメラ</summary>
	[SerializeField]
	private Camera _camera = null;
	/// <summary>水平視野合わせ機能をOFFにする（垂直視野を合わせる）か否か</summary>
	[SerializeField]
	private bool _isDisable = false;
	/// <summary>基準にするアスペクト比（解像度での指定も可）</summary>
	[SerializeField]
	private Vector2 _baseAspect = new Vector2(750, 1334);
	/// <summary>基準にするCameraのFOV（垂直視野）</summary>
	[SerializeField]
	private float _baseFieldOfView = 60f;

	private void Update() {
		if (_camera == null) {
			return;
		}
		if (_isDisable) {
			// 機能を無効化しているときは、通常通り垂直視野をしてFOVを反映する
			if (_camera.fieldOfView != _baseFieldOfView) {
				_camera.fieldOfView = _baseFieldOfView;
			}
			return;
		}
		// 基準の垂直視野とアスペクト比から、基準にする水平視野を計算する
		float baseHorizontalFOV = CalcHorizontalFOV(_baseFieldOfView, CalcAspect(_baseAspect.x, _baseAspect.y));
		float currentAspect = CalcAspect(Screen.width, Screen.height);
		// 基準にする水平視野と現在のアスペクト比から、反映すべき垂直視野を計算する
		_camera.fieldOfView = CalcVerticalFOV(baseHorizontalFOV, currentAspect);
	}

	/// <summary>
	/// アスペクト比を計算する
	/// </summary>
	private float CalcAspect(float width, float height) {
		return width / height;
	}

	/// <summary>
	/// 垂直視野とアスペクト比から、水平視野を計算する
	/// </summary>
	private float CalcHorizontalFOV(float verticalFOV, float aspect) {
		return Mathf.Atan(Mathf.Tan(verticalFOV / 2f * Mathf.Deg2Rad) * aspect) * 2f * Mathf.Rad2Deg;
	}

	/// <summary>
	/// 水平視野とアスペクト比から、垂直視野を計算する
	/// </summary>
	private float CalcVerticalFOV(float horizontalFOV, float aspect) {
		return Mathf.Atan(Mathf.Tan(horizontalFOV / 2f * Mathf.Deg2Rad) / aspect) * 2f * Mathf.Rad2Deg;
	}
}
```

##### ■Cameraにコンポーネント設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/80a79ea3-d8bc-78c3-b621-50c82d25137e.png)

* **Base Aspect** に、基準にするアス比（もしくは解像度）
* **Base Field Of View** に、基準にする画角

を設定して、ゲームを実行すれば完了！

# 理屈

※[こちらのteratailの質問への回答](https://teratail.com/questions/191194)が非常に参考になりました！

こちらの解説をもとに、以下の公式を作り、

* アスペクト比
* 垂直視野画角(CameraのFOV)
* 水平視野画角

の3要素のうち、アスペクト比と1つの情報があれば残りの1要素を算出できるようにしました。
その公式は以下の通りです。

```csharp:水平視野画角の公式
Atan(Tan(垂直視野画角/ 2) * アスペクト比) * 2
```
```csharp:垂直視野画角の公式
Atan(Tan(水平視野画角/ 2) / アスペクト比) * 2
```

後は、

1. 基準の垂直視野と基準のアスペクト比から、基準にする水平視野を計算する
2. 基準にする水平視野と現在のアスペクト比から、反映すべき垂直視野を計算する
3. カメラのFieldOfViewに垂直視野の値を設定する

で完成です！

# まとめ

この方法で、アスペクト比の違いに影響されず左右の視野を統一することができました！
ただし、今度はアスペクト比が横長になるほど縦の視野が狭まるようになるため、**アスペクトの横の比率が低い時だけ本実装を適用する** というような仕様で使うことも考えたほうがいいかもしれません。

遊びやすさと公平性をうまくバランス取って視野制御を！

# 余談
FPSゲームでは、タイトルによってはオプションで画角を調整できるものもあるようです。
https://esports-gaming.info/?p=10510

これは皆こぞって画角を広くしてるのだろうと思いましたが、意外とFOVを小さくする人もいるようです。
その理由として、画角が小さいと視野が狭くなる代わりに **「遠くのものでも、画面上で大きく表示される」** という望遠鏡的な特徴を利用できるから、というものでした。なるほど…

視野が広いほうが有利なのか、遠くのものが見えやすくなる方が有利なのか、この辺りはゲーム性やレベルデザイン次第で変わるところなので、どうすればユーザーさんが快適に、かつ不公平を感じずにプレイできるかを常に考えることが大事ですね。

# 使用デザイン
* キャラモデル - [Forest Creatures Pack](https://assetstore.unity.com/packages/3d/characters/creatures/forest-creatures-pack-26704)
* 背景モデル - [Stylized Nature Pack](https://assetstore.unity.com/packages/3d/environments/stylized-nature-pack-37457)
* 炎アイコン - [炎のイラスト | シルエットデザイン](http://kage-design.com/wp/?p=2407)