В этом выпуске надо было делать рельеф местности, но стоит немного отвлечься и поговорить про физику, так как я очень долго разбирался с одной проблемой, и с ней сталкивался не только я.
Предыдущие части: Начало, Объекты против кода, Генерация ландшафта
Что такое "физика"?
В играх объекты часто ведут себя так, как в реальном мире: падают под действием гравитации, ударяются о другие объекты, отскакивают, кувыркаются, скользят и т.п.
В совокупности всё это называется "физикой".
Все эти движения рассчитываются с помощью физических формул, которые учитывают массу и форму объекта, действие на него различных сил, а также такие свойства, как трение или упругость.
Очевидно, это весьма сложная система, и программировать её руками было бы трудно.
Поэтому обычно физику обрабатывает отдельный движок, который живёт своей жизнью.
В Юнити физика присутствует сразу, а вы можете ею пользоваться или не пользоваться. Для этого нужны два компонента: RigidBody и Collider.
RigidBody
Это "твёрдое тело". Имеется в виду, что все объекты рассматриваются движком как твёрдые тела. Они не могут гнуться или сплющиваться.
У GameObject, который вы создаёте в редакторе или в коде, изначально нет компонента RigidBody. Это значит, что ни в какой физике он не участвует.
Когда же вы добавляете к GameObject компонент RigidBody:
gameObject.AddComponent<RigidBody>();
то RigidBody автоматически наследует позицию и размеры объекта. Теперь физический движок "увидел" объект, но по факту он видит только прицепленный к нему RigidBody, и воздействует на объект через этот компонент.
С RigidBody можно делать следующее:
- задавать скорость и направление движения с помощью вектора Vector3
- задавать скорость и направление вращения с помощью кватерниона (обсудим позже)
- прикладывать силу, которая заставит RigidBody двигаться
У RigidBody есть такие свойства, как масса и торможение, что влияет на физику.
Также на RigidBody автоматически действует гравитация. По умолчанию гравитация направлена вертикально вниз и равна земной. Но её можно задать какой угодно в настройках проекта.
Таким образом, физика действует на RigidBody, а RigidBody транслирует все изменения положения на объект-родитель.
Collider
Хотя RigidBody движется в соответствии с физикой, то есть падает, реагирует на приложенные силы и т.п., он не может отскакивать от других объектов. Если два RigidBody столкнутся, они просто пройдут друг друга насквозь.
Чтобы отрабатывать столкновения, нужно добавить к GameObject ещё один компонент – коллайдер:
gameObject.AddComponent<Collider>();
Коллайдер по сути представляет собой некую геометрическую фигуру, описывающую форму объекта. Например, у куба коллайдер имеет форму куба (BoxCollider), у сферы – форму сферы (SphereCollider). Именно между этими формами и происходит столкновение.
Но также можно кубу назначить коллайдер в форме сферы, и наоборот. Для чего это вообще делать?
Отработка столкновений между двумя фигурами тоже затратное дело, поэтому по мере возможности надо всё упрощать. Самый простой вариант это куб или сфера. Можно взять объект сложной формы и назначить ему коллайдер в форме куба, приблизительно совпадающего по размерам.
Тогда этот объект будет сталкиваться с другими так, как будто он куб. Это не совсем точно, зато экономит много вычислений.
Также есть оптимизированные алгоритмы вычисления пересечений для сферы или цилиндра. В общем, мы ставим на объект такой коллайдер, который максимально близок к форме объекта, и который максимально оптимизирован под эту форму.
Этой информации достаточно для начала, а сейчас перейдём к той проблеме, которая возникла.
Платформы
Во многих 2D- и 3D-играх есть движущиеся платформы, на которые можно запрыгивать. Поэтому такие игры и называются "платформеры".
Давайте в качестве игрока и платформы рассмотрим два куба. Один куб (игрок) падает на другой (платформу). А платформа при этом движется.
Мы ожидаем, что когда один куб упадёт на другой, он должен остаться стоять на нём и двигаться вместе с ним.
Вместо этого, однако, происходит что-то другое: нижний куб продолжает ехать, а верхний скользит по нему, пока не упадёт с края.
Именно с этой проблемой я разбирался. Она с одной стороны очевидна, а с другой стороны её невозможно решить, не имея правильных знаний.
Решение
Сначала посмотрим, как реализуется эта задача.
И куб игрока, и куб платформы – это GameObject'ы с прикреплёнными RigidBody и BoxCollider.
Тут есть одна хитрость. Как только мы включим игру, физика заставит игрока и платформу падать вниз. Для игрока это нормально, а вот платформа должна левитировать, не падая. Кроме того, она должна ездить влево-вправо по заданному алгоритму, не поддаваясь на влияния внешних сил. Для этого у платформы включается флаг isKinematic:
platform.rigidBody.isKinematic = true;
Это значит, что RigidBody платформы не подчиняется законам физики и её движение происходит через вручную написанный код, который меняет её координаты.
Таким образом, платформа перестаёт падать, а мы пишем код, чтобы в каждом кадре её немного сдвигать.
Ловушка заключается в том, что первоначально я узнал свойства GameObject, и знаю, что у него есть свойство transform.position. Ранее я мог его использовать, поэтому логично использовать и сейчас:
gameObject.transform.position = Vector3(...);
То есть просто присваивая новые координаты, я перемещаю объект, и таким образом платформа начинает двигаться. Хотя я меняю координаты у GameObject, RigidBody движется вместе с ним, то есть перенимает эти координаты.
И тут я встречаю вышеописанную проблему: вроде бы внешне всё работает, платформа движется и куб падает на неё, но не едет вместе с платформой, а скользит по ней.
Это сразу увело в сторону от истины – я начал разбираться в свойствах материалов, в массе объектов, в трении, то есть думал, что скольжение происходит просто потому, что эти объекты плохо сцепляются между собой.
И попал в полный тупик. Ничего не помогало. Обратившись в гугл, я нашёл такие проблемы у многих людей, а после долгих поисков наконец нашёл решение.
Разгадка
В сущности, для понимания этого феномена достаточно одного предложения: присваивание координат объекту не двигает его. Но мы же видим, что он движется? Видим, но нет, он не движется. Он просто перескакивает из одних координат в другие. Но это и есть движение? Нет, это не движение, а телепортация.
Под движением понимается именно непрерывное движение от точки к точке. У движения есть скорость. У телепортации нет скорости. То есть объект имел нулевую скорость, а после изменения координат он появился в другом месте, но по-прежнему имеет нулевую скорость.
Теперь посмотрим на проблему с этим знанием:
- Падающий блок падает на другой блок, который постоянно телепортируется.
- Упавший блок стоит на месте, а телепортирующийся блок просто исчезает и появляется в новой позиции, поддерживая его снизу.
Естественно, раз скорость обоих блоков равна нулю, то верхний блок не будет ехать верхом на нижнем. Это и выглядит как эффект скольжения.
Как правильно?
Нужно усвоить, что с физикой работает только RigidBody. А у RigidBody, оказывается, есть метод MovePosition(Vector3 position). Он тоже перемещает тело к заданным координатам, но делает это именно перемещением, то есть у тела появляется скорость.
И теперь блок падает на другой блок, имеющий скорость, и эта скорость заставляет двигаться оба блока.
Вот и всё.
Как видите, решение было буквально в одной строчке, и если внимательно почитать документацию по RigidBody, то там прямо об этом говорится. Правда, нигде не говорилось, что GameObject.transform.position ведёт себя как телепорт.
В следующем выпуске вернёмся к генерации ландшафта.