UnityTowerDefense/Assets/_Project/Levels/SpawnerVolume.cs

118 lines
No EOL
4.8 KiB
C#

using UnityEngine;
using TD.Core;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace TD.Levels
{
/// <summary>
/// Authoring volume marking where enemies spawn into a player's zone. Spawner tiles are
/// <see cref="PlacementState.Restricted"/> in the baked grid (no tower placement allowed)
/// but remain walkable so enemies can leave the spawner.
/// </summary>
/// <remarks>
/// A zone may have multiple spawners; <see cref="spawnerIdInZone"/> disambiguates them.
/// Player 5 in the 9-player Wintermaul map is the canonical multi-spawner zone.
///
/// The spawner declares an owning player via <see cref="owner"/> rather than relying on
/// spatial containment within a PlayerZoneVolume. The bake validates (with a soft warning)
/// that the spawner's tiles fall inside its declared owner's zone.
/// </remarks>
public class SpawnerVolume : VolumeBase
{
[Tooltip("Which player's zone owns this spawner. Explicit — not inferred from spatial overlap.")]
public PlayerSlot owner = PlayerSlot.Player1;
[Tooltip("Disambiguator for zones with multiple spawners. Should be 0 for single-spawner zones, " +
"and contiguous starting from 0 for multi-spawner zones (e.g., 0 and 1).")]
public int spawnerIdInZone = 0;
[Tooltip("Direction enemies face when they spawn. Used for visual orientation and may bias " +
"initial enemy movement direction.")]
public Direction spawnFacing = Direction.South;
[Tooltip("Whether tiles in this volume are buildable. Defaults to Invalid (no placement on spawners).")]
public PlacementValidity placementValidity = PlacementValidity.Invalid;
// Spawners draw above player zones so the player zone color reads as background.
private const float FillYLevel = 0.06f;
private const float ArrowYLevel = 0.07f;
private const float ArrowLength = 1.5f; // 1.5 tiles per gizmo design
protected override bool GetAlwaysShowToggle(LevelAuthoring authoring)
{
return authoring.alwaysShowSpawners;
}
private void OnDrawGizmosSelected()
{
DrawGizmosCore();
}
private void OnDrawGizmos()
{
if (ShouldDrawAlwaysOn())
{
DrawGizmosCore();
}
}
private void DrawGizmosCore()
{
Color baseColor = PlayerColors.Get(owner);
DrawTileCoverageFill(baseColor, alpha: 0.40f, yLevel: FillYLevel);
DrawRectangularOutline(baseColor, yLevel: FillYLevel);
// Direction arrow originates from the volume's center and extends 1.5 tiles in the
// declared facing direction, rendered in opaque owner color.
var col = Collider;
if (col != null)
{
Vector3 arrowOrigin = new Vector3(col.bounds.center.x, ArrowYLevel, col.bounds.center.z);
Vector3 arrowDir = DirectionToWorld(spawnFacing);
DrawArrow(arrowOrigin, arrowDir, ArrowLength, baseColor);
}
#if UNITY_EDITOR
// Label conditional on multi-spawner status:
// - Single-spawner zone: "Player N Spawn"
// - Multi-spawner zone: "Player N Spawn 0", "Player N Spawn 1", etc.
// Multi-spawner detection scans other SpawnerVolumes in the scene with the same owner.
// Same posture as the leak-exit target lookup: cheap on realistic maps, can be cached
// if it ever shows up as an editor-perf issue.
if (col != null)
{
Vector3 labelPos = new Vector3(col.bounds.center.x, ArrowYLevel + 0.1f, col.bounds.center.z);
string labelText = ZoneHasMultipleSpawners()
? $"{FormatPlayerName(owner)} Spawn {spawnerIdInZone}"
: $"{FormatPlayerName(owner)} Spawn";
Handles.Label(labelPos, labelText);
}
#endif
}
/// <summary>
/// Returns true if more than one active SpawnerVolume in the scene shares this spawner's
/// <see cref="owner"/>. Used to decide whether to suffix the gizmo label with the spawner ID.
/// </summary>
private bool ZoneHasMultipleSpawners()
{
var spawners = Object.FindObjectsByType<SpawnerVolume>(FindObjectsInactive.Exclude);
if (spawners == null) return false;
int count = 0;
for (int i = 0; i < spawners.Length; i++)
{
if (spawners[i] == null) continue;
if (spawners[i].owner == owner)
{
count++;
if (count > 1) return true;
}
}
return false;
}
}
}