#if !CINEMACHINE_NO_CM2_SUPPORT //#define DEBUG_HELPERS #pragma warning disable CS0618 // obsolete warnings using System; using System.Linq; using UnityEditor; using UnityEngine; using UnityEngine.Splines; using Object = UnityEngine.Object; namespace Unity.Cinemachine.Editor { partial class UpgradeObjectToCm3 { /// /// In-place upgrade a GameObject. Obsolete components are disabled (not deleted), /// and replacement components are added. GameObject structure may be re-organized /// (hidden objects deleted, components moved). /// /// In a second pass, call DeleteObsoleteComponents(). /// /// The GameObject to upgrade /// If the GameObject is not fully upgradable, a clone of the original will be returned. /// A null return value means success. public GameObject UpgradeComponents(GameObject go) { GameObject notUpgradable = null; // Is it a DollyCart? if (ReplaceComponent(go)) { var obsoleteDolly = go.GetComponent(); var splineCart = go.GetComponent(); obsoleteDolly.UpgradeToCm3(splineCart); } // Is it a path? if (go.TryGetComponent(out CinemachinePathBase path)) UpgradePath(path); else if (!go.TryGetComponent(out _)) { // It's some kind of vcam. Check for FreeLook first because it has // hidden child VirtualCameras and we need to remove them if (go.TryGetComponent(out var freelook)) { notUpgradable = UpgradeFreelook(freelook); var manager = freelook.ParentCamera as CinemachineCameraManagerBase; if (manager != null) manager.InvalidateCameraCache(); } else if (go.TryGetComponent(out var vcam)) { notUpgradable = UpgradeVcam(vcam); var manager = vcam.ParentCamera as CinemachineCameraManagerBase; if (manager != null) manager.InvalidateCameraCache(); } // Upgrade the pipeline components (there will be more of these...) if (ReplaceComponent(go)) go.GetComponent().UpgradeToCm3(go.GetComponent()); if (ReplaceComponent(go)) { var gc = go.GetComponent(); gc.UpgradeToCm3(go.GetComponent()); if (!go.TryGetComponent(out var _)) { var framer = Undo.AddComponent(go); go.GetComponent().AddExtension(framer); gc.UpgradeToCm3(framer); } } if (ReplaceComponent(go)) go.GetComponent().UpgradeToCm3(go.GetComponent()); if (ReplaceComponent(go)) { var ft = go.GetComponent(); ft.UpgradeToCm3(go.GetComponent()); if (ft.FollowTargetAsGroup != null && ft.FollowTargetAsGroup.IsValid && ft.m_GroupFramingMode != CinemachineFramingTransposer.FramingMode.None && !go.TryGetComponent(out var _)) { var framer = Undo.AddComponent(go); go.GetComponent().AddExtension(framer); ft.UpgradeToCm3(framer); } } if (ReplaceComponent(go)) { var pov = go.GetComponent(); pov.UpgradeToCm3(go.GetComponent()); ConvertInputAxis(go, "Look X (Pan)", ref pov.m_HorizontalAxis); ConvertInputAxis(go, "Look Y (Tilt)", ref pov.m_VerticalAxis); } if (ReplaceComponent(go)) { var orbital = go.GetComponent(); orbital.UpgradeToCm3(go.GetComponent()); ConvertInputAxis(go, "Look Orbit X", ref orbital.m_XAxis); } if (ReplaceComponent(go)) go.GetComponent().UpgradeToCm3(go.GetComponent()); if (ReplaceComponent(go)) go.GetComponent().UpgradeToCm3(go.GetComponent()); if (ReplaceComponent(go)) go.GetComponent().UpgradeToCm3(go.GetComponent()); #if CINEMACHINE_PHYSICS if (ReplaceComponent(go)) go.GetComponent().UpgradeToCm3(go.GetComponent()); #endif #if CINEMACHINE_PHYSICS || CINEMACHINE_PHYSICS_2D if (go.TryGetComponent(out var confiner)) { #if CINEMACHINE_PHYSICS if (confiner.UpgradeToCm3_GetTargetType() == typeof(CinemachineConfiner3D)) { ReplaceComponent(go); confiner.UpgradeToCm3(go.GetComponent()); } #endif #if CINEMACHINE_PHYSICS_2D if (confiner.UpgradeToCm3_GetTargetType() == typeof(CinemachineConfiner2D)) { ReplaceComponent(go); confiner.UpgradeToCm3(go.GetComponent()); } #endif } #endif } return notUpgradable; } public static Type GetBehaviorReferenceUpgradeType(MonoBehaviour b) { #if CINEMACHINE_PHYSICS || CINEMACHINE_PHYSICS_2D // Hack for Confiner upgrade: 2D or 3D? if (b is CinemachineConfiner c) return c.UpgradeToCm3_GetTargetType(); #endif var oldType = b.GetType(); return ClassUpgradeMap.ContainsKey(oldType) ? ClassUpgradeMap[oldType] : oldType; } /// /// Handles animation clip reference updates for the input animation clip. /// Does not support Undo! /// public void ProcessAnimationClip(AnimationClip animationClip, Animator trackAnimator) { if (animationClip == null || trackAnimator == null) return; var existingEditorBindings = AnimationUtility.GetCurveBindings(animationClip); foreach (var previousBinding in existingEditorBindings) { // if type is not in class upgrade map, then we won't change binding if (!ClassUpgradeMap.ContainsKey(previousBinding.type)) continue; var newBinding = previousBinding; // upgrade type based on mapping newBinding.type = ClassUpgradeMap[previousBinding.type]; // clean path pointing to old structure where vcam components lived on a hidden child gameObject if (previousBinding.path.EndsWith("cm")) { var path = previousBinding.path; //path is either cm only, or someParent/someOtherParent/.../cm. In the second case, we need to remove /cm, thus -1 var index = Mathf.Max(0, path.IndexOf("cm") - 1); newBinding.path = path.Substring(0, index); } if (previousBinding.path.EndsWith("Rig")) { var path = newBinding.path; //path is either xRig only, or someParent/someOtherParent/.../xRig. In the second case, we need to remove /xRig, thus -1 newBinding.path = path.Substring(0, Mathf.Max(0, FindRig(path) - 1)); static int FindRig(string path) { var rigs = CinemachineFreeLook.RigNames; var index = 0; foreach (var rig in rigs) index = Mathf.Max(index, path.IndexOf(rig)); return index; } } // clean old convention if (previousBinding.propertyName.StartsWith("m_")) { var propertyName = previousBinding.propertyName; newBinding.propertyName = propertyName.Replace("m_", string.Empty); } // Check if previousBinding.type needs an API change if (m_APIUpgradeMaps.ContainsKey(previousBinding.type) && m_APIUpgradeMaps[previousBinding.type].ContainsKey(newBinding.propertyName)) { var mapping = m_APIUpgradeMaps[previousBinding.type][newBinding.propertyName]; newBinding.propertyName = mapping.Item1; // type can differ from previously set type, because some components became several separate ones newBinding.type = mapping.Item2; // special handling for references if (mapping.Item1.Contains("managedReferences")) { // find property name of the reference var propertyName = mapping.Item1; var start = propertyName.IndexOf("[") + 1; var end = propertyName.IndexOf("]"); propertyName = propertyName[start..end]; // find animated target component var target = trackAnimator.transform.gameObject.GetComponent(mapping.Item2); // find reference id of the property that's being animated var so = new SerializedObject(target); var prop = so.FindProperty(propertyName); var newPropertyName = newBinding.propertyName; newBinding.propertyName = newPropertyName[..start] + prop.managedReferenceId + newPropertyName[end..]; } } #if DEBUG_HELPERS Debug.Log(previousBinding.path + "." + previousBinding.type.Name + "." + previousBinding.propertyName + " -> " + newBinding.path + "." + newBinding.type.Name + "." + newBinding.propertyName); #endif var curve = AnimationUtility.GetEditorCurve(animationClip, previousBinding); //keep existing curves AnimationUtility.SetEditorCurve(animationClip, previousBinding, null); //remove previous binding AnimationUtility.SetEditorCurve(animationClip, newBinding, curve); //set new binding } } /// /// After a GameObject object has been in-place upgraded, call this to delete the /// obsolete components that have been disabled. /// /// The GameObject being upgraded public void DeleteObsoleteComponents(GameObject go) { if (go == null) return; foreach (var t in ObsoleteComponentTypesToDelete) { var components = go.GetComponentsInChildren(t); foreach (var c in components) Undo.DestroyObjectImmediate(c); } if (PrefabUtility.IsPartOfAnyPrefab(go)) PrefabUtility.RecordPrefabInstancePropertyModifications(go); } /// Disable an obsolete component and add a replacement static bool ReplaceComponent(GameObject go) where TOld : MonoBehaviour where TNew : MonoBehaviour { if (go.TryGetComponent(out var cOld) && cOld.GetType() == typeof(TOld)) { Undo.RecordObject(cOld, "Upgrader: disable obsolete"); cOld.enabled = false; if (!go.TryGetComponent(out var _)) Undo.AddComponent(go); return true; } return false; } // Copy all serializable values static void CopyValues(T from, T to) { var json = JsonUtility.ToJson(from); JsonUtility.FromJsonOverwrite(json, to); } static CinemachineCamera UpgradeVcamBaseToCmCamera(CinemachineVirtualCameraBase vcam) { var go = vcam.gameObject; if (!go.TryGetComponent(out CinemachineCamera cmCamera)) // Check if RequireComponent already added CinemachineCamera { // First disable the old vcamBase, or new one will be rejected Undo.RecordObject(vcam, "Upgrader: disable obsolete"); vcam.enabled = false; cmCamera = Undo.AddComponent(go); CopyValues(vcam, cmCamera); // Register the extensions with the cmCamera foreach (var extension in vcam.gameObject.GetComponents()) cmCamera.AddExtension(extension); } else if (vcam.enabled) // RequireComponent added CinemachineCamera, it should be enabled iff vcam was enabled { Undo.RecordObject(vcam, "Upgrader: disable obsolete"); vcam.enabled = false; Undo.RecordObject(cmCamera, "Upgrader: enable upgraded"); cmCamera.enabled = true; } return cmCamera; } static GameObject UpgradeVcam(CinemachineVirtualCamera vcam) { var go = vcam.gameObject; var cmCamera = UpgradeVcamBaseToCmCamera(vcam); cmCamera.Follow = vcam.m_Follow; cmCamera.LookAt = vcam.m_LookAt; cmCamera.Target.CustomLookAtTarget = vcam.m_Follow != vcam.m_LookAt; cmCamera.Lens = vcam.m_Lens.ToLensSettings(); cmCamera.BlendHint = vcam.BlendHint; if (vcam.m_OnCameraLiveEvent.GetPersistentEventCount() > 0) { var evts = Undo.AddComponent(go); evts.OnCameraLive = vcam.m_OnCameraLiveEvent; } // Transfer the component pipeline var pipeline = vcam.GetComponentPipeline(); if (pipeline != null) foreach (var c in pipeline) if (c != null) CopyValues(c, Undo.AddComponent(go, c.GetType()) as CinemachineComponentBase); // Destroy the hidden child object var owner = vcam.GetComponentOwner(); if (owner != null && owner.gameObject != go) UnparentAndDestroy(owner); return null; } static void UnparentAndDestroy(Transform child) { if (child != null) { Undo.SetTransformParent(child, null, "Upgrader: destroy hidden child"); Undo.DestroyObjectImmediate(child.gameObject); } } static void ConvertInputAxis(GameObject go, string name, ref AxisState axis) { if (!go.TryGetComponent(out var iac)) iac = Undo.AddComponent(go); // Force creation of missing input controllers iac.SynchronizeControllers(); #if CINEMACHINE_UNITY_INPUTSYSTEM var provider = go.GetComponent(); if (provider != null) { provider.enabled = false; iac.AutoEnableInputs = provider.AutoEnableInputs; } #endif for (var i = 0; i < iac.Controllers.Count; ++i) { var c = iac.Controllers[i]; if (c.Name == name) { #if ENABLE_LEGACY_INPUT_MANAGER c.Input.LegacyInput = axis.m_InputAxisName; c.Input.LegacyGain = axis.m_MaxSpeed; #endif #if CINEMACHINE_UNITY_INPUTSYSTEM if (provider != null) c.Input.InputAction = provider.XYAxis; c.Input.Gain = axis.m_MaxSpeed; if (axis.m_SpeedMode == AxisState.SpeedMode.MaxSpeed) c.Input.Gain /= 100; // very approx #endif c.Driver.AccelTime = axis.m_AccelTime; c.Driver.DecelTime = axis.m_DecelTime; break; } } } static GameObject UpgradeFreelook(CinemachineFreeLook freelook) { GameObject notUpgradable = null; var topRig = freelook.GetRig(0); var middleRig = freelook.GetRig(1); var bottomRig = freelook.GetRig(2); if (topRig == null || middleRig == null || bottomRig == null) return null; // invalid Freelook, nothing to do var go = freelook.gameObject; if (!IsFreelookUpgradable(freelook)) { notUpgradable = Object.Instantiate(go); notUpgradable.SetActive(false); Undo.AddComponent(notUpgradable); Undo.RegisterCreatedObjectUndo(notUpgradable, "Upgrader: clone of non upgradable"); } var cmCamera = UpgradeVcamBaseToCmCamera(freelook); cmCamera.Follow = freelook.m_Follow; cmCamera.LookAt = freelook.m_LookAt; cmCamera.Target.CustomLookAtTarget = freelook.m_Follow != freelook.m_LookAt; cmCamera.BlendHint = freelook.BlendHint; if (freelook.m_OnCameraLiveEvent.GetPersistentEventCount() > 0) { var evts = Undo.AddComponent(go); evts.OnCameraLive = freelook.m_OnCameraLiveEvent; } var freeLookModifier = Undo.AddComponent(go); ConvertFreelookLens(freelook, cmCamera, freeLookModifier); ConvertFreelookBody(freelook, go, freeLookModifier); ConvertFreelookAim(freelook, go, freeLookModifier); ConvertFreelookNoise(freelook, go, freeLookModifier); ConvertInputAxis(go, "Look Orbit X", ref freelook.m_XAxis); ConvertInputAxis(go, "Look Orbit Y", ref freelook.m_YAxis); // Destroy the hidden child objects UnparentAndDestroy(topRig.GetComponentOwner()); UnparentAndDestroy(topRig.transform); UnparentAndDestroy(middleRig.GetComponentOwner()); UnparentAndDestroy(middleRig.transform); UnparentAndDestroy(bottomRig.GetComponentOwner()); UnparentAndDestroy(bottomRig.transform); return notUpgradable; } /// /// Differences in these fields will be ignored because the FreeLookModifier /// will take care of them /// static string[] s_FreelookIgnoreFieldsList = { "m_ScreenX", "m_ScreenY", "m_DeadZoneWidth", "m_DeadZoneHeight", "m_SoftZoneWidth", "m_SoftZoneHeight", "m_BiasX", "m_BiasY", "m_AmplitudeGain", "m_FrequencyGain", }; static bool IsFreelookUpgradable(CinemachineFreeLook freelook) { // Freelook is not upgradable if it has: // - look at override (parent lookat != child lookat) // - different noise profiles on top, mid, bottom || // - different aim component types || // - same aim component types and with different parameters var topRig = freelook.GetRig(0); var middleRig = freelook.GetRig(1); var bottomRig = freelook.GetRig(2); var parentLookAt = freelook.LookAt; var topNoise = topRig.GetCinemachineComponent(); var middleNoise = middleRig.GetCinemachineComponent(); var bottomNoise = bottomRig.GetCinemachineComponent(); var midAim = middleRig.GetCinemachineComponent(CinemachineCore.Stage.Aim); return parentLookAt == topRig.LookAt && parentLookAt == middleRig.LookAt && parentLookAt == bottomRig.LookAt && IsCompatibleNoise(topNoise, middleNoise) && IsCompatibleNoise(middleNoise, bottomNoise) && PublicFieldsEqual(topRig.GetCinemachineComponent(CinemachineCore.Stage.Aim), midAim, s_FreelookIgnoreFieldsList) && PublicFieldsEqual(bottomRig.GetCinemachineComponent(CinemachineCore.Stage.Aim), midAim, s_FreelookIgnoreFieldsList); static bool IsCompatibleNoise(CinemachineBasicMultiChannelPerlin a, CinemachineBasicMultiChannelPerlin b) { // This check is conceived with the understanding that if a rig has empty // noise then any specific settings differences can be accounted for in // the FreeLookModifier by setting amplitude to 0 return a == null || b == null || ((a.NoiseProfile == null || b.NoiseProfile == null || a.NoiseProfile == b.NoiseProfile) && a.PivotOffset == b.PivotOffset); } static bool PublicFieldsEqual(CinemachineComponentBase a, CinemachineComponentBase b, params string[] ignoreList) { if (a == null && b == null) return true; if (a == null || b == null) return false; var aType = a.GetType(); if (aType != b.GetType()) return false; var publicFields = aType.GetFields(); foreach (var pi in publicFields) { var name = pi.Name; if (ignoreList.Contains(name)) continue; // ignore var field = aType.GetField(name); if (!field.GetValue(a).Equals(field.GetValue(b))) { #if DEBUG_HELPERS Debug.Log("Rig values differ: " + name); #endif return false; } } return true; } } static void ConvertFreelookLens( CinemachineFreeLook freelook, CinemachineCamera cmCamera, CinemachineFreeLookModifier freeLookModifier) { if (freelook.m_CommonLens) cmCamera.Lens = freelook.m_Lens.ToLensSettings(); else { cmCamera.Lens = freelook.GetRig(1).m_Lens.ToLensSettings(); var lens0 = freelook.GetRig(0).m_Lens.ToLensSettings(); var lens2 = freelook.GetRig(2).m_Lens.ToLensSettings(); if (!LensSettings.AreEqual(ref cmCamera.Lens, ref lens0) || !LensSettings.AreEqual(ref cmCamera.Lens, ref lens2)) { freeLookModifier.Modifiers.Add(new CinemachineFreeLookModifier.LensModifier { Top = lens0, Bottom = lens2 }); } } } static void ConvertFreelookBody( CinemachineFreeLook freelook, GameObject go, CinemachineFreeLookModifier freeLookModifier) { var top = freelook.GetRig(0).GetCinemachineComponent(); var middle = freelook.GetRig(1).GetCinemachineComponent(); var bottom = freelook.GetRig(2).GetCinemachineComponent(); if (top == null || middle == null || bottom == null) return; // Use middle rig as template var orbital = Undo.AddComponent(go); middle.UpgradeToCm3(orbital); orbital.HorizontalAxis.Recentering = new () { Enabled = freelook.m_RecenterToTargetHeading.m_enabled, Time = freelook.m_RecenterToTargetHeading.m_RecenteringTime, Wait = freelook.m_RecenterToTargetHeading.m_WaitTime }; orbital.OrbitStyle = CinemachineOrbitalFollow.OrbitStyles.ThreeRing; orbital.Orbits = new Cinemachine3OrbitRig.Settings { Top = new Cinemachine3OrbitRig.Orbit { Height = freelook.m_Orbits[0].m_Height, Radius = Mathf.Max(freelook.m_Orbits[0].m_Radius, 0.01f), }, Center = new Cinemachine3OrbitRig.Orbit { Height = freelook.m_Orbits[1].m_Height, Radius = Mathf.Max(freelook.m_Orbits[1].m_Radius, 0.01f), }, Bottom = new Cinemachine3OrbitRig.Orbit { Height = freelook.m_Orbits[2].m_Height, Radius = Mathf.Max(freelook.m_Orbits[2].m_Radius, 0.01f), }, SplineCurvature = freelook.m_SplineCurvature, }; // Preserve the 0...1 range for Y axis, in case some script is depending on it orbital.VerticalAxis.Range = new Vector2(0, 1); orbital.VerticalAxis.Center = 0.5f; orbital.VerticalAxis.Wrap = false; orbital.VerticalAxis.Value = freelook.m_YAxis.Value; orbital.VerticalAxis.Recentering = new () { Enabled = freelook.m_YAxisRecentering.m_enabled, Time = freelook.m_YAxisRecentering.m_RecenteringTime, Wait = freelook.m_YAxisRecentering.m_WaitTime }; // Do we need a modifier? var topDamping = new Vector3(top.m_XDamping, top.m_YDamping, top.m_ZDamping); var bottomDamping = new Vector3(bottom.m_XDamping, bottom.m_YDamping, bottom.m_ZDamping); if (!(orbital.TrackerSettings.PositionDamping - topDamping).AlmostZero() || !(orbital.TrackerSettings.PositionDamping - bottomDamping).AlmostZero()) { freeLookModifier.Modifiers.Add(new CinemachineFreeLookModifier.PositionDampingModifier { Damping = new CinemachineFreeLookModifier.TopBottomRigs { Top = topDamping, Bottom = bottomDamping, } }); } } static void ConvertFreelookAim( CinemachineFreeLook freelook, GameObject go, CinemachineFreeLookModifier freeLookModifier) { // We assume that the middle aim is a suitable template var template = freelook.GetRig(1).GetCinemachineComponent(CinemachineCore.Stage.Aim); if (template == null) return; var newAim = (CinemachineComponentBase)Undo.AddComponent(go, template.GetType()); CopyValues(template, newAim); // Add modifier if it is a composer var middle = newAim as CinemachineComposer; var top = freelook.GetRig(0).GetComponentInChildren(); var bottom = freelook.GetRig(2).GetComponentInChildren(); if (middle != null && top != null && bottom != null) { var topComposition = top.Composition; var middleComposition = middle.Composition; var bottomComposition = bottom.Composition; if (!ScreenComposerSettings.Approximately(middleComposition, topComposition) || !ScreenComposerSettings.Approximately(middleComposition, bottomComposition)) { freeLookModifier.Modifiers.Add(new CinemachineFreeLookModifier.CompositionModifier { Composition = new CinemachineFreeLookModifier.TopBottomRigs { Top = topComposition, Bottom = bottomComposition } }); } } } static void ConvertFreelookNoise( CinemachineFreeLook freelook, GameObject go, CinemachineFreeLookModifier freeLookModifier) { // Noise can be on any subset of the rigs var top = freelook.GetRig(0).GetCinemachineComponent(); var middle = freelook.GetRig(1).GetCinemachineComponent(); var bottom = freelook.GetRig(2).GetCinemachineComponent(); var template = middle != null && middle.NoiseProfile != null ? middle : (top != null && top.NoiseProfile != null? top : bottom); if (template == null || template.NoiseProfile == null) return; var middleNoise = Undo.AddComponent(go); CopyValues(template, middleNoise); var middleSettings = GetNoiseSettings(middle); middleNoise.AmplitudeGain = middleSettings.Amplitude; middleNoise.FrequencyGain = middleSettings.Frequency; var topSettings = GetNoiseSettings(top); var bottomSettings = GetNoiseSettings(bottom); if (!Mathf.Approximately(topSettings.Amplitude, middleSettings.Amplitude) || !Mathf.Approximately(topSettings.Frequency, middleSettings.Frequency) || !Mathf.Approximately(bottomSettings.Amplitude, middleSettings.Amplitude) || !Mathf.Approximately(bottomSettings.Frequency, middleSettings.Frequency)) { freeLookModifier.Modifiers.Add(new CinemachineFreeLookModifier.NoiseModifier { Noise = new CinemachineFreeLookModifier.TopBottomRigs { Top = topSettings, Bottom = bottomSettings } }); } CinemachineFreeLookModifier.NoiseModifier.NoiseSettings GetNoiseSettings(CinemachineBasicMultiChannelPerlin noise) { var settings = new CinemachineFreeLookModifier.NoiseModifier.NoiseSettings(); if (noise != null) { settings.Amplitude = noise.AmplitudeGain; settings.Frequency = noise.FrequencyGain; } return settings; } } static void UpgradePath(CinemachinePathBase pathBase) { var go = pathBase.gameObject; if (go.TryGetComponent(out SplineContainer spline)) return; // already converted spline = Undo.AddComponent(go); var splineRoll = Undo.AddComponent(go); splineRoll.Roll = new SplineData(); Undo.RecordObject(pathBase, "Upgrader: disable obsolete"); pathBase.enabled = false; switch (pathBase) { case CinemachinePath path: { var waypoints = path.m_Waypoints; spline.Spline = new Spline(waypoints.Length, path.Looped); for (var i = 0; i < waypoints.Length; i++) { spline.Spline.Add(new BezierKnot { Position = waypoints[i].position, Rotation = Quaternion.identity, TangentIn = -waypoints[i].tangent, TangentOut = waypoints[i].tangent, }); splineRoll.Roll.Add(new DataPoint(i, waypoints[i].roll)); } break; } case CinemachineSmoothPath smoothPath: { var waypoints = smoothPath.m_Waypoints; spline.Spline = new Spline(waypoints.Length, smoothPath.Looped); for (var i = 0; i < waypoints.Length; i++) { var tangent = smoothPath.EvaluateLocalTangent(i); tangent /= 3f; // divide by magic number to match spline tangent scale correctly spline.Spline.Add(new BezierKnot { Position = waypoints[i].position, Rotation = Quaternion.identity, TangentIn = -tangent, TangentOut = tangent, }); splineRoll.Roll.Add(new DataPoint(i, waypoints[i].roll)); } break; } default: { // GML todo: handle this message properly Undo.AddComponent(pathBase.gameObject); Debug.LogError($"{go.name}: Path type {pathBase.GetType().Name} is not handled by the upgrader"); break; } } } } } #pragma warning restore CS0618 #endif