Skip to content

Commit 487a43d

Browse files
committed
Variable System - Introduce CloningTools for deep cloning of variables
This commit adds a new CloningTools class to facilitate deep cloning of variable types, ensuring proper handling of value types, reference types, and collections. Additionally, it updates the ReferenceBase and VariableBaseSO classes to utilize the new cloning functionality for inlined values and default values.
1 parent 28925fe commit 487a43d

5 files changed

Lines changed: 135 additions & 5 deletions

File tree

Runtime/Base/ReferenceBase.cs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,14 @@
55
using System;
66
using UnityEngine.Events;
77
using Xprees.Core;
8+
using Xprees.Variables.Utils;
89

910
namespace Xprees.Variables.Base
1011
{
12+
/// <summary>
13+
/// Base class for all variable references used in the game. It can either use an inlined value or reference a VariableBaseSO Scriptable Object. The Value property abstracts this choice away, so users of ReferenceBase don't have to care about it.
14+
/// </summary>
15+
/// <typeparam name="T">Unity Serializable</typeparam>
1116
[Serializable]
1217
public class ReferenceBase<T> : IResettable
1318
{
@@ -83,14 +88,14 @@ public virtual void BackupStartState()
8388
{
8489
if (!useInlined) return; // Variables does that by themselves
8590

86-
_defaultInlinedValue = inlinedValue;
91+
_defaultInlinedValue = CloningTools.Clone(inlinedValue);
8792
}
8893

8994
public virtual void ResetState()
9095
{
9196
if (useInlined)
9297
{
93-
inlinedValue = _defaultInlinedValue;
98+
inlinedValue = CloningTools.Clone(_defaultInlinedValue);
9499
return;
95100
}
96101

Runtime/Base/VariableBaseSO.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using UnityEngine;
22
using UnityEngine.Events;
33
using Xprees.Core;
4+
using Xprees.Variables.Utils;
45

56
namespace Xprees.Variables.Base
67
{
@@ -22,7 +23,7 @@ private void OnEnable()
2223
/// Base class for all variables ScriptableObjects.
2324
/// Object holds the state on runtime, but reset every time OnEnable to defaultValue.
2425
/// </summary>
25-
/// <typeparam name="T">Unity Serializable</typeparam>
26+
/// <typeparam name="T">Unity Serializable or System.Serializable</typeparam>
2627
public class VariableBaseSO<T> : VariableBaseSO
2728
{
2829
[Tooltip("Value to which the variable will be reset on OnEnable or ResetState call.")]
@@ -64,7 +65,6 @@ public override void ResetState()
6465
ForceResetState();
6566
}
6667

67-
public override void ForceResetState() => CurrentValue = defaultValue;
68-
68+
public override void ForceResetState() => CurrentValue = CloningTools.Clone(defaultValue);
6969
}
7070
}

Runtime/Utils.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Runtime/Utils/CloningTools.cs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
using System;
2+
using System.Collections;
3+
using System.Runtime.CompilerServices;
4+
using UnityEngine;
5+
using UnityEngine.Scripting;
6+
using Object = UnityEngine.Object;
7+
8+
namespace Xprees.Variables.Utils
9+
{
10+
public static class CloningTools
11+
{
12+
/// <summary>
13+
/// Helper to clone a value of type T. It handles value types, reference types that implement ICloneable, and other reference types by serializing and deserializing them using Unity's JsonUtility. Note that the serialization approach only works for types that are compatible with JsonUtility (e.g., they must be Unity serializable). For more complex types, you might need to implement custom cloning logic or use a different serialization method.
14+
/// </summary>
15+
/// <param name="value">Value to clone</param>
16+
/// <typeparam name="T">primitive/value type/Unity serializable</typeparam>
17+
/// <returns>A clone of input value</returns>
18+
[Preserve]
19+
public static T Clone<T>(T value)
20+
{
21+
if (value == null) return default;
22+
23+
// Value types can be returned directly since they are copied by value. However, we can optimize for value types that do not contain references to avoid unnecessary cloning.
24+
var canReturnAsIs = typeof(T).IsValueType && !RuntimeHelpers.IsReferenceOrContainsReferences<T>();
25+
if (canReturnAsIs) return value;
26+
27+
if (value is ICloneable cloneable) return (T) cloneable.Clone();
28+
29+
// Handle collections (List<T>, arrays, etc.) - JsonUtility doesn't support them directly
30+
if (value is IList sourceList)
31+
{
32+
var listType = value.GetType();
33+
if (listType.IsArray)
34+
{
35+
var elementType = listType.GetElementType();
36+
var array = Array.CreateInstance(elementType!, sourceList.Count);
37+
for (var i = 0; i < sourceList.Count; i++)
38+
{
39+
var clonedItem = CloneItem(sourceList[i]);
40+
array.SetValue(clonedItem, i);
41+
}
42+
43+
return (T) (object) array;
44+
}
45+
46+
var clonedList = (IList) Activator.CreateInstance(listType);
47+
foreach (var item in sourceList)
48+
{
49+
clonedList.Add(CloneItem(item));
50+
}
51+
52+
return (T) clonedList;
53+
}
54+
55+
// For other types, we can try to serialize and deserialize to create a deep clone.
56+
// JsonUtility only supports a subset of types. Guard against unsupported types and failed serialization.
57+
var type = typeof(T);
58+
59+
// Allow UnityEngine.Object subclasses, or types explicitly marked as [Serializable].
60+
var isUnityObject = typeof(Object).IsAssignableFrom(type);
61+
var isSerializable = Attribute.IsDefined(type, typeof(SerializableAttribute));
62+
if (!isUnityObject && !isSerializable)
63+
{
64+
throw new InvalidOperationException(
65+
$"Type '{type.FullName}' is not supported for JsonUtility-based cloning. " +
66+
"Ensure the type is Unity-serializable (e.g., marked with [Serializable]) or implements ICloneable.");
67+
}
68+
69+
try
70+
{
71+
var json = JsonUtility.ToJson(value);
72+
if (string.IsNullOrEmpty(json) || json == "{}" || json == "null")
73+
{
74+
throw new InvalidOperationException(
75+
$"JsonUtility failed to serialize an instance of type '{type.FullName}' " +
76+
"for cloning. The resulting clone would be incomplete or empty.");
77+
}
78+
79+
return JsonUtility.FromJson<T>(json);
80+
}
81+
catch (Exception ex) when (ex is not InvalidOperationException)
82+
{
83+
throw new InvalidOperationException(
84+
$"AOT/IL2CPP error cloning type '{type.FullName}'. " +
85+
"Ensure the type is preserved and AOT-compatible.", ex);
86+
}
87+
}
88+
89+
/// Helper to clone individual items in collections, using the same logic as the main Clone method.
90+
private static object CloneItem(object item)
91+
{
92+
if (item == null) return null;
93+
94+
var itemType = item.GetType();
95+
96+
// Value types are copied by value
97+
if (itemType.IsValueType) return item;
98+
99+
// ICloneable
100+
if (item is ICloneable cloneable) return cloneable.Clone();
101+
102+
// UnityEngine.Object - return reference (can't deep clone ScriptableObjects easily)
103+
if (item is Object) return item;
104+
105+
// Try JSON serialization for other serializable types
106+
if (Attribute.IsDefined(itemType, typeof(SerializableAttribute)))
107+
{
108+
var json = JsonUtility.ToJson(item);
109+
if (!string.IsNullOrEmpty(json) && json != "{}" && json != "null")
110+
{
111+
return JsonUtility.FromJson(json, itemType);
112+
}
113+
}
114+
115+
// Fallback: return the same reference
116+
return item;
117+
}
118+
}
119+
}

Runtime/Utils/CloningTools.cs.meta

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)