using System; using System.IO; using UnityEngine; using UnityEngine.SceneManagement; public partial class ThirdPersonOrbitInput { // --- Persistence: third-person camera orbit (rotation + distance) --- // // Saved on quit / play-stop and restored on the next start, so the camera resumes at the same // view. The camera is a CinemachineOrbitalFollow whose world position/rotation are derived each // frame from the orbit axes (yaw/pitch) and radius relative to the follow target - so saving the // literal transform would be pointless (Cinemachine overwrites it). The canonical state is the // orbit yaw/pitch (= rotation) and radius (= distance); combined with the persisted player // position this reproduces the exact same view. Small structured state -> human-readable JSON // (like PlayerCharacterController.Persistence), not the painter's raw byte dump. [Header("Persistence")] [Tooltip("Save the camera orbit (yaw/pitch rotation and zoom distance) on quit / play-stop and " + "restore it on the next start. Off = the camera starts at its authored orbit state.")] public bool persistCamera = true; [Tooltip("Save file identifier. Empty = derived from the active scene + object name. Set " + "explicitly to keep a save stable across object renames.")] public string cameraSaveId = ""; private const int CameraFileVersion = 1; private bool _cameraSavedOnTeardown; // guards a double save across OnApplicationQuit + OnDestroy private static string CameraSaveDir => Path.Combine(Application.persistentDataPath, "MeshPaint"); private string CameraSavePath() { string key = string.IsNullOrEmpty(cameraSaveId) ? SanitizeCameraKey(SceneManager.GetActiveScene().name + "_" + gameObject.name) : SanitizeCameraKey(cameraSaveId); return Path.Combine(CameraSaveDir, key + ".camera"); } private static string SanitizeCameraKey(string s) { foreach (char c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_'); return s.Replace(' ', '_'); } [Serializable] private class CameraState { public int version = CameraFileVersion; public float yaw; // CinemachineOrbitalFollow.HorizontalAxis.Value public float pitch; // CinemachineOrbitalFollow.VerticalAxis.Value public float radius; // CinemachineOrbitalFollow.Radius (zoom distance) } void Start() => RestoreCameraState(); private void OnApplicationQuit() => SaveCameraOnTeardown(); private void OnDestroy() => SaveCameraOnTeardown(); /// Saves once during teardown (OnApplicationQuit and OnDestroy both call this; only the first writes). private void SaveCameraOnTeardown() { if (_cameraSavedOnTeardown) return; _cameraSavedOnTeardown = true; SaveCameraState(); } /// Writes the current orbit yaw/pitch and radius to disk. public void SaveCameraState() { if (!persistCamera || !_orbit) return; try { var state = new CameraState { yaw = _orbit.HorizontalAxis.Value, pitch = _orbit.VerticalAxis.Value, radius = _orbit.Radius, }; Directory.CreateDirectory(CameraSaveDir); File.WriteAllText(CameraSavePath(), JsonUtility.ToJson(state, true)); } catch (Exception e) { Debug.LogWarning($"[ThirdPersonOrbitInput] Saving camera state failed: {e.Message}", this); } } /// Restores the saved orbit yaw/pitch and radius, or does nothing if there is no save. private void RestoreCameraState() { if (!persistCamera || !_orbit) return; var state = LoadCameraState(); if (state == null) return; _orbit.HorizontalAxis.Value = state.yaw; _orbit.VerticalAxis.Value = state.pitch; // Clamp into the current zoom range (min/max may have changed in the inspector since saving) // and seed the eased zoom target so the radius doesn't drift back toward the authored value. float radius = Mathf.Clamp(state.radius, minRadius, maxRadius); _orbit.Radius = radius; _targetRadius = radius; _zoomInit = true; } private CameraState LoadCameraState() { string path = CameraSavePath(); if (!File.Exists(path)) return null; try { var state = JsonUtility.FromJson(File.ReadAllText(path)); if (state == null || state.version != CameraFileVersion) return null; return state; } catch (Exception e) { Debug.LogWarning($"[ThirdPersonOrbitInput] Loading camera state failed: {e.Message}", this); return null; } } /// Deletes the saved camera state (next start uses the authored orbit). Editor convenience. [ContextMenu("Clear Saved Camera State")] public void ClearSavedCameraState() { try { string path = CameraSavePath(); if (File.Exists(path)) File.Delete(path); Debug.Log($"[ThirdPersonOrbitInput] Cleared saved camera state: {path}", this); } catch (Exception e) { Debug.LogWarning($"[ThirdPersonOrbitInput] Clearing saved camera state failed: {e.Message}", this); } } }