Skip to content

Instantly share code, notes, and snippets.

@FleshMobProductions
Last active July 5, 2022 16:05
Show Gist options
  • Save FleshMobProductions/05cdf911e1dcec42dd31e07113c58108 to your computer and use it in GitHub Desktop.
Save FleshMobProductions/05cdf911e1dcec42dd31e07113c58108 to your computer and use it in GitHub Desktop.
Weighted Randomness class for Unity
using UnityEngine;
namespace FMPUtils.Randomness
{
[System.Serializable]
public class WeightedElement<T>
{
public T value;
[Range(0, 100)]
public int weight = 1;
}
// ToDo: implement weightedValues as list?
// The way the generics are done is to have a serialization support in unity later one for weightedValues
/// <summary>
/// Class for grabbing a random value from a weightened value array,
/// Either use GetRandomValue() to get a random value without weighting, or use
/// GetRandomValueWeighted() to get a random value where the randomness weight is considered,
/// whereas values with higher weight are more likely to be selected
/// </summary>
[System.Serializable]
public class WeightedRandomizer<T, WeightedT> where WeightedT : WeightedElement<T>, new()
{
[SerializeField] protected WeightedT[] weightedValues;
public WeightedT[] WeightedValues { get { return weightedValues; } }
public void AddValue(T newValue, int weight, bool preventAddIfSameElementExists = false)
{
if (preventAddIfSameElementExists)
{
for (int i = 0; i < weightedValues.Length; i++)
{
if (weightedValues[i].value.Equals(newValue))
{
weightedValues[i].weight = weight;
return;
}
}
}
WeightedT[] valuesNew = new WeightedT[weightedValues.Length + 1];
for (int i = 0; i < weightedValues.Length; i++)
{
valuesNew[i] = weightedValues[i];
}
var newWeightedEntry = new WeightedT();
newWeightedEntry.value = newValue;
newWeightedEntry.weight = weight;
valuesNew[valuesNew.Length - 1] = newWeightedEntry;
weightedValues = valuesNew;
}
/// <summary>
/// If value already exists, its weight will be updated, otherwise it will be added
/// </summary>
/// <param name="newValue"></param>
/// <param name="weight"></param>
public void AddOrUpdateValue(T value, int weight)
{
AddValue(value, weight, true);
}
public void RemoveValue(T valueToRemove)
{
int indexOf = -1;
for (int i = 0; i < weightedValues.Length; i++)
{
if (weightedValues[i].value.Equals(valueToRemove))
{
indexOf = i;
break;
}
}
if (indexOf >= 0)
{
WeightedT[] clipsNew = new WeightedT[weightedValues.Length - 1];
int newIndex = 0;
for (int i = 0; i < weightedValues.Length; i++)
{
if (i != indexOf)
{
clipsNew[newIndex] = weightedValues[i];
}
newIndex++;
}
weightedValues = clipsNew;
}
}
public T GetRandomValue()
{
return weightedValues[UnityEngine.Random.Range(0, weightedValues.Length)].value;
}
/// <summary>
/// Selects a random value while considering the assigned weights
/// </summary>
/// <returns></returns>
public T GetRandomValueWeighted()
{
int weightSum = 0;
for (int i = 0; i < weightedValues.Length; i++)
{
weightSum += weightedValues[i].weight;
}
int randomVal = UnityEngine.Random.Range(0, weightSum);
int weightSumTemp = 0;
for (int i = 0; i < weightedValues.Length; i++)
{
weightSumTemp += weightedValues[i].weight;
if (randomVal < weightSumTemp)
{
return weightedValues[i].value;
}
}
return weightedValues[weightedValues.Length - 1].value;
}
}
}
using UnityEngine;
namespace FMPUtils.Randomness
{
public class WeightedRandomPrefabSupply : MonoBehaviour
{
[System.Serializable]
public class WeightedGameObjectElement : WeightedElement<GameObject>
{
}
[System.Serializable]
public class WeightedGameObjectRandomizer : WeightedRandomizer<GameObject, WeightedGameObjectElement>
{
}
[SerializeField] private WeightedGameObjectRandomizer elements;
public GameObject GetRandomPrefab()
{
return elements.GetRandomValue();
}
public GameObject GetRandomPrefabWeighted()
{
return elements.GetRandomValueWeighted();
}
}
}
using UnityEngine;
namespace FMPUtils.Randomness
{
public class WeightedRandomUtility
{
/// <summary>
/// Takes an AnimationCurve that stores the weight distribution which will be applied as weight values to elmeents in the passed WeightenedElement array
/// </summary>,
/// <typeparam name="T">WeightedElement type</typeparam>
/// <param name="elements">The elements to apply the weight to</param>
/// <param name="weightCurve">Weight value curve, the time of the curve should go from 0 (first elment) to 1 (last element)</param>
/// <param name="weightMultiplier">multiplier applied to the weight curve values (because WeightedElement uses Int32 as weight, so we should stay outside of fraction ranges)</param>
public static void AssignWeightsFromNormalizedCurve<T>(WeightedElement<T>[] elements, AnimationCurve weightCurve, float weightMultiplier = 1f)
{
for (int i = 0; i < elements.Length; i++)
{
float sampleTime = ((float)i) / (elements.Length - 1);
// Simple round
int weight = (int)(weightCurve.Evaluate(sampleTime) * weightMultiplier + 0.5f);
elements[i].weight = weight;
}
}
/// <summary>
/// Takes an AnimationCurve that stores the weight distribution which will be applied as weight values to elmeents in the passed WeightenedElement array
/// </summary>,
/// <typeparam name="T">WeightedElement type</typeparam>
/// <param name="elements">The elements to apply the weight to</param>
/// <param name="weightCurve">Weight value curve. Needs at least 2 keyframes. takes the first keyframe time for the first element time and the last keyframe for the last element time</param>
/// <param name="weightMultiplier">multiplier applied to the weight curve values (because WeightedElement uses Int32 as weight, so we should stay outside of fraction ranges)</param>
public static void AssignWeightsFromCurve<T>(WeightedElement<T>[] elements, AnimationCurve weightCurve, float weightMultiplier = 1f)
{
var keys = weightCurve.keys;
if (keys == null)
{
Debug.LogError($"AssignWeightsFromCurve: AnimationCurve.keys are null");
return;
}
else if (keys.Length < 2)
{
Debug.LogError($"AssignWeightsFromCurve: AnimationCurve.keys need to have at least 2 keys");
return;
}
float startTime = keys[0].time;
float endTime = keys[keys.Length - 1].time;
float startToEnd = endTime - startTime;
float elementTimeDelta = startToEnd / (elements.Length - 1);
for (int i = 0; i < elements.Length; i++)
{
float sampleTime = startTime + i * elementTimeDelta;
// Simple round
int weight = (int)(weightCurve.Evaluate(sampleTime) * weightMultiplier + 0.5f);
elements[i].weight = weight;
}
}
}
}
@WooshiiDev
Copy link

Hey nice job!
One recommendation to this would be to remove the range limit on the weight and set the min/max on the array instead.

Better value validation this way

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment