Бесконечный 2D фон в Unity

Tulenber 3 April, 2020 ⸱ Intermediate ⸱ 7 min ⸱ 2019.3.7f1 ⸱

В этом посте мы рассмотрим как можно применить подход Двигателя на Тёмной Материи для создания бесконечного 2D фона.

Если вы сталкивались с мобильными играми в общественном транспорте, то наверняка могли заметить, что одними из самых популярных в поездке являются игры жанра Endless runner, например, Subway Surfers, а для любителей археологии примером может послужить Электроника ИМ23 “Автослалом”. Основным признаком данного жанра является бесконечно перемещающийся элемента(персонаж или другой объект), который обычно самостоятельно движется вперёд навстречу препятствиям, а управление даётся только для возможности эти препятствия избегать. Бесконечное перемещение подразумевает бесконечное пространство доступное для движения игрока, создание которых является не самой простой задачей, как могло бы показаться на первый взгляд. Этим постом мы открываем цикл, в котором рассмотрим элементы, которые помогут создать такие бесконечные, в каком-то смысле, миры.

Если поискать в интернете информацию по бесконечным пространствам для Unity, то одним из самых популярных туториалов будет создание бесконечных фоновых объектов для сайдскроллеров. В нашем случае мы немного усложним задачу и попробуем сделать бесконечный фон для топ-даун камеры. В качестве примера будет реализован бесконечный фон для космического пространства, в котором будет перемещаться модель космического корабля.

Проблематика

Первым что приходит в голову, когда вас просят сделать корабль, который перемещается в пространстве, это добавить модель корабля, задать ему вектор скорости, привязать к нему камеру, подложить фон и начать сдвигать корабль вдоль вектора, помноженного на Time.deltaTime. Если откинуть очевидные вещи с ограниченными размерами фона, которые в любом случае придётся решать, главной проблемой бесконечного перемещения окажется координатное пространство Unity, которое привязано к переменным типа float и с увеличением координат точность будет падать. Для более точного описания проблемы вы можете посмотреть вот это видео "64 Bit In Kerbal Space Program".

Dark Matter Engine

Для решения данного вопроса можно использовать подход, с помощью которого работает Двигатель на Тёмной Материи из “Футурамы”. Если коротко описать принцип его действия, то двигатель двигает не корабль, а пространство вокруг корабля. То есть, в нашем случае вместо того чтобы двигать вдоль вектора скорости наш корабль мы будем двигать все объекты вокруг него в противоположном направлении. Таким образом, если наша камера привязана к кораблю, то мы всегда будем оставаться в точке начала координат и наша точность не будет падать из-за увеличения расстояния. Да и в целом для решения вопроса с бесконечным перемещением, выбрать началом координат наше положение выглядит более красивым решением, нежели привязываться к какой-либо другой точке в пространстве.

Задача

Сделать перемещение космического корабля вдоль бесконечного 2D фона. Корабль можно поворачивать вокруг оси Z при помощи клика по правой/левой стороне экрана. Камера всегда будет сонаправлена с вектором движения корабля.

Решение

Я не буду распространять ассеты, которые используются для примера. Найти подходящие вам ресурсы это тоже большая работа и я предлагаю вам в этом потренироваться, если не знаете с чего начать, то можно посетить бесплатный itch.io или поискать в интернете "Free game assets".

Нам понадобится текстура для корабля и текстура для фона. Основное требование к фону будет его зацикленность как в вертикальном, так и в горизонтальном направлении.

  1. Добавьте в проект текстуры корабля и фона, для фона необходимо выставить Mesh type - Full rect и Wrap mode - Repeat
    Add sprites
  2. Создайте структуру объектов такого содержания
    Object structure
  3. Добавьте текстуру для корабля
    Spaceship
  4. Добавьте текстуру для фона, выставите Draw mode - Tiled и Order in Layer = -1
    Add scenes
  5. Создайте скрипт UniverseHandler, добавьте его к объекту Universe
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
using System;
using UnityEngine;

public class UnevirseHandler : MonoBehaviour
{
    // Ссылки на объекты сцены
    [SerializeField] private Camera mainCamera = null;
    [SerializeField] private GameObject ship = null;
    [SerializeField] private GameObject space = null;
    
    // Радиус возможного обзора камеры
    private float _spaceCircleRadius = 0;

    // Исходные размеры объекта фона
    private float _backgroundOriginalSizeX = 0;
    private float _backgroundOriginalSizeY = 0;

    // Направление движения
    private Vector3 _moveVector;
    // Скорость поворота в радианах
    private float _rotationSpeed = 1f;

    // Вспомогательные переменные
    private bool _mousePressed = false;
    private float _halfScreenWidth = 0;
    
    void Start()
    {
        // Стартовое направление движения
        _moveVector = new Vector3(0, 1.5f, 0);
        // Используется для определения направления поворота
        _halfScreenWidth = Screen.width / 2f;
        
        // Исходные размеры фона
        SpriteRenderer sr = space.GetComponent<SpriteRenderer>();
        var originalSize = sr.size;
        _backgroundOriginalSizeX = originalSize.x;
        _backgroundOriginalSizeY = originalSize.y;

        // Высота камеры равна ортографическому размеру
        float orthographicSize = mainCamera.orthographicSize;
        // Ширина камеры равна ортографическому размеру, помноженному на соотношение сторон
        float screenAspect = (float)Screen.width / (float)Screen.height;
        // Радиус окружности, описывающей камеру
        _spaceCircleRadius = Mathf.Sqrt(orthographicSize * screenAspect * orthographicSize * screenAspect + orthographicSize * orthographicSize);

        // Конечный размер фона должен позволять сдвинуться на один базовый размер фона в любом направлении + перекрыть радиус камеры также во всех направлениях
        sr.size = new Vector2(_spaceCircleRadius * 2 + _backgroundOriginalSizeX * 2, _spaceCircleRadius * 2 + _backgroundOriginalSizeY * 2);
    }

    void Update()
    {
        // Изменение направления движения по клику кнопки мыши
        if (Input.GetMouseButtonDown (0)) {
            _mousePressed = true;
        }

        if (Input.GetMouseButtonUp(0))
        {
            _mousePressed = false;
        }
        
        if (_mousePressed)
        {
            // Направление поворота определяется в зависимости от стороны экрана, по которой произошёл клик
            int rotation = Input.mousePosition.x >= _halfScreenWidth ? -1 : 1;

            // Расчёт поворота вектора направления
            float xComp = (float)(_moveVector.x * Math.Cos(_rotationSpeed * rotation * Time.deltaTime) - _moveVector.y * Math.Sin(_rotationSpeed * rotation * Time.deltaTime));
            float yComp = (float) (_moveVector.x * Math.Sin(_rotationSpeed * rotation * Time.deltaTime) + _moveVector.y * Math.Cos(_rotationSpeed * rotation * Time.deltaTime));
            _moveVector = new Vector3(xComp, yComp,0);

            // Поворот спрайта корабля и камеры вдоль вектора направления
            float rotZ = Mathf.Atan2(_moveVector.y, _moveVector.x) * Mathf.Rad2Deg;
            ship.transform.rotation = Quaternion.Euler(0f, 0f, rotZ - 90);
            mainCamera.transform.rotation = Quaternion.Euler(0f, 0f, rotZ - 90);
        }

        // Сдвигаем фон в противоположном движению направлении
        space.transform.Translate(-_moveVector.x * Time.deltaTime, -_moveVector.y * Time.deltaTime, 0);

        // При достижении фоном сдвига равного исходному размеру фона в каком-либо направлении, возвращаем его в исходную точно по этому направлению
        if (space.transform.position.x >= _backgroundOriginalSizeX)
        {
            space.transform.Translate(-_backgroundOriginalSizeX, 0, 0);
        }
        if (space.transform.position.x <= -_backgroundOriginalSizeX)
        {
            space.transform.Translate(_backgroundOriginalSizeX, 0, 0);
        }
        if (space.transform.position.y >= _backgroundOriginalSizeY)
        {
            space.transform.Translate(0, -_backgroundOriginalSizeY, 0);
        }
        if (space.transform.position.y <= -_backgroundOriginalSizeY)
        {
            space.transform.Translate(0, _backgroundOriginalSizeY, 0);
        }
    }
    
    private void OnDrawGizmos()
    {
        // Окружность, описывающая камеру
        UnityEditor.Handles.color = Color.yellow;
        UnityEditor.Handles.DrawWireDisc(Vector3.zero , Vector3.back, _spaceCircleRadius);

        // Направление движения
        UnityEditor.Handles.color = Color.green;
        UnityEditor.Handles.DrawLine(Vector3.zero, _moveVector);
    }
}
  1. Выставите в редакторе ссылки на Main camera, Ship и Space
    Univerce handler
  2. Насладитесь результатом
    Result

Результат

Как можно увидеть в результирующей гифке объект фона смещается в начальное положение по направлению, в котором достигается сдвиг на изначальный размер фона, за счёт чего достигается визуальная бесшовность его перемещения.

Альтернативные решения

Программирование - это такая область, в которой в целом нет абсолютно правильных решений, перед программистом при решении практически любой задачи открывается довольно большой выбор методов, при помощи которых будут выполняться поставленные условия. В данном случае также можно поступить несколькими способами. Например, можно вместо спрайта сделать объект типа quad и настроив материал, который также использует тайловую структуру управлять её смещением, что избавит нас от перемещения объекта совсем. Если же вернуться к более классическому варианту, то можно скомбинировать обычное перемещение объекта и сдвиг центра координат для всей сцены сразу, по достижению каких-либо пределов.

Заключение

Когда мы задумываемся о создании бесконечных пространств подход Двигателя на Тёмной Материи является основным средством обхода ограничений точности для Unity. Совмещённый с обычными передвижениями сдвиг центра координат тоже можно отнести в эту же группу. Приведённый же пример прекрасно подходит для создания фонов и эффекта параллакса. Однако, работа с более сложными объектами требует иных подходов и для создания реальных неограниченных пространств необходимо ввести множество других механик. Эти вопросы мы рассмотрим в следующих статьях цикла про бесконечные миры. Пока! =)



Privacy policyCookie policyTerms of service
Tulenber 2020