Передвижение персонажа в Unity 2D и 3D

Приветствую! В данной статье мы разберём правильную реализацию движения персонажа. Поскольку движение 2D и 3D персонажей почти на 100% эдентичны, то мы поочереди их и разберём. Благодаря чему, начинающий разработчик не только разберётся в том, как реализовать функцию передвижения, но увидит разницу между движениями 2D и 3D.
Начнём свой пусть с 2D передвижения, поскольку он чуток легче в понимании.
Передвижение 2D персонажа
Первым делом создадим игровой объект нашего персонажа, и назовём его, например, Player.

Далее добавим к нашему персонажу компонент Rigidbody 2D. Если в Вашей игре отсутствуют законы гравитации, то в в поле Body Type необходимо указать значение «Kinematic«.
P.S. Обычно гравитация отсутствует в 2д играх с видом сверху.

Теперь самое интересное, нам необходимо создать C# скрипт с названием, например movePlayer, и присвоить этот скрипт нашему объекту Player. А сам скрипт должен иметь следующее содержимое:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class movePlayer : MonoBehaviour < private Rigidbody2D rb; public float speed = 0.5f; private Vector2 moveVector; void Awake() < rb = GetComponent(); > void Update() < moveVector.x = Input.GetAxis("Horizontal"); moveVector.y = Input.GetAxis("Vertical"); rb.MovePosition(rb.position + moveVector * speed * Time.deltaTime); >>
Готово! Давайте разберём наш код. В строках 7,8,9 мы создали 3 переменные:
- rb — которая будет хранить ссылку на компонент Rigidbody2D
- speed — скорость передвижения вашего персонажа
- moveVector — направление движения персонажа в виде вектора
Далее в 13 строке мы в переменную rb присваивает ссылку на наш компонент Rigidbody2D, чтобы в дальнейшем можно было бы работать с его свойствами и методами.
В строке 18 в переменную moveVector.x мы записываем значение движения по горизонтали. Если максимально простыми словами, то при нажатии на клавишу «D», в переменной записывается значение «1» — вперёд. При нажатии на клавишу «A» — в переменную записывается «-1» — назад.
Аналогичная ситуация происходит и в 19 строке. При нажатии на клавишу «W», в переменной записывается значение «1» — вверх. При нажатии на клавишу «S» — в переменную записывается «-1» — вниз.
Ну и в 20 строке, с помощью метода MovePosition, мы заставляем двигаться нашего персонажа, исходя из значений которые мы получили в moveVector.
Теперь Ваш персонаж может спокойно передвигаться в мировом пространстве. Как видите, всё очень просто и понятно. Теперь как и обещал, перейдём к 3D.
Передвижение 3D персонажа
Для начала, создадим игровой объект персонажа с названием, например, Player. А так же создадим объект Земли, по которой персонаж будет передвигаться. И назовём его, например, Ground.

Так же добавляем к персонажу компонент Rigidbody — чтобы на него действовала гравитация(если нужно).
Теперь так же необходимо создать C# скрипт с названием, например movePlayer, и присваиваем этот скрипт нашему объекту Player. А сам скрипт должен иметь следующее содержимое:
using System.Collections; using System.Collections.Generic; using UnityEngine; public class movePlayer : MonoBehaviour < private Rigidbody rb; public float speed = 0.5f; private Vector3 moveVector; void Awake() < rb = GetComponent(); > void Update() < moveVector.x = Input.GetAxis("Horizontal"); moveVector.z = Input.GetAxis("Vertical"); rb.MovePosition(rb.position + moveVector * speed * Time.deltaTime); >>
Как видите, данный код почти на 100% аналогичен. Давайте посмотрим, что мы тут изменили.
- В 7 и 13 строке мы измении тип компонента с Rigidbody2d на Rigidbody, ведь мы уже имеем дело не с 2д, а с 3д.
- В 9 строке тип переменной moveVector изменён с Vector2 на Vector3, так как у игр 3D, три мировые оси а не две.
- В 19 строке мы изменили moveVector.y на moveVector.z, поскольку в 3D играх, ось по Y отвечает за верх. А вверх идти нельзя. Вверх можно передвигаться только с помощью прыжка, который мы реализуем в следующей статье.
На этом всё. Надеюсь данная статья научила Вас реализации передвижения персонажа, а так же показала в чём принципиальная разница передвижений между 2D и 3D играми.
Если Вам данная статья помогла, буду рад Вашим лайкам. Спасибо 🙂
Правильная реализация передвижения персонажа
Все эти вопросы, фактически, являются одним единым вопросом, который слишком уж часто встречается у начинающих.
Заодно создал тэг unity3d-faq
Отслеживать
47.6k 5 5 золотых знаков 22 22 серебряных знака 57 57 бронзовых знаков
задан 24 янв 2019 в 7:58
Andrew Stop_RU_war_in_UA Andrew Stop_RU_war_in_UA
19k 6 6 золотых знаков 35 35 серебряных знаков 98 98 бронзовых знаков
@Kromster Да, это можно использовать НЕ только как туториал для юнити, а и шире. Но код используется заточенный именно под юнити. Скриншоты/настройки/линки на документацию используются юнитовские. Если вопрос касается Array — то с него же не убирается метка C# только потому, что ответ будет подобный и для других языков. Думаю, что метки можно, все таки, оставлять.
29 янв 2019 в 6:51
Метка относится к вопросу, а не к ответу. Но вы правы, в данном случае все не так просто.
29 янв 2019 в 6:56
Нужно обязательно почитать, про силу трения это мощно)
23 мар 2019 в 19:52
1 ответ 1
Сортировка: Сброс на вариант по умолчанию
Перед прочтением важно знать
- В любом случае на персонаж должен быть навешан RigidBody — скрипт отвечающий за физику персонажа (силу притяжения, силу трения и т.д.)
- Хоть я здесь и разбираю в т.ч. нефизическое движение, я настоятельно рекомендую использовать ФИЗИЧЕСКОЕ движение. И переходить на нефизическое только в исключительных ситуациях.
- В коде не должно быть прямой привязки к кнопкам. Должна быть привязка к параметрам Input Manager . Которые можно найти в: Edit -> Project Settings -> Input . Нужно принять это как аксиому и не отходить, несмотря, на то что вы там нагуглите.
- Я буду использовать здесь 2 термина: «телепортация» и «плавное движение». В моем понимании:
- Плавное движение — перерасчет позиции обьекта в рамках физики или паралельно физике на вызове FixedUpdate() .
- Телепортация — перерасчет позиции обьекта на промежутке времени большем чем fixedDeltaTime .
Есть люди у которых мнение отличается.
- Плавное движение — исключительно физическое движение
- Телепортация — изменение позиции вручную или использование .Translate() метода.
- UPD: и кстате они правы — но мне влом править весь текст — Translate внутри использует телепортацию на каждом кадреhttps://github.com/Unity-Technologies/UnityCsReference/blob/master/Runtime/Transform/ScriptBindings/Transform.bindings.cs
Учтите, что все что написано ниже упирается в верхние значения терминов, а не эти.
Двигать обьекты в игровых движках можно следующими способами:
- используя физический движок (движение обусловленное физической моделью игрового движка)
- движение НЕфизическое. Неправильный подход — подход телепортации на каждом кадре. (в Update() )
- движение НЕфизическое. Правильный подход — плавное передвижение обьекта между кадрами (паралельно каждому просчету физики) (все равно желательно не использовать)
- Движение реализуемое через CharacterController (здесь пока что не рассматривается т.к. новички в его сторону вообще не смотрят, может, позже распишу)
На практике метод передвижения подбирается под конкретного персонажа[персонажа — не буквально. Это может быть и автомобиль]. В одном случае лучше будет физическое перемещение. В другом — нефизическое. В третьем случае будет лучше всего CharacterController. Понимание что лучше в каком случае прийдет с практикой.
Новички очень часто использую телепортацию на каждом кадре, что есть критически неправильным подходом. Потом на SO появляются кучи клонов вопросов вроде «почему персонажа дергает возле стены?» или «почему он проходит сквозь стену?» или «почему пуля не всегда наносит урон?» и подобные.
Нужно запомнить всего одно правило: Двигать/поворачивать через присвоение transform.position / transform.rotation нельзя. Это порождает проблемы. В любом случае это вам вылезет боком.
Все для чего нужно это — телепортация в другое место обьекта, но никак не его движения.
Пример правильной реализации движения:
( на примере обьекта-шара )
using UnityEngine; //эта строчка гарантирует что наш скрипт не завалится //ести на плеере будет отсутствовать компонент Rigidbody [RequireComponent(typeof(Rigidbody))] public class Movement : MonoBehaviour < public float Speed = 10f; public float JumpForce = 300f; //что бы эта переменная работала добавьте тэг "Ground" на вашу поверхность земли private bool _isGrounded; private Rigidbody _rb; void Start() < _rb = GetComponent(); > // обратите внимание что все действия с физикой // необходимо обрабатывать в FixedUpdate, а не в Update void FixedUpdate() < MovementLogic(); JumpLogic(); >private void MovementLogic() < float moveHorizontal = Input.GetAxis("Horizontal"); float moveVertical = Input.GetAxis("Vertical"); Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical); _rb.AddForce(movement * Speed); >private void JumpLogic() < if (Input.GetAxis("Jump") >0) < if (_isGrounded) < _rb.AddForce(Vector3.up * JumpForce); // Обратите внимание что я делаю на основе Vector3.up // а не на основе transform.up. Если персонаж упал или // если персонаж -- шар, то его личный "верх" может // любое направление. Влево, вправо, вниз. // Но нам нужен скачек только в абсолютный вверх, // потому и Vector3.up >> > void OnCollisionEnter(Collision collision) < IsGroundedUpate(collision, true); >void OnCollisionExit(Collision collision) < IsGroundedUpate(collision, false); >private void IsGroundedUpate(Collision collision, bool value) < if (collision.gameObject.tag == ("Ground")) < _isGrounded = value; >> >
Обратите внимание что за основу взят код из официальной документации/туториалов по юнити. Если есть несколько источников информации по какому-то мелкому но часто задаваемому вопросу (например движение персонажа ) — выбирайте официальную документацию! Там точно фигни не посоветуют,
в отличии от пестрящих дичью форумов, в т.ч. сервисе вопросов/ответов от юнити. Там в таких темах слишком часто пишут ответы те люди, которые понятия не имеют о правильном подходе.
Связанные с темой понятия:
Есть Update() — этот метод вызывается на каждой прорисовке кадра. Time.DeltaTime — это расчетное время между прорисовкой двух кадров. Если FPS проседает на компьютере, то этот параметр возрастает пропорционально проседанию.
Есть FixedUpdate() — это метод который вызывается при перепросчете физики. Time.FixedDeltaTime , как вы уже догадались, это время между вызовами FixedUpdate() . Оно может изменятся вручную через настройки, но упирается в физические возможности машины на которой игра будет запущена.
Если обьект не обладает физическими свойствами (не имеет RigidBody) эти параметры и методы можно использовать для НЕфизического передвижения.
Например поворот камеры.
Или крутящийся куб на небосводе.
Или движущийся изображение поезда где-то далеко, к которому нельзя подойти близко. Физика такому обьекту просто ни к чему — это просто лишняя трата ресурсов
но, даже, в этом случае предпочтительно использовать Transform.Translate , но про это позже
Мы не получим дергающуюся картинку при проседании кадров если сделаем НЕФИЗИЧЕСКОЕ движение правильно:
ВАЖНО: . ПРИМЕР ВРЕДНОГО КОДА. Не делайте так!
// код для Update() transform.position += transform.forward * speed * Time.DeltaTime; // или же код для FixedUpdate() transform.position += transform.forward * speed * Time.FixedDeltaTime;
мы присваиваем в новую позицию:
- старую позицию
- направление движения
- скорость передвижения умноженную на Time.deltaTime .
Поэтому, даже, если, у нас было 60 кадров и случилось проседание до 10 кадров — скорость вращения/движения обьекта не будет изменятся. Ведь, мы ее учитываем вместе с проседанием кадров.
Про физические свойства движения.
Допустим мы двигаем обьект через rb.Velocity или через AddForce() , то это физическое движение обьекта. То есть она может изменятся во времени сама под действием неких физических законов. Например, мы задумали сделать прыжок персонажа:
if (IsGrounded && Input.GetButtonDown("Jump"))
мы разово задаем вектор скачка. Только 1 долю секунды. Но он будет изменятся во времени автоматически равномерно уменьшаясь под силой тяжения. Пока не станет нулевым (верхняя точка прыжка), а потом не пойдет в минус по Y (падение), а потом не упадет на землю и не отскочит от нее (снова плюс по Y ) и так до полной остановки физической скорости обьекта на земле.
Допустим, мы двигаем изменением transform.Positon нашего плеера вперед по нажатию клавиши «пробел». В какой-то момент мы перестаем нажимать кнопку — движение резко остановится и замрет. Это потому, что наше движение НЕ является физическим. Допустим мы подойдем к стенке и попробуем пройти на нее. Т.к. мы занимаемся телепортацией обьекта, то наш персонаж сначала дойдет до стенки, а потом телепортируется ВНУТРЬ нее, после чего Collider ее вытолкнет из себя. Как глубоко телепортируется внутрь зависит лишь от того, на какое расстояние мы телепортируем нашего персонажа за кадр. То есть это «Bad Practice» так реализовать перемещение персонажа.
Но в то же время есть и допустимое не-физическое перемещение. Это использование метода Transform.Translate() . Это (вроде как) тоже телепортация, но с попыткой плавного нефизического перемещения обьекта. Но использование этого метода не освобождает нас от использования deltaTime/fixedDeltaTime, как в примере оф.документации.
( Снова таки — если на вашем обьекте есть RigidBody — наверняка нужно использовать физическое перемещение все равно! )
Если девайс с игрой сильно загружен, вызов методов Update() / FixedUpdate() тоже может просесть в скорости. И если в физике это учтено и без нас, то сейчас мы делаем НЕ физическое движение и именно по-этому это нужно учитывать добавлением даного множителя.
Но и без использования даного множителя у нас не появится проблем с провалами сквозь стены. Это просто фикс скорости.
Пример простой но хорошей НЕФИЗИЧЕСКОЙ реализации кода движения на примере персонажа.
Если в прошлом примере мы двигали шар, то было допустимо его толкать используя физ.модель. То есть мы использовали AddForce() для этих целей.
Допустим у нас персонаж — человек, а не шар. Давай создадим вместо человека его подобие — высокий куб 0.8х1.8х0.3 и попробуем нацепить на него наш скрипт движения шара. Выйдет следующее:
То есть когда мы пытаемся подвигать, наш персонаж падает (мы ж его толкаем, логично!). Когда он упал — он не может двигатся из-за силы трения. Зато мы можем двигать его в прыжке. 🙂
Давайте актуализируем этот код под даного персонажа. Мы заменим физический толчек обьекта на не-физическое, но ПЛАВНОЕ перемещение обьекта в пространстве:
using UnityEngine; //эта строчка гарантирует что наш скрипт не завалится ести на плеере будет отсутствовать компонент Rigidbody [RequireComponent(typeof(Rigidbody))] public class Movement : MonoBehaviour < // т.к. логика движения изменилась мы выставили меньшее и более стандартное значение public float Speed = 5f; public float JumpForce = 300f; //что бы эта переменная работала добавьте тэг "Ground" на вашу поверхность земли private bool _isGrounded; private Rigidbody _rb; void Start() < _rb = GetComponent(); > void FixedUpdate() < //обратите внимание что все действия с физикой //желательно делать в FixedUpdate, а не в Update JumpLogic(); // в даном случае допустимо использовать это здесь, но можно и в Update. // но раз уж вызываем здесь, то // двигать будем используя множитель fixedDeltaTimе MovementLogic(); >private void MovementLogic() < float moveHorizontal = Input.GetAxis("Horizontal"); float moveVertical = Input.GetAxis("Vertical"); Vector3 movement = new Vector3(moveHorizontal, 0.0f, moveVertical); // что бы скорость была стабильной в любом случае // и учитывая что мы вызываем из FixedUpdate мы умножаем на fixedDeltaTimе transform.Translate(movement * Speed * Time.fixedDeltaTime); >private void JumpLogic() < if (Input.GetAxis("Jump") >0) < if (_isGrounded) < // Обратите внимание что я делаю на основе Vector3.up а не на основе transform.up // если наш персонаж это шар -- его up может быть в том числе и вниз и влево и вправо. // Но нам нужен скачек только вверх! Потому и Vector3.up _rb.AddForce(Vector3.up * JumpForce); >> > void OnCollisionEnter(Collision collision) < IsGroundedUpate(collision, true); >void OnCollisionExit(Collision collision) < IsGroundedUpate(collision, false); >private void IsGroundedUpate(Collision collision, bool value) < if (collision.gameObject.tag == ("Ground")) < _isGrounded = value; >> >
С этим кодом мы получим такой результат:
С такой реализацией у нас не будет проблем вроде скачков скорости на проседании или повышении количества FPS, проваливаний, дерганости, прохождения сквозь стены или других неожиданностей.
Теперь мы можем занятся украшательствами — например повороты тела. Довольно приятно реализованы повороты вот здесь: Как сделать управление, как в игре «Overcooked»?
Так же можно добавить анимацию бега на нашего персонажа (ну если бы это был не куб).
Но как же реализация на физике?
Да, можно подобное реализовать и на физике.
Наша прошлая версия скрипта имела несколько недостатков. А именно:
- нужно было вручную отмечать каждый из предметов от которого мы можем прыгать. То есть добавив ящик на пол, нам нужно еще и его отметить тэгом Ground.
- если поставить кучу ящиков вертикально, присвоить каждому из них тэг «Ground», то просто подойдя к вертикальной стене из ящиков мы сможем взлететь вверх). То есть нам не важно к чему мы дотрагиваемся — к полу или к стене — оно давало нам возможность прыгать.
- наше движение все так же было НЕ физическим. То есть если мы начнем двигать игрока влево-вправо то он будет резко останавливатся а потом резко двигатся в противоположную сторону. В живом мире так не бывает.
Вспомните уроки физкультуры, когда нужно было пробежать 30 метров вперед, взять палочку, пробежать 30 метров назад, положить палочку и еще раз 30 метров в другую сторону. Что случалось с бегуном в этот момент если посмотреть сбоку? Сначала скорость растет, потом достигает пика, а потом торможение, взятие палочки, бег в другую сторону — снова возрастание скорости. Никаких резких скачков. Этого можно добится именно передвижением при помощи физики.
Давайте поместим на наш куб CapsuleCollider (минимальное торможение из-за силы трения) и заблочим в rigidBody rotateX и rotateZ (что б наш персонаж не падал на бок).
А потом нацепим на него вот этот скрипт:
using UnityEngine; //эти строчки гарантирют что наш скрипт не завалится если на плеере будет отсутствовать нужные компоненты [RequireComponent(typeof(Rigidbody))] [RequireComponent(typeof(CapsuleCollider))] public class Movement : MonoBehaviour < public float Speed = 0.3f; public float JumpForce = 1f; //даем возможность выбрать тэг пола. //так же убедитесь что ваш Player сам не относится к даному слою. //. Нацепите на него нестандартный Layer, например Player. public LayerMask GroundLayer = 1; // 1 == "Default" private Rigidbody _rb; private CapsuleCollider _collider; // теперь прийдется использовать CapsuleCollider //и удалите бокс коллайдер если он есть private bool _isGrounded < get < var bottomCenterPoint = new Vector3(_collider.bounds.center.x, _collider.bounds.min.y, _collider.bounds.center.z); //создаем невидимую физическую капсулу и проверяем не пересекает ли она обьект который относится к полу //_collider.bounds.size.x / 2 * 0.9f -- эта странная конструкция берет радиус обьекта. // был бы обязательно сферой -- брался бы радиус напрямую, а так пишем по-универсальнее return Physics.CheckCapsule(_collider.bounds.center, bottomCenterPoint, _collider.bounds.size.x / 2 * 0.9f, GroundLayer); // если можно будет прыгать в воздухе, то нужно будет изменить коэфициент 0.9 на меньший. >> private Vector3 _movementVector < get < var horizontal = Input.GetAxis("Horizontal"); var vertical = Input.GetAxis("Vertical"); return new Vector3(horizontal, 0.0f, vertical); >> void Start() < _rb = GetComponent(); _collider = GetComponent(); //т.к. нам не нужно что бы персонаж мог падать сам по-себе без нашего на то указания. //то нужно заблочить поворот по осях X и Z _rb.constraints = RigidbodyConstraints.FreezeRotationX | RigidbodyConstraints.FreezeRotationZ; // Защита от дурака if (GroundLayer == gameObject.layer) Debug.LogError("Player SortingLayer must be different from Ground SourtingLayer!"); > void FixedUpdate() < JumpLogic(); MoveLogic(); >private void MoveLogic() < // т.к. мы сейчас решили использовать физическое движение снова, // мы убрали и множитель Time.fixedDeltaTime _rb.AddForce(_movementVector * Speed, ForceMode.Impulse); >private void JumpLogic() < if (_isGrounded && (Input.GetAxis("Jump") >0)) < _rb.AddForce(Vector3.up * JumpForce, ForceMode.Impulse); >> >
Вы видите эту плавность, как будто человек бежит, останавливается, бежит в другую сторону? Красота!
А теперь вернитесь к прошлой гифке и присмотритесь. Движение совсем не такое 🙂 Там как буд-то рукой двигают шахматную фигуру по доске.
Ну и описанные выше баги поведения были пофикшены с такой реализацией.
Можно добавить еще физический материал нашему персонажу и откоректировать его поведение.
Вообще улучшать реализацию можно до бесконечности. Но, думаю, основные проблемы СПОСОБОВ ПЕРЕДВИЖЕНИЯ с которыми вы столкнетесь, я затронул 🙂
Оптимально использовать именно передвижение на базе физики.
Пытайтесь использовать исключительно физическое передвижение.
Реализация нестандартной физики движений.
Одним из моих любимейших примеров нестандартной физики движения является игра Ori and the Blind Forest
Такое перемещение/такие прыжки невозможно сделать на основе стандартной физики. Вероятнее всего, это делалось через физическое перемещение + костыли для получения нужных эфектов которые противоречат стандартной физике.
Сначала разрабатываются концепты движения. Они делаются в любом видеоредакторе с примитивными фигурами. Вот пример (если станет недоступным искать можно по Ori and the blind forest Enemy Concepts ) :
Обратите внимание на то, то здесь прорисовано не только перемещение обьекта, но и его вытягивания/сжатия. Изменения формы во время любого взаимодействия с внешним миром. В т.ч. выстрелы так же влияют на форму. А так же что указываются радиусы опознавания главного героя каждым отдельным врагом.
Костыли для каждого персонажа/врага свои собственные. Это делается что бы каждый из них обладал своей уникальной физикой. Сделать это на общей физике навряд ли возможно.
Движение реализовано «правильно» но предмет все равно пролетает сквозь стену
ДАЖЕ если вы реализовали физическое передвижение вашего персонажа, все равно может случится такое, что просчет CollisionDetect может проходить с ошибками. Такое бывает.
Для таких случаев есть настройки отвечающие за обработку CollisionDetect в настройках самого RigidBody.

Желательно такого не делать т.к. это негативно сказывается на производительности. Чем на большем количестве обьектов вы меняете эти настройки, тем более вероятно что вы делаете какую-то дичь, которую делать совсем не нужно. Считайте это спасательным кругом, а не панацеей. А если вы так будете делать, то рано или поздно вы прийдете на SO с вопросом почему игра тормозит, вас попросят показать код и ничего не найдут просто потому, что проблема тормозов не в коде. И намучаетесь вы с оптимизациями ой как сильно.
Так делать — не является ошибкой(!). Но чем меньше вы так будете делать — тем лучше. Подходите к изменению этих настроек с умом!
Информация для самостоятельного изучения:
- NavMesh
- Character Controller
- В вот этом видео (с которым я НАСТОЯТЕЛЬНО советую ознакомится) https://www.youtube.com/watch?v=puPjNRJMmOc рассказывается уже немного другие штуки. Если на примерах в которых я выше описывал наш персонаж двигался исключительно по ровной поверхности (т.е. могут возникнуть сложности на террейне или же на наклонных ровных проскостях), то здесь уже описано как персонаж должен двигаться по неровных поверхностях.
Основы создания 2D персонажа в Unity 3D 4.3. Часть 2: бегущий персонаж
Всем привет. Продолжаем дело, начатое в первой части. Сейчас у нас есть платформа и стоящий на ней персонаж с анимацией покоя. Настало время научить нашего персонажа бегать вправо-влево по платформе.
Загрузим сцену из первой части. Напомню, что в прошлый раз мы импортировали несколько спрайтов в папку Assets — Sprites. На всякий случай, внизу поста еще раз приведу ссылку на спрайты. Среди них должен быть спрайт под названием Run. Мы будем использовать его для создания анимации бега. Для этого нам надо проделать те же действия по превращению одиночного спрайта в коллекцию, как и при создании анимации покоя. Вкратце напомню: выделяем спрайт, в окне Inspector устанавливаем свойство Sprite Mode как Multiple, нажимаем ниже Sprite Editor, нарезаем изображение в режиме Grid или Automatic.

Теперь в окне Hierarchy выбираем Character и переходим в окно Animation. Нажимаем на поле с анимацией Idle и выбираем Create New Clip, чтобы создать анимацию бега. Сохраним файл анимации в папке Assets — Animations под именем Run.

Новая созданная анимация Run стала текущей в окне Animation. Разворачиваем спрайт Run в окне Project, выделяем все фалы Run_0… Run_9 и перетаскиваем в окно Animation. Установим пока значение Sample равное 24.

Все это мы уже делали в первой части, а теперь будет нечто новое. Перейдем в окно Animator. Сейчас там отображены три анимации: Any State, Idle и Run. Нам предстоит задать условия перехода из анимации Idle в анимацию Run, то есть из состояния покоя в состояние бега. В нижнем левом углу есть поле Parameters. Нажимаем на плюсик, выбираем Float и называем новый параметр как Speed. Тем самым мы создали параметр типа число с плавающей запятой, обозначающий скорость перемещения персонажа. Именно в зависимости от значения этого параметра будет происходить переключение из анимации покоя в анимацию бега. Теперь нажимаем правой кнопкой мыши на анимацию Idle, выбираем Make Transition и нажимаем левой кнопкой мыши на анимацию Run. Между анимациями появится линия со стрелкой. Передвиньте мышкой прямоугольники анимации, если плохо видно. Кликнем по линии со стрелкой. В окне Inspector отобразятся свойства перехода между анимациями. Обратим внимание на низ окна, в раздел Conditions. Кликнем на параметр Exit Time и поменяем его на Speed. Второе поле Greater оставим без изменений, а в третьем введем значение 0.01. Мы создали условие перехода из анимации покоя в анимацию бега — оно происходит, когда значение параметра скорости становится немногим больше нуля.

Теперь нужно сделать обратный переход — из Run в Idle. Делаем все с точностью наоборот: Make Transition от Run к Idle, выделяем переход, в Conditions устанавливаем Speed — Less — 0.01.

Теперь у нас есть две анимации и условия перехода между ними. Но пока ничего работать не будет, потому что все что мы сделали нужно «оживить» при помощи скрипта. Давайте перейдем в окно Project и создадим в папке Assets подпапку Scripts. Добавим в нее новый C# Script, назовем его CharacterControllerScript и откроем на редактирование.

Я приведу полный листинг скрипта с подробными комментариями, а ниже еще поясню, что в нем происходит.
using UnityEngine; using System.Collections; public class CharacterControllerScript : MonoBehaviour < //переменная для установки макс. скорости персонажа public float maxSpeed = 10f; //переменная для определения направления персонажа вправо/влево private bool isFacingRight = true; //ссылка на компонент анимаций private Animator anim; /// /// Начальная инициализация /// private void Start() < anim = GetComponent(); > /// /// Выполняем действия в методе FixedUpdate, т. к. в компоненте Animator персонажа /// выставлено значение Animate Physics = true и анимация синхронизируется с расчетами физики /// private void FixedUpdate() < //используем Input.GetAxis для оси Х. метод возвращает значение оси в пределах от -1 до 1. //при стандартных настройках проекта //-1 возвращается при нажатии на клавиатуре стрелки влево (или клавиши А), //1 возвращается при нажатии на клавиатуре стрелки вправо (или клавиши D) float move = Input.GetAxis("Horizontal"); //в компоненте анимаций изменяем значение параметра Speed на значение оси Х. //приэтом нам нужен модуль значения anim.SetFloat("Speed", Mathf.Abs(move)); //обращаемся к компоненту персонажа RigidBody2D. задаем ему скорость по оси Х, //равную значению оси Х умноженное на значение макс. скорости rigidbody2D.velocity = new Vector2(move * maxSpeed, rigidbody2D.velocity.y); //если нажали клавишу для перемещения вправо, а персонаж направлен влево if(move >0 && !isFacingRight) //отражаем персонажа вправо Flip(); //обратная ситуация. отражаем персонажа влево else if (move < 0 && isFacingRight) Flip(); >/// /// Метод для смены направления движения персонажа и его зеркального отражения /// private void Flip() < //меняем направление движения персонажа isFacingRight = !isFacingRight; //получаем размеры персонажа Vector3 theScale = transform.localScale; //зеркально отражаем персонажа по оси Х theScale.x *= -1; //задаем новый размер персонажа, равный старому, но зеркально отраженный transform.localScale = theScale; >>
Итак, мы завели несколько переменных: для задания максимальной скорости перемещения, для определения направления (вправо/влево) и для работы с компонентом Animator. Почти все действия происходят в методе FixedUpdate. В нем мы получаем значение оси Х, которое меняется при нажатии на клавиатуре клавиш влево-вправо или A-D (если не меняли соответствующие настройки проекта!). Затем устанавливаем это значение параметру Speed компонента Animator. Обратите внимание, что мы берем модуль этого значения при помощи метода Mathf.Abs, так как при создании условий перехода между анимациями покоя и бега мы сравниваем значение параметра с положительным числом 0.01. Нам здесь не важно, в какую сторону бежит персонаж. Важно лишь величина значения. Далее задаем скорость перемещения по оси Х в соответствии со значением максимальной скорости. И, наконец, проверяем, в какую сторону бежит персонаж, и в какую сторону он в этот момент повернут. Если он бежит вправо, а повернут влево — разворачиваем его вправо путем инвертирования его размера по оси Х. И наоборот. Этим нехитрым способом мы избавились от необходимости делать две анимации вместо одной: для бега вправо и для бега влево.
Сохраняем скрипт. В Unity перетаскиваем его на нашего Character в окне Hierarchy. Запускаем игру, нажимаем влево-вправо или A-D.

Капитан Коготь теперь умеет бегать! Скорость анимации получилась быстроватой. Ее можно снизить путем уменьшения значения Sample в окне Animation для анимации Run (значение 12 будет нормально). Если одновременно с игрой у вас видно окно Animator, то вы увидите, что во время покоя работает анимация Idle (бегает синий прогрессбар), а во время бега происходит переход на анимацию Run, и, соответственно, работает она.
На этом пока все. Нам осталось разобраться с прыжками… и узнать при этом еще несколько новых вещей!
Ссылка на спрайты.
Update: добавил видео результата.
Реализовать перемещение персонажа
Доброго времени!
Столкнулся с проблемой(прокрастинацией в коде!).
Пытаюсь реализовать перемещение персонажа и для разного типа перемещения использовать разную скорость.
Выглядит это примерно так.
if(Shift) скорость бега
else
if(Ctrl) скорость на корточках
else
if(Water) скорость плавания
else
скорость ходьбы
Я знаю о существовании паттернов программирования. Хотя и не понимаю с чем их едят. С помощью пространных пояснений от разных авторов понял, что мне надо что то вроде этих паттернов чтобы избавиться от ИФОВ.
Простое загугливание паттернов даёт слишком абстрактные знания. Или наоборот слишком частные случаи.
Вообще не понятно как их учить.
И какой выбрать для моего случая.
Буду рад любым советам.
Заранее спасибо!
Лучшие ответы ( 1 )
94731 / 64177 / 26122
Регистрация: 12.04.2006
Сообщений: 116,782
Ответы с готовыми решениями:
Перемещение персонажа
При перемещении персонажа вправо, перемещение происходит плавное. При перемещении персонажа влево.

Перемещение персонажа
При нажатие кнопок w или s персонаж двигается правильно(вперед или назад), но при нажатии a или d.
Перемещение персонажа
Здравствуйте. У меня есть объект, который движется из точки А в точке Б. Между этими точками(А, Б).

Непрерывное перемещение персонажа по Button UI
Здравствуйте, подскажите, пожалуйста, как сделать непрерывное перемещение персонажа пока нажата.
52 / 35 / 16
Регистрация: 03.08.2019
Сообщений: 389
персонаж постоянно в движении или движение происходит при нажатии на кнопки?
697 / 23 / 17
Регистрация: 30.07.2022
Сообщений: 1,069
Записей в блоге: 4
Fantom1987, при нажатии на кнопки. WASD + Mouse.
возможно мне подойдет паттерн состояния для переключения скорости.
Я нашел видео и понял как они создаются.
Но в конце автор показал дулю! Управляя переключениями между состояниями простыми if =(
52 / 35 / 16
Регистрация: 03.08.2019
Сообщений: 389
devillived, не понимаю зачем так заморачиваться. делайте так чтобы сами понимали, что написали. чем проще код тем лучше — имхо)
697 / 23 / 17
Регистрация: 30.07.2022
Сообщений: 1,069
Записей в блоге: 4
Fantom1987, проблема в том, что проще не работает.
и если я захочу добавить бег на корточках например. мне прийдется ТАКОЕ городить.
вот я и ищу способ простой реализации ветвления условий с возможностью расширения.
52 / 35 / 16
Регистрация: 03.08.2019
Сообщений: 389
писал под 2д, но возможно поможет в реализации
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
private bool ButtonLeft; private bool ButtonRight; private bool ButtonUp; private bool ButtonDown; private int Direction; // 2v private float MoveSpeed = 2; void GetInput() { ButtonLeft = Input.GetKey(KeyCode.LeftArrow) && !Input.GetKey(KeyCode.RightArrow) && !Input.GetKey(KeyCode.UpArrow) && !Input.GetKey(KeyCode.DownArrow); ButtonRight = !Input.GetKey(KeyCode.LeftArrow) && Input.GetKey(KeyCode.RightArrow) && !Input.GetKey(KeyCode.UpArrow) && !Input.GetKey(KeyCode.DownArrow); ButtonUp = !Input.GetKey(KeyCode.LeftArrow) && !Input.GetKey(KeyCode.RightArrow) && Input.GetKey(KeyCode.UpArrow) && !Input.GetKey(KeyCode.DownArrow); ButtonDown = !Input.GetKey(KeyCode.LeftArrow) && !Input.GetKey(KeyCode.RightArrow) && !Input.GetKey(KeyCode.UpArrow) && Input.GetKey(KeyCode.DownArrow); } void GetDirection() { if (ButtonLeft) Direction = 4; if (ButtonRight) Direction = 6; if (ButtonUp) Direction = 8; if (ButtonDown) Direction = 2; } private void Move() { switch (Direction) { case 2: transform.position = new Vector2(transform.position.x, transform.position.y - MoveSpeed * Time.deltaTime); break; case 4: transform.position = new Vector2(transform.position.x - MoveSpeed * Time.deltaTime, transform.position.y); break; case 6: transform.position = new Vector2(transform.position.x + MoveSpeed * Time.deltaTime, transform.position.y); break; case 8: transform.position = new Vector2(transform.position.x, transform.position.y + MoveSpeed * Time.deltaTime); break; } }
697 / 23 / 17
Регистрация: 30.07.2022
Сообщений: 1,069
Записей в блоге: 4
Fantom1987, Спасибо!
Я знаком с данной конструкцией.
Это по сути те же ифы, то лько завернутые в switch case.
Ну ладно пойду дальше книги по Unity читать. может там что то будет.
Всем спасибо! Всем удачи.
2633 / 1561 / 853
Регистрация: 23.02.2019
Сообщений: 3,876

Сообщение было отмечено devillived как решение
Решение
Сообщение от devillived 
и если я захочу добавить бег на корточках например. мне придется ТАКОЕ городить.
вот я и ищу способ простой реализации ветвления условий с возможностью расширения.
Тема конечно непростая и в двух словах объяснить сложно. Если попробовать использовать такую «машину состояний», то кода придётся писать много, но зато легче понимать происходящее, так как каждое состояние описано в отдельном, конкретном классе и сразу видно — что оно делает (это состояние). Но совсем не факт, что вам в итоге будет удобно пользоваться этим способом.
Допустим в следующем примере есть игрок. В Update этого игрока выполняется update его текущего состояния. То-есть состояния могут быть разные, и каждое описывает своё, конкретное поведение. Каждое состояние имеет ссылку на игрока, чтобы была возможно ему что-то сообщить, например дать команду «изменить состояние» или что-то другое.
Здесь у игрока 4 состояния (покой, ходьба, бег, плавание), изначально устанавливается состояния покоя.
— состояние покоя ловит нажатие клавиш движения, и если движение есть, то переходит в состояние ходьбы
— состояние ходьбы тоже ловит клавиши и двигает игрока, но если нажимается клавиша шифт, то переходит в состояние бега
— состояние бега так же ловит клавиши и увеличивает скорость игрока, а так же дополнительно крутит его, как только клавиши движения отжаты или отжат шифт, то состояние переходит в состояние ходьбы
— сам игрок (вне зависимости от состояния) ловит нахождения в триггере воды. И если попадает в него, то меняет своё состояние на состояние плавания. В котором добавляется возможность плавать не только влево-вправо, но и вверх-вниз.
Для наглядности добавлена смена цвета у игрока и надпись на ним с описанием текущего состояния
Сам код всего этого «добра» выглядит пугающе, но это потому что он сделан для примера, плюс всё в одном файле.
Кликните здесь для просмотра всего текста
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194
using System; using System.Collections.Generic; using System.Runtime.InteropServices; using UnityEngine; public class Player : MonoBehaviour { private IPlayerState _myState; private DictionaryType, IPlayerState> _states; private void Start() { _states = new DictionaryType, IPlayerState>(); _states.Add(typeof(IdleState), new IdleState(this)); _states.Add(typeof(WalkState), new WalkState(this)); _states.Add(typeof(RunState), new RunState(this)); _states.Add(typeof(SwimState), new SwimState(this)); SetStateIdleState>(); } private void Update() { _myState.Update(); } private void OnTriggerEnter2D(Collider2D collision) { if (collision.CompareTag("Water")) SetStateSwimState>(); } private void OnTriggerExit2D(Collider2D collision) { if (collision.CompareTag("Water")) SetStateIdleState>(); } private void OnDrawGizmos() { UnityEditor.Handles.Label(transform.position + Vector3.up, _myState?.ToString()); } public void SetSpriteColor(Color color) { GetComponentSpriteRenderer>().color = color; } public void SetStateT>() where T : IPlayerState { Type targetState = typeof(T); if (_states.ContainsKey(targetState)) { _myState?.EndState(); _myState = _states[targetState]; _myState.BeginState(); } } } public interface IPlayerState { void BeginState(); void EndState(); void Update(); } public class IdleState : IPlayerState { public readonly Player Player; public IdleState(Player player) { Player = player; } public void BeginState() { Player.SetSpriteColor(Color.grey); } public void EndState() { // empty } public void Update() { if (Input.GetAxis("Horizontal") != 0) Player.SetStateWalkState>(); } } public class WalkState : IPlayerState { public readonly Player Player; public float WalkSpeed = 1f; public WalkState(Player player) { Player = player; } public void BeginState() { Player.SetSpriteColor(Color.green); } public void EndState() { // empty } public void Update() { float moving = Input.GetAxis("Horizontal"); if (moving == 0) Player.SetStateIdleState>(); if (moving != 0 && Input.GetKey(KeyCode.LeftShift)) { Player.SetStateRunState>(); return; } Player.transform.Translate(Vector2.right * moving * WalkSpeed * Time.deltaTime); } } public class RunState : IPlayerState { public readonly Player Player; public float RunSpeed = 5; public RunState(Player player) { Player = player; } public void BeginState() { Player.SetSpriteColor(Color.red); } public void EndState() { Player.transform.rotation = Quaternion.identity; } public void Update() Player.transform.Translate(Vector2.right * moving * RunSpeed * Time.deltaTime, Space.World); Player.transform.Rotate(Vector3.forward, moving * -RunSpeed, Space.Self); } } public class SwimState : IPlayerState { public readonly Player Player; public float SwimSpeed = 0.5f; public SwimState(Player player) { Player = player; } public void BeginState() { Player.SetSpriteColor(Color.cyan); } public void EndState() { // empty } public void Update() { float movingX = Input.GetAxis("Horizontal"); float movingY = Input.GetAxis("Vertical"); Player.transform.Translate(new Vector2(movingX, movingY) * SwimSpeed * Time.deltaTime); } }
Ни в коем случае не принимайте это как панацею. Здесь лишь просто попытка объяснить мои понимания данного вопроса. Всё может выглядеть совершенно иначе, правильнее и гибче. Всё нужно подстраивать под свои конкретные задачи.
На всякий случай добавлю пакет со сценой, но не уверен, что он корректно экспортировался.
StatesExamplePackage.zip