Files
ARPGCastleDefence/Assets/Scripts/Splines/SplineObjectPlacer.cs

134 lines
4.5 KiB
C#

using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Splines;
namespace ActionRPG.Splines
{
/// <summary>
/// Places a prefab at regular intervals along a SplineContainer.
/// Useful for castle walls, fences, battlements, and path markers.
/// Add a SplineContainer to the same GameObject to edit the path in the Scene view.
/// </summary>
[ExecuteAlways]
[RequireComponent(typeof(SplineContainer))]
public class SplineObjectPlacer : MonoBehaviour
{
[Header("Placement")]
[SerializeField] private GameObject _prefab;
[SerializeField, Min(0.1f)] private float _spacing = 2f;
[Header("Alignment")]
[SerializeField] private bool _alignToSpline = true;
[SerializeField] private Vector3 _rotationOffset = Vector3.zero;
[SerializeField] private Vector3 _positionOffset = Vector3.zero;
[Header("Behaviour")]
[Tooltip("Automatically regenerate whenever the spline is modified in the editor.")]
[SerializeField] private bool _autoRegenerate = true;
private SplineContainer _splineContainer;
private readonly List<GameObject> _placed = new();
private void OnEnable()
{
_splineContainer = GetComponent<SplineContainer>();
Spline.Changed += OnSplineChanged;
if (_autoRegenerate)
Regenerate();
}
private void OnDisable()
{
Spline.Changed -= OnSplineChanged;
}
private void OnSplineChanged(Spline spline, int knotIndex, SplineModification modification)
{
if (!_autoRegenerate)
return;
// Only react to changes on our own spline container
if (_splineContainer == null || spline != _splineContainer.Spline)
return;
Regenerate();
}
/// <summary>Clears existing placed objects and spawns new ones along the spline.</summary>
public void Regenerate()
{
Clear();
if (_prefab == null || _splineContainer == null)
return;
float length = _splineContainer.CalculateLength();
if (length <= 0f)
return;
int count = Mathf.FloorToInt(length / _spacing);
if (count == 0)
return;
// For closed splines fill exactly one loop; for open splines include the last point
bool closed = _splineContainer.Spline.Closed;
int steps = closed ? count : count + 1;
for (int i = 0; i < steps; i++)
{
float t = (float)i / count;
SplineUtility.Evaluate(
_splineContainer.Spline,
t,
out float3 localPos,
out float3 localTangent,
out float3 localUp
);
Vector3 worldPos = transform.TransformPoint(localPos) + _positionOffset;
Vector3 worldTangent = transform.TransformDirection(localTangent);
Vector3 worldUp = transform.TransformDirection(localUp);
Quaternion splineRot = worldTangent != Vector3.zero
? Quaternion.LookRotation(worldTangent, worldUp)
: Quaternion.identity;
Quaternion finalRot = _alignToSpline
? splineRot * Quaternion.Euler(_rotationOffset)
: Quaternion.Euler(_rotationOffset);
#if UNITY_EDITOR
GameObject instance = UnityEditor.PrefabUtility.InstantiatePrefab(_prefab, transform) as GameObject;
if (instance == null)
instance = Instantiate(_prefab, transform);
#else
GameObject instance = Instantiate(_prefab, transform);
#endif
instance.transform.SetPositionAndRotation(worldPos, finalRot);
instance.name = $"{_prefab.name}_{i}";
_placed.Add(instance);
}
}
/// <summary>Destroys all objects previously placed by this component.</summary>
public void Clear()
{
foreach (GameObject obj in _placed)
{
if (obj == null) continue;
#if UNITY_EDITOR
if (!Application.isPlaying)
DestroyImmediate(obj);
else
#endif
Destroy(obj);
}
_placed.Clear();
}
}
}