Создайте эффект обтекания экрана, похожий на астероиды, с помощью Unity
Дамир Веапи 22 марта 2014 г.
Время чтения: 10 мин.
Взгляните на демо ниже и давайте начнем!
В моем случае у меня есть простой космический корабль (конус) с движением, подобным астероидам. Как вы можете видеть на картинке, я предпочитаю, чтобы сетка была привязана к основному объекту. Делаете вы это так или нет, имеете ли вы один или несколько мешей, это не имеет значения; мы собираемся сделать хороший сценарий обтекания экрана, который работает в любом случае.
Простая упаковка
Основная идея обтекания экрана заключается в следующем:
- Проверять будь то объект ушел за пределы экрана.
- Выяснить куда он ушел за кадром. Он вышел за левый край или за правый? Сверху или снизу?
- Телепортируйте объект прямо за противоположный края экрана. Например, если он выходит за левый край, мы телепортируем его за правый край. Телепортируем объект позади противоположный край, так что он на самом деле выглядит так, как будто он обертывается, а не телепортируется.
Делаем это в Unity
Итак, первое, что мы хотим сделать, это проверить, не ушел ли объект полностью за пределы экрана. Один простой способ сделать это в Unity — проверить, видны ли средства визуализации объекта. Если это не так, это означает, что объект полностью находится за пределами камеры и, следовательно, за пределами экрана.
Давайте вызовем рендереры в Start() и создадим служебную функцию для их проверки:
Renderer[] визуализаторы; void Start() < renderers = GetComponentsInChildren(); >bool CheckRenderers() < foreach(var renderer in renderers) < // Если виден хотя бы один рендер, вернуть true if(renderer.isVisible) < return true; >> // В противном случае объект невидим return false; >
Теперь мы можем сказать, ушел ли наш объект за пределы экрана, но нам все еще нужно это выяснить. куда он ушел, а затем телепортировать его на противоположную сторону. Для этого мы можем посмотреть на оси отдельно. Например, если x-позиция нашего корабля выходит за пределы экрана, это означает, что он ушел либо влево, либо вправо.
Самый простой способ проверить это — сначала преобразовать положение корабля в мире в положение окна просмотра, а затем выполнить проверку. Таким образом, это будет работать независимо от того, используете ли вы орфографическую или перспективную камеру.
var cam = Camera.main; var viewportPosition = cam.WorldToViewportPoint(transform.position);
Чтобы было понятнее, позвольте мне объяснить координаты области просмотра. Пространство области просмотра относительно камеры. Координаты варьируются от 0 до 1 для всего, что находится на экране, что означает:
- x = 0 — координата левого края экрана.
- x = 1 — координата правого края экрана.
- y = 0 — координата нижнего края экрана.
- y = 1 — координата верхнего края экрана.
Это означает, что если объект находится за пределами экрана, он будет иметь либо отрицательную координату (меньше 0), либо координату больше 1.
Поскольку положение нашей камеры находится в точках x = 0, y = 0, сцена выглядит как зеркало. Все, что находится справа, имеет положительные x-координаты; все налево, минус. Все в верхней половине имеет положительные координаты y; все в нижней половине, отрицательное. Итак, чтобы расположить наш объект на противоположной стороне экрана, мы просто инвертируем его положение по соответствующей оси. Например:
- Если наш корабль движется вправо и его позиция равна (20, 0), он становится (-20, 0).
- Если наш корабль движется над нижним краем и его положение равно (0, -15), оно становится (0, 15).
Обратите внимание, что мы трансформируем трансформировать положение, а не его окно просмотра должность.
В коде это выглядит так:
var newPosition = transform.position; если (viewportPosition.x > 1 || viewportPosition.x < 0) < newPosition.y = -newPosition.y; >if (viewportPosition.y > 1 || viewportPosition.y < 0) < newPosition.y = -newPosition.y; >transform.position = новое положение;
Если вы запустите проект сейчас, большую часть времени он будет работать нормально. Но иногда объект может не обернуться. Это происходит потому, что наш объект постоянно меняет позиции вне экрана, а не только один раз. Мы можем предотвратить это, добавив пару управляющих переменных:
bool isWrappingX = ложь; bool isWrappingY = ложь;
Теперь все должно работать идеально, и окончательный код обтекания экрана должен выглядеть так:
void ScreenWrap() < var isVisible = CheckRenderers(); if(isVisible) < isWrappingX = false; isWrappingY = ложь; возвращаться; >if(isWrappingX && isWrappingY) < return; >var cam = Camera.main; var viewportPosition = cam.WorldToViewportPoint(transform.position); var newPosition = transform.position; if (!isWrappingX && (viewportPosition.x > 1 || viewportPosition.x < 0)) < newPosition.x = -newPosition.x; isWrappingX = истина; >if (!isWrappingY && (viewportPosition.y > 1 || viewportPosition.y < 0)) < newPosition.y = -newPosition.y; isWrappingY = истина; >transform.position = новое положение; >
Расширенная упаковка
Простая упаковка работает хорошо, но могла бы выглядеть и лучше. Вместо того, чтобы объект уходил за пределы экрана перед обтеканием, у вас может быть идеальное обтекание, как на картинке ниже:
Самый простой способ сделать это — немного схитрить и иметь на сцене несколько кораблей. Таким образом, мы создадим иллюзию одиночного корабля, вращающегося вокруг. Нам понадобится восемь дополнительных кораблей (я назову их призраки): по одному для каждого края и по одному для каждого угла экрана.
Мы хотим, чтобы эти корабли-призраки были видны только тогда, когда игрок подходит к краю. Для этого нам нужно расположить их на определенных расстояниях от основного корабля:
- Два корабля расположились на расстоянии ширины экрана слева и справа соответственно.
- Два корабля располагались на высоте экрана выше и ниже соответственно.
- Четыре угловых корабля располагались на ширину экрана по горизонтали и на высоту экрана по вертикали.
Делаем это в Unity
Сначала нам нужно получить размер экрана, чтобы мы могли расположить наши корабли-призраки. Дело в том, что нам нужен размер экрана в мировых координатах относительно корабля игрока. На самом деле это не имеет значения, если мы используем орфографическую камеру, но с видом в перспективе очень важно, чтобы корабли-призраки находились в той же z-координате, что и главный корабль.
Итак, чтобы сделать это универсальным способом, мы собираемся преобразовать координаты области просмотра в верхнем правом и нижнем левом углах экрана в мировые координаты, лежащие на той же оси Z, что и главный корабль. Затем мы используем эти координаты для расчета ширины и высоты экрана в мировых единицах относительно положения нашего корабля.
Объявите screenWidth и screenHeight как переменные класса и добавьте их в Start() :
var cam = Camera.main; var screenBottomLeft = cam.ViewportToWorldPoint (новый Vector3 (0, 0, transform.position.z)); var screenTopRight = cam.ViewportToWorldPoint (новый Vector3 (1, 1, transform.position.z)); screenWidth = screenTopRight.x - screenBottomLeft.x; screenHeight = screenTopRight.y - screenBottomLeft.y;
Теперь, когда мы можем расположить их правильно, давайте создадим корабли-призраки. Мы будем использовать массив для их хранения:
Transform[] ghosts = новый Transform[8];
И давайте создадим функцию, которая будет заниматься нерестом. Я собираюсь клонировать главный корабль, чтобы создать призраков, а затем удалю с них свойство ScreenWrapBehaviour. Только главный корабль должен иметь ScreenWrapBehaviour, потому что он может иметь полный контроль над призраками, и мы не хотим, чтобы призраки порождали своих собственных призраков.Вы также можете создать отдельный префаб для кораблей-призраков и создать его экземпляр; это то, что вам нужно, если вы хотите, чтобы у призраков было какое-то особенное поведение.
void CreateGhostShips() < for(int i = 0; i < 8; i++) < ghosts[i] = Instantiate(transform, Vector3.zero, Quaternion.identity) as Transform; DestroyImmediate(призраки[i].GetComponent()); >>
Затем мы размещаем призраков, как на изображении выше:
void PositionGhostShips() < // Все положения призраков будут относительно кораблей (этого) преобразования, // так что давайте начнем с этого. var ghostPosition = transform.position; // Мы располагаем призраков по часовой стрелке за краями экрана. // Начнем с крайнего правого.ghostPosition.x = transform.position.x + ширина экрана; ghostPosition.y = transform.position.y; призраки[0].position = ghostPosition; // Внизу справа ghostPosition.x = transform.position.x + screenWidth; ghostPosition.y = transform.position.y - высота экрана; призраки[1].position = ghostPosition; // Нижний ghostPosition.x = transform.position.x; ghostPosition.y = transform.position.y - высота экрана; призраки[2].position = ghostPosition; // Нижний левый ghostPosition.x = transform.position.x - screenWidth; ghostPosition.y = transform.position.y - высота экрана; призраки[3].position = ghostPosition; // Left ghostPosition.x = transform.position.x - screenWidth; ghostPosition.y = transform.position.y; призраки[4].position = ghostPosition; // Верхний левый ghostPosition.x = transform.position.x - screenWidth; ghostPosition.y = transform.position.y + screenHeight; призраки[5].position = ghostPosition; // Наверху ghostPosition.x = transform.position.x; ghostPosition.y = transform.position.y + screenHeight; призраки[6].position = ghostPosition; // Вверху справа ghostPosition.x = transform.position.x + screenWidth; ghostPosition.y = transform.position.y + screenHeight; призраки[7].position = ghostPosition; // Все корабли-призраки должны иметь такое же вращение, как и основной корабль for(int i = 0; i < 8; i++) < ghosts[i].rotation = transform.rotation; >>
Запустите свой проект и попробуйте его. Если вы посмотрите на вид сцены, вы увидите, что все корабли-призраки движутся вместе с основным кораблем и поворачиваются, когда он поворачивается. Мы не кодировали это явно, но это все еще работает. У вас есть идея, почему?
Корабли-призраки — это клоны основного корабля без свойства ScreenWrappingBehaviour. У них по-прежнему должно быть отдельное поведение движения, и, поскольку все они получают один и тот же ввод, все они двигаются одинаково. Если вы хотите создавать призраков из префаба, не забудьте включить компонент движения или какой-нибудь другой скрипт, который будет синхронизировать их движение с основным кораблем.
Кажется, теперь все работает нормально, верно? Ну, почти.Если вы продолжите двигаться в одном направлении, в первый раз, когда он завернется, он будет работать нормально, но как только вы снова достигнете края, с другой стороны корабля не будет. Это имеет смысл, так как на этот раз мы не занимаемся телепортацией. Давайте исправим это.
Как только главный корабль выйдет за край, на экране появится корабль-призрак. Нам нужно поменять их местами, а затем переставить корабли-призраки вокруг главного корабля. Мы уже храним массив призраков, нам просто нужно определить, какой из них находится на экране. Затем мы делаем замену и перестановку. В коде:
void SwapShips() < foreach(var ghost in ghosts) < if (ghost.position.x < screenWidth && ghost.position.x >-screenWidth && ghost.position.y < screenHeight && ghost.position.y >-screenHeight) < преобразование.позиция = призрак.позиция; ломать; >> ПозицияGhostShips(); >
Попробуйте прямо сейчас, и все должно работать идеально.
Последние мысли
Теперь у вас есть работающий компонент обтекания экрана. Достаточно ли этого для вас, зависит от игры, которую вы делаете, и от того, чего вы пытаетесь достичь.
Простую упаковку довольно просто использовать: просто прикрепите ее к объекту, и вам не придется беспокоиться о ее поведении. С другой стороны, вы должны быть немного осторожны, если используете расширенную упаковку. Представьте ситуацию, когда пуля или астероид попадает в корабль-призрак: вам нужно будет передать события столкновения главному кораблю или объекту внешнего контроллера.
Вы также можете захотеть, чтобы ваши игровые объекты сворачивались только вдоль одной оси. Мы уже делаем отдельные проверки для каждой оси, так что нужно просто добавить в код пару логических значений.
Еще один интересный момент: что, если вы хотите, чтобы камера немного двигалась, а не фиксировалась в пространстве? Возможно, вы хотите, чтобы арена была больше экрана. В этом случае вы все равно можете использовать тот же сценарий упаковки. Вам нужно только отдельное поведение, которое контролирует границы движения камеры.Поскольку наш код основан на позиции области просмотра, положение камеры в игре не имеет большого значения.
Возможно, у вас уже есть собственные идеи. Так что давай, попробуй их и сделай несколько игр!
использованная литература
- Изображение предоставлено: Rocket Уильяма Дж. Сальвадора из проекта Noun.