com.wallstop-studios.unity-helpers 2.0.0-rc24 → 2.0.0-rc26

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/README.md CHANGED
@@ -10,3 +10,91 @@ Various Unity Helpers. Includes many deterministic, seedable random number gener
10
10
  - URL: `https://registry.npmjs.org`
11
11
  - Scope(s): `com.wallstop-studios.unity-helpers`
12
12
  5. Resolve the latest `com.wallstop-studios.unity-helpers`
13
+
14
+ # Package Contents
15
+ - Random Number Generators
16
+ - Spatial Trees
17
+ - Protobuf, Binary, and JSON formatting
18
+ - A resizable CyclicBuffer
19
+ - Simple single-threaded thread pool
20
+ - A LayeredImage for use with Unity's UI Toolkit
21
+ - Geometry Helpers
22
+ - Child/Parent/Sibling Attributes to automatically get components
23
+ - ReadOnly attribute to disable editing of serialized properties in the inspector
24
+ - An extensive collection of helpers
25
+ - Simple math functions including a generic Range
26
+ - Common buffers to reduce allocations
27
+ - A RuntimeSingleton implementation for automatic creation/accessing of singletons
28
+ - String helpers, like converting to PascalCase, like Unity does for variable names in the inspector
29
+ - A randomizable PerlinNoise implementation
30
+ - And more!
31
+
32
+ # Random Number Generators
33
+ This package implements several high quality, seedable, and serializable random number generators. The best one (currently) is the PCG Random. This has been hidden behind the `PRNG.Instance` class, which is thread-safe. As the package evolves, the exact implementation of this may change.
34
+
35
+ All of these implement a custom [IRandom](./Runtime/Core/Random/IRandom.cs) interface with a significantly richer suite of methods than the standard .Net and Unity randoms offer.
36
+
37
+ To use:
38
+
39
+ ```csharp
40
+ IRandom random = PRNG.Instance;
41
+
42
+ random.NextFloat(); // Something between 0.0f and 1.0f
43
+ random.NextDouble(); // Something between 0.0d and 1.0d
44
+ random.Next(); // Something between 0 and int.MaxValue
45
+ random.NextUint(); // Something between 0 and uint.MaxValue
46
+
47
+ int [] values = {1, 2, 3};
48
+ random.NextOf(values); // 1, 2, or 3
49
+ random.NextOf(Enumerable.Range(0, 3)); // 1, 2, or 3
50
+ HashSet<int> setValues = new() {1, 2, 3};
51
+ random.NextOf(setValues); // 1, 2, or 3
52
+
53
+ random.NextGuid(); // A valid UUIDv4
54
+ random.NextGaussian(); // A value sampled from a gaussian curve around mean=0, stdDev=1 (configurable via parameters)
55
+ random.NextEnum<T>(); // A randomly selected enum of type T
56
+
57
+ int width = 100;
58
+ int height = 100;
59
+ random.NextNoiseMap(width, height); // A configurable noise map generated using random octave offsets
60
+ ```
61
+
62
+ ## Implemented Random Number Generators
63
+ - PCG
64
+ - DotNet (uses the currently implemented .Net Random)
65
+ - RomoDuo
66
+ - SplitMix64
67
+ - Squirrel
68
+ - System (uses a port of the Windows .Net Random)
69
+ - Unity (uses Unity's random under the hood)
70
+ - Wy
71
+ - XorShift
72
+ - XorShiro
73
+
74
+ # Spatial Trees
75
+ There are three implemented 2D immutable spatial trees that can store generic objects, as long as there is some resolution function that can convert them into Vector2 spatial positions.
76
+
77
+ - QuadTree (easiest to use)
78
+ - KDTree
79
+ - RTree
80
+
81
+ Spatial trees, after construction, allow for O(log(n)) spatial query time instead of O(n). They are extremely useful if you need repeated spatial queries, or if you have relatively static spatial data.
82
+
83
+ ## Usage
84
+
85
+ ```csharp
86
+ GameObject [] spatialStorage = { myCoolGameObject };
87
+ QuadTree<GameObject> quadTree = new(spatialStorage, go => go.transform.position);
88
+
89
+ // Might return your object, might not
90
+ GameObject [] inBounds = quadTree.GetElementsInBounds(new Bounds(0, 0, 100, 100));
91
+
92
+ // Uses a "good-enough" nearest-neighbor approximately for cheap neighbors
93
+ List<GameObject> nearestNeighbors = new();
94
+ quadTree.GetApproximateNearestNeighbors(myCoolGameObject.transform.position, 1, nearestNeighbors);
95
+ Assert.AreEqual(1, nearestNeighbors.Count);
96
+ Assert.AreEqual(myCoolGameObject, nearestNeighbors[0]);
97
+ ```
98
+
99
+ ## Note
100
+ All spatial trees expect the positional data to be *immutable*. It is very important that the positions do not change. If they do, you will need to reconstruct the tree.
@@ -4,16 +4,16 @@
4
4
  using System.Collections;
5
5
  using System.Collections.Generic;
6
6
  using System.Linq;
7
+ using Extension;
7
8
  using Helper;
8
9
 
9
10
  [Serializable]
10
11
  public sealed class CyclicBuffer<T> : IReadOnlyList<T>
11
12
  {
13
+ public int Capacity { get; private set; }
12
14
  public int Count { get; private set; }
13
15
  public bool IsReadOnly => false;
14
16
 
15
- public readonly int capacity;
16
-
17
17
  private readonly List<T> _buffer;
18
18
  private int _position;
19
19
 
@@ -38,7 +38,7 @@
38
38
  throw new ArgumentException(nameof(capacity));
39
39
  }
40
40
 
41
- this.capacity = capacity;
41
+ Capacity = capacity;
42
42
  _position = 0;
43
43
  Count = 0;
44
44
  _buffer = new List<T>();
@@ -64,7 +64,7 @@
64
64
 
65
65
  public void Add(T item)
66
66
  {
67
- if (capacity == 0)
67
+ if (Capacity == 0)
68
68
  {
69
69
  return;
70
70
  }
@@ -78,8 +78,8 @@
78
78
  _buffer.Add(item);
79
79
  }
80
80
 
81
- _position = _position.WrappedIncrement(capacity);
82
- if (Count < capacity)
81
+ _position = _position.WrappedIncrement(Capacity);
82
+ if (Count < Capacity)
83
83
  {
84
84
  ++Count;
85
85
  }
@@ -93,6 +93,31 @@
93
93
  _buffer.Clear();
94
94
  }
95
95
 
96
+ public void Resize(int newCapacity)
97
+ {
98
+ if (newCapacity == Capacity)
99
+ {
100
+ return;
101
+ }
102
+
103
+ if (newCapacity < 0)
104
+ {
105
+ throw new ArgumentException(nameof(newCapacity));
106
+ }
107
+
108
+ int oldCapacity = Capacity;
109
+ Capacity = newCapacity;
110
+ _buffer.Shift(-_position);
111
+ if (newCapacity < _buffer.Count)
112
+ {
113
+ _buffer.RemoveRange(newCapacity, _buffer.Count - newCapacity);
114
+ }
115
+
116
+ _position =
117
+ newCapacity < oldCapacity && newCapacity <= _buffer.Count ? 0 : _buffer.Count;
118
+ Count = Math.Min(newCapacity, Count);
119
+ }
120
+
96
121
  public bool Contains(T item)
97
122
  {
98
123
  return _buffer.Contains(item);
@@ -100,7 +125,11 @@
100
125
 
101
126
  private int AdjustedIndexFor(int index)
102
127
  {
103
- long longCapacity = capacity;
128
+ long longCapacity = Capacity;
129
+ if (longCapacity == 0L)
130
+ {
131
+ return 0;
132
+ }
104
133
  unchecked
105
134
  {
106
135
  int adjustedIndex = (int)(
@@ -1,6 +1,8 @@
1
1
  namespace UnityHelpers.Core.Extension
2
2
  {
3
+ using System;
3
4
  using System.Collections.Generic;
5
+ using Helper;
4
6
  using Random;
5
7
 
6
8
  public static class IListExtensions
@@ -26,6 +28,44 @@
26
28
  }
27
29
  }
28
30
 
31
+ public static void Shift<T>(this IList<T> list, int amount)
32
+ {
33
+ int count = list.Count;
34
+ if (count <= 1)
35
+ {
36
+ return;
37
+ }
38
+
39
+ amount = amount.PositiveMod(count);
40
+ if (amount == 0)
41
+ {
42
+ return;
43
+ }
44
+
45
+ Reverse(list, 0, count - 1);
46
+ Reverse(list, 0, amount - 1);
47
+ Reverse(list, amount, count - 1);
48
+ }
49
+
50
+ public static void Reverse<T>(this IList<T> list, int start, int end)
51
+ {
52
+ if (start < 0 || list.Count <= start)
53
+ {
54
+ throw new ArgumentException(nameof(start));
55
+ }
56
+ if (end < 0 || list.Count <= end)
57
+ {
58
+ throw new ArgumentException(nameof(end));
59
+ }
60
+
61
+ while (start < end)
62
+ {
63
+ (list[start], list[end]) = (list[end], list[start]);
64
+ start++;
65
+ end--;
66
+ }
67
+ }
68
+
29
69
  public static void RemoveAtSwapBack<T>(this IList<T> list, int index)
30
70
  {
31
71
  if (list.Count <= 1)
@@ -31,6 +31,34 @@
31
31
  );
32
32
  }
33
33
 
34
+ public static float PositiveMod(this float value, float max)
35
+ {
36
+ value %= max;
37
+ value += max;
38
+ return value % max;
39
+ }
40
+
41
+ public static double PositiveMod(this double value, double max)
42
+ {
43
+ value %= max;
44
+ value += max;
45
+ return value % max;
46
+ }
47
+
48
+ public static int PositiveMod(this int value, int max)
49
+ {
50
+ value %= max;
51
+ value += max;
52
+ return value % max;
53
+ }
54
+
55
+ public static long PositiveMod(this long value, long max)
56
+ {
57
+ value %= max;
58
+ value += max;
59
+ return value % max;
60
+ }
61
+
34
62
  public static int WrappedAdd(this int value, int increment, int max)
35
63
  {
36
64
  WrappedAdd(ref value, increment, max);
@@ -32,7 +32,7 @@
32
32
  {
33
33
  int capacity = PRNG.Instance.Next(1, int.MaxValue);
34
34
  CyclicBuffer<int> buffer = new(capacity);
35
- Assert.AreEqual(capacity, buffer.capacity);
35
+ Assert.AreEqual(capacity, buffer.Capacity);
36
36
  }
37
37
  }
38
38
 
@@ -74,7 +74,7 @@
74
74
  if (!expected.SequenceEqual(buffer))
75
75
  {
76
76
  Assert.Fail(
77
- $"Failure at iteration {i}, capacity={buffer.capacity}, "
77
+ $"Failure at iteration {i}, capacity={buffer.Capacity}, "
78
78
  + $"capacityMultiplier={CapacityMultiplier}\n"
79
79
  + $"Expected: [{string.Join(",", expected)}], Actual: [{string.Join(",", buffer)}]"
80
80
  );
@@ -168,7 +168,7 @@
168
168
  if (!expected.SequenceEqual(buffer))
169
169
  {
170
170
  Assert.Fail(
171
- $"Failure at iteration {i}, j={j}, capacity={buffer.capacity}, "
171
+ $"Failure at iteration {i}, j={j}, capacity={buffer.Capacity}, "
172
172
  + $"capacityMultiplier={CapacityMultiplier}\n"
173
173
  + $"Expected: [{string.Join(",", expected)}], Actual: [{string.Join(",", buffer)}]"
174
174
  );
@@ -190,14 +190,18 @@
190
190
  [Test]
191
191
  public void ClearOk()
192
192
  {
193
+ HashSet<int> seen = new();
193
194
  for (int i = 0; i < NumTries; ++i)
194
195
  {
195
196
  int capacity = PRNG.Instance.Next(100, 1_000);
196
197
  CyclicBuffer<int> buffer = new(capacity);
197
198
  float fillPercent = PRNG.Instance.NextFloat(0.5f, 1.5f);
199
+ seen.Clear();
198
200
  for (int j = 0; j < capacity * fillPercent; ++j)
199
201
  {
200
- buffer.Add(PRNG.Instance.Next());
202
+ int value = PRNG.Instance.Next();
203
+ seen.Add(value);
204
+ buffer.Add(value);
201
205
  }
202
206
 
203
207
  Assert.AreNotEqual(0, buffer.Count);
@@ -205,8 +209,115 @@
205
209
  buffer.Clear();
206
210
 
207
211
  Assert.AreEqual(0, buffer.Count);
208
- Assert.AreEqual(capacity, buffer.capacity);
212
+ Assert.AreEqual(capacity, buffer.Capacity);
209
213
  Assert.IsTrue(Array.Empty<int>().SequenceEqual(buffer));
214
+
215
+ // Make sure our data is actually cleaned up, none of our input data should be "Contained"
216
+ foreach (int value in seen)
217
+ {
218
+ Assert.IsFalse(buffer.Contains(value));
219
+ }
220
+ }
221
+ }
222
+
223
+ [Test]
224
+ public void ResizeFullOk()
225
+ {
226
+ for (int i = 0; i < NumTries; ++i)
227
+ {
228
+ int capacity = PRNG.Instance.Next(100, 1_000);
229
+ CyclicBuffer<int> buffer = new(capacity);
230
+ float fillPercent = PRNG.Instance.NextFloat(1f, 2f);
231
+ float capacityPercent = PRNG.Instance.NextFloat(0.3f, 0.9f);
232
+ for (int j = 0; j < capacity * fillPercent; ++j)
233
+ {
234
+ int value = PRNG.Instance.Next();
235
+ buffer.Add(value);
236
+ }
237
+
238
+ int[] values = buffer.ToArray();
239
+
240
+ int newCapacity = Math.Max(0, (int)(capacity * capacityPercent));
241
+ buffer.Resize(newCapacity);
242
+ int[] newValues = buffer.ToArray();
243
+ Assert.AreEqual(newCapacity, buffer.Capacity);
244
+ Assert.AreEqual(newCapacity, newValues.Length);
245
+ Assert.That(values.Take(newCapacity), Is.EqualTo(newValues));
246
+
247
+ buffer.Add(1);
248
+ buffer.Add(2);
249
+ int[] afterAddition = buffer.ToArray();
250
+ Assert.That(afterAddition, Is.EqualTo(newValues.Skip(2).Concat(new[] { 1, 2 })));
251
+
252
+ newCapacity = 0;
253
+ buffer.Resize(newCapacity);
254
+ newValues = buffer.ToArray();
255
+ Assert.AreEqual(newCapacity, buffer.Capacity);
256
+ Assert.AreEqual(newCapacity, newValues.Length);
257
+ }
258
+ }
259
+
260
+ [Test]
261
+ public void ResizePartialOk()
262
+ {
263
+ for (int i = 0; i < NumTries; ++i)
264
+ {
265
+ int capacity = PRNG.Instance.Next(100, 1_000);
266
+ CyclicBuffer<int> buffer = new(capacity);
267
+ float fillPercent = PRNG.Instance.NextFloat(0.3f, 0.9f);
268
+ float capacityPercent = PRNG.Instance.NextFloat(0.3f, 0.9f);
269
+ int filled = (int)(capacity * fillPercent);
270
+ for (int j = 0; j < filled; ++j)
271
+ {
272
+ int value = PRNG.Instance.Next();
273
+ buffer.Add(value);
274
+ }
275
+
276
+ int[] values = buffer.ToArray();
277
+
278
+ int newCapacity = Math.Max(0, (int)(capacity * capacityPercent));
279
+ buffer.Resize(newCapacity);
280
+ int[] newValues = buffer.ToArray();
281
+ Assert.AreEqual(newCapacity, buffer.Capacity);
282
+ Assert.AreEqual(Math.Min(filled, newCapacity), newValues.Length);
283
+ Assert.That(values.Take(newCapacity), Is.EqualTo(newValues));
284
+
285
+ buffer.Add(1);
286
+ buffer.Add(2);
287
+ int[] afterAddition = buffer.ToArray();
288
+ if (newCapacity <= filled)
289
+ {
290
+ Assert.That(
291
+ afterAddition,
292
+ Is.EqualTo(newValues.Skip(2).Concat(new[] { 1, 2 })),
293
+ $"Resize failed for iteration {i}, fillPercent {fillPercent:0.00}, capacityPercent: {capacityPercent:0.00}. "
294
+ + $"Capacity: {capacity}, newCapacity: {newCapacity}, filled: {filled}."
295
+ );
296
+ }
297
+ else if (newCapacity == filled + 1)
298
+ {
299
+ Assert.That(
300
+ afterAddition,
301
+ Is.EqualTo(newValues.Skip(1).Concat(new[] { 1, 2 })),
302
+ $"Resize failed for iteration {i}, fillPercent {fillPercent:0.00}, capacityPercent: {capacityPercent:0.00}. "
303
+ + $"Capacity: {capacity}, newCapacity: {newCapacity}, filled: {filled}."
304
+ );
305
+ }
306
+ else
307
+ {
308
+ Assert.That(
309
+ afterAddition,
310
+ Is.EqualTo(newValues.Concat(new[] { 1, 2 })),
311
+ $"Resize failed for iteration {i}, fillPercent {fillPercent:0.00}, capacityPercent: {capacityPercent:0.00}. "
312
+ + $"Capacity: {capacity}, newCapacity: {newCapacity}, filled: {filled}."
313
+ );
314
+ }
315
+
316
+ newCapacity = 0;
317
+ buffer.Resize(newCapacity);
318
+ newValues = buffer.ToArray();
319
+ Assert.AreEqual(newCapacity, buffer.Capacity);
320
+ Assert.AreEqual(newCapacity, newValues.Length);
210
321
  }
211
322
  }
212
323
  }
@@ -0,0 +1,76 @@
1
+ namespace UnityHelpers.Tests.Extensions
2
+ {
3
+ using System;
4
+ using System.Linq;
5
+ using Core.Extension;
6
+ using NUnit.Framework;
7
+
8
+ public sealed class IListExtensionTests
9
+ {
10
+ [Test]
11
+ public void ShiftLeft()
12
+ {
13
+ int[] input = Enumerable.Range(0, 10).ToArray();
14
+ for (int i = 0; i < input.Length * 2; ++i)
15
+ {
16
+ int[] shifted = input.ToArray();
17
+ shifted.Shift(-1 * i);
18
+ Assert.That(
19
+ input.Skip(i % input.Length).Concat(input.Take(i % input.Length)),
20
+ Is.EqualTo(shifted)
21
+ );
22
+ }
23
+ }
24
+
25
+ [Test]
26
+ public void ShiftRight()
27
+ {
28
+ int[] input = Enumerable.Range(0, 10).ToArray();
29
+ for (int i = 0; i < input.Length * 2; ++i)
30
+ {
31
+ int[] shifted = input.ToArray();
32
+ shifted.Shift(i);
33
+ Assert.That(
34
+ input
35
+ .Skip((input.Length * 3 - i) % input.Length)
36
+ .Concat(input.Take((input.Length * 3 - i) % input.Length)),
37
+ Is.EqualTo(shifted),
38
+ $"Shift failed for amount {i}."
39
+ );
40
+ }
41
+ }
42
+
43
+ [Test]
44
+ public void Reverse()
45
+ {
46
+ int[] input = Enumerable.Range(0, 10).ToArray();
47
+ for (int i = 0; i < input.Length; ++i)
48
+ {
49
+ int[] shifted = input.ToArray();
50
+ shifted.Reverse(0, i);
51
+ Assert.That(
52
+ input.Take(i + 1).Reverse().Concat(input.Skip(i + 1)),
53
+ Is.EqualTo(shifted),
54
+ $"Reverse failed for reversal from [0, {i}]."
55
+ );
56
+ }
57
+
58
+ // TODO
59
+ }
60
+
61
+ [Test]
62
+ public void ReverseInvalidArguments()
63
+ {
64
+ int[] input = Enumerable.Range(0, 10).ToArray();
65
+ Assert.Throws<ArgumentException>(() => input.Reverse(-1, 1));
66
+ Assert.Throws<ArgumentException>(() => input.Reverse(input.Length, 1));
67
+ Assert.Throws<ArgumentException>(() => input.Reverse(int.MaxValue, 1));
68
+ Assert.Throws<ArgumentException>(() => input.Reverse(int.MinValue, 1));
69
+
70
+ Assert.Throws<ArgumentException>(() => input.Reverse(1, -1));
71
+ Assert.Throws<ArgumentException>(() => input.Reverse(1, input.Length));
72
+ Assert.Throws<ArgumentException>(() => input.Reverse(1, int.MaxValue));
73
+ Assert.Throws<ArgumentException>(() => input.Reverse(1, int.MinValue));
74
+ }
75
+ }
76
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 53f10c10d1a14859a9a8616477fd318a
3
+ timeCreated: 1741982863
@@ -44,6 +44,19 @@
44
44
  }
45
45
  }
46
46
 
47
+ [Test]
48
+ public void PositiveMod()
49
+ {
50
+ Assert.AreEqual(9, (-1).PositiveMod(10));
51
+ Assert.AreEqual(1, 1.PositiveMod(10));
52
+ Assert.AreEqual(9f, (-1f).PositiveMod(10f));
53
+ Assert.AreEqual(1f, 1f.PositiveMod(10f));
54
+ Assert.AreEqual(9.0, (-1.0).PositiveMod(10.0));
55
+ Assert.AreEqual(1.0, 1.0.PositiveMod(10.0));
56
+ Assert.AreEqual(9L, (-1L).PositiveMod(10L));
57
+ Assert.AreEqual(1L, 1L.PositiveMod(10L));
58
+ }
59
+
47
60
  [Test]
48
61
  public void ApproximatelyExpected()
49
62
  {
@@ -51,7 +64,6 @@
51
64
  Assert.IsTrue(0f.Approximately(0.5f, 1f));
52
65
  Assert.IsFalse(0.001f.Approximately(0f, 0f));
53
66
  Assert.IsFalse(100f.Approximately(5f, 2.4f));
54
-
55
67
  Assert.IsTrue(0.001f.Approximately(0.0001f));
56
68
  }
57
69
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.wallstop-studios.unity-helpers",
3
- "version": "2.0.0-rc24",
3
+ "version": "2.0.0-rc26",
4
4
  "displayName": "Unity Helpers",
5
5
  "description": "Various Unity Helper Library",
6
6
  "dependencies": {},