134 lines
4.5 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|