Крюки-кошки добавляют в игру забавные и интересные механики. Можно использовать их для перемещения по уровням, боёв на аренах и получения предметов. Но несмотря на кажущуюся простоту, физика управления верёвками и создание реалистичного поведения может оказаться сложной задачей!
В первой части этого туториала мы реализуем собственную двухмерную систему крюка-кошки и научимся следующему:
- Создавать систему прицеливания.
- Использовать рендер линии и distance joint для создания верёвки.
- Научим верёвку оборачиваться вокруг игровых объектов.
- Вычислять угол качания на верёвке и добавлять силу в этом направлении.
Примечание: этот туториал предназначен для продвинутых и опытных пользователей, и в нём не рассматриваются такие темы, как добавление компонентов, создание новых скриптов GameObject и синтаксис C#. Если вам нужно повысить навыки владения Unity, то изучите наши туториалы Getting Started with Unity и Introduction to Unity Scripting. Так как в этом туториале используется DistanceJoint2D, то стоит также просмотреть Physics Joints in Unity 2D, и уже потом вернуться к этому туториалу.
Приступаем к работе
Скачайте заготовку проекта для этого туториала, а затем откройте его в редакторе Unity. Для работы необходима версия Unity 2017.1 или выше.
Откройте сцену Game из папки Scenes и посмотрите, с чего мы начнём:
Пока у нас есть простой персонаж игрока (слизняк) и висящие в воздухе камни.
Важными компонентами GameObject Player пока являются capsule collider и rigidbody, которые позволяют ему взаимодействовать с физическими объектами на уровне. Также к персонажу прикреплён простой скрипт движения (PlayerMovement), позволяющий ему скользить по земле и выполнять простые прыжки.
Нажмите на кнопку Play, чтобы запустить игру, и попробуйте управлять персонажем. A и D перемещают его влево/вправо, а при нажатии на пробел он совершает прыжок. Постарайтесь не соскользнуть и не упасть со скалы, иначе погибнете!
Основы управления у нас уже есть, но самой большой проблемой сейчас является отсутствие крюков-кошек.
Создание крюков и верёвки
Поначалу система крюков-кошек кажется довольно простой, но для её качественной реализации необходимо учитывать множество аспектов. Вот некоторые из требований, предъявляемых к двухмерной механике крюков-кошек:
- Line Renderer для отображения верёвки. Когда верёвка оборачивается вокруг объектов, мы можем добавлять больше сегментов к line renderer и располагать вершины в точках, соответствующих изломам верёвки.
- DistanceJoint2D. Его можно использовать для прикрепления текущей опорной точки (anchor point) крюка-кошки, чтобы наш слизняк мог раскачиваться. Также он позволяет нам настраивать расстояние, которое можно использовать для удлинения и уменьшения верёвки.
- Дочерний GameObject с RigidBody2D, который можно перемещать в зависимости от текущего местоположения опорной точки крюка. По сути, он будет подвесом/опорной точкой верёвки.
- Raycast для выбрасывания крюка и прикрепления к объектам.
Выберите в Hierarchy объект Player и добавьте к нему новый дочерний GameObject с названием RopeHingeAnchor. Этот GameObject будет использоваться для позиционирования подвеса/опорной точки крюка-кошки.
Добавьте к RopeHingeAnchor компоненты SpriteRenderer и RigidBody2D.
Для SpriteRenderer задайте, чтобы свойство Sprite использовало значение UISprite и измените Order in Layer на 2. Отключите компонент, сняв флажок рядом с его названием.
Для компонента RigidBody2D задайте свойству Body Type значение Kinematic. Эта точка будет перемещаться не физическим движком, а кодом.
Выберите слой Rope и задайте для значений масштаба по X и Y компонента Transform величину 4.
Снова выберите Player и прикрепите новый компонент DistanceJoint2D.
Перетащите RopeHingeAnchor из Hierarchy на свойство 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
значение текущего положения Player. - Это самая важная часть основного цикла
Update()
. Сначала мы получаем позицию курсора мыши в мире с помощью метода камерыScreenToWorldPoint
. Затем мы вычисляем направление взгляда, вычитая позицию игрока из позиции мыши в мире. Потом используем его для созданияaimAngle
, который является представлением угла прицеливания курсора. Значение сохраняет положительную величину в конструкции if. aimDirection
— это поворот, который пригодится позже. Нас интересует только значение по Z, поскольку мы используем 2D-камеру, и это единственная соответствующая ос. Мы передаёмaimAngle * Mathf.Rad2Deg
, что преобразует радианный угол в угол в градусах.- Позиция игрока отслеживается с помощью удобной переменной, которая позволяет не ссылаться постоянно на
transform.Position
. - Наконец, у нас есть конструкция
if..else
, которую мы скоро используем для того, чтобы определять, прикреплена ли верёвка к опорной точке.
Сохраните скрипт и вернитесь в редактор.
Прикрепите компонент RopeSystem к объекту Player и навесьте различные компоненты к публичным полям, которые мы создали в скрипте 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
(значение float, которое мы вычислили в Update()
) так, чтобы он вращался вокруг игрока с радиусом в 1 единицу. Также мы включаем спрайт прицела на случай, если это ещё не сделано.
В Update()
меняем конструкцию if..else
, проверяющую !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>();
LineRenderer будет содержать ссылку на рендер линий, который отрисовывает верёвку. LayerMask позволяет настраивать слои физики, с которыми может взаимодействовать крюк. Значение ropeMaxCastDistance
задаёт максимальное расстояние, на которое может «выстреливаться» raycast.
Наконец, список позиций 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-raycast из позиции игрока в направлении прицеливания. Задаётся максимальное расстояние, чтобы крюк-кошку нельзя было выстреливать на бесконечное расстояние, и применяется маска, чтобы можно было выбрать слои физики, с которыми raycast способен столкнуться.
- Если обнаружено попадание raycast, то
ropeAttached
принимает значениеtrue
, и выполняется проверка списка позиций вершин верёвки, чтобы убедиться, что точки ещё там нет. - Если проверка возвратила true, то к слизняку добавляется небольшой импульс силы, чтобы он подпрыгнул над землёй, включается
ropeJoint
(DistanceJoint2D), которому задаётся расстояние, равное расстоянию между слизняком и точкой попадания raycast. Также включается спрайт опорной точки. - Если raycast ни во что не попал, то line renderer и ropeJoint отключаются, а флаг
ropeAttached
принимает значение false. - Если нажата правая клавиша мыши, то вызывается метод
ResetRope()
, который отключает и сбрасывает все связанные с верёвкой/крюком параметры на значения, которые должны быть, если крюк не используется.
В самом низу нашего метода Update
добавим вызов нового метода HandleInput()
и передадим ему значение aimDirection
:
HandleInput(aimDirection);
Сохраните изменения в RopeSystem.cs и вернитесь в редактор.
Добавление верёвки
Наш слизняк не сможет летать по воздуху без верёвки, поэтому самое время дать ему то, что будет визуальным отображением верёвки и имеет способность «оборачиваться» вокруг углов.
Для этого идеально подходит line renderer, потому что он позволяет нам передавать количество точек и их позиции в пространстве мира.
Идея здесь заключается в том, что мы всегда храним первую вершину верёвки (0) в позиции игрока, а все остальные вершины позиционируются динамически, когда верёвка должна обернуться вокруг чего-нибудь, в том числе и текущую позицию шарнира, которая является следующей точкой вдоль верёвки от игрока.
Выберите Player и добавьте к нему компонент LineRenderer. Задайте для Width значение 0.075. Разверните список Materials и в качестве Element 0 выберите материал RopeMaterial, находящийся в папке Materials проекта. Наконец, у Line Renderer для параметра Texture Mode выберите значение Distribute Per Segment.
Перетащите компонент Line Renderer в поле Rope Renderer компонента Rope System.
Нажмите на разворачивающийся список Rope Layer Mask и выберите в качестве слоёв, с которыми может взаимодействовать raycast Default, Rope и Pivot. Благодаря этому при «выстреливании» raycast он будет сталкиваться только с этими слоями, но не с другими объектами, такими как игрок.
Если запустить игру сейчас, то можно заметить странное поведение. Когда мы нацеливаемся в камень над головой слизняка и выстреливаем крюком, то получается небольшой прыжок вверх, после чего наш друг начинает вести себя довольно хаотично.
Мы пока не задали расстояние для distance joint, кроме того, не настроены вершины рендера линии. Поэтому мы не видим верёвку, а поскольку distance joint находится прямо над позицией слизняка, текущее значение расстояния distance joint толкает его вниз, на камни под ним.
Но не волнуйтесь, сейчас мы решим эту проблему.
В скрипте RopeSystem.cs добавим в начале класса новый оператор:
using System.Linq;
Это позволяет нам использовать запросы LINQ, которые в нашем случае просто позволяют удобно находить первый или последний элемент списка ropePositions
.
Примечание: Language-Integrated Query (LINQ) — это название набора технологий, основанных на встраивании возможностей запросов непосредственно в язык C#. Подробнее можно прочитать о нём здесь.
Добавьте новую приватную переменную bool с названием distanceSet
под другими переменными:
private bool distanceSet;
Мы будем использовать эту переменную в качестве флага, чтобы скрипт мог узнавать, что расстояние верёвки (для точки между игроком и текущей опорной точкой, где прикреплён крюк-кошка) задано правильно.
Теперь добавим новый метод, который мы будем использовать для задания позиций вершин верёвки для рендера линии и настройки расстояния distance joint в хранимом списке позиций верёвкой (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) // if not the Last point of line renderer
{
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
равным расстоянию между игроком и текущей позицией верёвки, которую мы обходим в цикле. - Конструкция if обрабатывает случай, когда текущая позиция верёвки в цикле является второй с конца; то есть точкой, в которой верёвка соединяется с объектом, т.е. текущий шарнир/опорная точка.
- Этот блок
else
обрабатывает присвоение позицию последней вершины верёвки значения текущей позиции игрока.
Не забудьте добавить в конце Update()
вызов UpdateRopePositions()
:
UpdateRopePositions();
Сохраните изменения в скрипте и снова запустите игру. Сделайте «пробелом» небольшой прыжок, нацелившись и выстрелив крюком в камень над персонажем. Теперь вы можете наслаждаться плодами своих трудов — слизняк спокойно качается над камнями.
Теперь можно перейти к окну сцены, выбрать Player, воспользоваться инструментом перемещения (по умолчанию клавиша W), чтобы перемещать его и наблюдать за тем, как две вершины рендера линии верёвки следуют за позицией крюка и позицией игрока для отрисовки верёвки. После того, как мы отпускаем игрока, DistanceJoint2D правильным образом пересчитывает расстояние и слизняк будет продолжать качаться на соединённом шарнире.
Обработка точек оборачивания
Игра с качающимся слизняком пока полезна не больше, чем водоотталкивающее полотенце, поэтому нам совершенно определённо нужно её дополнить.
Хорошая новость заключается в том, что только что добавленный метод обработки позиций верёвки можно использовать в будущем. Пока мы используем только две позиции верёвки. Одна соединена с позицией игрока, а вторая — к текущей позиции опорной точки крюка при выстреле им.
Единственная проблема заключается в том, что пока мы не отслеживаем все потенциальные позиции верёвки, и над этим нужно немного поработать.
Для распознавания позиций на камнях, вокруг которых должна оборачиваться верёвка, добавляя новую позицию вершины к рендеру линии, нам необходима система, определяющая, находится ли точка вершины коллайдера между прямой линией между текущей позицией слизняка и текущим шарниром/опорной точкой верёвки.
Похоже, это снова работа для старого доброго raycast!
Для начала нам нужно создать метод, который сможет находить ближайшую точку в коллайдере на основании точки попадания raycast и граней коллайдера.
Добавим новый метод в скрипт RopeSystem.cs:
// 1
private Vector2 GetClosestColliderPointFromRaycastHit(RaycastHit2D hit, PolygonCollider2D polyCollider)
{
// 2
var distanceDictionary = polyCollider.points.ToDictionary<Vector2, float, Vector2>(
position => Vector2.Distance(hit.point, polyCollider.transform.TransformPoint(position)),
position => polyCollider.transform.TransformPoint(position));
// 3
var orderedDictionary = distanceDictionary.OrderBy(e => e.Key);
return orderedDictionary.Any() ? orderedDictionary.First().Value : Vector2.zero;
}
Если вам не знакомы запросы LINQ, то этот код может показаться какой-то сложной магией C#.
Если это так, то не бойтесь. LINQ самостоятельно делает за нас много работы:
- Этот метод получает два параметра — объект RaycastHit2D и PolygonCollider2D. Все камни на уровне имеют коллайдеры PolygonCollider2D, поэтому если мы всегда будем использовать фигуры PolygonCollider2D, то сработает отлично.
- Здесь начинается магия запросов LINQ! Тут мы преобразуем коллекцию точек полигонального коллайдера в словарь позиций Vector2 (значение каждого элемента словаря является самой позицией), а ключу каждого элемента присваивается значение расстояния от этой точки до позиции игрока player (значение float). Иногда здесь происходит и кое-что ещё: получившаяся позиция преобразуется в пространство мира (по умолчанию позиции вершин коллайдера хранятся в локальном пространстве, т.е. в локальном относительно объекта, которому принадлежит коллайдер, а нам нужны позиции в пространстве мира).
- Словарь упорядочен по ключам. Другими словами, по ближайшему к текущей позиции игрока расстоянию. Возвращается ближайшее расстояние, то есть любая точка, возвращаемая этим методом, является точкой коллайдера между игроком и текущей точкой шарнира верёвки!
Вернёмся в скрипт RopeSystem.cs и добавим вверху новую приватную переменную поля:
private Dictionary<Vector2, int> wrapPointsLookup = new Dictionary<Vector2, int>();
Мы воспользуемся ею для отслеживания позиций, вокруг которых может обернуться верёвка.
В конце метода Update()
найдите конструкцию else
, в которой содержится crosshairSprite.enabled = false;
и добавьте следующее:
// 1
if (ropePositions.Count > 0)
{
// 2
var lastRopePoint = ropePositions.Last();
var playerToCurrentNextHit = Physics2D.Raycast(playerPosition, (lastRopePoint - playerPosition).normalized, Vector2.Distance(playerPosition, lastRopePoint) - 0.1f, ropeLayerMask);
// 3
if (playerToCurrentNextHit)
{
var colliderWithVertices = playerToCurrentNextHit.collider as PolygonCollider2D;
if (colliderWithVertices != null)
{
var closestPointToHit = GetClosestColliderPointFromRaycastHit(playerToCurrentNextHit, colliderWithVertices);
// 4
if (wrapPointsLookup.ContainsKey(closestPointToHit))
{
ResetRope();
return;
}
// 5
ropePositions.Add(closestPointToHit);
wrapPointsLookup.Add(closestPointToHit, 0);
distanceSet = false;
}
}
}
Объясним этот фрагмент кода:
- Если в списке
ropePositions
хранятся какие-то позиции, то... - Выстреливаем из позиции игрока в направлении игрока, смотрящего на последнюю позицию верёвки из списка — опорную точку, в которой крюк-кошка прицепился к камню — с расстоянием raycast равным расстоянию между игроком и позицией опорной точки верёвки.
- Если raycast с чем-то сталкивается, то коллайдер этого объекта безопасно приводится к типу PolygonCollider2D. Пока он является настоящим PolygonCollider2D, ближайшая позиция вершины этого коллайдера возвращается с помощью написанного нами ранее метода как Vector2.
- Проверяется
wrapPointsLookup
, чтобы убедиться, что та же позиция не проверяется снова. Если она проверяется, то мы сбрасываем верёвку и обрезаем её, роняя игрока. - Затем обновляется список
ropePositions
: добавляется позиция, вокруг которой должна обернуться верёвка. Также обновляется словарьwrapPointsLookup
. Наконец, сбрасывается флагdistanceSet
, чтобы методUpdateRopePositions()
мог переопределить расстояния верёвки с учётом новой длины верёвки и сегментов.
В ResetRope()
добавим следующую строку, чтобы словарь wrapPointsLookup
очищался каждый раз, когда игрок отсоединяет верёвку:
wrapPointsLookup.Clear();
Сохранитесь и запустите игру. Выстрелите крюком-кошкой в камень над слизняком и воспользуйтесь инструментом Move в окне Scene, чтобы переместить слизняка за несколько каменных выступов.
Вот так мы научили верёвку оборачиваться вокруг объектов!
Добавляем способность раскачивания
Слизняк висит на верёвке довольно статично. Чтобы исправить это, мы можем добавить способность раскачивания.
Для этого нам нужно получить позицию, перпендикулярную к позиции раскачивания вперёд (вбок), вне зависимости от угла, под которым он смотрит.
Откройте PlayerMovement.cs и добавьте в верхнюю часть скрипта две следующие публичные переменные:
public Vector2 ropeHook;
public float swingForce = 4f;
Переменной ropeHook
будет присваиваться любая позиция, в которой находится крюк верёвки в настоящее время, а swingForce
— это значение, которое мы используем для добавления движения раскачивания.
Замените метод FixedUpdate()
новым:
void FixedUpdate()
{
if (horizontalInput < 0f || horizontalInput > 0f)
{
animator.SetFloat("Speed", Mathf.Abs(horizontalInput));
playerSprite.flipX = horizontalInput < 0f;
if (isSwinging)
{
animator.SetBool("IsSwinging", true);
// 1 - получаем нормализованный вектор направления от игрока к точке крюка
var playerToHookDirection = (ropeHook - (Vector2)transform.position).normalized;
// 2 - Инвертируем направление, чтобы получить перпендикулярное направление
Vector2 perpendicularDirection;
if (horizontalInput < 0)
{
perpendicularDirection = new Vector2(-playerToHookDirection.y, playerToHookDirection.x);
var leftPerpPos = (Vector2)transform.position - perpendicularDirection * -2f;
Debug.DrawLine(transform.position, leftPerpPos, Color.green, 0f);
}
else
{
perpendicularDirection = new Vector2(playerToHookDirection.y, -playerToHookDirection.x);
var rightPerpPos = (Vector2)transform.position + perpendicularDirection * 2f;
Debug.DrawLine(transform.position, rightPerpPos, Color.green, 0f);
}
var force = perpendicularDirection * swingForce;
rBody.AddForce(force, ForceMode2D.Force);
}
else
{
animator.SetBool("IsSwinging", false);
if (groundCheck)
{
var groundForce = speed * 2f;
rBody.AddForce(new Vector2((horizontalInput * groundForce - rBody.velocity.x) * groundForce, 0));
rBody.velocity = new Vector2(rBody.velocity.x, rBody.velocity.y);
}
}
}
else
{
animator.SetBool("IsSwinging", false);
animator.SetFloat("Speed", 0f);
}
if (!isSwinging)
{
if (!groundCheck) return;
isJumping = jumpInput > 0f;
if (isJumping)
{
rBody.velocity = new Vector2(rBody.velocity.x, jumpSpeed);
}
}
}
Основные изменения здесь заключаются в том, что сначала проверяется флаг isSwinging
, чтобы действия выполнялись только когда слизняк висит на верёвке, а также мы добавляем перпендикуляр к углу слизняка, указывающий на его текущую опорную точку в верхней части верёвки, но перпендикулярный направлению его качания.
- Получаем нормализованный вектор направления от игрока к точке закрепления крюка.
- В зависимости от того, качается ли слизняк влево или вправо, перпендикулярное направление вычисляется с помощью
playerToHookDirection
. Также добавлен отладочный вызов отрисовки, чтобы при желании можно было увидеть его в редакторе.
Откройте RopeSystem.cs и в верхней части блока else внутри if(!ropeAttached)
метода Update()
добавьте следующее:
playerMovement.isSwinging = true;
playerMovement.ropeHook = ropePositions.Last();
В блоке if той же конструкции if(!ropeAttached)
добавьте следующее:
playerMovement.isSwinging = false;
Так мы сообщаем скрипту PlayerMovement, что игрок качается, а также определяем последнюю (за исключением позиции игрока) позицию верёвки — иными словами, опорную точку верёвки. Это необходимо для вычисления перпендикулярного угла, которое мы только что добавили в скрипт PlayerMovement.
Вот как это выглядит, если включить в запущенной игре gizmos и нажимать A или D для раскачивания влево/вправо:
Добавление спуска по верёвке
Пока у нас нет возможности перемещаться вверх-вниз по верёвке. Хоть в реальной жизни слизняк не мог бы с лёгкостью подниматься и опускаться по верёвке, но это же игра, в которой может случиться что угодно, верно?
В верхнюю часть скрипта RopeSystem добавим две новых переменных поля:
public float climbSpeed = 3f;
private bool isColliding;
climbSpeed
будет задавать скорость, с которой слизняк может двигаться вверх и вниз по верёвке, а isColliding
будет применяться как флаг для определения того, можно ли увеличивать или уменьшать свойство расстояния distance joint верёвки.
Добавим такой новый метод:
private void HandleRopeLength()
{
// 1
if (Input.GetAxis("Vertical") >= 1f && ropeAttached && !isColliding)
{
ropeJoint.distance -= Time.deltaTime * climbSpeed;
}
else if (Input.GetAxis("Vertical") < 0f && ropeAttached)
{
ropeJoint.distance += Time.deltaTime * climbSpeed;
}
}
Этот блок if..elseif
считывает ввод по вертикальной оси (вверх/вниз или W/S на клавиатуре), и с учётом флагов ropeAttached iscColliding
увеличивает или уменьшает расстояние ropeJoint
, создавая эффект удлинения или укорачивания верёвки.
Прицепим этот метод, добавив его вызов в конец Update()
:
HandleRopeLength();
Также нам понадобится способ задания флага isColliding
.
В нижнюю часть скрипта добавьте два следующих метода:
void OnTriggerStay2D(Collider2D colliderStay)
{
isColliding = true;
}
private void OnTriggerExit2D(Collider2D colliderOnExit)
{
isColliding = false;
}
Эти два метода являются нативными методами базового класса скриптов MonoBehaviour.
Если Collider в текущий момент касается другого физического объекта в игре, то постоянно будет срабатывать метод OnTriggerStay2D
, присваивая флагу isColliding
значение true
. Это значит, что когда слизняк касается камня, флагу isColliding присваивается значение true
.
Метод OnTriggerExit2D
срабатывает, когда один коллайдер покидает область другого коллайдера, присваивая флагу значение false.
Имейте в виду: метод OnTriggerStay2D
может быть очень затратным по вычислениям, поэтому используйте его аккуратно.
Куда двигаться дальше?
Снова запустите игру и на этот раз нажимайте клавиши со стрелками или W/S, чтобы двигаться вверх-вниз по верёвке.
Готовый проект этой части туториала можно скачать здесь.
Мы прошли долгий путь — от не умеющего качаться размазни-слизняка до акробатического беспанцирного брюхоногого моллюска!
Вы узнали, как создать систему прицеливания, которая может выстреливать крюк-кошку в любой объект, имеющий коллайдер, цепляться за него и одновременно качаться на нём, оборачиваясь на динамической верёвке вокруг граней объектов! Отличная работа.
Однако здесь отсутствует важная функция — верёвка не может «раскручиваться», когда это необходимо.
Во второй части этого туториала мы будем решать эту задачу.
Но если вы готовы рискнуть, то почему бы не попробовать сделать это самостоятельно? Для этого можно использовать словарь wrapPointsLookup
.
Автор: PatientZero