Skip to content

Instantly share code, notes, and snippets.

@lewiji
Last active March 6, 2021 16:33
Show Gist options
  • Save lewiji/83d3fdab65dd80b1005943666df22861 to your computer and use it in GitHub Desktop.
Save lewiji/83d3fdab65dd80b1005943666df22861 to your computer and use it in GitHub Desktop.
Godot C# Gridmap Cellular Automata sample code from my game DUNGEN PAPAÑCA

Godot Mono (C#) Gridmap Cellular Automata

This is some sample code for some cellular automata based generation I've been working on in Godot for the last couple of days.

It probably won't work if just dropped into your project but it should give a good reference for doing something similar.

MapGen.cs

using System;
using Godot;
using Godot.Collections;

namespace Dungen.scripts_cs.map
{
	[Tool]
	public class MapGen : Spatial
	{
		[Export] public OpenSimplexNoise Noise;
		[Export] public bool Reset
		{
			get => _reset;
			set
			{
				if (!_reset && value && _strategies != null && _strategies.Count > 0)
				{
					_reset = true;
					Playing = false;
					_currentStrategy = null;
					foreach (var strat in _strategies)
					{
						strat.QueueFree();
					}
					PrepMap();
					CreateStrategies();
					BeginStage();
				}

				_reset = false;
			}
		}
		[Export] public bool Playing
		{
			get => _playing;
			set
			{
				if (!_playing && value)
				{
					_playing = true;
					ResumeGeneration();
					return;
				}
				if (_playing && !value)
				{
					_playing = false;
					if (_strategies?.Count > 0)
					{
						PauseGeneration();
					}
				}
			}
		}
		private bool _playing; /* This allows toggling via the godot editor via {Playing} */
		private bool _generating; /* Whether to process generation steps or not. */
		private bool _reset; /* Reset and regenerate via {Reset} */
		private Array<MapGenStrategy> _strategies; /* The generation strategies to be used */
		private MapGenStrategy _currentStrategy;
		private Godot.GridMap _gridMap;
		private int _levelWidth = 52;
		private int _levelHeight = 96;
		private float _dtCounter;

		public override void _Ready()
		{
			base._Ready();
			GD.Randomize();
			_gridMap = (Godot.GridMap) GetNode("./GridMap");
			CreateStrategies();

			Playing = !Engine.EditorHint;

			PrepMap();
			BeginStage();
		}

		/* Fill in map with a floor and base tile.  */
		private void PrepMap()
		{
			var levelArea = _levelHeight * _levelWidth;
			for (int i = 0; i < levelArea; i++)
			{
				_gridMap.SetCellItem(
					i % _levelWidth,
					-1,
					(int) Math.Floor((float) i / _levelWidth),
					24); /* Stone floor */
				_gridMap.SetCellItem(
					i % _levelWidth, 0,
					(int) Math.Floor(((float) i / _levelWidth)),
					144 /* Fog */
					);
			}
		}

		/* My game has multiple mapgen strategies that can be layered,
		   but this is using just one. */
		private void CreateStrategies()
		{
			_strategies = new Array<MapGenStrategy> {new AutomataStrategy()};
			foreach (var strat in _strategies)
			{
				AddChild(strat);
			}
		}

		/* I am doing mapgen in process purely for presentation purposes (hence the delta time counter
		   to make it slower) in a production game you would probably do this synchronously or on a thread
		   for speed */
		public override void _Process(float delta)
		{
			base._Process(delta);
			if (_generating && _dtCounter == 0 && _strategies?.Count > 0)
			{
				RunGenerationStep(_currentStrategy);
			}
			/* this throttles */
			_dtCounter += delta;
			if (_dtCounter >= 0.4f)  _dtCounter = 0;
		}

		void BeginStage(int stage = 0)
		{
			if (stage >= _strategies.Count)
			{
				_generating = false;
				GD.Print($"# All stages complete #");
				return;
			}

			GD.Print($"## Stage {stage} ##");
			_currentStrategy = _strategies[stage];
			_currentStrategy.Init(_levelWidth, _levelHeight, Noise);
			_currentStrategy.OnFinishedStage += () =>
			{
				BeginStage(stage + 1);
			};
		}


		private void RunGenerationStep(MapGenStrategy genStrategy)
		{
			_gridMap = genStrategy.Step(_gridMap);
		}

		public void ResumeGeneration()
		{
			_generating = true;
		}

		public void PauseGeneration()
		{
			_generating = false;
		}
	}
}

MapGenStrategy.cs

AutomataStrategy.cs

using System;
using System.Collections.Generic;
using System.Linq;
using Godot;

namespace Dungen.scripts_cs.map
{
    /* cell data struct - to feed into AStar or similar */
    public struct AutomataCell
    {
        public int State;
        public Vector3 Position;
        public int Id;
        public bool Blocked;

        public AutomataCell(Vector3 pos, int id, bool blocked, int state = 0)
        {
            Position = pos;
            Id = id;
            Blocked = blocked;
            State = state;
        }

        public override string ToString() => $"Automata/{Id}: ({Position.x}, {Position.z}) blocked: {Blocked}";
    }

    public class AutomataStrategy : MapGenStrategy
    {
        /* My game is 3D and may support multi-levels later, but this is a 2D algorithm. */
        private Dictionary<Vector3, AutomataCell> _cells = new Dictionary<Vector3, AutomataCell>();

        /* array of tile ids to be used for rendering */
        private static int[] _bzTiles = new int[] {3, 4, 9, 14, 17, 18, 19, 20, 21, 22, 23};
        /* cellular automata parameters
           see:  https://www.hermetic.ch/compsci/cellular_automata_algorithms.htm#life */
        private int _k1 = Math.Max(1, (int) GD.Randi() % _bzTiles.Length);
        private int _k2 = Math.Max(1, (int) GD.Randi() % _bzTiles.Length);
        private int _g = ((int) GD.Randi() % _bzTiles.Length / 2) + 1;
        private int _q;

        /** See:  */
        public AutomataStrategy()
        {
            StrategyName = "cellular_automata";
            StepMax = 6;
        }

        public override void _Ready()
        {
            base._Ready();
            GD.Randomize();
        }

        public override void Init(int lvlWidth, int lvlHeight, OpenSimplexNoise noise)
        {
            base.Init(lvlWidth, lvlHeight, noise);
            GD.Print("AutomataStrategy initialising");
            noise.Seed = (int) GD.Randi();
            for (var i = 0; i < lvlWidth * lvlHeight; i++)
            {
                var cellpos = new Vector3(i % lvlWidth, 0, (int) Math.Floor(((float) i / lvlWidth)));
                var pointOfNoise = noise.GetNoise3dv(cellpos);
                var qstate = pointOfNoise > 0.2 ? 1 : 0;
                _cells.Add(cellpos, new AutomataCell(cellpos,
                    i, false, qstate));
            }
        }

        public override Godot.GridMap Step(Godot.GridMap gridMap)
        {
            GD.Print($"AutomataStrategy stepping: {StepCurrent}");
            var newCells = new Dictionary<Vector3, AutomataCell>();
            foreach (var item in _cells)
            {
                var cell = item.Value;
                var newCell = CalculateStrategyBelousovZhabotinsky(cell);
                newCells[item.Key] = newCell;
                if (newCell.State > 0)
                {
                    try
                    {
                        gridMap.SetCellItem((int) newCell.Position.x, (int) newCell.Position.y,
                            (int) newCell.Position.z, newCell.State > 0 ? _bzTiles[newCell.State] : 144);
                    }
                    catch (Exception)
                    {
                        GD.Print(newCell.State);
                    }
                }
            }

            _cells = newCells;

            return base.Step(gridMap);
        }



        private List<AutomataCell> GetNeighbours(AutomataCell cell)
        {
            var nList = new List<AutomataCell>();

            foreach (var nPos in _neighbourPositions)
            {
                var neighbourCell = MaybeGetCell(cell.Position + nPos);
                if (neighbourCell.HasValue)
                {
                    nList.Add(neighbourCell.Value);
                }
            }

            return nList;
        }

        AutomataCell? MaybeGetCell(Vector3 pos)
        {
            if (_cells.ContainsKey(pos))
            {
                return _cells[pos];
            }

            return null;
        }

        List<Vector3> _neighbourPositions = new List<Vector3>
        {
            new Vector3(-1, 0, 0),
            new Vector3(-1, 0, 0),
            new Vector3(1, 0, 0),
            new Vector3(-1, 0, 1),
            new Vector3(0, 0, 1),
            new Vector3(-1, 0, -1),
            new Vector3(1, 0, -1),
            new Vector3(0, 0, -1)
        };
        AutomataCell CalculateStrategyBelousovZhabotinsky(AutomataCell cell)
        {
            var S = 0;
            var neighbours = GetNeighbours(cell);

            _q = _bzTiles.Length - 1;

            if (cell.State == _q)
            {
                cell.State = 1;
            }
            else if (cell.State == 0)
            {
                var a = neighbours.Count(n => n.State >= 1 && n.State < _q);
                var b = neighbours.Count(n => n.State == _q);

                cell.State = a / _k1 + b / _k2 + 1;
                if (cell.State > _q) cell.State = _q;
            }
            else
            {
                foreach (var neighbour in neighbours)
                {
                    S += neighbour.State;
                }

                S += cell.State;

                var c = neighbours.Count(n => n.State == 1);
                cell.State = S / (9 - c) + _g;
                if (cell.State > _q) cell.State = _q;
            }

            return cell;
        }

        /* Standard GameOfLifelike */
        AutomataCell CalculateStrategyLife(AutomataCell cell)
        {
            var neighbourStateSum = 0;
            var neighbours = GetNeighbours(cell);


            foreach (var neighbour in neighbours)
            {

                neighbourStateSum += neighbour.State;
            }

            if (cell.State == 1)
            {
                if (neighbourStateSum < 2)
                {
                    cell.State = 0;
                }

                if (neighbourStateSum > 3)
                {
                    cell.State = 0;
                }
            }
            else if (cell.State == 0)
            {
                if (neighbourStateSum == 3)
                {
                    cell.State = 1;
                }
            }

            return cell;
        }
    }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment