Контроллер персонажа на Unity

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

Простой контроллер в 2 скрипта

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

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

Предлагаю минимальный набор скриптов и материалов для реализации перемещения, если нужно что-то более сложное, то придется доработать скрипты или можно у меня спросить, я помогу :)

Состав проекта

Состав проекта. Тут все понятно
Состав проекта. Тут все понятно

Аниматор

Аниматор состоит из двух BlendTree, одно для перемещения персонажа, а второе для поворотов на месте.

Аниматор состоит из двух BlendTree
Аниматор состоит из двух BlendTree

BlendTree для поворотов очень простой, состоит из двух анимаций. Одна для поворота направо, а вторая для поворота налево.

BlendTree для поворотов
BlendTree для поворотов

BlendTree для перемещения куда сложнее, он состоит из 9 анимаций, 8 отвечают за направление движения и 1 для неподвижного положения

BlendTree для перемещения. Все анимации зависят от двух чисел (от -1 до 1), Vert отвечает за перемещение вперед и назад, а Hor за перемещение вправо и влево
BlendTree для перемещения. Все анимации зависят от двух чисел (от -1 до 1), Vert отвечает за перемещение вперед и назад, а Hor за перемещение вправо и влево

Скрипты

Скрипты - самое главное. Я же программистом работаю :)

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

Не буду показывать здесь полностью весь код, покажу основные методы. Их всего 3.

Метод, отвечающий за перемещение персонажа вызывается каждый кадр, в него передаются координаты цели, вектор, содержащий инфу о том, куда идти персонажу (x - движение вправо или влево, 1 - вправо, -1 - влево, 0 - на месте; y - не используется, но можно использовать для переключения на ходьбу например; z - перемещение вперед или назад, 1 - вперед, -1 - назад, 0 - на месте) и время между кадрами.

public void Move(Vector3 targetTransPos, Vector3 moveAxis, float deltaTime) { //подстановка нужных данных в аниматор (скорость движения, поворота и т.п.) _anim.SetFloat(_inputParams.MoveSpeed, _moveSpeed); _anim.SetFloat(_inputParams.RotSpeed, _rotSpeed); _anim.SetFloat(_inputParams.Vert, moveAxis.z, 1f / _movingSens, deltaTime); _anim.SetFloat(_inputParams.Hor, moveAxis.x, 1f / _movingSens, deltaTime); //направление взгляда персонажа _anim.SetLookAtWeight(_inverseKinematicWeights.Weight, _inverseKinematicWeights.BodyWeight, _inverseKinematicWeights.HeadWeight, _inverseKinematicWeights.EyesWeight, _inverseKinematicWeights.ClampWeight); _targetTransPos = Vector3.Lerp(_targetTransPos, targetTransPos, _targetSens * deltaTime); _anim.SetLookAtPosition(_targetTransPos); //поворот тела персонажа, вызывается метод, отдельно отвечающий за это var isRot = _isSimpleRot ? false : Mathf.Abs(moveAxis.x) <= Mathf.Epsilon && Mathf.Abs(moveAxis.z) <= Mathf.Epsilon; Rot(_targetTransPos, isRot, Time.deltaTime); }

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

private void Rot(Vector3 targetTransPos, bool isRot, float deltaTime) { //запоминаем старый угол поворота персонажа var oldRot = _anim.transform.eulerAngles; //поворачиваем персонажа на цель (потом обратно вернем, тут нам нужны данные) _anim.transform.LookAt(targetTransPos); //находим угол между старым поворотом персонажа и новым, при котором персонаж развернут на цель var angleBetween = Mathf.DeltaAngle(_anim.transform.eulerAngles.y, oldRot.y); //выбираем направление поворота у персонажа _anim.SetFloat(_inputParams.Rot, (angleBetween < 0) ? 1 : -1, 1f / _movingSens, deltaTime); //убираем знак у угла поворота angleBetween = Mathf.Abs(angleBetween); //экономим на спичках и переиспользуем deltaTime, теперь это скорость разворота персонажа, когда он не стоит на месте deltaTime *= _movingSens; // если поворот по анимации не требуется, то плавно меняем старый угол персонажа if (!isRot) oldRot.y = Mathf.LerpAngle(oldRot.y, _anim.transform.eulerAngles.y, deltaTime); //если поворот по анимации нужен, проверяем не повернут ли уже персонаж на цель else if (angleBetween > _luft) _isRotation = true; //возвращаем персонажа к старому углу поворота _anim.transform.eulerAngles = oldRot; //если персонаж на цель повернут, то ничего больше не делаем if (!_isRotation) return; //проверяем достаточно ли повернулись на цель, если так, то прекращаем разворот if (angleBetween * Mathf.Deg2Rad <= deltaTime) _isRotation = isRot = false; /включаем или выключаем анимацию разворота в аниматоре _anim.SetBool(_inputParams.IsRot, isRot); }

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

public void SetInputs(float deltaTime, out Vector3 moveInput, out Vector3 targetPos) { //угол наклона камеры относительно персонажа var angle = (_bodyTrans.localEulerAngles.y - _camera.transform.localEulerAngles.y) * Mathf.Deg2Rad; //косинус этого угла var cosAngle = Mathf.Cos(angle); //синус этого угла var sinAngle = Mathf.Sin(angle); //вертикальный инпут игрока var vertInput = Input.GetAxis(_moveVertInput); //горизонтальный инпут игрока var horInput = Input.GetAxis(_moveHorInput); //высчитанные данные для аниматора игрока var hor = cosAngle * horInput - sinAngle * vertInput; var vert = cosAngle * vertInput + sinAngle * horInput; //подставляем полученные данные _moveInput.x = hor; _moveInput.y = 0; _moveInput.z = vert; //находим точку, куда наведена мышка и считаем ее целью if (Physics.Raycast(_camera.ScreenPointToRay(Input.mousePosition), out var hit)) _targetPos = hit.point; moveInput = _moveInput; targetPos = _targetPos; }

Префаб персонажа

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

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

Персонаж состоит из пары скриптов, аниматора и агента для поиска пути
Персонаж состоит из пары скриптов, аниматора и агента для поиска пути

Итог

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

Если такой формат кому-то интересен или что-то лучше поменять, то напишите в комментах - это позволит сделать более полезный контент в будущем, а если у вас есть какой-то конкретный вопрос, то пишите тоже, постараюсь разобрать :)

106106
66 комментариев

Отличный материал, особенно круто построчное комментирование — более чем полезно начинающим. Хвалю за вложенный труд!,)

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

10

Спасибо, могу показать как научить ботов так же чётко ходить по навмешу) 

5

Зачем комментировать каждую строчку?

2

Это только в статье, чтобы проще разобраться было

12

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

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

В общем видимо не такой уж и Умный человек, раз никогда ничего не делал и не учил других - не понимает зачем это.

Отличный материал
Спасибо за труд!

3