@FaulerAffe
-

Оптимально ли я сделал управление в 2D платформере?

Я начал изучать Unity, хотелось бы сделать управление таким, каким оно является в хороших платформерах. Сейчас я достиг неплохого результата, но есть вещи, которые мне не очень нравятся. Хотелось бы получить советы, правильно ли я вообще всё делаю или нет.
Основа управления простая:
void Start()
    {
        rb = gameObject.GetComponent<Rigidbody2D>();
    }

    private void ReactControlls()
    {
        if (Input.GetKey(KeyCode.D))
        {
           transform.Translate(new Vector2(Speed, 0) * Time.deltaTime);
        }
        else if (Input.GetKey(KeyCode.A))
        {
            transform.Translate(new Vector2(-Speed, 0) * Time.deltaTime);
        }
        }
        if (Input.GetKeyDown(KeyCode.W))
        {
            if (IsGrounded)
            {
                rb.AddForce(new Vector2(0, JumpForce));
            }
        }
    }

    private void Checkers()
    {
        IsGrounded = Physics2D.OverlapArea(GroundCheck.bounds.min, GroundCheck.bounds.max, GroundLayers);
    }

    void Update()
    {
        Checkers();
        ReactControlls();
    }


GroundCheck – пустой объект, на который повесили BoxCollider2D и сделали триггером. Этот объект является дочерним для главного героя, находится у него в ногах.
На данном этапе возникают следующие проблемы:
1)персонаж не поворачивается, когда бегает в разные стороны;
2)если упереться в стену, то персонаж будет дёргаться, потому что ему удаётся немного проникнуть в коллайдер стены, после чего его оттуда выталкивает.
Пофиксим это:
void Start()
    {
        rb = gameObject.GetComponent<Rigidbody2D>();
        sr = gameObject.GetComponent<SpriteRenderer>();
        WayIsFreeR = true;
        WayIsFreeL = true;
    }

    private void ReactControlls()
    {
        if (Input.GetKey(KeyCode.D))
        {
            if (WayIsFreeR)
            {
                transform.Translate(new Vector2(Speed, 0) * Time.deltaTime);
            }
        }
        else if (Input.GetKey(KeyCode.A))
        {
            if (WayIsFreeL)
            {
                transform.Translate(new Vector2(-Speed, 0) * Time.deltaTime);
            }
        }
        if (Input.GetKeyDown(KeyCode.W))
        {
            if (IsGrounded)
            {
                rb.AddForce(new Vector2(0, JumpForce));
            }
        }
    }

    private void Checkers()
    {
        IsGrounded = Physics2D.OverlapArea(GroundCheck.bounds.min, GroundCheck.bounds.max, GroundLayers);
        WayIsFreeR = !Physics2D.OverlapArea(FaceR.bounds.min, FaceR.bounds.max, FaceLayers);
        WayIsFreeL = !Physics2D.OverlapArea(FaceL.bounds.min, FaceL.bounds.max, FaceLayers);
    }

    private void Animations()
    {
        float direction = Input.GetAxisRaw("Horizontal") * Time.deltaTime;
        if (direction < 0)
        {
            sr.flipX = true;
        }
        else if (direction > 0)
        {
            sr.flipX = false;
        }
    }

    // Update is called once per frame
    void Update()
    {
        Checkers();
        Animations();
        ReactControlls();
    }


Объекты WayIsFreeR и WayIsFreeL – пустые объекты, на которые навесили BoxCollider2D и сделали триггером. Представляют они из себя тонкие прямоугольники, имеющие практически такую же высоту, какую имеет главный персонаж. Они являются дочерними объектами для главного персонажа и размещаются вплотную справа и слева от него. Выглядит это как-то так:
639cd5ba9da01283880782.png
Тут посередине находится основной коллайдер персонажа, справа от него тонкий WayIsFreeR , слева WayIsFreeL а снизу – GroundCheck.
Идея следующая: как только WayIsFreeR пересекает какой-то коллайдер, персонаж тупо больше не может идти вправо (аналогично для WayIsFreeL). Возникает первая проблема – да, герой больше не пытается воткнуться в стену, но в разное время расстояние, на котором он останавливается от стены, немного разное. Допустим мы передвигаемся по X от координаты 0, каждый кадр наша координата увеличивается на 20. Длина буфера, который сигнализирует нам о том, что справа преграда и дальше двигаться нельзя, равна 7. Преграда начинается с координаты 105. Вот мы попали в аналогичную ситуацию, как мне кажется – на прошлом кадре мы были на координате 80, всё было хорошо и мы шли дальше. На этом кадре мы на координате 100, дальше идти не можем. При этом расстояние до преграды меньше буфера.
Я нашёл лишь одно возможное решение этой проблемы: подогнать скорость персонажа так, чтобы мы не могли проскочить в этот "буфер" в принципе (объекты нужно будет размещать тоже в определённых точках). Но это, как мне кажется, ооооочень затратно по времени.

Собственно, вопрос можно сформулировать следующим образом: целесообразно ли всё делать через коллайдеры, как это сделал я. Если да, то как можно пофиксить мою проблему. Если нет, то как сделать иначе (хотя бы в какую сторону смотреть).
  • Вопрос задан
  • 83 просмотра
Решения вопроса 1
@hermer29
Прежде всего большинство Ваших задач здесь может решать Rigidbody2D. Для понимания, Rigidbody - это система которая реализует физические явления в движке и она подходит для практически любых ситуаций - если станете большим специалистом то может быть найдете решение получше. Обратите внимание, что движение, ускорение свободного падения и столкновение это физические процессы. Для начинающего лучший курс по физике в Unity.

Вы можете реализовать прыжок, коллизию (столкновения) и движение через Rigidbody.

[RequireComponent(typeof(Rigidbody2D), typeof(SpriteRenderer))]
public class Player : MonoBehaviour
{
    [SerializeField, Min(1)] private float _speed = 1;
    [SerializeField, Min(1)] private float _jumpForce = 1;
    [SerializeField] private LayerMask _ground;
    
    private Rigidbody2D _body;
    private SpriteRenderer _player;

    private void Start()
    {
        _body = GetComponent<Rigidbody2D>();
        _player = GetComponent<SpriteRenderer>();
    }
    
    private void Update()
    {
        var horizontal = GetHorizontalInput();
        var jump = Input.GetKey(KeyCode.W);

        if(jump && IsGrounded())
            Jump();
        
        Move(horizontal);
        Animate();
    }

    private float GetHorizontalInput()
    {
        return Input.GetAxis("Horizontal");
    }

    private bool IsGrounded()
    {
        return Physics2D.OverlapArea(
            GroundCheck.bounds.min, 
            GroundCheck.bounds.max, 
            _ground);
    }

    private void Move(float input)
    {
        var deltaDistance = Time.deltaTime * _speed;
        var deltaPosition = transform.position.x + (deltaDistance * input);
        _body.MoveDirection(new Vector2(deltaPosition, 0));
    }

    private void Jump()
    {
        _body.AddForce(Vector3.up * _jumpForce)
    }

    private void Animate()
    {
        var velocity = _body.velocity.normalized;
        if (velocity.x < 0)
        {
            sr.flipX = true;
        }
        else if (velocity.x > 0)
        {
            sr.flipX = false;
        }
    }
}


В чём разница между Rigidbody.MovePosition и Transform.Translate? Rigidbody управляет физическими явлениями, т.е. трение с поверхностями (когда мы движемся мы трёмся коллайдером), столкновениями и физикой во время прыжка. Когда мы движемся в препятствие и движемся с помощью Rigidbody.MovePosition физический движок замечает, что мы двигаемся в препятствие ну и симулирует реакцию поверхности, ну и мы не проваливаемся в коллайдер за счёт этого. Transform.Translate напрямую меняет позицию объекта, ему довольно безразлична физика и столкновения, он просто меняет позицию, и как вы могли заметить, он может изменить позицию прямо в препятствие.
Резюмируя: Transform.Translate хорош когда нас не волнуют столкновения: UI-анимации, движение облаков, перемещение препятствий на которые ничто не влияет физически. В остальных случаях Rigidbody.MovePosition.

Остальные детали,
* Если используем GetComponent значит надо гарантировать наличие компонента через RequireComponent. Когда компонентов становится больше, а ты такие вещи не предусмотрел однажды ты можешь словить 100+ Null reference exception в консоль
* Параметрам можно установить минимальные возможные значения через атрибут Min, таким образом случайно нельзя будет допустить неверное поведение
* Стирай изначальные комментарии и явно пиши private на методах, таким образом читается быстрее, лишнее внимание переводить не придётся
* gameObject.GetComponent не обязательно, можно просто GetComponent
* Вместо Update уместнее FixedUpdate. Разница в том, что FixedUpdate ВСЕГДА вызывается каждый определенный промежуток времени, в отличие от Update. В курсе наверху описывается почему так лучше, и разница между FixedUpdate и Update.
* Проверять нахождение на земле где-то читал предлагали с помощью рейкаста, стоит помнить что любой физический метод имеет 2D версию, которая тебе лучше подходит
* Научись придерживаться одного и того же стиля во всём своём коде
* Метод должен быть глаголом или словосочетанием с глаголом
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы