マップ上にプレイヤーの位置と回転を表示します。
画像を作る
マップの画像はシーンビューで真上からスクリーンショットしました。シーンギズモのY軸をクリックして真上から表示します。
ギズモの中央のキューブかテキストをクリックして、Orthographicビューにします。
ディレクショナルライトのShadow Mapやプレイヤーを無効化したり、シーンビューでエフェクトやアイコン、グリッドなどをオフにできます。
マップ画像にしたい部分をスクリーンショットして、Unityにインポートしスプライトにします。
UIを作成
マップのスプライトを表示するImageオブジェクトを追加します。
マップのImageオブジェクトの子として、プレイヤーのアイコンを設定するImageオブジェクトも追加します。プレイヤーのアイコンは上向きの矢印にしました。
ImageコンポーネントのSource Imageにスプライトを設定します。マップのRectTransformでは、元の画像のサイズの比率を変えないようにしてWidthとHeightの値を設定します。
画像をリサイズするツールを使うと簡単に計算できます。http://www.n1-sitemaker.com/resize/
今回はマップの左下を基準にするので、Pivotを(0,0)にします。
プレイヤーアイコンのRectTransfromでは、Pivotを(0.5, 0.5)にします。
アンカーとポジションを0にしたときに、プレイヤーアイコンの中心が、マップ画像の左下に来ます。
プレイヤーはマップの子なので、マップ画像は自由に移動や回転、拡大ができます。
マップ上の位置を計算
シーンでの実際のマップの大きさと画像の大きさの比を、プレイヤーの位置にかけることで、マップ上のアイコンの位置を計算できます。
マップの大きさ
マップの大きさを計算するために、マップの左下と右上の位置をCanvasに付けるスクリプトに保存します。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TestMapCanvas : TestCanvas
{
Image mapImage;
Image playerImage;
Transform player;
public Vector3 mapBottomLeftPosition;
public Vector3 mapTopRightPosition;
// ...
}
この値をシーンビューでハンドルを使って設定できるようにエディタを拡張します。
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(TestMapCanvas))]
public class TestMapCanvasEditor : Editor
{
private void OnEnable()
{
SceneView.duringSceneGui += OnSceneGUI;
}
private void OnDisable()
{
SceneView.duringSceneGui -= OnSceneGUI;
}
private void OnSceneGUI(SceneView sv)
{
TestMapCanvas tmc = (TestMapCanvas)target;
EditorGUI.BeginChangeCheck();
Vector3 newBottomLeftCorner = Handles.PositionHandle(tmc.mapBottomLeftPosition, Quaternion.identity);
Vector3 newTopRightCorner = Handles.PositionHandle(tmc.mapTopRightPosition, Quaternion.identity);
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(tmc, "Change Positions");
tmc.mapBottomLeftPosition = newBottomLeftCorner;
tmc.mapTopRightPosition = newTopRightCorner;
}
}
}
Canvasを選択するとシーンビューでハンドルが使えるようになります。
アイコンの位置を計算
Canvasのスクリプトでアイコンの位置を計算します。
Vector3 CalculateIconImagePosition(Vector3 targetWorldPosition)
{
// マップの幅
var mapWidth = mapTopRightPosition.x - mapBottomLeftPosition.x;
// マップの奥行
var mapDepth = mapTopRightPosition.z - mapBottomLeftPosition.z;
// 左下を基準にした位置
var targetRelativePosition = targetWorldPosition - mapBottomLeftPosition;
// アイコンの位置
var iconImagePosition = Vector3.zero;
iconImagePosition.x = targetRelativePosition.x * mapImage.rectTransform.sizeDelta.x / mapWidth;
iconImagePosition.y = targetRelativePosition.z * mapImage.rectTransform.sizeDelta.y / mapDepth;
return iconImagePosition;
}
まず、左下と右上に置いたハンドルの値を使って、3D空間のマップの幅と奥行を計算します。
public Vector3 mapBottomLeftPosition;
public Vector3 mapTopRightPosition;
// ----------
// マップの幅
var mapWidth = mapTopRightPosition.x - mapBottomLeftPosition.x;
// マップの奥行
var mapDepth = mapTopRightPosition.z - mapBottomLeftPosition.z;
上と右がプラスなので、右上の位置から左下の位置を引きます。マップの画像の向きが違うと符号などが変わります。
引数にはプレイヤーや敵、ゴール地点などの3D空間での位置が渡されます。これは原点が基準なので、マップの左下を基準とした位置に変換します。
var targetRelativePosition = targetWorldPosition - mapBottomLeftPosition;
UIでのアイコンの位置は、3D空間での位置に、マップの3D空間での幅や奥行と、マップ画像の幅や高さの比をかけると求められます。
var iconImagePosition = Vector3.zero;
iconImagePosition.x = targetRelativePosition.x * mapImage.rectTransform.sizeDelta.x / mapWidth;
iconImagePosition.y = targetRelativePosition.z * mapImage.rectTransform.sizeDelta.y / mapDepth;
return iconImagePosition;
}
アイコンの回転を計算
プレイヤーの向きによって矢印アイコンを回転させます。シーンの真上(Y)方向を軸とした回転を、アイコンのRectTransformのZ軸の回転にします。
Quaternion CalculateIconImageRotation(Quaternion targetWorldRotation)
{
var euler = Quaternion.Inverse(targetWorldRotation).eulerAngles;
return Quaternion.Euler(new Vector3(0f, 0f, euler.y));
}
プレイヤーとアイコンが逆に回転したので、Quaternion.Inverseメソッドを使っています。ここでも、マップ画像の向きによって計算が変わります。例えば、マップが上下逆さまだった場合、アイコンのZ回転に180を足します。
敵や目的地のアイコンを表示
リストを使って複数のターゲットをアイコン表示できるようにしました。
List<Transform> targets;
[SerializeField] Image targetIconPrefab;
// ----------------
public void AddTarget(Transform target)
{
if (targets == null) targets = new List<Transform>();
targets.Add(target);
}
アイコンのImageオブジェクトはプレハブにしてスクリプトにアタッチします。
スクリプト
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
public class TestMapCanvas : TestCanvas
{
Image mapImage;
Image playerImage;
Transform player;
public Vector3 mapBottomLeftPosition;
public Vector3 mapTopRightPosition;
List<Transform> targets;
[SerializeField] Image targetIconPrefab;
public void Init(Transform player)
{
this.player = player;
mapImage = transform.GetChild(0).GetComponent<Image>();
playerImage = transform.GetChild(0).GetChild(0).GetComponent<Image>();
}
public void Open()
{
gameObject.SetActive(true);
UpdateIconPositionAndRotation();
}
public void Close()
{
gameObject.SetActive(false);
}
void UpdateIconPositionAndRotation()
{
// プレイヤーアイコンの位置
playerImage.rectTransform.localPosition = CalculateIconImagePosition(player.position);
// プレイヤーアイコンの回転
playerImage.rectTransform.localRotation = CalculateIconImageRotation(player.rotation);
// 他のターゲットのアイコンをすべて削除
DeleteAllTargetIcons();
// ターゲットのアイコンを作成
if (targets != null)
{
foreach(var t in targets)
{
// インスタンス化
var icon = Instantiate(targetIconPrefab, mapImage.transform);
// アイコンの位置
icon.rectTransform.localPosition = CalculateIconImagePosition(t.position);
}
}
}
void DeleteAllTargetIcons()
{
for(int i = 0; i < mapImage.transform.childCount; i++)
{
var icon = mapImage.transform.GetChild(i);
if (mapImage.transform.GetChild(i) == playerImage.transform) continue;
Destroy(icon.gameObject);
}
}
Vector3 CalculateIconImagePosition(Vector3 targetWorldPosition)
{
// マップの幅
var mapWidth = mapTopRightPosition.x - mapBottomLeftPosition.x;
// マップの奥行
var mapDepth = mapTopRightPosition.z - mapBottomLeftPosition.z;
// 左下を基準にした位置
var targetRelativePosition = targetWorldPosition - mapBottomLeftPosition;
// アイコンの位置
var iconImagePosition = Vector3.zero;
iconImagePosition.x = targetRelativePosition.x * mapImage.rectTransform.sizeDelta.x / mapWidth;
iconImagePosition.y = targetRelativePosition.z * mapImage.rectTransform.sizeDelta.y / mapDepth;
return iconImagePosition;
}
Quaternion CalculateIconImageRotation(Quaternion targetWorldRotation)
{
var euler = Quaternion.Inverse(targetWorldRotation).eulerAngles;
return Quaternion.Euler(new Vector3(0f, 0f, euler.y));
}
public void AddTarget(Transform target)
{
if (targets == null) targets = new List<Transform>();
targets.Add(target);
}
}
UIや進行を管理するクラス等からpublicメソッドを呼んでいます。マップを開いたときに、アイコンの位置と回転を計算します。
public void Open()
{
gameObject.SetActive(true);
UpdateIconPositionAndRotation();
}
public void Close()
{
gameObject.SetActive(false);
}
プレイヤーアイコンの位置と回転を設定したら、それ以外のターゲットのアイコンをすべて削除してから、新たにインスタンス化して位置を設定しています。アイコンはすべてマップのImageオブジェクトの子にします。
void UpdateIconPositionAndRotation()
{
// プレイヤーアイコンの位置
playerImage.rectTransform.localPosition = CalculateIconImagePosition(player.position);
// プレイヤーアイコンの回転
playerImage.rectTransform.localRotation = CalculateIconImageRotation(player.rotation);
// 他のターゲットのアイコンをすべて削除
DeleteAllTargetIcons();
// ターゲットのアイコンを作成
if (targets != null)
{
foreach(var t in targets)
{
// インスタンス化
var icon = Instantiate(targetIconPrefab, mapImage.transform);
// アイコンの位置
icon.rectTransform.localPosition = CalculateIconImagePosition(t.position);
}
}
}
アイコンを削除するときは、マップのすべての子を見て行って、プレイヤーアイコンでなければゲームオブジェクトを破壊します。
void DeleteAllTargetIcons()
{
for(int i = 0; i < mapImage.transform.childCount; i++)
{
var icon = mapImage.transform.GetChild(i);
if (mapImage.transform.GetChild(i) == playerImage.transform) continue;
Destroy(icon.gameObject);
}
}