#if UNITY_EDITOR using UnityEngine; using UnityEditor; using UnityEngine.UIElements; using System; using System.Collections.Generic; namespace GSpawn { public enum ObjectSpawnCurveEditMode { None = 0, SelectControlPoints, InsertControlPoints, } public enum ObjectSpawnCurveRefreshReason { CurvePrefabProfileChanged = 0, CurvePrefabUsedStateChanged, CurvePrefabSpawnChanceChanged, CurvePrefabsDeleted, UseDefaultSettings, Refresh, Other } public struct ObjectSpawnCurvePOI { public Vector3 pointOnSegment; public int sampleSegmentIndex; public bool isValid; } public class ObjectSpawnCurveObjectData { public GameObject gameObject; public ObjectSpawnCurvePOI fwPOI; // Note: Doesn't matter if curve prefab is destroyed. This will be refreshed every time the curve is updated. public CurvePrefab curvePrefab; public Vector3 rightAxis; public Vector3 upAxis; public Vector3 forwardAxis; public float upSize; public float forwardSize; public OBB spawnOBB; public float scale; public float getUpSize() { return upSize * scale; } public float getForwardSize() { return forwardSize * scale; } public void generateScale(CurvePrefab curvePrefab) { if (curvePrefab.randomizeScale) scale = UnityEngine.Random.Range(curvePrefab.minRandomScale, curvePrefab.maxRandomScale); else scale = 1.0f; } } [Serializable] public class ObjectSpawnCurveLane { [NonSerialized] public List spawnedObjectData = new List(); [NonSerialized] public ObjectSpawnCurveObjectData prevObjectData; // Note: Serialize prefab info to avoid loosing prefabs when switching to playmode and back. [SerializeField] public List usedCurvePrefabs = new List(); [SerializeField] public int nextCurvePrefab; public CurvePrefab pickNextPrefab(CurvePrefabProfile prefabProfile) { CurvePrefab curvePrefab; if (nextCurvePrefab < usedCurvePrefabs.Count) { curvePrefab = usedCurvePrefabs[nextCurvePrefab]; if (curvePrefab == null) { // Note: Replace null entry with a new prefab. curvePrefab = prefabProfile.pickPrefab(); usedCurvePrefabs[nextCurvePrefab] = curvePrefab; } ++nextCurvePrefab; } else { curvePrefab = prefabProfile.pickPrefab(); // Add this prefab to the used prefab sequence ++nextCurvePrefab; usedCurvePrefabs.Add(curvePrefab); } return curvePrefab; } } public class ObjectSpawnCurve : ScriptableObject, IUIItemStateProvider { private class RayHit { public int segmentIndex; public Vector3 pointOnSegment; } private class PrefabData { public GameObject prefabAsset; public CurvePrefab curvePrefab; public OBB obb; public Vector3 upAxis; public Vector3 forwardAxis; public float upSize; public float forwardSize; } [SerializeField] private PluginGuid _guid = new PluginGuid(Guid.NewGuid()); [SerializeField] private string _curveName = string.Empty; [SerializeField] private CurveObjectSpawnSettings _settings; [SerializeField] private List _lanes = new List(); [SerializeField] private List _spawnedObjects = new List(); // Note: We need to store the spawned objects in a separate non-serialized // list in order to be able to handle Undo/Redo properly. [NonSerialized] private List _spawnedObjectsBuffer = new List(); [SerializeField] private PrefabInstancePool _prefabInstancePool; [SerializeField] private CatmullRomSpline3D _spline = new CatmullRomSpline3D(); [NonSerialized] private ObjectSpawnCurveEditMode _editMode = ObjectSpawnCurveEditMode.None; [SerializeField] private List _selectedCtrlPointIndices = new List(); [SerializeField] private ObjectProjectionSettings _terrainProjectionSettings; [NonSerialized] private Quaternion _gizmoRotation = Quaternion.identity; [NonSerialized] private Vector3 _gizmoScale = Vector3.one; [NonSerialized] private bool _draggingMoveGizmo; [NonSerialized] private Vector3 _moveGizmoDragAreaCenter; [NonSerialized] private ObjectBounds.QueryConfig _curvePrefabBoundsQConfig = ObjectBounds.QueryConfig.defaultConfig; [NonSerialized] private ObjectBounds.QueryConfig _spawnedObjectBoundsQConfig = ObjectBounds.QueryConfig.defaultConfig; [NonSerialized] private OBB[] _laneOBBs = new OBB[2]; [NonSerialized] private Vector3[] _laneOffsetAxes = new Vector3[2]; [NonSerialized] private List _samplePoints = new List(); [NonSerialized] private List _curveDrawPoints = new List(); [NonSerialized] private Dictionary _curvePrefabDataMap = new Dictionary(); [NonSerialized] private SceneRaycastFilter _projectionRaycastFilter = new SceneRaycastFilter() { objectTypes = GameObjectType.Mesh | GameObjectType.Terrain | GameObjectType.Sprite, raycastGrid = true, raycastObjects = true }; [NonSerialized] private ObjectOverlapFilter _overlapFilter = new ObjectOverlapFilter(); [NonSerialized] private List _vector3Buffer = new List(); [NonSerialized] private List _overlapIgnoreObjectBuffer = new List(); [NonSerialized] private TerrainCollection _terrains = new TerrainCollection(); [NonSerialized] private TerrainObjectOverlapFilter _terrainOverlapFilter = new TerrainObjectOverlapFilter(); [SerializeField] private bool _uiSelected = false; [NonSerialized] private CopyPasteMode _uiCopyPasteMode = CopyPasteMode.None; private ObjectProjectionSettings terrainProjectionSettings { get { if (_terrainProjectionSettings == null) { _terrainProjectionSettings = CreateInstance(); UndoEx.saveEnabledState(); UndoEx.enabled = false; _terrainProjectionSettings.halfSpace = ObjectProjectionHalfSpace.InFront; _terrainProjectionSettings.embedInSurface = true; _terrainProjectionSettings.projectAsUnit = false; UndoEx.restoreEnabledState(); } return _terrainProjectionSettings; } } public PluginGuid guid { get { return _guid; } } public int numControlPoints { get { return _spline.numControlPoints; } } public int numSegments { get { return _spline.numSegments; } } public int numSampleSegments { get { return _samplePoints.Count - 1; } } public int numSpawnedObjects { get { return _spawnedObjects.Count; } } public int numSelectedControlPoints { get { return _selectedCtrlPointIndices.Count; } } public ObjectSpawnCurveEditMode editMode { get { return _editMode; } set { _editMode = value; } } public CurveObjectSpawnSettings settings { get { return _settings; } } public string curveName { get { return _curveName; } set { if (!string.IsNullOrEmpty(value)) { UndoEx.record(this); _curveName = value; } } } public bool uiSelected { get { return _uiSelected; } set { UndoEx.record(this); _uiSelected = value; } } public CopyPasteMode uiCopyPasteMode { get { return _uiCopyPasteMode; } set { _uiCopyPasteMode = value; } } public ObjectSpawnCurve() { _curvePrefabBoundsQConfig.objectTypes = GameObjectType.All; _spawnedObjectBoundsQConfig.objectTypes = _curvePrefabBoundsQConfig.objectTypes; _overlapFilter.objectTypes = GameObjectType.Mesh | GameObjectType.Sprite; _overlapFilter.customFilter = new Func((GameObject go) => { return !go.isTerrainMesh() && !go.isSphericalMesh(); }); } public static int calcNumSpawnedObjects(List curves) { int numSpawnedObjects = 0; foreach (var curve in curves) numSpawnedObjects += curve.numSpawnedObjects; return numSpawnedObjects; } public GameObject getSpawnedObject(int index) { return _spawnedObjects[index]; } public AABB calcWorldAABB() { if (numControlPoints == 0) return AABB.getInvalid(); int lastCtrlPt = numControlPoints - 2; AABB aabb = new AABB(_spline.getControlPoint(1), Vector3.zero); for (int i = 2; i <= lastCtrlPt; ++i) aabb.enclosePoint(_spline.getControlPoint(i)); return aabb; } public void copy(ObjectSpawnCurve src) { if (this == src) return; UndoEx.saveEnabledState(); UndoEx.enabled = false; settings.copy(src.settings); _spline.copy(src._spline); onControlPointsDirty(); UndoEx.restoreEnabledState(); } public void frame() { if (numControlPoints == 0) return; SceneViewEx.frame(calcWorldAABB().toBounds(), false); } public void ignoreSpawnedObjectsDuringRaycast(SceneRaycastFilter sceneRaycastFilter) { foreach (var lane in _lanes) { foreach (var objectData in lane.spawnedObjectData) sceneRaycastFilter.addIgnoredObject(objectData.gameObject); } } public void refresh(ObjectSpawnCurveRefreshReason refreshReason) { if (refreshReason == ObjectSpawnCurveRefreshReason.UseDefaultSettings || refreshReason == ObjectSpawnCurveRefreshReason.CurvePrefabProfileChanged || refreshReason == ObjectSpawnCurveRefreshReason.CurvePrefabsDeleted) { // Note: We want to generate a new set of prefabs because the curve prefab profile has changed // and a new set of prefabs is now available. foreach (var lane in _lanes) lane.usedCurvePrefabs.Clear(); // Note: Before clearing the prefab instance pool, destroy the spawned game objects. // This will release any pooled objects and allow the pool to destroy all // such objects. destroySpawnedObjectsNoUndoRedo(); _prefabInstancePool.clear(); } else if (refreshReason == ObjectSpawnCurveRefreshReason.Refresh || refreshReason == ObjectSpawnCurveRefreshReason.CurvePrefabUsedStateChanged || refreshReason == ObjectSpawnCurveRefreshReason.CurvePrefabSpawnChanceChanged) { // Note: We want to generate a new set of prefabs. foreach (var lane in _lanes) lane.usedCurvePrefabs.Clear(); } spawnObjects(); } public void clear() { _spline.clear(); _selectedCtrlPointIndices.Clear(); onControlPointsDirty(); } public void selectAllControlPoints() { UndoEx.record(this); // Note: Don't select the first and last control points. // These are the dummy points inserted to make the curve work more intuitively. _selectedCtrlPointIndices.Clear(); for (int i = 1; i < numControlPoints - 1; ++i) _selectedCtrlPointIndices.Add(i); } public void projectSelectedControlPoints() { UndoEx.record(this); _projectionRaycastFilter.clearIgnoredObjects(); ignoreSpawnedObjectsDuringRaycast(_projectionRaycastFilter); var rayHit = PluginScene.instance.raycastClosest(PluginCamera.camera.getCursorRay(), _projectionRaycastFilter, ObjectRaycastConfig.defaultConfig); if (rayHit != null && rayHit.anyHit) { if (rayHit.wasObjectHit && !rayHit.wasGridHit) projectSelectedControlPointsOnObject(rayHit.objectHit); else if (rayHit.wasGridHit && !rayHit.wasObjectHit) projectedSelectedControlPointsOnGrid(rayHit.gridHit); else { if (Mathf.Abs(rayHit.objectHit.hitEnter - rayHit.gridHit.hitEnter) < 1e-5f) projectSelectedControlPointsOnObject(rayHit.objectHit); else { if (rayHit.objectHit.hitEnter < rayHit.gridHit.hitEnter) projectSelectedControlPointsOnObject(rayHit.objectHit); else projectedSelectedControlPointsOnGrid(rayHit.gridHit); } } onControlPointsDirty(); } } public void removeLastPoint() { _spline.removeLastControlPoint(); _selectedCtrlPointIndices.Clear(); onControlPointsDirty(); } public void removeSelectedControlPoints() { if (numControlPoints > 4) { UndoEx.record(this); // Sort the selected indices list to make things easier _selectedCtrlPointIndices.Sort(delegate (int i0, int i1) { return i0.CompareTo(i1); }); // Loop through each selected index for (int i = 0; i < _selectedCtrlPointIndices.Count;) { // Fetch the selected point index and validate it int ptIndex = _selectedCtrlPointIndices[i]; if (ptIndex > 1 && ptIndex < numControlPoints - 2) { // Remove the point _spline.removeControlPoint(ptIndex); _selectedCtrlPointIndices.RemoveAt(i); // Remap the selected indices // Note: This step is made easier by sorting the list beforehand. for (int j = i; j < _selectedCtrlPointIndices.Count; ++j) --_selectedCtrlPointIndices[j]; } else ++i; } onControlPointsDirty(); } } public void addControlPoint(Vector3 pt) { _spline.addControlPoint(pt); onControlPointsDirty(); } public void setControlPoint(int index, Vector3 pt) { _spline.setControlPoint(index, pt); onControlPointsDirty(); } public void setPenultimateControlPoint(Vector3 pt) { int index = _spline.numControlPoints - 2; _spline.setControlPoint(index, pt); onControlPointsDirty(); } public void setLastControlPoint(Vector3 pt) { int index = _spline.numControlPoints - 1; _spline.setControlPoint(index, pt); onControlPointsDirty(); } public void destroySpawnedObjectsNoUndoRedo() { UndoEx.saveEnabledState(); UndoEx.enabled = false; int numObjects = _spawnedObjects.Count; for (int i = 0; i < numObjects; ++i) { var go = _spawnedObjects[i]; if (go != null) { var prefabAsset = go.getOutermostPrefabAsset(); if (prefabAsset != null) _prefabInstancePool.releasePrefabInstance(prefabAsset, go); else GameObject.DestroyImmediate(go); } } _spawnedObjects.Clear(); _spawnedObjectsBuffer.Clear(); foreach (var lane in _lanes) lane.spawnedObjectData.Clear(); UndoEx.restoreEnabledState(); } public bool isControlPointSelected(int ptIndex) { return _selectedCtrlPointIndices.Contains(ptIndex); } public void draw() { if (_editMode != ObjectSpawnCurveEditMode.InsertControlPoints) drawCurve(); if (_editMode == ObjectSpawnCurveEditMode.InsertControlPoints) drawSegments(); drawControlPoints(); if (_editMode == ObjectSpawnCurveEditMode.InsertControlPoints) drawInsertedControlPoint(); if (_editMode == ObjectSpawnCurveEditMode.SelectControlPoints) { var activeGizmoId = ObjectSpawn.instance.curveObjectSpawn.activeGizmoId; if (activeGizmoId == ObjectSpawnCurveGizmoId.Move) drawPositionHandles(); else if (activeGizmoId == ObjectSpawnCurveGizmoId.Rotate) drawRotationHandles(); else if (activeGizmoId == ObjectSpawnCurveGizmoId.Scale) drawScaleHandles(); } Event e = Event.current; if (e.isLeftMouseButtonDownEvent() && GUIUtility.hotControl == 0) { // Note: Not really necessary and it causes control points in other // curves to get deselected when clicking on control point handles. /* UndoEx.record(this); _selectedCtrlPointIndices.Clear();*/ } else if (FixedShortcuts.cancelAction(e)) { if (_editMode == ObjectSpawnCurveEditMode.InsertControlPoints) _editMode = ObjectSpawnCurveEditMode.SelectControlPoints; } } private void drawCurve() { HandlesEx.saveColor(); Handles.color = ObjectSpawnPrefs.instance.curveSpawnCurveColor; float step = Mathf.Lerp(0.3f, 0.05f, ObjectSpawnPrefs.instance.curveSmoothness); _spline.evalPositions(_curveDrawPoints, step); int numPoints = _curveDrawPoints.Count; for (int i = 0; i < numPoints - 1; ++i) Handles.DrawLine(_curveDrawPoints[i], _curveDrawPoints[i + 1]); HandlesEx.restoreColor(); } private void drawControlPoints() { HandlesEx.saveColor(); float tickSize = ObjectSpawnPrefs.instance.curveSpawnTickSize; int numPoints = _spline.numControlPoints; // Note: Don't draw the last dummy point because we don't want to override // the penultimate point (see selectAllControlPoints). // However, draw the first control point because we need to be able to // see something when creating a curve (initially, it will have only 2 // control points - if we start from 1, no points will be rendered). for (int ptIndex = 0; ptIndex < numPoints - 1; ++ptIndex) { Vector3 controlPoint = _spline.getControlPoint(ptIndex); float tickDrawSize = HandleUtility.GetHandleSize(controlPoint) * tickSize; float tickPickSize = tickDrawSize; if (numSelectedControlPoints != 0 && isControlPointSelected(ptIndex)) Handles.color = ObjectSpawnPrefs.instance.curveSpawnSelectedTickColor; else Handles.color = ObjectSpawnPrefs.instance.curveSpawnTickColor; // Note: If this is the last point and edit mode is none, it might mean // we are building the curve. In that case, use Handles.DotHandleCap // because otherwise, camera pan won't work (i.e. Handles.Button // will eat the event because the cursor hovers the button). bool btnPressed = false; if (ptIndex == numPoints - 2 && _editMode == ObjectSpawnCurveEditMode.None) Handles.DotHandleCap(0, controlPoint, Quaternion.identity, tickDrawSize, EventType.Repaint); else btnPressed = Handles.Button(controlPoint, Quaternion.identity, tickDrawSize, tickPickSize, Handles.DotHandleCap); if (btnPressed) { if (_editMode == ObjectSpawnCurveEditMode.SelectControlPoints) { UndoEx.record(this); if (FixedShortcuts.selection_EnableAppend(Event.current)) { if (isControlPointSelected(ptIndex)) _selectedCtrlPointIndices.Remove(ptIndex); else _selectedCtrlPointIndices.Add(ptIndex); } else { _selectedCtrlPointIndices.Clear(); _selectedCtrlPointIndices.Add(ptIndex); } } } } HandlesEx.restoreColor(); } private void drawSegments() { HandlesEx.saveColor(); Handles.color = ObjectSpawnPrefs.instance.curveSpawnSegmentColor; for (int i = 1; i < numSegments - 1; ++i) Handles.DrawLine(_spline.getControlPoint(i), _spline.getControlPoint(i + 1)); HandlesEx.restoreColor(); } private void drawInsertedControlPoint() { RayHit rayHit = raycastSegments(PluginCamera.camera.getCursorRay()); if (rayHit != null) { float tickSize = ObjectSpawnPrefs.instance.curveSpawnTickSize; float tickDrawSize = HandleUtility.GetHandleSize(rayHit.pointOnSegment) * tickSize; Handles.DotHandleCap(0, rayHit.pointOnSegment, Quaternion.identity, tickDrawSize, EventType.Repaint); if (Event.current.isLeftMouseButtonDownEvent()) { UndoEx.record(this); int insertIndex = rayHit.segmentIndex + 1; _spline.insertControlPoint(insertIndex, rayHit.pointOnSegment); _editMode = ObjectSpawnCurveEditMode.SelectControlPoints; onControlPointsDirty(); Event.current.disable(); _selectedCtrlPointIndices.Clear(); _selectedCtrlPointIndices.Add(insertIndex); // Remap selected indices for (int i = insertIndex + 1; i < numSelectedControlPoints; ++i) ++_selectedCtrlPointIndices[i]; } } } private RayHit raycastSegments(Ray ray) { const float segmentThickness = 1.0f; int closestSegmentIndex = -1; float minT = float.MaxValue; for (int segIndex = 1; segIndex < numSegments - 1; ++segIndex) { OBB segOBB = OBB.createFromSegment(_spline.getSegmentStart(segIndex), _spline.getSegmentEnd(segIndex), segmentThickness); float t; if (segOBB.raycast(ray, out t)) { if (t < minT) { minT = t; closestSegmentIndex = segIndex; } } } if (closestSegmentIndex < 0) return null; Vector3 ptOnSegment = Vector3Ex.projectOnSegment(ray.GetPoint(minT), _spline.getSegmentStart(closestSegmentIndex), _spline.getSegmentEnd(closestSegmentIndex)); return new RayHit() { segmentIndex = closestSegmentIndex, pointOnSegment = ptOnSegment }; } private bool notDraggingGizmos() { return Event.current.type != EventType.Used && !Mouse.instance.isButtonDown((int)MouseButton.LeftMouse); } private void drawPositionHandles() { bool ctrlPointsDirty = false; if (numSelectedControlPoints > 0) { int lastSelectedPtIndex = numSelectedControlPoints - 1; EditorGUI.BeginChangeCheck(); Vector3 lastSelectedCtrlPt = _spline.getControlPoint(_selectedCtrlPointIndices[lastSelectedPtIndex]); if (!_draggingMoveGizmo) _moveGizmoDragAreaCenter = lastSelectedCtrlPt; Vector3 newPos = Handles.PositionHandle(lastSelectedCtrlPt, Quaternion.identity); if (EditorGUI.EndChangeCheck()) { _draggingMoveGizmo = true; float maxDragRadius = ObjectSpawnPrefs.instance.curveSpawnMoveGizmoDragRadius; Vector3 toNewPos = newPos - _moveGizmoDragAreaCenter; if (toNewPos.magnitude >= maxDragRadius) newPos = _moveGizmoDragAreaCenter + toNewPos.normalized * maxDragRadius; ctrlPointsDirty = true; UndoEx.record(this); bool snapToGrid = FixedShortcuts.curveObjectSpawn_EnableControlPointSnapToGrid(Event.current); Vector3 moveOffset = newPos - _spline.getControlPoint(_selectedCtrlPointIndices[lastSelectedPtIndex]); foreach (var selectedPtIndex in _selectedCtrlPointIndices) moveSelectedControlPoint(selectedPtIndex, moveOffset, snapToGrid); if (!snapToGrid && numSelectedControlPoints == numControlPoints - 2 && settings.projectionMode == CurveObjectSpawnProjectionMode.None) { ctrlPointsDirty = false; foreach (var go in _spawnedObjects) go.transform.position += moveOffset; } } } if (ctrlPointsDirty) onControlPointsDirty(); if (notDraggingGizmos()) _draggingMoveGizmo = false; } private void drawRotationHandles() { Vector3 ctrlPtCenter = _spline.calcControlPointCenter(true); EditorGUI.BeginChangeCheck(); Quaternion newRotation = Handles.RotationHandle(_gizmoRotation, ctrlPtCenter); if (EditorGUI.EndChangeCheck()) { UndoEx.record(this); Quaternion relativeRotation = QuaternionEx.createRelativeRotation(_gizmoRotation, newRotation); _gizmoRotation = newRotation; int numCtrlPoints = numControlPoints; for (int i = 1; i < numCtrlPoints - 1; ++i) { Vector3 pt = _spline.getControlPoint(i); Vector3 toPt = pt - ctrlPtCenter; toPt = relativeRotation * toPt; _spline.setControlPoint(i, ctrlPtCenter + toPt); syncDummyControlPointIfNecessary(i); } // Note: If no projection is used and no overlap checking is necessary, just // rotate the objects around the center. Otherwise, we need to refresh // the curve because we want to project the objects and avoid overlaps. if (settings.projectionMode == CurveObjectSpawnProjectionMode.None && !settings.avoidOverlaps) { foreach (var go in _spawnedObjects) { Transform objectTransform = go.gameObject.transform; UndoEx.recordTransform(objectTransform); objectTransform.rotateAround(relativeRotation, ctrlPtCenter); } } else onControlPointsDirty(); } if (notDraggingGizmos()) _gizmoRotation = Quaternion.identity; } private void drawScaleHandles() { Vector3 ctrlPtCenter = _spline.calcControlPointCenter(true); EditorGUI.BeginChangeCheck(); Vector3 newScale = Handles.ScaleHandle(_gizmoScale, ctrlPtCenter, Quaternion.identity); if (EditorGUI.EndChangeCheck()) { UndoEx.record(this); Vector3 relativeScale = Vector3.Scale(newScale, _gizmoScale.replaceZero(1.0f).getInverse()); _gizmoScale = newScale; int numCtrlPoints = numControlPoints; for (int i = 1; i < numCtrlPoints - 1; ++i) { Vector3 pt = _spline.getControlPoint(i); Vector3 toPt = pt - ctrlPtCenter; toPt = Vector3.Scale(toPt, relativeScale); _spline.setControlPoint(i, ctrlPtCenter + toPt); syncDummyControlPointIfNecessary(i); } onControlPointsDirty(); } if (notDraggingGizmos()) _gizmoScale = Vector3.one; } private void moveSelectedControlPoint(int selectedPtIndex, Vector3 moveOffset, bool snapToGrid) { Vector3 newPos = _spline.getControlPoint(selectedPtIndex) + moveOffset; if (snapToGrid) newPos = PluginScene.instance.grid.snapAllAxes(newPos); if (selectedPtIndex > 1 && selectedPtIndex < numControlPoints - 2) _spline.setControlPoint(selectedPtIndex, newPos); else { if (selectedPtIndex <= 1) { _spline.setControlPoint(0, newPos); _spline.setControlPoint(1, newPos); } else { _spline.setControlPoint(numControlPoints - 2, newPos); _spline.setControlPoint(numControlPoints - 1, newPos); } } } private void onControlPointsDirty() { spawnObjects(); } private void syncDummyControlPointIfNecessary(int dirtyPointIndex) { if (dirtyPointIndex > 1 && dirtyPointIndex < (numControlPoints - 2)) return; if (dirtyPointIndex == 0) _spline.setControlPoint(1, _spline.getControlPoint(0)); else if (dirtyPointIndex == 1) _spline.setControlPoint(0, _spline.getControlPoint(1)); else if (dirtyPointIndex == numControlPoints - 2) _spline.setControlPoint(numControlPoints - 1, _spline.getControlPoint(numControlPoints - 2)); else if (dirtyPointIndex == numControlPoints - 1) _spline.setControlPoint(numControlPoints - 2, _spline.getControlPoint(numControlPoints - 1)); } private void projectedSelectedControlPointsOnGrid(GridRayHit rayHit) { projectSelectedControlPointsOnPlane(rayHit.hitPlane); } private void projectSelectedControlPointsOnPlane(Plane plane) { int numPts = numSelectedControlPoints; for (int i = 0; i < numPts; ++i) { int ptIndex = _selectedCtrlPointIndices[i]; projectControlPointOnPlane(plane, ptIndex); } } private void projectControlPointOnPlane(Plane plane, int ptIndex) { Vector3 ctrlPt = _spline.getControlPoint(ptIndex); _spline.setControlPoint(ptIndex, plane.projectPoint(ctrlPt)); syncDummyControlPointIfNecessary(ptIndex); } private void projectSelectedControlPointsOnObject(ObjectRayHit rayHit) { GameObject hitObject = rayHit.hitObject; GameObjectType objectType = GameObjectDataDb.instance.getGameObjectType(hitObject); if (objectType == GameObjectType.Terrain) { Terrain terrain = hitObject.getTerrain(); float terrainYPos = terrain.transform.position.y; // Note: When projecting onto a terrain, we must handle the special case where // there are more terrains in the scene arranged in a grid-like manner. // So we will collect all terrain objects that overlap with the curve // and project each point on the terrain in whose area it resides. overlapTerrains(_terrains); int numPts = numSelectedControlPoints; for (int i = 0; i < numPts; ++i) { int ptIndex = _selectedCtrlPointIndices[i]; Vector3 controlPt = _spline.getControlPoint(ptIndex); if (terrain.isWorldPointInsideTerrainArea(controlPt)) projectControlPointOnTerrain(terrain, terrainYPos, ptIndex); else projectControlPointOnTerrains(_terrains, hitObject, ptIndex); } } else if (objectType == GameObjectType.Mesh) { if (hitObject.isTerrainMesh()) { PluginMesh terrainMesh = PluginMeshDb.instance.getPluginMesh(hitObject.getMesh()); // Note: See GameObject.Terrain branch. overlapTerrains(_terrains); int numPts = numSelectedControlPoints; for (int i = 0; i < numPts; ++i) { int ptIndex = _selectedCtrlPointIndices[i]; Vector3 controlPt = _spline.getControlPoint(ptIndex); if (TerrainMeshUtil.isWorldPointInsideTerrainArea(hitObject, controlPt)) projectControlPointOnTerrainMesh(hitObject, terrainMesh, ptIndex); else projectControlPointOnTerrains(_terrains, hitObject, ptIndex); } } else if (hitObject.isSphericalMesh()) { PluginMesh sphericalMesh = PluginMeshDb.instance.getPluginMesh(hitObject.getMesh()); int numPts = numSelectedControlPoints; for (int i = 0; i < numPts; ++i) { int ptIndex = _selectedCtrlPointIndices[i]; projectControlPointOnSphericalMesh(hitObject, sphericalMesh, ptIndex); } } else projectSelectedControlPointsOnPlane(rayHit.hitPlane); } else if (objectType == GameObjectType.Sprite) projectSelectedControlPointsOnPlane(rayHit.hitPlane); } private void projectControlPointOnTerrains(TerrainCollection terrains, GameObject ignoredTerrain, int ptIndex) { Vector3 controlPt = _spline.getControlPoint(ptIndex); bool projected = false; foreach (var terrain in terrains.unityTerrains) { if (terrain.gameObject != ignoredTerrain && terrain.isWorldPointInsideTerrainArea(controlPt)) { projectControlPointOnTerrain(terrain, terrain.transform.position.y, ptIndex); projected = true; break; } } if (!projected) { foreach (var go in terrains.terrainMeshes) { if (TerrainMeshUtil.isWorldPointInsideTerrainArea(go, controlPt)) { projectControlPointOnTerrainMesh(go, PluginMeshDb.instance.getPluginMesh(go.getMesh()), ptIndex); projected = true; break; } } } } private void projectControlPointOnTerrain(Terrain terrain, float terrainYPos, int ptIndex) { Vector3 ctrlPt = _spline.getControlPoint(ptIndex); _spline.setControlPoint(ptIndex, terrain.projectPoint(terrainYPos, ctrlPt)); syncDummyControlPointIfNecessary(ptIndex); } private void projectControlPointOnTerrainMesh(GameObject terrainObject, PluginMesh terrainMesh, int ptIndex) { Vector3 ctrlPt = _spline.getControlPoint(ptIndex); _spline.setControlPoint(ptIndex, TerrainMeshUtil.projectPoint(terrainObject, terrainMesh, ctrlPt)); syncDummyControlPointIfNecessary(ptIndex); } private void projectControlPointOnSphericalMesh(GameObject sphereObject, PluginMesh sphericalMesh, int ptIndex) { Vector3 ctrlPt = _spline.getControlPoint(ptIndex); _spline.setControlPoint(ptIndex, SphericalMeshUtil.projectPoint(sphereObject, sphericalMesh, ctrlPt)); syncDummyControlPointIfNecessary(ptIndex); } private void spawnObjects() { // Destroy the previous bulk of objects and exit if we don't have enough segments destroySpawnedObjectsNoUndoRedo(); if (numSegments < 3) return; // Cache prefab data needed during object spawn fillCurvePrefabDataMap(); // If the prefab data map is empty, it means no prefabs are being used, so we can exit if (_curvePrefabDataMap.Count == 0) return; // Evaluate the sample points that are used to approximate the curve evalSamplePoints(); CurvePrefabProfile curvePrefabProfile = settings.curvePrefabProfile; int sampleSegmentIndex = 0; Vector3 curveUpAxis = getCurveUpAxis(); ObjectSpawnCurvePOI prevPOI = new ObjectSpawnCurvePOI(); prevPOI.pointOnSegment = _samplePoints[0]; prevPOI.sampleSegmentIndex = sampleSegmentIndex; prevPOI.isValid = true; // Note: Disable Undo/Redo because this function will be called // from onUndoRedo and we can't record while Undo/Redo is // being handled. UndoEx.saveEnabledState(); UndoEx.enabled = false; // Clear overlap filter _overlapFilter.clearIgnoredObjects(); // Init lanes updateLaneListCapacity(); foreach (var lane in _lanes) { lane.nextCurvePrefab = 0; lane.prevObjectData = null; if (lane.usedCurvePrefabs.Count == 0) lane.usedCurvePrefabs.Add(curvePrefabProfile.pickPrefab()); } // Cache this here _spawnedObjectBoundsQConfig.volumelessSize = Vector3Ex.create(settings.volumlessObjectSize); CurvePrefab curvePrefab; while (true) { // Pick the next prefab in the main lane. // Note: We are working with the main lane only. Other lanes // will be handled at the end of each iteration. ObjectSpawnCurveLane mainLane = getMainLane(); curvePrefab = mainLane.pickNextPrefab(curvePrefabProfile); // Store data for easy access PrefabData prefabData = _curvePrefabDataMap[curvePrefab]; // Create the spawn data for the object we are about to spawn var objectData = new ObjectSpawnCurveObjectData(); objectData.curvePrefab = curvePrefab; objectData.forwardSize = prefabData.forwardSize; objectData.upSize = prefabData.upSize; objectData.generateScale(curvePrefab); // Generate the length value (prefab size + padding) float length = objectData.getForwardSize(); if (settings.paddingMode == CurveObjectSpawnPaddingMode.Constant) length += settings.padding; else if (settings.paddingMode == CurveObjectSpawnPaddingMode.Random) length += UnityEngine.Random.Range(settings.minRandomPadding, settings.maxRandomPadding); // Find the point on the curve where the prefab's forward extremity should reside objectData.fwPOI = findFowardPOI(prevPOI, length, numSampleSegments, ref sampleSegmentIndex); if (!objectData.fwPOI.isValid) break; // Calculate the OBB that is used to establish the object transform details objectData.forwardAxis = (objectData.fwPOI.pointOnSegment - prevPOI.pointOnSegment).normalized; objectData.rightAxis = calcObjectRightAxis(curveUpAxis, objectData.forwardAxis); objectData.upAxis = calcObjectUpAxis(objectData.rightAxis, objectData.forwardAxis); objectData.spawnOBB = calcObjectSpawnOBB(objectData, mainLane, prefabData, prevPOI); // Note: Apply lane padding to the main lane. But only if its random. When lane padding is Constant, // We want the main lane to always sit in the middle, extend along the curve so that the other lanes // move away from the curve in a symmetrical fashion. if (settings.lanePaddingMode == CurveObjectSpawnLanePaddingMode.Random) { float lanePadding = UnityEngine.Random.Range(settings.minRandomLanePadding, settings.maxRandomLanePadding); objectData.spawnOBB.center = objectData.spawnOBB.center + objectData.rightAxis * lanePadding; } // Try to spawn a game object trySpawnObject(objectData, prefabData, curvePrefabProfile, mainLane); // Regardless of whether an object was spawned or not, spawn object in parallel lanes spawnObjectsInParallelLanes(objectData, curvePrefabProfile); // Update previous POI prevPOI = objectData.fwPOI; } UndoEx.restoreEnabledState(); } private bool trySpawnObject(ObjectSpawnCurveObjectData objectData, PrefabData prefabData, CurvePrefabProfile curvePrefabProfile, ObjectSpawnCurveLane lane) { // Store data for easy access bool isMainLabe = lane == getMainLane(); var curvePrefab = objectData.curvePrefab; var prefabAsset = curvePrefab.prefabAsset; // Ensure no overlap with previous object if (settings.tryFixOverlap && isMainLabe && lane.prevObjectData != null) tryFixOverlap(lane.prevObjectData, objectData); // Calculate object position based on OBB center Vector3 objectScale = Vector3.Scale(Vector3Ex.create(objectData.scale), prefabAsset.transform.lossyScale); Vector3 position = ObjectPositionCalculator.calcRootPosition(prefabAsset, prefabData.obb, objectData.spawnOBB.center, objectScale, objectData.spawnOBB.rotation); // Spawn object lane.prevObjectData = null; // Reset this to null and only set it if an object is spawned. if (settings.objectSkipChance == 0.0f || !Probability.evalChance(settings.objectSkipChance)) { GameObject spawnedObject = _prefabInstancePool.acquirePrefabInstance(curvePrefab.prefabAsset, position, objectData.spawnOBB.rotation, objectScale); objectData.gameObject = spawnedObject; curvePrefab.pluginPrefab.attachInstanceToObjectGroup(spawnedObject); updateObjectTransform(objectData); // Handle overlaps. // Note: We need to handle them here because this is where we have access to the final OBB of the object. if (settings.avoidOverlaps) { if (checkForOverlaps(objectData, lane)) { _prefabInstancePool.releasePrefabInstance(curvePrefab.prefabAsset, spawnedObject); return false; } // Note: Add this spawned hierarchy to the ignored object list. spawnedObject.getAllChildrenAndSelf(false, false, _overlapIgnoreObjectBuffer); _overlapFilter.addIgnoredObjects(_overlapIgnoreObjectBuffer); } lane.spawnedObjectData.Add(objectData); _spawnedObjects.Add(spawnedObject); _spawnedObjectsBuffer.Add(spawnedObject); // Note: Add to secondary list to allow Undo/Redo to work correctly. lane.prevObjectData = objectData; return true; } return false; } private void spawnObjectsInParallelLanes(ObjectSpawnCurveObjectData mainLaneObjectData, CurvePrefabProfile curvePrefabProfile) { _laneOffsetAxes[0] = mainLaneObjectData.rightAxis; _laneOffsetAxes[1] = -_laneOffsetAxes[0]; _laneOBBs[0] = mainLaneObjectData.spawnOBB; _laneOBBs[1] = mainLaneObjectData.spawnOBB; // Generate the lane padding float lanePadding = settings.lanePadding; if (settings.lanePaddingMode == CurveObjectSpawnLanePaddingMode.Random) lanePadding = UnityEngine.Random.Range(settings.minRandomLanePadding, settings.maxRandomLanePadding); // Generate the number of lanes int numLanes = settings.numLanes; if (settings.laneMode == CurveObjectSpawnLaneMode.Random) numLanes = UnityEngine.Random.Range(settings.minRandomNumLanes, settings.maxRandomNumLanes + 1); // Note: Jump over the main lane which is always at index 0. for (int laneIndex = 1; laneIndex < numLanes; ++laneIndex) { int arrayIndex = (laneIndex - 1) % 2; Vector3 rightAxis = _laneOffsetAxes[arrayIndex]; OBB obb = _laneOBBs[arrayIndex]; var lane = _lanes[laneIndex]; var curvePrefab = lane.pickNextPrefab(curvePrefabProfile); var prefabData = _curvePrefabDataMap[curvePrefab]; var objectData = new ObjectSpawnCurveObjectData(); objectData.curvePrefab = curvePrefab; objectData.forwardSize = prefabData.forwardSize; objectData.upSize = prefabData.upSize; objectData.forwardAxis = mainLaneObjectData.forwardAxis; objectData.rightAxis = mainLaneObjectData.rightAxis; objectData.fwPOI = mainLaneObjectData.fwPOI; // Irrelevant in this case but let's go ahead and store it objectData.upAxis = calcObjectUpAxis(objectData.rightAxis, objectData.forwardAxis); objectData.generateScale(curvePrefab); // Note: Pass the forward POI of the main lane spawn data. It's irrelevant since the center // of the OBB will be calculated later. objectData.spawnOBB = calcObjectSpawnOBB(objectData, lane, prefabData, mainLaneObjectData.fwPOI); // Push the OBB in the right position float size0 = Vector3Ex.getSizeAlongAxis(obb.size, obb.rotation, rightAxis); float size1 = Vector3Ex.getSizeAlongAxis(objectData.spawnOBB.size, objectData.spawnOBB.rotation, rightAxis); objectData.spawnOBB.center = obb.center + rightAxis * ((size0 + size1) * 0.5f + lanePadding); trySpawnObject(objectData, prefabData, curvePrefabProfile, lane); _laneOBBs[arrayIndex] = objectData.spawnOBB; } } private void updateLaneListCapacity() { int numLanesNeeded = settings.numLanes; if (settings.laneMode == CurveObjectSpawnLaneMode.Random) numLanesNeeded = settings.maxRandomNumLanes; if (_lanes.Count < numLanesNeeded) { int numLanesToCreate = numLanesNeeded - _lanes.Count; for (int i = 0; i < numLanesToCreate; ++i) _lanes.Add(new ObjectSpawnCurveLane()); } else if (_lanes.Count > numLanesNeeded) { while (_lanes.Count > numLanesNeeded) _lanes.RemoveAt(_lanes.Count - 1); } } private ObjectSpawnCurveLane getMainLane() { return _lanes[0]; } private bool checkForOverlaps(ObjectSpawnCurveObjectData objectData, ObjectSpawnCurveLane lane) { OBB hierarchyWorldOBB = ObjectBounds.calcHierarchyWorldOBB(objectData.gameObject, _spawnedObjectBoundsQConfig); _overlapFilter.ignoredHierarchy = objectData.gameObject; if (PluginScene.instance.overlapBox(hierarchyWorldOBB, _overlapFilter, ObjectOverlapConfig.defaultConfig)) return true; return false; } private void tryFixOverlap(ObjectSpawnCurveObjectData prevObjectData, ObjectSpawnCurveObjectData currentObjectData) { OBB prevOBB = prevObjectData.spawnOBB; OBB currentOBB = currentObjectData.spawnOBB; Box3DFace prevFace = Box3D.findMostAlignedFace(prevOBB.center, prevOBB.size, prevOBB.rotation, prevObjectData.forwardAxis); Box3DFace currentFace = Box3D.findMostAlignedFace(currentOBB.center, currentOBB.size, currentOBB.rotation, -currentObjectData.forwardAxis); Plane prevFacePlane = Box3D.calcFacePlane(prevOBB.center, prevOBB.size, prevOBB.rotation, prevFace); Box3D.calcFaceCorners(currentOBB.center, currentOBB.size, currentOBB.rotation, currentFace, _vector3Buffer); int furthestPtBehind = prevFacePlane.findIndexOfFurthestPointBehind(_vector3Buffer); if (furthestPtBehind >= 0) { Vector3 projectedPt = prevFacePlane.projectPoint(_vector3Buffer[furthestPtBehind]); Vector3 offset = projectedPt - _vector3Buffer[furthestPtBehind]; // Note: When 'tryFixOverlap' and 'avoidOverlaps' are true, it can happen that // the algorithm is stuck in an infinite loop not being able to spawn the next // object. Not sure how this happens exactly, but this fixes it. // Note: No longer needed. Now, tryFixOverlap is only called if the previous object // data is not null (i.e. if the previous position on the curve contains // a game object). //if (Vector3.Dot(prevObjectData.forwardAxis, currentObjectData.forwardAxis) + 1.0f < 1e-5f) { currentObjectData.spawnOBB.center += offset; currentObjectData.fwPOI.pointOnSegment += offset; } } } private void updateObjectTransform(ObjectSpawnCurveObjectData objectData) { CurvePrefab curvePrefab = objectData.curvePrefab; GameObject spawnedObject = objectData.gameObject; // Note: Project before we offset along up axis. if (settings.projectionMode == CurveObjectSpawnProjectionMode.Terrains) projectObjectOnTerrains(objectData); if (curvePrefab.upAxisOffsetMode == CurvePrefabUpAxisOffsetMode.Constant) { if (curvePrefab.upAxisOffset != 0.0f) spawnedObject.transform.position += objectData.upAxis * curvePrefab.upAxisOffset; } else if (curvePrefab.upAxisOffsetMode == CurvePrefabUpAxisOffsetMode.Random) { spawnedObject.transform.position += objectData.upAxis * UnityEngine.Random.Range(curvePrefab.minRandomUpAxisOffset, curvePrefab.maxRandomUpAxisOffset); } } private void projectObjectOnTerrains(ObjectSpawnCurveObjectData objectData) { OBB overlapOBB = objectData.spawnOBB; overlapOBB.size = overlapOBB.size.replace(1, PluginScene.terrainOverlapBoxVerticalSize); PluginScene.instance.terrainsOverlapBox(overlapOBB, _terrainOverlapFilter, TerrainObjectOverlapConfig.defaultConfig, _terrains); terrainProjectionSettings.alignAxis = objectData.curvePrefab.alignUpAxisWhenProjected; terrainProjectionSettings.invertAlignmentAxis = objectData.curvePrefab.invertUpAxis; terrainProjectionSettings.alignmentAxis = objectData.curvePrefab.upAxis; ObjectProjection.projectHierarchyOnTerrains(objectData.gameObject, objectData.spawnOBB, _terrains, terrainProjectionSettings); // Note: Need to do this again if up axis alignment is used, because // the alignment might have canceled the random rotation. if (objectData.curvePrefab.randomizeForwardAxisRotation && objectData.curvePrefab.alignUpAxisWhenProjected) { Quaternion randRotation = Quaternion.AngleAxis(UnityEngine.Random.Range(objectData.curvePrefab.minRandomForwardAxisRotation, objectData.curvePrefab.maxRandomForwardAxisRotation), objectData.forwardAxis); objectData.gameObject.transform.rotation = randRotation * objectData.gameObject.transform.rotation; } } private OBB calcObjectSpawnOBB(ObjectSpawnCurveObjectData objectData, ObjectSpawnCurveLane lane, PrefabData prefabData, ObjectSpawnCurvePOI prevPOI) { bool isMainLane = (lane == getMainLane()); var curvePrefab = objectData.curvePrefab; OBB obb = new OBB(); obb.center = prevPOI.pointOnSegment; obb.center += objectData.upAxis * objectData.getUpSize() * 0.5f; obb.center += objectData.forwardAxis * objectData.getForwardSize() * 0.5f; if (curvePrefab.randomizeScale) obb.size = Vector3.Scale(prefabData.obb.size, Vector3Ex.create(objectData.scale)); else obb.size = prefabData.obb.size; Quaternion rotation = Quaternion.identity; if (curvePrefab.alignAxes) { Quaternion upAlignmentRotation = prefabData.prefabAsset.transform.calcAlignmentRotation(prefabData.upAxis, objectData.upAxis); // Note: When aligning the forward axis, we need to rotate the prefab's forward axis by the previous // rotation in order to simulate a prefab rotation. // Also, we need to handle the case where the prefab's forward axis is at 180 degrees // from the desired forward axis AFTER the up alignment rotation is applied. This is // necessary to handle loops. if (Vector3.Dot(upAlignmentRotation * prefabData.forwardAxis, objectData.forwardAxis) + 1.0f < 1e-5f) { upAlignmentRotation = Quaternion.AngleAxis(180.0f, objectData.upAxis) * upAlignmentRotation; } rotation = prefabData.prefabAsset.transform.calcAlignmentRotation(upAlignmentRotation * prefabData.forwardAxis, objectData.forwardAxis) * upAlignmentRotation; } obb.rotation = rotation; if (curvePrefab.randomizeForwardAxisRotation) { Quaternion randRotation = Quaternion.AngleAxis(UnityEngine.Random.Range(curvePrefab.minRandomForwardAxisRotation, curvePrefab.maxRandomForwardAxisRotation), objectData.forwardAxis); obb.rotation = randRotation * obb.rotation; } if (curvePrefab.randomizeUpAxisRotation) { Quaternion randRotation = Quaternion.AngleAxis(UnityEngine.Random.Range(curvePrefab.minRandomUpAxisRotation, curvePrefab.maxRandomUpAxisRotation), objectData.upAxis); obb.rotation = randRotation * obb.rotation; } if (isMainLane && curvePrefab.jitterMode != CurvePrefabJitterMode.None) { float jitter = curvePrefab.jitter; if (curvePrefab.jitterMode == CurvePrefabJitterMode.Random) jitter = UnityEngine.Random.Range(curvePrefab.minRandomJitter, curvePrefab.maxRandomJitter); Vector3 jitterAxis = objectData.rightAxis; if (Probability.evalChance(0.5f)) jitterAxis = -jitterAxis; obb.center += jitterAxis * jitter; } return obb; } private Vector3 calcObjectRightAxis(Vector3 curveUpAxis, Vector3 forwardAxis) { Vector3 rightAxis = Vector3.Cross(curveUpAxis, forwardAxis); if (rightAxis.magnitude < 1e-5f) { Vector3 newCurveUpAxis; if (settings.curveUpAxis == CurveObjectSpawnUpAxis.Y) newCurveUpAxis = Vector3.forward; else if (settings.curveUpAxis == CurveObjectSpawnUpAxis.Z) newCurveUpAxis = Vector3.right; else newCurveUpAxis = Vector3.up; rightAxis = Vector3.Cross(newCurveUpAxis, forwardAxis).normalized; } return rightAxis; } private Vector3 calcObjectUpAxis(Vector3 rightAxis, Vector3 forwardAxis) { return Vector3.Cross(forwardAxis, rightAxis).normalized; } private ObjectSpawnCurvePOI findFowardPOI(ObjectSpawnCurvePOI prevPOI, float length, int numSampleSegments, ref int segmentIndex) { ObjectSpawnCurvePOI fwPOI = new ObjectSpawnCurvePOI(); fwPOI.isValid = false; while (segmentIndex < numSampleSegments) { fwPOI = findFowardPOI(prevPOI, length, segmentIndex); if (!fwPOI.isValid) ++segmentIndex; else break; } if (segmentIndex == numSampleSegments) return fwPOI; fwPOI.isValid = true; return fwPOI; } private ObjectSpawnCurvePOI findFowardPOI(ObjectSpawnCurvePOI prevFWExtremity, float length, int segmentIndex) { ObjectSpawnCurvePOI poi = new ObjectSpawnCurvePOI(); poi.isValid = false; poi.sampleSegmentIndex = segmentIndex; Vector3 p0; if (prevFWExtremity.sampleSegmentIndex == segmentIndex) p0 = prevFWExtremity.pointOnSegment; else p0 = _samplePoints[segmentIndex]; Vector3 p1 = _samplePoints[segmentIndex + 1]; if (!Vector3Ex.calcPointOnSegment(prevFWExtremity.pointOnSegment, p0, p1, length, out poi.pointOnSegment)) return poi; poi.isValid = true; return poi; } private void fillCurvePrefabDataMap() { _curvePrefabDataMap.Clear(); _curvePrefabBoundsQConfig.volumelessSize = Vector3Ex.create(settings.volumlessObjectSize); // Loop through each prefab int numCurvePrefabs = settings.curvePrefabProfile.numPrefabs; for (int i = 0; i < numCurvePrefabs; ++i) { // Retrieve the current prefab and calculate its data. // Note: Ignore the prefab if it's not being used. CurvePrefab curvePrefab = settings.curvePrefabProfile.getPrefab(i); if (curvePrefab.used) { // Create a new prefab data record PrefabData prefabData = new PrefabData(); prefabData.prefabAsset = curvePrefab.prefabAsset; prefabData.curvePrefab = curvePrefab; _curvePrefabDataMap.Add(curvePrefab, prefabData); // Calculate the prefab OBB prefabData.obb = ObjectBounds.calcHierarchyWorldOBB(curvePrefab.prefabAsset, _curvePrefabBoundsQConfig); // Extract info about the prefab's up axis GameObject prefabAsset = curvePrefab.prefabAsset; AxisDescriptor upAxisDesc = prefabAsset.transform.flexiToLocalAxisDesc(prefabData.obb, curvePrefab.upAxis, curvePrefab.invertUpAxis); prefabData.upSize = prefabData.obb.size[upAxisDesc.index]; prefabData.upAxis = prefabAsset.transform.getLocalAxis(upAxisDesc); // Extract info about the prefab's forward axis AxisDescriptor forwardAxisDesc = prefabAsset.transform.flexiToLocalAxisDesc(prefabData.obb, curvePrefab.forwardAxis, curvePrefab.invertForwardAxis); prefabData.forwardSize = prefabData.obb.size[forwardAxisDesc.index]; prefabData.forwardAxis = prefabAsset.transform.getLocalAxis(forwardAxisDesc); } } } private Vector3 getCurveUpAxis() { if (settings.curveUpAxis == CurveObjectSpawnUpAxis.Y) return settings.invertUpAxis ? Vector3.down : Vector3.up; if (settings.curveUpAxis == CurveObjectSpawnUpAxis.Z) return settings.invertUpAxis ? Vector3.back : Vector3.forward; return settings.invertUpAxis ? Vector3.left : Vector3.right; } private void evalSamplePoints() { _samplePoints.Clear(); _spline.evalPositions(_samplePoints, settings.step); } private void overlapTerrains(TerrainCollection terrains) { terrains.clear(); // Note: Use a large enough value along the Y axis for the size. OBB overlapOBB = new OBB(calcWorldAABB()); overlapOBB.size = overlapOBB.size.replace(1, PluginScene.terrainOverlapBoxVerticalSize); PluginScene.instance.terrainsOverlapBox(overlapOBB, _terrainOverlapFilter, TerrainObjectOverlapConfig.defaultConfig, terrains); } private void OnEnable() { if (_settings == null) _settings = ScriptableObject.CreateInstance(); if (_prefabInstancePool == null) _prefabInstancePool = ScriptableObject.CreateInstance(); else _prefabInstancePool.hidePrefabInstancesInInspector(); Undo.undoRedoPerformed += onUndoRedo; EditorApplication.playModeStateChanged += onPlayModeStateChanged; _spawnedObjectsBuffer.Clear(); foreach (var go in _spawnedObjects) _spawnedObjectsBuffer.Add(go); // Init lanes for (int i = 0; i < 1; ++i) _lanes.Add(new ObjectSpawnCurveLane()); // Note: Always start out in control point selection mode. _editMode = ObjectSpawnCurveEditMode.SelectControlPoints; } private void OnDisable() { Undo.undoRedoPerformed -= onUndoRedo; EditorApplication.playModeStateChanged -= onPlayModeStateChanged; } private void onPlayModeStateChanged(PlayModeStateChange stateChange) { _prefabInstancePool.hidePrefabInstancesInInspector(); } private void OnDestroy() { if (_settings != null) UndoEx.destroyObjectImmediate(_settings); if (_terrainProjectionSettings != null) UndoEx.destroyObjectImmediate(_terrainProjectionSettings); if (_prefabInstancePool != null) { // Note: Need to clear instance pool before destruction. Otherwise, Undo/Redo won't work. _prefabInstancePool.clear(); UndoEx.destroyObjectImmediate(_prefabInstancePool); } } // Note: This function has to be like this in order for Undo/Redo to work. private void onUndoRedo() { if (uiSelected && GSpawn.active.levelDesignToolId == LevelDesignToolId.ObjectSpawn && ObjectSpawn.instance.activeToolId == ObjectSpawnToolId.Curve && GSpawn.isActiveSelected) { // Note: Destroy previous objects. This works because the secondary object list is not serialized and // the old objects are still there. foreach (var go in _spawnedObjectsBuffer) { if (go != null) GameObject.DestroyImmediate(go); } _spawnedObjects.Clear(); _spawnedObjectsBuffer.Clear(); foreach (var lane in _lanes) lane.spawnedObjectData.Clear(); refresh(ObjectSpawnCurveRefreshReason.Other); } } } } #endif