using System.Collections.Generic; using Unity.Mathematics; using UnityEngine; using UnityEngine.Splines; namespace ActionRPG.Splines { /// /// 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. /// [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 _placed = new(); private void OnEnable() { _splineContainer = GetComponent(); 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(); } /// Clears existing placed objects and spawns new ones along the spline. 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); } } /// Destroys all objects previously placed by this component. 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(); } } }