using System; using System.Collections.Generic; using System.Linq; using Unity.Collections; using Unity.Mathematics; using UnityEngine; using UnityEngine.Serialization; #if UNITY_EDITOR using UnityEditor; #endif namespace UnityEngine.Splines { /// /// 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. [HideInInspector] [Obsolete("Use Prefab instead.", false)] public GameObject prefab; /// The prefab to instantiate. [FormerlySerializedAs("prefab")] public GameObject Prefab; /// Probability for this prefab. [HideInInspector] [Obsolete("Use Probability instead.", false)] public float probability; /// Probability for this prefab. [FormerlySerializedAs("probability")] 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 } /// /// Describes the coordinate space that is used to orient 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. /// [Obsolete("Use Container instead.", false)] public SplineContainer container => Container; /// /// The SplineContainer containing the targeted spline. /// public SplineContainer Container { get => m_Container; set => m_Container = value; } [SerializeField] List m_ItemsToInstantiate = new List(); /// /// The items to use in the instantiation. /// public InstantiableItem[] itemsToInstantiate { get => m_ItemsToInstantiate.ToArray(); set { m_ItemsToInstantiate.Clear(); m_ItemsToInstantiate.AddRange(value); } } [SerializeField] Method m_Method = Method.SpacingDistance; /// /// The instantiation method to use. /// [Obsolete("Use InstantiateMethod instead.", false)] public Method method => InstantiateMethod; /// /// The instantiation method to use. /// public Method InstantiateMethod { get => m_Method; set => m_Method = value; } [SerializeField] Space m_Space = Space.Spline; /// /// The coordinate space in which to orient the instanced object. /// [Obsolete("Use CoordinateSpace instead.", false)] public Space space => CoordinateSpace; /// /// The coordinate space in which to orient the instanced object. /// public Space CoordinateSpace { 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. /// public 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 /// public 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. /// [Obsolete("Use UpAxis instead.", false)] public AlignAxis upAxis => UpAxis; /// /// 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 /// [Obsolete("Use ForwardAxis instead.", false)] public AlignAxis forwardAxis => ForwardAxis; /// /// 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. /// [Obsolete("Use MinPositionOffset instead.", false)] public Vector3 minPositionOffset => MinPositionOffset; /// /// 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. /// [Obsolete("Use MaxPositionOffset instead.", false)] public Vector3 maxPositionOffset => MaxPositionOffset; /// /// 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. /// [Obsolete("Use PositionSpace instead.", false)] public OffsetSpace positionSpace => PositionSpace; /// /// 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. /// [Obsolete("Use MinRotationOffset instead.", false)] public Vector3 minRotationOffset => MinRotationOffset; /// /// 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. /// [Obsolete("Use MaxRotationOffset instead.", false)] public Vector3 maxRotationOffset => MaxRotationOffset; /// /// 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. /// [Obsolete("Use RotationSpace instead.", false)] public OffsetSpace rotationSpace => RotationSpace; /// /// 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. /// [Obsolete("Use MinScaleOffset instead.", false)] public Vector3 minScaleOffset => MinScaleOffset; /// /// 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. /// [Obsolete("Use MaxScaleOffset instead.", false)] public Vector3 maxScaleOffset => MaxScaleOffset; /// /// 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). /// [Obsolete("Use ScaleSpace instead.", false)] public OffsetSpace scaleSpace => ScaleSpace; /// /// 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); } } // Keep old serialization of instances to ensure that no zombie instances will remain serialized. [SerializeField, HideInInspector, FormerlySerializedAs("m_Instances")] List m_DeprecatedInstances = new List(); const string k_InstancesRootName = "root-"; GameObject m_InstancesRoot; internal GameObject InstancesRoot => m_InstancesRoot; Transform instancesRootTransform { get { if (m_InstancesRoot == null) { m_InstancesRoot = new GameObject(k_InstancesRootName+GetInstanceID()); m_InstancesRoot.hideFlags |= HideFlags.HideAndDontSave; m_InstancesRoot.transform.parent = transform; m_InstancesRoot.transform.localPosition = Vector3.zero; m_InstancesRoot.transform.localRotation = Quaternion.identity; } return m_InstancesRoot.transform; } } readonly 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; } } } [SerializeField] int m_Seed = 0; /// /// Value used to initialize the pseudorandom number generator of the created instances. /// public int Seed { get => m_Seed; set { m_Seed = value; m_InstancesCacheDirty = true; } } List m_TimesCache = new(); List m_LengthsCache = new(); void OnEnable() { if (m_Seed == 0) m_Seed = GetInstanceID(); #if UNITY_EDITOR Undo.undoRedoPerformed += UndoRedoPerformed; #if UNITY_2022_3_OR_NEWER // A problem can be raised when undoing Splines changes. Insuring the caches are clear at that moment. if (Undo.isProcessing) { //Insure caches are clear after undo m_Container?.ClearCaches(); } #else m_Container?.ClearCaches(); #endif #endif //Bugfix for SPLB-107: Duplicating a SplineInstantiate is making children visible //This ensure to delete the invalid children. CheckChildrenValidity(); Spline.Changed += OnSplineChanged; UpdateInstances(); } void OnDisable() { #if UNITY_EDITOR Undo.undoRedoPerformed -= UndoRedoPerformed; #endif Spline.Changed -= OnSplineChanged; Clear(); } void UndoRedoPerformed() { m_InstancesCacheDirty = true; m_SplineDirty = true; } void OnValidate() { ValidateSpacing(); m_SplineDirty = m_AutoRefresh; EnsureItemsValidity(); m_PositionOffset.CheckMinMaxValidity(); m_RotationOffset.CheckMinMaxValidity(); m_ScaleOffset.CheckMinMaxValidity(); } void EnsureItemsValidity() { float probability = 0; for (int i = 0; i < m_ItemsToInstantiate.Count; i++) { var item = m_ItemsToInstantiate[i]; if (item.Prefab != null) { if (transform.IsChildOf(item.Prefab.transform)) { Debug.LogWarning("Instantiating a parent of the SplineInstantiate object itself is not permitted" + $" ({item.Prefab.name} is a parent of {transform.gameObject.name}).", this); item.Prefab = null; m_ItemsToInstantiate[i] = item; } else probability += item.Probability; } } maxProbability = probability; } void CheckChildrenValidity() { // All the children have to be checked in case multiple SplineInstantiate components are used on the same GameObject. // We want to be able to have multiple components as it allows for example to instantiate grass and // trees with different parameters on the same object. var ids = GetComponents().Select(sInstantiate => sInstantiate.GetInstanceID()).ToList(); var childCount = transform.childCount; for (int i = childCount - 1; i >= 0; --i) { var child = transform.GetChild(i).gameObject; if (child.name.StartsWith(k_InstancesRootName)) { var invalid = true; foreach (var instanceID in ids) { if (child.name.Equals(k_InstancesRootName + instanceID)) { invalid = false; break; } } if (invalid) #if UNITY_EDITOR DestroyImmediate(child); #else Destroy(child); #endif } } } void ValidateSpacing() { var xSpacing = Mathf.Max(0.1f, m_Spacing.x); if (m_Method != Method.LinearDistance) { var ySpacing = float.IsNaN(m_Spacing.y) ? xSpacing : Mathf.Max(0.1f, m_Spacing.y); m_Spacing = new Vector2(xSpacing, Mathf.Max(xSpacing, ySpacing)); } else if (m_Method == Method.LinearDistance) { var ySpacing = float.IsNaN(m_Spacing.y) ? m_Spacing.y : xSpacing; m_Spacing = new Vector2(xSpacing, ySpacing); } } /// /// 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 && m_Container.Splines.Contains(spline) && m_AutoRefresh) UpdateInstances(); } void InitContainer() { if (m_Container == null) m_Container = GetComponent(); } /// /// 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 } #if UNITY_EDITOR DestroyImmediate(m_InstancesRoot); #else Destroy(m_InstancesRoot); #endif m_Instances.Clear(); m_InstancesCacheDirty = false; } } void ClearDeprecatedInstances() { foreach (var instance in m_DeprecatedInstances) { #if UNITY_EDITOR DestroyImmediate(instance); #else Destroy(instance); #endif } m_DeprecatedInstances.Clear(); } /// /// Change the Random seed to obtain a new generation along the Spline /// public void Randomize() { 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() { ClearDeprecatedInstances(); TryClearCache(); if (m_Container == null) InitContainer(); if (m_Container == null || m_Container.Splines.Count ==0 || m_ItemsToInstantiate.Count == 0) return; const float k_Epsilon = 0.001f; var randomState = Random.state; Random.InitState(m_Seed); int index = 0; int indexOffset = 0; m_LengthsCache.Clear(); var splineLength = 0f; var totalSplineLength = 0f; for (int splineIndex = 0; splineIndex < m_Container.Splines.Count; splineIndex++) { var length = m_Container.CalculateLength(splineIndex); m_LengthsCache.Add(length); totalSplineLength += length; } var spacing = Random.Range(m_Spacing.x, m_Spacing.y); var currentDist = 0f; var instanceCountModeStep = 0f; if (m_Method == Method.InstanceCount) { // Advance dist by half length if we only need to spawn one item - we want to spawn it mid total spline length if (spacing == 1) currentDist = totalSplineLength / 2f; else if (spacing < 1) // Using less operator here as the spacing setters always clamp spacing to a minimum value of 0.1 { // If there's nothing to spawn, make currentDist larger than length to effectively skip prefab spawnning but still trigger previous spawn cleanup currentDist = totalSplineLength + 1f; } // Take into account the Closed property only if there's one spline in container if (m_Container.Splines.Count == 1) instanceCountModeStep = totalSplineLength / (m_Container.Splines[0].Closed ? (int)spacing : (int)spacing - 1); else instanceCountModeStep = totalSplineLength / ((int)spacing - 1); } // Needs to ensure the validity of the items to instantiate to be certain we don't have a parent of the hierarchy in these. EnsureItemsValidity(); for (int splineIndex = 0; splineIndex < m_Container.Splines.Count; splineIndex++) { var spline = m_Container.Splines[splineIndex]; using (var nativeSpline = new NativeSpline(spline, m_Container.transform.localToWorldMatrix, Allocator.TempJob)) { splineLength = m_LengthsCache[splineIndex]; var terminateSpawning = false; if (m_Method == Method.InstanceCount) { if (currentDist > (splineLength + k_Epsilon) && currentDist <= (totalSplineLength + k_Epsilon)) { currentDist -= splineLength; terminateSpawning = true; } } else currentDist = 0f; m_TimesCache.Clear(); int timeIndex = 0; while (currentDist <= (splineLength + k_Epsilon) && !terminateSpawning) { if (!SpawnPrefab(index)) break; m_TimesCache.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 += instanceCountModeStep; if (previousDist < splineLength && currentDist > (splineLength + k_Epsilon)) { currentDist -= splineLength; terminateSpawning = true; } } // If we're here, we're spawning 1 object or none, therefore add total length to currentDist // so that we no longer enter the while loop as the object has been spawned already else currentDist += totalSplineLength; } 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(m_TimesCache[timeIndex], spacing, out var nextT); currentDist = nextT >= 1f ? splineLength + 1f : nextT * splineLength; } index++; timeIndex++; } //removing extra unnecessary instances for (int i = m_Instances.Count - 1; i >= index; i--) { if (m_Instances[i] != null) { #if UNITY_EDITOR DestroyImmediate(m_Instances[i]); #else Destroy(m_Instances[i]); #endif m_Instances.RemoveAt(i); } } //Positioning elements for (int i = indexOffset; i < index; i++) { var instance = m_Instances[i]; var splineT = m_TimesCache[i - indexOffset]; 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 ? m_TimesCache[i + 1 - indexOffset] : 1f); direction = nextPosition - position; } var up = math.normalizesafe(splineUp); var forward = math.normalizesafe(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 = math.normalizesafe(GetAxis(m_Forward)); var remappedUp = math.normalizesafe(GetAxis(m_Up)); var axisRemapRotation = Quaternion.Inverse(quaternion.LookRotationSafe(remappedForward, remappedUp)); instance.transform.rotation = quaternion.LookRotationSafe(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_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).normalized; customForward = instance.transform.InverseTransformDirection(customForward).normalized; 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; } 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.LookRotationSafe(customForward, customUp) * axisRemapRotation; } } indexOffset = index; } } m_SplineDirty = false; Random.state = randomState; } bool SpawnPrefab(int index) { var prefabIndex = m_ItemsToInstantiate.Count == 1 ? 0 : GetPrefabIndex(); m_CurrentItem = m_ItemsToInstantiate[prefabIndex]; if (m_CurrentItem.Prefab == null) return false; 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}].", this); return false; } if (assetType != PrefabAssetType.NotAPrefab && !Application.isPlaying) m_Instances.Add(PrefabUtility.InstantiatePrefab(m_CurrentItem.Prefab, instancesRootTransform) as GameObject); else #endif m_Instances.Add(Instantiate(m_CurrentItem.Prefab, instancesRootTransform)); #if UNITY_EDITOR m_Instances[index].hideFlags |= HideFlags.HideAndDontSave; // Retrieve current static flags to pass them along on created instances var staticFlags = GameObjectUtility.GetStaticEditorFlags(gameObject); GameObjectUtility.SetStaticEditorFlags(m_Instances[index], staticFlags); #endif } 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; return true; } 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; } void OnSplineChanged(Spline spline, int knotIndex, SplineModification modificationType) { if (m_Container != null && m_Container.Spline == spline) m_SplineDirty = m_AutoRefresh; } } }