[Unity]スマホゲームでゼルダっぽい3Dカメラ挙動の研究(1/3) - カメラ設計

# スマホゲーのカメラの不満

ちょっと前までは、パズル＆RPG的なゲームばっかりだと思ってたら、いつの間にかガンガン動くアクションゲームがドンドン出ています。

けど、エフェクトとかカメラ効果はキレイだったりするけど、いざプレイすると**カメラが固定角度**だったり、**引き気味での表示**だったり、**視界変更は移動とは別操作**だったりと、どれも**スマホならではアクションゲームのカメラ挙動**の確立に苦戦してたり、割り切ってしまってたりするように思います。（インストールしてみてガッカリ…という経験ある人も多いのでは）

処理負荷やリソース量を抑えるために背景を特定の角度からしか映せない！という事情もあるかもですが、**やっぱりスマホでもいろんな視点からモノが見えるビューを実現したい！**

ということで、

 - スマホでも無理なくできる操作量で
 - 観たい場所に無理なくカメラが動き
 - 自由度の権化であるゼルダっぽい移動とカメラ

が実現できるかを、実際に実装しながら考えてみました。
長くなりそうなので、前編・中編・後編に分けて書いていきます。

**前編：カメラ設計 ← 今回はココ**
TPSのカメラ挙動を作るための構造。カンタンで制御しやすいコンポーネント・プレハブを用意

**中編：探索パート**
TPSのカメラ挙動の基本。スマホで課題になりがちな「キャラ移動とカメラの操作をどう共存させるか？」について

**後編：注目パート**
ゼルダで言うところのZ注目の動きについて。意外と制御が面倒。。。
いろんなゲームタイトルで踏襲されてる由緒あるビューなのでポイントをおさえていく


# 探索パート

こういうビューです。

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">3Dゲームでカメラの動かし方試し。360度回して遊ばせるのはやっぱきついかな。遮蔽物に隠れたときに透かすのはいいんだけど結局道が見えないからイマイチ。角度ももうちょい浅くして臨場感増やしたいけどこれまた道が見えん <a href="https://t.co/fuU40OMaLf">pic.twitter.com/fuU40OMaLf</a></p>&mdash; グラ殺バーガー (@flankids) <a href="https://twitter.com/flankids/status/828205800199827456?ref_src=twsrc%5Etfw">2017年2月5日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">ユニティちゃんトゥーンシェーダーのパラメータ豊富でテキトーに設定しても影とアウトラインだけ気をつければそれっぽくなる優秀<a href="https://t.co/xDT5s1leMA">https://t.co/xDT5s1leMA</a> <a href="https://t.co/UyfslWCtWp">pic.twitter.com/UyfslWCtWp</a></p>&mdash; グラ殺バーガー (@flankids) <a href="https://twitter.com/flankids/status/877589026928074752?ref_src=twsrc%5Etfw">2017年6月21日</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script> 

スワイプした方向にキャラクターが進みつつ、他の入力無しにカメラも回しています。

* 視界をY軸で回転させる
* むやみにキャラの後ろに回り込ませない（視界の安定）
* カメラ専用の操作はしない

という方針の仕様です

# 元ネタ
先にネタばらしすると、
**LEGO Star Wars: The Force Awakens** （[iOS版](https://itunes.apple.com/jp/app/lego-star-wars-the-force-awakens/id1106014973?mt=8) | [Android版](https://play.google.com/store/apps/details?id=com.wb.goog.legoswtfa&hl=ja)）
というアプリの、とある場面での操作＆カメラを見て「これメッチャいいな…」と思い、いろいろ触って表面から分かる仕様をマネしたモノです。

https://youtu.be/Wv0kmlEMyTU?t=19s
参考にしたのはこの動画の19秒からの移動シーンです。
動画だとちょっと分かりづらいんですが、

* カメラに対して前後に動くときはカメラ角度はほとんど変わらない
* カメラに対して右に動くと右回転、左に動くと左回転する

というような仕様になっています（と解釈しています）
これが触ってみるとなかなか**直感的にわかりやすく視界がついてきて**、もしかしたら自由な視点のアクションゲームにかなりイケてる仕様なんじゃないか？と思ってます。

# 実装 - カメラ
カメラの操作ロジックを実装するために、
カメラを制御するためのパラメータを用意します。

## オブジェクト構成
まず、下記のようにCameraを入れ子構造の孫にしてください。

![kouzou.JPG](https://qiita-image-store.s3.amazonaws.com/0/184294/cec4f821-a84c-12b9-8e5e-5777e82b2aad.jpeg)

![Inspector.JPG](https://qiita-image-store.s3.amazonaws.com/0/184294/770da14a-f386-2d69-0ec0-362139c46516.jpeg)

完成です。楽ちんですね。

この構成の中で、それぞれ下記の要素が、記載してあるパラメータに対応しています。

|パラメータ|要素|
|---|---|
|注視点|CameraParentの座標|
|注視点からの距離|CameraChildのローカルZ座標（負数）|
|注視点への回り込み角度|CameraParentの角度(X,Y)|
|視界オフセット座標|Main Cameraのローカル座標(X,Y)|

### 例１

|要素|値|
|---|---|
|CameraParentの座標|(0, 0, 0)|
|CameraChildのローカルZ座標|-10|
|CameraParentの角度(X,Y)|(30, 0)|
|Main Cameraのローカル座標(X,Y)|(0, 1)|
※キャラクターのモデルの座標、角度共に(0, 0, 0)のとき
※Main CameraのFieldOfView（画角）が60のとき
※アスペクト比は9:16（≒iPhone6）

このように値を入れると、下記のようなビューになるはずです。

<img width="359" alt="ビューサンプル.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/184294/7faa0cfb-6a16-188e-57df-3f72a1f99b6b.jpeg">
<br>△白い箱はサイズ比較用。１㎥の立方体です

CameraParentのX角度を小さくすると、カメラが低いアングルから映したり、CameraChildのローカルZ座標を0に近くするとカメラが寄ったりするのがわかると思います。

### 例２

Main Cameraのローカル座標は、注視点（キャラクター）の画面上の位置をズラすような目的で利用します。

|要素|値|
|---|---|
|CameraParentの座標|(0, 0, 0)|
|CameraChildのローカルZ座標|-7|
|CameraParentの角度(X,Y)|(5, 0)|
|Main Cameraのローカル座標(X,Y)|(1, 1.75)|

このように値を入れると、下記のようなビューになるはずです。

<img width="359" alt="オフセット.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/184294/e14cc753-98cd-d403-2bd2-fc0a70de87fa.jpeg">

注視点であるキャラクターは必ずしも画面中央付近に表示することがベストではありません。
エイムをするようなシーンがある場合は、このようにキャラクターを脇にずらして、画面中央に照準を持ってくるのが一般的だと思います。

これでオブジェクトの入れ子関係を使って、複雑な計算無しに大まかなカメラ制御が出来るようになりました。

## スクリプト

いちいち各オブジェクトの要素を触るのは面倒なので変数で動かせるようにしておきます。

```csharp:CameraManager.cs
using UnityEngine;

public class CameraManager : MonoBehaviour {
	private Transform _cameraParent;
	private Transform _cameraChild;
	private Transform _camera;

	/// <summary>
	/// 注視点（CameraParentの座標）
	/// </summary>
	public Vector3 LookPosition;

	/// <summary>
	/// 注視点からの距離（CameraChildのローカルZ座標）
	/// </summary>
	public float Distance;

	/// <summary>
	/// 注視点への回り込み角度（CameraParentの角度）
	/// </summary>
	public Vector2 LookAngles;

	/// <summary>
	/// 視界オフセット座標（Main Cameraのローカル座標）
	/// </summary>
	public Vector2 OffsetPosition;

	void Start () {
		_cameraParent = transform;
		_cameraChild = _cameraParent.GetChild(0);
		_camera = _cameraChild.GetChild(0);
	}
	
	void Update () {
		_cameraParent.position = LookPosition;
		_cameraChild.localPosition = new Vector3(0, 0, -Distance); // 負数にする
		_cameraParent.eulerAngles = LookAngles;
		_camera.localPosition = OffsetPosition;
	}
}
```

Inspector上の表示はこんな感じになります。

<img width="360" alt="Inspector.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/184294/f4b4c2f1-16ab-c1fc-7ecb-d35d82874b72.jpeg">

実行中に各値を変更すると、カメラ距離や回り込み位置を変えることが出来ます。

### 注視対象を追う機能
このスクリプトだと注視座標を直接指定する形になっていますが、TPS視点のカメラの場合、基本的には**特定のオブジェクトの移動を追いかける**はずです。

CameraManagerにTransformオブジェクトを登録して、注視対象を設定できるように改修しましょう。

```csharp:CameraManager.cs
using UnityEngine;

public class CameraManager : MonoBehaviour {
	private Transform _cameraParent;
	private Transform _cameraChild;
	private Transform _camera;

	/// <summary>
	/// 注視対象
	/// 座標をCameraParentの座標に代入
	/// </summary>
	public Transform LookTarget;

	/// <summary>
	/// 注視点からの距離（CameraChildのローカルZ座標）
	/// </summary>
	public float Distance;

	/// <summary>
	/// 注視点への回り込み角度（CameraParentの角度）
	/// </summary>
	public Vector2 LookAngles;

	/// <summary>
	/// 視界オフセット座標（Main Cameraのローカル座標）
	/// </summary>
	public Vector2 OffsetPosition;

	void Start () {
		_cameraParent = transform;
		_cameraChild = _cameraParent.GetChild(0);
		_camera = _cameraChild.GetChild(0);
	}
	
	void Update () {
		_cameraParent.position = LookTarget.position;
		_cameraChild.localPosition = new Vector3(0, 0, -Distance); // 負数にする
		_cameraParent.eulerAngles = LookAngles;
		_camera.localPosition = OffsetPosition;
	}
}
```

こんな感じです。Inspector上で"LookTarget"に注視対象とするキャラクターのTransformを登録するのを忘れずに。

<img width="359" alt="LookTarget.jpg" src="https://qiita-image-store.s3.amazonaws.com/0/184294/351f5a8a-1bd7-15de-d0f5-156a3ef51ca4.jpeg">

これで実行中に注視対象のオブジェクトの座標を動かしても、カメラがついてくるようになったはずです。

#### TIPS
LookTargetの座標を直接代入するとカメラがピッタリ付いてきすぎて硬い感じがするので、座標更新に補間をかけるとイイ感じになったりします。

```csharp:CameraManager.cs
_cameraParent.position = Vector3.Lerp(_cameraParent.position, LookTarget.position, 0.1f);
```
この実装はやや雑ですが、どんな感じになるか試してみると雰囲気の変化がわかると思います。
この考え方は注視点の更新のみではなく、他のパラメータについても同じことが言えます。適宜補間をかけるといいでしょう。

※ただし、エイム操作など、入力に対してきっちりカメラがついてくることが重要なゲームや操作モードに関しては補間をかけない方がベターだったりします。ケースバイケース。

# 次回
今回はここまで。次回が本題の操作パートです。

* インプットの管理
* キャラクターの移動操作
* カメラの視点操作

に分けて説明していきます。