405 lines
18 KiB
C#
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;
|
|
}
|
|
}
|
|
} |