Skip to content

Instantly share code, notes, and snippets.

@RobertBouillon
Last active December 3, 2019 15:48
Show Gist options
  • Save RobertBouillon/3e1890b00f09144e39eeccabe3e05230 to your computer and use it in GitHub Desktop.
Save RobertBouillon/3e1890b00f09144e39eeccabe3e05230 to your computer and use it in GitHub Desktop.
2D Map Generator C#
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using Texture = System.Object;
namespace Core3Sandbox
{
[Flags]
public enum Faces
{
None = 0,
Up = 1,
Down = 2,
Left = 4,
Right = 8,
All = Up + Down + Left + Right
}
public enum TileMaterial
{
Stone,
Dirt,
TheTearsOfMyEnemies
}
public class Tile
{
int X { get; set; }
int Y { get; set; }
public TileMaterial Material { get; }
public Tile(int x, int y, TileMaterial material)
{
X = x;
Y = y;
Material = material;
}
public override string ToString() => $"Tile of {Material}";
}
public class Wall : Tile
{
public Faces Faces { get; }
public Wall(int x, int y, TileMaterial material, Faces faces) : base(x, y, material) => Faces = faces;
public override string ToString() => $"Wall of {Material} - Faces: {Faces}";
}
//Use this class to load your tiles from files or whatever for your UI.
//For example, if your tile is STONE and has a corner in the bottom-right, this will try to
public class TileSet
{
private static Regex _fileNameParser = new Regex(@"(?<material>\w+)_(?<faces>\d{1,2})", RegexOptions.Compiled);
private static Texture LoadTexture(string fileName) => throw new NotImplementedException("Your game engine determines how you should load textures. Override this to load the texture how your game engine needs it.");
private Dictionary<(TileMaterial, Faces), Texture> _cache;
private static IEnumerable<(TileMaterial Material, Faces Faces, Texture Texture)> LoadTextures(string name) =>
Directory.GetFiles($@"Textures\{name}")
.Select(x => _fileNameParser.Match(x))
.Where(x => x.Success)
.Select(x => (Material: Enum.Parse<TileMaterial>(x.Groups["material"].Value), Faces: (Faces)Int32.Parse(x.Groups["faces"].Value), Texture: LoadTexture(x.Value)));
public string Name { get; }
public object GetTexture(TileMaterial material, Faces faces) => LoadTexture($@"{Name}\{material}_{(int)faces}");
public TileSet(string name)
{
#region Validation
if (String.IsNullOrWhiteSpace(name))
throw new ArgumentNullException(nameof(name));
#endregion
Name = name;
_cache = LoadTextures(name).ToDictionary(x => (x.Material, x.Faces), x => x.Texture);
}
public Texture this[TileMaterial material, Faces faces] => _cache[(material, faces)];
}
public class Map : IEnumerable<Tile>
{
#region Map Generation
public static Map Generate(int width, int height, int? seed = null)
{
var map = SeedNewMap(width, height, seed);
RefineMap(map);
var faces = CalculateFaces(map);
Tile[,] tiles = new Tile[width, height];
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
tiles[x, y] = (!map[x, y]) ? new Tile(x, y, TileMaterial.Dirt) :
faces[x, y] == Faces.None ? new Tile(x, y, TileMaterial.Stone) :
new Wall(x, y, TileMaterial.Stone, faces[x, y]);
return new Map(tiles);
}
private static Faces[,] CalculateFaces(bool[,] map)
{
var width = map.GetUpperBound(0) + 1;
var height = map.GetUpperBound(1) + 1;
var faces = new Faces[width, height];
for (int x = 0; x < width - 1; x++)
for (int y = 0; y < height; y++)
if (!map[x, y])
faces[x + 1, y] |= Faces.Left;
for (int x = 1; x < width; x++)
for (int y = 0; y < height; y++)
if (!map[x, y])
faces[x - 1, y] |= Faces.Right;
for (int x = 0; x < width; x++)
for (int y = 0; y < height - 1; y++)
if (!map[x, y])
faces[x, y + 1] |= Faces.Up;
for (int x = 0; x < width; x++)
for (int y = 1; y < height; y++)
if (!map[x, y])
faces[x, y - 1] |= Faces.Down;
return faces;
}
private static void RefineMap(bool[,] map, int passes = 5)
{
var width = map.GetUpperBound(0) + 1;
var height = map.GetUpperBound(1) + 1;
var counts = new int[width, height];
for (int pass = 0; pass < passes; pass++)
{
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
{
var xx_max = Math.Min(width - 1, x + 1);
var yy_max = Math.Min(height - 1, y + 1);
for (var xx = Math.Max(0, x - 1); xx <= xx_max; xx++)
for (var yy = Math.Max(0, y - 1); yy <= yy_max; yy++)
if (map[xx, yy])
counts[xx, yy] += 1;
}
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
map[x, y] = counts[x, y] >= 5;
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
counts[x, y] = 0;
}
}
private static bool[,] SeedNewMap(int width, int height, int? seed)
{
Random rnd = new Random(seed ?? (int)DateTime.Now.Ticks);
var map = new bool[width, height];
for (int x = 0; x < width; x++)
for (int y = 0; y < height; y++)
map[x, y] = rnd.NextDouble() > 0.5;
return map;
}
#endregion
private Tile[,] _tiles;
public int Width => _tiles.GetUpperBound(0) + 1;
public int Height => _tiles.GetUpperBound(1) + 1;
internal Map(Tile[,] tiles)
{
#region Validation
const int MAX_SIZE = 50000;
if (tiles is null)
throw new ArgumentNullException(nameof(tiles));
//Sanity check. Limit is arbitrary
if (tiles.GetUpperBound(0) > MAX_SIZE)
throw new ArgumentOutOfRangeException($"Cannot create a map with a width greater than {MAX_SIZE}");
if (tiles.GetUpperBound(1) > MAX_SIZE)
throw new ArgumentOutOfRangeException($"Cannot create a map with a height greater than {MAX_SIZE}");
#endregion
_tiles = tiles;
}
public Tile this[int x, int y] => _tiles[x, y];
public Texture GetTexture(TileSet tileSet, int x, int y)
{
var tile = this[x, y];
return tile is Wall wall ? tileSet[wall.Material, wall.Faces] : tileSet[tile.Material, Faces.None];
}
public IEnumerator<Tile> GetEnumerator() => Enumerable.Range(0, Width).SelectMany(x => Enumerable.Range(0, Height).Select(y => _tiles[x, y])).GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => _tiles.GetEnumerator();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment