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;
|
||
}
|
||
}
|
||
} |