Bounce project: шаг 1 (мяч, его движение, второй закон Ньютона)

Published
Published from UnityTensor Games » Bounce project: шаг 1 (мяч, его движение, второй закон Ньютона)

Принцип написания кода: от плохого к приемлемому, чтобы вам было понятно, как это работает
Буду признателен отзывам / критике и в принципе обратной связи, чтобы понимать: а нужно ли это здесь?
Без метровых вступлений опишу задачу на этом этапе/шаге:
  • Нужно добавить базовую физику нашему персонажу и примитивные обьекты для проверки коллизий
  • Добавим движение на клавишах: влево вправо и прыжок
  • Оптимизируем это все

1. Добавляем физику

Здесь все просто, через "Add Component" добавляем circleCollider2D и rigidbody2D
Должно быть так в действии:

2. Движение

Пока сделаем базовое движение с "кривым и нерегулируемым прыжком"
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    private Rigidbody2D playerRb;
    private float speedRatio = 100; // регулируем для умеренно быстрого ускорения

    private void Start()
    {
        playerRb = this.GetComponent<Rigidbody2D>();
    }

    private void Update()
    {
        Vector2 resultSpeed = Vector2.zero;

        if (Input.GetKey(KeyCode.A))
            resultSpeed += Vector2.left; 

        if (Input.GetKey(KeyCode.D))
            resultSpeed += Vector2.right;

        if (Input.GetKey(KeyCode.W) 
            || Input.GetKey(KeyCode.Space))
            resultSpeed += Vector2.up;

        playerRb.AddForce(resultSpeed * speedRatio); // применяем результирующий вектор
    }
}
Результат:
Теперь мы делаем прыжок. Здесь будет чуть сложнее:
Для начала нужно определить момент приземления мяча. Если мяч на земле то разрешим ему совершить прыжок. Прыжок не должен происходить с ускореним, а именно: сделали импульс и все - запретили дальше прыгать.
Рассчитаем позицию точки, где будем проверять приземление мяча через:
    private void OnDrawGizmos()
    {
        Gizmos.DrawLine(transform.position, (Vector2)transform.position + Vector2.down / 3);
        // идем вниз по оси Y на 0.33 (я подобрал таким образом - радиус окружности коллайдера мяча + 0.03). Это значение в следующем коде будет напрямую зависеть от радиуса мяча и размера Scale, 
        // поэтому не зацикливаемся на реализации и идем дальше
    }
Сделаем мяч алым и протестируем:
Результат пока устраивает. Сделаем отлов приземления через Raycast? IsTouching? Physics2D.OverlapCircleAll. Почему через окружность? Бывают ситуации, когда мяч приземляется не строго по центру на край блока, ближе к боковой части - таким образом мяч приземлится, но ничего в коде не выявится.
Перепишем отладку:
Добавим коллайдер в код для получения радиуса мяча
    private void OnDrawGizmos()
    {
        Vector2 globalPosition = (Vector2)transform.position + (Vector2.down * playerCl.radius * transform.lossyScale.y); // позиция игрока + его физ. радиус

        Gizmos.DrawLine(transform.position, globalPosition);
        Gizmos.DrawWireSphere(globalPosition, (playerCl.radius * transform.lossyScale.y) / 3);
        // overlap будем делать по трети его физ. радиуса
    }
Так как наш игрок круглый, то физ. радиус считаем по оси ординат, предполагая, что его Scale по двум осям меняется пропорционально
Очевидно это все делается это для того, чтобы при увеличении радиуса коллайдера или scale'а объекта радиус Overlap'a в свое время также увеличивался или уменьшался
Overlap создает коллекцию игровых объектов, которые оказались в поле зрения заданной нами окружности. Мы все сделали правильно и все что нам остается - найти объект, на который приземлился мяч. Проблема заключается в том, что могут в коллекции оказаться лишние объекты и даже сам игрок. Тогда нужен идентификатор, что это "земля, на которую приземлился мяч". Это решается добавлением тэга "ground" к блокам и поиску по этому тэгу в коллекции.
Теперь наш некрасивый код выглядит так:
using System.Linq;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    private Rigidbody2D playerRb;
    private CircleCollider2D playerCl;
    private float speedRatio = 100;
    private bool isLanded;

    private void Start()
    {
        playerRb = this.GetComponent<Rigidbody2D>();
        playerCl = this.GetComponent<CircleCollider2D>();
    }

    private void Update()
    {
        Vector2 resultSpeed = Vector2.zero;
        Vector2 groundHittingPoint = (Vector2)transform.position
            + (Vector2.down * playerCl.radius * transform.lossyScale.y);

        float overlapRadius = playerCl.radius * transform.lossyScale.y / 3;

        isLanded = Physics2D.OverlapCircleAll(groundHittingPoint, overlapRadius).Any(x => x.tag.Equals("ground"));

        if (Input.GetKey(KeyCode.A))
            resultSpeed += Vector2.left;

        if (Input.GetKey(KeyCode.D))
            resultSpeed += Vector2.right;

        if (isLanded && (Input.GetKey(KeyCode.W)
            || Input.GetKey(KeyCode.Space)))
            playerRb.velocity = new Vector2(playerRb.velocity.x, 5);

        playerRb.AddForce(resultSpeed * speedRatio, ForceMode2D.Force);
    }
Реализация вышеприведенного кода приведена ниже:
Возникает несколько проблемных ситуаций:
  • Мы зависим от DeltaTime. При слабой машине мяч будет двигаться очень медленно, а при мощной он будет мгновенно улетать в разные стороны (хотя второе фиксится ограничением кадров в настройках юнити)
  • Силы по X и Y зависимы, а мы бы хотели например ускорять мяч по X в зависимости от массы, а по Y прыгать с одинаковой скоростью и в то же время не дергать AddForce Impulse/Force, которые в свою очередь связаны с проблемой 1.
Теперь приступим к самому интересному:

3. Оптимизация

using System.Linq;
using UnityEngine;

public class PlayerMovement : MonoBehaviour
{
    private Rigidbody2D playerRb;
    private CircleCollider2D playerCl;

    private bool isLanded;

    private const float FIXED_SECOND = 1.0f;
    private const string GROUND_TAG = "ground";

    private void Start()
    {
        playerRb = this.GetComponent<Rigidbody2D>();
        playerCl = this.GetComponent<CircleCollider2D>();
    }

    private void Update()
    {
        Vector2 resultVector = Vector2.zero;
        Vector2 groundHittingPoint = (Vector2)transform.position
            + (Vector2.down * playerCl.radius * transform.lossyScale.y);

        float overlapRadius = playerCl.radius * transform.lossyScale.y / 3;

        isLanded = Physics2D.OverlapCircleAll(groundHittingPoint, overlapRadius).Any(x => x.tag.Equals(GROUND_TAG));


        /*
        https://ru.wikipedia.org/wiki/Второй_закон_Ньютона
        F = m*a,
        a = dv / dt,
        dv = (v - v0),
        a = (v - v0) / dt, 
        dt = (t - t0),
        a = (v - v0) / (t - t0),
        a = (v - 0) / (t - 0),
        a = v/t,
        F = m*(v/t)
        v = F/mt
        */

        resultVector = AddForceX() / (playerRb.mass * FIXED_SECOND); 
        // FIXED_SECOND можно заменить на Time.DeltaTime, но тогда скорость мяча будет зависеть от FPS

        resultVector += AddForceY(); 
        // прикладываем константную силу по Y отдельно, потому что не хотим, чтобы прыжок зависел от массы мяча
        // потом мб поменяем

        playerRb.velocity += resultVector;
    }

    private Vector2 AddForceX()
    {
        Vector2 force = Vector2.zero;

        if (Input.GetKey(KeyCode.A))
            force += Vector2.left;

        if (Input.GetKey(KeyCode.D))
            force += Vector2.right;

        return force;
    }

    private Vector2 AddForceY()
    {
        Vector2 force = Vector2.zero;

        if (isLanded && (Input.GetKey(KeyCode.W)
            || Input.GetKey(KeyCode.Space)))
            force += Vector2.up * 2;
        // прыжок сделаем вдвое выше

        return force;
    }
}
Добавляем PhysicsMaterial2d с коэффициентом Bounciness = 0.25, применяем к коллайдеру мяча через перетаскивание и радуемся конечному результату.
Да данном шаге мы все сделали. Следующий шаг: 2, как ни странно. Понравилось? Напиши об этом


Views: 141

ledoed #1 - 3 weeks ago 1
Голосов: +1 / -0
зачем использовать твердое тело и двигать его через velocity(его масса будет бесконечной при встрече с другим тв. телом),если использовать физику то двигать через силы,а так для колизии есть character controller.Проверка того что он на земле,превращает его в скалолаза,так как боковая стена считается за приземление.Почему на слабых машинах будет некорректно работать deltatime,за 1 сек обьект примерно пройдет одно и тоже расстояние что на слабых и на сильных машинах,просто на слабых его скорость будет больше.Использование physMaterial + rigitbody.velocity сомнительная вещь.
Msey #2 - 3 weeks ago (изм. ) 0
Голосов: +0 / -0
Проверка того что он на земле,превращает его в скалолаза,так как боковая стена считается за приземление
Нет, не будет (см. статью, пока не увидишь почему)
если использовать физику то двигать через силы
Я с твоей стороны не вижу никакой аргументации за это, поэтому скажу лишь, что пожалуйста, двигай. По факту у меня тот же addforce, но без учета времени
Почему на слабых машинах будет некорректно работать deltatime
Она всегда корректно работает. Разница будет во времени между кадрами, которые в свою очередь зависят от нагрузки процессора / видеокарты / движка юнити
на слабых его скорость будет больше
Скорость будет ниже, так как она напрямую зависит от времени между кадрами
Использование physMaterial + rigitbody.velocity сомнительная вещь
аргументы
ledoed #3 - 3 weeks ago (изм. ) 0
Голосов: +0 / -0
ну использование ригид озночает что в игре будут столкновения,ну вот шарик ударит обьект "n" массой по оси x а velocity ему покажет палец,ну использование танка по воробьям я конечно не осуждаю ну в реал игре будут же тоже ригид обьекты,если используйте физику то используйте её.Так вопрос про physmaterial а если платформа будет наклонной на 45C,то обьект ускориться если я при этом жму кнопку по оси x,опять вернулись к 1 ответу вы используете физику с прямым воздействием на физ тело.Вопрос про дельту я наверно не понел что вы имеете в виду,но будет выдавать 1 при любом фпс
пс. а если нужен обьект с 0 массой
 float t = 1f;
    float value = 0;
    float speed = 1f;
    bool b = true;

    // Start is called before the first frame update
    void Start()
    {


    }



    private void Update()
    {
        value += speed * Time.deltaTime;
        t -= Time.deltaTime;

        if (t <= 0f)
        {
            if (b)
            {
                Get();
                b = false;
            }
            
        }
    }

    private void Get()
    {
        Debug.Log(value);
    }
Msey #4 - 3 weeks ago (изм. ) 0
Голосов: +0 / -0
ну использование ригид озночает что в игре будут столкновения,ну вот шарик ударит обьект "n" массой по оси x а velocity ему покажет палец
Ну почему же покажет. Силы наложатся друг на друга. Мы же не используем стороннюю физику. Мы разложили существующую и добавили свое.
С углом 45 то же самое.
upd: я понял, что имеется ввиду. Если бы мы заменяли скорость на ту, что мы посчитали, то да - было бы плохо, но мы там их складываем, как это происходит в addforce, поэтому вcе учитывается
С дельтой намудрил, но не суть. Идея была в том, чтобы показать, как это работает изнутри, но согласен, что многое тут излишне
Прикрепленные файлы