July 1, 2023

Динамическая камера в 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));    
    
    }        
}

Статья написана для медиа ресурсов Субстанции