Is This Even ML?
Simulating natural selection with a genetic algorithm in Unity.
I’m trying to combine my love for games with the newest tech toy out there ML. I’m learning how to use ML in game development. I came across a tutorial on Genetic Algorithms (GA) which is a very simple ML algorithm.
To cover the basics, GA is an algorithm that can be classified as Machine Learning. Genetic Algorithms is a natural computing algorithm. GA is also an optimization algorithm inspired by natural selection, genetics, and evolution. GA simulates the process of natural selection which means a species that can adapt to the changes in their environment, can then survive and reproduce to the next generation. To summarize, the algorithm simulates “survival of the fittest” in a population group.
We’re going to make a simple game in Unity which simulates a GA. The objective of the game is to click on the brightest colored people that stick out to you in 10 seconds. We’re trying to create a population of people who will survive in the dark. We’re trying to see which colors survive the longest. You only have 10 seconds since we want the player to instinctually click on people that stick out to them in our game. What happens is that as each generation reproduces we will have darker and darker colors. In environments where darker color matters we can see that the darker colors survive longer by measuring how long it takes for a player to click the darker color. The GA works by finding out the parents who survived the longest and breeding those parents. Each generation we will breed the “most fit” parents and with this we are optimizing our population. I didn’t think that natural selection and evolution could also be a Machine Learning algorithm, although it does make sense when I think about it more. Let’s go over what I created together so it makes more sense for me and you!
We first start out with what we need in Unity. We will need a black screen which simulates our environment. Then we will need a Person game object which will have a Box Collider 2D component and a DNA script. At a very high level we’re creating the “DNA” for our person object. The DNA will define the color of our person game object. We will be randomizing this color and showing it to the player of our game. When the player clicks on the brightest people we save this time that the person object was clicked as timeToDie. We will have a list of all the people that died and we will be sorting that list in descending order. This means we will have the strongest, fittest people at the top and the weakest people (the brightest people) at the bottom of the list. We will then use a Breed method to create a new population. This Breed method randomly gets the color DNA from two parents and then combines those colors for our new population. We then repeat the cycle using Unity’s update method which calls the BreedNewPopulation method (I’ll go over this in more detail below).
In our DNA script which we attach to our Person gameobject has the following variables:
Public float red // We need to utilize a RGB value for our sprite to randomize the colors so we define the red, green and blue variables
Public float green
Public float blue
Bool dead = false // This variable is for determining when we click a sprite, the Person gameobject dies and dead is then set to true
Public float timeToDie = 0f // For counting how long a Person survived for
SpriteRenderer spriteRenderer // For accessing the Sprite Renderer associated to our Person
Collider2D sCollider2D // For accessing the collider associated to our Person
The methods we have in our DNA script are:
Void OnMouseDown // This method is for handling when a Person is clicked
Void Start // We retrieve the necessary components like our Sprite Renderer and Collider 2D. We also set the sprite’s color in our start method.
The OnMouseDown method is a callback function that belongs to the MonoBehaviour class. It is invoked when the user presses the left mouse button while the mouse cursor is positioned over a Collider component attached to the same gameobject. In our method we set dead to true because the Person gameobject was clicked on. We set the timeToDie with a time and we also set the active state of our Sprite Renderer and Collider 2D to false. PopulationManager is another script which has all the meat of our project, more details on this class below.
void OnMouseDown()
{
dead = true;
timeToDie = PopulationManager.elapsed;
spriteRenderer.enabled = false;
sCollider2D.enabled = false;
}In our start method we do some simple initialization for our Sprite Renderer and Collider 2D. What’s also important here is we set the red, green, and blue values for the Sprite Renderer’s color. Remember the Sprite Renderer is attached to our Person gameobject so we’re essentially setting the color for our Person.
void Start()
{
spriteRenderer = GetComponent<SpriteRenderer>();
sCollider2D = GetComponent<Collider2D>();
spriteRenderer.color = new Color(red, green, blue);
}In our PopulationManager script we have the following variables:
Public GameObject personPrefab;
Public int populationSize = 10;
List<GameObject> population = new List<GameObject>();
Public static float elapsed = 0f;
Int trialTime = 10;
Int generation = 1;
The methods in our PopulationManager script are:
OnGUI
Void Start
GameObject Breed
Void BreedNewPopulation
Void Update
void OnGUI()
{
guiStyle.fontSize = 50;
guiStyle.normal.textColor = Color.white; // Set text color to white for visibility
GUI.Label(new Rect(10, 10, 200, 20), “Generation: “ + generation, guiStyle);
GUI.Label(new Rect(10, 65, 100, 20), “Trial Time: “ + (int)elapsed, guiStyle);
}In the OnGUI method we use Unity’s immediate UI instead of using TextMeshPro and a canvas. We use code instead of Unity’s components to create a quick UI. We’re essentially setting up a font size, a font color, and where in the game screen should we display this UI. We display a UI in the top left of our screen.
void Start()
{
for (int i = 0; i < populationSize; i++)
{
Vector3 pos = new Vector3(Random.Range(-9, 9), Random.Range(-4.5f, 4.5f), 0);
GameObject go = Instantiate(personPrefab, pos, Quaternion.identity);
go.GetComponent<DNA>().red = Random.Range(0f, 1f);
go.GetComponent<DNA>().green = Random.Range(0f, 1f);
go.GetComponent<DNA>().blue = Random.Range(0f, 1f);
population.Add(go);
}
}In our Start method we set up a for loop, which loops through the populationSize that we defined. We then set up a position on the screen where we will create a person at a random position. The random range that we use for the person’s random position was measured based on the size of our sprite for our person gameobject. We measured across the screen we could fit about 18 of our person gameobjects lengthwise and about 9 person gameobjects heightwise. This is why our range for the Vector3 pos is (-9, 9) and (-4.5, 4.5). Next up, we instantiate a person gameobject and that person will get a random red, green, or blue value. We then add these new instantiated person gameobjects to our population list that we created.
GameObject Breed(GameObject parent1, GameObject parent2)
{
Vector3 pos = new Vector3(Random.Range(-9, 9), Random.Range(-4.5f, 4.5f), 0);
GameObject offspring = Instantiate(personPrefab, pos, Quaternion.identity);
DNA dna1 = parent1.GetComponent<DNA>();
DNA dna2 = parent2.GetComponent<DNA>();
// Inherit from parents with 50/50 chance for each color component
// this is to create a 50% chance of getting DNA from either parent 1 or parent 2
offspring.GetComponent<DNA>().red = Random.Range(0, 10) < 5 ? dna1.red : dna2.red;
offspring.GetComponent<DNA>().green = Random.Range(0, 10) < 5 ? dna1.green : dna2.green;
offspring.GetComponent<DNA>().blue = Random.Range(0, 10) < 5 ? dna1.blue : dna2.blue;
return offspring;
}The Breed method returns an object because we use this method within the BreedNewPopulation method below. We set another random position. We create an offspring gameobject and then we also get the colors from the two parents. The parents are retrieved from a sorted list which I’ll explain more below. The most important part of our Breed method is the lines below:
offspring.GetComponent<DNA>().red = Random.Range(0, 10) < 5 ? dna1.red : dna2.red;
offspring.GetComponent<DNA>().green = Random.Range(0, 10) < 5 ? dna1.green : dna2.green;
offspring.GetComponent<DNA>().blue = Random.Range(0, 10) < 5 ? dna1.blue : dna2.blue; This code is how we are deciding which color we get from the two parents. There is a 50% chance we get a red color from parent 1 or parent 2. The same randomization occurs for the green and blue values. This randomization occurs after we have a sorted list of who is the most fit in our population. Let’s look into the method below to clarify what’s happening.
void BreedNewPopulation()
{
// STEP 1: Sort population by fitness (survival time) - DESCENDING ORDER
// Individuals never clicked have timeToDie = 0, so we need to handle this
List<GameObject> sortedList = population.OrderByDescending(o => {
float timeToDie = o.GetComponent<DNA>().timeToDie;
// If timeToDie is 0, it means they survived the full 10 seconds
return timeToDie == 0f ? elapsed : timeToDie;
}).ToList();
population.Clear(); // Clear the current population list
// STEP 2: Identify the top 50% survivors as breeding candidates
int half = sortedList.Count / 2;
// STEP 3: Create exactly 10 new offspring by calling Breed() repeatedly
for (int i = 0; i < populationSize; i++)
{
// Pick two random parents from the top 50% (indices 0 to half-1)
GameObject parent1 = sortedList[Random.Range(0, half)];
GameObject parent2 = sortedList[Random.Range(0, half)];
// Call Breed() to create one child and add it to population
population.Add(Breed(parent1, parent2));
}
// STEP 4: Clean up - destroy all old generation GameObjects
for (int i = 0; i < sortedList.Count; i++)
{
Destroy(sortedList[i]);
}
generation++;
}The BreedNewPopulation method has some comments added from AI but I can add more context to help us understand what is happening. We first sort the list in descending order meaning we sort the list with the most fit or longest time surviving at the top of the list, the least fit or the person gameobjects who were clicked the fastest are at the bottom of the list. If timeToDie is zero then the person gameobject was not clicked and they survived the whole time. They would then be at the top of the sorted list. We then clear the population list and keep only the top half of the sorted list. These are the candidates that we will use for breeding within the Breed method earlier. We set up a for loop here and randomly pick parents from the sorted list. Then we call the Breed method from earlier on the 2 parents we randomly selected from our sorted list. We then add the parents which have breeded, and now have offspring from the colors that we randomly chose from each parent. Recall that we had a 50% chance of getting the colors from one of the two parents. The colors from those parents are then added to an offspring which is returned from our Breed method and passed into the new population list. Lastly we have another for loop which destroys all the old generation’s gameobjects.
void Update()
{
elapsed += Time.deltaTime;
if (elapsed > trialTime)
{
BreedNewPopulation();
elapsed = 0f; // Reset elapsed time for the next trial
}
}In our update method we keep track of the time elapsed by adding Time.deltaTime to our elapsed variables. The conditional we set up is to check if elapsed > trialTime (10 seconds). Once we get over 10 seconds then we run the BreedNewPopulation method and reset our timer. This is the game loop wherein you have 10 seconds to really select the brightest colors, otherwise you’ll be thinking about which colors to pick and natural selection will be a bit biased.
This is my foray into ML within Unity. I had a fun time following a tutorial to create this game. There were some ways to add some variety to the game like changing the size of the person gameobject or adding a harmful or beneficial mutation to the person gameobject. This is a great starter project on how to start messing with Machine Learning.
I’ve added a link to my game below so you can play it yourself, enjoy!




Hey, great read as always. What if the environment had dynamic light conditions, altering fitnes criteria?