Cellular Automaton

What will be covered in this Tutorial?

  • What is Cellular Automaton or Automata?
  • How will it affect our tilemap?
  • Implementation

What is Cellular Automaton or Automata?

A cellular automaton (CA) is a collection of cells arranged in a grid, such that each cell changes state as a function of time according to a defined set of rules that includes the states of neighboring cells.

Now that we have the complicated explanation out of the way, let’s simplify things a little.

Imagine having a 10×10 Grid, in some cells you have water, in some cells you have grass.
And you would step over each cell one by one, and check the 8 neighbor cells and count
how many of those neighbors are of the same type as the one you are standing on.

We can decide based on that number, if we want to keep this tile the same type or replace it with the dominant tile type or any regular tile type of choice.

I will be implementing both variants.

How will it affect our tilemap?

Since the last tutorial we have generated a bunch of trees and scattered them randomly across our grass terrain. But in some cases we want to have more dense areas of trees..
Also known as forests. In this tutorial I’ll be applying Cellular Automata on our trees to group them together and create dense forests around our island.

Implementation

Cellular automata might sound complicated when reading the definition, but it is really not. All we need is a method to retrieve our neighbors of any given coordinate.
Let’s begin with that.

In TilemapStructure let’s add a method GetNeighbors(int x, int y);

public List<KeyValuePair<Vector2Int, int>> GetNeighbors(int tileX, int tileY)
{
	int startX = tileX - 1;
	int startY = tileY - 1;
	int endX = tileX + 1;
	int endY = tileY + 1;

	var neighbors = new List<KeyValuePair<Vector2Int, int>>();
	for (int x = startX; x < endX + 1; x++)
	{
		for (int y = startY; y < endY + 1; y++)
		{
			// We don't need to add the tile we are getting the neighbors of.
			if (x == tileX && y == tileY) continue;

			// Check if the tile is within the tilemap, otherwise we don't need to pass it along
			// As it would be an invalid neighbor
			if (InBounds(x, y))
			{
				// Pass along a key value pair of the coordinate + the tile type
				neighbors.Add(new KeyValuePair<Vector2Int, int>(new Vector2Int(x, y), GetTile(x, y)));
			}
		}
	}
	return neighbors;
}

With this method, we can now retrieve all 8 neighbors of a given tile coordinate.
Let’s create another Script called ‘CellularAutomata’ and derive it from AlgorithmBase.
We should also add the CreateAssetMenu attribute, so we can properly create the object in unity.

We’ll also add some configuration, like what is the target tile type we should apply the automata on, what tile should we replace it by if the criteria is not met.
And we’ll also implement the dominant tile selection as I mentioned before.

[CreateAssetMenu(fileName = "CellularAutomata", menuName = "Algorithms/CellularAutomata")]
public class CellularAutomata : AlgorithmBase
{
	public int MinAlive, Repetitions;

	[Tooltip("If this is checked, ReplacedBy will have no effect.")]
	public bool ReplaceByDominantTile;

	public ObjectTileType TargetTile, ReplacedBy;

	public override void Apply(TilemapStructure tilemap)
	{
		
	}
}

A brief idea of how the dominant tile would work is simple.
Imagine tile x5y5 is of type Tree, and only 3 neighbors are of the same tree type.
And our MinAlive criteria is 4, then we check all neighbors types and check which type has the most alive, so let’s say 1 neighbor is a blue rose, and the other 4 that are left are red roses. (because the other 3 are trees)
Then the dominant type will be the red rose because there are 4 tiles of it and that will be the tiletype x5y5 will be replaced with.

Let’s take a look at the plain replace by tile logic WITHOUT the dominant tile logic.

int targetTileId = (int)TargetTile;
int replaceTileId = (int)ReplacedBy;
for (int i = 0; i < Repetitions; i++)
{
	for (int x = 0; x < tilemap.Width; x++)
	{
		for (int y = 0; y < tilemap.Height; y++)
		{
			// Check if the current tile is our target tile
			var tile = tilemap.GetTile(x, y);
			if (tile == targetTileId)
			{
				// Retrieve all 8 neighbors of our current tile
				var neighbors = tilemap.GetNeighbors(x, y);

				// Count all the neighbors that are of type target tile
				int targetTilesCount = neighbors.Count(a => a.Value == targetTileId);

				// If the min alive count is not reached, we replace the tile
				if (targetTilesCount < MinAlive)
				{
					tilemap.SetTile(x, y, replaceTileId);   
				}
			}
		}
	}
}

Here we loop over all the tiles and we retrieve the neighbors of the current tile only if the current tile matches our configured TargetTile.
Then we get the total count of neighbors where the tiletype is the target tile type.
And if that count is smaller than the MinAlive configuration, we replace it with our configured ReplaceTile.

Let’s add the dominant tile logic as an extra option.

// If the min alive count is not reached, we replace the tile
if (targetTilesCount < MinAlive)
{
	if (ReplaceByDominantTile)
	{
		// Group tiles on tiletype, then order them in descending order based on group size
		// Select the group's key which is the tiletype because thats what we grouped on
		// And select the first one (first group's key), because that's the dominant tile type
		var dominantTile = neighbors
			.GroupBy(a => a.Value)
			.OrderByDescending(a => a.Count())
			.Select(a => a.Key)
			.First();

		tilemap.SetTile(x, y, dominantTile);
	}
	else
	{
		tilemap.SetTile(x, y, replaceTileId);
	}     
}

Here we use System.Linq to chain several actions together.
We are grouping all the neighbors on their TileType.
Which means that if there are 3 different tile types let’s say, red roses, blue roses, and trees.
It will create a group for each tile type, and each group will have the correct amount of roses, or trees.
We then Order the groups by descending on the Amount that the groups contain.
We then select the Key value of the group which is the TileType, and we return the first.
So the top most value which is the highest count of tiletype in the neighbors. (dominant tiletype).

Here is how the final class should look like:

[CreateAssetMenu(fileName = "CellularAutomata", menuName = "Algorithms/CellularAutomata")]
public class CellularAutomata : AlgorithmBase
{
	public int MinAlive, Repetitions;

	[Tooltip("If this is checked, ReplacedBy will have no effect.")]
	public bool ReplaceByDominantTile;

	public ObjectTileType TargetTile, ReplacedBy;

	public override void Apply(TilemapStructure tilemap)
	{
		int targetTileId = (int)TargetTile;
		int replaceTileId = (int)ReplacedBy;
		for (int i = 0; i < Repetitions; i++)
		{
			for (int x = 0; x < tilemap.Width; x++)
			{
				for (int y = 0; y < tilemap.Height; y++)
				{
					// Check if the current tile is our target tile
					var tile = tilemap.GetTile(x, y);
					if (tile == targetTileId)
					{
						// Retrieve all 8 neighbors of our current tile
						var neighbors = tilemap.GetNeighbors(x, y);

						// Count all the neighbors that are of type target tile
						int targetTilesCount = neighbors.Count(a => a.Value == targetTileId);

						// If the min alive count is not reached, we replace the tile
						if (targetTilesCount < MinAlive)
						{
							if (ReplaceByDominantTile)
							{
								// Group tiles on tiletype, then order them in descending order based on group size
								// Select the group's key which is the tiletype because thats what we grouped on
								// And select the first one (first group's key), because that's the dominant tile type
								var dominantTile = neighbors
									.GroupBy(a => a.Value)
									.OrderByDescending(a => a.Count())
									.Select(a => a.Key)
									.First();

								tilemap.SetTile(x, y, dominantTile);
							}
							else
							{
								tilemap.SetTile(x, y, replaceTileId);
							}     
						}
					}
				}
			}
		}
	}
}

Let’s now Create the ScriptableObject algorithm in unity and assign it to our ObjectMap algorithm’s right after the TreeGeneration algorithm.
Remember, the algorithms are executed in the same order. So the tree’s must be generated first, then the automata applied.

Here is my configuration:

With automata applied at tree generation 65% per tile
Without automata applied at tree generation 65% per tile

Note that 65% tree generation without automata is very obscure and unrealistic.

Thanks for checking out this tutorial, and I hope you now have a better understanding of
how Cellular automata can be applied to create interesting effects.

The github repository commit for this Tutorial can be found here:
https://github.com/Venom0us/Code2DTutorials/commit/15084d18396d1db638b6e4323e52b0a902f489da

The source files can be found here:
https://github.com/Venom0us/Code2DTutorials/releases/tag/v6

Leave a comment

Design a site like this with WordPress.com
Get started