Jump to content

Как создать автомобиль


photo

Recommended Posts

Привет народ. Изучаю как в этом движке создать автомобиль. Из-за очень слабой документации  возможно эта тема будет попадаться в топе поисковиков (как это иногда у меня происходит на форумах других движках), поэтому я приведу тут все ссылки которые я нашел на документацию в которых описывается как создать авто.

Перво наперво сам видеоролик, в который вроде как обозначен в качестве туториала, но на самом деле просто показывают что есть такой функционал. Тем не менее объясняют для чего нужны джоинты типа Wheel и Suspension. Видео в спойлере.

Spoiler

 

Затем когда вы из него ничего не поймете, скорее всего наберете поисковый запрос "unigine car" или же "unigine vehicles". И перейдете вот на эту страничку:

City Traffic > Cars - https://developer.unigine.com/en/docs/2.6/code/uniginescript/scripts/traffic/cars/?rlang=cpp или же https://developer.unigine.com/ru/docs/1.0/scripting/scripts/traffic/cars?rlang=cpp

ВНИМАНИЕ! https://developer.unigine.com/ru/docs/2.6/code/uniginescript/scripts/traffic/cars/ и https://developer.unigine.com/ru/docs/1.0/scripting/scripts/traffic/cars?rlang=cpp - РАЗНЫЕ СТРАНИЦЫ!

Становится лучше, но не на много. Далее может попасться страничка https://developer.unigine.com/ru/docs/2.10/code/usage/vehicle_physics/index. Если у вас нет подписки за 6000 бакинских, она для вас бесполезна.

И наконец после всего выше перечисленного вы наткнетесь на https://developer.unigine.com/ru/docs/2.7/code/usage/car_suspension_joints/index

Теперь очень тонкий момент - вам необходимо выбрать актуальную для вас версию. В данном случае страница открывается для версии SDK 2.7 - практически весь приведенный код не актуален для последних версий! У меня версия 2.11, для которой страничка еще не обновлена. Что с этими скриптами делать, как запускать проект, нафига вообще создать через код автомобиль - совершенно не понятно.

Сами джоинты - https://developer.unigine.com/ru/docs/2.7/principles/physics/joints/?rlang=cpp#suspension

 

Дополнено от 14.06.2020

Хм, невероятная оперативность разработчиков в решении вопроса. Такого я еще не видел. Вышел стрим под названием UNIGINE Livestream: Making a C# Car Project  где разработчик рассказывает как сделать автомобиль непосредственно в редакторе. Так что очевидно, я просто тут сделаю некий конспект того видео со своими наблюдениями (конечно если разработчики и тут меня не опередят).

 

Дополнено от 16.06.2020

Алгоритм создания автомобиля. 

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

Внимание! Алгоритм верен для Уазика из бесплатного плагина! Настоятельно рекомендую проходить шаги именно на нем, чтобы можно было без проблем сопоставлять то что изложено и то что на практике.

Внимание! Правые колеса Уазика имеют неверную ориентацию из-за чего всегда поворачиваются на 180 на дефолтную ротацию при просчете физики. Решается это просто — дочерите к примитиву колесо, а сам примитив в качестве колеса прикуручиваете к кузову. В видео этот вопрос решается через сетки модели, что по мне, очень грубое решение.

От себя хочу сказать, что это вообще очень хорошая практика игровые объекты на сцене дочерить либо к пустышкам (ПКМ в World Nodes > Node > NodeDummy), либо к примитивам если первое не возможно. Ваш игровой объект (обычно обозначен как Game Object, сокращенно GO, в Unigine это Node) должен иметь набор свойств и визуализация объекта, это так же одно из его свойств. Так же в его свойствах может быть звук, партиклы, скрипты и т.д. И уже этот игровой объект который обладает всеми необходимыми свойствами, вы сохраняете как node и в дальнейшем используете на сцене.

Сам алгоритм. Еще раз ссылка на видео UNIGINE Livestream: Making a C# Car Project

  1. Самое важное. У всех объектов на сцене (в Unigine это World что по мне слишком глобально для обозначения конкретной локальной области. Так что я буду использовать слово сцена чтобы обозначить место действия) по которым планируется езда, жмякаем collision. На дефолтной сцене это ground_plane.

  2. Берем уазик кидаем на сцену, вытаскиваем из него колеса.

  3. На него цепляем коллайдер и ригидбоди. Ставим массу в Shapes 100 (Вообще все это странно. В поле Physics объекта в итоге целых три строчки с массой.)

  4. На колеса так же кидаем коллайдер и ригид боди

  5. Когда вы кинете на колеса коллайдер, то он будет ориентрован не так как колеса. Подбираем его ротацию и радиус. В моем случае радиус был 0.38 (на видео 0.40).

  6. А вот теперь внимание. Кидаем на колеса wheel из кузова автомобиля, а не из самого колеса. Т.е. выделяем кузов уазика, в поле Physics в Joints жмякаем add, в открывшемся окошке выбираем нужное колесо, где находятся (!!!) ВСЕ объекты с физикой (опупеть блин. Даже поисковой строки нет). Интересно, что если вы будете это делать из колес, то автомобиль будет работать несколько иначе, хотя казалось бы.

  7. Жмякаем Fix 0 на каждом колесе.

  8. Теперь очень не очевидный момент — надо изменить Wheel Radius и он должен соответствовать радиусу коллайдера который вы только что настроили. Не очевидно это по тому что во-первых по факту это дублирующие друг друга величины, во-вторых, по умолчанию визуально Wheel Radius не отображается.

  9. Добавляем массу колесам в 25. Внимание! Физические величины из реального мира не подходят для применения в движке! Все высчитывается методом научного тыка, так чтобы поведение автомобиля казалось естественным. Это нормальная практика, просто начальные значения все же лучше брать из реальности чтобы можно было от чего то отталкиваться. Однако всегда следует помнить, что конечные значения могут значительно отличаться от начальных и пугаться этого не нужно.

  10. Ограничиваем ход колес путем установки значений в Linear From/To: -0,1/0,1

  11. Теперь настраиваем жесткость пружин/рессор подвески. Идем в Linear Spiring, ставим 100

  12. Затем настраиваем амортизаторы — Lianer Damping. Ставим 40

  13. Снижаем шаг итераций просчета в Iterations до 10. Соответственно чем больше значение, тем меньше шаг, тем чаще обрабатывается физика колес, тем больше нагрузка на железо. Такие вот парадоксы.

  14. Теперь автомобиль ведет себя более менее предсказуемо, так что можно регулируя его настройки добиться максимально естественного поведения.

На форуме Unity по русски я писал общее про автомобиль Wheelcollider - как его использовать. Это для общего развития - там я объясняю основные принципы поведения автомобиля.

 Дополнение от 21.06.2020

Решил максимально полно предоставить информацию об автомобиле в рамках движка, так что скрипт из видео

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

Раз уж скрипт-туториал имеет углы Аккермана, то сразу же сюда записываем минус - нет регулятора тормозных усилий между передней и задней осями автомобиля, который в простонародье называется "колдун". Вообще с усилиями что-то странное, ведущая передняя ось, тормозные усилия на заднюю. Ну да ладно, главное что обозначено как это делать.

Что еще. Подвеска тут как и везде - ходит она по идеально вертикальной траектории, что далеко не реализм для не зависимых подвесок по типу Макферсон и тем более зависимых. Следует это учитывать. В случае если у вашего автомобиля  везде продольные рычаги, как например на задней подвеске Ситроена С5 первого поколения, или же независимая рессорная, то конечно это на поведении автомобиля ни как не скажется (Я правда таких еще пока не встречал, но мы же тем и занимаемся что создаем то чего нет). Зато не надо делать развал колес. 

Кастор так же  судя по всему выставлять не придется :-).

Скрипт в спойлере.

Spoiler
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
 
[Component(PropertyGuid = "Ваш уникальный номер скрипта")]
public class Car : Component
{
    public float acceleration = 50.0f;
    public float turn_speed = 25.0f;
    public float max_velocity = 90.0f;
    public float max_turn_angle = 30.0f;
    public float default_torque = 5.0f;
    public float car_base = 3.0f;
    public float car_width = 2.0f;
 
    public Node wheel_bl = null;
    public Node wheel_br = null;
    public Node wheel_fl = null;
    public Node wheel_fr = null;
 
    public Player player = null;
 
    private JointWheel joint_wheel_bl = null;
    private JointWheel joint_wheel_br = null;
    private JointWheel joint_wheel_fl = null;
    private JointWheel joint_wheel_fr = null;
 
    private Controls controls = null;
 
    private float current_velocity = 0.0f;
    private float current_torque = 0.0f;
    private float current_turn_angle = 0.0f;
    
 
    private void Init()
    {
        // write here code to be called on component initialization
        if(wheel_bl)
            joint_wheel_bl = wheel_bl.ObjectBody.GetJoint(0as JointWheel;
 
        if(wheel_br)
            joint_wheel_br = wheel_br.ObjectBody.GetJoint(0as JointWheel;
 
        if(wheel_fl)
            joint_wheel_fl = wheel_fl.ObjectBody.GetJoint(0as JointWheel;
 
        if(wheel_fr)
            joint_wheel_fr = wheel_fr.ObjectBody.GetJoint(0as JointWheel;
            
        if(player)
            controls = player.Controls;
 
    }
    
    private void Update()
    {
        float deltaTime = Game.IFps;
        current_torque = 0.0f;
 
        if(controls)
        {
            bool is_moving_foward = controls.GetState(Controls.STATE_FORWARD) != 0;
            bool is_moving_backward = controls.GetState(Controls.STATE_BACKWARD) != 0;
            bool is_moving_right = controls.GetState(Controls.STATE_MOVE_RIGHT) != 0;
            bool is_moving_left = controls.GetState(Controls.STATE_MOVE_LEFT) != 0;
            bool is_handbreak = controls.GetState(Controls.STATE_USE) != 0;
            
 
            if(is_moving_foward)
            {
                current_torque = default_torque;
                current_velocity = MathLib.Max(current_velocity0.0f);
                current_velocity += deltaTime * acceleration;
            }
            else if(is_moving_backward)
            {
                current_torque = default_torque;
                current_velocity = MathLib.Min(current_velocity0.0f);
                current_velocity -= deltaTime * acceleration;
            }
            else
            {
                current_velocity*= MathLib.Exp(-deltaTime);
            }
 
            if(is_moving_left)
            {
                current_turn_angle += deltaTime * turn_speed;
            }
            else if(is_moving_right)
            {
                current_turn_angle -= deltaTime * turn_speed;
            }
            else
            {
                if(MathLib.Abs(current_turn_angle) < 0.25f)
                    current_turn_angle = 0.0f;
 
                current_turn_angle -= MathLib.Sign(current_turn_angle) * turn_speed * deltaTime;
            }
 
            if(is_handbreak)
            {
                //joint_wheel_fl.AngularDamping = 10000.0f;
                //joint_wheel_fr.AngularDamping = 10000.0f;
                joint_wheel_bl.AngularDamping = 10000.0f;
                joint_wheel_br.AngularDamping = 10000.0f;
            }
            else
            {
                //joint_wheel_fl.AngularDamping = 0.0f;
                //joint_wheel_fr.AngularDamping = 0.0f;
                joint_wheel_bl.AngularDamping = 0.0f;
                joint_wheel_br.AngularDamping = 0.0f;
            }
        }
 
        current_velocity = MathLib.Clamp(current_velocity, -max_velocitymax_velocity);
        current_turn_angle = MathLib.Clamp(current_turn_angle, -max_turn_anglemax_turn_angle);
 
        float angle_0 = current_turn_angle;
        float angle_1 = current_turn_angle;
 
        if(MathLib.Abs(current_turn_angle) > MathLib.EPSILON)
        {
            float radius = car_base / MathLib.Tan(current_turn_angle * MathLib.DEG2RAD);
            float radius_0 = radius - car_width * 0.5f;
            float radius_1 = radius + car_width * 0.5f;
 
            angle_0 = MathLib.Atan(car_base / radius_0) * MathLib.RAD2DEG;
            angle_1 = MathLib.Atan(car_base / radius_1) * MathLib.RAD2DEG;
        }
 
        joint_wheel_fl.Axis10 = MathLib.RotateZ(angle_1).GetColumn3(0);
        joint_wheel_fr.Axis10 = MathLib.RotateZ(angle_0).GetColumn3(0);
    }
 
    private void UpdatePhysics()
    {
        joint_wheel_fl.AngularVelocity = current_velocity;
        joint_wheel_fr.AngularVelocity = current_velocity;      
        
        joint_wheel_fl.AngularTorque = current_torque;
        joint_wheel_fr.AngularTorque = current_torque;
    }
}

 

Дополнение от 28.06.2020

Итак, докончил я ковырять документацию и собственно рабочая базовая машинка из Юнити.

Позволил себе немного модифицировать скрипт от разработчика, так что теперь там массив колес. С углами Аккермана я пока не заморачивался, однако скорее всего это будет похоже на вот это -    Расчет упреждения для тела летящего по параболе    (ВНИМАНИЕ! Переходя по ссылке ты попадешь на крайне простую и критически сложную к пониманию тему. Прошло 5 лет, но я до сих пор получаю на нее "опровежения". Если вдруг ты чувствуешь пригорание в области попы, крайнюю степень возмущенности и желание во чтобы то не стало раздавить автора своим авторитетом - не трать время.). Короче говоря обычная триангуляция.

Что еще. Поскольку для меня движок новый и я еще не знаю все его особенности, то мог несколько не оптимально собрать код и провести аналоги функционала. Например получаю доступ к скрипту Car я через поле свойств скрипта в Node Components. Это не очень изящно, поскольку скрипт CarAudio работает только со скриптом Car который висит на том же объекте. Ну и т.п.

Итак, скрипты. Два скрипта Car и CarAudio должны висеть где угодно, но лучше конечно на самом автомобиле.

Далее в массиве wheelParametr определяем количество колес и закидываем их туда. Настраиваем.

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

Затем, в иерархии автомобиля создаем 4 SoundSource, включаем ему Loop. В первые два закидываем звук двигателя на низких оборотах (m_LowAccel и m_LowDecel), затем высоких (m_HighAccel) и в последний странный звук который назван как DecelerationHigh. Соответственно в CarAudio закидываем эти SoundSource в соответствующие поля.

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

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

Позже добавлю частицы и звук визга резины - тут просто надо уже получше понять как работает движок.

Скрипты в спойлере:

Spoiler
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
 
[Component(PropertyGuid = "Ваш ID  скрипта")]
public class CarAudio : Component
{
 
    // This script reads some of the car's current properties and plays sounds accordingly.
    // The engine sound can be a simple single clip which is looped and pitched, or it
    // can be a crossfaded blend of four clips which represent the timbre of the engine
    // at different RPM and Throttle state.
 
    // the engine clips should all be a steady pitch, not rising or falling.
 
    // when using four channel engine crossfading, the four clips should be:
    // lowAccelClip : The engine at low revs, with throttle open (i.e. begining acceleration at very low speed)
    // highAccelClip : Thenengine at high revs, with throttle open (i.e. accelerating, but almost at max speed)
    // lowDecelClip : The engine at low revs, with throttle at minimum (i.e. idling or engine-braking at very low speed)
    // highDecelClip : Thenengine at high revs, with throttle at minimum (i.e. engine-braking at very high speed)
 
    // For proper crossfading, the clips pitches should all match, with an octave offset between low and high.
 
    public enum EngineAudioOptions // Options for the engine audio
    {
        Simple// Simple style audio
        FourChannel // four Channel audio
    }
 
    public EngineAudioOptions engineSoundStyle = EngineAudioOptions.FourChannel;// Set the default audio options to be four channel
    //public Sound lowAccelClip;                                              // Audio clip for low acceleration
    //public Sound lowDecelClip;                                              // Audio clip for low deceleration
    //public Sound highAccelClip;                                             // Audio clip for high acceleration
    //public Sound highDecelClip;                                             // Audio clip for high deceleration
    public float pitchMultiplier = 1f;                                          // Used for altering the pitch of audio clips
    public float lowPitchMin = 1f;                                              // The lowest possible pitch for the low sounds
    public float lowPitchMax = 6f;                                              // The highest possible pitch for the low sounds
    public float highPitchMultiplier = 0.25f;                                   // Used for altering the pitch of high sounds
    public float maxRolloffDistance = 500;                                      // The maximum distance where rollof starts to take place
    public float dopplerLevel = 1;                                              // The mount of doppler effect used in the audio
    public bool useDoppler = true;                                              // Toggle for using doppler
 
    public SoundSource m_LowAccel// Source for the low acceleration sounds
    public SoundSource m_LowDecel// Source for the low deceleration sounds
    public SoundSource m_HighAccel// Source for the high acceleration sounds
    public SoundSource m_HighDecel// Source for the high deceleration sounds
    private bool m_StartedSound// flag for knowing if we have started sounds
    public Car m_CarController// Reference to car we are controlling
 
    private void StartSound()
    {
        // get the carcontroller ( this will not be null as we have require component)
        //m_CarController = GetComponent<CarController>();
 
        // setup the simple audio source
        //m_HighAccel = SetUpEngineAudioSource(highAccelClip);
        //m_HighAccel.SampleName = highAccelClip;
 
        // if we have four channel audio setup the four audio sources
        /*
        if (engineSoundStyle == EngineAudioOptions.FourChannel)
        {
            m_LowAccel = SetUpEngineAudioSource(lowAccelClip);
            m_LowDecel = SetUpEngineAudioSource(lowDecelClip);
            m_HighDecel = SetUpEngineAudioSource(highDecelClip);
        }
*/
        // flag that we have started the sounds playing
        m_StartedSound = true;
    }
 
    private void StopSound()
    {
        /*
        //Destroy all audio sources on this object:
        foreach (var source in GetComponents<AudioSource>())
        {
            Destroy(source);
        }
        */
 
        m_StartedSound = false;
    }
 
    void Init()
    {
        Unigine.Console.Run("show_messages 1");
    }
 
    // Update is called once per frame
    private void Update()
    {
        // get the distance to main camera
        float camDist = 1//(Camera.main.transform.position - transform.position).sqrMagnitude;
 
        // stop sound if the object is beyond the maximum roll off distance
        if (m_StartedSound && camDist > maxRolloffDistance*maxRolloffDistance)
        {
            StopSound();
        }
 
        // start the sound if not playing and it is nearer than the maximum distance
        if (!m_StartedSound && camDist < maxRolloffDistance*maxRolloffDistance)
        {
            StartSound();
        }
 
        if (m_StartedSound)
        {
            // The pitch is interpolated between the min and max values, according to the car's revs.
            float pitch = ULerp(lowPitchMinlowPitchMaxm_CarController.Revs);
 
            // clamp to minimum pitch (note, not clamped to max for high revs while burning out)
            pitch = Math.Min(lowPitchMaxpitch);
 
            if (engineSoundStyle == EngineAudioOptions.Simple)
            {
                // for 1 channel engine sound, it's oh so simple:
                m_HighAccel.Pitch = pitch*pitchMultiplier*highPitchMultiplier;
                //m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                m_HighAccel.Gain = 1;
            }
            else
            {
                // for 4 channel engine sound, it's a little more complex:
 
                // adjust the pitches based on the multipliers
                Log.Warning("pitch = {0:0.0}\n"pitch);
 
                m_LowAccel.Pitch = pitch*pitchMultiplier;
                m_LowDecel.Pitch = pitch*pitchMultiplier;
                m_HighAccel.Pitch = pitch*highPitchMultiplier*pitchMultiplier;
                m_HighDecel.Pitch = pitch*highPitchMultiplier*pitchMultiplier;
 
                // get values for fading the sounds based on the acceleration
                float accFade = Math.Abs(m_CarController.AccelInput);
                float decFade = 1 - accFade;
 
                // get the high fade value based on the cars revs
                float highFade = InverseLerp(0.2f0.8fm_CarController.Revs);
                float lowFade = 1 - highFade;
 
                // adjust the values to be more realistic
                highFade = 1 - ((1 - highFade)*(1 - highFade));
                lowFade = 1 - ((1 - lowFade)*(1 - lowFade));
                accFade = 1 - ((1 - accFade)*(1 - accFade));
                decFade = 1 - ((1 - decFade)*(1 - decFade));
 
                // adjust the source volumes based on the fade values
                m_LowAccel.Gain = lowFade*accFade;
                m_LowDecel.Gain = lowFade*decFade;
                m_HighAccel.Gain = highFade*accFade;
                m_HighDecel.Gain = highFade*decFade;
 
                // adjust the doppler levels
                //m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                //m_LowAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                //m_HighDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                //m_LowDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
            }
        }
    }
 
    // unclamped version of Lerp, to allow value to exceed the from-to range
    private static float ULerp(float fromfloat tofloat value)
    {
        return (1.0f - value)*from + value*to;
    }
 
    float InverseLerp(float start,float endfloat t)
    {
        return (t - start)/(end - start);
    }
    /*
    float InverseLerp(float min, float max, float value) 
    {
        if(Mathf.Abs(max - min) < epsilon) return min;
        return (value - min) / (max - min);
    }
    */
/*
    // sets up and adds new audio source to the gane object
    private AudioSource SetUpEngineAudioSource(AudioClip clip)
    {
        // create the new audio source component on the game object and set up its properties
        AudioSource source = gameObject.AddComponent<AudioSource>();
        source.clip = clip;
        source.volume = 0;
        source.loop = true;
 
        // start the clip from a random point
        source.time = Random.Range(0f, clip.length);
        source.Play();
        source.minDistance = 5;
        source.maxDistance = maxRolloffDistance;
        source.dopplerLevel = 0;
        return source;
    }
 
    // unclamped versions of Lerp and Inverse Lerp, to allow value to exceed the from-to range
    private static float ULerp(float from, float to, float value)
    {
        return (1.0f - value)*from + value*to;
    }
 
    private void Init()
    {
        // write here code to be called on component initialization
        
    }
*/
}
Spoiler
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
 
[Component(PropertyGuid = "Ваш ID  скрипта")]
public class Car : Component
{
    public class Parametr_Settings
    {
        public Node wheel_Node = null;
        public JointWheel joint_wheel = null;
        public bool drive_Wheels;
        public bool wheels_Turn;
        public bool wheels_Break;
        public float braking_Force;
        public bool wheels_Hand_Break;
        public float braking_Force_Hand;
    }
 
    public class Wheel_Parametr
    {
        public bool enabled;
        public Parametr_Settings parametr_Settings;
    }
    
    public Wheel_Parametr[] wheelParametr;
    //public List<Wheel_Parametr> wheel_Parametr;
 
    public float acceleration = 50.0f;
    public float turn_speed = 25.0f;
    public float max_velocity = 90.0f;
    public float max_turn_angle = 30.0f;
    public float default_torque = 5.0f;
    public float car_base = 3.0f;
    public float car_width = 2.0f;
 
    public Player player = null;
 
    private Controls controls = null;
 
    private float current_velocity = 0.0f;
    private float current_torque = 0.0f;
    private float current_turn_angle = 0.0f;
 
    float deltaTime;
 
    private void Init()
    {
        // write here code to be called on component initialization
        for(int a = 0a < wheelParametr.Lengtha++)
        {
            if(wheelParametr[a].parametr_Settings.wheel_Node)
                wheelParametr[a].parametr_Settings.joint_wheel = wheelParametr[a].parametr_Settings.wheel_Node.ObjectBody.GetJoint(0as JointWheel;
        }
            
        if(player)
            controls = player.Controls;
 
    }
    
    private void Update()
    {
        deltaTime = Game.IFps;
        current_torque = 0.0f;
 
        if(controls)
        {
            bool is_moving_foward = controls.GetState(Controls.STATE_FORWARD) != 0;
            bool is_moving_backward = controls.GetState(Controls.STATE_BACKWARD) != 0;
            bool is_moving_right = controls.GetState(Controls.STATE_MOVE_RIGHT) != 0;
            bool is_moving_left = controls.GetState(Controls.STATE_MOVE_LEFT) != 0;
            bool is_handbreak = controls.GetState(Controls.STATE_USE) != 0;
            
            if(is_moving_foward)
            {
                current_torque = default_torque;
                current_velocity = MathLib.Max(current_velocity0.0f);
                current_velocity += deltaTime * acceleration;
                accel = 1;
            }
            else if(is_moving_backward)
            {
                current_torque = default_torque;
                current_velocity = MathLib.Min(current_velocity0.0f);
                current_velocity -= deltaTime * acceleration;
                accel = -1;
            }
            else
            {
                current_velocity*= MathLib.Exp(-deltaTime);
                accel = 0;
            }
 
            if(is_moving_left)
            {
                current_turn_angle += deltaTime * turn_speed;
            }
            else if(is_moving_right)
            {
                current_turn_angle -= deltaTime * turn_speed;
            }
            else
            {
                if(MathLib.Abs(current_turn_angle) < 0.25f)
                    current_turn_angle = 0.0f;
 
                current_turn_angle -= MathLib.Sign(current_turn_angle) * turn_speed * deltaTime;
            }
 
            if(is_handbreak)
            {
                for(int a = 0a < wheelParametr.Lengtha++)
                {
                    if(wheelParametr[a].parametr_Settings.wheels_Hand_Break)
                        wheelParametr[a].parametr_Settings.joint_wheel.AngularDamping = wheelParametr[a].parametr_Settings.braking_Force_Hand;
                }
            }
            else
            {
                for(int a = 0a < wheelParametr.Lengtha++)
                {
                    if(wheelParametr[a].parametr_Settings.wheels_Hand_Break)
                        wheelParametr[a].parametr_Settings.joint_wheel.AngularDamping = 0;
                }
            }
        }
 
        current_velocity = MathLib.Clamp(current_velocity, -max_velocitymax_velocity);
        current_turn_angle = MathLib.Clamp(current_turn_angle, -max_turn_anglemax_turn_angle);
 
        float angle = current_turn_angle;
 
        //Log.Warning("current_turn_angle = {0:0.0}\n", current_turn_angle);
        //Log.Warning("{0:0.0}\n", 40.0f - Game.Time);
 
        for(int a = 0a < wheelParametr.Lengtha++)
        {
            if(wheelParametr[a].parametr_Settings.wheels_Turn)
            {
                if(MathLib.Abs(current_turn_angle) > MathLib.EPSILON)
                {
                    float radius = car_base / MathLib.Tan(current_turn_angle * MathLib.DEG2RAD);
                    radius = radius - car_width * 0.5f;
                    //float radius_1 = radius + car_width * 0.5f;
 
                    angle = MathLib.Atan(car_base / radius) * MathLib.RAD2DEG;
                }
 
                wheelParametr[a].parametr_Settings.joint_wheel.Axis10 = MathLib.RotateZ(angle).GetColumn3(0);
            }
        }
 
        AccelInput = accel = Math.Clamp(accel01);
 
        CalculateRevs();
        GearChanging();
 
        //Log.Warning("m_GearNum = {0:0.0}\n", m_GearNum);
    }
 
    private void UpdatePhysics()
    {
        for(int a = 0a < wheelParametr.Lengtha++)
        {
            if(wheelParametr[a].parametr_Settings.drive_Wheels)
                wheelParametr[a].parametr_Settings.joint_wheel.AngularVelocity = current_velocity;
                wheelParametr[a].parametr_Settings.joint_wheel.AngularTorque = current_torque;
        }
    }
 
    public static int NoOfGears = 5;
    public Node m_Rigidbody;
    //public BodyRigid m_Rigidbody;
    //определяем длину вектора (скорость) у ригидбоди в км/ч. Для миль в час вместо 3.6f надо вставить 2.23693629f
    public float CurrentSpeedget { return m_Rigidbody.BodyLinearVelocity.Length * 3.6f; }}
    public float MaxSpeed{get { return max_velocity; }}
    private int m_GearNum;
    private float m_GearFactor;
    private float m_RevRangeBoundary = 1f;
    public float Revs { getprivate set; }
    public float AccelInput { getprivate set; }
 
    float accel;
 
    //переключение передач исходя из текущей скорости
    private void GearChanging()
    {
        float f = Math.Abs(CurrentSpeed/MaxSpeed);
        float upgearlimit = (1/(floatNoOfGears)*(m_GearNum + 1);
        float downgearlimit = (1/(floatNoOfGears)*m_GearNum;
 
        if (m_GearNum > 0 && f < downgearlimit)
        {
            m_GearNum--;
        }
 
        if (f > upgearlimit && (m_GearNum < (NoOfGears - 1)))
        {
            m_GearNum++;
        }
    }
 
    // simple function to add a curved bias towards 1 for a value in the 0-1 range
    private static float CurveFactor(float factor)
    {
        return 1 - (1 - factor)*(1 - factor);
    }
 
    // unclamped version of Lerp, to allow value to exceed the from-to range
    private static float ULerp(float fromfloat tofloat value)
    {
        return (1.0f - value)*from + value*to;
    }
 
    float InverseLerp(float start,float endfloat t)
    {
        return (t - start)/(end - start);
    }
    /*
    float InverseLerp(float min, float max, float value) 
    {
        if(Mathf.Abs(max - min) < epsilon) return min;
        return (value - min) / (max - min);
    }
    */
 
    private static float Lerp(float start,float endfloat t) {
        return start * (1 - t) + end * t;
    }
 
    private void CalculateGearFactor()
    {
        float f = (1/(floatNoOfGears);
        // gear factor is a normalised representation of the current speed within the current gear's range of speeds.
        // We smooth towards the 'target' gear factor, so that revs don't instantly snap up or down when changing gear.
        var targetGearFactor = InverseLerp(f*m_GearNumf*(m_GearNum + 1), Math.Abs(CurrentSpeed/MaxSpeed));
        m_GearFactor = Lerp(m_GearFactortargetGearFactordeltaTime*5f);
    }
 
    private void CalculateRevs()
    {
        // calculate engine revs (for display / sound)
        // (this is done in retrospect - revs are not used in force/power calculations)
        CalculateGearFactor();
        var gearNumFactor = m_GearNum/(floatNoOfGears;
        var revsRangeMin = ULerp(0fm_RevRangeBoundaryCurveFactor(gearNumFactor));
        var revsRangeMax = ULerp(m_RevRangeBoundary1fgearNumFactor);
        Revs = ULerp(revsRangeMinrevsRangeMaxm_GearFactor);
    }   
 
    // checks if the wheels are spinning and is so does three things
    // 1) emits particles
    // 2) plays tiure skidding sounds
    // 3) leaves skidmarks on the ground
    // these effects are controlled through the WheelEffects class
    /*
    private void CheckForWheelSpin()
    {
        // loop through all wheels
        for (int i = 0; i < 4; i++)
        {
            JointWheel wheelHit;
            m_WheelColliders.GetGroundHit(out wheelHit);
 
            // is the tire slipping above the given threshhold
            if (Mathf.Abs(wheelHit.forwardSlip) >= m_SlipLimit || Mathf.Abs(wheelHit.sidewaysSlip) >= m_SlipLimit)
            {
                m_WheelEffects.EmitTyreSmoke();
 
                // avoiding all four tires screeching at the same time
                // if they do it can lead to some strange audio artefacts
                if (!AnySkidSoundPlaying())
                {
                    m_WheelEffects.PlayAudio();
                }
                continue;
            }
 
            // if it wasnt slipping stop all the audio
            if (m_WheelEffects.PlayingAudio)
            {
                m_WheelEffects.StopAudio();
            }
            // end the trail generation
            m_WheelEffects.EndSkidTrail();
        }
    }
    */
 
}

 

 

Обновление темы от 12.01.2021

Добрался таки я до углов рулевых колес, но пока что только теория.

Углы аккермана.

Решать на какой угол поворачивать колеса будем через треугольники. Вообще возьмите себе за правило в любой непонятной ситуации все решать через треугольники (если  мне не изменяет память, то это геометрия за 8й класс).

Ранее я давал ссылку на тему где я уже решал нечто подобное (оказалось что я просто повторил то что уже решили пол века назад наши ракетчики, так что что наши системы ПВО с точки зрения вычислений имеют абсолютную точность). Я очень прошу — если вы не понимаете как это, если вы чувствуете что в вас просыпаются эмоции и негодование — оставьте все это при себе. Так же хочу обратиться к людям которые сейчас учатся в школе — я вас очень прошу — учитесь, если вы решили связать свою жизнь с разработкой программ, то как минимум алгебру, геометрию, физику вы должны понимать на интуитивном уровне. В школьной программе нет лишних знаний, чем лучше вы ее усвоите, тем проще вам будет жить.

Итак к теме.

Чтобы успешно решить на какой угол поворачивать колеса, нам необходима точка отсчета, относительно которой мы и буде решать все остальное. В данном случае точкой отсчета будет переднее правое колесо. Крутиться машина будет (и всегда крутится) вокруг самого заднего неповорачиваемого колеса, а первое колесо которое поворачивает по наименьшему радиусу — ближайшее к нему. В случае если задних неповорачиваемых колес будет несколько, то крутится машина будет по средней точке между этими колесами. Если же задние колеса будут поворачиваемыми, то вокруг средней точки между ближайшими разнонаправленными колесами.

Для простоты понимая я взял целочисленные значения и равнобедренный треугольник (это тот который имеет две равные стороны) и углом в 45 градусов. Длина базы автомобиля — это первый наш катет a который пусть будет равен 6, угол рулевого колеса — первый известный угол B. Проводим мысленную черту (вот тут для многих камень преткновения) перпендикулярную рулевому колесу — это гипотенуза с и мысленную черту от заднего колеса — второй катет b. Точка их пересечения будет завершать первый треугольник, из этой точки кидаем гипотенузу ко второму поворотному колесу. Соответственно между двумя катетами угол равен 90. Сумма всех углов треугольника равна 180 градусов, так что легко найти третий угол — 180-90-45 = 45. Находим длину второго катета b – он так же должен быть равен 6. Проверяем. b = a * (sin B / sin A) = 6 (0.85/ 0.85) = 6. К этой длине добавляем ширину автомобиля равную 4 — это катет второго треугольника b = 4 + 6 = 10. Так что у нас есть теперь два катета a = 6, b = 10 и угол между ними C = 90. Вычисляем гипотенузу (а вот тут слабое место. Скалярные вычисления очень ресурсно затратные, так что по возможности их надо избегать, или же вычислять в разных сопроцессах и всяких других потоках) - c = √(a2 + b2 – 2 ab cosС) = 100+36 – 2 * 10 * 6 * 0 = 136 = 11,66. Далее находим второй угол Acos A = (b2+c2-a2)/(2bc) = (100 + 136 – 36)/ 2* 10 * 11,66 = 0,86. Переводим все это в градусы, иначе говоря применяем аркосинус 0,86 = 30,68. Находим угол B = 180 – 90 – 30,68 = 59,32. Относительно направления автомобиля, второе рулевое колесо надо повернуть на 90 - 59,32 = 30,68. Это по тому что у нас в движке текущее положение колеса равно 0 градусов и раскрываются углы влево и вправо на 180 градусов соответственно.

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

Схематичные примеры в картинках прикрепил в файлах. 

 

5.png

5.png

Edited by nikolay.sykharev
  • Like 2
Link to comment

Здравствуйте, Николай!

Действительно, версии документации отличаются, так же как и версии движка, который претерпевает ряд изменений на пути усовершенствования (API, функционал, UI и т.д.). Соответственно, если у Вас версия движка 2.11, то для того, чтобы получить актуальную информацию о работе с ней, следует обращаться к Документации 2.11

Что касается статьи по созданию машинки, то обновленная версия присутствует в 2.11:(https://developer.unigine.com/ru/docs/2.11/code/usage/car_wheel_joints/index). А называется она по-другому поскольку начиная с 2.11 SuspensionJoint был помечен как "deprecated" и будет удален в версии 2.12.

Относительно того, что делать со скриптами и как запускать проект, в документации есть отдельные статьи, посвященные этим вопросам (в зависимости от выбранного API и workflow), например:

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

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

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

Спасибо!

  • Like 2
Link to comment

Алгоритм создания автомобиля. 

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

Внимание! Алгоритм верен для Уазика из бесплатного плагина! Настоятельно рекомендую проходить шаги именно на нем, чтобы можно было без проблем сопоставлять то что изложено и то что на практике.

Внимание! Правые колеса Уазика имеют неверную ориентацию из-за чего всегда поворачиваются на 180 на дефолтную ротацию при просчете физики. Решается это просто — дочерите к примитиву колесо, а сам примитив в качестве колеса прикуручиваете к кузову. В видео этот вопрос решается через сетки модели, что по мне, очень грубое решение.

От себя хочу сказать, что это вообще очень хорошая практика игровые объекты на сцене дочерить либо к пустышкам, либо к примитивам если первое не возможно. Ваш игровой объект (Game Object, сокращенно GO) должен иметь набор свойств и визуализация объекта, это так же одно из его свойств. Так же в его свойствах может быть звук, партиклы, скрипты и т.д. И уже этот игровой объект который обладает всеми необходимыми свойствами, вы сохраняете как node и в дальнейшем используете на сцене.

Сам алгоритм. Еще раз ссылка на видео UNIGINE Livestream: Making a C# Car Project

  1. Самое важное. У всех объектов на сцене по которым планируется езда, жмякаем collision. На дефолтной сцене это ground_plane.

  2. Берем уазик кидаем на сцену, вытаскиваем из него колеса.

  3. На него цепляем коллайдер и ригидбоди. Ставим массу в Shapes 100 (Вообще все это странно. В поле Physics объекта в итоге целых три строчки с массой.)

  4. На колеса так же кидаем коллайдер и ригид боди

  5. Когда вы кинете на колеса коллайдер, то он будет ориентрован не так как колеса. Подбираем его ротацию и радиус. В моем случае радиус был 0.38 (на видео 0.40).

  6. А вот теперь внимание. Кидаем на колеса wheel из кузова автомобиля, а не из самого колеса. Т.е. выделяем кузов уазика, в поле Physics в Joints жмякаем add, в открывшемся окошке выбираем нужное колесо, где находятся (!!!) ВСЕ объекты с физикой (опупеть блин. Даже поисковой строки нет). Интересно, что если вы будете это делать из колес, то автомобиль будет работать несколько иначе, хотя казалось бы.

  7. Теперь очень не очевидный момент — надо изменить Wheel Radius и он должен соответствовать радиусу коллайдера который вы только что настроили. Не очевидно это по тому что во-первых по факту это дублирующие друг друга величины, во-вторых, по умолчанию визуально Wheel Radius не отображается.

  8. Добавляем массу колесам в 25. Внимание! Физические величины из реального мира не подходят для применения в движке! Все высчитывается методом научного тыка, так чтобы поведение автомобиля казалось естественным. Это нормальная практика, просто начальные значения все же лучше брать из реальности чтобы можно было от чего то отталкиваться. Однако всегда следует помнить, что конечные значения могут значительно отличаться от начальных и пугаться этого не нужно.

  9. Ограничиваем ход колес путем установки значений в Linear From/To: -0,1/0,1

  10. Теперь настраиваем жесткость пружин/рессор подвески. Идем в Linear Spiring, ставим 100

  11. Затем настраиваем амортизаторы — Lianer Damping. Ставим 40

  12. Снижаем шаг итераций просчета в Iterations до 10. Соответственно чем больше значение, тем меньше шаг, тем чаще обрабатывается физика колес, тем больше нагрузка на железо. Такие вот парадоксы.

  13. Теперь автомобиль ведет себя более менее предсказуемо, так что можно регулируя его настройки добиться максимально естественного поведения.

 

Скрипты для управления я не буду брать из видео стрима, поскольку это настолько примитивно что заслуживает внимания только в качестве демонстрации, а перенесу превосходный в качестве тутора дефолтный автомобиль из Unity. Думаю Юниты не будут в обиде.

Так же, на форуме Unity по русски я писал общее про автомобиль Wheelcollider - как его использовать. Это для общего развития - там я объясняю основные принципы поведения автомобиля.

  • Like 1
Link to comment

Дополнение от 21.06.2020

Решил максимально полно предоставить информацию об автомобиле в рамках движка, так что скрипт из видео

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

Раз уж скрипт-туториал имеет углы Аккермана, то сразу же сюда записываем минус - нет регулятора тормозных усилий между передней и задней осями автомобиля, который в простонародье называется "колдун". Вообще с усилиями что-то странное, ведущая передняя ось, тормозные усилия на заднюю. Ну да ладно, главное что обозначено как это делать.

Что еще. Подвеска тут как и везде - ходит она по идеально вертикальной траектории, что далеко не реализм для не зависимых подвесок по типу Макферсон и тем более зависимых. Следует это учитывать. В случае если у вашего автомобиля  везде продольные рычаги, как например на задней подвеске Ситроена С5 первого поколения, или же независимая рессорная, то конечно это на поведении автомобиля ни как не скажется (Я правда таких еще пока не встречал, но мы же тем и занимаемся что создаем то чего нет). Зато не надо делать развал колес. 

Кастор так же  судя по всему выставлять не придется :-).

Spoiler
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
 
[Component(PropertyGuid = "Ваш уникальный номер скрипта")]
public class Car : Component
{
    public float acceleration = 50.0f;
    public float turn_speed = 25.0f;
    public float max_velocity = 90.0f;
    public float max_turn_angle = 30.0f;
    public float default_torque = 5.0f;
    public float car_base = 3.0f;
    public float car_width = 2.0f;
 
    public Node wheel_bl = null;
    public Node wheel_br = null;
    public Node wheel_fl = null;
    public Node wheel_fr = null;
 
    public Player player = null;
 
    private JointWheel joint_wheel_bl = null;
    private JointWheel joint_wheel_br = null;
    private JointWheel joint_wheel_fl = null;
    private JointWheel joint_wheel_fr = null;
 
    private Controls controls = null;
 
    private float current_velocity = 0.0f;
    private float current_torque = 0.0f;
    private float current_turn_angle = 0.0f;
    
 
    private void Init()
    {
        // write here code to be called on component initialization
        if(wheel_bl)
            joint_wheel_bl = wheel_bl.ObjectBody.GetJoint(0as JointWheel;
 
        if(wheel_br)
            joint_wheel_br = wheel_br.ObjectBody.GetJoint(0as JointWheel;
 
        if(wheel_fl)
            joint_wheel_fl = wheel_fl.ObjectBody.GetJoint(0as JointWheel;
 
        if(wheel_fr)
            joint_wheel_fr = wheel_fr.ObjectBody.GetJoint(0as JointWheel;
            
        if(player)
            controls = player.Controls;
 
    }
    
    private void Update()
    {
        float deltaTime = Game.IFps;
        current_torque = 0.0f;
 
        if(controls)
        {
            bool is_moving_foward = controls.GetState(Controls.STATE_FORWARD) != 0;
            bool is_moving_backward = controls.GetState(Controls.STATE_BACKWARD) != 0;
            bool is_moving_right = controls.GetState(Controls.STATE_MOVE_RIGHT) != 0;
            bool is_moving_left = controls.GetState(Controls.STATE_MOVE_LEFT) != 0;
            bool is_handbreak = controls.GetState(Controls.STATE_USE) != 0;
            
 
            if(is_moving_foward)
            {
                current_torque = default_torque;
                current_velocity = MathLib.Max(current_velocity0.0f);
                current_velocity += deltaTime * acceleration;
            }
            else if(is_moving_backward)
            {
                current_torque = default_torque;
                current_velocity = MathLib.Min(current_velocity0.0f);
                current_velocity -= deltaTime * acceleration;
            }
            else
            {
                current_velocity*= MathLib.Exp(-deltaTime);
            }
 
            if(is_moving_left)
            {
                current_turn_angle += deltaTime * turn_speed;
            }
            else if(is_moving_right)
            {
                current_turn_angle -= deltaTime * turn_speed;
            }
            else
            {
                if(MathLib.Abs(current_turn_angle) < 0.25f)
                    current_turn_angle = 0.0f;
 
                current_turn_angle -= MathLib.Sign(current_turn_angle) * turn_speed * deltaTime;
            }
 
            if(is_handbreak)
            {
                //joint_wheel_fl.AngularDamping = 10000.0f;
                //joint_wheel_fr.AngularDamping = 10000.0f;
                joint_wheel_bl.AngularDamping = 10000.0f;
                joint_wheel_br.AngularDamping = 10000.0f;
            }
            else
            {
                //joint_wheel_fl.AngularDamping = 0.0f;
                //joint_wheel_fr.AngularDamping = 0.0f;
                joint_wheel_bl.AngularDamping = 0.0f;
                joint_wheel_br.AngularDamping = 0.0f;
            }
        }
 
        current_velocity = MathLib.Clamp(current_velocity, -max_velocitymax_velocity);
        current_turn_angle = MathLib.Clamp(current_turn_angle, -max_turn_anglemax_turn_angle);
 
        float angle_0 = current_turn_angle;
        float angle_1 = current_turn_angle;
 
        if(MathLib.Abs(current_turn_angle) > MathLib.EPSILON)
        {
            float radius = car_base / MathLib.Tan(current_turn_angle * MathLib.DEG2RAD);
            float radius_0 = radius - car_width * 0.5f;
            float radius_1 = radius + car_width * 0.5f;
 
            angle_0 = MathLib.Atan(car_base / radius_0) * MathLib.RAD2DEG;
            angle_1 = MathLib.Atan(car_base / radius_1) * MathLib.RAD2DEG;
        }
 
        joint_wheel_fl.Axis10 = MathLib.RotateZ(angle_1).GetColumn3(0);
        joint_wheel_fr.Axis10 = MathLib.RotateZ(angle_0).GetColumn3(0);
    }
 
    private void UpdatePhysics()
    {
        joint_wheel_fl.AngularVelocity = current_velocity;
        joint_wheel_fr.AngularVelocity = current_velocity;      
        
        joint_wheel_fl.AngularTorque = current_torque;
        joint_wheel_fr.AngularTorque = current_torque;
    }
}

 

  • Like 1
Link to comment

Дополнение от 23.06.2020

Развиваем тему. КПП и колеса.

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

[System.Serializable]
public struct Wheel_Parametr
{
    //склонность решать любые вопросы войной
    public bool ведущие_колеса;
    public bool колеса_поворачиваются;
//и т.д.
 
    public Wheel_Parametr(bool ведущие_колеса1bool колеса_поворачиваются1)
    {
        ведущие_колеса = ведущие_колеса1;
        колеса_поворачиваются = колеса_поворачиваются1;
    }
}
public class Car : Component
{
    public Wheel_Parametr[] wheelParametr;
}

Код должен быть как можно более гибким и универсальным, что бы он мог подходить для всех однотипных игровых объектов, без изменений. Пока не ясно как делать выпадающий список с набором параметров в Node Components And Properties, но надеюсь разработчики или более опытные пользователи мне ответят.

Теперь о КПП.

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

Итак, пока что псевдо код КПП. Надо разобраться как добавлять звуки и партиклы, и можно будет собирать все вместе.

using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
 
[System.Serializable]
public struct Wheel_Parametr
{
    //склонность решать любые вопросы войной
    public bool ведущие_колеса;
    public bool колеса_поворачиваются;
 
    public Wheel_Parametr(bool ведущие_колеса1bool колеса_поворачиваются1)
    {
        ведущие_колеса = ведущие_колеса1;
        колеса_поворачиваются = колеса_поворачиваются1;
    }
}
 
[Component(PropertyGuid = "Ваш уникальный ID скрипта")]
public class Car : Component
{
    float deltaTime;
 
    private void Update()
    {
        deltaTime = Game.IFps;
 
        CalculateRevs();
        GearChanging();
    }
 
    public static int NoOfGears = 5;
    private BodyRigid m_Rigidbody;
    //определяем длину вектора (скорость) у ригидбоди в км/ч. Для миль в час вместо 3.6f надо вставить 2.23693629f
    public float CurrentSpeedget { return m_Rigidbody.LinearVelocity.Length * 3.6f; }}
    public float MaxSpeed{get { return max_velocity; }}
    private int m_GearNum;
    private float m_GearFactor;
    private float m_RevRangeBoundary = 1f;
    public float Revs { getprivate set; }
 
    //переключение передач исходя из текущей скорости
    private void GearChanging()
    {
        float f = Math.Abs(CurrentSpeed/MaxSpeed);
        float upgearlimit = (1/(floatNoOfGears)*(m_GearNum + 1);
        float downgearlimit = (1/(floatNoOfGears)*m_GearNum;
 
        if (m_GearNum > 0 && f < downgearlimit)
        {
            m_GearNum--;
        }
 
        if (f > upgearlimit && (m_GearNum < (NoOfGears - 1)))
        {
            m_GearNum++;
        }
    }
 
    // simple function to add a curved bias towards 1 for a value in the 0-1 range
    private static float CurveFactor(float factor)
    {
        return 1 - (1 - factor)*(1 - factor);
    }
 
    // unclamped version of Lerp, to allow value to exceed the from-to range
    private static float ULerp(float fromfloat tofloat value)
    {
        return (1.0f - value)*from + value*to;
    }

//К сожалению я не увидел функционал для интерполяций, так что это отдельные функции

    float InverseLerp(float start,float endfloat t)
    {
        return (t - start)/(end - start);
    }
//На полях интернета нашел информацию, что надо бы в InverseLerp вставлять if. Пока не подтвержденная игформация
    /*
    float InverseLerp(float min, float max, float value) 
    {
        if(Mathf.Abs(max - min) < epsilon) return min;
        return (value - min) / (max - min);
    }
    */
 
    private static float Lerp(float start,float endfloat t) {
        return start * (1 - t) + end * t;
    }
 
    private void CalculateGearFactor()
    {
        float f = (1/(floatNoOfGears);
        // gear factor is a normalised representation of the current speed within the current gear's range of speeds.
        // We smooth towards the 'target' gear factor, so that revs don't instantly snap up or down when changing gear.
        var targetGearFactor = InverseLerp(f*m_GearNumf*(m_GearNum + 1), Math.Abs(CurrentSpeed/MaxSpeed));
        m_GearFactor = Lerp(m_GearFactortargetGearFactordeltaTime*5f);
    }
 
    private void CalculateRevs()
    {
        // calculate engine revs (for display / sound)
        // (this is done in retrospect - revs are not used in force/power calculations)
        CalculateGearFactor();
        var gearNumFactor = m_GearNum/(floatNoOfGears;
        var revsRangeMin = ULerp(0fm_RevRangeBoundaryCurveFactor(gearNumFactor));
        var revsRangeMax = ULerp(m_RevRangeBoundary1fgearNumFactor);
        Revs = ULerp(revsRangeMinrevsRangeMaxm_GearFactor);
    }   

 

//Далее идут эффекты звука и дыма от колес. 

    // checks if the wheels are spinning and is so does three things
    // 1) emits particles
    // 2) plays tiure skidding sounds
    // 3) leaves skidmarks on the ground
    // these effects are controlled through the WheelEffects class
    /*
    private void CheckForWheelSpin()
    {
        // loop through all wheels
        for (int i = 0; i < 4; i++)
        {
            JointWheel wheelHit;
            m_WheelColliders.GetGroundHit(out wheelHit);
 
            // is the tire slipping above the given threshhold
            if (Mathf.Abs(wheelHit.forwardSlip) >= m_SlipLimit || Mathf.Abs(wheelHit.sidewaysSlip) >= m_SlipLimit)
            {
                m_WheelEffects.EmitTyreSmoke();
 
                // avoiding all four tires screeching at the same time
                // if they do it can lead to some strange audio artefacts
                if (!AnySkidSoundPlaying())
                {
                    m_WheelEffects.PlayAudio();
                }
                continue;
            }
 
            // if it wasnt slipping stop all the audio
            if (m_WheelEffects.PlayingAudio)
            {
                m_WheelEffects.StopAudio();
            }
            // end the trail generation
            m_WheelEffects.EndSkidTrail();
        }
    }
    */
 
}

 

  • Like 1
Link to comment

Итак, докончил я ковырять документацию и собственно рабочая базовая машинка из Юнити.

Позволил себе немного модифицировать скрипт от разработчика, так что теперь там массив колес. С углами Аккермана я пока не заморачивался, однако скорее всего это будет похоже на вот это - Расчет упреждения для тела летящего по параболе (ВНИМАНИЕ! Переходя по ссылке ты попадешь на крайне простую и критически сложную к пониманию тему. Прошло 5 лет, но я до сих пор получаю на нее "опровежения". Если вдруг ты чувствуешь пригорание в области попы, крайнюю степень возмущенности и желание во чтобы то не стало раздавить автора своим авторитетом - не трать время.). Короче говоря обычная триангуляция.

Что еще. Поскольку для меня движок новый и я еще не знаю все его особенности, то мог несколько не оптимально собрать код и провести налоги функционала. Например получаю доступ к скрипту Car я через поле свойств скрипта в Node Components. Это не очень изящно, поскольку скрипт CarAudio работает только со скриптом Car который висит на том же объекте. Ну и т.п.

Итак, скрипты. Два скрипта Car и CarAudio должны висеть где угодно, но лучше конечно на самом автомобиле.

Далее в массиве wheelParametr определяем количество колес и закидываем их туда. Настраиваем.

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

Затем, в иерархии автомобиля создаем 4 SoundSource, включаем ему Loop. В первые два закидываем звук двигателя на низких оборотах (m_LowAccel и m_LowDecel), затем высоких (m_HighAccel) и в последний странный звук который назван как DecelerationHigh. Соответственно в CarAudio закидываем эти SoundSource в соответствующие поля.

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

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

Позже добавлю частицы и звук визга резины - тут просто надо уже получше понять как работает движок.

Скрипты в спойлере:

Spoiler
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
 
[Component(PropertyGuid = "Ваш ID  скрипта")]
public class CarAudio : Component
{
 
    // This script reads some of the car's current properties and plays sounds accordingly.
    // The engine sound can be a simple single clip which is looped and pitched, or it
    // can be a crossfaded blend of four clips which represent the timbre of the engine
    // at different RPM and Throttle state.
 
    // the engine clips should all be a steady pitch, not rising or falling.
 
    // when using four channel engine crossfading, the four clips should be:
    // lowAccelClip : The engine at low revs, with throttle open (i.e. begining acceleration at very low speed)
    // highAccelClip : Thenengine at high revs, with throttle open (i.e. accelerating, but almost at max speed)
    // lowDecelClip : The engine at low revs, with throttle at minimum (i.e. idling or engine-braking at very low speed)
    // highDecelClip : Thenengine at high revs, with throttle at minimum (i.e. engine-braking at very high speed)
 
    // For proper crossfading, the clips pitches should all match, with an octave offset between low and high.
 
    public enum EngineAudioOptions // Options for the engine audio
    {
        Simple// Simple style audio
        FourChannel // four Channel audio
    }
 
    public EngineAudioOptions engineSoundStyle = EngineAudioOptions.FourChannel;// Set the default audio options to be four channel
    //public Sound lowAccelClip;                                              // Audio clip for low acceleration
    //public Sound lowDecelClip;                                              // Audio clip for low deceleration
    //public Sound highAccelClip;                                             // Audio clip for high acceleration
    //public Sound highDecelClip;                                             // Audio clip for high deceleration
    public float pitchMultiplier = 1f;                                          // Used for altering the pitch of audio clips
    public float lowPitchMin = 1f;                                              // The lowest possible pitch for the low sounds
    public float lowPitchMax = 6f;                                              // The highest possible pitch for the low sounds
    public float highPitchMultiplier = 0.25f;                                   // Used for altering the pitch of high sounds
    public float maxRolloffDistance = 500;                                      // The maximum distance where rollof starts to take place
    public float dopplerLevel = 1;                                              // The mount of doppler effect used in the audio
    public bool useDoppler = true;                                              // Toggle for using doppler
 
    public SoundSource m_LowAccel// Source for the low acceleration sounds
    public SoundSource m_LowDecel// Source for the low deceleration sounds
    public SoundSource m_HighAccel// Source for the high acceleration sounds
    public SoundSource m_HighDecel// Source for the high deceleration sounds
    private bool m_StartedSound// flag for knowing if we have started sounds
    public Car m_CarController// Reference to car we are controlling
 
    private void StartSound()
    {
        // get the carcontroller ( this will not be null as we have require component)
        //m_CarController = GetComponent<CarController>();
 
        // setup the simple audio source
        //m_HighAccel = SetUpEngineAudioSource(highAccelClip);
        //m_HighAccel.SampleName = highAccelClip;
 
        // if we have four channel audio setup the four audio sources
        /*
        if (engineSoundStyle == EngineAudioOptions.FourChannel)
        {
            m_LowAccel = SetUpEngineAudioSource(lowAccelClip);
            m_LowDecel = SetUpEngineAudioSource(lowDecelClip);
            m_HighDecel = SetUpEngineAudioSource(highDecelClip);
        }
*/
        // flag that we have started the sounds playing
        m_StartedSound = true;
    }
 
    private void StopSound()
    {
        /*
        //Destroy all audio sources on this object:
        foreach (var source in GetComponents<AudioSource>())
        {
            Destroy(source);
        }
        */
 
        m_StartedSound = false;
    }
 
    void Init()
    {
        Unigine.Console.Run("show_messages 1");
    }
 
    // Update is called once per frame
    private void Update()
    {
        // get the distance to main camera
        float camDist = 1//(Camera.main.transform.position - transform.position).sqrMagnitude;
 
        // stop sound if the object is beyond the maximum roll off distance
        if (m_StartedSound && camDist > maxRolloffDistance*maxRolloffDistance)
        {
            StopSound();
        }
 
        // start the sound if not playing and it is nearer than the maximum distance
        if (!m_StartedSound && camDist < maxRolloffDistance*maxRolloffDistance)
        {
            StartSound();
        }
 
        if (m_StartedSound)
        {
            // The pitch is interpolated between the min and max values, according to the car's revs.
            float pitch = ULerp(lowPitchMinlowPitchMaxm_CarController.Revs);
 
            // clamp to minimum pitch (note, not clamped to max for high revs while burning out)
            pitch = Math.Min(lowPitchMaxpitch);
 
            if (engineSoundStyle == EngineAudioOptions.Simple)
            {
                // for 1 channel engine sound, it's oh so simple:
                m_HighAccel.Pitch = pitch*pitchMultiplier*highPitchMultiplier;
                //m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                m_HighAccel.Gain = 1;
            }
            else
            {
                // for 4 channel engine sound, it's a little more complex:
 
                // adjust the pitches based on the multipliers
                Log.Warning("pitch = {0:0.0}\n"pitch);
 
                m_LowAccel.Pitch = pitch*pitchMultiplier;
                m_LowDecel.Pitch = pitch*pitchMultiplier;
                m_HighAccel.Pitch = pitch*highPitchMultiplier*pitchMultiplier;
                m_HighDecel.Pitch = pitch*highPitchMultiplier*pitchMultiplier;
 
                // get values for fading the sounds based on the acceleration
                float accFade = Math.Abs(m_CarController.AccelInput);
                float decFade = 1 - accFade;
 
                // get the high fade value based on the cars revs
                float highFade = InverseLerp(0.2f0.8fm_CarController.Revs);
                float lowFade = 1 - highFade;
 
                // adjust the values to be more realistic
                highFade = 1 - ((1 - highFade)*(1 - highFade));
                lowFade = 1 - ((1 - lowFade)*(1 - lowFade));
                accFade = 1 - ((1 - accFade)*(1 - accFade));
                decFade = 1 - ((1 - decFade)*(1 - decFade));
 
                // adjust the source volumes based on the fade values
                m_LowAccel.Gain = lowFade*accFade;
                m_LowDecel.Gain = lowFade*decFade;
                m_HighAccel.Gain = highFade*accFade;
                m_HighDecel.Gain = highFade*decFade;
 
                // adjust the doppler levels
                //m_HighAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                //m_LowAccel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                //m_HighDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
                //m_LowDecel.dopplerLevel = useDoppler ? dopplerLevel : 0;
            }
        }
    }
 
    // unclamped version of Lerp, to allow value to exceed the from-to range
    private static float ULerp(float fromfloat tofloat value)
    {
        return (1.0f - value)*from + value*to;
    }
 
    float InverseLerp(float start,float endfloat t)
    {
        return (t - start)/(end - start);
    }
    /*
    float InverseLerp(float min, float max, float value) 
    {
        if(Mathf.Abs(max - min) < epsilon) return min;
        return (value - min) / (max - min);
    }
    */
/*
    // sets up and adds new audio source to the gane object
    private AudioSource SetUpEngineAudioSource(AudioClip clip)
    {
        // create the new audio source component on the game object and set up its properties
        AudioSource source = gameObject.AddComponent<AudioSource>();
        source.clip = clip;
        source.volume = 0;
        source.loop = true;
 
        // start the clip from a random point
        source.time = Random.Range(0f, clip.length);
        source.Play();
        source.minDistance = 5;
        source.maxDistance = maxRolloffDistance;
        source.dopplerLevel = 0;
        return source;
    }
 
    // unclamped versions of Lerp and Inverse Lerp, to allow value to exceed the from-to range
    private static float ULerp(float from, float to, float value)
    {
        return (1.0f - value)*from + value*to;
    }
 
    private void Init()
    {
        // write here code to be called on component initialization
        
    }
*/
}
Spoiler
using System;
using System.Collections;
using System.Collections.Generic;
using Unigine;
 
[Component(PropertyGuid = "Ваш ID  скрипта")]
public class Car : Component
{
    public class Parametr_Settings
    {
        public Node wheel_Node = null;
        public JointWheel joint_wheel = null;
        public bool drive_Wheels;
        public bool wheels_Turn;
        public bool wheels_Break;
        public float braking_Force;
        public bool wheels_Hand_Break;
        public float braking_Force_Hand;
    }
 
    public class Wheel_Parametr
    {
        public bool enabled;
        public Parametr_Settings parametr_Settings;
    }
    
    public Wheel_Parametr[] wheelParametr;
    //public List<Wheel_Parametr> wheel_Parametr;
 
    public float acceleration = 50.0f;
    public float turn_speed = 25.0f;
    public float max_velocity = 90.0f;
    public float max_turn_angle = 30.0f;
    public float default_torque = 5.0f;
    public float car_base = 3.0f;
    public float car_width = 2.0f;
 
    public Player player = null;
 
    private Controls controls = null;
 
    private float current_velocity = 0.0f;
    private float current_torque = 0.0f;
    private float current_turn_angle = 0.0f;
 
    float deltaTime;
 
    private void Init()
    {
        // write here code to be called on component initialization
        for(int a = 0a < wheelParametr.Lengtha++)
        {
            if(wheelParametr[a].parametr_Settings.wheel_Node)
                wheelParametr[a].parametr_Settings.joint_wheel = wheelParametr[a].parametr_Settings.wheel_Node.ObjectBody.GetJoint(0as JointWheel;
        }
            
        if(player)
            controls = player.Controls;
 
    }
    
    private void Update()
    {
        deltaTime = Game.IFps;
        current_torque = 0.0f;
 
        if(controls)
        {
            bool is_moving_foward = controls.GetState(Controls.STATE_FORWARD) != 0;
            bool is_moving_backward = controls.GetState(Controls.STATE_BACKWARD) != 0;
            bool is_moving_right = controls.GetState(Controls.STATE_MOVE_RIGHT) != 0;
            bool is_moving_left = controls.GetState(Controls.STATE_MOVE_LEFT) != 0;
            bool is_handbreak = controls.GetState(Controls.STATE_USE) != 0;
            
            if(is_moving_foward)
            {
                current_torque = default_torque;
                current_velocity = MathLib.Max(current_velocity0.0f);
                current_velocity += deltaTime * acceleration;
                accel = 1;
            }
            else if(is_moving_backward)
            {
                current_torque = default_torque;
                current_velocity = MathLib.Min(current_velocity0.0f);
                current_velocity -= deltaTime * acceleration;
                accel = -1;
            }
            else
            {
                current_velocity*= MathLib.Exp(-deltaTime);
                accel = 0;
            }
 
            if(is_moving_left)
            {
                current_turn_angle += deltaTime * turn_speed;
            }
            else if(is_moving_right)
            {
                current_turn_angle -= deltaTime * turn_speed;
            }
            else
            {
                if(MathLib.Abs(current_turn_angle) < 0.25f)
                    current_turn_angle = 0.0f;
 
                current_turn_angle -= MathLib.Sign(current_turn_angle) * turn_speed * deltaTime;
            }
 
            if(is_handbreak)
            {
                for(int a = 0a < wheelParametr.Lengtha++)
                {
                    if(wheelParametr[a].parametr_Settings.wheels_Hand_Break)
                        wheelParametr[a].parametr_Settings.joint_wheel.AngularDamping = wheelParametr[a].parametr_Settings.braking_Force_Hand;
                }
            }
            else
            {
                for(int a = 0a < wheelParametr.Lengtha++)
                {
                    if(wheelParametr[a].parametr_Settings.wheels_Hand_Break)
                        wheelParametr[a].parametr_Settings.joint_wheel.AngularDamping = 0;
                }
            }
        }
 
        current_velocity = MathLib.Clamp(current_velocity, -max_velocitymax_velocity);
        current_turn_angle = MathLib.Clamp(current_turn_angle, -max_turn_anglemax_turn_angle);
 
        float angle = current_turn_angle;
 
        //Log.Warning("current_turn_angle = {0:0.0}\n", current_turn_angle);
        //Log.Warning("{0:0.0}\n", 40.0f - Game.Time);
 
        for(int a = 0a < wheelParametr.Lengtha++)
        {
            if(wheelParametr[a].parametr_Settings.wheels_Turn)
            {
                if(MathLib.Abs(current_turn_angle) > MathLib.EPSILON)
                {
                    float radius = car_base / MathLib.Tan(current_turn_angle * MathLib.DEG2RAD);
                    radius = radius - car_width * 0.5f;
                    //float radius_1 = radius + car_width * 0.5f;
 
                    angle = MathLib.Atan(car_base / radius) * MathLib.RAD2DEG;
                }
 
                wheelParametr[a].parametr_Settings.joint_wheel.Axis10 = MathLib.RotateZ(angle).GetColumn3(0);
            }
        }
 
        AccelInput = accel = Math.Clamp(accel01);
 
        CalculateRevs();
        GearChanging();
 
        //Log.Warning("m_GearNum = {0:0.0}\n", m_GearNum);
    }
 
    private void UpdatePhysics()
    {
        for(int a = 0a < wheelParametr.Lengtha++)
        {
            if(wheelParametr[a].parametr_Settings.drive_Wheels)
                wheelParametr[a].parametr_Settings.joint_wheel.AngularVelocity = current_velocity;
                wheelParametr[a].parametr_Settings.joint_wheel.AngularTorque = current_torque;
        }
    }
 
    public static int NoOfGears = 5;
    public Node m_Rigidbody;
    //public BodyRigid m_Rigidbody;
    //определяем длину вектора (скорость) у ригидбоди в км/ч. Для миль в час вместо 3.6f надо вставить 2.23693629f
    public float CurrentSpeedget { return m_Rigidbody.BodyLinearVelocity.Length * 3.6f; }}
    public float MaxSpeed{get { return max_velocity; }}
    private int m_GearNum;
    private float m_GearFactor;
    private float m_RevRangeBoundary = 1f;
    public float Revs { getprivate set; }
    public float AccelInput { getprivate set; }
 
    float accel;
 
    //переключение передач исходя из текущей скорости
    private void GearChanging()
    {
        float f = Math.Abs(CurrentSpeed/MaxSpeed);
        float upgearlimit = (1/(floatNoOfGears)*(m_GearNum + 1);
        float downgearlimit = (1/(floatNoOfGears)*m_GearNum;
 
        if (m_GearNum > 0 && f < downgearlimit)
        {
            m_GearNum--;
        }
 
        if (f > upgearlimit && (m_GearNum < (NoOfGears - 1)))
        {
            m_GearNum++;
        }
    }
 
    // simple function to add a curved bias towards 1 for a value in the 0-1 range
    private static float CurveFactor(float factor)
    {
        return 1 - (1 - factor)*(1 - factor);
    }
 
    // unclamped version of Lerp, to allow value to exceed the from-to range
    private static float ULerp(float fromfloat tofloat value)
    {
        return (1.0f - value)*from + value*to;
    }
 
    float InverseLerp(float start,float endfloat t)
    {
        return (t - start)/(end - start);
    }
    /*
    float InverseLerp(float min, float max, float value) 
    {
        if(Mathf.Abs(max - min) < epsilon) return min;
        return (value - min) / (max - min);
    }
    */
 
    private static float Lerp(float start,float endfloat t) {
        return start * (1 - t) + end * t;
    }
 
    private void CalculateGearFactor()
    {
        float f = (1/(floatNoOfGears);
        // gear factor is a normalised representation of the current speed within the current gear's range of speeds.
        // We smooth towards the 'target' gear factor, so that revs don't instantly snap up or down when changing gear.
        var targetGearFactor = InverseLerp(f*m_GearNumf*(m_GearNum + 1), Math.Abs(CurrentSpeed/MaxSpeed));
        m_GearFactor = Lerp(m_GearFactortargetGearFactordeltaTime*5f);
    }
 
    private void CalculateRevs()
    {
        // calculate engine revs (for display / sound)
        // (this is done in retrospect - revs are not used in force/power calculations)
        CalculateGearFactor();
        var gearNumFactor = m_GearNum/(floatNoOfGears;
        var revsRangeMin = ULerp(0fm_RevRangeBoundaryCurveFactor(gearNumFactor));
        var revsRangeMax = ULerp(m_RevRangeBoundary1fgearNumFactor);
        Revs = ULerp(revsRangeMinrevsRangeMaxm_GearFactor);
    }   
 
    // checks if the wheels are spinning and is so does three things
    // 1) emits particles
    // 2) plays tiure skidding sounds
    // 3) leaves skidmarks on the ground
    // these effects are controlled through the WheelEffects class
    /*
    private void CheckForWheelSpin()
    {
        // loop through all wheels
        for (int i = 0; i < 4; i++)
        {
            JointWheel wheelHit;
            m_WheelColliders.GetGroundHit(out wheelHit);
 
            // is the tire slipping above the given threshhold
            if (Mathf.Abs(wheelHit.forwardSlip) >= m_SlipLimit || Mathf.Abs(wheelHit.sidewaysSlip) >= m_SlipLimit)
            {
                m_WheelEffects.EmitTyreSmoke();
 
                // avoiding all four tires screeching at the same time
                // if they do it can lead to some strange audio artefacts
                if (!AnySkidSoundPlaying())
                {
                    m_WheelEffects.PlayAudio();
                }
                continue;
            }
 
            // if it wasnt slipping stop all the audio
            if (m_WheelEffects.PlayingAudio)
            {
                m_WheelEffects.StopAudio();
            }
            // end the trail generation
            m_WheelEffects.EndSkidTrail();
        }
    }
    */
 
}

 

  • Like 1
Link to comment

Обновление темы от 12.01.2021

Добрался таки я до углов рулевых колес, но пока что только теория.

Углы аккермана.

Решать на какой угол поворачивать колеса будем через треугольники. Вообще возьмите себе за правило в любой непонятной ситуации все решать через треугольники (если  мне не изменяет память, то это геометрия за 8й класс).

Ранее я давал ссылку на тему где я уже решал нечто подобное (оказалось что я просто повторил то что уже решили пол века назад наши ракетчики, так что что наши системы ПВО с точки зрения вычислений имеют абсолютную точность). Я очень прошу — если вы не понимаете как это, если вы чувствуете что в вас просыпаются эмоции и негодование — оставьте все это при себе. Так же хочу обратиться к людям которые сейчас учатся в школе — я вас очень прошу — учитесь, если вы решили связать свою жизнь с разработкой программ, то как минимум алгебру, геометрию, физику вы должны понимать на интуитивном уровне. В школьной программе нет лишних знаний, чем лучше вы ее усвоите, тем проще вам будет жить.

Итак к теме.

Чтобы успешно решить на какой угол поворачивать колеса, нам необходима точка отсчета, относительно которой мы и буде решать все остальное. В данном случае точкой отсчета будет переднее правое колесо. Крутиться машина будет (и всегда крутится) вокруг самого заднего неповорачиваемого колеса, а первое колесо которое поворачивает по наименьшему радиусу — ближайшее к нему. В случае если задних неповорачиваемых колес будет несколько, то крутится машина будет по средней точке между этими колесами. Если же задние колеса будут поворачиваемыми, то вокруг средней точки между ближайшими разнонаправленными колесами.

Для простоты понимая я взял целочисленные значения и равнобедренный треугольник (это тот который имеет две равные стороны) и углом в 45 градусов. Длина базы автомобиля — это первый наш катет a который пусть будет равен 6, угол рулевого колеса — первый известный угол B. Проводим мысленную черту (вот тут для многих камень преткновения) перпендикулярную рулевому колесу — это гипотенуза с и мысленную черту от заднего колеса — второй катет b. Точка их пересечения будет завершать первый треугольник, из этой точки кидаем гипотенузу ко второму поворотному колесу. Соответственно между двумя катетами угол равен 90. Сумма всех углов треугольника равна 180 градусов, так что легко найти третий угол — 180-90-45 = 45. Находим длину второго катета b – он так же должен быть равен 6. Проверяем. b = a * (sin B / sin A) = 6 (0.85/ 0.85) = 6. К этой длине добавляем ширину автомобиля равную 4 — это катет второго треугольника b = 4 + 6 = 10. Так что у нас есть теперь два катета a = 6, b = 10 и угол между ними C = 90. Вычисляем гипотенузу (а вот тут слабое место. Скалярные вычисления очень ресурсно затратные, так что по возможности их надо избегать, или же вычислять в разных сопроцессах и всяких других потоках) - c = √(a2 + b2 – 2 ab cosС) = 100+36 – 2 * 10 * 6 * 0 = 136 = 11,66. Далее находим второй угол Acos A = (b2+c2-a2)/(2bc) = (100 + 136 – 36)/ 2* 10 * 11,66 = 0,86. Переводим все это в градусы, иначе говоря применяем аркосинус 0,86 = 30,68. Находим угол B = 180 – 90 – 30,68 = 59,32. Относительно направления автомобиля, второе рулевое колесо надо повернуть на 90 - 59,32 = 30,68. Это по тому что у нас в движке текущее положение колеса равно 0 градусов и раскрываются углы влево и вправо на 180 градусов соответственно.

Схематичные примеры в картинках прикрепил в файлах у основной темы

  • Like 1
Link to comment
×
×
  • Create New...