Посмотрело: 734
Мой странный творческий путь занес меня в разработку игр. Благодаря отличной студенческой программе от IT-компании, название которой СостоИт из одной Греческой МАленькой буквы, сотрудничающей с нашим университетом, удалось собрать команду, родить документацию и наладить Agile разработку игры под присмотром высококлассного QA-инженера (здравствуйте, Анна!)
Без особо долгих размышлений, в качестве движка был выбран Unity. Это замечательный движок, на котором действительно быстро и легко можно сделать очень плохую игру, в которую, в здравом уме, никто и никогда не будет играть. Чтобы создать хорошую игру, все же придется перелопатить документацию, вникнуть в некоторые особенности и набраться опыта разработки.
Наша игра использовала физический движок неожиданным для него способом, что породило множество проблем с производительностью на мобильных платформах. В этой статье, на примере нашей игры, описана моя борьба с физическим движком и все те особенности его работы, которые были замечены на пути к жизнеспособной бета-версии.
Пару слов о том, как она сделана.
Сделана с помощью Blender и пары скриптов на питоне. На время съемки, в углу экрана находились 16 квадратиков, цвет которых кодировал 32 бита числа с плавающей запятой - вращение телефона в данный момент времени. R, G - данные, B - четность. 0 - 0, 255 - 1. Снятое на компьютере видео разбивалось на кадры с помощью ffmpeg, каждому кадру рендера в соответствие ставился расшифрованный угол. Такой формат позволил пережить любое сжатие в процессе съемки и поборол тот факт, что все программы имеют несколько разные представления о течении времени. В реальности игра играется так же как и на рендере.
В нашем случае, единственный допустимый вариант - третий, который и был реализован. О реализации - чуть позже.
Первый - игнорирование - абсолютно недопустим. Создание робота, который сможет вечно играть в нашу игру - интересная (и весьма простая) задача, которую кто-нибудь решит. Да и обычных корейских игроков недооценивать не стоит - самолетик быстрый, уровень генерируется непредсказуемо. И если до прохождений сквозь стены лететь и лететь, то куда более точная стрельба начнет очевидно подглючивать уже через 5 минут полета.
Второй - телепортация игрока и всего мира - ставит мобильные устройства на колени, в некоторых случаях - где-то на полсекунды. Это очень заметно, а потому - недопустимо. Но это вполне приемлемый вариант для простеньких бесконечных игр для ПК.
Конечно же, мы выбрали самый сложный вариант. В его сердце находится весьма сложная машина состояний, которая выполняет по ним случайные переходы. Но в рамках данной статьи интересен не механизм, а процесс генерации уровня и его организация, с учетом выбранной точки отсчета.
Что остается за бортом? Включениевыключение объекта и масштаб. Я не знаю, есть ли способ изменить размер объекта, не смущая движок. Вполне возможно, что нет. Выключение объекта проходит безболезненно, а включение… да, вызывает пересчет физики, в окрестностях включенного объекта. Поэтому старайтесь не включать одновременно слишком много объектов, растяните этот процесс во времени, чтобы пользователь не заметил.
Есть закон, не влияющий на производительность, но влияющий на работоспособность: твердое тело не может быть частью твердого тела. Родительский объект будет доминировать, поэтому ребенок будет или стоять на месте относительно родителя, или вести себя непредсказуемо и неправильно.
Есть еще одна особенность Unity, не относящаяся к физике, но достойная упоминания: динамическое создание и удаление объектов через методы Instantiate/Destroy - БЕЗУМНО медленный процесс. Я боюсь себе даже представить, что там происходит под капотом во время создания объекта. Если вам нужно создавать и удалять что-то динамически - используйте фабрики и заправляйте их нужными объектами во время загрузки игры. Instantiate должен вызываться в крайнем случае - если у фабрики вдруг закончились свободные объекты, а про Destroy забудьте навсегда - все созданное должно использоваться повторно.
Скрипт на оси получает данные с гироскопа и выставляет ей соответствующий угол… И нарушает сразу множество правил, потому что вращение передастся по иерархии на коллайдеры, что сведет физический движок с ума. Придется делать ось твердым телом и вращать ее через соответствующий метод. Но что с движением уровня? Очевидно, что ось вращения и объект уровня перемещаться не будут, каждый сегмент нужно двигать персонально, иначе мы сталкиваемся с проблемой бесконечности. Значит, твердыми телами должны быть сегменты. Но у нас уже есть твердое тело выше в иерархии и твердое тело не может быть частью твердого тела. Логичная и элегантная иерархия не подходит, все придется делать руками - и вращение, и перемещение, без использования объекта для оси вращения. Будьте готовы к такому, если у вас уникальные геймплейные фичи.
Если двигать непосредственно сегменты пришлось бы и так, то вращать их придется вынужденно. Основная сложность в том, что в физическом движке Unity нет метода «вращать объект вокруг произвольной точки» (он есть у Transform, но не искушайтесь). Есть только «вращать вокруг своего центра». Это логично, потому что вращение вокруг произвольной оси - одновременно и вращение, и движение, а это две разные операции. Но его можно имитировать. Сначала вращаем сегмент вокруг своей оси, потом вращаем координаты «своей оси» вокруг самолета. Благодаря тому, что самолет у нас в начале координат, не придется вспоминать даже школьную геометрию и лезть в википедию, в Unity уже все есть. Достаточно перевести угол поворота в кватернион и умножить его на координаты точки. Кстати, узнал я об этом прямо во время написания статьи, до этого использовалась матрица поворота.
У нас есть враги, которые отталкивают самолет в стену, надеясь убить. Есть щит, который отталкивает самолет от стен, помогая выжить. Реализовано это тривиально - есть вектор смещения, который каждый кадр прибавляется к координатам каждого сегмента и сбрасывается после этого. Любой желающий пнуть самолетик, через специальный метод, может оставить вектор своего пинка, который прибавится к этому вектору смещения.
В конечном итоге, настоящие координаты сегмента, каждый кадр, вычисляются центром управления движением уровня как-то так:
Vector3 position = segment.CachedRigidbody.position;
Vector3 deltaPos = Time.deltaTime * Vector3.left * settings.Speed;
segment.truePosition = Quaternion.Euler(0, 0, deltaAngle) * (position + deltaPos + movementOffset);
После всех вычислений и костылей, необходимых для работы точной стыковки сегментов при регенерации, segment.truePosition отправляется в метод MovePosition твердого тела сегмента.
Теперь, зная все главные подводные камни физического движка Unity, вы сможете быстро склонировать нашу игру, разрушив мечты, жизни и веру трех бедных студентов в человечество. Я надеюсь, эта статья сэкономит вам много времени в будущем и поможет найти не совсем очевидные нарушения законов производительной физики в своих проектах.
Читайте документацию и экспериментируйте, даже если пользуетесь простыми и интуитивно понятными инструментами.
Unity
- очень мощный, прогрессивный движок с большим потенциалом. Он обладает множеством уже встроенных функций (в том числе и физическим движком NvidiaPhysX
), которые нам, пользователям, прописывать вручную не придется. :)
В этой небольшой статье я бы хотел обсудить физические возможности движка. Итак, начнем:
За функцией Rigidbody скрывается Абсолютно Твердое Тело (АТТ ). Если объяснять грубо и понятно, то АТТ в физике и механике - это идеальное твердое тело, которое под воздействием силы не может менять свои свойства, но может (под ее воздействием) перемещаться в 3х измерениях (вниз, вверх, вперед и т.д., т.е. в наших X-Y-Z осях), а также вращаться в 3х измерениях (опять же по осям X-Y-Z).
В Unity , как и в других игровых движках (опять же называю их именно "игровыми" движками грубо), Rigidbody используется для различных объектов, с которыми мы можем взаимодействовать, толкая, пиная и т.п. Подобные объекты под нашим влиянием будут далее под воздействием гравитации кататься, передвигаться и сталкиваться с другими предметами.
К примеру, для создания автомобиля, кроме Rigidbody нам понадобятся 4 Wheel Collider "а и код (скрипт ) , применяющий силовое воздействия к колесам, в зависимости от нажатых клавиш.
Чтобы использовать АТТ
, нам нужен уже созданный игровой объект (GameObject
), кликнув на нем, мы проходим в меню по следующему пути: Components - Physics - Rigidbody
. Все, АТТ
добавлено! :)
Теперь объект подвержен гравитации, к нему можно применять силы с помощью скриптов, но для того, чтобы объект вел себя именно так, как вам нужно, следует добавить Collider
или Joint
.
В скрипте манипулировать нашим объектом теперь мы будем с помощью функций AddForce()
и AddTorque()
.
Так как я в Unity
применяю JavaScript
, мои примеры будут с ним, ссылки на другие примеры скриптинга (на C#
или Boo
) вы найдете ниже, в пункте Дополнительная информаия по АТТ
.
» Rigidbody.AddForce
// Rigidbody.AddForce использует 2 типа формул, как и многие другие функции, связанные с перемещениями в пространстве. // 1 тип: function AddForce (force: Vector3, mode: ForceMode = ForceMode.Force) : void // Сила, подбрасывающая объект вверх, относительно глобальной системы координат. function FixedUpdate () { rigidbody.AddForce (Vector3.up * 10); } // Используется Vector3 - встроенная функция Unity, которая, в принципе, аналогична стандартной системе координат. // 2 тип: function AddForce (x: float, y: float, z: float, mode: ForceMode = ForceMode.Force) : void // То же самое, но тут используется X-Y-Z система координат. function FixedUpdate () { rigidbody.AddForce (0, 10, 0); }
» Rigidbody.AddTorque
// Функция раскручивает объект вокруг заданной оси. // 1 тип: function AddTorque (torque: Vector3, mode: ForceMode = ForceMode.Force) : void // Раскручивает АТТ вокруг глобальной оси Y. function FixedUpdate () { rigidbody.AddTorque (Vector3.up * 10); } // 2 тип: function AddTorque (x: float, y: float, z: float, mode: ForceMode = ForceMode.Force) : void // Делает то же самое, но снова в другой системе измерения. function FixedUpdate () { rigidbody.AddTorque (0, 10, 0); }
Для правильной работы наших АТТ
их нужно снабдить Collider
"ами (или Collision
"ами, как вам будет угодно ^.^).
Подробно о коллайдерах читайте ниже.
Соблюдайте размеры вашего объекта, ведь они гораздо более значимы даже массы АТТ . Если ваш объект движется неправильно, висит в воздухе или не сталкивается, попробуйте настроить его величину (не АТТ , а самого объекта). При импортировании модели из 3D редактора ее размеры сохраняются, так что будьте внимательны на стадии моделирования и соблюдайте размеры всех моделей.
На этом, описывать АТТ или Rigidbody , я, пожалуй, закончу. Однако, есть пара подсказок, специально для тех, кто до сюда долистал:)
В предыдущем разделе мы рассмотрели принцип работы Rigidbody
и упомянули так называемые коллайдеры
. Коллайдер
для нас - вспомогательный объект в виде сетки простой примитивной или, наоборот, сложной формы, который находится вокруг нашей модели или части модели и взаимодействует с другими объектами, если те тоже окружены коллайдерами.
Чтобы наглядно объяснить знатокам редактора мира *Warcraft 3*, представьте себе импортированную нами модель, которой мы в редакторе дудадов не присвоили текстуры путей - это будет наш объект; а роль коллайдеров тут будут играть блокираторы пути вокруг модели. Естественно, это довольно грубое сравнение, ведь в Unity
они гораздо более функциональны. Что-ж, рассмотрим поподробнее.
Коллайдеры добавляются через меню Component - Physics . Есть несколько видов:
В принципе, все коллайдеры похожи друг на друга, просто используются для объектов разных форм, однако у них есть несколько разных параметров.
Мне нравится +15 - 0
*Unity* - очень мощный, прогрессивный движок с большим потенциалом. Он обладает множеством уже встроенных функций (в том числе и физическим движком *NvidiaPhysX*), которые нам, пользователям, прописывать вручную не придется. :)
В этой небольшой статье я бы хотел обсудить физические возможности движка. Итак, начнем:
Rigidbody
=
= Что это такое? =
За функцией *Rigidbody* скрывается Абсолютно Твердое Тело (*АТТ*). Если объяснять грубо и понятно, то *АТТ* в физике и механике - это идеальное твердое тело, которое под воздействием силы не может менять свои свойства, но может (под ее воздействием) перемещаться в 3х измерениях (вниз, вверх, вперед и т.д., т.е. в наших X-Y-Z осях), а также вращаться в 3х измерениях (опять же по осям X-Y-Z).
В *Unity*, как и в других игровых движках (опять же называю их именно "игровыми" движками грубо), *Rigidbody* используется для различных объектов, с которыми мы можем взаимодействовать, толкая, пиная и т.п. Подобные объекты под нашим влиянием будут далее под воздействием гравитации кататься, передвигаться и сталкиваться с другими предметами.
Какое применение мы можем найти этой функции? =
К примеру, для создания автомобиля, кроме *Rigidbody* нам понадобятся 4 Wheel Collider
"а и *код* (*скрипт*)
, применяющий силовое воздействия к колесам, в зависимости от нажатых клавиш.
Как мы можем использовать эту функцию? =
= Базовые знания.
Чтобы использовать *АТТ*, нам нужен уже созданный игровой объект (*GameObject*), кликнув на нем, мы проходим в меню по следующему пути: Components - Physics - Rigidbody
. Все, *АТТ* добавлено! :)
Теперь объект подвержен гравитации, к нему можно применять силы с помощью скриптов, но для того, чтобы объект вел себя именно так, как вам нужно, следует добавить *Collider* или *Joint*.
Код правит миром.
В скрипте манипулировать нашим объектом теперь мы будем с помощью функций AddForce()
и AddTorque()
.
Так как я в *Unity* применяю *JavaScript*, мои примеры будут с ним, ссылки на другие примеры скриптинга (на C#
или *Boo*) вы найдете ниже, в пункте Дополнительная информаия по АТТ
.
Rigidbody.AddForce
// Rigidbody.AddForce использует 2 типа формул, как и многие другие функции, связанные с перемещениями в пространстве. // 1 тип: function AddForce (force: Vector3, mode: ForceMode = ForceMode.Force) : void // Сила, подбрасывающая объект вверх, относительно глобальной системы координат. function FixedUpdate () { rigidbody.AddForce (Vector3.up * 10); } // Используется Vector3 - встроенная функция Unity, которая, в принципе, аналогична стандартной системе координат. // 2 тип: function AddForce (x: float, y: float, z: float, mode: ForceMode = ForceMode.Force) : void // То же самое, но тут используется X-Y-Z система координат. function FixedUpdate () { rigidbody.AddForce (0, 10, 0); }
Rigidbody.AddTorque
// Функция раскручивает объект вокруг заданной оси. // 1 тип: function AddTorque (torque: Vector3, mode: ForceMode = ForceMode.Force) : void // Раскручивает АТТ вокруг глобальной оси Y. function FixedUpdate () { rigidbody.AddTorque (Vector3.up * 10); } // 2 тип: function AddTorque (x: float, y: float, z: float, mode: ForceMode = ForceMode.Force) : void // Делает то же самое, но снова в другой системе измерения. function FixedUpdate () { rigidbody.AddTorque (0, 10, 0); }
АТТ взаимодействует с объектами.
Для правильной работы наших *АТТ* их нужно снабдить Collider
"ами (или Collision
"ами, как вам будет угодно ^.^).
Подробно о коллайдерах читайте ниже.
Размер имеет значение!
Соблюдайте размеры вашего объекта, ведь они гораздо более значимы даже массы *АТТ*. Если ваш объект движется неправильно, висит в воздухе или не сталкивается, попробуйте настроить его величину (не *АТТ*, а самого объекта). При импортировании модели из 3D редактора ее размеры сохраняются, так что будьте внимательны на стадии моделирования и соблюдайте размеры всех моделей.
Дополнительная информация по АТТ =
На этом, описывать *АТТ* или *Rigidbody*, я, пожалуй, закончу. Однако, есть пара подсказок, специально для тех, кто до сюда долистал:)
Посмотреть скриптовые примеры воздействия внешних сил на объект с функцией *АТТ* можно по следующим ссылкам:
*AddForce*
*AddTorque*
Colliders
=
= Что это такое? =
В предыдущем разделе мы рассмотрели принцип работы *Rigidbody* и упомянули так называемые *коллайдеры*. *Коллайдер* для нас - вспомогательный объект в виде сетки простой примитивной или, наоборот, сложной формы, который находится вокруг нашей модели или части модели и взаимодействует с другими объектами, если те тоже окружены коллайдерами.
Чтобы наглядно объяснить знатокам редактора мира *Warcraft 3*, представьте себе импортированную нами модель, которой мы в редакторе дудадов не присвоили текстуры путей - это будет наш объект; а роль коллайдеров тут будут играть блокираторы пути вокруг модели. Естественно, это довольно грубое сравнение, ведь в *Unity* они гораздо более функциональны. Что-ж, рассмотрим поподробнее.
Виды коллайдеров. =
Коллайдеры добавляются через меню Component - Physics
. Есть несколько видов:
Настраиваемые характеристики =
В принципе, все коллайдеры похожи друг на друга, просто используются для объектов разных форм, однако у них есть несколько разных параметров.
* *Material*
- Показывает, как коллайдер взаимодействует с остальными объектами, при этом присваивая физический материал, например, металл, лед и т.п.
* Is Trigger
- Если параметр включен, то на объект воздействует скрипт, а не физика.
* *Size*
- Размер коллайдера по осям X-Y-Z.
* *Center*
- Положение коллайдера, относительно локальных координат объекта.
* *Radius*
- Радиус сферы, заменяет параметр *Size*.
* Остальные параметры без изменений.
* *Radius*
- Толщина капсулы.
* *Height*
- Высота цилиндрической части коллайдера (без скругленных оснований).
* *Direction*
- Направление коллайдера, относительно локальных координат объекта.
* *Mesh*
- Выбор нужного меша для создания коллайдера.
* Smooth Sphere Collisions
- Включение этой функции сглаживает поверхность коллайдера. Использовать следует на гладких поверхностях, к примеру, наклонный ландшафт без лишней углоатости, по которому должны скатываться сферы.
* *Convex*
- При включении позволяет нашему коллайдеру сталкиваться с другими такими же. Convex Mesh Collider
"ы ограничены до 255 трианглов
.
* *Radius*
- Радиус колеса.
* Suspension Distance
- Максимальная дистания увеличения подвески колеса. Подвеска всегда увеличивается вниз по локальной оси *Y*.
* Suspension Spring
- Подвеска пытается достигнуть указанной точки, используя различные силы.
* *Mass*
- Масса колеса.
* Forward/Sideways Friction
- Параметры трения при простом качении колеса и при качении боком (такое бывает в заносах или при дрифте).
Пишем арканоид на Unity. Механика мяча и платформы
Итак, мы продолжаем цикл статей о написании простой игры на Unity - классического арканоида. Использовать будем только 2D инструменты, предоставляемые нам движком. В каждой из статей мы затронем один из аспектов написания игры, а в этой приведем в движение мячик и платформу, находящуюся под управлением игрока.
Вот список всех статей:
В предыдущем уроке мы настроили проект, перенесли в него ресурсы и создали первую простенькую сцену. Если вы не прочитали , мы настоятельно рекомендуем вам исправить этот недочет.
Сама платформа у нас уже есть - мы ее создали еще в прошлом уроке. Осталось научить ее двигаться, причем исключительно влево или вправо, т.е. по оси X. Для этого нам потребуется написать сценарий (Script ).
Сценарии представляют собой фрагменты программного кода, которые ответственны за какую-то конкретную задачу. Unity может работать со скриптами, написанными на трех языках программирования: Boo, JavaScript и C#. Мы будем использовать последний, но вы можете попробовать свои силы и с другими языками.
Итак, для создания скрипта перейдем на вкладку Project , найдем там одноименную папку Scripts и кликнем на нее правой кнопкой мыши. Выберем Create -> C# Script . Появится новый файл с названием NewBehaviourScript . Переименуйте его в PlayerScript для удобства. На вкладке Inspector вы можете видеть содержимое скрипта.
Двойным кликом откройте скрипт. Запуститься среда разработки MonoDevelop, которую вы впоследствии можете изменить на любой удобный для вас редактор. Вот то, что вы увидите:
Using UnityEngine; using System.Collections; public class NewBehaviourScript: MonoBehaviour { // используйте этот метод для инициализации void Start () { } // Update вызывается при отрисовке каждого кадра игры void Update () { } }
Все сценарии на Unity имеют по умолчанию два метода:
Для того, чтобы сдвинуть платформу, нам потребуется два вида информации: позиция и скорость.
Таким образом, необходимо создать две переменные для сохранения этой информации:
public float playerVelocity;
private Vector3 playerPosition;
Обратите внимание, что одна переменная объявлена публично, а вторая - приватно. Для чего это делается? Дело в том, что Unity позволяет редактировать значения публичных переменных не переходя в редактор MonoDevelop, без необходимости изменения кода. Эта возможность очень полезна в тех случаях. когда необходимо «на лету» корректировать какое-то значение. Скорость платформы - одно из таких значений, и именно поэтому мы объявили его публично.
Сохраните сценарий в редакторе MonoDevelop и перейдите в редактор Unity. Теперь у нас есть сценарий и нам нужно присвоить его какому то объекту, в нашем случае - платформе. Выберите нашу платформу во вкладке Hierarchy и в окне Inspector добавьте компонент, кликнув на кнопку Add Component .
Добавление нашего скрипта в компонент можно сделать и по-другому. Перетащите наш сценарий в область кнопки Add Component . Во вкладке Inspector вы должны увидеть что-то подобное:
Обратите внимание, что в компоненте скрипта появилось поле Player Velocity , которое можно тут же изменить. Это получилось возможным благодаря публичному объявлению переменной. Установите параметр в значение 0.3 и перейдите в редактор MonoDevelop.
Теперь нам надо узнать позицию платформы: playerPosition . Для того, чтобы инициализировать переменную, следует обратиться к объекту сценария в методе Start() :
// используйте этот метод для инициализации void Start () { // получим начальную позицию платформы playerPosition = gameObject.transform.position; }
Отлично, мы определили начальную позицию платформы, и теперь можно ее двигать. Так как нам надо, чтобы платформа перемещалась только по оси X, то мы сможем использовать метод GetAxis класса Input . Этой функции мы передадим строку Horizontal , и она вернет нам 1, если была нажата клавиша «вправо», и -1 - «влево». Умножив полученное число на скорость и прибавив эту величину к текущей позиции игрока, мы и получим движение.
Также добавим проверку на выход из приложения по нажатию на клавишу Esc .
Вот то, что у нас должно получиться в итоге:
Using UnityEngine; using System.Collections; public class PlayerScript: MonoBehaviour { public float playerVelocity; private Vector3 playerPosition; // используйте этот метод для инициализации void Start () { // получим начальную позицию платформы playerPosition = gameObject.transform.position; } // Update вызывается при отрисовке каждого кадра игры void Update () { // горизонтальное движение playerPosition.x += Input.GetAxis ("Horizontal") * playerVelocity; // выход из игры if (Input.GetKeyDown(KeyCode.Escape)){ Application.Quit(); } // обновим позицию платформы transform.position = playerPosition; } }
Сохраните скрипт и вернитесь в редактор Unity. Нажмите кнопку Play и попробуйте передвинуть платформу при помощи кнопок «влево» и «вправо».
Вы, скорее всего, заметили, что платформа может двигаться и за пределами игрового поля. Дело в том, что у нас нет никаких проверок на выход за пределы каких-то границ.
Давайте добавим в наш существующий скрипт еще одну публичную переменную и назовем его boundary .
Эта переменная будет хранить максимальную координату платформы по оси X. Так как мы собираемся строить уровни в симметричной форме вокруг точки с координатами (0, 0, 0), то абсолютное значение переменной boundary будет одинаковым и для положительной части оси X, и для отрицательной.
А теперь добавим пару условий. Поступим достаточно просто: если вычисленная нами позиция будет больше boundary или меньше -boundary , то мы просто зададим новую позицию по оси X, равную значению переменной boundary . Таким образом, мы гарантируем, что платформа не уедет за пределы наших границ и никогда не покинет игровую зону. Вот код:
Using UnityEngine; using System.Collections; public class PlayerScript: MonoBehaviour { public float playerVelocity; private Vector3 playerPosition; // используйте этот метод для инициализации void Start () { // получим начальную позицию платформы playerPosition = gameObject.transform.position; } // Update вызывается при отрисовке каждого кадра игры void Update () { // горизонтальное движение playerPosition.x += Input.GetAxis ("Horizontal") * playerVelocity; // выход из игры if (Input.GetKeyDown(KeyCode.Escape)){ Application.Quit(); } // обновим позицию платформы transform.position = playerPosition; // проверка выхода за границы if (playerPosition.x < -boundary) { transform.position = new Vector3 (-boundary, playerPosition.y, playerPosition.z); } if (playerPosition.x > boundary) { transform.position = new Vector3(boundary, playerPosition.y, playerPosition.z); } } }
Теперь вернитесь в редактор и, переключаясь в игру, найдите оптимальное значение переменной boundary . В нашем случае подошло число 5.46. Откройте Inspector и сбросьте позицию платформы по оси X на 0, а параметр Boundary выставьте согласно найденному вами значению.
Нажмите кнопку Play и убедитесь в том, что вы все сделали правильно. Платформа должна двигаться только в пределах игрового поля.
Чтобы столкновения были более реалистичные - воспользуемся симуляцией физики. В этой статье мы добавим физические свойства мячику, платформе и границам поля. Так как мы пишем 2D игру, то будем использовать 2D коллайдеры. Коллайдер - это отдельный тип компонентов, который позволяет объекту реагировать на коллайдеры других объектов.
В окне Hierarchy выберем нашу платформу, перейдем в Inspector и нажмем на кнопку Add Component . В появившемся окошке наберем collider . Как вы можете увидеть - вариантов достаточно много. Каждый коллайдер имеет специфические свойства, соответствующие связанным объектам - прямоугольникам, кругам и т.д.
Так как наша платформа имеет прямоугольную форму, мы будем использовать Box Collider 2D . Выберите именно его, и компонент автоматически определит размеры платформы: вам не нужно будет задавать их вручную, Unity сделает это за вас.
Сделайте то же самое и для 3 границ (Hierarchy -> Inspector -> Add Component -> Box Collider 2D ).
С мячиком чуть-чуть по другому: он имеет круглую форму. Выберем мяч и добавим для нее компонент Circle Collider 2D .
На самом деле коллайдер окружности и прямоугольника очень похожи, за исключением того, что вместо параметра Size , определяющим ширину и длину, в окружности используется Radius . Объяснения здесь, думаем, излишни.
Для того, чтобы наш мячик отскакивал от блоков, стен и платформы, нам следует задать поверхность (material) для физического компонента, добавленного ранее. В Unity все уже имеется, нам остается только добавить нужный материал.
Откройте окно Project и внутри папки Asset создайте новую папку под названием Physics . Кликните по только что созданной папке правой кнопкой мыши и выберите Create -> Physics2D Material . Задайте название BallPhysicsMaterial .
Каждая поверхность в Unity имеет два параметра: трение (friction) и упругость (bounciness) . Более подробно вы можете прочитать про физический движок и ряд физических параметров . Если вам требуется абсолютно упругое тело, то следует выставить трение на 0, а упругость на 1.
Сейчас у нас есть готовый материал, но он пока никак не связан с мячом. Выберите объект мяча во вкладке Hierarchy и в окне Inspector вы увидите поле Material компонента Circle Collider 2D . Перетащите сюда недавно созданный материал.
Для того, чтобы наш мячик двигался под контролем физики, мы должны добавить ему еще один компонент: Rigid Body 2D . Выберите объект мяча в окне Hierarchy и добавьте вышеупомянутый компонент - хоть он и имеет несколько параметров, нас интересует только один: Gravity Scale . Так как наш шарик будет двигаться только за счет отскоков, то мы зададим этому параметру 0 - таким образом мы гарантируем, что гравитация не будет реагировать на объект. Все остальное можно не менять.
Давайте создадим для шарика отдельный скрипт (снова воспользуемся C# в качестве языка программирования) и назовем его BallScript . Свяжите созданный скрипт с объект (Hierarchy -> Inspector -> Add Component ).
Перед тем, как начать писать скрипт, давайте определим поведение шарика:
Основываясь на этой информации, давайте создадим глобальные переменные ballIsActive , ballPosition и ballInitialForce:
private bool ballIsActive;
private Vector3 ballPosition;
private Vector2 ballInitialForce;
Теперь, когда у нас есть набор переменных, мы должны подготовить объект. В методе Start() мы должны:
Вот, как это можно сделать:
Void Start () { // создаем силу ballInitialForce = new Vector2 (100.0f,300.0f); // переводим в неактивное состояние ballIsActive = false; // запоминаем положение ballPosition = transform.position; }
Как вы могли заметить, сила прилагается не строго вертикальная, а наклоненная вправо - шарик будет двигаться по диагонали.
Void Update () { // проверка нажатия на пробел if (Input.GetButtonDown ("Jump") == true) { } }
Следующим шагом является проверка состояния шара, поскольку задать силу нам надо только в том случае, если шар находится в неактивном состоянии:
Void Update () { // проверка нажатия на пробел if (Input.GetButtonDown ("Jump") == true) { // проверка состояния if (!ballIsActive){ } } }
Если предположить, что мы находимся в начале игры, то мы должны применить силу к шару и установить его в активное состояние:
Void Update () { // проверка нажатия на пробел if (Input.GetButtonDown ("Jump") == true) { // проверка состояния if (!ballIsActive){ // применим силу rigidbody2D.AddForce(ballInitialForce); // зададим активное состояние ballIsActive = !ballIsActive; } } }
Если теперь вы включите игру, то, нажав на пробел, шар действительно начнет движение. Однако вы можете заметить, что мяч в неактивном состоянии ведет себя не совсем правильно: если мы будем двигать платформу, то мяч должен двигаться вместе с ней, но на самом деле остается на прежней позиции. Остановите игру, давайте исправим это.
В методе Update мы должны проверять состояние шарика, и в случае если оно неактивное, нам надо задать позицию мячика по оси X таким же, какое оно у платформы.
Решение достаточно простое, но как нам получить координату совсем другого объекта? Элементарно - мы создадим переменную типа GameObject и сохраним ссылку на объект платформы:
public GameObject playerObject;
Вернемся к методу Update() :
Void Update () { // проверка нажатия на пробел if (Input.GetButtonDown ("Jump") == true) { // проверка состояния if (!ballIsActive){ // применим силу rigidbody2D.AddForce(ballInitialForce); // зададим активное состояние ballIsActive = !ballIsActive; } if (!ballIsActive && playerObject != null){ // задаем новую позицию шарика ballPosition.x = playerObject.transform.position.x; // устанавливаем позицию шара transform.position = ballPosition; } } }
Сохраните скрипт и вернитесь в редактор Unity. Вы наверняка заметили, что переменная playerObject объявлена, используется, но нигде не инициализирована. Да, так и есть. Чтобы ее проинициализировать, перейдите во вкладку Hierarchy , найдите шар и в окне Inspector найдите компонент Ball Script . У данного компонента есть параметр Player Object , в настоящее время пустующий:
Найдите во вкладке Hierarchy нашу платформу и перетащите ее на поле Player Object . Запустите игру, нажав кнопку Play , и убедитесь, что все работает.
Если на данном этапе запустить игру и проиграть (чтобы шар упал за пределы поля), то ничего не вернется на круги своя. А на самом деле должен произойти сброс состояния игры. Давайте исправим это.
Это состояние отловить очень просто: шар будет активен, а его положение по оси Y отрицательно. Если это так, то мы переводим шар в неактивное состояние и ставим его на платформу:
If (ballIsActive && transform.position.y < -6) { ballIsActive = !ballIsActive; ballPosition.x = playerObject.transform.position.x; ballPosition.y = -4.2f; transform.position = ballPosition; }
Но и это не конец. Сохранив скрипт и запустив игру, вы заметите, что каждый раз при перезапуске уровня шар обретает всю большую и большую силу. Почему так происходит? Дело в том, что мы не очищаем силы, приложенные к шару. Чтобы исправить этот недочет, мы можем воспользоваться параметром IsKinematic , выключая его перед добавлением силы и включая - после падения.
Вот такой вышел у нас итоговый метод Update() :
Public class BallScript: MonoBehaviour { private bool ballIsActive; private Vector3 ballPosition; private Vector2 ballInitialForce; // GameObject public GameObject playerObject; // используйте этот метод для инициализации void Start () { // создаем силу ballInitialForce = new Vector2 (100.0f,300.0f); // переводим в неактивное состояние ballIsActive = false; // запоминаем положение ballPosition = transform.position; } void Update () { // проверка нажатия на пробел if (Input.GetButtonDown ("Jump") == true) { // проверка состояния if (!ballIsActive){ // сброс всех сил rigidbody2D.isKinematic = false; // применим силу rigidbody2D.AddForce(ballInitialForce); // зададим активное состояние ballIsActive = !ballIsActive; } if (!ballIsActive && playerObject != null){ // задаем новую позицию шарика ballPosition.x = playerObject.transform.position.x; // устанавливаем позицию шара transform.position = ballPosition; } // проверка падения шара if (ballIsActive && transform.position.y < -6) { ballIsActive = !ballIsActive; ballPosition.x = playerObject.transform.position.x; ballPosition.y = -4.2f; transform.position = ballPosition; rigidbody2D.isKinematic = true; } } }
А вот теперь точно все. Запустите игру и проверьте, все ли работает как положено.
Итак, вторая статья подошла к концу. Теперь вы научились работать со скриптами, коллайдерами и с клавиатурой. В следующий раз мы поговорим о механике игровых блоков.