using System; using UnityEngine; using System.Collections.Generic; #if UNITY_EDITOR using UnityEditor; #endif namespace Pegasus { /// /// Spawn a bunch of special objects in a scene, and the flythrough manager will find and visit them /// /// public class PegasusManager : MonoBehaviour { /// /// The target game object we will be driving - or main scene camera if nothing selected /// public GameObject m_target; /// /// Default flythrough type /// public PegasusConstants.FlythroughType m_flythroughType = PegasusConstants.FlythroughType.Looped; /// /// What to do when we fet to the end - only relevant on Once through flythroughs /// public PegasusConstants.FlythroughEndAction m_flythroughEndAction = PegasusConstants.FlythroughEndAction.StopFlythrough; /// /// Select the targetted frame rate for the fly through /// public PegasusConstants.TargetFrameRate m_targetFramerateType = PegasusConstants.TargetFrameRate.LeaveAlone; /// /// The algorithm the system uses to check minimum heights against /// public PegasusConstants.HeightCheckType m_heightCheckType = PegasusConstants.HeightCheckType.Terrain; /// /// Autostart the manager at runtime /// public bool m_autoStartAtRuntime = true; /// /// The POI list /// public List m_poiList = new List(); /// /// The minimum height above the terrain /// public float m_minHeightAboveTerrain = PegasusConstants.FlybyOffsetDefaultHeight; /// /// Display debug messages /// public bool m_displayDebug = false; //Set to true if we want debug messages /// /// Always show gizmos /// public bool m_alwaysShowGizmos = true; public PegasusConstants.FlythroughState m_currentState = PegasusConstants.FlythroughState.Stopped; public int m_currentSegmentIdx; public PegasusPoi m_currentSegment; public float m_currentSegmentDistanceTravelled = 0f; public float m_totalDistanceTravelled = 0f; public float m_totalDistanceTravelledPct = 0f; public float m_totalDistance = 0f; public TimeSpan m_totalDuration = TimeSpan.Zero; public float m_currentVelocity = 0f; public Vector3 m_currentPosition = Vector3.zero; public Quaternion m_currentRotation = Quaternion.identity; public bool m_canUpdateNow = false; //Used to trigger when the camera and targetLocation can be updated public DateTime m_lastUpdateTime = DateTime.MinValue; public float m_frameUpdateTime = 1f / 60f; public float m_frameUpdateDistance = 0f; public float m_rotationDamping = 0.75f; public float m_positionDamping = 0.3f; public PegasusManager m_nextPegasus = null; //Used when single shot is assigned and end action is play next pegasus //Editor related public bool m_alwaysShowPath = false; public bool m_showScrubber = false; public bool m_showPOIHelpers = false; public float m_poiGizmoSize = 0.75f; public bool m_showAdvanced = false; public float m_collisionHeightOffset = 1000f; //How far above a location to check for collisions public float m_managerSpeed = PegasusConstants.SpeedMedium; //Defaults public PegasusDefaults m_defaults = null; //Autobank settings public float m_autoRollMaxSpeed = PegasusConstants.SpeedFast; public float m_autoRollMaxAngle = 15f; /// /// Scene playback and initialisation /// void Start() { //Make sure we are stopped m_currentState = PegasusConstants.FlythroughState.Stopped; //Grab the main camera if nothing defined if (m_target == null) { if (Camera.main == null) { Debug.LogWarning("Can not start Pegasus - no target has been assigned."); return; } else { if (m_displayDebug == true) { Debug.Log("Assigning main camera to target : " + Camera.main.name); } m_target = Camera.main.gameObject; } } //Grab the defaults if none have been defined if (m_defaults == null) { SetDefaults(); } //Set the applications target framerate ChangeFramerate(m_targetFramerateType); //Initialise the flythrough - does all the expensive calcs in one hit InitialiseFlythrough(); //Auto start at runtime if necessary if (m_autoStartAtRuntime == true) { StartFlythrough(); } } /// /// Get or create the pegasus defaults - allows people to override keys /// /// public void SetDefaults() { m_defaults = null; #if UNITY_EDITOR string[] guids = AssetDatabase.FindAssets("PegasusDefaults"); for (int idx = 0; idx < guids.Length; idx++) { string path = AssetDatabase.GUIDToAssetPath(guids[idx]); if (path.Contains("PegasusDefaults.asset")) { m_defaults = AssetDatabase.LoadAssetAtPath(path); return; } } if (m_defaults == null) { m_defaults = ScriptableObject.CreateInstance(); AssetDatabase.CreateAsset(m_defaults, "Assets/PegasusDefaults.asset"); AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } #else m_defaults = ScriptableObject.CreateInstance(); #endif } #region Setup and control routines /// /// Initialise a flythrough - setup all the segments /// public void InitialiseFlythrough() { if (m_displayDebug) { Debug.Log("Initialising flythrough..."); } m_currentState = PegasusConstants.FlythroughState.Initialising; //Rebuild the POI list based on order of poi children m_poiList.Clear(); int idx; for (idx = 0; idx < transform.childCount; idx++) { m_poiList.Add(transform.GetChild(idx).GetComponent()); } //Iterate thru the list and set first, last, prev next on the poi PegasusPoi poi = null; for (idx = 0; idx < m_poiList.Count; idx++) { poi = m_poiList[idx]; //Update the index poi.m_segmentIndex = idx; //Set first / last flags if (idx == 0) { poi.m_isFirstPOI = true; } else { poi.m_isFirstPOI = false; } if (idx == m_poiList.Count - 1) { poi.m_isLastPOI = true; } else { poi.m_isLastPOI = false; } //Set prev / next pointers if (m_flythroughType == PegasusConstants.FlythroughType.SingleShot) { //Previous if (poi.m_isFirstPOI) { if (m_poiList.Count > 1) { poi.m_prevPoi = m_poiList[1]; } else { poi.m_prevPoi = poi; } } else { poi.m_prevPoi = m_poiList[idx - 1]; } //Next if (poi.m_isLastPOI) { if (m_poiList.Count > 1) { poi.m_nextPoi = m_poiList[idx - 1]; } else { poi.m_nextPoi = poi; } } else { poi.m_nextPoi = m_poiList[idx + 1]; } } else { //Previous if (idx == 0) { poi.m_prevPoi = m_poiList[m_poiList.Count - 1]; } else { poi.m_prevPoi = m_poiList[idx - 1]; } //Next if (idx == m_poiList.Count - 1) { poi.m_nextPoi = m_poiList[0]; } else { poi.m_nextPoi = m_poiList[idx + 1]; } } poi.m_alwaysShowGizmos = m_alwaysShowGizmos; } //Get the poi to initialise themselves - run twice as some settings depend on the next in the sequence which may not yet be initialised for (idx = 0; idx < m_poiList.Count; idx++) { m_poiList[idx].Initialise(false); } //Next time thru grab the total distance and duration m_totalDuration = TimeSpan.Zero; m_totalDistanceTravelledPct = 0f; m_totalDistanceTravelled = 0f; m_totalDistance = 0f; for (idx = 0; idx < m_poiList.Count; idx++) { m_poiList[idx].Initialise(true); m_poiList[idx].m_segmentStartTime = new TimeSpan(m_totalDuration.Ticks); m_totalDistance += m_poiList[idx].m_segmentDistance; m_totalDuration += m_poiList[idx].m_segmentDuration; } //Set up the initial state m_currentSegmentIdx = 0; if (m_poiList.Count > 0) { m_currentSegment = m_poiList[m_currentSegmentIdx]; } else { m_currentSegment = null; } m_currentSegmentDistanceTravelled = 0f; //Current time m_lastUpdateTime = DateTime.Now; //Signal that we can update m_canUpdateNow = true; } /// /// Restart the flythrough /// private void RestartFlythrough() { if (m_displayDebug) { Debug.Log("Restarting flythrough..."); } m_currentState = PegasusConstants.FlythroughState.Initialising; m_totalDistanceTravelledPct = 0f; m_totalDistanceTravelled = 0f; //Set up the initial state m_currentSegmentIdx = 0; if (m_poiList.Count > 0) { m_currentSegment = m_poiList[m_currentSegmentIdx]; } else { m_currentSegment = null; } m_currentSegmentDistanceTravelled = 0f; //Current time m_lastUpdateTime = DateTime.Now; //Signal that we can update m_canUpdateNow = true; } /// /// Update the meta data influenced by the connectivity of the flythrough - distance and duration. /// Assumes that the segments have previously been initialised so that distance and durations are correct. /// public void UpdateFlythroughMetaData() { m_totalDuration = TimeSpan.Zero; m_totalDistance = 0f; for (int idx = 0; idx < m_poiList.Count; idx++) { m_poiList[idx].m_segmentStartTime = new TimeSpan(m_totalDuration.Ticks); m_totalDuration += m_poiList[idx].m_segmentDuration; m_totalDistance += m_poiList[idx].m_segmentDistance; } } /// /// Update the dependencies that will change when changes are made to an individual segment as /// they flow back 2, and forward 1 segments due to the underlying spline system. /// /// public void UpdateSegmentWithDependencies(PegasusPoi segment) { if (segment == null) { Debug.LogError("Attempting to update null segment!"); return; } if (m_flythroughType == PegasusConstants.FlythroughType.SingleShot) { int idx = segment.m_segmentIndex; SafeInitialise(idx - 2, false, true); SafeInitialise(idx - 1, false, true); SafeInitialise(idx, false, true); SafeInitialise(idx + 1, false, true); } else { int idx = segment.m_segmentIndex; SafeInitialise(idx - 2, true, true); SafeInitialise(idx - 1, true, true); SafeInitialise(idx, true, true); SafeInitialise(idx + 1, true, true); } //Then do a full flythrough metadata update UpdateFlythroughMetaData(); } /// /// Run a safe initialise - bury out of range and handle wrapping /// /// /// /// private void SafeInitialise(int idx, bool wrap, bool updateSegments) { if (!wrap) { if (idx >= 0 && idx < m_poiList.Count) { m_poiList[idx].Initialise(updateSegments); } } else { idx = idx % m_poiList.Count; if (idx < 0) { idx += m_poiList.Count; } m_poiList[idx].Initialise(updateSegments); } } #endregion #region Public API routines /// /// Start a flythrough from scratch /// public void StartFlythrough(bool fullInitialise = false) { //Enable the application to be able to run in the background Application.runInBackground = true; //Initialise it if (fullInitialise == true) { InitialiseFlythrough(); } else { RestartFlythrough(); } //Now start the flythough if (m_displayDebug) { Debug.Log("Starting flythrough.."); } //Move target to its initial position and location if (m_target != null) { m_currentSegment.CalculateProgress(0, out m_currentVelocity, out m_currentPosition, out m_currentRotation); m_target.transform.rotation = m_currentRotation; m_target.transform.position = m_currentPosition; } else { Debug.LogWarning("Cannot start Pegasus - no target has been assigned!"); m_currentState = PegasusConstants.FlythroughState.Stopped; return; } //Kick off triggers m_currentSegment.OnStartTriggers(); //Mark as started m_currentState = PegasusConstants.FlythroughState.Started; } /// /// Resume a flythrough /// public void ResumeFlythrough() { if (m_displayDebug) { Debug.Log("Resuming flythrough"); } if (m_currentState != PegasusConstants.FlythroughState.Paused) { Debug.LogWarning("Can not resume flythrough - it was not paused."); return; } //Mark as started m_currentState = PegasusConstants.FlythroughState.Started; } /// /// Pause a flythrough - can be resumed /// public void PauseFlythrough() { if (m_displayDebug) { Debug.Log("Pausing flythrough"); } m_currentState = PegasusConstants.FlythroughState.Paused; } /// /// Stop a flythrough - cant be resumed - can be restarted /// public void StopFlythrough() { if (m_displayDebug) { Debug.Log("Stopping flythrough"); } m_currentState = PegasusConstants.FlythroughState.Stopped; m_canUpdateNow = false; } /// /// Change the frame rate and apply it immediately /// /// New framerate public void ChangeFramerate(PegasusConstants.TargetFrameRate newRate) { m_targetFramerateType = newRate; if (m_targetFramerateType == PegasusConstants.TargetFrameRate.LeaveAlone) { return; } //Apply the target framerate only if we are playing if (Application.isPlaying) { switch (m_targetFramerateType) { case PegasusConstants.TargetFrameRate.NineFps: Application.targetFrameRate = 9; #if HELIOS3D Time.captureFramerate = 9; #endif m_frameUpdateTime = 1f / 9f; break; case PegasusConstants.TargetFrameRate.FifteenFps: Application.targetFrameRate = 15; #if HELIOS3D Time.captureFramerate = 15; #endif m_frameUpdateTime = 1f / 15f; break; case PegasusConstants.TargetFrameRate.TwentyFourFps: Application.targetFrameRate = 24; #if HELIOS3D Time.captureFramerate = 24; #endif m_frameUpdateTime = 1f / 24f; break; case PegasusConstants.TargetFrameRate.TwentyFiveFps: Application.targetFrameRate = 25; #if HELIOS3D Time.captureFramerate = 25; #endif m_frameUpdateTime = 1f / 25f; break; case PegasusConstants.TargetFrameRate.ThirtyFps: Application.targetFrameRate = 30; #if HELIOS3D Time.captureFramerate = 30; #endif m_frameUpdateTime = 1f / 30f; break; case PegasusConstants.TargetFrameRate.SixtyFps: Application.targetFrameRate = 60; #if HELIOS3D Time.captureFramerate = 60; #endif m_frameUpdateTime = 1f / 60f; break; case PegasusConstants.TargetFrameRate.NinetyFps: Application.targetFrameRate = 90; #if HELIOS3D Time.captureFramerate = 90; #endif m_frameUpdateTime = 1f / 90f; break; case PegasusConstants.TargetFrameRate.MaxFps: Application.targetFrameRate = -1; #if HELIOS3D Time.captureFramerate = 60; #endif m_frameUpdateTime = 0f; break; } } } /// /// Set the speed of the fly through for every node /// /// public void SetSpeed(float speed) { for (int idx = 0; idx < m_poiList.Count; idx++) { m_poiList[idx].m_startSpeed = speed; m_poiList[idx].m_startSpeedType = PegasusConstants.SpeedType.Custom; } InitialiseFlythrough(); } /// /// Update state of auto roll on all POI /// /// public void SetAutoRoll(bool autoRoll) { for (int idx = 0; idx < m_poiList.Count; idx++) { m_poiList[idx].m_autoRollOn = autoRoll; if (!autoRoll) { m_poiList[idx].transform.localEulerAngles = Vector3.zero; } } InitialiseFlythrough(); } #endregion #region Editor specific routines - use at own peril /// /// Select the POI - influences gizmo rendering /// /// public void SelectPoi(PegasusPoi poi) { for (int idx = 0; idx < m_poiList.Count; idx++) { m_poiList[idx].m_isSelected = false; } if (poi != null) { poi.m_isSelected = true; } } /// /// Do a validated poi movement /// /// Point of interest to move /// Amount of movement public void MovePoi(PegasusPoi poi, Vector3 movement) { poi.transform.position = GetValidatedPoiPosition(poi.transform.position + movement, poi.m_heightCheckType); poi.GetRelativeOffsets(poi.transform.position, poi.m_lookatLocation, out poi.m_lookAtDistance, out poi.m_lookAtHeight, out poi.m_lookAtAngle); UpdateSegmentWithDependencies(poi); } /// /// Do a validated poi lookat movement - including setting it into target mode /// /// public void MovePoiLookat(PegasusPoi poi, Vector3 movement) { poi.m_lookatType = PegasusConstants.LookatType.Target; Vector3 lookAtLocation = GetValidatedLookatPosition(poi.m_lookatLocation + movement, poi.m_heightCheckType); if (lookAtLocation != poi.m_lookatLocation) { poi.m_lookatLocation = lookAtLocation; poi.m_lookatType = PegasusConstants.LookatType.Target; poi.GetRelativeOffsets(poi.transform.position, poi.m_lookatLocation, out poi.m_lookAtDistance, out poi.m_lookAtHeight, out poi.m_lookAtAngle); UpdateSegmentWithDependencies(poi); } } /// /// Move target to current location /// public void MoveTargetNow() { MoveTargetTo(m_totalDistanceTravelled / m_totalDistance); } /// /// Move the target if one has been set to the point in the sequence selected - designed to be an editor only method /// /// Value betrween 0..1 which represents completion public void MoveTargetTo(float percent) { if (m_target == null) { Debug.LogWarning("Can not move target as none has been set"); return; } PegasusPoi poi; float targetPoint = percent * m_totalDistance; float segmentStart = 0f; float segmentEnd = 0f; for (int idx = 0; idx < m_poiList.Count; idx++) { poi = m_poiList[idx]; segmentEnd = segmentStart + poi.m_segmentDistance; if (targetPoint >= segmentStart && targetPoint <= segmentEnd) { m_totalDistanceTravelled = targetPoint; m_totalDistanceTravelledPct = m_totalDistanceTravelled / m_totalDistance; m_currentPosition = poi.CalculatePositionLinear((targetPoint - segmentStart) / poi.m_segmentDistance); m_currentRotation = poi.CalculateRotation((targetPoint - segmentStart) / poi.m_segmentDistance); m_currentVelocity = poi.CalculateVelocity((targetPoint - segmentStart) / poi.m_segmentDistance); m_target.transform.position = m_currentPosition; m_target.transform.rotation = m_currentRotation; return; } segmentStart += poi.m_segmentDistance; } } /// /// Calculate target data based on distance through the flythrough /// /// Distance through flythrough /// Calculated position /// Calculated rotation /// Calculated velocity public void CalculateTargetAtDistance(float distance, out Vector3 position, out Quaternion rotation, out float velocity) { if (m_target == null || m_poiList.Count == 0) { position = Vector3.zero; rotation = Quaternion.identity; velocity = 0f; return; } PegasusPoi poi; float targetPoint = distance; if (m_flythroughType == PegasusConstants.FlythroughType.SingleShot) { //Correct for invalid distances targetPoint = Mathf.Clamp(distance, 0f, m_totalDistance); } else { //Correct for looping targetPoint = targetPoint % m_totalDistance; if (targetPoint < 0f) { targetPoint += m_totalDistance; } } float segmentStart = 0f; float segmentEnd = 0f; for (int idx = 0; idx < m_poiList.Count; idx++) { poi = m_poiList[idx]; segmentEnd = segmentStart + poi.m_segmentDistance; if (targetPoint >= segmentStart && targetPoint <= segmentEnd) { position = poi.CalculatePositionLinear((targetPoint - segmentStart) / poi.m_segmentDistance); rotation = poi.CalculateRotation((targetPoint - segmentStart) / poi.m_segmentDistance); velocity = poi.CalculateVelocity((targetPoint - segmentStart) / poi.m_segmentDistance); return; } segmentStart += poi.m_segmentDistance; } position = m_poiList[m_poiList.Count - 1].transform.position; rotation = m_poiList[m_poiList.Count - 1].transform.localRotation; velocity = m_poiList[m_poiList.Count - 1].m_startSpeed; } /// /// Calculate target data based at the percentage movement through the flythrough /// /// /// /// /// public void CalculateTargetAtPercent(float percent, out Vector3 position, out Quaternion rotation, out float velocity) { float targetPoint = Mathf.Clamp(percent, 0f, 100f) * m_totalDistance; CalculateTargetAtDistance(targetPoint, out position, out rotation, out velocity); } /// /// Move target to the given poi /// /// Poi to move target to public void MoveTargetToPoi(PegasusPoi targetPoi) { if (m_target == null) { Debug.LogWarning("Can not move target as none has been set"); return; } m_totalDistanceTravelled = 0f; for (int idx = 0; idx < m_poiList.Count; idx++) { if (m_poiList[idx].GetInstanceID() == targetPoi.GetInstanceID()) { m_totalDistanceTravelledPct = m_totalDistanceTravelled / m_totalDistance; m_currentPosition = targetPoi.CalculatePositionLinear(0f); m_currentRotation = targetPoi.CalculateRotation(0f); m_currentVelocity = targetPoi.CalculateVelocity(0f); m_target.transform.position = m_currentPosition; m_target.transform.rotation = m_currentRotation; } else { m_totalDistanceTravelled += m_poiList[idx].m_segmentDistance; } } } /// /// Step the target 1m backward - only called by editor / /scrubber functions /// public void StepTargetBackward(float distMeters) { m_totalDistanceTravelled -= distMeters; if (m_totalDistanceTravelled < 0) { m_totalDistanceTravelled = 0f; } MoveTargetTo(m_totalDistanceTravelled / m_totalDistance); } /// /// Step the target 1m forward - only called by editor / /scrubber functions /// public void StepTargetForward(float distMeters) { m_totalDistanceTravelled += distMeters; if (m_totalDistanceTravelled > m_totalDistance) { m_totalDistanceTravelled = m_totalDistance; } MoveTargetTo(m_totalDistanceTravelled / m_totalDistance); } /// /// Create debug objects for visualisation /// public void CreateDebugObjects() { PegasusPoi poi; for (int idx = 0; idx < m_poiList.Count; idx++) { poi = m_poiList[idx]; if (poi.transform.childCount == 0) { GameObject newCube = GameObject.CreatePrimitive(PrimitiveType.Cube); DestroyImmediate(newCube.GetComponent()); newCube.transform.position = poi.transform.position; newCube.transform.localScale = new Vector3(0.05f, 10f, 0.05f); newCube.transform.parent = poi.transform; newCube = GameObject.CreatePrimitive(PrimitiveType.Cube); DestroyImmediate(newCube.GetComponent()); newCube.transform.position = poi.transform.position; newCube.transform.localScale = new Vector3(0.05f, 0.05f, 5f); newCube.transform.parent = poi.transform; newCube = GameObject.CreatePrimitive(PrimitiveType.Cube); DestroyImmediate(newCube.GetComponent()); newCube.transform.position = poi.transform.position; newCube.transform.localScale = new Vector3(5f, 0.05f, 0.05f); newCube.transform.parent = poi.transform; } } } /// /// Delete visualisation debug objects /// public void DeleteDebugObjects() { PegasusPoi poi; for (int idx = 0; idx < m_poiList.Count; idx++) { poi = m_poiList[idx]; while (poi.transform.childCount > 0) { DestroyImmediate(poi.transform.GetChild(0).gameObject); } } } /// /// Return a validated / corrected poi position /// /// Source location being checked /// A height check overide /// Updated position taking into account minimum height public Vector3 GetValidatedPoiPosition(Vector3 source, PegasusConstants.PoiHeightCheckType heightCheckOverride = PegasusConstants.PoiHeightCheckType.ManagerSettings) { //Update if there is an override PegasusConstants.HeightCheckType heightCheckType = m_heightCheckType; if (heightCheckOverride != PegasusConstants.PoiHeightCheckType.ManagerSettings) { if (heightCheckOverride == PegasusConstants.PoiHeightCheckType.Collision) { heightCheckType = PegasusConstants.HeightCheckType.Collision; } else if (heightCheckOverride == PegasusConstants.PoiHeightCheckType.Terrain) { heightCheckType = PegasusConstants.HeightCheckType.Terrain; } else { heightCheckType = PegasusConstants.HeightCheckType.None; } } //Peform the height check if (heightCheckType == PegasusConstants.HeightCheckType.None) { return source; } else if (heightCheckType == PegasusConstants.HeightCheckType.Collision) { RaycastHit hit; if (Physics.Raycast(new Vector3(source.x, source.y + m_collisionHeightOffset, source.z), Vector3.down, out hit, 10000f)) { if ((hit.point.y + m_minHeightAboveTerrain) > source.y) { source.y = hit.point.y + m_minHeightAboveTerrain; } } return source; } else { Terrain t = GetTerrain(source); if (t != null) { float height = t.SampleHeight(source); if ((height + m_minHeightAboveTerrain) > source.y) { source.y = height + m_minHeightAboveTerrain; } } return source; } } /// /// Return a validated / poi position at the minimum height /// /// Source location being checked /// A height check overide /// Updated position taking into account minimum height public Vector3 GetLowestPoiPosition(Vector3 source, PegasusConstants.PoiHeightCheckType heightCheckOverride = PegasusConstants.PoiHeightCheckType.ManagerSettings) { //Update if there is an override PegasusConstants.HeightCheckType heightCheckType = m_heightCheckType; if (heightCheckOverride != PegasusConstants.PoiHeightCheckType.ManagerSettings) { if (heightCheckOverride == PegasusConstants.PoiHeightCheckType.Collision) { heightCheckType = PegasusConstants.HeightCheckType.Collision; } else if (heightCheckOverride == PegasusConstants.PoiHeightCheckType.Terrain) { heightCheckType = PegasusConstants.HeightCheckType.Terrain; } else { heightCheckType = PegasusConstants.HeightCheckType.None; } } //Peform the height check if (heightCheckType == PegasusConstants.HeightCheckType.None) { return source; } else if (heightCheckType == PegasusConstants.HeightCheckType.Collision) { RaycastHit hit; if (Physics.Raycast(new Vector3(source.x, source.y + m_collisionHeightOffset, source.z), Vector3.down, out hit, 10000f)) { source.y = hit.point.y + m_minHeightAboveTerrain; } return source; } else { Terrain t = GetTerrain(source); if (t != null) { source.y = t.SampleHeight(source) + m_minHeightAboveTerrain; } return source; } } /// /// Return a validated / corrected lookat position /// /// Source location /// Updated position taking into account minimum height public Vector3 GetValidatedLookatPosition(Vector3 source, PegasusConstants.PoiHeightCheckType heightCheckOverride = PegasusConstants.PoiHeightCheckType.ManagerSettings) { //Update if there is an override PegasusConstants.HeightCheckType heightCheckType = m_heightCheckType; if (heightCheckOverride != PegasusConstants.PoiHeightCheckType.ManagerSettings) { if (heightCheckOverride == PegasusConstants.PoiHeightCheckType.Collision) { heightCheckType = PegasusConstants.HeightCheckType.Collision; } else if (heightCheckOverride == PegasusConstants.PoiHeightCheckType.Terrain) { heightCheckType = PegasusConstants.HeightCheckType.Terrain; } else { heightCheckType = PegasusConstants.HeightCheckType.None; } } //Perform the height check if (heightCheckType == PegasusConstants.HeightCheckType.None) { return source; } else if (heightCheckType == PegasusConstants.HeightCheckType.Collision) { RaycastHit hit; if (Physics.Raycast(new Vector3(source.x, source.y + m_collisionHeightOffset, source.z), Vector3.down, out hit, 2000f)) { if (hit.point.y > source.y) { source.y = hit.point.y; } } return source; } else { Terrain t = GetTerrain(source); if (t != null) { float height = t.SampleHeight(source); if (height > source.y) { source.y = height; } } return source; } } /// /// Return a validated lookat position at the minimum height /// /// Source location being checked /// A height check overide /// Updated position taking into account minimum height public Vector3 GetLowestLookatPosition(Vector3 source, PegasusConstants.PoiHeightCheckType heightCheckOverride = PegasusConstants.PoiHeightCheckType.ManagerSettings) { //Update if there is an override PegasusConstants.HeightCheckType heightCheckType = m_heightCheckType; if (heightCheckOverride != PegasusConstants.PoiHeightCheckType.ManagerSettings) { if (heightCheckOverride == PegasusConstants.PoiHeightCheckType.Collision) { heightCheckType = PegasusConstants.HeightCheckType.Collision; } else if (heightCheckOverride == PegasusConstants.PoiHeightCheckType.Terrain) { heightCheckType = PegasusConstants.HeightCheckType.Terrain; } else { heightCheckType = PegasusConstants.HeightCheckType.None; } } //Peform the height check if (heightCheckType == PegasusConstants.HeightCheckType.None) { return source; } else if (heightCheckType == PegasusConstants.HeightCheckType.Collision) { RaycastHit hit; if (Physics.Raycast(new Vector3(source.x, source.y + m_collisionHeightOffset, source.z), Vector3.down, out hit, 2000f)) { source.y = hit.point.y; } return source; } else { Terrain t = GetTerrain(source); if (t != null) { source.y = t.SampleHeight(source); } return source; } } /// /// Return a validated / corrected lookat position /// /// Source location /// Updated position taking into account minimum height public float GetValidatedLookatHeightRelativeToMinimum(Vector3 source, PegasusConstants.PoiHeightCheckType heightCheckOverride = PegasusConstants.PoiHeightCheckType.ManagerSettings) { Vector3 minPosition = GetLowestLookatPosition(source, heightCheckOverride); return source.y - minPosition.y; } /// /// Get the terrain in this location, otherwise return null /// /// Location to check in world units /// Terrain here or null public Terrain GetTerrain(Vector3 location) { Terrain terrain; Vector3 terrainMin = new Vector3(); Vector3 terrainMax = new Vector3(); //First check active terrain - most likely already selected terrain = Terrain.activeTerrain; if (terrain != null) { terrainMin = terrain.GetPosition(); terrainMax = terrainMin + terrain.terrainData.size; if (location.x >= terrainMin.x && location.x <= terrainMax.x) { if (location.z >= terrainMin.z && location.z <= terrainMax.z) { return terrain; } } } //Then check rest of terrains for (int idx = 0; idx < Terrain.activeTerrains.Length; idx++) { terrain = Terrain.activeTerrains[idx]; terrainMin = terrain.GetPosition(); terrainMax = terrainMin + terrain.terrainData.size; if (location.x >= terrainMin.x && location.x <= terrainMax.x) { if (location.z >= terrainMin.z && location.z <= terrainMax.z) { return terrain; } } } return null; } #endregion #region Update processing /// /// Both drive (if not coroutine based) and apply the flythrough into the scene /// void LateUpdate() { //Keep updating current time if paused to stop big resume time delta issue **** THis may no longer be valit if (m_currentState == PegasusConstants.FlythroughState.Paused) { m_lastUpdateTime = DateTime.Now; } //Exit if we are not in running mode if (m_currentState != PegasusConstants.FlythroughState.Started) { return; } //Calculate the update CalculateFlythroughUpdates(); //Apply the update into the scene if (m_canUpdateNow && m_target != null) { //Apply the rotation smoothly if (m_rotationDamping > 0f) { m_target.transform.rotation = Quaternion.Slerp(m_target.transform.rotation, m_currentRotation, Time.deltaTime * (1f / m_rotationDamping)); } else { m_target.transform.rotation = m_currentRotation; } //Apply the position update smoothly if (m_positionDamping > 0f) { m_target.transform.position = Vector3.Slerp(m_target.transform.position, m_currentPosition, Time.deltaTime * (1f / m_positionDamping)); } else { m_target.transform.position = m_currentPosition; } m_canUpdateNow = false; } } /// /// Perform the calculations required for the next update - will trigger an update in the LateUpdate method to apply it to the scene /// private void CalculateFlythroughUpdates() { //Make sure we are on a segment if (m_currentSegment != null) { //Calculate progress and update velocity, position and rotation variables (will be physically applied in late update) m_currentSegment.CalculateProgress(m_currentSegmentDistanceTravelled / m_currentSegment.m_segmentDistance, out m_currentVelocity, out m_currentPosition, out m_currentRotation); //Update the rotation to allow for the rotation offset //m_currentRotation = Quaternion.FromToRotation(Vector3.up, m_targetRotationCorrection) * m_currentRotation; //Update distance travelled for next iteration through if (m_targetFramerateType == PegasusConstants.TargetFrameRate.MaxFps || m_targetFramerateType == PegasusConstants.TargetFrameRate.LeaveAlone) { m_frameUpdateTime = (float)((DateTime.Now - m_lastUpdateTime).Milliseconds) / 1000f; m_lastUpdateTime = DateTime.Now; } m_frameUpdateDistance = m_frameUpdateTime * m_currentVelocity; m_currentSegmentDistanceTravelled += m_frameUpdateDistance; m_totalDistanceTravelled += m_frameUpdateDistance; m_totalDistanceTravelledPct = m_totalDistanceTravelled / m_totalDistance; //Handle segment changes if (m_currentSegmentDistanceTravelled >= m_currentSegment.m_segmentDistance) { //Call any end state triggers m_currentSegment.OnEndTriggers(); //Increment to next segment m_currentSegmentIdx++; if (m_currentSegmentIdx >= m_poiList.Count) { //Detect if we are at end game if (m_flythroughType == PegasusConstants.FlythroughType.Looped) { m_currentSegmentIdx = 0; m_currentSegmentDistanceTravelled -= m_currentSegment.m_segmentDistance; m_totalDistanceTravelled = m_currentSegmentDistanceTravelled; } else { m_currentSegmentIdx--; m_currentSegmentDistanceTravelled = m_currentSegment.m_segmentDistance; m_totalDistanceTravelled = m_totalDistance; m_totalDistanceTravelledPct = 1f; if (m_flythroughEndAction == PegasusConstants.FlythroughEndAction.StopFlythrough) { StopFlythrough(); return; } else if (m_flythroughEndAction == PegasusConstants.FlythroughEndAction.QuitApplication) { StopFlythrough(); #if UNITY_EDITOR UnityEditor.EditorApplication.isPlaying = false; #else Application.Quit(); #endif return; } else { StopFlythrough(); if (m_nextPegasus != null) { m_nextPegasus.StartFlythrough(); } else { Debug.Log("Next Pegasus has not been configured. Can not start."); } return; } } } else { m_currentSegmentDistanceTravelled -= m_currentSegment.m_segmentDistance; } m_totalDistanceTravelledPct = m_totalDistanceTravelled / m_totalDistance; m_currentSegment = m_poiList[m_currentSegmentIdx]; m_currentSegment.OnStartTriggers(); if (m_currentState != PegasusConstants.FlythroughState.Started) { m_canUpdateNow = false; return; } } //Override lookat for path travellers // if (m_currentSegment.m_lookatType == PegasusConstants.LookatType.Path && m_currentSegment.m_nextPoi.m_lookatType == PegasusConstants.LookatType.Path) // { // //This is inherently jittery - needs more investigation // Vector3 forward = m_currentPosition - m_target.transform.position; // // //And then apply if non zero // if (forward != Vector3.zero) // { // m_currentRotation = Quaternion.LookRotation(forward); // } // } //Call the update for the current segment m_currentSegment.OnUpdateTriggers(m_currentSegmentDistanceTravelled / m_currentSegment.m_segmentDistance); //Flag that we can do an update now m_canUpdateNow = true; } } #endregion #region POI routines /// /// Add a new POI at the targetLocation given /// /// Location to add the POI at public void AddPOI(Vector3 targetLocation, Vector3 lookatLocation) { GameObject newPoiGo = new GameObject("POI " + m_poiList.Count); newPoiGo.transform.parent = this.transform; newPoiGo.transform.position = targetLocation; PegasusPoi newPoi = newPoiGo.AddComponent(); newPoi.m_manager = this; newPoi.m_lookatLocation = lookatLocation; if (targetLocation != lookatLocation) { newPoi.m_lookatType = PegasusConstants.LookatType.Target; } newPoi.m_startSpeedType = m_defaults.m_flyThroughSpeed; m_poiList.Add(newPoi); InitialiseFlythrough(); } /// /// Add a new POI halfway between this POI and the next POI, taking traversal into account /// /// /// public PegasusPoi AddPoiAfter(PegasusPoi currentPoi) { GameObject newPoiGo = new GameObject("POI " + m_poiList.Count); PegasusPoi newPoi = newPoiGo.AddComponent(); newPoi.m_manager = this; newPoi.m_startSpeedType = m_defaults.m_flyThroughSpeed; m_poiList.Insert(m_poiList.IndexOf(currentPoi) + 1, newPoi); Vector3 newPoiLocation = GetValidatedPoiPosition(currentPoi.CalculatePositionLinear(0.5f), newPoi.m_heightCheckType); newPoiGo.transform.position = newPoiLocation; newPoiGo.transform.parent = this.transform; newPoiGo.transform.SetSiblingIndex(currentPoi.m_segmentIndex + 1); InitialiseFlythrough(); return newPoi; } /// /// Add a new POI halfway between this POI and the previous POI, taking traversal into account /// /// /// public PegasusPoi AddPoiBefore(PegasusPoi currentPoi) { return AddPoiAfter(GetPrevPOI(currentPoi)); } /// /// Get the first poi /// /// First POI or null if no data public PegasusPoi GetFirstPOI() { if (m_poiList.Count < 1) { return null; } return m_poiList[0]; } /// /// Get the poi from POI index /// /// POI to get /// POI or null if no data or invalid index public PegasusPoi GetPOI(int poiIndex) { if (m_poiList.Count == 0 || poiIndex < 0 || poiIndex >= m_poiList.Count) { return null; } return m_poiList[poiIndex]; } /// /// Get the prev poi from POI passed in /// /// POI to start from /// Whether or not to wrap around /// Prev POI or null if no data, invalid params or wrap boundary hit public PegasusPoi GetPrevPOI(PegasusPoi currentPoi, bool wrap = true) { if (currentPoi != null) { if (currentPoi.m_segmentIndex > 0) { return m_poiList[currentPoi.m_segmentIndex - 1]; } else { if (wrap) { return m_poiList[m_poiList.Count - 1]; } else { return null; } } } return null; } /// /// Get the next poi on from poi passed in /// /// POI to start from /// Whether or not to wrap around /// Next POI or null if no data, invalid params or wrap boundary hit /// public PegasusPoi GetNextPOI(PegasusPoi currentPoi, bool wrap = true) { if (currentPoi != null) { if (currentPoi.m_segmentIndex < m_poiList.Count - 1) { return m_poiList[currentPoi.m_segmentIndex + 1]; } else { if (wrap) { return m_poiList[0]; } else { return null; } } } return null; } /// /// Go and update all POI heights to min height of the manager /// public void SetPoiToMinHeight() { PegasusPoi poi; for (int idx = 0; idx < m_poiList.Count; idx++) { poi = m_poiList[idx]; poi.transform.position = poi.m_manager.GetLowestPoiPosition(poi.transform.position, poi.m_heightCheckType); } InitialiseFlythrough(); } #endregion } }