using System; using System.IO; using UnityEngine; using UnityEngine.SceneManagement; public partial class PlayerCharacterController { // --- Persistence: player position, rotation and wall-climb state --- // // Saved on quit / play-stop and restored on the next start, so the player resumes exactly // where (and how) they left off - including hanging on a wall. This is small structured // state, so it goes to a human-readable JSON file (like PaintHud.Persistence), not the raw // byte dump the painter uses for its bulk pixel maps. [Header("Persistence")] [Tooltip("Save the player's position, rotation and wall-climb state on quit / play-stop and " + "restore them on the next start. Off = the player starts at its authored scene transform.")] public bool persistPlayer = 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 playerSaveId = ""; private const int PlayerFileVersion = 1; private bool _playerSavedOnTeardown; // guards a double save across OnApplicationQuit + OnDestroy private static string PlayerSaveDir => Path.Combine(Application.persistentDataPath, "MeshPaint"); private string PlayerSavePath() { string key = string.IsNullOrEmpty(playerSaveId) ? SanitizePlayerKey(SceneManager.GetActiveScene().name + "_" + gameObject.name) : SanitizePlayerKey(playerSaveId); return Path.Combine(PlayerSaveDir, key + ".player"); } private static string SanitizePlayerKey(string s) { foreach (char c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_'); return s.Replace(' ', '_'); } [Serializable] private class PlayerState { public int version = PlayerFileVersion; public Vector3 position; public Quaternion rotation; // Wall-climb attachment. The normal/offset/yaw/left-ground flags fully describe the grip; // ProbeWall re-validates contact on the next frame, so a grip restored away from any wall // drops cleanly after the grace period rather than freezing the player in mid-air. public bool isWallClimbing; public Vector3 wallNormal; public float wallOffset; public float wallYaw; public bool climbLeftGround; } void Start() => RestorePlayerState(); private void OnApplicationQuit() => SavePlayerOnTeardown(); private void OnDestroy() => SavePlayerOnTeardown(); /// Saves once during teardown (OnApplicationQuit and OnDestroy both call this; only the first writes). private void SavePlayerOnTeardown() { if (_playerSavedOnTeardown) return; _playerSavedOnTeardown = true; SavePlayerState(); } /// Writes the current transform and wall-climb state to disk. public void SavePlayerState() { if (!persistPlayer) return; try { var state = new PlayerState { position = transform.position, rotation = transform.rotation, isWallClimbing = _isWallClimbing, wallNormal = _wallNormal, wallOffset = _wallOffset, wallYaw = _wallYaw, climbLeftGround = _climbLeftGround, }; Directory.CreateDirectory(PlayerSaveDir); File.WriteAllText(PlayerSavePath(), JsonUtility.ToJson(state, true)); } catch (Exception e) { Debug.LogWarning($"[PlayerCharacterController] Saving player state failed: {e.Message}", this); } } /// Restores the saved transform and wall-climb state, or does nothing if there is no save. private void RestorePlayerState() { if (!persistPlayer) return; var state = LoadPlayerState(); if (state == null) return; // Teleport through the CharacterController: toggling enabled clears its cached internal // position so it neither snaps back nor fights the restored transform. bool wasEnabled = _controller && _controller.enabled; if (_controller) _controller.enabled = false; transform.SetPositionAndRotation(state.position, state.rotation); if (_controller) _controller.enabled = wasEnabled; _isWallClimbing = state.isWallClimbing; if (state.wallNormal.sqrMagnitude > 1e-6f) _wallNormal = state.wallNormal.normalized; _wallOffset = state.wallOffset; _wallYaw = state.wallYaw; _climbLeftGround = state.climbLeftGround; if (_isWallClimbing) _verticalVelocity = 0f; // a restored grip starts at rest, not mid-fall } private PlayerState LoadPlayerState() { string path = PlayerSavePath(); if (!File.Exists(path)) return null; try { var state = JsonUtility.FromJson(File.ReadAllText(path)); if (state == null || state.version != PlayerFileVersion) return null; return state; } catch (Exception e) { Debug.LogWarning($"[PlayerCharacterController] Loading player state failed: {e.Message}", this); return null; } } /// Deletes the saved player state (next start uses the authored scene transform). Editor convenience. [ContextMenu("Clear Saved Player State")] public void ClearSavedPlayerState() { try { string path = PlayerSavePath(); if (File.Exists(path)) File.Delete(path); Debug.Log($"[PlayerCharacterController] Cleared saved player state: {path}", this); } catch (Exception e) { Debug.LogWarning($"[PlayerCharacterController] Clearing saved player state failed: {e.Message}", this); } } }