Pentakl BlogArticles: 2
Playing with life2018-12-18
author Lukerelated with Game of LifeShow article »
Scare the birds2018-11-18
author Lukerelated with Mini CityShow article »
Scare the birds

 Purpose 

Mini City as a living city has to live. How to achieve this? It's not a simple task because not only vehicles and citizens makes city full of life. As the first step to reach this goal we decided to put birds all around the city. Birds make player to feel more immerse with the scene, with the simulated environment.

Birds on street lamp

 Trees, buildings and other props 

This kind of ideas have to be considered from the very beginning otherwise developers and level designers will have a lot more work. Every tree, building, street lamp, dumpster should be equipped with proper navigation (landing) points (this red spheres on image below are not an apples). Navigation points are necessary for birds' assets to know where they are able to land and where they can fly next.

Tree with navigation points

 Movement 

Every movable element of the game such us citizens and birds is calculated at play time even behind camera. Because birds are not crucial part of the game, they can not make big impact on performance factor. So we had to find a simple way of creating not a linear path of birds movement and at the same time as much close to real birds behavior as it is possible. Our choice fell on randomization of destination point with remembaring of ten last visited points. This means that at the moment when birds start their flight, it chooses randomly next destination navigation point with exclusion of visited points.

Birds movement between navigation points

 Scaring 

When player approaches near birds, they will become scared. This action will instantly trigger their begging of flight. This will simulate situation when scared birds fly away from scaring source. Their choice of destination will be limited to negative direction from scaring source.

Scaring birds
Playing with life

 Conway's 23/3 

Game of life is a cell's state simulator. It's rules are calculated once per generation, so it is some kind of turn based approach. In mode 23/3 rules are very simple for every cell: living cell with 2 or 3 neighbors will stay alive to another generation otherwise it will die, empty cell will become alive only if it has exactly 3 neighbors.

There are different modes of Game of life (or you can even define your own), but we will use 23/3 as it is mode which was originally invented and used by Conway.

Final look

 Three classes solution 

We will provide three classes solution for the purpose of this tutorial. First class will represent generation flow manager, which will be also our main unity script. It will be also responsible for initialization and update of the whole application. Other two classes will be specialized classes for encapsulation Cells and Rules.

 Project concepts 

The project is conceptually very simple, it has only one aspect which should be taken under consideration. We assume that project array of cells will be 8000 long and wide, so this gives 64 million cells for update every generation. It is too much. As you probably already see, empty cells should be updated but not all, only those that are neighbors of living cells.

In addition, it follows from the above that the mode of 23/3 in our application should be extended by one new rule: if absolute position of living cell in X or Z axis is grater than 8000 it will die.

 Project settings 

Project in Unity for the purpose of this tutorial should have at two GameObjects named: 'Scripts' and 'Cells'. To 'Scripts' GameObject you should add component - script named Generation.cs. GameObject named 'Cells' will be root of all Cell GameObjects, only GameObjects placed as a child of 'Cells' will be take into account during update.

Unity3D Scene Settings

 Generations 

Generation.cs

using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Generation : MonoBehaviour
{
	#region Unity Singleton
	public static Generation m_Instance = null;
	public static Generation Instance
	{
		get
		{
			if (Generation.m_Instance == null)
			{
				Generation.m_Instance = GameObject.Find("Scripts").GetComponent<Generation>();
			}
			return Generation.m_Instance;
		}

		private set
		{
			Generation.m_Instance = value;
		}
	}
	#endregion

	public Text Info = null;

	private List<Cell> Cells = new List<Cell>();
	private HashSet<Rule> Rules = new HashSet<Rule>();

	private Int64 CellsCount = 0;
	private Int64 GenerationNbr = 0;

	private GameObject CellsRootGO = null;
	private Boolean IsAlive = false;
	private Boolean IsAuto = true;
	private DateTime LastTime = DateTime.Now; 

	/// <summary>
	/// Register adds cell to cells list
	/// </summary>
	/// <param name="p_Cell">Cell object</param>
	public void Register(Cell p_Cell)
	{
		this.Cells.Add(p_Cell);
		this.CellsCount++;
	}

	/// <summary>
	/// UnRegister removes cell from cells list
	/// </summary>
	/// <param name="p_Cell">Cell object</param>
	public void UnRegister(Cell p_Cell)
	{
		this.Cells.Remove(p_Cell);
		this.CellsCount--;
	}
	
	/// <summary>
	/// RulesCheck is responsible for call rules checking in all living cells
	/// </summary>
	public void RulesCheck()
	{
		foreach (Cell l_Cell in this.Cells)
			l_Cell.CheckRules();
	}

	/// <summary>
	/// EnumerateExisting is initialization method used to construct initial list of cells
	/// </summary>
	public void EnumerateExisting()
	{
		for(Int32 i=0; i<this.CellsRootGO.transform.childCount;i++)
		{
			GameObject l_Child = this.CellsRootGO.transform.GetChild(i).gameObject;
			Cell l_Cell = l_Child.GetComponent<Cell>();
			if (l_Cell == null)
				l_Cell = l_Child.AddComponent<Cell>();
			this.Register(l_Cell);
		}
	}

	/// <summary>
	/// RuleAdd adds rule to rule hashset
	/// </summary>
	/// <param name="p_Rule">Rule object</param>
	public void RuleAdd(Rule p_Rule)
	{
		this.Rules.Add(p_Rule);
	}

	/// <summary>
	/// RuleApply calls all rules to be applied
	/// </summary>
	public void RulesApply()
	{
		foreach (Rule l_Rule in this.Rules)
		{
			l_Rule.Apply(this.CellsRootGO.transform);
		}
	}

	/// <summary>
	/// Clear is responsible for clearing after full generation process ends
	/// </summary>
	public void Clear()
	{
		this.Rules.Clear();
	}

	/// <summary>
	/// GameObject Start handler
	/// </summary>
	public void Start()
	{
		this.CellsRootGO = GameObject.Find("Cells");
		this.EnumerateExisting();
	}

	/// <summary>
	/// GameObject Update handler
	/// </summary>
	private void Update()
	{
		if (this.IsAlive)
		{
			this.GenerationNbr++;
			this.RulesCheck();
			this.RulesApply();
			this.Clear();

			this.IsAlive = false;
		}

		if(this.Info != null)
			this.Info.text = "Generation: " + this.GenerationNbr + "\n" + "Cells: " + this.CellsCount;

		if(this.IsAuto)
		{
			if ((DateTime.Now - this.LastTime) > TimeSpan.FromSeconds(1))
			{
				this.LastTime = DateTime.Now;
				this.IsAlive = true;
			}
		}
	}
}

 Cells 

Cell.cs

using System;
using UnityEngine;

public class Cell : MonoBehaviour
{
	/// <summary>
	/// Primary implementation of checking rules.
	/// Checking is made for cell itself and for neighbours cells.
	/// </summary>
	public void CheckRules()
	{
		//23\3
		Int32 l_X = Mathf.FloorToInt(this.transform.position.x);
		Int32 l_Z = Mathf.FloorToInt(this.transform.position.z);
		Int32 l_NeighboursLive = Cell.NeighboursAliveCount(l_X, l_Z);

		if (Math.Abs(l_X) > 8000 || Math.Abs(l_Z) > 8000)
			Generation.Instance.RuleAdd(new Rule(l_X, l_Z, false, this));
		else if (l_NeighboursLive != 2 && l_NeighboursLive != 3)
				Generation.Instance.RuleAdd(new Rule(l_X, l_Z, false, this));

		for (Int32 i = l_X - 1; i <= l_X + 1; i++)
			for (Int32 j = l_Z - 1; j <= l_Z + 1; j++)
			{	
				if (!(i == l_X && j == l_Z))
				{
					l_NeighboursLive = Cell.NeighboursAliveCount(i, j);
					if (l_NeighboursLive == 3)
					{
						Cell l_Cell = Cell.Get(i, j);
						if (l_Cell == null)
							Generation.Instance.RuleAdd(new Rule(i, j, true, l_Cell));
					}
				}
			}
	}

	/// <summary>
	/// Destroys GameObject and removes it from cells list
	/// </summary>
	public void Destroy()
	{
		Generation.Instance.UnRegister(this);
		GameObject.Destroy(this.gameObject);
	}

	/// <summary>
	/// Helper. Gets Cell located on coordinates.
	/// </summary>
	/// <param name="p_X">X axis coordinate</param>
	/// <param name="p_Z">Z axis coordinate</param>
	/// <returns></returns>
	public static Cell Get(Int32 p_X, Int32 p_Z)
	{
		Collider[] l_Colliders = Physics.OverlapSphere(new Vector3(p_X, 0, p_Z), 1.1f);
		foreach (Collider l_Collider in l_Colliders)
		{
			GameObject l_CellGO = l_Collider.gameObject;
			if (l_CellGO.transform.position.x == p_X && l_CellGO.transform.position.z == p_Z)
			{
				return l_CellGO.GetComponent<Cell>();
			}
		}
		return null;
	}

	/// <summary>
	/// Helper. Gets count of alive neighbours.
	/// </summary>
	/// <param name="p_X">X axis coordinate.</param>
	/// <param name="p_Z">Z axis coordinate.</param>
	/// <returns></returns>
	public static Int32 NeighboursAliveCount(Int32 p_X, Int32 p_Z)
	{
		Int32 l_NeighboursLive = 0;
		for (Int32 x = p_X - 1; x <= p_X + 1; x++)
			for (Int32 z = p_Z - 1; z <= p_Z + 1; z++)
				if (!(x == p_X && z == p_Z))
					if (Cell.Get(x, z) != null)
						l_NeighboursLive++;

		return l_NeighboursLive;
	}
}

 Rules 

Rule.cs

using System;
using UnityEngine;

public class Rule : IEquatable<Rule>
{
	public Int32 X = 0;
	public Int32 Z = 0;
	public Boolean MakeLive = false;
	public Boolean MakeDead = false;
	public Cell Target = null;

	/// <summary>
	/// Constructor.
	/// </summary>
	/// <param name="p_X">X axis coordinate</param>
	/// <param name="p_Z">Z axis coordinate</param>
	/// <param name="p_IsLive">Cell's state to apply</param>
	/// <param name="p_Target">Cell object</param>
	public Rule(Int32 p_X, Int32 p_Z, Boolean p_IsLive, Cell p_Target)
	{
		this.X = p_X;
		this.Z = p_Z;
		if (p_IsLive)
			this.MakeLive = true;
		else
			this.MakeDead = true;
		this.Target = p_Target;
	}

	#region IEquatable implementation
	public override bool Equals(object p_Coordinate)
	{
		Rule l_Coordinate = (Rule)p_Coordinate;
		return (this.X == l_Coordinate.X && this.Z == l_Coordinate.Z);
	}

	public bool Equals(Rule p_Coordinate)
	{
		return (this.X == p_Coordinate.X && this.Z == p_Coordinate.Z);
	}

	public override int GetHashCode()
	{
		return this.X * 31 + this.Z;
	}
	#endregion

	/// <summary>
	/// Makes cell alive or dead
	/// </summary>
	/// <param name="p_Parent">Cell's root transfrom</param>
	public void Apply(Transform p_Parent)
	{
		if(this.MakeLive)
		{
			GameObject l_CellGO = GameObject.CreatePrimitive(PrimitiveType.Cube);
			l_CellGO.transform.parent = p_Parent;
			l_CellGO.transform.position = new Vector3(this.X, 0, this.Z);
			Cell l_Cell = l_CellGO.AddComponent<Cell>();
			Generation.Instance.Register(l_Cell);
		}

		if(this.MakeDead)
		{
			if(this.Target!=null)
				this.Target.Destroy();
		}
	}
}

 Let's play with life 

Additionaly you could add Canvas object with Text UI object. Add them to the scene. Text object have to be dragged to Generation script on Info field. It will display basic informations such us generation cycle number and living cells count.

Now you can place cubes (place them as a childs of the 'Cells' GameObject) in any combination on scene and test your initially designed composition. Remember to place cubes only on X and Z axis with Y axis always set to 0, also remember that any coordinates must be real numbers.

 Online version 

If you would like to see, how this works in final product you can check our full version of Conway's 23/3 Game of Life.