【Unity】範囲外のオブジェクトを端っこで表示させ続けるミニマップ

# 概要
重要な建造物はミニマップやレーダー上で範囲外になっても端っこに表示させ続けるゲームは数多く存在します、多分。
今回はそんなミニマップをUnity上で作ってみようと思います。

ちなみに完成するとこんな感じになります。
今回は範囲外であることがわかるように半透明で表示させています。
![05_minimapComplete.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/14b5e038-508e-35a2-53da-cf1a84e265ab.gif)

# 下準備
## 1. とりあえず動くキャラクターと適当にオブジェクトを配置する
- とりあえず地面と動かせるものを準備する。困ったら豆腐
	- 地面は`Ground`、動かせるものは`Player`とかわかりやすいようにしておく
- 後はミニマップに表示するためのオブジェクトも適当に配置
	- 名前は`Object`とか適当に
- ついでにカメラもそれっぽく追尾させる

![01_moveBox.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/446bfd24-dc23-40c3-2d89-d0fba83e4f0f.gif)

## 2. ミニマップのアイコンを用意する
それっぽいアイコンを作ります。
面倒くさかったらProjectから`Create->Sprites`を使ってもいいです。
色は`SpriteRenderer`で変えることもできるのでとりあえず白のみの画像で。
![minimap_icon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/8971de50-b34c-1af8-2536-874a7a90b080.png)

## 3. ミニマップのフレームとマスク画像を用意する
今回は円形で作るので円形のフレームとマスク画像を準備します。即席で作ったので使いたい方はどうぞ。
![minimap_frame.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/d2e94146-798d-99b0-c60b-f0a893773e05.png) ![minimap_mask.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/5d7bc67a-5489-84b0-50ea-fa4704b65cd0.png)

# ミニマップを表示する
まずはシンプルな円形ミニマップを作ります。

## 1. ミニマップ用のカメラを作成する
- 空のゲームオブジェクト`Minimap`を作る
- `Minimap`の子になるようにミニマップ用のカメラ`Minimap Camera`を追加
	- Rotationを(90, 0, 0)にしてカメラを真下に向かせる
- プレイヤーを中心にカメラを動かすスクリプトを作って`Minimap Camera`にアタッチする
	- InspectorからプレイヤーのTransformを設定するのを忘れずに
- ProjectionをOrthographicにしてSizeを適宜調整する
![01_minimapcamera2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/a2867dea-cd4a-b73d-4ec1-cd4ee6d490fc.png)

```cs:MinimapCamera.cs
public class Minimap : MonoBehaviour {
	[SerializeField] private Player player;

	void Update () {
		var pos = player.transform.position;
		pos.y = transform.position.y;
		transform.position = pos;
	}
}
```

`Minimap Camera`のカメラプレビューでプレイヤーを追尾していることを確認します。
![02_minimapCamera.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/9610ffbe-ce91-1dbb-e57d-c46fd85db94b.gif)

## 2. ミニマップを円形で表示する
ミニマップカメラで映しているものを画面上に表示します。

- ProjectからRender Textureを作成、ファイル名は`MinimapRender`にする
- Minimap Cameraを選択して、CameraのTarget Textureに作成した`MinimapRender`を設定する

これでRender TextureにMinimap Cameraの映しているものを表示することができます。
次にミニマップを円形にして表示します。

- フレーム用のImageを作成する
	- 名前は`Minimap Frame`にしてSource Imageにフレーム画像を設定する
- `Minimap Frame`の子にマスク画像のImageを作成する
	- 名前は`Minimap Mask`にしてSource Imageにマスク画像を設定する
	- 設定したらMaskをアタッチして`Show Mask Graphic`のチェックを外す
- `Minimap Mask`の子にRaw Imageを作成する
	- 名前は'Minimap Render'にしてTextureに`MinimapRender`を設定する
- Rect TransformのWidthとHeightは適宜調整する

ヒエラルキーはこんな感じに
![02_minimapdisp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/fbb103df-1499-a395-efb7-a60d1842d851.png)

ミニマップが円形で表示されてることを確認します。
![03_minimap.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/9d5ff936-674b-83ce-baa2-730110a966c2.gif)

## 3. ミニマップにアイコンを表示する
現在のミニマップは上から撮ったものをそのまま映しているだけです。そのため、地面とアイコンのみを表示します。

- 地面用とアイコン用のレイヤーを追加する
	- それぞれ`Ground`, `Minimap`にする
- `Main Camera`のCulling Maskから`Minimap`を除外する
- `Minimap Camera`のCullin Maskを`Ground`と`Minimap`にする

これでメインカメラは`Minimap`以外、ミニマップカメラは`Ground`と`Minimap`のレイヤーのみを映すことができます。
アイコンのレイヤーを`Minimap`に設定するとミニマップ上にアイコンを表示することができます。

早速プレイヤーのアイコンをミニマップに表示させてみます。

- ヒエラルキーの`Player`にミニマップ用のアイコンをD&Dする
	- 名前は`MinimapIcon`とかにリネームする
- Rotationを(90, 0, 0)にする
- レイヤーを`Minimap`にする

![03_minimapicon.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/b911376e-630c-4675-35d6-f72bd6d3dfd1.png)

これでミニマップ上にプレイヤーを示すアイコンを表示することができます。
`Object`に対しても同じことを行い、オブジェクトのアイコンも表示させてみるとこんな感じ。
![04_minimapIcon.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/c4558e42-7639-7f6a-ef2c-9b73ca37a227.gif)

# ミニマップ範囲外のオブジェクトを半透明で表示する
アイコン表示を制御するためのスクリプトを作成して`Object`の`MinimapIcon`にアタッチします。
minimapCameraはInspector上から設定していますが、ミニマップカメラにタグつけて`GameObject.FindGameObjectWithTag`を使ってもいいと思います。
表示範囲は何も考えずにカメラの`orthographicSize`に設定しておきます（`orthographicSize`はカメラ縦幅の半分の大きさ）。

```cs:MinimapIcon.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))]
public class MinimapIcon : MonoBehaviour {
	[SerializeField] private Camera minimapCamera;				// ミニマップ用カメラ
	[SerializeField] private Transform iconTarget;				// アイコンに対応するオブジェクト（建造物等）
	[SerializeField] private float rangeRadiusOffset = 1.0f;	// 表示範囲のオフセット

	// 必要なコンポーネント
	private SpriteRenderer spriteRenderer;

	private float minimapRangeRadius;	// ミニマップの表示範囲
	private float defaultPosY;			// アイコンのデフォルトY座標
	const float normalAlpha = 1.0f;		// 範囲内のアルファ値
	const float outRangeAlpha = 0.5f;	// 範囲外のアルファ値

	private void Start () {
		minimapRangeRadius = minimapCamera.orthographicSize;
		spriteRenderer = gameObject.GetComponent<SpriteRenderer>();
		defaultPosY = transform.position.y;
	}

	private void Update () {
	}
}
```

## 1.オブジェクトがミニマップ範囲内か確認する
ミニマップ用カメラとオブジェクトの距離を求めて、その距離がミニマップ範囲内かチェックします。
今回は[Vector3.Distance][2]で距離を求めます。平面上の距離を求める必要があるので、Y座標は統一しておきます。

```cs:MinimapIcon.cs
	private bool CheckInsideMap() {
		var cameraPos = minimapCamera.transform.position;
		var targetPos = iconTarget.position;

		// 直線距離で判定するため、yは0扱いにする
		cameraPos.y = targetPos.y = 0;

		return Vector3.Distance(cameraPos, targetPos) <= minimapRangeRadius - rangeRadiusOffset;
	}
```

## 2.ミニマップ範囲内のオブジェクトを表示
ミニマップ範囲内の場合は`SpriteRenderer`のアルファ値を1にしてそのまま表示すればOKです。

```cs:MinimapIcon.cs
	private void Update () {
		DispIcon();
	}

	private void DispIcon() {
		// アイコンを表示する座標
		var iconPos = new Vector3(iconTarget.position.x, defaultPosY, iconTarget.position.z);

		// ミニマップ範囲内の場合はそのまま表示する
		if (CheckInsideMap()) {
			spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, normalAlpha);
			transform.position = iconPos;
			return;
		}
	}
```

## 3.ミニマップ範囲外のオブジェクトを表示
ミニマップ端にアイコンを表示するためにベクトルを計算する必要がありますが、[Vector3.ClampMagnitude][1]を使うことで簡単に求めることができます。
このAPIはベクトルと最大距離を指定すると最大距離までに制限したベクトルを返してくれるすごいやつです。~~これを紹介するためにこの記事を書いてると言っても過言ではない。~~
方向ベクトルを`Vector3.ClampMagnitude`で制限することでミニマップ端までのベクトルを簡単に求めることができます。
半透明にするには`SpriteRenderer`のアルファ値を0.5くらいにすればOKです。

```cs:MinimapIcon.cs
		// アイコンを半透明にする
		spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, outRangeAlpha);

		// カメラとアイコンの位置から方向ベクトルを求める
		var centerPos = new Vector3(minimapCamera.transform.position.x, defaultPosY, minimapCamera.transform.position.z);
		var offset = iconPos - centerPos;

		// 指定距離で制限した方向ベクトルを求めてアイコン位置を設定する
		transform.position = centerPos + Vector3.ClampMagnitude(offset, minimapRangeRadius - rangeRadiusOffset);
```

## 4.コード全文
```cs:MinimapIcon.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

[RequireComponent(typeof(SpriteRenderer))]
public class MinimapIcon : MonoBehaviour {
	[SerializeField] private Camera minimapCamera;				// ミニマップ用カメラ
	[SerializeField] private Transform iconTarget;				// アイコンに対応するオブジェクト（建造物等）
	[SerializeField] private float rangeRadiusOffset = 1.0f;	// 表示範囲のオフセット

	// 必要なコンポーネント
	private SpriteRenderer spriteRenderer;

	private float minimapRangeRadius;	// ミニマップの表示範囲
	private float defaultPosY;			// アイコンのデフォルトY座標
	const float normalAlpha = 1.0f;		// 範囲内のアルファ値
	const float outRangeAlpha = 0.5f;	// 範囲外のアルファ値

	private void Start () {
		minimapRangeRadius = minimapCamera.orthographicSize;
		spriteRenderer = gameObject.GetComponent<SpriteRenderer>();
		defaultPosY = transform.position.y;
	}

	private void Update () {
		DispIcon();
	}

	/// <summary>
	/// アイコン表示を更新する
	/// </summary>
	private void DispIcon() {
		// アイコンを表示する座標
		var iconPos = new Vector3(iconTarget.position.x, defaultPosY, iconTarget.position.z);

		// ミニマップ範囲内の場合はそのまま表示する
		if (CheckInsideMap()) {
			spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, normalAlpha);
			transform.position = iconPos;
			return;
		}

		// マップ範囲外の場合、ミニマップ端までのベクトルを求めて半透明で表示する
		spriteRenderer.color = new Color(spriteRenderer.color.r, spriteRenderer.color.g, spriteRenderer.color.b, outRangeAlpha);
		var centerPos = new Vector3(minimapCamera.transform.position.x, defaultPosY, minimapCamera.transform.position.z);
		var offset = iconPos - centerPos;
		transform.position = centerPos + Vector3.ClampMagnitude(offset, minimapRangeRadius - rangeRadiusOffset);
	}

	/// <summary>
	/// オブジェクトがミニマップ範囲内にあるか確認する
	/// </summary>
	/// <returns>ミニマップ範囲内の場合、trueを返す</returns>
	private bool CheckInsideMap() {
		var cameraPos = minimapCamera.transform.position;
		var targetPos = iconTarget.position;

		// 直線距離で判定するため、yは0扱いにする
		cameraPos.y = targetPos.y = 0;

		return Vector3.Distance(cameraPos, targetPos) <= minimapRangeRadius - rangeRadiusOffset;
	}
}
```

## 5.動作確認
スクリプトのInspectorから`minimapCamera`, `iconTarget`, `Range Radius Offset`を設定しつつ、`Object`のアイコン表示が期待通りに動作すればOKです。今回は`Range Radius Offset`を3.5にしてみました。
![05_minimapComplete.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/274329/14b5e038-508e-35a2-53da-cf1a84e265ab.gif)

# おしまい
というわけで割とお手軽に作ることができました。
作り始めたときは「画面端へのベクトル計算とかどうするねん」って思っていたのですが、`Vector3.ClampMagnitude`を使えば面倒な計算とかしなくても実装することができました。

すごく簡単に作ったので「ここおかしくね？」みたいなのあったらぜひコメントください。

# 参考
- [Qiita:Unity　ミニマップの作り方][3]
- [Unityでゲームを作ろう！ ミニマップの作り方][4]

[1]:https://docs.unity3d.com/ja/current/ScriptReference/Vector3.ClampMagnitude.html
[2]:https://docs.unity3d.com/ja/current/ScriptReference/Vector3.Distance.html
[3]:https://qiita.com/kakichoko/items/0ec67edebcc10f53ce73
[4]:https://unity.moon-bear.com/unitychan-coin/minimap/