cz.xprees.core 1.0.39 → 1.0.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Editor/StatePlayModeLifecycleHook.cs +78 -0
- package/Editor/StatePlayModeLifecycleHook.cs.meta +3 -0
- package/Editor/cz.xprees.core.editor.asmdef +1 -1
- package/README.md +80 -7
- package/Runtime/ResetOnPlayModeAttribute.cs +33 -0
- package/Runtime/ResetOnPlayModeAttribute.cs.meta +3 -0
- package/Runtime/StateSnapshotService.cs +31 -35
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Concurrent;
|
|
3
|
+
using System.Reflection;
|
|
4
|
+
using UnityEditor;
|
|
5
|
+
using UnityEngine;
|
|
6
|
+
|
|
7
|
+
namespace Xprees.Core.Editor
|
|
8
|
+
{
|
|
9
|
+
/// Centralized Editor runner that coordinates Play Mode entry and exit transitions for stateful ScriptableObjects.
|
|
10
|
+
/// Resets objects marked with [ResetOnPlayMode] and restores mutated assets upon exiting Play Mode.
|
|
11
|
+
/// Can be migrated to future Unity lifecycle hooks in future versions.
|
|
12
|
+
[InitializeOnLoad]
|
|
13
|
+
public static class StatePlayModeLifecycleHook
|
|
14
|
+
{
|
|
15
|
+
private readonly static ConcurrentDictionary<Type, PlayModeResetTiming> timingCache = new();
|
|
16
|
+
|
|
17
|
+
static StatePlayModeLifecycleHook()
|
|
18
|
+
{
|
|
19
|
+
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
|
20
|
+
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private static void OnPlayModeStateChanged(PlayModeStateChange change)
|
|
24
|
+
{
|
|
25
|
+
switch (change)
|
|
26
|
+
{
|
|
27
|
+
case PlayModeStateChange.EnteredPlayMode:
|
|
28
|
+
ResetPlayModeObjects(PlayModeResetTiming.EnterPlayMode);
|
|
29
|
+
break;
|
|
30
|
+
|
|
31
|
+
case PlayModeStateChange.ExitingPlayMode:
|
|
32
|
+
// Automatically revert all mutated ScriptableObjects to baseline before returning to Edit Mode
|
|
33
|
+
StateSnapshotService.RestoreAll();
|
|
34
|
+
ResetPlayModeObjects(PlayModeResetTiming.ExitPlayMode);
|
|
35
|
+
break;
|
|
36
|
+
|
|
37
|
+
case PlayModeStateChange.EnteredEditMode:
|
|
38
|
+
StateSnapshotService.ClearAll();
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Resets loaded ScriptableObjects decorated with [ResetOnPlayMode] matching the specified timing.
|
|
44
|
+
/// Respects StateLifetime (skips StateLifetime.Persistent).
|
|
45
|
+
public static void ResetPlayModeObjects(PlayModeResetTiming timing)
|
|
46
|
+
{
|
|
47
|
+
var loadedObjects = Resources.FindObjectsOfTypeAll<ScriptableObject>();
|
|
48
|
+
foreach (var so in loadedObjects)
|
|
49
|
+
{
|
|
50
|
+
if (!so || so.IsStateless() || so.GetStateLifetime() == StateLifetime.Persistent) continue;
|
|
51
|
+
|
|
52
|
+
var type = so.GetType();
|
|
53
|
+
var configuredTiming = GetPlayModeTiming(type);
|
|
54
|
+
if ((configuredTiming & timing) == 0) continue;
|
|
55
|
+
|
|
56
|
+
var resetMethod = type.GetMethod("ResetToDefault", BindingFlags.Public | BindingFlags.Instance);
|
|
57
|
+
if (resetMethod != null)
|
|
58
|
+
{
|
|
59
|
+
resetMethod.Invoke(so, null);
|
|
60
|
+
}
|
|
61
|
+
else if (so is IRuntimeStateOwner stateOwner)
|
|
62
|
+
{
|
|
63
|
+
stateOwner.ClearTransientState();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public static PlayModeResetTiming GetPlayModeTiming(Type type)
|
|
69
|
+
{
|
|
70
|
+
if (timingCache.TryGetValue(type, out var timing)) return timing;
|
|
71
|
+
|
|
72
|
+
var attr = type.GetCustomAttribute<ResetOnPlayModeAttribute>(true);
|
|
73
|
+
timing = attr?.Timing ?? PlayModeResetTiming.None;
|
|
74
|
+
timingCache[type] = timing;
|
|
75
|
+
return timing;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
package/README.md
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
|
-
# Unity Core classes
|
|
1
|
+
# Unity Core classes - cz.xprees.core
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/cz.xprees.core)
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
This is base package containing a few common classes for all of my Unity projects.
|
|
5
|
+
Base package containing core systems, state lifecycle orchestration, and ScriptableObject utilities for Unity projects.
|
|
7
6
|
|
|
8
7
|
## Installation
|
|
9
8
|
|
|
10
|
-
Install the package using npm scoped registry in `Project Settings > Package Manager > Scoped Registries
|
|
11
|
-
|
|
12
|
-
[Unity Docs - Install a UPM package from a Git URL](https://docs.unity3d.com/6000.1/Documentation/Manual/upm-ui-giturl.html)
|
|
9
|
+
Install the package using npm scoped registry in `Project Settings > Package Manager > Scoped Registries`:
|
|
13
10
|
|
|
14
11
|
```json
|
|
15
12
|
{
|
|
@@ -19,7 +16,83 @@ Install the package using npm scoped registry in `Project Settings > Package Man
|
|
|
19
16
|
"cz.xprees"
|
|
20
17
|
]
|
|
21
18
|
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then install `cz.xprees.core` via the Unity Package Manager.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## State Management & Lifecycle Architecture
|
|
26
|
+
|
|
27
|
+
The package provides automated, zero-boilerplate state snapshotting and restoration for ScriptableObjects across scenario boundaries and Editor play
|
|
28
|
+
mode transitions.
|
|
29
|
+
|
|
30
|
+
### 1. State Lifetime (`StateLifetime`)
|
|
31
|
+
|
|
32
|
+
Every stateful ScriptableObject inherits from `DescriptionBaseSO` or declares its lifecycle scope via `[StatefulLifetime]`:
|
|
33
|
+
|
|
34
|
+
- **`StateLifetime.Scenario` (Default)**:
|
|
35
|
+
Scoped strictly to the active scenario. Captured before mutations and automatically restored to its baseline on scenario start/restart.
|
|
36
|
+
- **`StateLifetime.Session`**:
|
|
37
|
+
Persists across scenarios throughout a play session. Restored only on game/session boundaries or play mode exit.
|
|
38
|
+
- **`StateLifetime.Persistent`**:
|
|
39
|
+
Excluded from state snapshots. Ideal for immutable databases, user settings, or content saved externally to disk.
|
|
40
|
+
|
|
41
|
+
```csharp
|
|
42
|
+
// Per-instance: Set the "Lifetime" dropdown in the Inspector.
|
|
43
|
+
// Per-class: Override via attribute:
|
|
44
|
+
[StatefulLifetime(StateLifetime.Persistent)]
|
|
45
|
+
public class GlobalPlayerConfigSO : DescriptionBaseSO { }
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. State Snapshot Service (`StateSnapshotService`)
|
|
49
|
+
|
|
50
|
+
A centralized, pure runtime service that handles:
|
|
51
|
+
|
|
52
|
+
- **`EnsureCaptured(ScriptableObject target)`**: Serializes the baseline JSON state prior to the first mutation.
|
|
53
|
+
- **`Restore(ScriptableObject target)`**: Reverts serialized fields in-place from baseline JSON and triggers
|
|
54
|
+
`IRuntimeStateOwner.ClearTransientState()`.
|
|
55
|
+
- **`RestoreAll(IEnumerable<ScriptableObject> targets, StateLifetime maxLifetime)`**: Multi-pass restoration that ensures all targets are
|
|
56
|
+
baseline-captured before restoring, preventing inter-object dependency races.
|
|
57
|
+
- **`[SnapshotIgnore]`**: Decorate fields to preserve their values across snapshot restorations.
|
|
22
58
|
|
|
59
|
+
### 3. Editor PlayMode Lifecycle (`StatePlayModeLifecycleHook`)
|
|
60
|
+
|
|
61
|
+
Located in `cz.xprees.core.editor`, this centralized runner hooks into `EditorApplication.playModeStateChanged`:
|
|
62
|
+
|
|
63
|
+
- **`EnteredPlayMode`**: Resets stateful objects decorated with `[ResetOnPlayMode(PlayModeResetTiming.EnterPlayMode)]` (skips `Persistent` objects).
|
|
64
|
+
- **`ExitingPlayMode`**: Restores all mutated ScriptableObjects in-place back to their baseline via `StateSnapshotService.RestoreAll()` and clears
|
|
65
|
+
editor dirty flags before returning to edit mode.
|
|
66
|
+
- **`EnteredEditMode`**: Purges snapshot dictionaries.
|
|
67
|
+
|
|
68
|
+
### 4. Play Mode Reset Attribute (`ResetOnPlayModeAttribute`)
|
|
69
|
+
|
|
70
|
+
Control Play Mode transition resets declaratively on classes:
|
|
71
|
+
|
|
72
|
+
```csharp
|
|
73
|
+
[Flags]
|
|
74
|
+
public enum PlayModeResetTiming
|
|
75
|
+
{
|
|
76
|
+
None = 0,
|
|
77
|
+
EnterPlayMode = 1 << 0,
|
|
78
|
+
ExitPlayMode = 1 << 1,
|
|
79
|
+
Both = EnterPlayMode | ExitPlayMode
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Example: opt-out of play mode reset
|
|
83
|
+
[ResetOnPlayMode(PlayModeResetTiming.None)]
|
|
84
|
+
public class PersistentSettingsSO : ScriptableObject { }
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 5. Runtime State Ownership (`IRuntimeStateOwner`)
|
|
88
|
+
|
|
89
|
+
Implement `IRuntimeStateOwner` on objects with non-serialized runtime state (active handles, listeners, temporary collections):
|
|
90
|
+
|
|
91
|
+
```csharp
|
|
92
|
+
public interface IRuntimeStateOwner
|
|
93
|
+
{
|
|
94
|
+
void ClearTransientState();
|
|
95
|
+
}
|
|
23
96
|
```
|
|
24
97
|
|
|
25
|
-
|
|
98
|
+
Invoked automatically after serialized fields are restored by `StateSnapshotService`.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
|
|
3
|
+
namespace Xprees.Core
|
|
4
|
+
{
|
|
5
|
+
[Flags]
|
|
6
|
+
public enum PlayModeResetTiming
|
|
7
|
+
{
|
|
8
|
+
/// No reset will occur on Play Mode transitions
|
|
9
|
+
None = 0,
|
|
10
|
+
|
|
11
|
+
/// Reset state when entering Play Mode
|
|
12
|
+
EnterPlayMode = 1 << 0,
|
|
13
|
+
|
|
14
|
+
/// Reset state when exiting Play Mode
|
|
15
|
+
ExitPlayMode = 1 << 1,
|
|
16
|
+
|
|
17
|
+
/// Reset state on both entering and exiting Play Mode
|
|
18
|
+
Both = EnterPlayMode | ExitPlayMode,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Controls whether and when a stateful ScriptableObject (such as a Variable) resets to its authored default value during Editor Play Mode transitions.
|
|
22
|
+
/// Hooked and executed centrally by StateSnapshotService.
|
|
23
|
+
[AttributeUsage(AttributeTargets.Class, Inherited = true)]
|
|
24
|
+
public sealed class ResetOnPlayModeAttribute : Attribute
|
|
25
|
+
{
|
|
26
|
+
public PlayModeResetTiming Timing { get; }
|
|
27
|
+
|
|
28
|
+
public ResetOnPlayModeAttribute(PlayModeResetTiming timing = PlayModeResetTiming.EnterPlayMode)
|
|
29
|
+
{
|
|
30
|
+
Timing = timing;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
using System;
|
|
2
2
|
using System.Collections.Concurrent;
|
|
3
3
|
using System.Collections.Generic;
|
|
4
|
+
using System.Linq;
|
|
4
5
|
using System.Reflection;
|
|
5
6
|
using UnityEngine;
|
|
6
7
|
using Object = UnityEngine.Object;
|
|
@@ -12,8 +13,9 @@ namespace Xprees.Core
|
|
|
12
13
|
{
|
|
13
14
|
// TODO later consider adding limits to not blow up memory - probably not an issue
|
|
14
15
|
// TODO consider using OdinSerializer for more robust serialization in future
|
|
16
|
+
|
|
15
17
|
/// Core engine service for zero-boilerplate capturing and in-place restoration
|
|
16
|
-
/// of ScriptableObject serialized state
|
|
18
|
+
/// of ScriptableObject serialized state across scenario and session boundaries.
|
|
17
19
|
public static class StateSnapshotService
|
|
18
20
|
{
|
|
19
21
|
private readonly static Dictionary<int, string> snapshots = new();
|
|
@@ -99,6 +101,33 @@ namespace Xprees.Core
|
|
|
99
101
|
}
|
|
100
102
|
}
|
|
101
103
|
|
|
104
|
+
/// Restores a collection of ScriptableObjects to their baseline state up to the specified max lifetime.
|
|
105
|
+
/// Multi-pass restoration:
|
|
106
|
+
/// Pass 1 captures the baseline for any uncaptured objects before modifying state.
|
|
107
|
+
/// Pass 2 restores serialized fields and triggers IRuntimeStateOwner.ClearTransientState.
|
|
108
|
+
public static void RestoreAll(IEnumerable<ScriptableObject> targets, StateLifetime maxLifetime = StateLifetime.Scenario)
|
|
109
|
+
{
|
|
110
|
+
if (targets == null) return;
|
|
111
|
+
|
|
112
|
+
var targetsList = targets as ScriptableObject[] ?? targets.ToArray();
|
|
113
|
+
|
|
114
|
+
// Pass 1: Ensure baseline is captured for ALL objects before any object is restored
|
|
115
|
+
foreach (var so in targetsList)
|
|
116
|
+
{
|
|
117
|
+
if (!so || so.IsStateless() || so.GetStateLifetime() > maxLifetime) continue;
|
|
118
|
+
|
|
119
|
+
EnsureCaptured(so);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Pass 2: Restore all objects to baseline (and clear transient state via Restore)
|
|
123
|
+
foreach (var so in targetsList)
|
|
124
|
+
{
|
|
125
|
+
if (!so || so.IsStateless() || so.GetStateLifetime() > maxLifetime) continue;
|
|
126
|
+
|
|
127
|
+
Restore(so);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
102
131
|
/// Restores all currently tracked ScriptableObjects to their baseline state.
|
|
103
132
|
public static int RestoreAll()
|
|
104
133
|
{
|
|
@@ -236,39 +265,6 @@ namespace Xprees.Core
|
|
|
236
265
|
/// SubsystemRegistration executes before any Awake/OnEnable calls when entering Play Mode,
|
|
237
266
|
/// ensuring static state dictionaries from previous play sessions or editor runs are completely purged.
|
|
238
267
|
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
|
|
239
|
-
private static void
|
|
240
|
-
|
|
241
|
-
#if UNITY_EDITOR
|
|
242
|
-
[InitializeOnLoadMethod]
|
|
243
|
-
private static void InitEditorLifecycle()
|
|
244
|
-
{
|
|
245
|
-
EditorApplication.playModeStateChanged -= OnPlayModeStateChanged;
|
|
246
|
-
EditorApplication.playModeStateChanged += OnPlayModeStateChanged;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
private static void OnPlayModeStateChanged(PlayModeStateChange change)
|
|
250
|
-
{
|
|
251
|
-
// When exiting Play Mode, restore before scene objects begin tearing down
|
|
252
|
-
if (change == PlayModeStateChange.ExitingPlayMode)
|
|
253
|
-
{
|
|
254
|
-
RestoreAll();
|
|
255
|
-
return;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Once fully transitioned back to Edit Mode, all scene unload and OnDisable/OnDestroy
|
|
259
|
-
// lifecycle calls have finished. Re-run RestoreAll() to guarantee any mutations occurring
|
|
260
|
-
// during teardown are cleanly reverted, then clear the snapshot tracking.
|
|
261
|
-
if (change == PlayModeStateChange.EnteredEditMode)
|
|
262
|
-
{
|
|
263
|
-
var count = RestoreAll();
|
|
264
|
-
if (count > 0)
|
|
265
|
-
{
|
|
266
|
-
Debug.Log($"[StateSnapshotService] Restored {count} stateful ScriptableObjects back to pre-play baseline.");
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
ClearAll();
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
#endif
|
|
268
|
+
private static void ResetStaticFields() => ClearAll();
|
|
273
269
|
}
|
|
274
270
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cz.xprees.core",
|
|
3
3
|
"displayName": "Core Classes",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.40",
|
|
5
5
|
"unity": "2021.3",
|
|
6
6
|
"description": "This package contains core classes and interfaces for Unity projects. It is intended to be used as a dependency for other packages. By providing base classes in a separate package, you can easily update them across all your projects.",
|
|
7
7
|
"license": "Apache-2.0",
|