Замер necrourn budget: тики всех урн приходят пачкой раз в ~1.4 с в порядке участков от ближних к игроку; лимит «3 за кадр» доставался трём ближним, 11 урн (сборщики, логист, дальние) не получали шага никогда - «стою в 19 м - не работают, подхожу - работают». Теперь 30 дел/с, запас 2 с, пополнение по реальному времени; отказанная урна в очереди и обслуживается первой. После: 578 с, 19 урн по ~128 шагов, отказов 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
240 lines
11 KiB
C#
240 lines
11 KiB
C#
using System.Collections;
|
||
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using UnityEngine.Scripting;
|
||
|
||
namespace NecromancerTome
|
||
{
|
||
/// <summary>
|
||
/// Меры нагрузки урн (решение 17.10: «общий бюджет шагов работы духов за кадр (очередь по кругу - нагрузка постоянна при
|
||
/// любом числе урн); полная анимация только ближайших ~15-20 духов; замер 100 урн»).
|
||
/// Бюджет: дел (пробный шаг + настоящий) в секунду - не больше StepsPerSecond на все урны сейва (23.10, было за кадр); урна, которой не досталось,
|
||
/// пробует в следующем кадре. Урна делает дело раз в CinerariumWork.StepSeconds, поэтому очередь сама идёт по кругу.
|
||
/// Анимация: двигаются (полёт, жесты, покачивание) только AnimateNearest ближайших к игроку духов, остальные замирают дома,
|
||
/// как уснувшие (CinerariumGenieMotion); работа у всех идёт.
|
||
/// Прилив (решение 17.10, «Прилив прячет некротехнологов»): ночью кровавой луны духи всех урн уходят в урны и работа стоит -
|
||
/// признак берётся раз за кадр.
|
||
/// </summary>
|
||
public static class CinerariumLoad
|
||
{
|
||
/// <summary>Дел в секунду на все урны (23.10, было «3 за кадр»). Замер: урна получает тик раз в ~1.4 с, и тики всех урн
|
||
/// приходят пачкой в одном кадре, в порядке участков от ближних к игроку. Лимит «3 за кадр» доставался трём ближним урнам,
|
||
/// дальние не получали шага никогда («стою в 19 м - не работают, подхожу - работают»). Теперь бюджет копится между
|
||
/// тиками (не больше StepsPerSecond за раз), и пачка обслуживает всех, у кого подошёл срок. Консоль: necrourn budget N.</summary>
|
||
public static float StepsPerSecond = 30f;
|
||
/// <summary>Запас бюджета, секунд (пачка тиков раз в ~1.4 с должна уместиться целиком).</summary>
|
||
public const float BurstSeconds = 2f;
|
||
static float _tokens;
|
||
static float _lastRefill;
|
||
/// <summary>Очередь (23.10, пользователь: «нужно иметь очередь урн»): урна, которой отказали, ждёт; пока очередь не пуста,
|
||
/// бюджет сначала ей - новая урна получит шаг, только если после всех ждущих ещё останется. Иначе ближние урны (их участки
|
||
/// идут первыми) снова забирали бы всё. Запись старше QueueForget секунд выбрасывается (урну сняли или участок выгружен).</summary>
|
||
static readonly Dictionary<Vector3i, float> _queue = new Dictionary<Vector3i, float>();
|
||
const float QueueForget = 10f;
|
||
static readonly List<Vector3i> _stale = new List<Vector3i>();
|
||
public static int AnimateNearest = 16;
|
||
|
||
static int _frame = -1;
|
||
static bool _tide;
|
||
|
||
static void NewFrame()
|
||
{
|
||
if (Time.frameCount != _frame)
|
||
{
|
||
_frame = Time.frameCount;
|
||
// По реальному времени с прошлого пополнения: NewFrame зовут только кадры с тиками урн (раз в ~1.4 с), и
|
||
// Time.deltaTime (один кадр) давал полшага на пачку (замер 23.10: 279 шагов за 600 с при лимите 30/с).
|
||
float now = Time.time;
|
||
_tokens = Mathf.Min(StepsPerSecond * BurstSeconds, _tokens + StepsPerSecond * Mathf.Max(0f, now - _lastRefill));
|
||
_lastRefill = now;
|
||
_stale.Clear();
|
||
foreach (KeyValuePair<Vector3i, float> kv in _queue)
|
||
{
|
||
if (now - kv.Value > QueueForget)
|
||
{
|
||
_stale.Add(kv.Key);
|
||
}
|
||
}
|
||
foreach (Vector3i p in _stale)
|
||
{
|
||
_queue.Remove(p);
|
||
}
|
||
_tide = SnakeMode.IsBloodTide();
|
||
}
|
||
}
|
||
|
||
/// <summary>Взять одно дело из бюджета кадра. _who - урна (учёт для necrourn budget).</summary>
|
||
public static bool TryTakeStep(Vector3i _who)
|
||
{
|
||
NewFrame();
|
||
bool waiting = _queue.ContainsKey(_who);
|
||
// Ждущая урна - если есть хоть один шаг; новая - только если хватит и всем ждущим.
|
||
if (_tokens < 1f || (!waiting && _tokens < 1f + _queue.Count))
|
||
{
|
||
if (!waiting)
|
||
{
|
||
_queue[_who] = Time.time;
|
||
}
|
||
Count(_denied, _who);
|
||
return false;
|
||
}
|
||
_tokens -= 1f;
|
||
_queue.Remove(_who);
|
||
Count(_granted, _who);
|
||
return true;
|
||
}
|
||
|
||
// Учёт бюджета (23.10: сборщики 600 с не получили ни одного шага, логист рядом - получал): кому дали, кому отказали.
|
||
static readonly Dictionary<Vector3i, int> _granted = new Dictionary<Vector3i, int>();
|
||
static readonly Dictionary<Vector3i, int> _denied = new Dictionary<Vector3i, int>();
|
||
static float _countSince;
|
||
static int _frames0;
|
||
|
||
static void Count(Dictionary<Vector3i, int> _d, Vector3i _who)
|
||
{
|
||
_d.TryGetValue(_who, out int n);
|
||
_d[_who] = n + 1;
|
||
}
|
||
|
||
/// <summary>Сводка с прошлого вызова и сброс (команда necrourn budget).</summary>
|
||
public static string Budget()
|
||
{
|
||
float sec = Time.time - _countSince;
|
||
int frames = Time.frameCount - _frames0;
|
||
string s = "за " + sec.ToString("0") + " с, кадров " + frames + ", лимит " + StepsPerSecond + "/с, в очереди " + _queue.Count + "; дано:";
|
||
foreach (KeyValuePair<Vector3i, int> kv in _granted)
|
||
{
|
||
s += " " + kv.Key + "=" + kv.Value;
|
||
}
|
||
s += "; отказано:";
|
||
foreach (KeyValuePair<Vector3i, int> kv in _denied)
|
||
{
|
||
s += " " + kv.Key + "=" + kv.Value;
|
||
}
|
||
_granted.Clear();
|
||
_denied.Clear();
|
||
_countSince = Time.time;
|
||
_frames0 = Time.frameCount;
|
||
return s;
|
||
}
|
||
|
||
/// <summary>Идёт ли кровавый прилив (раз за кадр).</summary>
|
||
public static bool Tide
|
||
{
|
||
get
|
||
{
|
||
NewFrame();
|
||
return _tide;
|
||
}
|
||
}
|
||
|
||
// ------------------------------------------------------------------
|
||
// Ближайшие духи.
|
||
// ------------------------------------------------------------------
|
||
|
||
static float _nextRank;
|
||
static readonly HashSet<CinerariumGenieMotion> _animated = new HashSet<CinerariumGenieMotion>();
|
||
static readonly List<CinerariumGenieMotion> _sorted = new List<CinerariumGenieMotion>();
|
||
|
||
/// <summary>Двигается ли дух: среди AnimateNearest ближайших к игроку (пересчёт раз в 0.5 с, без выделений памяти).</summary>
|
||
public static bool Animated(CinerariumGenieMotion _m)
|
||
{
|
||
if (Time.time >= _nextRank)
|
||
{
|
||
_nextRank = Time.time + 0.5f;
|
||
Rank();
|
||
}
|
||
return _animated.Contains(_m);
|
||
}
|
||
|
||
static void Rank()
|
||
{
|
||
_animated.Clear();
|
||
EntityPlayerLocal pl = GameManager.Instance?.World?.GetPrimaryPlayer();
|
||
if (pl == null)
|
||
{
|
||
return;
|
||
}
|
||
Vector3 me = pl.GetPosition() - Origin.position;
|
||
_sorted.Clear();
|
||
_sorted.AddRange(CinerariumGenie.Live);
|
||
if (_sorted.Count > AnimateNearest)
|
||
{
|
||
_sorted.Sort((a, b) => (a.transform.position - me).sqrMagnitude.CompareTo((b.transform.position - me).sqrMagnitude));
|
||
}
|
||
for (int i = 0; i < _sorted.Count && i < AnimateNearest; i++)
|
||
{
|
||
_animated.Add(_sorted[i]);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>necrofps [секунд] - средний и худший кадр за время замера (по умолчанию 5 с), число духов и урн; ответ - строкой
|
||
/// почтового ящика в лог. Для замера нагрузки урн (17.10: «замер 100 урн»).</summary>
|
||
[Preserve]
|
||
public class ConsoleCmdNecroFps : ConsoleCmdAbstract
|
||
{
|
||
public override string[] getCommands() => new[] { "necrofps" };
|
||
public override string getDescription() => "NecromancerTome (разработка): средний и худший кадр за N секунд";
|
||
public override bool AllowedInMainMenu => false;
|
||
|
||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||
{
|
||
float sec = _params.Count > 0 ? float.Parse(_params[0], System.Globalization.CultureInfo.InvariantCulture) : 5f;
|
||
bool uncap = _params.Count > 1 && _params[1] == "uncap";
|
||
SdtdConsole.Instance.Output("necrofps: замер " + sec + " с" + (uncap ? " без ограничения кадров" : ""));
|
||
ThreadManager.StartCoroutine(Measure(sec, uncap));
|
||
}
|
||
|
||
/// <summary>_uncap - на время замера снять вертикальную синхронизацию и предел кадров: при 60 fps по синхронизации
|
||
/// запас не виден (18.10, замер 100 урн).</summary>
|
||
static IEnumerator Measure(float _sec, bool _uncap)
|
||
{
|
||
int vs = QualitySettings.vSyncCount, fr = Application.targetFrameRate;
|
||
if (_uncap)
|
||
{
|
||
QualitySettings.vSyncCount = 0;
|
||
Application.targetFrameRate = -1;
|
||
yield return new WaitForSecondsRealtime(0.5f);
|
||
}
|
||
float end = Time.realtimeSinceStartup + _sec;
|
||
int frames = 0;
|
||
float worst = 0f, sum = 0f;
|
||
long gc0 = System.GC.CollectionCount(0);
|
||
while (Time.realtimeSinceStartup < end)
|
||
{
|
||
yield return null;
|
||
frames++;
|
||
sum += Time.unscaledDeltaTime;
|
||
worst = Mathf.Max(worst, Time.unscaledDeltaTime);
|
||
}
|
||
if (_uncap)
|
||
{
|
||
QualitySettings.vSyncCount = vs;
|
||
Application.targetFrameRate = fr;
|
||
}
|
||
float avg = frames > 0 ? sum / frames * 1000f : 0f;
|
||
Debug.Log("[NecromancerTome] mailbox: necrofps: кадров " + frames + ", средний " + avg.ToString("F1") + " мс (" + (avg > 0 ? 1000f / avg : 0f).ToString("F0") +
|
||
" fps), худший " + (worst * 1000f).ToString("F1") + " мс, сборок мусора " + (System.GC.CollectionCount(0) - gc0) +
|
||
", духов " + CinerariumGenie.Live.Count + ", урн в сейве " + CinerariumRegistry.All().Count);
|
||
}
|
||
}
|
||
|
||
/// <summary>necrotide on|off|auto - прилив для проверок без ожидания ночи кровавой луны (SnakeMode.ForceTide).</summary>
|
||
[Preserve]
|
||
public class ConsoleCmdNecroTide : ConsoleCmdAbstract
|
||
{
|
||
public override string[] getCommands() => new[] { "necrotide" };
|
||
public override string getDescription() => "NecromancerTome (разработка): прилив on|off|auto";
|
||
public override bool AllowedInMainMenu => false;
|
||
|
||
public override void Execute(List<string> _params, CommandSenderInfo _senderInfo)
|
||
{
|
||
string v = _params.Count > 0 ? _params[0] : "";
|
||
SnakeMode.ForceTide = v == "on" ? true : v == "off" ? (bool?)false : null;
|
||
SdtdConsole.Instance.Output("necrotide: " + (SnakeMode.ForceTide.HasValue ? (SnakeMode.ForceTide.Value ? "прилив" : "нет прилива") : "по часам") +
|
||
", сейчас " + (SnakeMode.IsBloodTide() ? "прилив" : "не прилив"));
|
||
}
|
||
}
|
||
}
|