443 lines
20 KiB
C#
443 lines
20 KiB
C#
//Copyright(c)2021 Procedural Worlds Pty Limited
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
namespace GeNa.Core
|
|
{
|
|
[CreateAssetMenu(fileName = "Area Spawner Builder", menuName = "Procedural Worlds/GeNa/Builders/Area Spawner Builder", order = 1)]
|
|
public class AreaSpawnBuilder : AreaBuilder
|
|
{
|
|
#region Definitions
|
|
private class GridNode
|
|
{
|
|
public uint id;
|
|
public Vector3 position;
|
|
public bool isIntersection;
|
|
public GridNode(Vector3 position)
|
|
{
|
|
id = 0;
|
|
this.position = position;
|
|
isIntersection = false;
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Divides up a circle into a grid of blockSize cells.
|
|
/// The grid is a 2 dimensional array of enum BlockType.
|
|
/// Some roads are assigned space at creation time.
|
|
/// The grid is axis aligned in the XZ plane, and
|
|
/// will therefore need to be rotated by the consumer.
|
|
/// </summary>
|
|
private class AreaGrid
|
|
{
|
|
private enum BlockType
|
|
{
|
|
Empty = 0,
|
|
Road = 1,
|
|
Buiding = 2,
|
|
Vacant = 3,
|
|
Reserved = 4
|
|
}
|
|
private float BlockSize;
|
|
private BlockType[,] Grid;
|
|
#region Properties
|
|
public bool IsFullTown
|
|
{
|
|
get
|
|
{
|
|
int divisions = Grid.GetLength(0);
|
|
for (int r = 0; r < divisions; r++)
|
|
for (int c = 0; c < divisions; c++)
|
|
if (Grid[r, c] == BlockType.Empty)
|
|
return false;
|
|
return true;
|
|
}
|
|
}
|
|
public int EmptyBlocks
|
|
{
|
|
get
|
|
{
|
|
int divisions = Grid.GetLength(0);
|
|
int count = 0;
|
|
for (int r = 0; r < divisions; r++)
|
|
for (int c = 0; c < divisions; c++)
|
|
if (Grid[r, c] == BlockType.Empty)
|
|
count++;
|
|
return count;
|
|
}
|
|
}
|
|
#endregion
|
|
public AreaGrid(float radius, float blockSize)
|
|
{
|
|
float sideLength = radius * 1.4142135f;
|
|
int divisions = (int) (sideLength / blockSize);
|
|
if ((divisions & 1) == 0)
|
|
divisions--;
|
|
BlockSize = sideLength / divisions;
|
|
Grid = new BlockType[divisions, divisions];
|
|
// Reserve the corners as "unused".
|
|
Grid[0, 0] = BlockType.Reserved;
|
|
Grid[0, divisions - 1] = BlockType.Reserved;
|
|
Grid[divisions - 1, 0] = BlockType.Reserved;
|
|
Grid[divisions - 1, divisions - 1] = BlockType.Reserved;
|
|
// Reserve the verticle roads. There is always 1 down the center.
|
|
int centerColumn = divisions >> 1;
|
|
int sideSize;
|
|
if (divisions < 14)
|
|
sideSize = 4;
|
|
else
|
|
sideSize = Random.Range(3, 6) * 2;
|
|
int numVertRoads = (divisions - 1 - sideSize) / 6;
|
|
numVertRoads = (numVertRoads < 0) ? 0 : numVertRoads;
|
|
for (int i = 0; i <= numVertRoads; i++)
|
|
{
|
|
for (int row = 0; row < divisions; row++)
|
|
{
|
|
Grid[row, centerColumn + i * 3] = BlockType.Road;
|
|
Grid[row, centerColumn - i * 3] = BlockType.Road;
|
|
}
|
|
}
|
|
int horzTownBlockSize = Random.Range(5, 8);
|
|
int numHorzRoads = (divisions - 10) / horzTownBlockSize;
|
|
numHorzRoads = (numHorzRoads < 0) ? 0 : numHorzRoads + 1;
|
|
int horzStart = Random.Range(4, 7);
|
|
if (numHorzRoads <= 1 || horzStart >= divisions)
|
|
{
|
|
horzStart = divisions / 2;
|
|
numHorzRoads = 1;
|
|
}
|
|
for (int i = 0; i < numHorzRoads; i++)
|
|
{
|
|
int row = horzStart + i * horzTownBlockSize;
|
|
if (row >= divisions)
|
|
break;
|
|
for (int col = 0; col < divisions; col++)
|
|
Grid[row, col] = BlockType.Road;
|
|
}
|
|
FindVacantLots();
|
|
//Debug.Log($"Area with Radius = {Radius} has {numVertRoads * 2 + 1} verticle road(s), {numHorzRoads} horizontal road(s), and {numVacantLots} vacant lots.");
|
|
}
|
|
#region Methods
|
|
private void FindVacantLots()
|
|
{
|
|
int divisions = Grid.GetLength(0);
|
|
for (int r = 1; r < divisions - 1; r++)
|
|
{
|
|
for (int c = 1; c < divisions - 1; c++)
|
|
{
|
|
if (Grid[r - 1, c] != BlockType.Road && Grid[r + 1, c] != BlockType.Road && Grid[r, c - 1] != BlockType.Road && Grid[r, c + 1] != BlockType.Road)
|
|
Grid[r, c] = BlockType.Vacant;
|
|
}
|
|
}
|
|
}
|
|
internal Vector2 CalcBuildingPosition(int row, int col, int numBlocksWide)
|
|
{
|
|
int divisions = Grid.GetLength(0);
|
|
float offset = (divisions * 0.5f) * BlockSize;
|
|
float x = (float) col + (numBlocksWide * 0.5f);
|
|
float z = (float) divisions - ((float) row + (numBlocksWide * 0.5f));
|
|
Vector2 position = new Vector2(x * BlockSize, z * BlockSize);
|
|
position -= new Vector2(offset, offset);
|
|
return position;
|
|
}
|
|
internal Vector3 GridToPosition(int row, int col)
|
|
{
|
|
int divisions = Grid.GetLength(0);
|
|
float offset = (divisions * 0.5f) * BlockSize;
|
|
float x = (float) col + 0.5f;
|
|
float z = (float) divisions - ((float) row + 0.5f);
|
|
Vector3 position = new Vector3(x * BlockSize, 0.0f, z * BlockSize);
|
|
position -= new Vector3(offset, 0.0f, offset);
|
|
return position;
|
|
}
|
|
internal float CalcBuildingRotation(int row, int col, int numBlocksWide)
|
|
{
|
|
int divisions = Grid.GetLength(0);
|
|
int roadMod = divisions % 3;
|
|
roadMod = (roadMod == 1) ? 0 : (roadMod == 0) ? 1 : roadMod;
|
|
float rotation = ((col % 3) == ((roadMod + 1) % 3)) ? -90f : 90.0f;
|
|
if (numBlocksWide > 1)
|
|
rotation = (col < divisions / 2) ? 90.0f : -90.0f;
|
|
// If there is a road to the left or the right, we are done.
|
|
if (col > 0 && Grid[row, col - 1] == BlockType.Road)
|
|
return rotation;
|
|
if (col + numBlocksWide < divisions && Grid[row, col + numBlocksWide] == BlockType.Road)
|
|
return rotation;
|
|
// Check above and below.
|
|
if (row > 0 && Grid[row - 1, col] == BlockType.Road)
|
|
rotation = 0.0f;
|
|
if (row + numBlocksWide < divisions && Grid[row + numBlocksWide, col] == BlockType.Road)
|
|
rotation = 180.0f;
|
|
return rotation;
|
|
}
|
|
/// <summary>
|
|
/// Returns the outer nodes that can be connected to externally to the town.
|
|
/// </summary>
|
|
/// <returns>List of internal nodes.</returns>
|
|
internal List<List<GridNode>> CreateInternalRoads()
|
|
{
|
|
List<List<GridNode>> internalNodes = new List<List<GridNode>>();
|
|
int divisions = Grid.GetLength(0);
|
|
for (int c = 1; c < divisions - 1; c++)
|
|
{
|
|
if (Grid[0, c] == BlockType.Road)
|
|
{
|
|
List<GridNode> thisRoad = new List<GridNode>();
|
|
internalNodes.Add(thisRoad);
|
|
Vector3 pos = GridToPosition(0, c);
|
|
thisRoad.Add(new GridNode(pos + Vector3.forward * BlockSize));
|
|
thisRoad.Add(new GridNode(pos));
|
|
for (int r = 1; r < divisions - 1; r++)
|
|
{
|
|
if (Grid[r, c - 1] == BlockType.Road || Grid[r, c + 1] == BlockType.Road)
|
|
{
|
|
GridNode node = new GridNode(GridToPosition(r, c));
|
|
node.id = (uint) r * 1000 + (uint) c;
|
|
node.isIntersection = true;
|
|
thisRoad.Add(node);
|
|
}
|
|
}
|
|
pos = GridToPosition(divisions - 1, c);
|
|
thisRoad.Add(new GridNode(pos));
|
|
thisRoad.Add(new GridNode(pos + Vector3.back * BlockSize));
|
|
}
|
|
}
|
|
for (int r = 1; r < divisions - 1; r++)
|
|
{
|
|
if (Grid[r, 0] == BlockType.Road)
|
|
{
|
|
List<GridNode> thisRoad = new List<GridNode>();
|
|
internalNodes.Add(thisRoad);
|
|
Vector3 pos = GridToPosition(r, 0);
|
|
thisRoad.Add(new GridNode(pos + Vector3.left * BlockSize));
|
|
thisRoad.Add(new GridNode(pos));
|
|
for (int c = 1; c < divisions - 1; c++)
|
|
{
|
|
if (Grid[r - 1, c] == BlockType.Road || Grid[r + 1, c] == BlockType.Road)
|
|
{
|
|
GridNode node = new GridNode(GridToPosition(r, c));
|
|
node.id = (uint) r * 1000 + (uint) c;
|
|
node.isIntersection = true;
|
|
thisRoad.Add(node);
|
|
}
|
|
}
|
|
pos = GridToPosition(r, divisions - 1);
|
|
thisRoad.Add(new GridNode(pos));
|
|
thisRoad.Add(new GridNode(pos + Vector3.right * BlockSize));
|
|
}
|
|
}
|
|
return internalNodes;
|
|
}
|
|
internal bool CanFit(float size) => FindBlocks(size, out _, out _, false);
|
|
/// <summary>
|
|
/// Returns the axis aligned relative position of the
|
|
/// reserved blocks center if enough room found.
|
|
/// </summary>
|
|
/// <param name="size"></param>
|
|
/// <param name="position"></param>
|
|
/// <param name="rotation"></param>
|
|
/// <param name="reserve"></param>
|
|
/// <returns></returns>
|
|
internal bool FindBlocks(float size, out Vector2 position, out float rotation, bool reserve = true)
|
|
{
|
|
position = Vector2.zero;
|
|
rotation = 0.0f;
|
|
int divisions = Grid.GetLength(0);
|
|
int numBlocksWide = (int) (size / BlockSize + 1.0f);
|
|
int count = divisions - numBlocksWide + 1;
|
|
for (int r = 0; r < count; r++)
|
|
{
|
|
for (int c = 0; c < count; c++)
|
|
{
|
|
bool found = true;
|
|
int vacants = 0;
|
|
for (int i = 0; i < numBlocksWide; i++)
|
|
{
|
|
for (int j = 0; j < numBlocksWide; j++)
|
|
{
|
|
if (Grid[r + i, c + j] != BlockType.Empty)
|
|
{
|
|
if (numBlocksWide > 1 && Grid[r + i, c + j] == BlockType.Vacant)
|
|
{
|
|
vacants++;
|
|
continue;
|
|
}
|
|
found = false;
|
|
break;
|
|
}
|
|
}
|
|
if (!found)
|
|
break;
|
|
}
|
|
if (numBlocksWide < 3 && vacants == numBlocksWide * numBlocksWide)
|
|
found = false;
|
|
if (found)
|
|
{
|
|
if (reserve)
|
|
for (int i = 0; i < numBlocksWide; i++)
|
|
for (int j = 0; j < numBlocksWide; j++)
|
|
Grid[r + i, c + j] = BlockType.Buiding;
|
|
position = CalcBuildingPosition(r, c, numBlocksWide);
|
|
rotation = CalcBuildingRotation(r, c, numBlocksWide);
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
#endregion
|
|
}
|
|
#endregion
|
|
#region Methods
|
|
protected override List<GeNaNode> BuildArea(Vector2 axis, AreaFinder.AreaPoly area, float cellSize, List<GeNaSpawner> buildingSpawners, List<GameObject> buildingPrefabs, GeNaSpline roadSpline)
|
|
{
|
|
if (buildingSpawners == null || buildingSpawners.Count == 0)
|
|
return new List<GeNaNode>();
|
|
List<GeNaSpawner> randomSpawners = new List<GeNaSpawner>(buildingSpawners);
|
|
List<GameObject> randomPrefabs = null;
|
|
if (buildingPrefabs != null)
|
|
randomPrefabs = new List<GameObject>(buildingPrefabs);
|
|
else
|
|
randomPrefabs = new List<GameObject>();
|
|
|
|
// divide the area up based on spawners available and fitting.
|
|
axis.Normalize();
|
|
area.Axis = axis;
|
|
float areaAngle = Vector2.SignedAngle(axis, Vector3.up);
|
|
Quaternion axisRot = Quaternion.AngleAxis(areaAngle, Vector3.up);
|
|
|
|
// Vector3 forward = new Vector3(axis.x, 0.0f, axis.y).normalized;
|
|
// Vector3 right = new Vector3(forward.z, 0.0f, -forward.x).normalized;
|
|
// Vector3 movForward = forward * area.m_radius;
|
|
// Vector3 movRight = right * area.m_radius;
|
|
Vector3 spawnPoint = Vector3.zero;
|
|
AreaGrid areaGrid = new AreaGrid(area.Radius, cellSize);
|
|
int totalPlaced = 0;
|
|
int totalCount = randomSpawners.Count + randomPrefabs.Count;
|
|
while (totalCount > 0)
|
|
{
|
|
if (Random.Range(0, totalCount) < randomSpawners.Count)
|
|
{
|
|
GeNaSpawner spawner = GetNextRandomSpawner(randomSpawners);
|
|
if (spawner != null)
|
|
{
|
|
spawner.Load();
|
|
GeNaSpawnerData data = spawner.SpawnerData;
|
|
if (areaGrid.FindBlocks(data.SpawnRange, out Vector2 buildingLocalPos, out float buildingLocalRot))
|
|
{
|
|
if (m_buildingParent != null)
|
|
data.SpawnParent = m_buildingParent.transform;
|
|
data.SpawnToTarget = true;
|
|
data.PlacementCriteria.MinRotationY = data.PlacementCriteria.MaxRotationY = buildingLocalRot + areaAngle;
|
|
data.SpawnCriteria.ForceSpawn = true;
|
|
Vector3 pos = new Vector3(buildingLocalPos.x, 0.0f, buildingLocalPos.y);
|
|
pos = axisRot * pos;
|
|
spawnPoint = area.Center + pos;
|
|
spawnPoint.y = HeightAt(spawnPoint, spawnPoint.y);
|
|
SpawnCall spawnCall = spawner.Spawn(spawnPoint);
|
|
var spawnedEntities = spawnCall.SpawnedEntities;
|
|
m_undoRecord.Record(spawnedEntities);
|
|
totalPlaced++;
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
GameObject prefab = GetNextRandomPrefab(randomPrefabs);
|
|
if (prefab != null)
|
|
{
|
|
float prefabSize = GetSizeOfPrefab(prefab);
|
|
if (areaGrid.FindBlocks(prefabSize, out Vector2 buildingLocalPos, out float buildingLocalRot))
|
|
{
|
|
GameObject go = Instantiate(prefab);
|
|
Vector3 pos = new Vector3(buildingLocalPos.x, 0.0f, buildingLocalPos.y);
|
|
pos = axisRot * pos;
|
|
spawnPoint = area.Center + pos;
|
|
spawnPoint.y = HeightAt(spawnPoint, spawnPoint.y);
|
|
go.transform.localPosition = spawnPoint;
|
|
go.transform.rotation = Quaternion.AngleAxis(buildingLocalRot + areaAngle, Vector3.up);
|
|
if (m_buildingParent != null)
|
|
go.transform.parent = m_buildingParent.transform;
|
|
totalPlaced++;
|
|
}
|
|
}
|
|
}
|
|
totalCount = randomSpawners.Count + randomPrefabs.Count;
|
|
if (totalPlaced > 0 && totalCount == 0 && areaGrid.EmptyBlocks > 3)
|
|
{
|
|
totalPlaced = 0;
|
|
if (buildingSpawners != null)
|
|
randomSpawners = new List<GeNaSpawner>(buildingSpawners);
|
|
else
|
|
randomSpawners = new List<GeNaSpawner>();
|
|
if (buildingPrefabs != null)
|
|
randomPrefabs = new List<GameObject>(buildingPrefabs);
|
|
else
|
|
randomPrefabs = new List<GameObject>();
|
|
totalCount = randomSpawners.Count + randomPrefabs.Count;
|
|
}
|
|
}
|
|
List<List<GridNode>> internalNodes = areaGrid.CreateInternalRoads();
|
|
// rotate the external nodes into the area axis and position into world space.
|
|
List<GeNaNode> externalConnectingNodes = new List<GeNaNode>();
|
|
Dictionary<uint, uint> GridNodeToGenaNodeID = new Dictionary<uint, uint>();
|
|
foreach (List<GridNode> node in internalNodes)
|
|
{
|
|
GeNaNode prevNode = null;
|
|
for (int j = 0; j < node.Count; j++)
|
|
{
|
|
GeNaNode genaNode = null;
|
|
Vector3 pos = area.Center + (axisRot * node[j].position);
|
|
if (node[j].isIntersection)
|
|
{
|
|
if (GridNodeToGenaNodeID.ContainsKey(node[j].id))
|
|
genaNode = roadSpline.GetNode(GridNodeToGenaNodeID[node[j].id]);
|
|
else
|
|
{
|
|
genaNode = roadSpline.CreateNewNode(pos);
|
|
GridNodeToGenaNodeID.Add(node[j].id, genaNode.ID);
|
|
}
|
|
}
|
|
if (genaNode == null)
|
|
genaNode = roadSpline.CreateNewNode(pos);
|
|
if (j == 0)
|
|
externalConnectingNodes.Add(genaNode);
|
|
else
|
|
{
|
|
roadSpline.AddCurve(prevNode, genaNode);
|
|
if (j == node.Count - 1)
|
|
externalConnectingNodes.Add(genaNode);
|
|
}
|
|
prevNode = genaNode;
|
|
}
|
|
}
|
|
//Debug.Log($"Number of cache hits for Prefab Size = {NumCacheHits}.");
|
|
return externalConnectingNodes;
|
|
}
|
|
public GeNaSpawner GetNextRandomSpawner(List<GeNaSpawner> list)
|
|
{
|
|
GeNaSpawner spawner = null;
|
|
while (spawner == null)
|
|
{
|
|
if (list == null || list.Count == 0)
|
|
return null;
|
|
int index = Random.Range(0, list.Count);
|
|
spawner = list[index];
|
|
GeNaSpawner last = list[list.Count - 1];
|
|
list[index] = last;
|
|
list.RemoveAt(list.Count - 1);
|
|
}
|
|
return spawner;
|
|
}
|
|
public GameObject GetNextRandomPrefab(List<GameObject> list)
|
|
{
|
|
if (list == null || list.Count == 0)
|
|
return null;
|
|
int index = Random.Range(0, list.Count);
|
|
GameObject go = list[index];
|
|
GameObject last = list[list.Count - 1];
|
|
list[index] = last;
|
|
list.RemoveAt(list.Count - 1);
|
|
return go;
|
|
}
|
|
#endregion
|
|
}
|
|
} |