Persist player and third-person camera state across sessions
Add player position/rotation/wall-climb persistence and camera orbit (yaw/pitch/radius) persistence, auto-saved on quit/play-stop and auto-loaded on start. Also includes the wall-climb fixes (directional top-out, base detach on push-away). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa7082817e
commit
cda1373979
8 files changed
+340
-16
No files matched your search
@@ -0,0 +1,143 @@
|
||||
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();
|
||||
|
||||
/// <summary>Saves once during teardown (OnApplicationQuit and OnDestroy both call this; only the first writes).</summary>
|
||||
private void SaveCameraOnTeardown()
|
||||
{
|
||||
if (_cameraSavedOnTeardown) return;
|
||||
_cameraSavedOnTeardown = true;
|
||||
SaveCameraState();
|
||||
}
|
||||
|
||||
/// <summary>Writes the current orbit yaw/pitch and radius to disk.</summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Restores the saved orbit yaw/pitch and radius, or does nothing if there is no save.</summary>
|
||||
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<CameraState>(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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Deletes the saved camera state (next start uses the authored orbit). Editor convenience.</summary>
|
||||
[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user