paint-brush
Как создать контроллер 2D-персонажа в Unity: часть 1к@deniskondratev
2,267 чтения
2,267 чтения

Как создать контроллер 2D-персонажа в Unity: часть 1

к Denis Kondratev15m2024/04/22
Read on Terminal Reader

Слишком долго; Читать

В этой статье мы погружаемся в процесс создания 2D-контроллера персонажей в Unity, уделяя особое внимание интеграции нового метода Slide для Rigidbody2D, который упрощает перемещение персонажа. Он охватывает настройку, физическое поведение, обработку столкновений и ограничения движения, предоставляя базовое руководство по разработке контроллера персонажа, который можно адаптировать и расширить для различных 2D-платформеров.
featured image - Как создать контроллер 2D-персонажа в Unity: часть 1
Denis Kondratev HackerNoon profile picture
0-item
1-item
2-item


Одним из ключевых элементов любого 2D-платформера является главный герой. То, как он движется и управляется, существенно формирует атмосферу игры — будь то уютная олдскульная игра или динамичный слэшер. Поэтому создание контроллера персонажей — важный ранний этап разработки платформера.


В этой статье мы подробно рассмотрим процесс создания персонажа с нуля, научив его передвигаться по уровню, придерживаясь законов физики. Даже если у вас уже есть опыт создания контроллеров персонажей, вам будет интересно узнать о нововведениях Unity 2023. К моему удивлению, для компонента Rigidbody2D добавлен долгожданный метод Slide , который значительно упрощает написание контроллера персонажа позволяя более эффективно использовать Rigidbody2D в кинематическом режиме. Раньше весь этот функционал приходилось реализовывать вручную.


Если вы хотите не только прочитать статью, но и попробовать ее на практике, рекомендую скачать шаблон уровня из репозитория GitHub Treasure Hunters , куда уже включены необходимые ассеты и готовый уровень для тестирования вашего персонажа.


Закладываем основу нашего характера

Для нашего платформера мы установили правило, что в игре будут только вертикальные и горизонтальные поверхности, а сила тяжести будет направлена строго вниз. Это значительно упрощает создание платформера на начальном этапе, особенно если вы не хотите углубляться в векторную математику.


В будущем я, возможно, отступлю от этих правил в своем проекте Treasure Hunters, где исследую создание механики для 2D-платформера на Unity. Но это будет тема другой статьи.


Исходя из нашего решения, что движение будет осуществляться по горизонтальным поверхностям, основа нашего персонажа будет прямоугольной. Использование наклонных поверхностей потребует разработки коллайдера в форме капсулы и дополнительных механик, таких как скольжение.


Для начала создайте на сцене пустой объект и назовите его Капитан — это будет наш главный герой. Добавьте к объекту компоненты Rigidbody2D и BoxCollider2D . Установите тип Rigidbody2D на Kinematic, чтобы мы могли контролировать движение персонажа, при этом используя встроенные физические возможности Unity. Также заблокируйте вращение персонажа по оси Z, активировав опцию «Заморозить вращение Z».


Ваша установка должна выглядеть так, как показано на рисунке ниже.


Теперь добавим внешний вид нашему капитану. Найдите текстуру в разделе «Активы/Текстуры/Охотники за сокровищами/Нос капитана клоуна/Спрайты/Нос капитана клоуна/Нос капитана клоуна с мечом/09-Idle Sword/Idle Sword 01.png» и установите для него значение Пиксель на единицу равное 32. Мы часто будем используйте это значение в этом курсе, потому что наши текстуры создаются с таким разрешением. Установите для режима спрайта значение «Одиночный» и для удобства установите для параметра «Поворот» значение «Низ». Не забудьте применить изменения, нажав кнопку «Применить». Убедитесь, что все настройки выполнены правильно.



В этой статье мы не будем касаться анимации персонажей, поэтому пока воспользуемся одним спрайтом. В объекте Captain создайте вложенный объект с именем Appearance и добавьте к нему компонент Sprite Renderer, указав ранее настроенный спрайт.



Когда я увеличил изображение капитана, я заметил, что оно было довольно размытым из-за неправильных настроек спрайтов. Чтобы это исправить, выберите текстуру Idle Sword 01 в окне «Проект» и установите режим фильтра на «Точка» (без фильтра). Теперь изображение выглядит намного лучше.


Настройка коллайдера персонажей

На данный момент положение коллайдера и изображение нашего капитана не совпадают, и коллайдер оказывается слишком большим для наших нужд.

Давайте исправим это, а также правильно расположим ось персонажа внизу. Мы применим это правило ко всем объектам в игре, чтобы облегчить их размещение на одном уровне независимо от размера.


Для простоты управления этим процессом используйте параметр «Переключить положение ручки инструмента» с набором «Поворот», как показано ниже.


Следующий шаг — настроить коллайдер нашего героя так, чтобы его точка поворота находилась точно в центре внизу. Размер коллайдера должен точно соответствовать размерам персонажа. Настройте параметры Offset и Size коллайдера, а также положение вложенного объекта Appearance, чтобы добиться необходимой точности.


Особое внимание обратите на параметр коллайдера Offset.X: его значение должно быть строго 0. Это обеспечит симметричное размещение коллайдера относительно центра объекта, что крайне важно для последующих поворотов персонажа влево и вправо, где значение Transform.Scale.X изменяется на -1 и 1. Коллайдер должен оставаться на месте, а визуальное вращение должно выглядеть естественно.


Введение в физическое поведение персонажей

Прежде всего, важно научить персонажей — будь то главный герой, неигровые персонажи или враги — взаимодействовать с физическим миром. Например, персонажи должны иметь возможность ходить по плоской поверхности, спрыгивать с нее и подчиняться силе гравитации, притягивающей их обратно на землю.


Unity имеет встроенный физический движок, который управляет движением тел, обрабатывает столкновения и добавляет воздействие внешних сил на объекты. Все это реализовано с помощью компонента Rigidbody2D . Однако для персонажей полезно иметь более гибкий инструмент, который взаимодействует с физическим миром, предоставляя разработчикам больше контроля над поведением объекта.


Как я упоминал ранее, в предыдущих версиях Unity всю эту логику разработчикам приходилось реализовывать самостоятельно. Однако в Unity 2023 к компоненту Rigidbody2D был добавлен новый метод Slide , позволяющий гибко управлять физическим объектом, предоставляя при этом всю необходимую информацию о выполняемом движении.


Начнем с создания класса CharacterBody , который будет содержать базовую логику перемещения персонажей в физическом мире. Этот класс будет использовать Rigidbody2D в Kinematic режиме, который мы уже добавили нашему персонажу. Итак, первое, что нужно сделать, это добавить ссылку на этот компонент.


 public class CharacterBody : MonoBehaviour { [SerializeField] private Rigidbody2D _rigidbody; }


Иногда для динамики движения персонажа необходимо, чтобы гравитация действовала сильнее обычного. Для этого добавим коэффициент влияния гравитации с начальным значением 1, и этот коэффициент не может быть меньше 0.


 [Min(0)] [field: SerializeField] public float GravityFactor { get; private set; } = 1f;


Нам также необходимо определить, какие объекты будут считаться непроходимыми поверхностями. Для этого мы создадим поле, позволяющее указать необходимые слои.


 [SerializeField] private LayerMask _solidLayers;


Мы ограничим скорость персонажей, чтобы они не развивали чрезмерно большую скорость, например, из-за какого-то внешнего воздействия, в том числе гравитации. Мы устанавливаем начальное значение 30 и ограничиваем возможность установки значения меньше 0 в инспекторе.


 [Min(0)] [SerializeField] private float _maxSpeed = 30;


При движении по поверхности мы хотим, чтобы персонаж всегда цеплялся за нее, если расстояние между ними достаточно мало.


 [Min(0)] [SerializeField] private float _surfaceAnchor = 0.01f;


Хоть мы и решили, что поверхности в нашей игре будут только горизонтальными или вертикальными, на всякий случай укажем максимальный угол наклона поверхности, на котором персонаж может устойчиво стоять, с начальным значением 45º.


 [Range(0, 90)] [SerializeField] private float _maxSlop = 45f;


Через инспектор я также хочу видеть текущую скорость персонажа и его состояние, поэтому добавлю два поля с атрибутом SerializeField .


 [SerializeField] private Vector2 _velocity; [field: SerializeField] public CharacterState State { get; private set; }


Да, здесь я представил новую, пока еще не определённую сущность CharacterState . Мы обсудим это дальше.


Состояния персонажа

Чтобы упростить разработку нашего платформера, давайте определим всего два основных состояния персонажа.

Первое — Grounded , состояние, в котором персонаж надежно стоит на поверхности. В этом состоянии персонаж может свободно передвигаться по поверхности и спрыгивать с нее.


Второй — Airborne , состояние свободного падения, при котором персонаж находится в воздухе. Поведение персонажа в этом состоянии может меняться в зависимости от специфики платформера. В общем случае, близком к реальности, персонаж движется под действием первоначального импульса и не может повлиять на свое поведение. Однако в платформерах физика часто упрощается в пользу удобства и динамики игрового процесса: например, во многих играх, даже находясь в свободном падении, мы можем управлять горизонтальным движением персонажа. В нашем случае это тоже возможно, как и популярная механика двойного прыжка, позволяющая совершить дополнительный прыжок в воздух.


Представим в коде состояния нашего персонажа:


 /// <summary> /// Describes the state of <see cref="CharacterBody"/>. /// </summary> public enum CharacterState { /// <summary> /// The character stays steady on the ground and can move freely along it. /// </summary> Grounded, /// <summary> /// The character is in a state of free fall. /// </summary> Airborne }


Стоит отметить, что состояний может быть гораздо больше. Например, если в игре есть наклонные поверхности, персонаж может находиться в состоянии скольжения, когда он не может свободно перемещаться вправо или влево, но может скользить по склону. В таком состоянии персонаж также может прыгать, отталкиваясь от склона, но только в сторону склона. Другой возможный случай — скольжение по вертикальной стене, где влияние гравитации ослаблено, и персонаж может оттолкнуться горизонтально.


Ограничение скорости движения

Мы уже определили приватное поле _velocity , но нам нужно иметь возможность получать и устанавливать это значение извне, ограничивая при этом максимальную скорость персонажа. Для этого необходимо сравнить заданный вектор скорости с максимально допустимой скоростью.


Это можно сделать, вычислив длину вектора скорости или, говоря математическим языком, его величину. Структура Vector2 уже содержит свойство magnitude , которое позволяет нам это сделать. Таким образом, если величина пройденного вектора превышает максимально допустимую скорость, нам следует сохранить направление вектора, но ограничить его величину. Для этого мы умножаем _maxSpeed на нормализованный вектор скорости (нормализованный вектор — это вектор с тем же направлением, но с величиной, равной 1).


Вот как это выглядит в коде:


 public Vector2 Velocity { get => _velocity; set => _velocity = value.magnitude > _maxSpeed ? value.normalized * _maxSpeed : value; }


Теперь давайте внимательно посмотрим, как рассчитывается величина вектора. Оно определяется формулой:



Вычисление квадратного корня — ресурсоемкая операция. Хотя в большинстве случаев скорость не превысит максимальную, нам все равно придется выполнять это сравнение хотя бы один раз за такт. Однако мы можем существенно упростить эту операцию, если сравним квадрат величины вектора с квадратом максимальной скорости.


Для этого вводим дополнительное поле для хранения квадрата максимальной скорости и вычисляем его один раз в методе Awake :


 private float _sqrMaxSpeed; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; }


Теперь настройку скорости можно выполнить более оптимально:


 public Vector2 Velocity { get => _velocity; set => _velocity = value.sqrMagnitude > _sqrMaxSpeed ? value.normalized * _maxSpeed : value; }


Таким образом мы избегаем лишних вычислений и повышаем производительность обработки скорости движения персонажа.


Метод движения твердого тела

Как я упоминал ранее, в Unity добавлен новый метод Slide() , который значительно упростит разработку нашего CharacterBody . Однако прежде чем использовать этот метод, необходимо определить правила, по которым объект будет перемещаться в пространстве. Такое поведение задается структурой Rigidbody2D.SlideMovement .


Давайте введем новое поле _slideMovement и установим его значения.


 private Rigidbody2D.SlideMovement _slideMovement; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; _slideMovement = CreateSlideMovement(); } private Rigidbody2D.SlideMovement CreateSlideMovement() { return new Rigidbody2D.SlideMovement { maxIterations = 3, surfaceSlideAngle = 90, gravitySlipAngle = 90, surfaceUp = Vector2.up, surfaceAnchor = Vector2.down * _surfaceAnchor, gravity = Vector2.zero, layerMask = _solidLayers, useLayerMask = true, }; }



Важно объяснить, что maxIterations определяет, сколько раз объект может изменить направление в результате столкновения. Например, если персонаж находится в воздухе рядом со стеной и игрок пытается сдвинуть ее вправо, пока на нее действует гравитация. Таким образом, для каждого вызова метода Slide() будет задан вектор скорости, направленный вправо и вниз. При ударе о стену вектор движения пересчитывается, и объект продолжит движение вниз.


В такой ситуации, если для maxIterations установлено значение 1, объект ударится о стену, остановится и фактически застрянет там.


Значения maxIterations и layerMask были определены ранее. Более подробную информацию по остальным полям смотрите в официальной документации по структуре .


Наконец, перемещение персонажа

Теперь все готово, чтобы капитан мог двигаться. Мы сделаем это в FixedUpdate — обратном вызове Unity, предназначенном для обработки физики. За последние несколько лет команда Unity значительно улучшила обработку 2D-физики. На данный момент обработку можно выполнить в обратном вызове Update или даже вызвав необходимый метод самостоятельно.


Однако в этом примере мы будем использовать традиционный и проверенный метод FixedUpdate . Прежде чем продолжить, стоит сказать несколько слов о значении Time.fixedDeltaTime .


Для обеспечения предсказуемости игровой физики моделирование проводится итерациями через фиксированные промежутки времени. Это гарантирует, что изменения FPS или лаги не повлияют на поведение объекта.


В начале каждого цикла мы будем учитывать влияние гравитации на объект. Поскольку сила тяжести определяется вектором ускорения свободного падения, мы можем рассчитать изменение скорости Δv объекта со временем Δt по формуле:




где a — постоянное ускорение тела. В нашем случае это ускорение силы тяжести с учетом введенного нами коэффициента — Physics2D.gravity * GravityFactor . Следовательно, Δv можно рассчитать следующим образом:


 Time.fixedDeltaTime * GravityFactor * Physics2D.gravity


Конечный результат, когда мы меняем скорость, выглядит так:


 Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity;


Теперь мы можем выполнить твердотельное движение персонажа:


 var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement);


Переменная slideResults является значением структуры SlideResults и хранит результаты перемещения. Основными полями этого результата для нас slideHit , результат столкновения с поверхностью во время движения, и surfaceHit — результат приведения вниз, который поможет определить, стоит ли персонаж на устойчивой поверхности.


Обработка столкновений

При столкновении с поверхностями крайне важно ограничить скорость персонажа, направленную к этой поверхности. Простой пример: если персонаж стоит на земле, он не должен продолжать набирать скорость под действием силы тяжести. В конце каждого цикла их скорость должна быть равна нулю. Аналогично, при движении вверх и ударе о потолок персонаж должен потерять всю вертикальную скорость и начать двигаться вниз.


Результаты столкновений, slideHit и surfaceHit , представлены значениями структуры RaycastHit2D , которая включает в себя нормаль поверхности столкновения.


Ограничение скорости можно рассчитать, вычитая проекцию исходного вектора скорости на нормаль столкновения из самого вектора скорости. Это делается с помощью скалярного произведения . Напишем метод, который будет выполнять эту операцию:


 private static Vector2 ClipVector(Vector2 vector, Vector2 hitNormal) { return vector - Vector2.Dot(vector, hitNormal) * hitNormal; }


Теперь давайте интегрируем этот метод в наш FixedUpdate . Здесь для surfaceHit мы ограничим скорость только в том случае, если она направлена вниз, так как заброс, определяющий, находится ли объект на поверхности, всегда выполняется для проверки контакта с землей.


 private void FixedUpdate() { Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity; var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement); if (slideResults.slideHit) { _velocity = ClipVector(_velocity, slideResults.slideHit.normal); } if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; _velocity = ClipVector(_velocity, surfaceHit.normal); } }


Такая реализация позволяет правильно управлять движением персонажа, избегать нежелательного ускорения при столкновении с различными поверхностями и сохранять движение персонажей в игре предсказуемым и плавным.


Определение состояния персонажа

В конце каждого цикла необходимо определить, находится ли персонаж на твёрдой поверхности (состояние «Grounded») или в свободном падении (или, как мы это определили, в управляемом падении — состояние «Airborne»).


Чтобы считать персонажа находящимся в состоянии Grounded, прежде всего, его вертикальная скорость должна быть нулевой или отрицательной, что мы определяем по значению _velocity.y .


Еще одним важным критерием является наличие поверхности под ногами персонажа, которую мы идентифицируем по результатам движения Rigidbody, а именно по наличию surfaceHit .


Третий фактор — это угол наклона поверхности, который мы анализируем на основе нормали к этой поверхности, т.е. значения surfaceHit.normal . Необходимо сравнить этот угол с _maxSlop — максимально возможным углом поверхности, на которой персонаж может устойчиво стоять.


Для полностью вертикальной поверхности нормаль будет строго горизонтальной, т. е. ее векторное значение будет (1, 0) или (-1, 0). Для горизонтальной поверхности значение нормали будет (0, 1). Чем меньше угол наклона, тем больше значение y . Для угла alpha это значение можно рассчитать как:



Поскольку угол у нас задан в градусах, а функция $\cos$ требует радиан, формула преобразуется в:



Для этого введем новое поле и посчитаем его в методе Awake .


 private float _minGroundVertical; private void Awake() { _minGroundVertical = Mathf.Cos(_maxSlop * Mathf.PI / 180f); //... }


Теперь обновим наш код в FixedUpdate , проверив все вышеперечисленные условия.


 if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); State = surfaceHit.normal.y >= _minGroundVertical ? CharacterState.Grounded : CharacterState.Airborne; } else { State = CharacterState.Airborne; }


Эта логика позволит нам точно определять, когда персонаж находится на земле, и правильно реагировать на изменения его состояния.


Добавление тела персонажа к капитану

Теперь, когда наш компонент CharacterBody готов, последний шаг — добавить его к нашему Капитану. На сцене выберите объект Captain и добавьте к нему компонент CharacterBody .

Не забудьте настроить Rigidbody, как показано на рисунке выше. Установите коэффициент гравитации на 3 и выберите параметр по умолчанию для сплошного слоя.


Теперь вы можете запустить игру и поэкспериментировать с установкой различных значений скорости, чтобы наблюдать, как наш персонаж перемещается по сцене.

Подведем итоги

Конечно, нам еще нужно добавить элементы управления персонажем. Однако эта статья уже стала довольно длинной, поэтому я подробно расскажу об управлении персонажем с помощью новой системы ввода в следующей статье: «Создание 2D-контроллера персонажа в Unity: Часть 2».


Полный проект, описанный в этой статье, вы можете скачать здесь: Охотники за сокровищами и проверить все на практике, если у вас возникнут какие-либо трудности. Разработка контроллера персонажа — ключевой аспект при создании 2D-платформера, поскольку он определяет дальнейшее развитие игры. Он влияет на то, насколько легко будут добавляться новые возможности в поведение главного героя или врагов. Поэтому очень важно понимать основы, чтобы иметь возможность самостоятельно разработать собственную игру.