1 Востаннє редагувалося FakiNyan (16.10.2016 17:54:00)

Тема: Кастомна фізика персонажа Unity3d

Прів.
Надумав був гру зробити, в котрій ми управляємо колобком. Я планую зробити її красивою та динамічною, аби було багацько екшону, гра буде нагадувати консольну гру Sonic.
Вбудована в Unity фізика мені не підійшла, тому що в моєму світі будуть свої закони фізики.

Озьдо деяка інфа про те, як має рухатись персонаж:

1. Швидко рухатись по всіляким горбам та ямам
2. Гнучке налаштування інерції, персонаж повинен вміти швидко змінювати напрямок руху, але в деяких моментах він буде буксувати
3. Гнучке налаштування моментів, в котрих персонаж відривається від землі. Наприклад, коли на великій швидкості забігає на гірку.
4. Персонаж повинен мати змогу рухатись по вертикальним стінам, та навіть стелям. Наприклад, коли забігає на петлю, ну як в Sonic'у.
5. Ще щось, чого я не знаю.

Поки що я більш-менш зробив наступне:

1. Персонаж не відривається від землі коли на великій швидкості забігає на гірку
2. Швидкість лінійно змінюється. Спочатку персонаж має швидкість n, а через секунду вона досягає максимуму на поточній "поверхні"
  a) на пласкій землі вона трошки збільшується на одну секунду
  б) коли персонаж забігає на гірку, то швидкість починає зменшуватись
  в) коли персонаж біжить з гірки, то швидкість збільшується

http://не-дійсний-домен/rKIHU/05f38d6762.gif

Оце поки що все, що я зробив.


Але в мене виникла проблема з:

1. Коректністю швидкості (при переході з пласкої поверхні на гірку - швидкістю підскакує)
2. Положенням персонажа на гірках (деяка частина персонажа провалюється в гірку)
3. Рухом по вертикальним стінам (воно просто нікуди не їде, або проходить через стіну)
4. Великими швидкостями (проходить через стіни/гірки)

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


Зараз алгоритм працює так:

1. Обираємо напрямок в котрому рухається персонаж (право, чи ліво)
2. Множимо вектор напрямку на змінну швидкості
3. Додаємо отриманий раніше вектор до позиції персонажа
4. З отриманої позиції пускаємо прямо вниз промінь
5. Беремо точку, в котрій промінь потрапив в якусь поверхню
6. Додаємо до отриманої раніше точки радіус персонажа (ну воно ж коло) та отримуємо нову позицію для персонажа
7. За допомогою нормалі до площини, в котру потрапив проміню, обраховуємо нахил поверхні
8. За допомогою нахилу поверхні та часу руху персонажа після останної зупинки обраховуємо нову швидкість

http://не-дійсний-домен/rKMt7/27310f91f5.png


В кого які ідеї, як покращити цей алгоритм?

2

Re: Кастомна фізика персонажа Unity3d

Зараз у вас стабільна горизонтальна швидкість персонажу, але вертикальна швидкість ніяк не обмежена, звідси стрибки швидкості. Пропоную:
- обчислювати положення персонажу не по центру, а по точці, де він стоїть;
- положення цієї точки зсувати на певну відстань не по вісі x, а по поверхні (тобто якщо там схил, то рух по x сповільнюється, а по y зростає; шкільна задача "обчислити довжину кривої на малюнку").

3

Re: Кастомна фізика персонажа Unity3d

Прихований текст

шкільна задача "обчислити довжину кривої на малюнку"

http://не-дійсний-домен/rKTdl/868a7919ee.png
*THUMBSUP*

4

Re: Кастомна фізика персонажа Unity3d

Ця крива, якщо я про ту ж криву, що й ви, називається хордою. Її довжина обчислюється набагато легше.

5

Re: Кастомна фізика персонажа Unity3d

гей-гей, досить балакати про криві.
В мене немає простої кривої, котра б представляла поверхню. В мене є набір колайдерів, в котрі я можу пускати промені

6

Re: Кастомна фізика персонажа Unity3d

А чому б Вам замість того, щоб пускати перпендикуляр униз, не опустити перпендикуляр до поверхні по нормалі?

Подякували: FakiNyan1

7

Re: Кастомна фізика персонажа Unity3d

Думаю, вартувало б алгоритм змінити на наступний.
1. Опустити перпендикуляр по нормалі до найближчої поверхні. Якщо таких точок декілька, вибрати крайню залежно від вибору напрямку. Вибрати напрямок в котрому рухається персонаж відносно поверхні (право від нормалі, чи ліво від нормалі)
2. Множимо вектор напрямку на змінну швидкості
3. Додаємо отриманий раніше вектор до позиції персонажа
4. З отриманої позиції пускаємо  перпендикуляр по нормалі до найближчої поверхні.
5. Беремо точку, в котрій промінь потрапив в якусь поверхню
6. Додаємо до отриманої раніше точки радіус персонажа (ну воно ж коло) по нормалі та отримуємо нову позицію для персонажа

Подякували: FakiNyan1

8

Re: Кастомна фізика персонажа Unity3d

Patlatus написав:

Думаю, вартувало б алгоритм змінити на наступний.
1. Опустити перпендикуляр по нормалі до найближчої поверхні. Якщо таких точок декілька, вибрати крайню залежно від вибору напрямку. Вибрати напрямок в котрому рухається персонаж відносно поверхні (право від нормалі, чи ліво від нормалі)
2. Множимо вектор напрямку на змінну швидкості
3. Додаємо отриманий раніше вектор до позиції персонажа
4. З отриманої позиції пускаємо  перпендикуляр по нормалі до найближчої поверхні.
5. Беремо точку, в котрій промінь потрапив в якусь поверхню
6. Додаємо до отриманої раніше точки радіус персонажа (ну воно ж коло) по нормалі та отримуємо нову позицію для персонажа

ну це я вчора теж таке придумав, тепер персонаж не втоплюється в землю на схилах, але тут є проблемка з лівостороннім рухом

9 Востаннє редагувалося FakiNyan (18.10.2016 16:58:44)

Re: Кастомна фізика персонажа Unity3d

озьдо шлях персонажа
початок червоної лінії - це позиція персонажа
кінець червоної лінії - точка, котру ми отримали, коли помножили вектор напрямку на швидкість
зелена лінія - це отой перпендикуляр до поверхні

як бачите, коли градус стає більше 90, то перпендикуляр вже не перпендикуляр

Прихований текст

http://не-дійсний-домен/rNcPJ/20f1a0dc6f.png
http://не-дійсний-домен/rNcWe/0deeea2cd2.png

і коли рухається назад, то така ж фігня

Прихований текст

http://не-дійсний-домен/rNcY4/537c09875a.png

10

Re: Кастомна фізика персонажа Unity3d

Може якісь проблеми реалізації?

Ви точно так реалізували?

Patlatus написав:
Думаю, вартувало б алгоритм змінити на наступний.
1. Опустити перпендикуляр по нормалі до найближчої поверхні. Якщо таких точок декілька, вибрати крайню залежно від вибору напрямку. Вибрати напрямок в котрому рухається персонаж відносно поверхні (право від нормалі, чи ліво від нормалі)
2. Множимо вектор напрямку на змінну швидкості
3. Додаємо отриманий раніше вектор до позиції персонажа
4. З отриманої позиції пускаємо  перпендикуляр по нормалі до найближчої поверхні.
5. Беремо точку, в котрій промінь потрапив в якусь поверхню
6. Додаємо до отриманої раніше точки радіус персонажа (ну воно ж коло) по нормалі та отримуємо нову позицію для персонажа

Бо тоді картинка мала б бути інакшою, наскільки я собі уявляю

11

Re: Кастомна фізика персонажа Unity3d

ну так ,це проблема реалізації, воно ж навіть не їде вліво. Тут біда з кутами. Вся ця штука робить лише в першій чверті http://не-дійсний-домен/rNhti/23c0e21155.png
тобто, коли персонаж дивиться вправо і кут його погляду в межах Pi/2

12

Re: Кастомна фізика персонажа Unity3d

озьдо вам скрипт

Прихований текст
using UnityEngine;
using UnityEngine.UI;

public class Controller : MonoBehaviour
{
    public LayerMask mask = -1;
    public float minSpeed = 3, maxSpeed = 25, maxFlatSpeed = 15, climbSpeedLimit = 5, climpSpeedAdd = 20, currentSpeed = 0, currentSpeedLimit = 0;
    public Rigidbody2D ball; //це об'єкт, що представляє персонажа
    public float maxSlopeAngle = 70;
    public Color lowSpeedColor, highSpeedColor;
    public Image filler, speedPanel;
    public float lastAngle, lastAddSpeed;
    public float realSpeed, maxRealSpeed;
    public StateOfSurface stateOfSurface, oldStateOfSurf;
    public float oldSpeed;

    private CircleCollider2D circle; //ця штука теж відноситься до персонажа, але це просто коло, котре потрібно, аби дістати з нього радіус
    private Vector2 oldPos;
    private Vector2 currentDirection, oldDirection;
    private float startAccelTime = 0;
    private float realCircleRadius;

    private float timeOfAcceleration
    {
        get
        {
            //Debug.Log("start accel time: " + startAccelTime + "  Time.time: " + Time.time);
            return Time.time - startAccelTime;
        }
    }

    // Use this for initialization
    void Start()
    {
        circle = ball.GetComponent<CircleCollider2D>();
        oldPos = ball.position;
        currentSpeed = minSpeed;
        maxRealSpeed = maxSpeed * Time.deltaTime;
        realCircleRadius = circle.radius * ball.transform.localScale.x; //радіус радіусом, але ж ми ще можемо збільшувати об'єкт за допомогою масштабування, і при цьому всі колайдери, що на ньому висять, теж масштабуються
        oldDirection = Vector2.right;
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKeyDown(KeyCode.LeftArrow) || Input.GetKeyDown(KeyCode.RightArrow))
        {
            startAccelTime = Time.time;
        }

        if (Input.GetKey(KeyCode.LeftArrow) && Input.GetKey(KeyCode.RightArrow))
        {
            currentSpeed = 0;
        }

        if (Input.GetKey(KeyCode.RightArrow))
        {
            currentDirection = Vector2.right;
            CheckAndSetNewPos(); //оце основний метод
        }
        else
        if (Input.GetKey(KeyCode.LeftArrow))
        {
            currentDirection = Vector2.left;
            CheckAndSetNewPos();
        }
        if (Input.GetKeyUp(KeyCode.RightArrow) || Input.GetKeyUp(KeyCode.LeftArrow))
        {
            currentSpeed = minSpeed;
            oldSpeed = currentSpeed;
        }
        if (Input.GetKey(KeyCode.LeftArrow) && Input.GetKey(KeyCode.RightArrow))
        {
            currentSpeed = 0;
        }

        realSpeed = (ball.position - oldPos).magnitude;
        oldPos = ball.position;

        filler.fillAmount = realSpeed / (maxSpeed * Time.deltaTime);
        filler.color = Color.Lerp(lowSpeedColor, highSpeedColor, filler.fillAmount);
        speedPanel.color = filler.color;
    }

    void CheckAndSetNewPos() // тут вся фігня обраховується
    {
        float angle = 0;

        if (currentDirection != oldDirection) // якщо ми змінили напрямок руху (по замовчування він == Вправо)
        {
            Vector3 eulerAngles = ball.transform.localRotation.eulerAngles; // дістаємо інфу про поворот персонажа
            eulerAngles.y += 180; // повертаємо його на 180 градусів, аби він дививися в потрібний бік
            ball.transform.localRotation = Quaternion.Euler(eulerAngles); // назначаємо новий поворот

            oldDirection = currentDirection;
        }

        RaycastHit2D hit = Physics2D.Raycast(ball.position, Vector2.down, 10); // пускаємо промінь з центру персонажа вниз на 10 юнітів
        Vector2 tempDirection = currentDirection;
        if (hit) //якщо промінь вдарився
        {
            angle = Vector2.Angle(hit.normal, currentDirection) - 90; // обраховуємо нахил поверхні
            angle = GetActualAngle(angle); // деяка корекція кута, зараз це повертає той самий кут
            ball.MoveRotation(angle); // повертаємо персонажа на потрібний кут
            tempDirection = ball.transform.right; // оце я хз шо таке, але що з ним, що без нього - не робе
        }


        Vector2 newPos = ball.position + (tempDirection * currentSpeed * Time.deltaTime); // обраховуємо нову позицію, з котрої буде пускатись промінь

        Debug.DrawLine(ball.position, newPos, Color.red, 1);
        Debug.Log("ball pos: " + ball.position + "  new pos: " + newPos + "  ball forward: " + ball.transform.forward + "  current speed: " + currentSpeed);


        hit = Physics2D.Raycast(newPos, -ball.transform.up, 10, mask); //пускаємо промінь з нової позицію вниз (відносно повороту персонажа) на 10 метрів. Це оцей ваш промінь, що перпендикулярний до поверхні
        if (hit)
        {
            Debug.DrawLine(newPos, hit.point, Color.green, 1);
        }
        else
        {
            Debug.DrawLine(newPos, newPos - (Vector2)ball.transform.up * 10, Color.green, 1);
        }

        if (hit) //якщо промінь врізався кудись
        {
            angle = Vector2.Angle(hit.normal, currentDirection) - 90; //магія

            ball.MoveRotation(GetActualAngle(angle)); // повертаємо персонажа на кут, що дорівнює нахилу поверхні

            oldStateOfSurf = stateOfSurface;
            if (angle == 0)
            {
                stateOfSurface = StateOfSurface.Flat;
                currentSpeedLimit = maxFlatSpeed;
            }
            else if (angle > 0)
            {
                stateOfSurface = StateOfSurface.SlopeUp;
                currentSpeedLimit = climbSpeedLimit;
            }
            else
            {
                stateOfSurface = StateOfSurface.SlopeDown;
                currentSpeedLimit = climpSpeedAdd;
            }
            if (oldStateOfSurf != stateOfSurface)
            {
                startAccelTime = Time.time;
                oldSpeed = currentSpeed;
            }

            currentSpeed = LinearAcceleration(oldSpeed, currentSpeedLimit, 1); // змінюємо швидкість

            lastAngle = angle;

            newPos = hit.point + (Vector2)(ball.transform.up * realCircleRadius); // встановлюємо персонажа в нову позицію (це воно перпендикулярно до поверхні)
            ball.position = newPos;
        }
    }

    float GetActualAngle(float angle)
    {
        /*if (currentDirection == Vector2.left)
            return angle * -1 + 180;*/
        return angle;
    }

    RaycastHit2D CircleCast(Vector2 pos)
    {
        return Physics2D.CircleCast(pos, realCircleRadius, Vector2.down, 10, mask);
    }

    float LinearAcceleration(float from, float to, float time)
    {
        return from + (Mathf.Min(timeOfAcceleration, time) / time) * (to - from);
    }

    float EaseIn(float from, float to, float value)
    {
        return Mathf.Sin((from / to) * (Mathf.PI / 2f) + Mathf.PI * 1.5f) * value + value;
    }
}

public enum StateOfSurface
{
    Flat,
    SlopeUp,
    SlopeDown
}

13

Re: Кастомна фізика персонажа Unity3d

Ви десь рахуєте арктангенс вручну? Типова ситуація для таких випадків.
Мається на увазі ситуація на малюнку
Вручну - використовувати atan від частки замість atan2

14

Re: Кастомна фізика персонажа Unity3d

quez написав:

Ви десь рахуєте арктангенс вручну? Типова ситуація для таких випадків.
Мається на увазі ситуація на малюнку
Вручну - використовувати atan від частки замість atan2

вручну не рахую, там є метод, котрий повертає кут в градусах, а приймає два вектора

15

Re: Кастомна фізика персонажа Unity3d

озьдо бачу, коли кут стає більше 90, то він починає рости в іншу сторону, тобто далі йде не 100, а 80

Прихований текст

http://не-дійсний-домен/rNn7Q/13e36887fa.png

16

Re: Кастомна фізика персонажа Unity3d

отаке виходе

Прихований текст

http://не-дійсний-домен/rNp6A/be17ea43aa.png

тобто, якщо Y нормалі до поверхні від'ємний, то мені тре зробити 180+(поточний кут - 90)

17

Re: Кастомна фізика персонажа Unity3d

quez написав:
 angle = Vector2.Angle(hit.normal, currentDirection) - 90; // обраховуємо нахил поверхні

Шось непоняв. Шо ви тут рахуєте, коли нахил поверхні — hit.normal ± 90

ну з самого початку я рахував кут, на котрий треба повернути персонажа, аби на всяких схилах він дивився в правильну сторону. Типу, якщо від збирається на схил, то має дивитись трохи вверх, і це трохи залежить від нахилу того схила.
По суті це кут між напрямком руху (вправо, чи вліво) і нормалю до поверхні. На рівній поверхні воно завжди повертало 90, тому що напрямок руху (1; 0), а нормаль (0; 1), тому я віднімаю 90

18

Re: Кастомна фізика персонажа Unity3d

ой, переплутав, мені треба було 90+(90-поточний кут)
http://не-дійсний-домен/rNq1q/c58d3a9d97.gif
але от зі швидкістю проблемка, вона має набиратись, коли шарик вниз зпускається

19

Re: Кастомна фізика персонажа Unity3d

FakiNyan написав:
quez написав:
 angle = Vector2.Angle(hit.normal, currentDirection) - 90; // обраховуємо нахил поверхні

Шось непоняв. Шо ви тут рахуєте, коли нахил поверхні — hit.normal ± 90

ну з самого початку я рахував кут, на котрий треба повернути персонажа, аби на всяких схилах він дивився в правильну сторону. Типу, якщо від збирається на схил, то має дивитись трохи вверх, і це трохи залежить від нахилу того схила.
По суті це кут між напрямком руху (вправо, чи вліво) і нормалю до поверхні. На рівній поверхні воно завжди повертало 90, тому що напрямок руху (1; 0), а нормаль (0; 1), тому я віднімаю 90

Вже зрозумів  :)

20

Re: Кастомна фізика персонажа Unity3d

Допоможіть мені розбити один отой скрипт на організовану та малозв'язану купку класів.

Зараз я виділив декілька "дій", з котрих складається рух.

1. Швидкість
2. Поворот
3. Переміщення
4. Контроль (натиснення на кніпочки)

Створив такі класи:

1. CircleInput - оброблює натиснення кніпочок
2. CircleRotation : IRotation - обраховує нахил
3. CircleSpeed : ISpeed - калькулює швидкість
4. CircleMove : IMove - рухає персонажа

public interface IRotation<T> where T : Collider2D
{
    float GetAngle(T collider);
}

public interface IMove
{
    void Move(float angle, float speed);
}

public interface ISpeed
{
    float GetSpeed(float angle);
}

ну ви зрозуміли.

Поки що все починається в CircleInput, і виглядає от так

using UnityEngine;

[RequireComponent(typeof(IMove))]
[RequireComponent(typeof(IRotation<CircleCollider2D>))]
[RequireComponent(typeof(ISpeed))]
[RequireComponent(typeof(CircleCollider2D))]
public class CircleInput : MonoBehaviour
{
    IMove iMove;
    IRotation<CircleCollider2D> iRotation;
    ISpeed iSpeed;
    CircleCollider2D circleCollider;

    // Use this for initialization
    void Start()
    {
        iMove = GetComponent<IMove>();
        iSpeed = GetComponent<ISpeed>();
        iRotation = GetComponent<IRotation<CircleCollider2D>>();
        circleCollider = GetComponent<CircleCollider2D>();
    }

    // Update is called once per frame
    void Update()
    {
        if (Input.GetKey(KeyCode.RightArrow))
        {
            Move(Vector2.right);
        }

        if (Input.GetKey(KeyCode.LeftArrow))
        {
            Move(Vector2.left);
        }
    }

    void Move(Vector2 direction)
    {
        float angle = iRotation.GetAngle(circleCollider);
        float speed = iSpeed.GetSpeed(angle);
        iMove.Move(angle, speed);
    }
}

Але на етапі імплементації IRotation виникла проблема.
В оригінальному скрипті частина алгоритма працює так

1. Пускаємо промінь вниз
2. Якщо промінь вдаряється в поверхню, то обраховуємо нахил поверхні
3. За допомогою швидкості та вектора паралельному поверхні обраховуємо точку в просторі
4. З обрахованої точки пускаємо промінь перпендикулярний до поверхні
5. Від точки, в котрій промінь вдарився об поверхню, віднімаємо радіус персонажа, що помножений на нормалізований промінь та ставимо персонажа в цю точку

картинка

http://не-дійсний-домен/s0sZj/71afaaf12b.png

Ну ви пойняли.

Так от. Проблема виникла в тому, що спочатку я розглядав IRotation як засіб отримання будь-якої інфи, що стосується поворотів. І я хтів зробити там лише один метод, котрий відповідає за отримання одного кута. І тоді, для керування персонажем мені потрібно було б викликати методи відповідних персонажів один раз, і все було б чудово в тому сенці, що не було б каші. Була б чітка послідовність  CircleInput -> IRotation -> ISpeed -> IMove.
Але через те, що в мене замір нахилу проводиться два рази, виходить щось типу
CircleInput -> IRotation -> ISpeed -> IRotation -> IMove.

Що робити?