Liven up animal movement with a walking sway (presentation only)

AnimalBobSystem tilts a moving animal side-to-side via Transform2D.Rotation (a
gentle waddle) and eases it upright when standing. Phase runs on game time (frozen
on pause, faster when sped up) and is offset per entity id so animals sway out of
sync. Purely presentational -- the simulation ignores rotation, and the facing
system writes scale (not rotation), so no conflict. Corpses/eggs have no brain so
they stay still. Build + --check-content clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-06-14 23:29:14 +03:00
co-authored by Claude Opus 4.8
parent 405ed192a8
commit 0c07eba021
2 changed files with 47 additions and 0 deletions
+1
View File
@@ -323,6 +323,7 @@ public sealed class WorldScene : Scene
UpdateSystems.Add(new AnimalAppearanceSystem(_needs));
UpdateSystems.Add(new AnimalGrowthSystem(Context.Clock, SecondsPerDay, _animals, CellSize));
UpdateSystems.Add(new AnimalFacingSystem(_animals, CellSize));
UpdateSystems.Add(new AnimalBobSystem(Context.Clock));
UpdateSystems.Add(new AnimalMortalitySystem(Store, _animals, CellSize));
UpdateSystems.Add(new CorpseSystem(Store, Context.Clock, SecondsPerDay, climate));
UpdateSystems.Add(
+46
View File
@@ -1688,6 +1688,52 @@ public sealed class AnimalFacingSystem(AnimalSet animals, int cellSize)
}
}
/// <summary>
/// «Раскачка» при ходьбе (презентация): идущий зверь покачивается из стороны в сторону (наклон спрайта
/// через <see cref="Transform2D.Rotation"/>), стоящий — плавно выпрямляется. Делает движение живее, не
/// затрагивая симуляцию (она вращение не использует). Фаза покачивания идёт по игровому времени (стоп на
/// паузе, быстрее на ускорении) и сдвинута по id особи — звери качаются вразнобой. Граница sim/presentation
/// цела: система пишет только поворот трансформа.
/// </summary>
public sealed class AnimalBobSystem(GameClock clock) : QuerySystem<AnimalBrain, Transform2D>
{
private const float SwaySpeed = 9f; // скорость набега фазы (рад/сек игрового времени)
private const float SwayAmplitude = 0.08f; // амплитуда наклона (рад ≈ 4.6°)
private const float MinMoveSq = 0.5f * 0.5f; // ближе к цели — считаем стоящим
private const float UprightDecay = 0.85f; // как быстро выпрямляется стоя (за кадр)
private float _phase;
protected override void OnUpdate()
{
var dt = clock.DeltaTime;
if (dt <= 0f)
{
return; // пауза — не качаем и не выпрямляем
}
_phase += dt * SwaySpeed;
foreach (var (brains, transforms, entities) in Query.Chunks)
{
var b = brains.Span;
var t = transforms.Span;
for (var i = 0; i < b.Length; i++)
{
ref var rot = ref t[i].Rotation;
if ((b[i].Target - t[i].Position).LengthSquared() > MinMoveSq)
{
var offset = entities.EntityAt(i).Id * 0.7f; // десинхрон между особями
rot = MathF.Sin(_phase + offset) * SwayAmplitude;
}
else
{
rot *= UprightDecay; // у цели/стоя — плавно к вертикали
}
}
}
}
}
/// <summary>
/// Рост и стадии (фаза A3): копит возраст в игровых днях, переключает стадию (Baby→Juvenile→Adult→
/// Senior) по порогам из генов и при смене стадии меняет спрайт (детёныш/самка/самец) и размер.