Files
VOIDRPG/Assets/90 - Scripts/Generic/World/WorldGeneratorManager.cs
T
2026-07-09 21:33:33 +02:00

405 lines
18 KiB
C#

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<Vector2Int, Area> _mapArea = new();
private readonly Dictionary<Vector2Int, ComplexArea> _mapComplexArea = new();
private readonly Dictionary<Vector2Int, List<AreaConnection>> _mapOpenConnections = new();
private readonly Dictionary<WorldAreaGeneratorSettings, int> _groupCount = new();
private readonly Dictionary<Vector2Int, int> _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);
}
/// <summary>
/// Initializes all prepared <see cref="ComplexArea"/>
/// </summary>
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;
});
}
/// <summary>
/// TODO: DEBUG - creates temporary walls :D
/// </summary>
private void CreateWalls()
{
var walledPosIndexes = new List<Vector2Int>();
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<MeshRenderer>().material = wallMaterial;
wallGameObject.transform.parent = wallsParentGameObject.transform;
wallGameObject.layer = LayerManager.StaticIndex;
});
walledPosIndexes.Add(posIndex);
});
}
/// <summary>
/// TODO: DEBUG - area distance from starting area
/// </summary>
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>();
textMesh.color = Color.red;
textMesh.text = _distanceToArea[posIndex].ToString();
textMesh.fontSize = 100;
textMesh.alignment = TextAlignment.Center;
textMesh.anchor = TextAnchor.MiddleCenter;
});
}
/// <summary>
/// Selects random starting area at position 0,0.
/// </summary>
private void GenerateStartingArea()
{
var startingArea = RandomUtil.RandomElement(WithVariants(worldSettings.startingComplexAreas));
UpdateMap(startingArea, Vector2Int.zero);
RecreateAreasDistance();
}
/// <summary>
/// Alias to <see cref="AddRandomValidComplexArea(Vector2Int, Direction2D)"/>, but runs it multiple times
/// </summary>
private void GenerateAreas(int count)
{
while (_mapArea.Count <= count && _mapOpenConnections.Count > 0)
{
FindClosestConnection(out var posIndex, out var direction);
AddRandomValidComplexArea(posIndex, direction);
}
}
/// <summary>
/// Returns in out parameters closest and open connection at position index and direction
/// </summary>
/// <param name="posIndex">Position index where open connection exists</param>
/// <param name="direction">In which direction connection is open</param>
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;
}
}
/// <summary>
/// Select random <see cref="ComplexArea"/> from given list that can be connected to given position.
/// </summary>
/// <param name="thisPosIndex">To which position new area will be connected</param>
/// <param name="thisDir">From which direction new area will be connected</param>
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<ComplexArea> WithVariants(List<ComplexArea> complexAreas, bool includeRotatedVariants = true)
{
if (!includeRotatedVariants) return complexAreas;
return complexAreas.Concat(complexAreas.SelectMany(complexArea => complexArea.variants)).ToList();
}
/// <summary>
/// Checks if whole <see cref="ComplexArea"/> which given <see cref="Area"/> belongs to can be placed at given position index
/// </summary>
private bool IsComplexAreaValid(Area area, Vector2Int posIndex)
{
return IsComplexAreaValid(area.complexArea, posIndex - area.position);
}
/// <summary>
/// Checks if whole <see cref="ComplexArea"/> can be placed at given position index
/// </summary>
private bool IsComplexAreaValid(ComplexArea complexArea, Vector2Int posIndex)
{
return complexArea.areas.All(area => IsAreaValid(area, posIndex + area.position));
}
/// <summary>
/// Checks if given <see cref="Area"/> can be placed at position index
/// </summary>
private bool IsAreaValid(Area area, Vector2Int posIndex)
{
// Position already occupied
if (_mapArea.ContainsKey(posIndex)) return false;
return area.GetConnections().All(connection => IsConnectionValid(connection, posIndex));
}
/// <summary>
/// Checks if <see cref="AreaConnection"/> can be placed at given position index
/// Its valid when:
/// - Neighbor is empty
/// - Neighbor connection is also open AND have identical tag
/// </summary>
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> { 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));
}
}
/// <summary>
/// Returns all index positions of existing and connected neighbours
/// </summary>
private List<Vector2Int> GetNeighbourPositions(Vector2Int posIndex)
{
return Enum.GetValues(typeof(Direction2D)).Cast<Direction2D>()
// 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;
}
}
}