277 lines
14 KiB
C#
277 lines
14 KiB
C#
using UnityEngine;
|
|
|
|
// КАМЕРА 3D-ПОСЕЛЕНИЯ (2.5D): наклонный вид под фиксированным углом, без вращения.
|
|
// Перемещение -- перетаскиванием (палец / мышь), приближение -- двумя пальцами или колесом.
|
|
// Не уходит за пределы полезной карты. FocusOn -- плавно показать область (открытие новой зоны).
|
|
public class City3DCamera : MonoBehaviour
|
|
{
|
|
public float pitch = 52f; // наклон, градусы от горизонта
|
|
public float yaw = 45f; // поворот вида вокруг вертикали
|
|
public float distance = 26f;
|
|
public float minDistance = 8f;
|
|
public float maxDistance = 55f;
|
|
|
|
public Vector3 target; // точка на земле, на которую смотрит камера
|
|
public Rect bounds = new Rect(0, 0, 48, 48); // где может быть target (x, z)
|
|
|
|
// Открытая часть поселения (зоны по уровню Ратуши), 06.11: центр кадра из неё не уходит, а при самом дальнем
|
|
// отдалении она видна целиком -- даже если "Дальше всего" (maxDistance) подобрано меньше. Ширина 0 -- не задана
|
|
Rect openArea;
|
|
float fitOpenDistance; // с какого расстояния открытая часть видна целиком (между HUD сверху и снизу)
|
|
public const float HudTopShare = 0.16f, HudBottomShare = 0.16f; // какую долю экрана закрывает HUD (ТЕСТ)
|
|
|
|
// Дальше всего сейчас: подобранное число, но не ближе, чем нужно, чтобы увидеть всё открытое поселение
|
|
public float MaxDistanceNow => Mathf.Max(maxDistance, fitOpenDistance);
|
|
|
|
public void SetOpenArea(Rect area)
|
|
{
|
|
openArea = area;
|
|
fitOpenDistance = FitDistance(area);
|
|
Clamp();
|
|
}
|
|
|
|
// Плавное приближение кнопками "+ / −" и колесом: камера доезжает до нужного расстояния за доли секунды
|
|
float zoomTo = -1f;
|
|
|
|
Camera cam;
|
|
Vector3 focusTarget;
|
|
float focusDistance;
|
|
bool focusing;
|
|
Vector3 lastPointer;
|
|
bool panning;
|
|
|
|
// Перемещение камеры пальцем можно выключить на время перетаскивания здания
|
|
public bool AllowPan { get; set; } = true;
|
|
|
|
// Камера перспективная (решение 05.11): поселение объёмнее, есть глубина. Угол, высота и поле зрения -- ТЕСТ,
|
|
// подбираются в Unity и на телефоне. Ортографическую можно вернуть галочкой (тогда зум -- размер кадра, см. Apply)
|
|
public bool perspective = true;
|
|
public float fieldOfView = 40f;
|
|
|
|
// Подбор камеры в панели DEV (окно «Камера»): наклон, поле зрения, ближе / дальше всего. Запоминается на этом
|
|
// устройстве -- только в редакторе и отладочной сборке; в обычной сборке -- числа из сцены (05.11)
|
|
public const string PrefPitch = "dev_cam3d_pitch", PrefFov = "dev_cam3d_fov", PrefMin = "dev_cam3d_min", PrefMax = "dev_cam3d_max";
|
|
|
|
// Числа по умолчанию (из сцены) -- для кнопки «Сбросить» в DEV
|
|
public float DefaultPitch { get; private set; }
|
|
public float DefaultFov { get; private set; }
|
|
public float DefaultMin { get; private set; }
|
|
public float DefaultMax { get; private set; }
|
|
|
|
public void SetFieldOfView(float fov)
|
|
{
|
|
fieldOfView = fov;
|
|
if (cam != null && !cam.orthographic) cam.fieldOfView = fov;
|
|
}
|
|
|
|
void LoadDevTuning()
|
|
{
|
|
DefaultPitch = pitch; DefaultFov = fieldOfView; DefaultMin = minDistance; DefaultMax = maxDistance;
|
|
#if UNITY_EDITOR || DEVELOPMENT_BUILD
|
|
pitch = PlayerPrefs.GetFloat(PrefPitch, pitch);
|
|
fieldOfView = PlayerPrefs.GetFloat(PrefFov, fieldOfView);
|
|
minDistance = PlayerPrefs.GetFloat(PrefMin, minDistance);
|
|
maxDistance = Mathf.Max(minDistance + 1f, PlayerPrefs.GetFloat(PrefMax, maxDistance));
|
|
distance = Mathf.Clamp(distance, minDistance, maxDistance);
|
|
#endif
|
|
}
|
|
|
|
void Awake()
|
|
{
|
|
cam = GetComponent<Camera>();
|
|
LoadDevTuning();
|
|
if (cam != null && perspective)
|
|
{
|
|
cam.orthographic = false;
|
|
cam.fieldOfView = fieldOfView;
|
|
}
|
|
}
|
|
|
|
public void FocusOn(Rect area)
|
|
{
|
|
focusTarget = new Vector3(area.center.x, 0, area.center.y);
|
|
// отойти так, чтобы область поместилась между HUD сверху и снизу
|
|
focusDistance = Mathf.Clamp(FitDistance(area), minDistance, MaxDistanceNow);
|
|
focusing = true;
|
|
zoomTo = -1f;
|
|
}
|
|
|
|
// Плавно подъехать к точке, не меняя приближения
|
|
public void MoveTo(Vector3 point)
|
|
{
|
|
focusTarget = new Vector3(point.x, 0, point.z);
|
|
focusDistance = distance;
|
|
focusing = true;
|
|
zoomTo = -1f;
|
|
}
|
|
|
|
// Расстояние, с которого прямоугольник земли (x, z) виден целиком, если смотреть на его центр. Считается по
|
|
// настоящей проекции камеры (наклон, поле зрения, форма экрана), поэтому верно при любых числах из DEV
|
|
public float FitDistance(Rect area)
|
|
{
|
|
if (cam == null) cam = GetComponent<Camera>();
|
|
if (cam == null || area.width <= 0 || area.height <= 0) return 0f;
|
|
Vector3 keepPos = transform.position;
|
|
Quaternion keepRot = transform.rotation;
|
|
float keepOrtho = cam.orthographicSize;
|
|
var center = new Vector3(area.center.x, 0, area.center.y);
|
|
Vector3[] corners =
|
|
{
|
|
new Vector3(area.xMin, 0, area.yMin), new Vector3(area.xMax, 0, area.yMin),
|
|
new Vector3(area.xMin, 0, area.yMax), new Vector3(area.xMax, 0, area.yMax),
|
|
};
|
|
float lo = 1f, hi = 400f;
|
|
for (int i = 0; i < 24; i++) // двоичный поиск: ближе -- не помещается, дальше -- помещается
|
|
{
|
|
float d = (lo + hi) * 0.5f;
|
|
PlaceAt(center, d);
|
|
bool fits = true;
|
|
foreach (var c in corners)
|
|
{
|
|
Vector3 v = cam.WorldToViewportPoint(c);
|
|
if (v.z <= 0 || v.x < 0.02f || v.x > 0.98f || v.y < HudBottomShare || v.y > 1f - HudTopShare) { fits = false; break; }
|
|
}
|
|
if (fits) hi = d; else lo = d;
|
|
}
|
|
transform.SetPositionAndRotation(keepPos, keepRot);
|
|
cam.orthographicSize = keepOrtho;
|
|
return hi;
|
|
}
|
|
|
|
// Начать / продолжить / закончить перемещение камеры. Зовёт контроллер ввода (он решает, что под пальцем)
|
|
public void BeginPan(Vector3 screen)
|
|
{
|
|
panning = AllowPan; lastPointer = screen; focusing = false; zoomTo = -1f;
|
|
panPlaneY = GroundHeightUnder(screen);
|
|
}
|
|
public void EndPan() { panning = false; }
|
|
|
|
// Палец ещё не сдвинулся настолько, чтобы считать это перетаскиванием: камера стоит, но точка запоминается --
|
|
// когда перетаскивание начнётся, карта не прыгнет (как на карте мира, 05.11)
|
|
public void Track(Vector3 screen) { lastPointer = screen; }
|
|
|
|
public void Pan(Vector3 screen)
|
|
{
|
|
if (!panning || cam == null) return;
|
|
Vector3 a = GroundPoint(lastPointer), b = GroundPoint(screen);
|
|
target += a - b;
|
|
lastPointer = screen;
|
|
Clamp();
|
|
Apply(); // сразу: иначе следующий шаг считался бы от старого положения камеры -- камера дёргалась (05.11)
|
|
}
|
|
|
|
// Кнопки "+ / −" и колесо: плавно (06.11). delta > 0 -- ближе
|
|
public void Zoom(float delta)
|
|
{
|
|
focusing = false;
|
|
float from = zoomTo > 0 ? zoomTo : distance;
|
|
zoomTo = Mathf.Clamp(from * (1f - delta), minDistance, MaxDistanceNow);
|
|
}
|
|
|
|
float fitPitch, fitFov, fitAspect; // при каких наклоне, поле зрения и форме экрана считали fitOpenDistance
|
|
|
|
void Update()
|
|
{
|
|
// Наклон или поле зрения поменяли в DEV, повернули экран -- пересчитать, откуда видно всё поселение
|
|
if (openArea.width > 0 && cam != null && (fitPitch != pitch || fitFov != cam.fieldOfView || fitAspect != cam.aspect))
|
|
{
|
|
fitPitch = pitch; fitFov = cam.fieldOfView; fitAspect = cam.aspect;
|
|
fitOpenDistance = FitDistance(openArea);
|
|
}
|
|
|
|
// Колесо мыши (редактор, ПК)
|
|
float wheel = Input.mouseScrollDelta.y;
|
|
if (City3DInput.OverHud(Input.mousePosition)) wheel = 0f; // колесо над окнами HUD -- не приближать камеру (05.11)
|
|
if (Mathf.Abs(wheel) > 0.01f) Zoom(wheel * 0.1f);
|
|
|
|
// Два пальца -- приближение, как на карте мира: расстояние камеры меняется во столько раз,
|
|
// во сколько изменилось расстояние между пальцами за кадр (не зависит от того, увидели ли касание) (05.11)
|
|
if (Input.touchCount >= 2)
|
|
{
|
|
Touch a = Input.GetTouch(0), b = Input.GetTouch(1);
|
|
float previous = ((a.position - a.deltaPosition) - (b.position - b.deltaPosition)).magnitude;
|
|
float current = (a.position - b.position).magnitude;
|
|
if (previous > 0.01f && current > 0.01f)
|
|
{
|
|
focusing = false;
|
|
zoomTo = -1f;
|
|
distance = Mathf.Clamp(distance * previous / current, minDistance, MaxDistanceNow);
|
|
}
|
|
}
|
|
|
|
if (zoomTo > 0)
|
|
{
|
|
distance = Mathf.Lerp(distance, zoomTo, 1f - Mathf.Exp(-Time.unscaledDeltaTime * 12f));
|
|
if (Mathf.Abs(distance - zoomTo) < 0.02f) { distance = zoomTo; zoomTo = -1f; }
|
|
}
|
|
distance = Mathf.Clamp(distance, minDistance, MaxDistanceNow);
|
|
|
|
if (focusing)
|
|
{
|
|
target = Vector3.Lerp(target, focusTarget, Time.unscaledDeltaTime * 2.5f);
|
|
distance = Mathf.Lerp(distance, focusDistance, Time.unscaledDeltaTime * 2.5f);
|
|
if (Vector3.Distance(target, focusTarget) < 0.05f && Mathf.Abs(distance - focusDistance) < 0.05f) focusing = false;
|
|
}
|
|
Apply();
|
|
}
|
|
|
|
void Clamp()
|
|
{
|
|
Rect r = bounds;
|
|
if (openArea.width > 0 && openArea.height > 0) // центр кадра -- только над открытой частью поселения
|
|
r = Rect.MinMaxRect(Mathf.Max(r.xMin, openArea.xMin), Mathf.Max(r.yMin, openArea.yMin),
|
|
Mathf.Min(r.xMax, openArea.xMax), Mathf.Min(r.yMax, openArea.yMax));
|
|
target.x = Mathf.Clamp(target.x, r.xMin, r.xMax);
|
|
target.z = Mathf.Clamp(target.z, r.yMin, r.yMax);
|
|
target.y = 0;
|
|
}
|
|
|
|
// Дымка начинается за городом, а не на фиксированном расстоянии от камеры: иначе при отдалении весь город
|
|
// уходил в серую дымку -- казалось, что камера не отдаляется, а сцена сереет (05.11)
|
|
public float fogStartBehind = 4f, fogEndBehind = 59f;
|
|
|
|
public void Apply()
|
|
{
|
|
if (cam == null) cam = GetComponent<Camera>();
|
|
PlaceAt(target, distance);
|
|
if (RenderSettings.fog && RenderSettings.fogMode == FogMode.Linear)
|
|
{
|
|
RenderSettings.fogStartDistance = distance + fogStartBehind;
|
|
RenderSettings.fogEndDistance = distance + fogEndBehind;
|
|
}
|
|
}
|
|
|
|
// Поставить камеру: смотрит на точку center с расстояния d.
|
|
// Камера сцены может быть ортографической (вид 2.5D без перспективы): у неё расстояние не меняет размер города
|
|
// на экране -- «+ / −» и щипок ничего не делали (05.11). Тогда приближение -- через размер кадра: столько же
|
|
// земли, сколько видно перспективной камерой с этого расстояния
|
|
void PlaceAt(Vector3 center, float d)
|
|
{
|
|
Quaternion rot = Quaternion.Euler(pitch, yaw, 0);
|
|
transform.rotation = rot;
|
|
transform.position = center - rot * Vector3.forward * d;
|
|
if (cam != null && cam.orthographic)
|
|
cam.orthographicSize = d * Mathf.Tan(cam.fieldOfView * 0.5f * Mathf.Deg2Rad);
|
|
}
|
|
|
|
// Точка под пальцем на горизонтальной плоскости на высоте земли, где палец коснулся (06.11): на холме карта
|
|
// идёт за пальцем точно, а не чуть быстрее или медленнее, как было с плоскостью y = 0
|
|
float panPlaneY;
|
|
|
|
Vector3 GroundPoint(Vector3 screen)
|
|
{
|
|
Ray ray = cam.ScreenPointToRay(screen);
|
|
var plane = new Plane(Vector3.up, new Vector3(0, panPlaneY, 0));
|
|
return plane.Raycast(ray, out float t) ? ray.GetPoint(t) : target;
|
|
}
|
|
|
|
// Высота земли (объект "Ground" 3D-поселения) под экранной точкой; не попали -- 0
|
|
float GroundHeightUnder(Vector3 screen)
|
|
{
|
|
if (cam == null) return 0f;
|
|
foreach (var hit in Physics.RaycastAll(cam.ScreenPointToRay(screen), 500f))
|
|
if (hit.collider.gameObject.name == "Ground") return hit.point.y;
|
|
return 0f;
|
|
}
|
|
}
|