Веревки добавляют забавную и интересную механику игровым сценам. Вы можете использовать их для перемещения по уровням, сражения на аренах или для извлечения предметов.
В первой части этой серии вы сможете реализовать свою собственную систему 2D веревок для перемещения и узнать о следующем:
- Создадите систему прицеливания.
- Использование линейного рендеринга и объединения дистанций для создания веревки.
- Веревка может обертываться вокруг объектов в игре.
- Расчет угла вращения веревки и придания силы в этом направлении.
Этот учебник предназначен для продвинутой аудитории и не будет охватывать такие вещи, как добавление компонентов, создание новых GameObjects-скриптов или синтаксис C#. Если вам нужно повысить свои навыки Unity, проработайте наши прошлые учебные пособия по Unity. Поскольку этот учебник также основан на DistanceJoint2D, мы советуем заглянуть в документацию и освежить свои знания по той теме.
Начало работы с Unity
Скачайте стартовый проект для этого учебника и откройте его в редакторе Unity. Убедитесь, что вы используете Unity 2017.1 или новее.
Откройте сцену Game, расположенную в папке Scenes и взгляните на то, с чем вы скоро начнете работать:
У нас есть основной персонаж (слизняк) и скалы.
Значимыми компонентами являются игровой объект Player, который имеет капсульный коллайдер и rigidbody (жесткое тело), которые позволяют ему взаимодействовать с физическими объектами. Также у нам есть простой скрипт для перемещения (PlayerMovement), который позволяет нашему скользкому персонажу ползать по земле и прыгать.
Нажмите на кнопку Play в редакторе, чтобы начать игру и испытать элементы управления. Клавиши A и D позволяют нам перемещаться влево и вправо, а клавиша "пробел" - выполнять прыжки. Будьте осторожны, чтобы не проскользнуть и не упасть с камней, иначе вы умрете!
Основные элементы управления реализованы, но сейчас самое большое беспокойство - отсутствие крючков для захвата.
Создание крючков и веревок
Система захвата крючков - звучит довольно просто, но есть много вещей, которые вам потребуется реализовать, чтобы заставить ее работать как следует. Вот некоторые из них:
- Line Renderer, который покажет веревку. Когда веревка обертывает предметы, вы можете добавить больше сегментов в рендеринг линии и правильно расположить вершины по краям изгибающейся веревки.
- DistanceJoint2D. Может использоваться для придания веревке конкретной точки захвата и для того, чтобы наш персонаж на ней качался. Это также позволит настроить расстояние, которое можно использовать для спуска вверх и вниз по канату.
- Игровой объект с RigidBody2D, который может быть перемещен в зависимости от текущего местоположения точки привязки крючка. Это будет, по сути, шарнирная петля / опорная точка.
- Рейкаст для метания крюка и прикрепления к объектам.
Выберите Player в Иерархии и добавьте новый Игровой объект в Player по имени RopeHingeAnchor. Этот Игровой объект будет использоваться, чтобы расположить шарнир / опорную точку крюка для захвата, где бы этот крюк ни находился во время игры.
Добавьте компоненты SpriteRenderer и RigidBody2D в RopeHingeAnchor.
В SpriteRenderer установите свойство Sprite в UISprite
и измените Order in Layer на 2
. Отключите компонент unchecking, сняв флажок рядом с его именем.
В компоненте RigidBody2D установите свойство Body Type равным Kinematic
. Эта точка будет вращаться не за счет физического движка, а благодаря коду.
Задайте Layer значение Rope
и установите значения Scale по осям X и Y равными 4
в компоненте Transform.
Выберите Player снова и добавьте новый компонент DistanceJoint2D.
Захватите и перетащите RopeHingeAnchor на Иерархии в свойство Connected Rigid Body компонента DistanceJoint2D и отключите Auto Configure Distance.
Создайте новый C#-скрипт по имени RopeSystem в папке Scripts и откройте его в вашем редакторе.
Удалите метод Update. В верхней части скрипта, внутри объявления класса RopeSystem, добавьте следующие переменные, а также методы Awake и Update:
// 1
public GameObject ropeHingeAnchor;
public DistanceJoint2D ropeJoint;
public Transform crosshair;
public SpriteRenderer crosshairSprite;
public PlayerMovement playerMovement;
private bool ropeAttached;
private Vector2 playerPosition;
private Rigidbody2D ropeHingeAnchorRb;
private SpriteRenderer ropeHingeAnchorSprite;
void Awake() {
// 2
ropeJoint.enabled = false;
playerPosition = transform.position;
ropeHingeAnchorRb = ropeHingeAnchor.GetComponent<Rigidbody2D>();
ropeHingeAnchorSprite = ropeHingeAnchor.GetComponent<SpriteRenderer>();
}
void Update() {
// 3
var worldMousePosition =
Camera.main.ScreenToWorldPoint(new Vector3(Input.mousePosition.x, Input.mousePosition.y, 0f));
var facingDirection = worldMousePosition - transform.position;
var aimAngle = Mathf.Atan2(facingDirection.y, facingDirection.x);
if (aimAngle < 0f) {
aimAngle = Mathf.PI * 2 + aimAngle;
}
// 4
var aimDirection = Quaternion.Euler(0, 0, aimAngle * Mathf.Rad2Deg) * Vector2.right;
// 5
playerPosition = transform.position;
// 6
if (!ropeAttached) {
...
} else {
...
}
}
Поясним, что здесь происходит:
- Эти переменные мы станем использовать для отслеживания различных компонентов, с которыми будет взаимодействовать скрипт RopeSystem.
- Метод Awake будет запущен когда игра начнется и отключит ropeJoint (компонент DistanceJoint2D). Он также установит
playerPosition
на текущую позицию Игрока.
- Это самая важная часть вашего основного цикла Update(). Во-первых, вы фиксируете положение курсора мыши в игровом мире, используя метод
ScreenToWorldPoint
. Затем вы вычисляете направление движения, вычитая позицию игрока из позиции мыши. Зная его, мы можем вычислить aimAngle, который представляет собой угол наклона курсора мыши. Значение корректируется с помощью условного оператора if.
- aimDirection - это вращение. Нас интересует только значение Z, поскольку вы используете 2D-камеру. Мы используем
Mathf.Rad2Deg
для преобразования угла из радиан в градусы.
- Позиция игрока отслеживается с помощью удобной переменной, которая позволяет вам не ссылаться все время на
transform.Position
.
- Наконец, условие
if..else
, которое использовать, чтобы определить крепится ли веревка к опорной точке.
Сохраните скрипт и вернитесь в редактор.
Прикрепите компонент RopeSystem к Игроку и подключите различные компоненты к публичным полям, которые вы создали в скрипте RopeSystem. Перетащите Player, Crosshair и RopeHingeAnchor в соответствующие поля:
- Rope Hinge Anchor: RopeHingeAnchor
- Rope Joint: Player
- Crosshair: Crosshair
- Crosshair Sprite: Crosshair
- Player Movement: Player
Забавно: мы возимся над вычислениями для прицеливания, но нет никаких визуальных маркеров, чтобы показать результаты нашей работы. Давайте исправим это.
Откройте скрипт RopeSystem и добавьте к нему новый метод:
private void SetCrosshairPosition(float aimAngle) {
if (!crosshairSprite.enabled) {
crosshairSprite.enabled = true;
}
var x = transform.position.x + 1f * Mathf.Cos(aimAngle);
var y = transform.position.y + 1f * Mathf.Sin(aimAngle);
var crossHairPosition = new Vector3(x, y, 0);
crosshair.transform.position = crossHairPosition;
}
Этот метод будет определять перекрестье, основанное на aimAngle
, который вы проходите (и значение которого вы рассчитали в Update()
) таким образом, что вокруг вас он делает круг, радиус которого равен 1 единице. Это также включит спрайт перекрестия, если даже до этого мы этого не делали.
В Update() измените условие, которое проверяет !ropeAttached
:
if (!ropeAttached) {
SetCrosshairPosition(aimAngle);
} else {
crosshairSprite.enabled = false;
}
Сохраните скрипт и запустите игру. Теперь у вашего червячка должна быть возможность прицелиться с помощью перекрестия.
. У нас уже есть отработанная система прицеливания, поэтому вам понадобится метод, который мы здесь будем использовать в качестве параметра.
Добавьте следующие переменные после имеющихся в скрипт RopeSystem:
public LineRenderer ropeRenderer;
public LayerMask ropeLayerMask;
private float ropeMaxCastDistance = 20f;
private List<Vector2> ropePositions = new List<Vector2>();
The LineRenderer хранит ссылку на линейный рендеринг, который отображает веревку. LayerMask позволяет вам самостоятельно определить, с какими физическими слоями рейкаст сможет работать. Значение ropeMaxCastDistance устанавливает максимальную дистанцию рейкаста.
Наконец, перечисленные позиции Vector2 будут использоваться для того, чтобы отследить точки вращения веревки, но об этом чуть позже в этом же уроке.
Добавьте следующие новые методы:
// 1
private void HandleInput(Vector2 aimDirection) {
if (Input.GetMouseButton(0)) {
// 2
if (ropeAttached) return;
ropeRenderer.enabled = true;
var hit = Physics2D.Raycast(playerPosition, aimDirection, ropeMaxCastDistance, ropeLayerMask);
// 3
if (hit.collider != null) {
ropeAttached = true;
if (!ropePositions.Contains(hit.point)) {
// 4
// Порыгайте немного после схватки за что-либо
transform.GetComponent<Rigidbody2D>().AddForce(new Vector2(0f, 2f), ForceMode2D.Impulse);
ropePositions.Add(hit.point);
ropeJoint.distance = Vector2.Distance(playerPosition, hit.point);
ropeJoint.enabled = true;
ropeHingeAnchorSprite.enabled = true;
}
}
// 5
else {
ropeRenderer.enabled = false;
ropeAttached = false;
ropeJoint.enabled = false;
}
}
if (Input.GetMouseButton(1)) {
ResetRope();
}
}
// 6
private void ResetRope() {
ropeJoint.enabled = false;
ropeAttached = false;
playerMovement.isSwinging = false;
ropeRenderer.positionCount = 2;
ropeRenderer.SetPosition(0, transform.position);
ropeRenderer.SetPosition(1, transform.position);
ropePositions.Clear();
ropeHingeAnchorSprite.enabled = false;
}
Вот объяснение того, что делает приведенный выше код:
- HandleInput вызывается из цикла
Update()
и ожидает клик мышкой.
- При левом клике включается линейный рендеринг веревки, а 2D-рейкаст выстреливается от игрока в направлении прицела. Максимальное расстояние задается таким, что веревку нельзя выстрелить на бесконечно длинное расстояние, а еще появляется специальный маркер, позволяющий определить, какие слои рейкаст может задеть, а какие нет.
- Если рейкаст что-либо задевает, ropeAttached устанавливается в
true
и идет проверка списка верхних позиций веревки, чтобы убедиться, что задетая точка не имеет максимальное значение.
- 4. Если значение из предыдущего пункта устанавливается в true, игровому персонажу придается небольшой импульс, который заставляет его подпрыгивать, а ropeJoint (DistanceJoint2D) включается и создает дистанцию, равную между персонажем и точкой присоединения. Также включается спрайт этого самого присоединения.
- Если рейкаст ничего не задевает, то линейный рендеринг и присоединение веревки отключены, а значение ropeAttached устанавливается в
false
.
- Если щелкнуть правой кнопкой мыши, то вызовется метод
ResetRope()
, который отключит и сбросит все параметры веревок и крюков на такие, как если бы они не использовались.
В самом низу метода Update добавьте вызов нового метода HandleInput()
и передайте в него значение aimDirection
:
HandleInput(aimDirection);
Сохраните ваши изменения в RopeSystem.cs и вернитесь обратно в ваш редактор.
Добавление веревки
Этот слизняк не сможет летать без веревки, так что теперь самое подходящее время, чтобы дать ему ее.
Линейный рендеринг идеально для этого подходит, т.к. он позволяет создавать вам необходимое количество точек и их позиций в пространстве игрового мира.
Идея состоит в том, что первая вершина веревки всегда находится на позиции игрока, а остальные вершины размещаются динамически там, куда нужно прикрепить веревку, в том числе текущую позицию поворота, которая является следующей точкой от игрока.
Выберите Player и добавьте к нему компонент LineRenderer component to it. Задайте Width равным 0.075
. Разверните Materials и для Element 0, выберите материал RopeMaterial из папки Materials. Наконец, для Line Renderer (Линейного рендеринга) выберите Distribute Per Segment напротив пункта Texture Mode.
Перетащите компонент Line Renderer в Rope System поля Rope Renderer.
Кликните на раскрывающемся списке Rope Layer Mask и выберите Default, Rope, and Pivot в качестве слоев, с которыми может взаимодействовать рейкаст. Это гарантирует, что когда рейкаст будет создан, он будет сталкиваться только с этими слоями, а не с другими вещами, такими как игрок.
Если сейчас запустить игру, то вы сможете заметить нечто странное: если прицелиться ровно над персонажем и выстрелить, результатом будет лишь маленький прыжок вверх, а сам персонаж будет затем двигаться несколько хаотично.
Дистанция соединения не задана, а значит, что и вершины линейного рендеринга тоже не настроены. Таким образом, получается, что веревку мы не видим из-за того, что максимальная дистанция ее выстрела находится прямо на голове нашего персонажа, и это значение сталкивает его вниз на камни.
Не волнуйтесь, сейчас мы с этим разберемся.
В скрипте RopeSystem.cs добавьте новый оператор using
в начало класса:
using System.Linq;
Это позволяет использовать LINQ-запросы, которые позволят вам легко найти первый или последний элемент в списке ropePositions.
Language-Integrated Query (LINQ) – это набор технологий по добавлению синтаксиса языка запросов непосредственно для языка C#. Дополнительную информацию вы можете получить здесь.
Добавьте под имеющимися переменными новую переменную по имени distanceSet логического типа:
private bool distanceSet;
Вы будете использовать ее как флаг, чтобы дать скрипту понять, что дистанция веревки (для той точки между игроком и текущим поворотом, в которой установлен "крюк") установлена правильно.
Теперь добавьте новый метод, который вы будете использовать для настройки верхних позиций веревки на линейном рендеринге и настройки дистанции. Он основан на имеющемся списке позиций веревки, с которым мы будем работать (ropePositions
):
private void UpdateRopePositions()
{
// 1
if (!ropeAttached) {
return;
}
// 2
ropeRenderer.positionCount = ropePositions.Count + 1;
// 3
for (var i = ropeRenderer.positionCount - 1; i >= 0; i--) {
if (i != ropeRenderer.positionCount - 1) //если это не последняя точка линейного рендеринга
{
ropeRenderer.SetPosition(i, ropePositions[i]);
// 4
if (i == ropePositions.Count - 1 || ropePositions.Count == 1) {
var ropePosition = ropePositions[ropePositions.Count - 1];
if (ropePositions.Count == 1) {
ropeHingeAnchorRb.transform.position = ropePosition;
if (!distanceSet) {
ropeJoint.distance = Vector2.Distance(transform.position, ropePosition);
distanceSet = true;
}
} else {
ropeHingeAnchorRb.transform.position = ropePosition;
if (!distanceSet) {
ropeJoint.distance = Vector2.Distance(transform.position, ropePosition);
distanceSet = true;
}
}
}
// 5
else if (i - 1 == ropePositions.IndexOf(ropePositions.Last())) {
var ropePosition = ropePositions.Last();
ropeHingeAnchorRb.transform.position = ropePosition;
if (!distanceSet) {
ropeJoint.distance = Vector2.Distance(transform.position, ropePosition);
distanceSet = true;
}
}
} else {
// 6
ropeRenderer.SetPosition(i, transform.position);
}
}
}
Поясним этот код:
- Выходим из этого метода, если веревка на самом деле не присоединилась.
- Задаем счет вершин линейного рендеринга веревки (позиций) до числа позиций, хранящегося в
ropePositions
, плюс еще 1 (для позиции игрока).
- Прокручиваем назад по списку
ropePositions
и для каждой позиции (кроме последней), устанавливаем позицию вершины линейного рендеринга как Vector2 которая хранится в ropePositions
.
- Устанавливаем позицию крюка на предпоследнюю позицию, где он должен быть; либо, если у нас есть только одна позиция, устанавливаем ее. Это задает дистанцию
ropeJoint
ropeJoint как дистанцию между игроком и текущей позицией веревки.
- Оператор
if
обрабатывает случай, когда позиция веревки является предпоследней; то есть точку, когда веревка присоединяется к объекту, являющемуся текущей точкой присоединения.
- Блок
else
обрабатывает настройки последней позиции вершины веревки и текущей позиции игрового персонажа.
Добавьте вызов UpdateRopePositions();
внизу Update()
, сохраните изменения в скрипте и снова запустите игру. Сделайте небольшой прыжок с помощью клавиши 'пробел', прицельтесь и выстрелите по скале над вами. Теперь вы можете полюбоваться плодами своего труда, смотря как червяк болтается над скалами.
Вы также можете переключиться на вкладку Scene (Сцена), выбрать Player, затем использовать инструмент «Перемещение» (клавиша W по умолчанию), чтобы передвинуть его и посмотреть две верщины линейного рендеринга веревки следуют за позицией крюка, а игрок создает веревку. Движение игрока будет перестраивать дистанцию нужным образом, а персонаж продолжит качаться на веревке.
Теперь, я предлагаю, сделать перерыв и продолжить изучение во второй части веревочного урока по Unity.