"Smart" car behaves diffrently with the same inputs

Viewed 56

So i made a car controller that can get input from keyboard using Input.GetAxis() and can receive an array that mimics the Input.GetAxis(). My track is devided in sections and at each section the car is gonna read alternative input for speed or input for turn.

At the begging of trainning the starting position is saved and if a car hits a wall or just sits for 10 sec then is restarted at the starting position with a new set of inputs.

This is scripts moves the car

   public class Movment : MonoBehaviour
   {
       [SerializeField]
       private new Rigidbody rigidbody;
   
       [SerializeField]
       private float
           speed,
           top_speed,
           turn_speed,
           gravity_multiplayer;
   
       private bool isTurning = false;
   
       private float turn_dir = 0, drive_dir = 0;
   
       public Individual indv;
       public int current_index;
   
       // Start is called before the first frame update
       void Start()
       {
   
       }
   
       // Update is called once per frame
       void FixedUpdate()
       {
   
           Drive(drive_dir);
           Turn(turn_dir);
           ExtraGravity();
   
   
           //Debug.Log("Speed" + drive_dir.ToString());
           //Debug.Log("Turn" + turn_dir.ToString());
       }
       private void Drive()
       {
           float drive_dir = Input.GetAxisRaw("Vertical");
           rigidbody.AddRelativeForce(new Vector3(Vector3.forward.x, 0, Vector3.forward.z) * speed * drive_dir);
   
           Vector3 localVelocity = transform.InverseTransformDirection(rigidbody.velocity);
           localVelocity.x = 0;
           rigidbody.velocity = transform.TransformDirection(localVelocity);
       }
   
       void Turn()
       {
           Vector3 update_rotation = Vector3.zero;
           float turn_dir = Input.GetAxisRaw("Horizontal");
   
           update_rotation = Vector3.up * turn_speed * turn_dir;
           rigidbody.AddTorque(update_rotation);
       }
   
       private void Drive(float network_drive_dir)
       {
           rigidbody.AddRelativeForce(new Vector3(Vector3.forward.x, 0, Vector3.forward.z) * speed * network_drive_dir);
   
           Vector3 localVelocity = transform.InverseTransformDirection(rigidbody.velocity);
           localVelocity.x = 0;
           rigidbody.velocity = transform.TransformDirection(localVelocity);
       }
   
       void Turn(float network_turn)
       {
           Vector3 update_rotation = Vector3.zero;
   
           update_rotation = Vector3.up * turn_speed * network_turn;
   
           rigidbody.AddTorque(update_rotation);
       }
   
       public void Swap(bool newInvd)
       {
           if (newInvd)
               current_index = 0;
   
           if (!isTurning)
           {
               turn_dir = indv.turn[current_index / 2];
               //Debug.Log(current_index + "(t): " + turn_dir);
               isTurning = true;
             
           }
           else
           {
               drive_dir = indv.acc[current_index / 2];
               //Debug.Log(current_index + "(a): " + drive_dir);
               isTurning = false;
           }
           current_index++;
       }
   
       private void ExtraGravity()
       {
           rigidbody.AddForce(Vector3.down * gravity_multiplayer);
       }
   
       public void Fresh()
       {
           turn_dir = 0f;
           drive_dir = indv.acc[0];
       }
   }

This script checks for collisions, idle or if it finished the track to reset and for each section switches the input for car


    public class EnvObserv : MonoBehaviour
    {
        [SerializeField]
        private GameObject[] checkpoints;
    
        private Movment player;
        private Vector3
            pos_start,
            rot_start;
    
        private float time_start;
        private float last_time_check = 0;
    
        private int no_checkpoints = 0;
        private int last_checkpoint = 0;
    
        private bool forward = true;
        private float best_time = 999999f;
    
    
        // Start is called before the first frame update
        void Awake()
        {
            player = GetComponent<Movment>();
            pos_start = new Vector3(116.9f, 4.29f, -96f);
            rot_start = Vector3.zero;
            time_start = Time.time;
        }
    
        // Update is called once per frame
        void Update()
        {
            if(last_time_check > 10f)
                CheckIdle();
    
            last_time_check += Time.deltaTime;
        }
    
        private void OnCollisionEnter(Collision collision)
        {
            if(collision.collider.CompareTag("TrackWall"))
                Reset(false);
        }
    
        private void OnTriggerEnter(Collider other)
        {
            if (other.CompareTag("Checkpoint"))
            {
                float time = Time.time - time_start;
                Save(time);
                //SaveFirstRun(forward);
                Reset(true, time);
            }
            if (other.CompareTag("Swap"))
            {
                no_checkpoints++;
                player.Swap(false);
                other.gameObject.SetActive(false);
                //SaveFirstRun(forward);
                forward = !forward;
            }
        }
    
        private void Reset(bool finished, float time = -1)
        {
            GetComponent<Rigidbody>().velocity = Vector3.zero;
            GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
    
            if (!finished)
            {
                player.indv = FindObjectOfType<Population>().GetNext(no_checkpoints, time);
            }
            else
            {
                player.indv = FindObjectOfType<Population>().GetNext(no_checkpoints, time);
            }
    
            player.current_index = 0;
            transform.position = pos_start;
            transform.eulerAngles = rot_start;
            no_checkpoints = 0;
            last_time_check = 0;
            time_start = Time.time;
            GetComponent<Rigidbody>().velocity = Vector3.zero;
            GetComponent<Rigidbody>().angularVelocity = Vector3.zero;
            ResetCheckpoints();
            player.Fresh();
        }
    
        void ResetCheckpoints()
        {
            foreach (var check in checkpoints)
            {
                check.SetActive(true);
            }
        }
    
        void CheckIdle()
        {
            if(no_checkpoints - last_checkpoint < 2)
            {
                Reset(false);
            }
    
            last_checkpoint = no_checkpoints;
            last_time_check = 0f;
        }

This script handles the evolution


public class Population : MonoBehaviour
{
    [SerializeField]
    private Movment car;

    [Header("Population variables")]
    public int pop_size = 20;
    public int indv_lenght = 97;
    //public float mutate_prob = 0.01f;
    public int genes_to_mutate = 10;
    public float genes_cross_prob = 0.05f;
    public float deviation = 0.1f;
    public int pos_to_mutate = 4;

    [Header("View")]
    public int current_indv = 0;
    public int current_generation = 1;
    public float best_time_all_generations = 9999f;

    [HideInInspector]
    public List<Individual> population = new List<Individual>();
    // Start is called before the first frame update
    void Awake()
    {
        Init();
        best_time_all_generations = 9999f;
    }

    // Update is called once per frame
    void Update()
    {
    }

    void InitPop()
    {
        for (int i = 0; i < pop_size; i++)
        {
            Individual indv = new Individual();
            indv.Randomize(indv_lenght);
            population.Add(indv);
        }
        StartTrainning();
    }

    void Init()
    {
        Individual first = ReadIndv("E:/Unity/Projects/MiniRace Sectioned/Assets/Scripts/Initial data.txt");
        population.Add(first.GetCopy());

        population = FillWithMutations(first);

        StartTrainning();
    }

    void StartTrainning()
    {
        car.indv = population[0];
        car.Fresh();
    }

    public Individual GetNext(int fitness, float time)
    {
        if(current_indv == pop_size - 1)
        {
            Repopulate();
            current_indv = 0;
            current_generation++;

            if (current_generation > 1000) Debug.Break();
        }
        else
        {
            population[current_indv].fitness = fitness;
            population[current_indv].time = time;
            current_indv++;
        }

        return population[current_indv];
    }

    void Repopulate()
    {
        List<Individual> new_pop = new List<Individual>();

        new_pop.Add(Best());//CHECKED
        
        while (new_pop.Count < pop_size / 2)
        {
            new_pop.Add(Turnir());
        }//CHECKED

        new_pop = Cross(new_pop);//CHECKED
        new_pop = MutationManager(new_pop);//CHECKED

        population.Clear();
        for (int i = 0; i < new_pop.Count; i++)
        {
            population.Add(new_pop[i]);
            population[i].fitness = 0;
            population[i].time = -1;
        }
    }

    Individual Best()
    {
        Individual best = new Individual();
        float best_time = 999999f;
        int best_value = 0;

        for(int i = 0; i < pop_size; i++)
        {
            if (population[i].fitness > best_value)
            {
                best = population[i].GetCopy();
                best_time = population[i].time;
                best_value = population[i].fitness;
            }
            else if (population[i].time > 0) {
                if (population[i].fitness == best_value && population[i].time <= best_time)
                {
                    best = population[i].GetCopy();
                    best_time = population[i].time;
                    best_value = population[i].fitness;
                }
            }
        }
        if (best_time < best_time_all_generations && best_time > 0)
            best_time_all_generations = best_time;

        Debug.Log(current_generation.ToString() + "  :  " + best_time);
        return best;
    }

    //PICKS 3 RANDOM INDIVIDUALS FROM POPULATION AND RETURNS THE BEST ONE
    Individual Turnir()
    {
        Individual indv1 = new Individual();
        Individual indv2 = new Individual();
        Individual indv3 = new Individual();

        int val1 = Random.Range(0, pop_size - 1);
        int val2 = Random.Range(0, pop_size - 1);
        int val3 = Random.Range(0, pop_size - 1);

        indv1 = population[val1];
        indv2 = population[val2];
        indv3 = population[val3];

        List<Individual> turnir = new List<Individual>();

        turnir.Add(indv1);
        turnir.Add(indv2);
        turnir.Add(indv3);

        int best_val = 0;
        int best_ind = -1;
        float best_time = 99999f;

        for(int i = 0; i < 3; i++)
        {
            if(turnir[i].fitness > best_val)
            {
                best_val = turnir[i].fitness;
                best_ind = i;
                best_time = turnir[i].time;
            }
            else if(turnir[i].fitness == best_val && turnir[i].time <= best_time)
            {
                best_val = turnir[i].fitness;
                best_ind = i;
                best_time = turnir[i].time;
            }
        }
        return turnir[best_ind];    
    }

    
    List<Individual> Cross(List<Individual> pop)
    {
        List<Individual> new_pop = new List<Individual>();

        for(int i = 0; i < pop.Count; i++)
        {
            Individual indv1 = new Individual();
            Individual indv2 = new Individual();
            Individual kid = new Individual();

            indv1 = pop[i].GetCopy();
            indv2 = pop[Random.Range(0, pop.Count - 1)].GetCopy();
            kid.fitness = indv1.fitness;
            kid.time = indv1.time;

            for (int j = 0; j < indv1.acc.Count; j++)
            {
                if (Random.Range(0f, 1f) < genes_cross_prob)
                {
                    kid.acc.Add(indv2.acc[j]);
                    kid.turn.Add(indv2.turn[j]);
                }
                else
                {
                    kid.acc.Add(indv1.acc[j]);
                    kid.turn.Add(indv1.turn[j]);
                }
            }

            new_pop.Add(kid);
        }

        for(int i = 0; i < new_pop.Count; i++)
        {
            pop.Add(new_pop[i]);

        }

        return pop;
    }

    //CHECKS IF AN INDIVIDUAL FINISHED OR NOT THE RACE
    //IF IT DIDN'T THEN IT MUTATES LAST 6 GENES THAT WERE ACTIVE BEFOARE CRASH
    //AND IF IT DID FINISHED THE RACE THEN IT MUTATES 10 RANDOM GENES IN HOPE THAT IT
    //WILL LEAD TO BETTER TIME
    List<Individual> MutationManager(List<Individual> pop)
    {
        List<Individual> new_pop = new List<Individual>();
        
        for(int i = 0; i < pop.Count; i++)
        {
            if (i > 0)
            {
                if (pop[i].time == -1)
                {
                    new_pop.Add(MutateLastPos(pop[i].GetCopy()));
                }   
                else
                {
                    new_pop.Add(MutateOne(pop[i].GetCopy()));
                }
            }
            else
                new_pop.Add(pop[i].GetCopy());
        }

        return new_pop;
    }

    //The function mutates last x number of genes that were active before crash
    Individual MutateLastPos(Individual indv)
    {
        int last_checkpoint = indv.fitness;
        int local_pos_to_mutate = pos_to_mutate;

        if(last_checkpoint < local_pos_to_mutate)
        {
            local_pos_to_mutate = last_checkpoint;
        }

        last_checkpoint--;
        last_checkpoint /= 2;

        for (int j = 1; j <= local_pos_to_mutate / 2; j++)
        {
            float new_val_acc = indv.acc[last_checkpoint - j] + Random.Range(0, deviation);
            float new_val_turn = indv.turn[last_checkpoint - j] + Random.Range(-deviation, deviation);

            new_val_acc = Mathf.Clamp(new_val_acc, 0f, 1f);
            new_val_turn = Mathf.Clamp(new_val_turn, -1f, 1f);

            indv.acc[last_checkpoint - j] = new_val_acc;
            indv.turn[last_checkpoint - j] = new_val_turn;
        }
        
        return indv;
    }

    //REFACTORED
    List<Individual> FillWithMutations(Individual indv)
    {
        List<Individual> new_pop = new List<Individual>();

        new_pop.Add(indv.GetCopy());

        while(new_pop.Count < pop_size)
        {
            var mutation = MutateOne(indv.GetCopy());
            new_pop.Add(mutation);
        }
        return new_pop;
    }

    //REFACTORED
    //MUTATES A NUMBER OF GENES FOR THE INDIVIDUAL SENT AS PARAMATER
    Individual MutateOne(Individual indv)
    {
        List<int> genes_to_mutate_acc = new List<int>();
        List<int> genes_to_mutate_turn = new List<int>();

        for(int i = 0; i < genes_to_mutate / 2; i++)
        {
            int val_acc = Random.Range(0, (indv_lenght - 1) / 2);
            int val_turn = Random.Range(0, (indv_lenght - 1) / 2);

            while (genes_to_mutate_acc.Contains(val_acc))
            {
                val_acc = Random.Range(0, (indv_lenght - 1) / 2);
            }

            while (genes_to_mutate_turn.Contains(val_turn))
            {
                val_turn = Random.Range(0, (indv_lenght - 1) / 2);
            }

            genes_to_mutate_acc.Add(val_acc);
            genes_to_mutate_turn.Add(val_turn);
        }

        for(int i = 0; i < genes_to_mutate / 2; i++)
        {
            float acc = indv.acc[genes_to_mutate_acc[i]];
            float turn = indv.turn[genes_to_mutate_turn[i]];

            acc += Random.Range(0, deviation);
            turn += Random.Range(-deviation, deviation);

            acc = Mathf.Clamp(acc, 0f, 1f);
            turn = Mathf.Clamp(turn, -1f, 1f);

            indv.acc[genes_to_mutate_acc[i]] = acc;
            indv.turn[genes_to_mutate_turn[i]] = turn;
        }

        indv.fitness = 0;
        indv.time = -1;

        return indv;
    }

    //READ AN INDIVIDUAL FROM A FILE
    // !!!IMPORTATNT!!! THIS GUYS WILL FINISH THE RACE 100% IN THE FIRST GENERATION
    Individual ReadIndv(string path)
    {
        List<string> lines = new List<string>();
        lines = File.ReadAllLines(path).ToList();

        Individual good_indv = new Individual();

        for(int i = 0; i < lines.Count; i += 2)
        {
            good_indv.acc.Add(float.Parse(lines[i].Substring(2)));
            good_indv.turn.Add(float.Parse(lines[i + 1].Substring(2)));
        }
        return good_indv;
    }


I made the evolution part so that the information for the first car(witch finish the track 100% if I play it first time) is never lost. Indeed i checked it and all the values are the same but for some weird reason when the script reaches 2 generation and that first car plays it will crash even if it has no changed values in its "brain" and the reset function from EnvObserv class resets the car at the starting position with zero velocity/angular velocity.

Here is the Individual class where I store the actions for car


    public class Individual
    {
        public List<float> acc;
        public List<float> turn;
        public int fitness;
        public float time;
        public Individual()
        {
            acc = new List<float>();
            turn = new List<float>();
            fitness = 0;
            time = 0;
        }
    
        public void Randomize(int size)
        {
            for (int i = 0; i < (size + 1) / 2; i++)
            {
                acc.Add(Random.Range(0f, 1f));
                turn.Add(Random.Range(-1f, 1f));
    
            }
        }
    
        public Individual GetCopy()
        {
            Individual new_indv = new Individual();
    
            for(int i = 0; i < acc.Count; i++)
            {
                new_indv.acc.Add(acc[i]);
                new_indv.turn.Add(turn[i]);
                new_indv.fitness = fitness;
                new_indv.time = time;
            }
    
            return new_indv;
        }
     }

I'm 100% sure that the problem is somewhere in reset function. Maybe it doesn't start with zero velocity, or maybe is a slightly error in starting position.

BTW I hardcoded the starting position cuz i tought that maybe unity rounds it in some way and I hardcoded the values from inspector before staring the game

---------->EDIT UPDATE<---------- So I let my script run for 1000 generations and got a some small impruvments that are automatycli saved to a file so I can reuse that genes in hope that the new guys will work better BUT I found out that when I load those genes in the file "Initial data.txt" that is used by Population.ReadIndv() to load the very first individual in my population, the car will actually crash?!

The save functionality is EnvObserv.Save() that is called when a guy finishes the track.

Maybe there is a small change in something between generation 0 and the rest. I can't explain what, how, or why this happens... :/

Here is the map for better vision Here is the map for better vision

0 Answers
Related