using System; using System.Collections.Generic; using Unity.Collections; using Unity.Mathematics; using UnityEngine; using UnityEngine.Splines; using Random = UnityEngine.Random; #if UNITY_EDITOR using UnityEditor; #endif /// /// SplineInstantiate is used to automatically instantiate prefabs or objects along a spline. /// [ExecuteInEditMode] [AddComponentMenu("Splines/Spline Instantiate")] public class SplineInstantiate : SplineComponent { /// /// The space in which to interpret the offset, this can be different from the orientation space used to instantiate objects. /// public enum OffsetSpace { /// Use the spline space to orient instances. [InspectorName("Spline Element")] Spline = Space.Spline, /// Use the spline GameObject space to orient instances. [InspectorName("Spline Object")] Local = Space.Local, /// Use world space to orient instances. [InspectorName("World Space")] World = Space.World, /// Use the original object space to orient instances. [InspectorName("Instantiated Object")] Object } [Serializable] internal struct Vector3Offset { [Flags] public enum Setup { None = 0x0, HasOffset= 0x1, HasCustomSpace= 0x2 } public Setup setup; public Vector3 min; public Vector3 max; public bool randomX; public bool randomY; public bool randomZ; public OffsetSpace space; public bool hasOffset => ( setup & Setup.HasOffset ) != 0; public bool hasCustomSpace => ( setup & Setup.HasCustomSpace ) != 0; internal Vector3 GetNextOffset() { if(( setup & Setup.HasOffset ) != 0) { return new Vector3( randomX ? Random.Range(min.x,max.x) : min.x, randomY ? Random.Range(min.y,max.y) : min.y, randomZ ? Random.Range(min.z,max.z) : min.z); } return Vector3.zero; } internal void CheckMinMaxValidity() { max.x = Mathf.Max(min.x, max.x); max.y = Mathf.Max(min.y, max.y); max.z = Mathf.Max(min.z, max.z); } internal void CheckMinMax() { CheckMinMaxValidity(); if(max.magnitude > 0) setup |= Setup.HasOffset; else setup &= ~Setup.HasOffset; } internal void CheckCustomSpace(Space instanceSpace) { if((int)space == (int)instanceSpace) setup &= ~Setup.HasCustomSpace; else setup |= Setup.HasCustomSpace; } } /// /// Describe the item prefab to instantiate and associate it with a probability /// [Serializable] public struct InstantiableItem { /// The prefab to instantiate. public GameObject prefab; /// Probability for this prefab. public float probability; } /// /// Describe the possible methods to instantiate instances along the spline. /// public enum Method { /// Use exact number of instances. [InspectorName("Instance Count")] InstanceCount, /// Use distance along the spline between 2 instances. [InspectorName("Spline Distance")] SpacingDistance, /// Use distance in straight line between 2 instances. [InspectorName("Linear Distance")] LinearDistance } /// /// Describe the space that is used to orientate the instantiated object /// public enum Space { /// Use the spline space to orient instances. [InspectorName("Spline Element")] Spline, /// Use the spline GameObject space to orient instances. [InspectorName("Spline Object")] Local, /// Use world space to orient instances. [InspectorName("World Space")] World, } [SerializeField] SplineContainer m_Container; /// /// The SplineContainer containing the targeted spline. /// public SplineContainer container { get => m_Container; set => m_Container = value; } [SerializeField] List m_ItemsToInstantiate = new List(); [SerializeField] Method m_Method = Method.SpacingDistance; /// /// The instantiation method to use. /// public Method method { get => m_Method; set => m_Method = value; } [SerializeField] Space m_Space = Space.Spline; /// /// The space in which to orient the instanced object. /// public Space space { get => m_Space; set => m_Space = value; } [SerializeField] Vector2 m_Spacing = new Vector2(1f,1f); /// /// Minimum spacing between 2 generated instances, /// if equal to the maxSpacing, then all instances will have the exact same spacing /// float minSpacing { get => m_Spacing.x; set { m_Spacing = new Vector2(value, m_Spacing.y); ValidateSpacing(); } } /// /// Maximum spacing between 2 generated instances, /// if equal to the minSpacing, then all instances will have the exact same spacing /// float maxSpacing { get => m_Spacing.y; set { m_Spacing = new Vector2(m_Spacing.x, value); ValidateSpacing(); } } [SerializeField] AlignAxis m_Up = AlignAxis.YAxis; /// /// Up axis of the object, by default set to the Y Axis /// public AlignAxis upAxis { get => m_Up; set => m_Up = value; } [SerializeField] AlignAxis m_Forward = AlignAxis.ZAxis; /// /// Forward axis of the object, by default set to the Z Axis /// public AlignAxis forwardAxis { get => m_Forward; set { m_Forward = value; ValidateAxis(); } } [SerializeField] Vector3Offset m_PositionOffset; /// /// Minimum (X,Y,Z) position offset to randomize instanced objects positions. /// (X,Y and Z) values have to be lower to the ones of maxPositionOffset. /// public Vector3 minPositionOffset { get => m_PositionOffset.min; set { m_PositionOffset.min = value; m_PositionOffset.CheckMinMax(); } } /// /// Maximum (X,Y,Z) position offset to randomize instanced objects positions. /// (X,Y and Z) values have to be higher to the ones of minPositionOffset. /// public Vector3 maxPositionOffset { get => m_PositionOffset.max; set { m_PositionOffset.max = value; m_PositionOffset.CheckMinMax(); } } /// /// Coordinate space to use to offset positions of the instances. /// public OffsetSpace positionSpace { get => m_PositionOffset.space; set { m_PositionOffset.space = value; m_PositionOffset.CheckCustomSpace(m_Space); } } [SerializeField] Vector3Offset m_RotationOffset; /// /// Minimum (X,Y,Z) euler rotation offset to randomize instanced objects rotations. /// (X,Y and Z) values have to be lower to the ones of maxRotationOffset. /// public Vector3 minRotationOffset { get => m_RotationOffset.min; set { m_RotationOffset.min = value; m_RotationOffset.CheckMinMax(); } } /// /// Maximum (X,Y,Z) euler rotation offset to randomize instanced objects rotations. /// (X,Y and Z) values have to be higher to the ones of minRotationOffset. /// public Vector3 maxRotationOffset { get => m_RotationOffset.max; set { m_RotationOffset.max = value; m_RotationOffset.CheckMinMax(); } } /// /// Coordinate space to use to offset rotations of the instances. /// public OffsetSpace rotationSpace { get => m_RotationOffset.space; set { m_RotationOffset.space = value; m_RotationOffset.CheckCustomSpace(m_Space); } } [SerializeField] Vector3Offset m_ScaleOffset; /// /// Minimum (X,Y,Z) scale offset to randomize instanced objects scales. /// (X,Y and Z) values have to be lower to the ones of maxScaleOffset. /// public Vector3 minScaleOffset { get => m_ScaleOffset.min; set { m_ScaleOffset.min = value; m_ScaleOffset.CheckMinMax(); } } /// /// Maximum (X,Y,Z) scale offset to randomize instanced objects scales. /// (X,Y and Z) values have to be higher to the ones of minScaleOffset. /// public Vector3 maxScaleOffset { get => m_ScaleOffset.max; set { m_ScaleOffset.max = value; m_ScaleOffset.CheckMinMax(); } } /// /// Coordinate space to use to offset rotations of the instances (usually OffsetSpace.Object). /// public OffsetSpace scaleSpace { get => m_ScaleOffset.space; set { m_ScaleOffset.space = value; m_ScaleOffset.CheckCustomSpace(m_Space); } } [SerializeField] List m_Instances = new List(); internal List instances => m_Instances; bool m_InstancesCacheDirty = false; [SerializeField] bool m_AutoRefresh = true; InstantiableItem m_CurrentItem; bool m_SplineDirty = false; float m_MaxProbability = 1f; float maxProbability { get => m_MaxProbability; set { if(m_MaxProbability != value) { m_MaxProbability = value; m_InstancesCacheDirty = true; } } } [HideInInspector] [SerializeField] int m_Seed = 0; int seed { get => m_Seed; set { m_Seed = value; m_InstancesCacheDirty = true; Random.InitState(m_Seed); } } void OnEnable() { if(m_Seed == 0) m_Seed = GetInstanceID(); #if UNITY_EDITOR Undo.undoRedoPerformed += UndoRedoPerformed; #endif } void OnDestroy() { #if UNITY_EDITOR Undo.undoRedoPerformed -= UndoRedoPerformed; #endif Clear(); } void UndoRedoPerformed() { m_InstancesCacheDirty = true; m_SplineDirty = true; UpdateInstances(); } void OnValidate() { if(m_Container != null && m_Container.Spline != null) m_Container.Spline.changed += delegate() { m_SplineDirty = m_AutoRefresh; }; ValidateSpacing(); m_SplineDirty = m_AutoRefresh; float probability = 0; for(int i = 0; i /// This method prevents Up and Forward axis to be aligned. /// Up axis will always be kept as the prioritized one. /// If Forward axis is in the same direction than the Up (or -Up) it'll be changed to the next axis. /// void ValidateAxis() { if(m_Forward == m_Up || (int)m_Forward == ( (int)m_Up + 3 ) % 6) m_Forward = (AlignAxis)(( (int)m_Forward + 1 ) % 6); } internal void SetSplineDirty(Spline spline) { if(m_Container != null && spline == m_Container.Spline && m_AutoRefresh) UpdateInstances(); } void InitContainer() { if(m_Container == null) m_Container = GetComponent(); if(m_Container != null && m_Container.Spline != null) { m_Container.Spline.changed += () => { m_SplineDirty = m_AutoRefresh; }; } } /// /// Clear all the created instances along the spline /// public void Clear() { SetDirty(); TryClearCache(); } /// /// Set the created instances dirty to erase them next time instances will be generated /// (otherwise the next generation will reuse cached instances) /// public void SetDirty() { m_InstancesCacheDirty = true; } void TryClearCache() { if(!m_InstancesCacheDirty) { for(int i = 0; i < m_Instances.Count; i++) { if(m_Instances[i] == null) { m_InstancesCacheDirty = true; break; } } } if(m_InstancesCacheDirty) { for(int i = m_Instances.Count - 1; i >= 0; --i) { #if UNITY_EDITOR DestroyImmediate(m_Instances[i]); #else Destroy(m_Instances[i]); #endif } m_Instances.Clear(); m_InstancesCacheDirty = false; } } /// /// Change the Random seed to obtain a new generation along the Spline /// public void Randomize() { #if UNITY_EDITOR Undo.RecordObject(this, "Changing SplineInstantiate seed"); #endif seed = Random.Range(int.MinValue, int.MaxValue); m_SplineDirty = true; } void Update() { if(m_SplineDirty) UpdateInstances(); } /// /// Create and update all instances along the spline based on the list of available prefabs/objects. /// public void UpdateInstances() { TryClearCache(); if(m_Container == null) InitContainer(); if(m_Container == null || m_ItemsToInstantiate.Count == 0) return; #if UNITY_EDITOR Undo.RegisterFullObjectHierarchyUndo(this,"Update spline instances"); #endif using(var nativeSpline = new NativeSpline(m_Container.Spline, m_Container.transform.localToWorldMatrix, Allocator.TempJob)) { float currentDist = 0f; float splineLength = m_Container.CalculateLength(); Random.InitState(m_Seed); //Spawning instances var times = new List(); int index = 0; var spacing = Random.Range(m_Spacing.x, m_Spacing.y); if(m_Method == Method.InstanceCount && spacing <= 1) currentDist = (int)spacing == 1 ? splineLength / 2f : splineLength + 1f; while(currentDist <= splineLength) { var prefabIndex = m_ItemsToInstantiate.Count == 1 ? 0 : GetPrefabIndex(); m_CurrentItem = m_ItemsToInstantiate[prefabIndex]; if(m_CurrentItem.prefab == null) return; if(index >= m_Instances.Count) { #if UNITY_EDITOR var assetType = PrefabUtility.GetPrefabAssetType(m_CurrentItem.prefab); if(assetType == PrefabAssetType.MissingAsset) { Debug.LogError($"Trying to instantiate a missing asset for item index [{prefabIndex}]."); return; } if(assetType != PrefabAssetType.NotAPrefab) m_Instances.Add(PrefabUtility.InstantiatePrefab(m_CurrentItem.prefab, transform) as GameObject); else #endif m_Instances.Add(Instantiate(m_CurrentItem.prefab, transform)); m_Instances[index].hideFlags |= HideFlags.HideInHierarchy; } m_Instances[index].transform.localPosition = m_CurrentItem.prefab.transform.localPosition; m_Instances[index].transform.localRotation = m_CurrentItem.prefab.transform.localRotation; m_Instances[index].transform.localScale = m_CurrentItem.prefab.transform.localScale; times.Add(currentDist / splineLength); if(m_Method == Method.SpacingDistance) { spacing = Random.Range(m_Spacing.x, m_Spacing.y); currentDist += spacing; } else if(m_Method == Method.InstanceCount) { if(spacing > 1) { var previousDist = currentDist; currentDist += splineLength / ( nativeSpline.Closed ? (int)spacing : (int)spacing - 1 ); if(previousDist < splineLength && currentDist > splineLength) currentDist = splineLength; } else currentDist += splineLength; } else if(m_Method == Method.LinearDistance) { //m_Spacing.y is set to NaN to trigger automatic computation if(float.IsNaN(m_Spacing.y)) { var meshfilter = m_Instances[index].GetComponent(); var axis = Vector3.right; if(m_Forward == AlignAxis.ZAxis || m_Forward == AlignAxis.NegativeZAxis) axis = Vector3.forward; if(m_Forward == AlignAxis.YAxis || m_Forward == AlignAxis.NegativeYAxis) axis = Vector3.up; if(meshfilter == null) { meshfilter = m_Instances[index].GetComponentInChildren(); if(meshfilter != null) axis = Vector3.Scale(meshfilter.transform.InverseTransformDirection(m_Instances[index].transform.TransformDirection(axis)), meshfilter.transform.lossyScale); } if(meshfilter != null) { var bounds = meshfilter.sharedMesh.bounds; var filters = meshfilter.GetComponentsInChildren(); foreach(var filter in filters) { var localBounds = filter.sharedMesh.bounds; bounds.size = new Vector3(Mathf.Max(bounds.size.x, localBounds.size.x), Mathf.Max(bounds.size.z, localBounds.size.z), Mathf.Max(bounds.size.z, localBounds.size.z)); } spacing = Vector3.Scale(bounds.size, axis).magnitude; } } else spacing = Random.Range(m_Spacing.x, m_Spacing.y); nativeSpline.GetPointAtLinearDistance(times[index], spacing, out var nextT); currentDist = nextT >= 1f ? splineLength + 1f : nextT * splineLength; } index++; } //removing extra unnecessary instances for(int u = m_Instances.Count-1; u >= index; u--) { if(m_Instances[u] != null) { #if UNITY_EDITOR DestroyImmediate(m_Instances[u]); #else Destroy(m_Instances[u]); #endif m_Instances.RemoveAt(u); } } //Positioning elements for(int i = 0; i < index; i++) { var instance = m_Instances[i]; var splineT = times[i]; nativeSpline.Evaluate(splineT, out var position, out var direction, out var splineUp); instance.transform.position = position; if(m_Method == Method.LinearDistance) { var nextPosition = nativeSpline.EvaluatePosition(i + 1 < index ? times[i + 1] : 1f); direction = nextPosition - position; } var up = splineUp; var forward = direction; if(m_Space == Space.World) { up = Vector3.up; forward = Vector3.forward; }else if(m_Space == Space.Local) { up = transform.TransformDirection(Vector3.up); forward = transform.TransformDirection(Vector3.forward); } // Correct forward and up vectors based on axis remapping parameters var remappedForward = GetAxis(m_Forward); var remappedUp = GetAxis(m_Up); var axisRemapRotation = Quaternion.Inverse(Quaternion.LookRotation(remappedForward, remappedUp)); instance.transform.rotation = Quaternion.LookRotation(forward, up) * axisRemapRotation; var customUp = up; var customForward = forward; if(m_PositionOffset.hasOffset) { if(m_PositionOffset.hasCustomSpace) GetCustomSpaceAxis(m_PositionOffset.space,splineUp, direction, instance.transform, out customUp, out customForward); var offset = m_PositionOffset.GetNextOffset(); var right = Vector3.Cross(customUp, customForward).normalized; instance.transform.position += offset.x * right + offset.y * (Vector3)customUp + offset.z * (Vector3)customForward; } if(m_RotationOffset.hasOffset) { customUp = up; customForward = forward; if(m_RotationOffset.hasCustomSpace) { GetCustomSpaceAxis(m_RotationOffset.space,splineUp, direction, instance.transform, out customUp, out customForward); if(m_RotationOffset.space == OffsetSpace.Object) axisRemapRotation = quaternion.identity; } var offset = m_RotationOffset.GetNextOffset(); var right = Vector3.Cross(customUp, customForward).normalized; customForward = Quaternion.AngleAxis(offset.y, customUp) * Quaternion.AngleAxis(offset.x, right) * customForward; customUp = Quaternion.AngleAxis(offset.x, right) * Quaternion.AngleAxis(offset.z, customForward) * customUp; instance.transform.rotation = Quaternion.LookRotation(customForward, customUp) * axisRemapRotation; } if(m_ScaleOffset.hasOffset) { customUp = up; customForward = forward; if(m_ScaleOffset.hasCustomSpace) GetCustomSpaceAxis(m_ScaleOffset.space,splineUp, direction, instance.transform, out customUp, out customForward); customUp = instance.transform.InverseTransformDirection(customUp); customForward = instance.transform.InverseTransformDirection(customForward); var offset = m_ScaleOffset.GetNextOffset(); var right = Vector3.Cross(customUp, customForward).normalized; instance.transform.localScale += offset.x * right + offset.y * (Vector3)customUp + offset.z * (Vector3)customForward;; } } } m_SplineDirty = false; } void GetCustomSpaceAxis(OffsetSpace space, float3 splineUp, float3 direction, Transform instanceTransform, out float3 customUp,out float3 customForward) { customUp = Vector3.up; customForward = Vector3.forward; if(space == OffsetSpace.Local) { customUp = transform.TransformDirection(Vector3.up); customForward = transform.TransformDirection(Vector3.forward); } else if(space == OffsetSpace.Spline) { customUp = splineUp; customForward = direction; } else if(space == OffsetSpace.Object) { customUp = instanceTransform.TransformDirection(Vector3.up); customForward = instanceTransform.TransformDirection(Vector3.forward); } } int GetPrefabIndex() { var prefabChoice = Random.Range(0, m_MaxProbability); var currentProbability = 0f; for(int i = 0; i < m_ItemsToInstantiate.Count; i++) { if(m_ItemsToInstantiate[i].prefab == null) continue; var itemProbability = m_ItemsToInstantiate[i].probability; if(prefabChoice < currentProbability + itemProbability) return i; currentProbability += itemProbability; } return 0; } }