// MIT License - Copyright (c) 2023 wallstop
// Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE
namespace WallstopStudios.UnityHelpers.Core.Extension
{
using System;
using System.Collections.Generic;
using Helper;
using Random;
using UnityEngine;
using UnityEngine.Pool;
using WallstopStudios.UnityHelpers.Utils;
///
/// Provides extension methods for generating random Unity types (vectors, quaternions, colors) using the IRandom interface.
///
///
/// Thread Safety: All methods are thread-safe if the IRandom implementation provided is thread-safe.
/// Performance: Most methods are O(1). NextSubset is O(n) where n is the collection size.
///
public static class RandomExtensions
{
///
/// Returns an index sampled from the provided weights (unnormalized). Negative weights are treated as zero.
///
public static int NextWeightedIndex(this IRandom random, IReadOnlyList weights)
{
if (weights == null)
{
throw new ArgumentNullException(nameof(weights));
}
if (weights.Count == 0)
{
throw new ArgumentException("Weights cannot be empty", nameof(weights));
}
double total = 0;
for (int i = 0; i < weights.Count; i++)
{
if (weights[i] > 0)
{
total += weights[i];
}
}
if (total <= 0)
{
throw new ArgumentException("Sum of weights must be > 0", nameof(weights));
}
double r = random.NextDouble() * total;
double acc = 0;
for (int i = 0; i < weights.Count; i++)
{
float w = weights[i];
if (w <= 0)
{
continue;
}
acc += w;
if (r <= acc)
{
return i;
}
}
return weights.Count - 1;
}
///
/// Returns an element sampled according to the given weights list. Throws if lengths mismatch.
///
public static T NextWeightedElement(
this IRandom random,
IReadOnlyList items,
IReadOnlyList weights
)
{
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
if (weights == null)
{
throw new ArgumentNullException(nameof(weights));
}
if (items.Count != weights.Count)
{
throw new ArgumentException(
"Items and weights length must match.",
nameof(weights)
);
}
int idx = random.NextWeightedIndex(weights);
return items[idx];
}
///
/// Generates a random 2D vector with components in the range [-amplitude, amplitude].
///
/// The random number generator to use.
/// The maximum absolute value for each component (must be positive).
/// A Vector2 with x and y components each in [-amplitude, amplitude].
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - two random number generations.
/// Allocations: No heap allocations (Vector2 is a value type).
/// Edge Cases: Negative amplitude is normalized via absolute value. Zero amplitude returns Vector2.zero.
///
public static Vector2 NextVector2(this IRandom random, float amplitude)
{
float range = Mathf.Abs(amplitude);
if (range <= 0f)
{
return Vector2.zero;
}
return random.NextVector2(-range, range);
}
///
/// Randomly selects an element from a collection with no exclusions.
///
/// The type of elements in the collection.
/// The random number generator to use.
/// The collection to select from.
/// A randomly selected element from values.
///
/// Null Handling: Will throw NullReferenceException if random or values is null.
/// Thread Safety: Thread-safe if random is thread-safe and values is not modified during execution.
/// Performance: O(1) for lists/arrays, O(n) for general enumerables.
/// Allocations: Zero allocation for this overload. Materializes non-list/collection enumerables to pooled list.
/// Edge Cases: Empty values collection will cause NextOf to fail.
///
public static T NextOfExcept(this IRandom random, IEnumerable values)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
return random.NextOf(values);
}
///
/// Randomly selects an element from a collection, excluding one specified value.
///
/// The type of elements in the collection.
/// The random number generator to use.
/// The collection to select from.
/// The value to exclude from selection.
/// A randomly selected element from values that is not the excluded value.
///
/// Null Handling: Will throw NullReferenceException if random or values is null.
/// Thread Safety: Thread-safe if random is thread-safe and values is not modified during execution.
/// Performance: O(n) where n is collection size.
/// Allocations: Zero allocation - uses pooled collections internally.
/// Edge Cases: Throws if all values are excluded. Empty values collection will fail.
///
public static T NextOfExcept(this IRandom random, IEnumerable values, T exception1)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
if (values is IReadOnlyList source)
{
return NextOfExceptCore(random, source, exception1);
}
using PooledResource> lease = Buffers.List.Get(out List materializedList);
materializedList.AddRange(values);
return NextOfExceptCore(random, materializedList, exception1);
}
///
/// Randomly selects an element from a collection, excluding two specified values.
///
/// The type of elements in the collection.
/// The random number generator to use.
/// The collection to select from.
/// The first value to exclude from selection.
/// The second value to exclude from selection.
/// A randomly selected element from values that is not one of the excluded values.
///
/// Null Handling: Will throw NullReferenceException if random or values is null.
/// Thread Safety: Thread-safe if random is thread-safe and values is not modified during execution.
/// Performance: O(n) where n is collection size.
/// Allocations: Zero allocation - uses pooled collections internally.
/// Edge Cases: Throws if all values are excluded. Empty values collection will fail.
///
public static T NextOfExcept(
this IRandom random,
IEnumerable values,
T exception1,
T exception2
)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
if (values is IReadOnlyList source)
{
return NextOfExceptCore(random, source, exception1, exception2);
}
using PooledResource> lease = Buffers.List.Get(out List materializedList);
materializedList.AddRange(values);
return NextOfExceptCore(random, materializedList, exception1, exception2);
}
///
/// Randomly selects an element from a collection, excluding three specified values.
///
/// The type of elements in the collection.
/// The random number generator to use.
/// The collection to select from.
/// The first value to exclude from selection.
/// The second value to exclude from selection.
/// The third value to exclude from selection.
/// A randomly selected element from values that is not one of the excluded values.
///
/// Null Handling: Will throw NullReferenceException if random or values is null.
/// Thread Safety: Thread-safe if random is thread-safe and values is not modified during execution.
/// Performance: O(n) where n is collection size.
/// Allocations: Zero allocation - uses pooled collections internally.
/// Edge Cases: Throws if all values are excluded. Empty values collection will fail.
///
public static T NextOfExcept(
this IRandom random,
IEnumerable values,
T exception1,
T exception2,
T exception3
)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
if (values is IReadOnlyList source)
{
return NextOfExceptCore(random, source, exception1, exception2, exception3);
}
using PooledResource> lease = Buffers.List.Get(out List materializedList);
materializedList.AddRange(values);
return NextOfExceptCore(random, materializedList, exception1, exception2, exception3);
}
///
/// Randomly selects an element from a collection, excluding specified exception values via IEnumerable.
///
/// The type of elements in the collection.
/// The random number generator to use.
/// The collection to select from.
/// An enumerable of values to exclude from selection.
/// A randomly selected element from values that is not in exceptions.
///
/// Null Handling: Will throw NullReferenceException if random or values is null. Null exceptions treated as empty.
/// Thread Safety: Thread-safe if random is thread-safe and values is not modified during execution.
/// Performance: O(n*k) where n is collection size and k is exceptions count.
/// Allocations: Uses pooled collections internally. Does not allocate params array.
/// Edge Cases: Throws if all values are excluded. Empty values collection will fail.
///
public static T NextOfExcept(
this IRandom random,
IEnumerable values,
IEnumerable exceptions
)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
if (values is IReadOnlyList source)
{
return NextOfExceptCore(random, source, exceptions);
}
using PooledResource> lease = Buffers.List.Get(out List materializedList);
materializedList.AddRange(values);
return NextOfExceptCore(random, materializedList, exceptions);
}
///
/// Randomly selects an element from a collection, excluding specified exception values.
///
/// The type of elements in the collection.
/// The random number generator to use.
/// The collection to select from.
/// Values to exclude from selection.
/// A randomly selected element from values that is not in exceptions.
///
/// Null Handling: Will throw NullReferenceException if random or values is null.
/// Thread Safety: Thread-safe if random is thread-safe and values is not modified during execution.
/// Performance: O(k*n) worst case where k is number of exceptions and n is selection attempts.
/// Allocations: This params overload allocates an array on each call. Prefer the specific 0-3 arg overloads
/// or the IEnumerable overload for zero-allocation hot paths.
/// Edge Cases: Throws if all values are excluded. Empty values collection will cause NextOf to fail.
///
public static T NextOfExcept(
this IRandom random,
IEnumerable values,
params T[] exceptions
)
{
if (values == null)
{
throw new ArgumentNullException(nameof(values));
}
if (values is IReadOnlyList source)
{
return NextOfExceptCore(random, source, exceptions);
}
using PooledResource> lease = Buffers.List.Get(out List materializedList);
materializedList.AddRange(values);
return NextOfExceptCore(random, materializedList, exceptions);
}
private static T NextOfExceptCore(IRandom random, IReadOnlyList source, T exception1)
{
if (source.Count == 0)
{
throw new ArgumentException("Collection cannot be empty", nameof(source));
}
EqualityComparer comparer = EqualityComparer.Default;
using PooledArray pooled = SystemArrayPool.Get(source.Count, out T[] buffer);
int n = 0;
for (int i = 0; i < source.Count; ++i)
{
T v = source[i];
if (!comparer.Equals(v, exception1))
{
buffer[n++] = v;
}
}
if (n == 0)
{
throw new ArgumentException("All values are excluded", nameof(exception1));
}
return n == 1 ? buffer[0] : buffer[random.Next(n)];
}
private static T NextOfExceptCore(
IRandom random,
IReadOnlyList source,
T exception1,
T exception2
)
{
if (source.Count == 0)
{
throw new ArgumentException("Collection cannot be empty", nameof(source));
}
EqualityComparer comparer = EqualityComparer.Default;
using PooledArray pooled = SystemArrayPool.Get(source.Count, out T[] buffer);
int n = 0;
for (int i = 0; i < source.Count; ++i)
{
T v = source[i];
if (!comparer.Equals(v, exception1) && !comparer.Equals(v, exception2))
{
buffer[n++] = v;
}
}
if (n == 0)
{
throw new ArgumentException("All values are excluded", nameof(exception1));
}
return n == 1 ? buffer[0] : buffer[random.Next(n)];
}
private static T NextOfExceptCore(
IRandom random,
IReadOnlyList source,
T exception1,
T exception2,
T exception3
)
{
if (source.Count == 0)
{
throw new ArgumentException("Collection cannot be empty", nameof(source));
}
EqualityComparer comparer = EqualityComparer.Default;
using PooledArray pooled = SystemArrayPool.Get(source.Count, out T[] buffer);
int n = 0;
for (int i = 0; i < source.Count; ++i)
{
T v = source[i];
if (
!comparer.Equals(v, exception1)
&& !comparer.Equals(v, exception2)
&& !comparer.Equals(v, exception3)
)
{
buffer[n++] = v;
}
}
if (n == 0)
{
throw new ArgumentException("All values are excluded", nameof(exception1));
}
return n == 1 ? buffer[0] : buffer[random.Next(n)];
}
private static T NextOfExceptCore(
IRandom random,
IReadOnlyList source,
IEnumerable exceptions
)
{
if (source.Count == 0)
{
throw new ArgumentException("Collection cannot be empty", nameof(source));
}
if (exceptions == null)
{
return random.NextOf(source);
}
using PooledResource> excludeLease = Buffers.HashSet.Get(
out HashSet exclude
);
if (exceptions is IReadOnlyList exceptionList)
{
for (int i = 0; i < exceptionList.Count; ++i)
{
exclude.Add(exceptionList[i]);
}
}
else
{
foreach (T exception in exceptions)
{
exclude.Add(exception);
}
}
if (exclude.Count == 0)
{
return random.NextOf(source);
}
using PooledArray pooled = SystemArrayPool.Get(source.Count, out T[] buffer);
int n = 0;
for (int i = 0; i < source.Count; ++i)
{
T v = source[i];
if (!exclude.Contains(v))
{
buffer[n++] = v;
}
}
if (n == 0)
{
throw new ArgumentException("All values are excluded", nameof(exceptions));
}
return n == 1 ? buffer[0] : buffer[random.Next(n)];
}
private static T NextOfExceptCore(
IRandom random,
IReadOnlyList source,
T[] exceptions
)
{
if (source.Count == 0)
{
throw new ArgumentException("Collection cannot be empty", nameof(source));
}
if (exceptions == null || exceptions.Length == 0)
{
return random.NextOf(source);
}
using PooledResource> excludeLease = Buffers.HashSet.Get(
out HashSet exclude
);
for (int i = 0; i < exceptions.Length; ++i)
{
exclude.Add(exceptions[i]);
}
using PooledArray pooled = SystemArrayPool.Get(source.Count, out T[] buffer);
int n = 0;
for (int i = 0; i < source.Count; ++i)
{
T v = source[i];
if (!exclude.Contains(v))
{
buffer[n++] = v;
}
}
if (n == 0)
{
throw new ArgumentException("All values are excluded", nameof(exceptions));
}
return n == 1 ? buffer[0] : buffer[random.Next(n)];
}
///
/// Generates a random 2D vector with components in the specified range.
///
/// The random number generator to use.
/// The minimum value for each component (inclusive).
/// The maximum value for each component (exclusive).
/// A Vector2 with x and y components each in [minAmplitude, maxAmplitude).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - two random number generations.
/// Allocations: No heap allocations.
/// Edge Cases: If minAmplitude >= maxAmplitude, behavior depends on IRandom.NextFloat implementation.
///
public static Vector2 NextVector2(
this IRandom random,
float minAmplitude,
float maxAmplitude
)
{
float x = random.NextFloat(minAmplitude, maxAmplitude);
float y = random.NextFloat(minAmplitude, maxAmplitude);
return new Vector2(x, y);
}
///
/// Generates a random 2D point uniformly distributed within a circular area.
///
/// The random number generator to use.
/// The radius of the circle.
/// The center of the circle (default: Vector2.zero).
/// A Vector2 uniformly distributed within the circle defined by origin and range.
///
/// Null Handling: Will throw NullReferenceException if random is null. Null origin defaults to Vector2.zero.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - uses square root for uniform distribution.
/// Allocations: No heap allocations.
/// Edge Cases: Negative range is normalized via absolute value. Zero range returns origin.
///
public static Vector2 NextVector2InRange(
this IRandom random,
float range,
Vector2? origin = null
)
{
float radius = Mathf.Abs(range);
if (radius <= 0f)
{
return origin ?? Vector2.zero;
}
return Helpers.GetRandomPointInCircle(origin ?? Vector2.zero, radius, random);
}
///
/// Generates a random 3D vector with components in the range [-amplitude, amplitude].
///
/// The random number generator to use.
/// The maximum absolute value for each component.
/// A Vector3 with x, y, and z components each in [-amplitude, amplitude].
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - three random number generations.
/// Allocations: No heap allocations.
/// Edge Cases: Negative amplitude is normalized via absolute value. Zero amplitude returns Vector3.zero.
///
public static Vector3 NextVector3(this IRandom random, float amplitude)
{
float range = Mathf.Abs(amplitude);
if (range <= 0f)
{
return Vector3.zero;
}
return random.NextVector3(-range, range);
}
///
/// Generates a random 3D vector with components in the specified range.
///
/// The random number generator to use.
/// The minimum value for each component (inclusive).
/// The maximum value for each component (exclusive).
/// A Vector3 with x, y, and z components each in [minAmplitude, maxAmplitude).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - three random number generations.
/// Allocations: No heap allocations.
/// Edge Cases: If minAmplitude >= maxAmplitude, behavior depends on IRandom.NextFloat implementation.
///
public static Vector3 NextVector3(
this IRandom random,
float minAmplitude,
float maxAmplitude
)
{
float z = random.NextFloat(minAmplitude, maxAmplitude);
Vector3 result = random.NextVector2(minAmplitude, maxAmplitude);
result.z = z;
return result;
}
///
/// Generates a random 3D point uniformly distributed within a spherical volume.
///
/// The random number generator to use.
/// The radius of the sphere.
/// The center of the sphere (default: Vector3.zero).
/// A Vector3 uniformly distributed within the sphere defined by origin and range.
///
/// Null Handling: Will throw NullReferenceException if random is null. Null origin defaults to Vector3.zero.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - uses cube root for uniform distribution.
/// Allocations: No heap allocations.
/// Edge Cases: Negative range is normalized via absolute value. Zero range returns origin.
///
public static Vector3 NextVector3InRange(
this IRandom random,
float range,
Vector3? origin = null
)
{
float radius = Mathf.Abs(range);
if (radius <= 0f)
{
return origin ?? Vector3.zero;
}
return Helpers.GetRandomPointInSphere(origin ?? Vector3.zero, radius, random);
}
///
/// Generates a random 3D point uniformly distributed on the surface of a sphere using Marsaglia's method.
///
/// The random number generator to use.
/// The radius of the sphere.
/// The center of the sphere (default: Vector3.zero).
/// A Vector3 on the surface of the sphere with exact distance 'radius' from center.
///
/// Null Handling: Will throw NullReferenceException if random is null. Null center defaults to Vector3.zero.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) average case - Marsaglia rejection sampling averages ~1.3 iterations. Uses square root.
/// Allocations: No heap allocations.
/// Edge Cases: Very small radius (near zero) works correctly. Negative radius is treated as its absolute value.
///
public static Vector3 NextVector3OnSphere(
this IRandom random,
float radius,
Vector3? center = null
)
{
const int MaxAttempts = 128;
const float MinLengthSquared = 0.0001f;
float radiusMagnitude = Mathf.Abs(radius);
if (radiusMagnitude <= 0f)
{
return center ?? Vector3.zero;
}
for (int attempt = 0; attempt < MaxAttempts; ++attempt)
{
float x = random.NextFloat(-1f, 1f);
float y = random.NextFloat(-1f, 1f);
float z = random.NextFloat(-1f, 1f);
float lengthSquared = x * x + y * y + z * z;
if (
!float.IsFinite(lengthSquared)
|| lengthSquared > 1f
|| lengthSquared < MinLengthSquared
)
{
continue;
}
float invLength = radiusMagnitude / Mathf.Sqrt(lengthSquared);
Vector3 sampled = new(x * invLength, y * invLength, z * invLength);
return center.HasValue ? sampled + center.Value : sampled;
}
Vector3 fallback = new(radiusMagnitude, 0f, 0f);
return center.HasValue ? fallback + center.Value : fallback;
}
///
/// Generates a random 3D point uniformly distributed within a spherical volume.
///
/// The random number generator to use.
/// The radius of the sphere.
/// The center of the sphere (default: Vector3.zero).
/// A Vector3 uniformly distributed within the sphere defined by center and radius.
///
/// Null Handling: Will throw NullReferenceException if random is null. Null center defaults to Vector3.zero.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - uses cube root for uniform volumetric distribution.
/// Allocations: No heap allocations.
/// Edge Cases: Negative radius is treated as its absolute value. Zero radius returns center.
///
public static Vector3 NextVector3InSphere(
this IRandom random,
float radius,
Vector3? center = null
)
{
return Helpers.GetRandomPointInSphere(center ?? Vector3.zero, radius, random);
}
///
/// Generates a uniformly distributed random rotation quaternion using Shoemake's algorithm.
///
/// The random number generator to use.
/// A uniformly distributed rotation quaternion (all orientations equally likely).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - involves square roots and trigonometric functions.
/// Allocations: No heap allocations.
/// Edge Cases: Produces normalized quaternions. Algorithm based on Shoemake, "Uniform Random Rotations", Graphics Gems III.
///
public static Quaternion NextQuaternion(this IRandom random)
{
// Uniform random rotation using Shoemake's algorithm
float u1 = Helpers.ClampUnitInterval(random.NextFloat());
float u2 = Helpers.ClampUnitInterval(random.NextFloat());
float u3 = Helpers.ClampUnitInterval(random.NextFloat());
float sqrt1MinusU1 = Mathf.Sqrt(1f - u1);
float sqrtU1 = Mathf.Sqrt(u1);
float twoPiU2 = 2f * Mathf.PI * u2;
float twoPiU3 = 2f * Mathf.PI * u3;
return new Quaternion(
sqrt1MinusU1 * Mathf.Sin(twoPiU2),
sqrt1MinusU1 * Mathf.Cos(twoPiU2),
sqrtU1 * Mathf.Sin(twoPiU3),
sqrtU1 * Mathf.Cos(twoPiU3)
);
}
///
/// Generates a random rotation around a specified axis within an angle range.
///
/// The random number generator to use.
/// The axis to rotate around (will be normalized).
/// The minimum rotation angle in degrees (inclusive).
/// The maximum rotation angle in degrees (exclusive).
/// A quaternion representing a rotation around axis by a random angle in [minAngle, maxAngle).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - involves vector normalization and quaternion construction.
/// Allocations: No heap allocations.
/// Edge Cases: Zero-length axis will produce undefined results from normalization.
///
public static Quaternion NextQuaternionAxisAngle(
this IRandom random,
Vector3 axis,
float minAngle,
float maxAngle
)
{
float angle = random.NextFloat(minAngle, maxAngle);
return Quaternion.AngleAxis(angle, axis.normalized);
}
///
/// Generates a random rotation that would make an object "look" in a random direction.
///
/// The random number generator to use.
/// A quaternion representing a look rotation toward a random 3D direction.
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - involves sphere sampling and look rotation calculation.
/// Allocations: No heap allocations.
/// Edge Cases: The "up" direction for LookRotation is always Vector3.up, which may cause issues near poles.
///
public static Quaternion NextQuaternionLookRotation(this IRandom random)
{
Vector3 direction = random.NextDirection3D();
return Quaternion.LookRotation(direction);
}
///
/// Generates a random color with RGB components uniformly distributed in [0, 1].
///
/// The random number generator to use.
/// If true, alpha is random [0, 1]; if false, alpha is 1.0 (opaque).
/// A random Color with all components in [0, 1].
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - three or four random float generations.
/// Allocations: No heap allocations.
/// Edge Cases: None - all values are clamped to valid Color range.
///
public static Color NextColor(this IRandom random, bool randomAlpha = false)
{
float r = random.NextFloat();
float g = random.NextFloat();
float b = random.NextFloat();
float a = randomAlpha ? random.NextFloat() : 1f;
return new Color(r, g, b, a);
}
///
/// Generates a random color within a specified variance range from a base color in HSV space.
///
/// The random number generator to use.
/// The base color to vary from.
/// The maximum hue deviation (0-1 scale, wraps around).
/// The maximum saturation deviation (clamped to [0, 1]).
/// The maximum value/brightness deviation (clamped to [0, 1]).
/// A color randomly varied from baseColor within the specified HSV ranges, with HDR enabled.
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - involves RGB-to-HSV conversion, random generation, and HSV-to-RGB conversion.
/// Allocations: No heap allocations.
/// Edge Cases: Hue wraps around at boundaries (0 and 1 are adjacent). Saturation and value are clamped.
///
public static Color NextColorInRange(
this IRandom random,
Color baseColor,
float hueVariance,
float saturationVariance,
float valueVariance
)
{
Color.RGBToHSV(baseColor, out float h, out float s, out float v);
h += random.NextFloat(-hueVariance, hueVariance);
h = Mathf.Repeat(h, 1f); // Wrap hue to [0, 1]
s = Mathf.Clamp01(s + random.NextFloat(-saturationVariance, saturationVariance));
v = Mathf.Clamp01(v + random.NextFloat(-valueVariance, valueVariance));
return Color.HSVToRGB(h, s, v, true);
}
///
/// Generates a random 32-bit color with RGB components uniformly distributed in [0, 255].
///
/// The random number generator to use.
/// If true, alpha is random [0, 255]; if false, alpha is 255 (opaque).
/// A random Color32 with byte-precision components.
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - three or four random byte generations.
/// Allocations: No heap allocations.
/// Edge Cases: None - all byte values are valid.
///
public static Color32 NextColor32(this IRandom random, bool randomAlpha = false)
{
byte r = random.NextByte();
byte g = random.NextByte();
byte b = random.NextByte();
byte a = randomAlpha ? random.NextByte() : (byte)255;
return new Color32(r, g, b, a);
}
///
/// Generates a random 2D integer vector with components in the range [-amplitude, amplitude).
///
/// The random number generator to use.
/// The maximum absolute value for each component.
/// A Vector2Int with x and y components each in [-amplitude, amplitude).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - two random integer generations.
/// Allocations: No heap allocations.
/// Edge Cases: Negative amplitude is normalized via absolute value. Zero amplitude returns Vector2Int.zero.
///
public static Vector2Int NextVector2Int(this IRandom random, int amplitude)
{
int range = Mathf.Abs(amplitude);
if (range == 0)
{
return Vector2Int.zero;
}
return random.NextVector2Int(-range, range);
}
///
/// Generates a random 2D integer vector with components in the specified range.
///
/// The random number generator to use.
/// The minimum value for each component (inclusive).
/// The maximum value for each component (exclusive).
/// A Vector2Int with x and y components each in [minAmplitude, maxAmplitude).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - two random integer generations.
/// Allocations: No heap allocations.
/// Edge Cases: If minAmplitude >= maxAmplitude, behavior depends on IRandom.Next implementation.
///
public static Vector2Int NextVector2Int(
this IRandom random,
int minAmplitude,
int maxAmplitude
)
{
int x = random.Next(minAmplitude, maxAmplitude);
int y = random.Next(minAmplitude, maxAmplitude);
return new Vector2Int(x, y);
}
///
/// Generates a random 2D integer vector with components independently bounded by min and max vectors.
///
/// The random number generator to use.
/// The minimum bounds (inclusive) for each component.
/// The maximum bounds (exclusive) for each component.
/// A Vector2Int with x in [min.x, max.x) and y in [min.y, max.y).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - two random integer generations.
/// Allocations: No heap allocations.
/// Edge Cases: If any min component >= corresponding max component, behavior depends on IRandom.Next.
///
public static Vector2Int NextVector2Int(this IRandom random, Vector2Int min, Vector2Int max)
{
int x = random.Next(min.x, max.x);
int y = random.Next(min.y, max.y);
return new Vector2Int(x, y);
}
///
/// Generates a random 3D integer vector with components in the range [-amplitude, amplitude).
///
/// The random number generator to use.
/// The maximum absolute value for each component.
/// A Vector3Int with x, y, and z components each in [-amplitude, amplitude).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - three random integer generations.
/// Allocations: No heap allocations.
/// Edge Cases: Negative amplitude is normalized via absolute value. Zero amplitude returns Vector3Int.zero.
///
public static Vector3Int NextVector3Int(this IRandom random, int amplitude)
{
int range = Mathf.Abs(amplitude);
if (range == 0)
{
return Vector3Int.zero;
}
return random.NextVector3Int(-range, range);
}
///
/// Generates a random 3D integer vector with components in the specified range.
///
/// The random number generator to use.
/// The minimum value for each component (inclusive).
/// The maximum value for each component (exclusive).
/// A Vector3Int with x, y, and z components each in [minAmplitude, maxAmplitude).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - three random integer generations.
/// Allocations: No heap allocations.
/// Edge Cases: If minAmplitude >= maxAmplitude, behavior depends on IRandom.Next implementation.
///
public static Vector3Int NextVector3Int(
this IRandom random,
int minAmplitude,
int maxAmplitude
)
{
int x = random.Next(minAmplitude, maxAmplitude);
int y = random.Next(minAmplitude, maxAmplitude);
int z = random.Next(minAmplitude, maxAmplitude);
return new Vector3Int(x, y, z);
}
///
/// Generates a random 3D integer vector with components independently bounded by min and max vectors.
///
/// The random number generator to use.
/// The minimum bounds (inclusive) for each component.
/// The maximum bounds (exclusive) for each component.
/// A Vector3Int with components independently ranged: x in [min.x, max.x), y in [min.y, max.y), z in [min.z, max.z).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - three random integer generations.
/// Allocations: No heap allocations.
/// Edge Cases: If any min component >= corresponding max component, behavior depends on IRandom.Next.
///
public static Vector3Int NextVector3Int(this IRandom random, Vector3Int min, Vector3Int max)
{
int x = random.Next(min.x, max.x);
int y = random.Next(min.y, max.y);
int z = random.Next(min.z, max.z);
return new Vector3Int(x, y, z);
}
///
/// Generates a uniformly distributed random 2D unit direction vector.
///
/// The random number generator to use.
/// A normalized Vector2 pointing in a random direction (magnitude 1.0).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - one random generation and two trigonometric functions.
/// Allocations: No heap allocations.
/// Edge Cases: Always returns a normalized vector with magnitude 1.0.
///
public static Vector2 NextDirection2D(this IRandom random)
{
float angle = random.NextFloat(0f, 2f * Mathf.PI);
return new Vector2(Mathf.Cos(angle), Mathf.Sin(angle));
}
///
/// Generates a uniformly distributed random 3D unit direction vector.
///
/// The random number generator to use.
/// A normalized Vector3 pointing in a random direction (magnitude 1.0).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) average - uses Marsaglia sphere sampling.
/// Allocations: No heap allocations.
/// Edge Cases: Always returns a normalized vector with magnitude 1.0.
///
public static Vector3 NextDirection3D(this IRandom random)
{
return random.NextVector3OnSphere(1f, Vector3.zero);
}
///
/// Generates a random angle in degrees within the specified range.
///
/// The random number generator to use.
/// The minimum angle in degrees (inclusive, default: 0).
/// The maximum angle in degrees (exclusive, default: 360).
/// A random angle in degrees within [min, max).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - single random float generation.
/// Allocations: No heap allocations.
/// Edge Cases: Does not normalize angle to [0, 360) - can return negative or >360 values if range allows.
///
public static float NextAngle(this IRandom random, float min = 0f, float max = 360f)
{
return random.NextFloat(min, max);
}
///
/// Generates a random 2D point uniformly distributed within a rectangle.
///
/// The random number generator to use.
/// The bounding rectangle to generate points within.
/// A Vector2 uniformly distributed within the rect bounds.
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - two random float generations.
/// Allocations: No heap allocations.
/// Edge Cases: Works with negative or inverted rectangles. Zero-area rectangles return the min corner.
///
public static Vector2 NextVector2InRect(this IRandom random, Rect rect)
{
float xMin = Mathf.Min(rect.xMin, rect.xMax);
float xMax = Mathf.Max(rect.xMin, rect.xMax);
float yMin = Mathf.Min(rect.yMin, rect.yMax);
float yMax = Mathf.Max(rect.yMin, rect.yMax);
float x = xMax - xMin <= 0f ? xMin : random.NextFloat(xMin, xMax);
float y = yMax - yMin <= 0f ? yMin : random.NextFloat(yMin, yMax);
return new Vector2(x, y);
}
///
/// Generates a random 3D point uniformly distributed within an axis-aligned bounding box.
///
/// The random number generator to use.
/// The bounding box to generate points within.
/// A Vector3 uniformly distributed within the bounds.
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - three random float generations.
/// Allocations: No heap allocations.
/// Edge Cases: Degenerate (zero-volume) bounds return the center point.
///
public static Vector3 NextVector3InBounds(this IRandom random, Bounds bounds)
{
Vector3 size = bounds.size;
if (size.x <= 0f || size.y <= 0f || size.z <= 0f)
{
return bounds.center;
}
Vector3 min = bounds.min;
Vector3 max = bounds.max;
float x = random.NextFloat(min.x, max.x);
float y = random.NextFloat(min.y, max.y);
float z = random.NextFloat(min.z, max.z);
return new Vector3(x, y, z);
}
///
/// Selects a random item from a weighted collection where each item has an associated probability weight.
///
/// The type of items in the collection.
/// The random number generator to use.
/// A collection of (item, weight) tuples where weight determines selection probability.
/// A randomly selected item, with probability proportional to its weight relative to total weight.
///
/// Thrown if collection is empty, any weight is negative, or total weight is zero or negative.
///
///
/// Null Handling: Will throw NullReferenceException if random or weighted is null.
/// Thread Safety: Thread-safe if random is thread-safe and weighted is not modified during execution.
/// Performance: O(n) where n is the number of items - iterates twice (sum weights, then select).
/// Allocations: Materializes non-list collections to array on first pass.
/// Edge Cases: Due to floating point precision, may return last item if randomValue equals totalWeight.
///
public static T NextWeighted(
this IRandom random,
IEnumerable<(T item, float weight)> weighted
)
{
if (weighted is IReadOnlyList<(T, float)> items)
{
return NextWeightedCore(random, items);
}
// Materialize enumerable to pooled list - AddRange is preferred for performance:
// it checks for ICollection and pre-allocates, and uses Array.Copy for arrays/lists
using PooledResource> lease = Buffers<(T, float)>.List.Get(
out List<(T, float)> materializedList
);
materializedList.AddRange(weighted);
return NextWeightedCore(random, materializedList);
}
private static T NextWeightedCore(IRandom random, IReadOnlyList<(T, float)> items)
{
if (items.Count == 0)
{
throw new ArgumentException("Weighted collection cannot be empty", nameof(items));
}
float totalWeight = 0f;
for (int i = 0; i < items.Count; ++i)
{
float weight = items[i].Item2;
if (weight < 0f)
{
throw new ArgumentException("Weights cannot be negative", nameof(items));
}
totalWeight += weight;
}
if (totalWeight <= 0f)
{
throw new ArgumentException(
"Total weight must be greater than zero",
nameof(items)
);
}
float randomValue = random.NextFloat(0f, totalWeight);
float cumulative = 0f;
for (int i = 0; i < items.Count; ++i)
{
(T item, float weight) = items[i];
cumulative += weight;
if (randomValue < cumulative)
{
return item;
}
}
// Fallback due to floating point precision
return items[items.Count - 1].Item1;
}
///
/// Selects a random index from an array of weights, where each weight determines selection probability.
///
/// The random number generator to use.
/// An array of weights where each element determines the probability of selecting that index.
/// A random index in [0, weights.Length), with probability proportional to weights[i] / totalWeight.
///
/// Thrown if weights is null/empty, any weight is negative, or total weight is zero or negative.
///
///
/// Null Handling: Throws ArgumentException if weights is null.
/// Thread Safety: Thread-safe if random is thread-safe and weights array is not modified during execution.
/// Performance: O(n) where n is weights.Length - iterates to sum weights, then to select index.
/// Allocations: No heap allocations.
/// Edge Cases: Due to floating point precision, may return last index if randomValue equals totalWeight.
///
public static int NextWeightedIndex(this IRandom random, float[] weights)
{
if (weights == null || weights.Length == 0)
{
throw new ArgumentException(
"Weights array cannot be null or empty",
nameof(weights)
);
}
float totalWeight = 0f;
for (int i = 0; i < weights.Length; ++i)
{
float weight = weights[i];
if (weight < 0f)
{
throw new ArgumentException("Weights cannot be negative", nameof(weights));
}
totalWeight += weight;
}
if (totalWeight <= 0f)
{
throw new ArgumentException(
"Total weight must be greater than zero",
nameof(weights)
);
}
float randomValue = random.NextFloat(0f, totalWeight);
float cumulative = 0f;
for (int i = 0; i < weights.Length; ++i)
{
cumulative += weights[i];
if (randomValue < cumulative)
{
return i;
}
}
// Fallback due to floating point precision
return weights.Length - 1;
}
///
/// Generates a random boolean with a specified probability of being true.
///
/// The random number generator to use.
/// The probability [0, 1] that the result will be true.
/// True with probability 'probability', false otherwise.
/// Thrown if probability is not in [0, 1].
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - single random float generation and comparison.
/// Allocations: No heap allocations.
/// Edge Cases: probability=0 always returns false, probability=1 always returns true.
///
public static bool NextBool(this IRandom random, float probability)
{
if (probability < 0f || probability > 1f)
{
throw new ArgumentOutOfRangeException(
nameof(probability),
"Probability must be between 0 and 1"
);
}
return random.NextFloat() < probability;
}
///
/// Generates a random sign (1 or -1) with equal probability.
///
/// The random number generator to use.
/// Either 1 or -1, each with 50% probability.
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - single random boolean generation.
/// Allocations: No heap allocations.
/// Edge Cases: None - always returns exactly 1 or -1.
///
public static int NextSign(this IRandom random)
{
return random.NextBool() ? 1 : -1;
}
///
/// Generates a random float centered around a value with specified variance.
///
/// The random number generator to use.
/// The center value of the range.
/// The maximum deviation from center (can be positive or negative).
/// A random float in [center - variance, center + variance).
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - arithmetic and single random generation.
/// Allocations: No heap allocations.
/// Edge Cases: Negative variance inverts the range. Zero variance returns center.
///
public static float NextFloatAround(this IRandom random, float center, float variance)
{
if (variance <= 0f)
{
return center;
}
return random.NextFloat(center - variance, center + variance);
}
///
/// Generates a random integer centered around a value with specified variance.
///
/// The random number generator to use.
/// The center value of the range.
/// The maximum deviation from center (inclusive).
/// A random integer in [center - variance, center + variance].
///
/// Null Handling: Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe.
/// Performance: O(1) - arithmetic and single random generation.
/// Allocations: No heap allocations.
/// Edge Cases: Note the +1 adjustment makes upper bound inclusive. Negative variance inverts range.
///
public static int NextIntAround(this IRandom random, int center, int variance)
{
if (variance <= 0)
{
return center;
}
return random.Next(center - variance, center + variance + 1);
}
///
/// Selects a random subset of items from a collection using reservoir sampling for uniform distribution.
///
/// The type of items in the collection.
/// The random number generator to use.
/// The collection to select from.
/// The number of items to select.
/// An array containing 'count' randomly selected items from the collection, with uniform probability.
/// Thrown if items is null.
/// Thrown if count is negative.
/// Thrown if count exceeds the number of items.
///
/// Null Handling: Throws ArgumentNullException if items is null. Will throw NullReferenceException if random is null.
/// Thread Safety: Thread-safe if random is thread-safe and items is not modified during execution.
/// Performance: O(n) where n is items.Count - uses reservoir sampling algorithm. Materializes non-list collections.
/// Allocations: Uses pooled array for result (returned to pool when disposed). Materializes IEnumerable to array/list.
/// Edge Cases: count=0 returns empty enumerable. Uses Algorithm R (reservoir sampling) for uniform selection probability.
/// The returned array is pooled and will be returned to the pool - caller should not hold reference long-term.
///
public static IEnumerable NextSubset(
this IRandom random,
IEnumerable items,
int count
)
{
if (items == null)
{
throw new ArgumentNullException(nameof(items));
}
if (count < 0)
{
throw new ArgumentOutOfRangeException(nameof(count), "Count cannot be negative");
}
if (items is IReadOnlyList itemsList)
{
if (count > itemsList.Count)
{
throw new ArgumentException(
"Count cannot exceed the number of items",
nameof(count)
);
}
if (count == 0)
{
return Array.Empty();
}
return NextSubsetIterator(random, itemsList, count);
}
// Materialize enumerable to pooled list - AddRange is preferred for performance:
// it checks for ICollection and pre-allocates, and uses Array.Copy for arrays/lists
using PooledResource> lease = Buffers.List.Get(out List materializedList);
materializedList.AddRange(items);
if (count > materializedList.Count)
{
throw new ArgumentException(
"Count cannot exceed the number of items",
nameof(count)
);
}
if (count == 0)
{
return Array.Empty();
}
return NextSubsetIterator(random, materializedList, count);
}
private static IEnumerable NextSubsetIterator(
IRandom random,
IReadOnlyList items,
int count
)
{
using PooledArray arrayBuffer = SystemArrayPool.Get(count, out T[] result);
for (int i = 0; i < count; ++i)
{
result[i] = items[i];
}
for (int i = count; i < items.Count; ++i)
{
int j = random.Next(0, i + 1);
if (j < count)
{
result[j] = items[i];
}
}
for (int i = 0; i < count; ++i)
{
yield return result[i];
}
}
}
}