146 lines
4.3 KiB
C#
146 lines
4.3 KiB
C#
|
|
using UnityEngine;
|
|||
|
|
using System;
|
|||
|
|
|
|||
|
|
namespace SLSUtilities.General
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 伪随机分布 (Pseudo-Random Distribution, PRD) 计算器。
|
|||
|
|
/// 给定目标概率 P,算法会得出一个常数 C。
|
|||
|
|
/// 第一次判定的概率为 C,第二次为 2C,第 N 次为 NC。
|
|||
|
|
/// 在判定成功后,概率回归为 C。
|
|||
|
|
/// </summary>
|
|||
|
|
[Serializable]
|
|||
|
|
public class StableProbability
|
|||
|
|
{
|
|||
|
|
[SerializeField, Range(0, 1)]
|
|||
|
|
private float _probability;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 设定的目标概率 (0.0 ~ 1.0)。
|
|||
|
|
/// 每次更改时将自动重新计算常量 C。
|
|||
|
|
/// </summary>
|
|||
|
|
public float Probability
|
|||
|
|
{
|
|||
|
|
get => _probability;
|
|||
|
|
set
|
|||
|
|
{
|
|||
|
|
float clamped = Mathf.Clamp01(value);
|
|||
|
|
if (Mathf.Abs(_probability - clamped) > 0.0001f)
|
|||
|
|
{
|
|||
|
|
_probability = clamped;
|
|||
|
|
C = GetCFromP(_probability);
|
|||
|
|
//Reset();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public float C { get; private set; }
|
|||
|
|
public int N { get; private set; } = 1;
|
|||
|
|
|
|||
|
|
public StableProbability()
|
|||
|
|
{
|
|||
|
|
_probability = 0f;
|
|||
|
|
C = 0f;
|
|||
|
|
N = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public StableProbability(float probability)
|
|||
|
|
{
|
|||
|
|
_probability = Mathf.Clamp01(probability);
|
|||
|
|
C = GetCFromP(_probability);
|
|||
|
|
N = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 进行一次概率判定。
|
|||
|
|
/// </summary>
|
|||
|
|
/// <param name="retainRealProbability">如果为 true,则在失败时不增加 N,从而保持每次判定的实际概率不变;如果为 false,则在失败时增加 N,使得每次判定的实际概率逐渐接近 1。</param>
|
|||
|
|
/// <returns>判定是否成功。如果成功,累积次数归 1;如果失败,次数递增。</returns>
|
|||
|
|
public bool Roll(bool retainRealProbability = false)
|
|||
|
|
{
|
|||
|
|
if (_probability <= 0f) return false;
|
|||
|
|
if (_probability >= 1f) return true;
|
|||
|
|
|
|||
|
|
float currentProb = N * C;
|
|||
|
|
if (UnityEngine.Random.value <= currentProb)
|
|||
|
|
{
|
|||
|
|
N = 1;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
if (!retainRealProbability) N++;
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 重置当前的失败次数积累。
|
|||
|
|
/// </summary>
|
|||
|
|
public void Reset()
|
|||
|
|
{
|
|||
|
|
N = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 强制重新计算自身的C值(通常用于反序列化之后)
|
|||
|
|
/// </summary>
|
|||
|
|
public void RecalculateC()
|
|||
|
|
{
|
|||
|
|
C = GetCFromP(_probability);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 基于目标概率 P 计算常量 C。(基于二分查找算法)
|
|||
|
|
/// </summary>
|
|||
|
|
public static float GetCFromP(float p)
|
|||
|
|
{
|
|||
|
|
if (p <= 0f) return 0f;
|
|||
|
|
if (p >= 1f) return 1f;
|
|||
|
|
|
|||
|
|
float low = 0f;
|
|||
|
|
// C 的绝对上限不能超过 P 本身
|
|||
|
|
float high = p;
|
|||
|
|
float mid = 0f;
|
|||
|
|
|
|||
|
|
// 20次迭代二分查找精度足够
|
|||
|
|
for (int i = 0; i < 20; i++)
|
|||
|
|
{
|
|||
|
|
mid = (low + high) / 2f;
|
|||
|
|
float pTested = SimulatePFromC(mid);
|
|||
|
|
|
|||
|
|
if (Mathf.Abs(pTested - p) < 0.0001f)
|
|||
|
|
break;
|
|||
|
|
|
|||
|
|
if (pTested < p)
|
|||
|
|
low = mid;
|
|||
|
|
else
|
|||
|
|
high = mid;
|
|||
|
|
}
|
|||
|
|
return mid;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 给定常量 C,计算出整体期望表现出的概率 P。
|
|||
|
|
/// </summary>
|
|||
|
|
private static float SimulatePFromC(float C)
|
|||
|
|
{
|
|||
|
|
if (C <= 0f) return 0f;
|
|||
|
|
|
|||
|
|
float expectedTrials = 0f;
|
|||
|
|
float waitProbability = 1f; // 到目前为止仍然未触发的概率
|
|||
|
|
|
|||
|
|
int maxN = Mathf.CeilToInt(1f / C);
|
|||
|
|
for (int n = 1; n <= maxN; n++)
|
|||
|
|
{
|
|||
|
|
float pSuccess = Mathf.Min(n * C, 1f);
|
|||
|
|
float pSuccessOnN = pSuccess * waitProbability;
|
|||
|
|
waitProbability *= (1f - pSuccess);
|
|||
|
|
|
|||
|
|
expectedTrials += n * pSuccessOnN;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return 1f / expectedTrials;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|