Динамическая камера в 2D. Туториал.
В этой статье хочу рассказать о моей собственной реализации динамической камеры в моем понимании.
Задача: динамическая камера должна держаться середины между данными точками и изменять размеры области видимости для того, чтобы данные точки всегда были на экране. Движения камеры должны быть плавными.
Начнем с поиска середины. Середину удобнее всего получать с помощью среднего арифметического. Код:
Vector2 Center(List<Transform> transforms) { // Поиск средней точки через среднее арифметическое float summX = 0; float summY = 0; foreach (var i in transforms) { summX += i.position.x; summY += i.position.y; } return new Vector2(summX / transforms.Count, summY / transforms.Count); }
Ищем сумму абсцисс и ординат, делим на количество элементов в списке и возвращаем точку середины
Изменение области видимости. Поведение камеры писалось для игры CircleGuys. Там я использовал камеру с перспективной проекцией. По этой причине для увеличения масштаба камеры я менял значение FOV(field of view - англ. поле зрения). Но в 2Д играх зачастую используют камеру с ортографической проекцией, так что будем взаимодействовать с параметром Size. Измениять его будем относительно дистанции между точками фокуса
Для поиска дистанции мы можем применить соответствующую формулу из курса геометрии за 9 класс:
Но что делать, если количество точек больше двух? Легко, нам нужно будет найти точки с самой большой и с самой малой абсциссой и тоже самое с ординатой.
float GetDistance(List<Transform> transforms, float xFactor = 1f, float yFactor = 1f) { // Сортировка точек от большего к меньшему List<Transform> trX = transforms.OrderBy(tr => tr.position.x).ToList(); List<Transform> trY = transforms.OrderBy(tr => tr.position.y).ToList(); // Модифицированная формула расстояния между точками return Mathf.Sqrt(Mathf.Pow(trX[0].position.x - trX[trX.Count - 1].position.x, 2) * xFactor + Mathf.Pow((trY[0].position.y - trY[trY.Count-1].position.y) * yFactor, 2)); }
Передается лист с точками и мы сортируем его абсциссы и ординаты по убыванию и подставляем значения в формулу. Формулу я модифицировал для того, чтобы можно было контролировать влияние абсциссы и ординаты на конечное число дистанции с помощью xFactor и yFactor (по умолчанию стоит 1 - без изменения)
Далее, получив расстояние, мы делим его на sizeRatio (Отношение. Подбирается вручную). Число, которое у нас получилось, является нужным нам значением области видимости. Далее в Update():
void Update() { CleanupFocus(); // Чистим список Vector2 centered = Center(focusPoints); // Ищем центр targetX = centered.x; targetY = centered.y; // Ищем расстоние меж крайними точками distance = GetDistance(focusPoints, sizeFactorX, sizeFactorY); // Получаем Size с помощью расстояния и отношения targetSize = distance / sizeRatio; // Применяем ограничения if (targetSize > maxSize & maxSize != 0) targetSize = maxSize; if (targetSize < minSize & minSize != 0) targetSize = minSize; // Плавно меняем будущие координаты камеры float x = Mathf.Lerp(transform.position.x, targetX, Time.deltaTime * movingSpeedX); float y = Mathf.Lerp(transform.position.y, targetY, Time.deltaTime * movingSpeedY); // Плавно меняем будущий размер _camera.orthographicSize = Mathf.Lerp(_camera.orthographicSize, targetSize, Time.deltaTime * movingSpeedSize); // Передаем координаты Vector3 xyz = new Vector3(x,y, transform.position.z); transform.position = xyz; } void CleanupFocus() { // Отчистка списка от недоступных точек focusPoints = focusPoints.Where(tr => tr != null).ToList(); }
Метод CleanupFocus() нужен для отчистки листа от трансформов, которых уже нету (x == null). Далее идет получение середины, после поиск расстояния, вычисление значения области видимости, применение ограничений для области видимости. Далее плавное изменение x, y и size через метод линейной интерполяции (Lerp который). И в конце передача значений в камеру.
using System.Collections.Generic; using System.Linq; using UnityEngine; public class cameraPos : MonoBehaviour { // Мои настройки могут не подойти, настройте сами [SerializeField] private float movingSpeedX = 10; // Плавность камеры по Х [SerializeField] private float movingSpeedY = 2.5f; // Плавность камеры по У [SerializeField] private float movingSpeedSize = 2.5f; // Плавность изменения области видимости камеры [SerializeField] private float sizeRatio = 1.15f; // Отношение дистанции к области видимости (Distance/Size) [SerializeField] private float sizeFactorX = 1f; // Множитель влияния Х на дистанцию [SerializeField] private float sizeFactorY = 2f; // Множитель влияния У на дистанцию // Ограничения по размеру видимой области [Header("Limits")] [SerializeField] private float minSize = 10f; [SerializeField] private float maxSize = 20f; // Список точек фокуса [Header("Focus points")] [SerializeField] private List<Transform> focusPoints; private float distance; private float targetSize; private Camera _camera; private float targetX; private float targetY; private void Awake() { _camera = GetComponent<Camera>(); // Получение обьекта камеры } public void AddFocusPoint(Transform tr) // Метод для добавления точек фокуса из кода { focusPoints.Add(tr); } public void RemoveFocusPoint(Transform tr) // Метод для удаления точек фокуса из кода { focusPoints.RemoveAll(element => element == tr); } void Update() { CleanupFocus(); // Чистим список Vector2 centered = Center(focusPoints); // Ищем центр targetX = centered.x; targetY = centered.y; // Ищем расстоние меж крайними точками distance = GetDistance(focusPoints, sizeFactorX, sizeFactorY); // Получаем Size с помощью расстояния и отношения targetSize = distance / sizeRatio; // Применяем ограничения if (targetSize > maxSize & maxSize != 0) targetSize = maxSize; if (targetSize < minSize & minSize != 0) targetSize = minSize; // Плавно меняем будущие координаты камеры float x = Mathf.Lerp(transform.position.x, targetX, Time.deltaTime * movingSpeedX); float y = Mathf.Lerp(transform.position.y, targetY, Time.deltaTime * movingSpeedY); // Плавно меняем будущий размер _camera.orthographicSize = Mathf.Lerp(_camera.orthographicSize, targetSize, Time.deltaTime * movingSpeedSize); // Передаем координаты Vector3 xyz = new Vector3(x,y, transform.position.z); transform.position = xyz; } void CleanupFocus() { // Отчистка списка от недоступных точек focusPoints = focusPoints.Where(tr => tr != null).ToList(); } Vector2 Center(List<Transform> transforms) { // Поиск средней точки через среднее арифметическое float summX = 0; float summY = 0; foreach (var i in transforms) { summX += i.position.x; summY += i.position.y; } return new Vector2(summX / transforms.Count, summY / transforms.Count); } float GetDistance(List<Transform> transforms, float xFactor = 1f, float yFactor = 1f) { // Сортировка точек от большего к меньшему List<Transform> trX = transforms.OrderBy(tr => tr.position.x).ToList(); List<Transform> trY = transforms.OrderBy(tr => tr.position.y).ToList(); // Модифицированная формула расстояния между точками return Mathf.Sqrt(Mathf.Pow(trX[0].position.x - trX[trX.Count - 1].position.x, 2) * xFactor + Mathf.Pow((trY[0].position.y - trY[trY.Count-1].position.y) * yFactor, 2)); } }