Веревки добавляют забавную и интересную механику игровым сценам. В этом уроке по Unity вы продолжите заниматься ими.
Работа с точками вращения
Метод, который вы только что добавили для обработки позиций каната - настоящий прорыв. В настоящее время вы используете только два положения каната. Первое соответствует позиции игрока, а второе - текущему положению поворота, когда вы запускаете крюк для захвата.
Единственная проблема заключается в том, что вы еще не отслеживаете все возможные позиции веревки, и поэтому вам придется еще немного над этим поработать.
Чтобы обнаружить позиции, к которым будет «крепиться» веревка или добавить новую позицию вершины в рендерер линий, вам понадобится система, которая будет определять, находится ли точка вершины между прямой линией над червяком и текущей точкой крепления веревки.
Во-первых, вам нужно будет создать метод, который может найти ближайшую точку в коллайдере, пользуясь местом выстрела луча и краев коллайдера.
Откройте скрипт 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 (значение каждой строки словаря – это сами позиции), а ключи каждой строки настроены как дистанции от данной точки до позиции игрока. Вот, что еще здесь происходит: итоговая позиция трансформируется в пространство (по умолчанию вершина коллайдера хранится в локальном пространстве – то есть местном для самого коллайдера, а нам нужны пространственные позиции).
- Словарь упорядочен по ключам. Другими словами, ближайшее расстояние к текущей позиции игрока возвращается, это значит, что вне зависимости от того какая точка возвращается данным методом, это та точка, которая находится между игроком и петлей веревки!
Вернитесь назад в скрипт RopeSystem.cs и добавьте новое приватное поле для переменной наверхуp:
private Dictionary<Vector2, int> wrapPointsLookup = new Dictionary<Vector2, int>();
Вы будете использовать ее для отслеживания позиций, в которых должна изгибаться веревка.
В Update()
, найдите else
, которое находится внизу и содержит состояние crosshairSprite.enabled = false;
statement и добавьте следующее:
// 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
не пустой, то...
- Выстрелите с позиции игрока луч в направлении последней веревки в списке – поворотной точки, в которой крюк будет крепиться к скале; дистанция луча равна дистанции между игроком и позицией крепления веревки.
- Если луч ударяется обо что-то кроме коллайдера, коллайдер может смело отражаться в PolygonCollider2D. Если это настоящий PolygonCollider2D, ближайшая вершина возвращается как Vector2, благодаря этому великолепному методу, который вы написали ранее.
- wrapPointsLookup проверяется для того, чтобы убедиться, что одна и та же позиция не оборачивалась дважды. В противном случае веревка сбросится и обрежется, роняя игрока.
- Список ropePositions обновляется, добавляя позицию, через которую должна оборачиваться веревка; wrapPointsLookup здесь тоже обновляется. Наконец флаг distanceSet отключен, поэтому метод
UpdateRopePositions()
может переконфигурировать дистанции веревки, чтобы учесть длину и сегменты новой веревки.
Добавьте это в ResetRope()
, чтобы очистить wrapPointsLookup каждый раз, когда игрок отсоединяется от веревки:
wrapPointsLookup.Clear();
Сохраните и запустите игру. Выстрелите крюком в скалу и используйте инструмент Move (Перемещение), чтобы червяк передвигался вдоль этой самой скалы.
Вот так мы заставляем веревку и червяка двигаться!
Добавляем раскачивание
Сейчас наш червяк на веревке выглядит статичным. Чтобы исправить это, вы можете добавить способность раскачиаться вися на веревке.
Для этого вам понадобится сделать позицию, перпендикулярную качению червяка, неважно под каким углом он находится в данный момент.
Откройте 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 и в Update()
в верхней части блока else
для условия if(!ropeAttached)
добавьте:
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 или нет. Добавьте новый метод:
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.
Если в данный момент коллайдер касается другого физического объектав игре, метод OnTriggerStay2D будет срабатывать, устанавливая флаг isColliding в true
. Это означает, что всякий раз, когда червяк касается каменя, флаг isColliding становится равным true
.
Метод OnTriggerExit2D будет срабатывать, когда один коллайдер покинет зону другого коллайдера, устанавливая флаг isColliding в false
.
Метод OnTriggerStay2D может снижать производительность, так что будьте осторожнее с ним.
Запустите игру еще раз и, на этот раз, используйте клавиши со стрелками или с буквами W
/ S
для перемещения вверх или вниз по канату.
Вот ссылка на проект, который мы с вами сделали.
Вы прошли долгий путь от вялого слизня до моллюска-акробата! Вы научились создавать систему прицеливания, которая может стрелять крюком для захвата в любой предмет, покрытый коллайдером, прикрепляться к нему, раскачиваться и обматывать этот предмет веревкой!
Хотя и есть небольшой пропущенный кусочек, веревка не перематывается ни в одну сторону, когда она и не должна. Оставайтесь с нами и в следующей части этой серии уроков вы как раз этим займетесь.