New PlayerPose ScriptableObject (AnimationClip + ceiling Y-offset) and PlayerPoseController that drives poses via a PlayableGraph, overriding the Animator controller. Poses are held continuously (idle, moving, on wall/ ceiling) so cycle and sprint changes show immediately. Sprinting in any state resets to pose 0. Non-keyed bones fall back to a captured rest pose so empty/partial clips apply fully. Ceiling Y-offset is folded into the controller's single mesh-offset writer. PlayerCharacterController gains IsIdle / IsSprinting / IsOnCeiling / AnimatorOverridden accessors and SetPoseLocalYOffset; SetAnimatorRunning is guarded while a pose owns the Animator. Removed obsolete Paintman.controller. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
123 lines
4.0 KiB
C#
123 lines
4.0 KiB
C#
using System;
|
|
using System.IO;
|
|
using UnityEngine;
|
|
using UnityEngine.SceneManagement;
|
|
|
|
public partial class PlayerPoseController
|
|
{
|
|
// --- Persistence: the selected pose index ---
|
|
//
|
|
// Saved on quit / play-stop and restored on the next start, so the player keeps the pose they
|
|
// had selected. Mirrors the project's other X.Persistence.cs partials (small JSON state under
|
|
// the shared MeshPaint folder).
|
|
|
|
[Header("Persistence")]
|
|
[Tooltip("Save the selected pose index on quit / play-stop and restore it on the next start. " +
|
|
"Off = the selection starts at the default pose.")]
|
|
public bool persistPose = 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 poseSaveId = "";
|
|
|
|
private const int PoseFileVersion = 1;
|
|
private bool _poseSavedOnTeardown; // guards a double save across OnApplicationQuit + OnDestroy
|
|
|
|
private static string PoseSaveDir => Path.Combine(Application.persistentDataPath, "MeshPaint");
|
|
|
|
private string PoseSavePath()
|
|
{
|
|
string key = string.IsNullOrEmpty(poseSaveId)
|
|
? SanitizePoseKey(SceneManager.GetActiveScene().name + "_" + gameObject.name)
|
|
: SanitizePoseKey(poseSaveId);
|
|
return Path.Combine(PoseSaveDir, key + ".pose");
|
|
}
|
|
|
|
private static string SanitizePoseKey(string s)
|
|
{
|
|
foreach (char c in Path.GetInvalidFileNameChars()) s = s.Replace(c, '_');
|
|
return s.Replace(' ', '_');
|
|
}
|
|
|
|
[Serializable]
|
|
private class PoseState
|
|
{
|
|
public int version = PoseFileVersion;
|
|
public int index;
|
|
}
|
|
|
|
void Start() => RestorePoseState();
|
|
|
|
private void OnApplicationQuit() => SavePoseOnTeardown();
|
|
private void OnDestroy() => SavePoseOnTeardown();
|
|
|
|
/// <summary>Saves once during teardown (OnApplicationQuit and OnDestroy both call this; only the first writes).</summary>
|
|
private void SavePoseOnTeardown()
|
|
{
|
|
if (_poseSavedOnTeardown) return;
|
|
_poseSavedOnTeardown = true;
|
|
SavePoseState();
|
|
}
|
|
|
|
/// <summary>Writes the current pose index to disk.</summary>
|
|
public void SavePoseState()
|
|
{
|
|
if (!persistPose) return;
|
|
|
|
try
|
|
{
|
|
var state = new PoseState { index = _currentIndex };
|
|
Directory.CreateDirectory(PoseSaveDir);
|
|
File.WriteAllText(PoseSavePath(), JsonUtility.ToJson(state, true));
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[PlayerPoseController] Saving pose state failed: {e.Message}", this);
|
|
}
|
|
}
|
|
|
|
/// <summary>Restores the saved pose index, or does nothing if there is no save.</summary>
|
|
private void RestorePoseState()
|
|
{
|
|
if (!persistPose) return;
|
|
|
|
var state = LoadPoseState();
|
|
if (state == null) return;
|
|
SetPoseIndex(state.index);
|
|
}
|
|
|
|
private PoseState LoadPoseState()
|
|
{
|
|
string path = PoseSavePath();
|
|
if (!File.Exists(path)) return null;
|
|
|
|
try
|
|
{
|
|
var state = JsonUtility.FromJson<PoseState>(File.ReadAllText(path));
|
|
if (state == null || state.version != PoseFileVersion) return null;
|
|
return state;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[PlayerPoseController] Loading pose state failed: {e.Message}", this);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/// <summary>Deletes the saved pose state (next start uses the default pose). Editor convenience.</summary>
|
|
[ContextMenu("Clear Saved Pose State")]
|
|
public void ClearSavedPoseState()
|
|
{
|
|
try
|
|
{
|
|
string path = PoseSavePath();
|
|
if (File.Exists(path)) File.Delete(path);
|
|
Debug.Log($"[PlayerPoseController] Cleared saved pose state: {path}", this);
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
Debug.LogWarning($"[PlayerPoseController] Clearing saved pose state failed: {e.Message}", this);
|
|
}
|
|
}
|
|
}
|