using System; using System.Collections.Generic; using System.Linq; using Sirenix.OdinInspector; using Sirenix.Utilities; using UnityEngine; using VOID.Generic.Dict; using VOID.Generic.Util; using VOID.ScriptableObjects.World; namespace VOID.Generic.World { public class WorldGeneratorManager : MonoBehaviour { public static WorldGeneratorManager current { get; private set; } [InlineEditor] public WorldGeneratorSettings worldSettings; // Temp data used for generation private readonly Dictionary _mapArea = new(); private readonly Dictionary _mapComplexArea = new(); private readonly Dictionary> _mapOpenConnections = new(); private readonly Dictionary _groupCount = new(); private readonly Dictionary _distanceToArea = new(); // Config [SerializeField] private Material wallMaterial; private void Awake() { current = this; } [Button] public void Generate(int minAreas = 150, int seed = 2137, bool debugWalls = false, bool debugDistance = false) { if (worldSettings == null) return; RandomUtil.InitState(seed); // Clear previous data Clear(); GenerateStartingArea(); GenerateAreas(minAreas); CreateComplexAreas(); if (debugWalls) CreateWalls(); if (debugDistance) CreateAreaDistanceText(); } private void Clear() { _mapArea.Clear(); _mapComplexArea.Clear(); _mapOpenConnections.Clear(); _distanceToArea.Clear(); worldSettings.list.ForEach(areaSettings => _groupCount[areaSettings] = 0); } /// /// Initializes all prepared /// private void CreateComplexAreas() { var areasParentGameObject = new GameObject("areas parent"); _mapComplexArea.Keys.ForEach(posIndex => { var complexArea = _mapComplexArea[posIndex]; var position = new Vector3(posIndex.x * Area.Size, 0, posIndex.y * Area.Size); var areaGameObject = Instantiate(complexArea, position, Quaternion.identity); areaGameObject.transform.parent = areasParentGameObject.transform; }); } /// /// TODO: DEBUG - creates temporary walls :D /// private void CreateWalls() { var walledPosIndexes = new List(); var wallsParentGameObject = new GameObject("walls parent"); _mapArea.Keys.ForEach(posIndex => { _mapArea[posIndex].GetConnections().ForEach(connection => { var otherPosIndex = GetPosIndexInDirection(posIndex, connection.direction); if (walledPosIndexes.Contains(otherPosIndex)) return; if (connection.type is not AreaConnectionType.None && _mapArea.ContainsKey(otherPosIndex)) return; var wallPosition = Vector3.Lerp( new Vector3(posIndex.x * Area.Size, 0, posIndex.y * Area.Size), new Vector3(otherPosIndex.x * Area.Size, 0, otherPosIndex.y * Area.Size), 0.5f); var wallScale = new Vector3(Area.Size, 10, 1); var isVertical = connection.direction is Direction2D.Back or Direction2D.Forward; var wallRotation = Quaternion.Euler(0, isVertical ? 0 : 90, 0); var wallGameObject = GameObject.CreatePrimitive(PrimitiveType.Cube); wallGameObject.transform.position = wallPosition; wallGameObject.transform.localScale = wallScale; wallGameObject.transform.rotation = wallRotation; wallGameObject.GetComponent().material = wallMaterial; wallGameObject.transform.parent = wallsParentGameObject.transform; wallGameObject.layer = LayerManager.StaticIndex; }); walledPosIndexes.Add(posIndex); }); } /// /// TODO: DEBUG - area distance from starting area /// private void CreateAreaDistanceText() { var distanceParentGameObject = new GameObject("distance to areas"); _mapArea.Keys.ForEach(posIndex => { var position = new Vector3(posIndex.x * Area.Size, 25, posIndex.y * Area.Size); var rotation = new Vector3(90, 0, 0); var textGameObject = new GameObject($"{_distanceToArea} ({posIndex})"); textGameObject.transform.parent = distanceParentGameObject.transform; textGameObject.transform.position = position; textGameObject.transform.rotation = Quaternion.Euler(rotation); var textMesh = textGameObject.AddComponent(); textMesh.color = Color.red; textMesh.text = _distanceToArea[posIndex].ToString(); textMesh.fontSize = 100; textMesh.alignment = TextAlignment.Center; textMesh.anchor = TextAnchor.MiddleCenter; }); } /// /// Selects random starting area at position 0,0. /// private void GenerateStartingArea() { var startingArea = RandomUtil.RandomElement(WithVariants(worldSettings.startingComplexAreas)); UpdateMap(startingArea, Vector2Int.zero); RecreateAreasDistance(); } /// /// Alias to , but runs it multiple times /// private void GenerateAreas(int count) { while (_mapArea.Count <= count && _mapOpenConnections.Count > 0) { FindClosestConnection(out var posIndex, out var direction); AddRandomValidComplexArea(posIndex, direction); } } /// /// Returns in out parameters closest and open connection at position index and direction /// /// Position index where open connection exists /// In which direction connection is open private void FindClosestConnection(out Vector2Int posIndex, out Direction2D direction) { posIndex = default; direction = default; var minDistance = float.MaxValue; foreach (var pair in _mapOpenConnections) { var distance = Vector2Int.Distance(Vector2Int.zero, pair.Key); if (!(distance < minDistance)) continue; minDistance = distance; posIndex = pair.Key; direction = RandomUtil.RandomElement(pair.Value).direction; } } /// /// Select random from given list that can be connected to given position. /// /// To which position new area will be connected /// From which direction new area will be connected private void AddRandomValidComplexArea(Vector2Int thisPosIndex, Direction2D thisDir) { var otherPosIndex = GetPosIndexInDirection(thisPosIndex, thisDir); var otherPosDistance = _distanceToArea[thisPosIndex] + 1; // 1. Get all Area that can be placed // - Check group limits // - Check if group can be placed in this distance // - Include rotated variants if needed // - Check if it would collide others var validAreas = worldSettings.list .Where(areaSettings => _groupCount[areaSettings] < areaSettings.limit) .Where(areaSettings => !areaSettings.requireDistance || (otherPosDistance >= areaSettings.distance.x && otherPosDistance <= areaSettings.distance.y)) .SelectMany(areaSettings => WithVariants(areaSettings.group, areaSettings.useRotatedVariants)) .SelectMany(complexArea => complexArea.areas) .Where(area => IsComplexAreaValid(area, otherPosIndex)).ToList(); // 2. No available Area can be placed here! if (!validAreas.Any()) { // No valid areas found CloseConnection(thisPosIndex, _mapArea[thisPosIndex].GetConnection(thisDir)); return; } // 3. Get all ComplexArea, by found validated Area var validComplexAreas = validAreas.Select(area => area.complexArea).Distinct().ToList(); // 4. Get all groups from WorldGeneratorSettings that contain ANY validated ComplexArea // - Include rotated variants if needed var validWorldAreaSettings = worldSettings.list .Where(areaSettings => validComplexAreas.Intersect(WithVariants(areaSettings.group)).Any()).ToList(); // 5. Get random group var worldAreaSettingsToAdd = RandomUtil.RandomElementByWeight(validWorldAreaSettings, wags => wags.weight); // 6. Randomized group still can contain few ComplexArea, so we need to get only valid ones var areasToAdd = WithVariants(worldAreaSettingsToAdd.group, worldAreaSettingsToAdd.useRotatedVariants) .Where(complexArea => validComplexAreas.Contains(complexArea)) .SelectMany(complexArea => complexArea.areas) .Where(area => validAreas.Contains(area)).ToList(); // 7. Randomize again to get final ComplexArea from group var areaToAdd = RandomUtil.RandomElement(areasToAdd); // 8. Add ComplexArea with correct offset to its Area UpdateMap(areaToAdd.complexArea, otherPosIndex - areaToAdd.position); _groupCount[worldAreaSettingsToAdd]++; // 9. Recreate Distances to areas RecreateAreasDistance(); } private List WithVariants(List complexAreas, bool includeRotatedVariants = true) { if (!includeRotatedVariants) return complexAreas; return complexAreas.Concat(complexAreas.SelectMany(complexArea => complexArea.variants)).ToList(); } /// /// Checks if whole which given belongs to can be placed at given position index /// private bool IsComplexAreaValid(Area area, Vector2Int posIndex) { return IsComplexAreaValid(area.complexArea, posIndex - area.position); } /// /// Checks if whole can be placed at given position index /// private bool IsComplexAreaValid(ComplexArea complexArea, Vector2Int posIndex) { return complexArea.areas.All(area => IsAreaValid(area, posIndex + area.position)); } /// /// Checks if given can be placed at position index /// private bool IsAreaValid(Area area, Vector2Int posIndex) { // Position already occupied if (_mapArea.ContainsKey(posIndex)) return false; return area.GetConnections().All(connection => IsConnectionValid(connection, posIndex)); } /// /// Checks if can be placed at given position index /// Its valid when: /// - Neighbor is empty /// - Neighbor connection is also open AND have identical tag /// private bool IsConnectionValid(AreaConnection thisConnection, Vector2Int thisPosIndex) { var otherPosIndex = GetPosIndexInDirection(thisPosIndex, thisConnection.direction); var otherConnection = _mapArea.GetValueOrDefault(otherPosIndex)?.GetConnection(GetReverseDirection(thisConnection.direction)); // No area in that direction - VALID if (otherConnection == null) return true; // Open connection leads to closed connection - INVALID if (IsConnectionOpen(thisConnection) != IsConnectionOpen(otherConnection)) return false; // Connections have mismatched tags - INVALID if (thisConnection.tag != otherConnection.tag) return false; return true; } private void UpdateMap(ComplexArea complexArea, Vector2Int posIndex) { _mapComplexArea.Add(posIndex, complexArea); complexArea.areas.ForEach(area => { var localPosIndex = posIndex + area.position; var connections = area.GetConnections().Where(IsConnectionOpen).ToList(); _mapArea.Add(localPosIndex, area); _mapOpenConnections.Add(localPosIndex, connections); UpdateMapConnections(localPosIndex); }); } private void RecreateAreasDistance() { // Clear all distances and start from beginning // Just to be sure that connected branches of generated world will have correct distance from start _distanceToArea.Clear(); _distanceToArea.Add(Vector2Int.zero, 0); var toUpdate = new List { Vector2Int.zero }; var distance = 0; while (toUpdate.Count > 0) { distance++; toUpdate = toUpdate // Get all neighbours... .SelectMany(GetNeighbourPositions) // ...but only those without distance calculated already .Where(posIndex => !_distanceToArea.ContainsKey(posIndex)) .ToList(); toUpdate.ForEach(posIndex => _distanceToArea.TryAdd(posIndex, distance)); } } /// /// Returns all index positions of existing and connected neighbours /// private List GetNeighbourPositions(Vector2Int posIndex) { return Enum.GetValues(typeof(Direction2D)).Cast() // Only directions which area is open to .Where(direction => IsConnectionAny(_mapArea[posIndex].GetConnection(direction))) // Get neighbour index positions .Select(direction => GetPosIndexInDirection(posIndex, direction)) // Neighbour need to exists .Where(otherPosIndex => _mapArea.ContainsKey(otherPosIndex)) .ToList(); } private void UpdateMapConnections(Vector2Int thisPosIndex) { if (!_mapOpenConnections.ContainsKey(thisPosIndex)) return; if (_mapOpenConnections[thisPosIndex].IsNullOrEmpty()) { _mapOpenConnections.Remove(thisPosIndex); return; } _mapOpenConnections[thisPosIndex].ToList().ForEach(thisConnection => { var otherPosIndex = GetPosIndexInDirection(thisPosIndex, thisConnection.direction); var otherConnection = _mapArea.GetValueOrDefault(otherPosIndex)?.GetConnection(GetReverseDirection(thisConnection.direction)); if (otherConnection == null) return; CloseConnection(thisPosIndex, thisConnection); CloseConnection(otherPosIndex, otherConnection); }); } private void CloseConnection(Vector2Int posIndex, AreaConnection connection) { var connections = _mapOpenConnections.GetValueOrDefault(posIndex); connections?.Remove(connection); if (connections.IsNullOrEmpty()) _mapOpenConnections.Remove(posIndex); } private Vector2Int GetPosIndexInDirection(Vector2Int fromPosIndex, Direction2D direction) { return direction switch { Direction2D.Forward => fromPosIndex + Vector2Int.up, Direction2D.Right => fromPosIndex + Vector2Int.right, Direction2D.Back => fromPosIndex + Vector2Int.down, Direction2D.Left => fromPosIndex + Vector2Int.left, _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) }; } private Direction2D GetReverseDirection(Direction2D direction) { return direction switch { Direction2D.Forward => Direction2D.Back, Direction2D.Right => Direction2D.Left, Direction2D.Back => Direction2D.Forward, Direction2D.Left => Direction2D.Right, _ => throw new ArgumentOutOfRangeException(nameof(direction), direction, null) }; } private bool IsConnectionOpen(AreaConnection connection) { return connection.type is AreaConnectionType.Tunnel or AreaConnectionType.Whole; } private bool IsConnectionAny(AreaConnection connection) { return connection.type is not AreaConnectionType.None; } } }