riversong code showcase

This commit is contained in:
Daniele Marotta
2026-05-21 15:52:18 +02:00
commit 4c9eea1c02
462 changed files with 23406 additions and 0 deletions

View File

@@ -0,0 +1,102 @@
using Cysharp.Threading.Tasks;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.VFX;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingDeleteAnimation : MonoBehaviour
{
private static readonly int IntensityPropertyID = Shader.PropertyToID("Intensity");
private static readonly int SizePropertyID = Shader.PropertyToID("Size");
[SerializeField]
[ReadOnly]
private float _sinkingHeight;
[SerializeField]
private float _sinkingSpeed = 1;
[SerializeField]
private AnimationCurve _sinkingSpeedCurve = AnimationCurve.Constant(0, 1, 1);
[SerializeField]
private float _shakeFrequency = 10;
[SerializeField]
private float _shakeAmplitude = 0.1f;
[SerializeField]
private VisualEffect _vfxPrefab;
[SerializeField]
private int _vfxCount = 1;
[SerializeField]
private float _vfxInterval = 1;
[SerializeField]
private float _vfxIntensityDecay = 0.5f;
private void ComputeSinkingHeight()
{
var bounds = default(Bounds);
var renderers = GetComponentsInChildren<Renderer>();
foreach (var renderer in renderers) bounds.Encapsulate(renderer.bounds);
_sinkingHeight = bounds.size.y;
}
private void Awake()
{
if (_sinkingHeight == 0) ComputeSinkingHeight();
}
public async UniTask PlayAsync(Building building)
{
var initialPosition = transform.position;
var animatedPosition = transform.position;
var angularFrequency = 2 * Mathf.PI * _shakeFrequency;
var shakeWeights = new Vector3(Random.Range(-1, 1), 0, Random.Range(-1, 1));
for (var i = 0; i < _vfxCount; i++)
{
var delay = i * _vfxInterval;
var intensity = Mathf.Pow(_vfxIntensityDecay, i);
_ = PlayVfxAsync(building, initialPosition, delay, intensity);
}
var t = 0f;
while (animatedPosition.y > initialPosition.y - _sinkingHeight)
{
var dt = Time.deltaTime;
t += dt;
var sinkingSpeed = _sinkingSpeed * _sinkingSpeedCurve.Evaluate(t);
animatedPosition += Vector3.down * (sinkingSpeed * dt);
var shake = Mathf.Sin(t * angularFrequency) * _shakeAmplitude * _sinkingSpeed;
transform.position = animatedPosition + shake * shakeWeights;
await UniTask.NextFrame();
}
}
private async UniTask PlayVfxAsync(Building building, Vector3 position, float delay, float intensity)
{
if (delay > 0) await UniTask.WaitForSeconds(delay);
var vfx = Instantiate(_vfxPrefab, position, Quaternion.identity);
vfx.SetFloat(IntensityPropertyID, intensity);
vfx.SetVector2(SizePropertyID, new Vector2(building.Rect.Width, building.Rect.Height));
}
private void OnValidate()
{
ComputeSinkingHeight();
}
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct BuildingDeleteAnimationCompletedSignal
{
public Building Building;
public BuildingDeleteAnimationCompletedSignal(Building building)
{
Building = building;
}
}
}

View File

@@ -0,0 +1,123 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Animations;
using UnityEngine.Playables;
using UnityEngine.VFX;
namespace DanieleMarotta.RiversongCodeShowcase
{
[RequireComponent(typeof(Animator))]
public class BuildingPlacementAnimation : MonoBehaviour, IAnimationClipSource
{
[SerializeField]
private AnimationClip _animationClip;
[SerializeField]
private float _playbackSpeed = 1;
[SerializeField]
private Vector3 _positionLerpWeights = new(1.5f, 1, 1.5f);
[SerializeField]
private float _rotationLerpWeight = 1.5f;
[SerializeField]
private VisualEffect _impactVfxPrefab;
[SerializeField]
private float _impactIntensityDecay = 0.75f;
[SerializeField]
[HideInInspector]
private float _animationProgress;
[SerializeField]
[HideInInspector]
private float _bounce;
private PlayableGraph _playableGraph;
private AnimationClipPlayable _playableClip;
private BuildingDefinition _building;
private Vector3 _startPosition;
private Vector3 _finalPosition;
private Quaternion _finalRotation;
public bool IsPlaying { get; private set; }
private void Awake()
{
_playableGraph = PlayableGraph.Create();
_playableGraph.SetTimeUpdateMode(DirectorUpdateMode.Manual);
_playableClip = AnimationClipPlayable.Create(_playableGraph, _animationClip);
_playableClip.SetSpeed(_playbackSpeed);
var animator = GetComponent<Animator>();
var output = AnimationPlayableOutput.Create(_playableGraph, nameof(BuildingPlacementAnimation), animator);
output.SetSourcePlayable(_playableClip);
}
private void OnDestroy()
{
_playableGraph.Destroy();
}
private void Update()
{
if (!IsPlaying) return;
_playableGraph.Evaluate(Time.unscaledDeltaTime);
var position = transform.position;
position.x = Mathf.Lerp(_startPosition.x, _finalPosition.x, _positionLerpWeights.x * _animationProgress);
position.y = Mathf.Lerp(_startPosition.y, _finalPosition.y, _positionLerpWeights.y * _bounce);
position.z = Mathf.Lerp(_startPosition.z, _finalPosition.z, _positionLerpWeights.z * _animationProgress);
transform.position = position;
transform.rotation = Quaternion.Lerp(transform.rotation, _finalRotation, _rotationLerpWeight * _animationProgress);
}
public async UniTask PlayAsync(BuildingDefinition building, Vector3 startPosition, Vector3 finalPosition, Quaternion finalRotation)
{
IsPlaying = true;
_building = building;
_startPosition = startPosition;
_finalPosition = finalPosition;
_finalRotation = finalRotation;
_playableClip.SetTime(0);
_playableClip.SetTime(0);
_playableGraph.Play();
_animationProgress = 0;
while (_animationProgress < 1) await UniTask.NextFrame();
_playableGraph.Stop();
transform.SetPositionAndRotation(finalPosition, finalRotation);
IsPlaying = false;
}
public void GetAnimationClips(List<AnimationClip> results)
{
if (_animationClip) results.Add(_animationClip);
}
private void OnImpact(int impact)
{
var impactVfx = Instantiate(_impactVfxPrefab, transform.position, transform.rotation);
var intensity = Mathf.Pow(_impactIntensityDecay, impact);
var size = new Vector2(_building.Width, _building.Height);
DustVfxProperties.Setup(impactVfx, intensity, size);
}
}
}

View File

@@ -0,0 +1,18 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct BuildingPlacementAnimationCompletedSignal
{
public BuildingDefinition Building;
public TileRect Rect;
public Directions Orientation;
public BuildingPlacementAnimationCompletedSignal(BuildingDefinition building, TileRect rect, Directions orientation)
{
Building = building;
Rect = rect;
Orientation = orientation;
}
}
}

View File

@@ -0,0 +1,18 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct BuildingPlacementAnimationStartedSignal
{
public BuildingDefinition Building;
public TileRect Rect;
public Directions Orientation;
public BuildingPlacementAnimationStartedSignal(BuildingDefinition building, TileRect rect, Directions orientation)
{
Building = building;
Rect = rect;
Orientation = orientation;
}
}
}

View File

@@ -0,0 +1,75 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingStorageVisualization : MonoBehaviour
{
[PropertySpace(SpaceAfter = 20)]
[SerializeField]
private List<Transform> _slots;
[VerticalGroup("Arrange Slots/Controls")]
[LabelText("Min")]
[SerializeField]
private Vector2 _boundsMin;
[VerticalGroup("Arrange Slots/Controls")]
[LabelText("Max")]
[SerializeField]
private Vector2 _boundsMax;
[VerticalGroup("Arrange Slots/Controls")]
[SerializeField]
private int _columns = 4;
public int SlotCount => _slots.Count;
public Transform GetSlot(int slot)
{
return _slots[slot];
}
public void SetProductVisualization(int slotIndex, GameObject productVisualization)
{
productVisualization.transform.SetParent(GetSlot(slotIndex));
productVisualization.transform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
}
public bool TryGetProductVisualization(int slotIndex, out GameObject productVisualization)
{
var slot = GetSlot(slotIndex);
productVisualization = slot.childCount > 0 ? slot.GetChild(0).gameObject : null;
return productVisualization;
}
[HorizontalGroup("Arrange Slots", 200)]
[Button(ButtonSizes.Large)]
[GUIColor("cyan")]
private void ArrangeSlots()
{
var rows = Mathf.CeilToInt((float)_slots.Count / _columns);
var w = _boundsMax.x - _boundsMin.x;
var h = _boundsMax.y - _boundsMin.y;
var spacing = new Vector2(w, h) / new Vector2(_columns, rows);
for (var i = 0; i < _slots.Count; i++)
{
var slot = _slots[i];
var row = i / _columns;
var column = i % _columns;
var p = slot.position;
p.x = _boundsMin.x + (0.5f + column) * spacing.x;
p.z = _boundsMin.y + (0.5f + row) * spacing.y;
slot.position = p;
}
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingStorageVisualizationSystem : GameSystem, IInitializable, IDisposable, IUpdatable
{
private const int StackProductCount = 10;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IPoolingService _poolingService;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IBuildingVisualizationCollection _buildingVisualizations;
[InjectService]
private ISignalBus _signalBus;
public BuildingStorageVisualizationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<BuildingDeletedSignal>(OnBuildingDeleted);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<BuildingDeletedSignal>(OnBuildingDeleted);
}
public void Update()
{
foreach (var storehouse in _entityCache.GetStorageBuildings()) UpdateStorehouse(storehouse);
}
private void UpdateStorehouse(Building storehouse)
{
if (!_buildingVisualizations.TryGetVisualization(storehouse.Id, out var buildingVisualization)) return;
var storageVisualization = buildingVisualization.GetComponent<BuildingStorageVisualization>();
if (storageVisualization.SlotCount <= 0) return;
ReleaseProductVisualizations(storageVisualization);
ref var storage = ref storehouse.GetStorageRW();
var slotIndex = 0;
foreach (var (productHandle, amount) in storage)
{
var product = _productCatalog.GetProduct(productHandle);
var poolKey = ((GameObject)product.ProductStackVisualization.Asset).GetInstanceID();
var amountLeft = amount;
while (amountLeft > 0)
{
var productVisualization = _poolingService.Get<GameObject>(poolKey);
storageVisualization.SetProductVisualization(slotIndex, productVisualization);
if (++slotIndex >= storageVisualization.SlotCount) return;
amountLeft -= StackProductCount;
}
}
}
private void OnBuildingDeleted(BuildingDeletedSignal signal)
{
if (!signal.Building.Definition.IsStorage) return;
_buildingVisualizations.TryGetVisualization(signal.Building.Id, out var buildingVisualization);
var storageVisualization = buildingVisualization.GetComponent<BuildingStorageVisualization>();
ReleaseProductVisualizations(storageVisualization);
}
private void ReleaseProductVisualizations(BuildingStorageVisualization storageVisualization)
{
for (var i = 0; i < storageVisualization.SlotCount; i++)
{
if (!storageVisualization.TryGetProductVisualization(i, out var productVisualization)) return;
_poolingService.Release(productVisualization);
}
}
}
}

View File

@@ -0,0 +1,71 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.VFX;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingUpgradeAnimation : MonoBehaviour
{
[SerializeField]
private VisualEffect _vfxPrefab;
[SerializeField]
private float _stretchDuration = 0.25f;
[SerializeField]
private float _stretchFactor = 1.2f;
[SerializeField]
private float _settleDuration = 0.75f;
[SerializeField]
private float _settleElasticity = 4.5f;
public async UniTask PlayAsync(Building building)
{
var vfx = Instantiate(_vfxPrefab, transform.position, transform.rotation);
DustVfxProperties.Setup(vfx, 1, new Vector2(building.Rect.Width, building.Rect.Height));
var fromScale = transform.localScale;
var toScale = new Vector3(fromScale.x, fromScale.y * _stretchFactor, fromScale.z);
var elapsed = 0f;
while (elapsed < _stretchDuration)
{
elapsed += Time.unscaledDeltaTime;
var t = EaseOutBack(Mathf.Clamp01(elapsed / _stretchDuration));
transform.localScale = Vector3.LerpUnclamped(fromScale, toScale, t);
await UniTask.NextFrame();
}
elapsed = 0;
while (elapsed < _settleDuration)
{
elapsed += Time.unscaledDeltaTime;
var t = EaseOutElastic(Mathf.Clamp01(elapsed / _settleDuration), _settleElasticity);
transform.localScale = Vector3.LerpUnclamped(toScale, fromScale, t);
await UniTask.NextFrame();
}
transform.localScale = fromScale;
}
private static float EaseOutBack(float t)
{
const float c1 = 1.70158f;
const float c3 = c1 + 1;
return 1 + c3 * Mathf.Pow(t - 1, 3) + c1 * Mathf.Pow(t - 1, 2);
}
private static float EaseOutElastic(float t, float elasticity)
{
var c4 = 2 * Mathf.PI / elasticity;
return t switch
{
0 => 0,
1 => 1,
_ => Mathf.Pow(2, -10 * t) * Mathf.Sin((10 * t - 0.75f) * c4) + 1
};
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildingVisualization : MonoBehaviour
{
public List<GameObject> Tiers;
public void SetTier(int tier)
{
for (var i = 0; i < Tiers.Count; i++) Tiers[i].SetActive(i == tier);
}
}
}

View File

@@ -0,0 +1,15 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct BuildingVisualizationCreatedSignal
{
public Building Building;
public BuildingVisualization Visualization;
public BuildingVisualizationCreatedSignal(Building building, BuildingVisualization visualization)
{
Building = building;
Visualization = visualization;
}
}
}

View File

@@ -0,0 +1,163 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IBuildingVisualizationCollection))]
public class BuildingVisualizationManager : GameSystem, IInitializable, IDisposable, IBuildingVisualizationCollection
{
private const int SpatialLookupCellSize = 5;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IScene _scene;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private World _world;
private HashSet<int> _pendingVisualizations = new();
private Dictionary<int, BuildingVisualization> _visualizations = new();
private SpatialLookup<GameObject> _allVisualizationsSpatialLookup = new(SpatialLookupCellSize);
private SpatialLookup<GameObject> _canBeDeletedSpatialLookup = new(SpatialLookupCellSize);
public BuildingVisualizationManager(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<BuildingCreatedSignal>(OnBuildingCreated);
_signalBus.Subscribe<CollectDeletedGameObjectsSignal>(OnCollectDeletedGameObjects);
_signalBus.Subscribe<BuildingDeletedSignal>(OnBuildingDeleted);
_signalBus.Subscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<BuildingCreatedSignal>(OnBuildingCreated);
_signalBus.Unsubscribe<CollectDeletedGameObjectsSignal>(OnCollectDeletedGameObjects);
_signalBus.Unsubscribe<BuildingDeletedSignal>(OnBuildingDeleted);
_signalBus.Unsubscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
}
private void OnBuildingCreated(BuildingCreatedSignal signal)
{
var building = signal.Building;
_pendingVisualizations.Add(building.Id);
_ = CreateVisualizationAsync(building);
}
private async UniTask CreateVisualizationAsync(Building building)
{
var folder = _scene.SceneFolders.Buildings;
var visualizationGameObject = await building.Definition.Visualization.InstantiateAsync(folder);
if (!_pendingVisualizations.Remove(building.Id))
{
building.Definition.Visualization.ReleaseInstance(visualizationGameObject);
return;
}
var visualization = visualizationGameObject.GetComponent<BuildingVisualization>();
if (!visualization)
{
Debug.LogError($"Building visualization '{visualizationGameObject.name}' has no {nameof(BuildingVisualization)} component");
return;
}
visualization.SetTier(0);
var position = _tileSpace.GetRectWorldCenter(building.Rect);
var rotation = building.Orientation.ToQuaternion();
visualization.transform.SetPositionAndRotation(position, rotation);
_visualizations.Add(building.Id, visualization);
_signalBus.Raise(new BuildingVisualizationCreatedSignal(building, visualization));
_allVisualizationsSpatialLookup.Add(visualization.gameObject, building.Rect, building.Id);
if (building.Definition.CanBeDeleted) _canBeDeletedSpatialLookup.Add(visualization.gameObject, building.Rect, building.Id);
}
private void OnCollectDeletedGameObjects(CollectDeletedGameObjectsSignal signal)
{
if ((signal.Filter & DeletedGameObjectsFilter.Buildings) == 0) return;
_canBeDeletedSpatialLookup.Find(signal.Rect, signal.GameObjects);
}
private void OnBuildingDeleted(BuildingDeletedSignal signal)
{
var building = signal.Building;
if (_pendingVisualizations.Remove(building.Id))
{
_signalBus.Raise(new BuildingDeleteAnimationCompletedSignal(building));
return;
}
_visualizations.Remove(building.Id, out var visualization);
_allVisualizationsSpatialLookup.Remove(building.Rect, building.Id);
_canBeDeletedSpatialLookup.Remove(building.Rect, building.Id);
if (signal.Options == DeleteBuildingOptions.Silent)
{
FinalizeBuildingDeletion(building, visualization);
return;
}
_ = PlayDeleteAnimationAsync(building, visualization);
}
private async UniTask PlayDeleteAnimationAsync(Building building, BuildingVisualization visualization)
{
var deleteAnimation = visualization.GetComponent<BuildingDeleteAnimation>();
await deleteAnimation.PlayAsync(building);
FinalizeBuildingDeletion(building, visualization);
}
private void FinalizeBuildingDeletion(Building building, BuildingVisualization visualization)
{
_signalBus.Raise(new BuildingDeleteAnimationCompletedSignal(building));
building.Definition.Visualization.ReleaseInstance(visualization.gameObject);
}
private void OnBuildingUpgraded(BuildingUpgradedSignal signal)
{
var building = signal.Building;
var visualization = _visualizations[building.Id];
visualization.SetTier(building.TierIndex);
if (visualization.TryGetComponent<BuildingUpgradeAnimation>(out var animation)) _ = animation.PlayAsync(building);
}
public bool IsVisualizationPending(int id)
{
return _pendingVisualizations.Contains(id);
}
public bool TryGetVisualization(int id, out BuildingVisualization visualization)
{
return _visualizations.TryGetValue(id, out visualization);
}
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IBuildingVisualizationCollection
{
bool IsVisualizationPending(int id);
bool TryGetVisualization(int id, out BuildingVisualization visualization);
}
}

View File

@@ -0,0 +1,9 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public abstract class ProducerAnimation : MonoBehaviour
{
public abstract void UpdateAnimation(in BuildingProductionState productionState);
}
}

View File

@@ -0,0 +1,32 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EconomySystemGroup))]
[UpdateAfter(typeof(ProductionTickGameSystem))]
public class ProducerAnimationSystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IBuildingVisualizationCollection _buildingVisualizations;
public ProducerAnimationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (var producer in _entityCache.GetProducers()) UpdateProducer(producer);
}
private void UpdateProducer(Building producer)
{
if (!_buildingVisualizations.TryGetVisualization(producer.Id, out var buildingVisualization)) return;
if (!buildingVisualization.TryGetComponent<ProducerAnimation>(out var producerAnimation)) return;
ref var productionState = ref producer.GetProductionStateRW();
producerAnimation.UpdateAnimation(productionState);
}
}
}

View File

@@ -0,0 +1,35 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class WindmillProducerAnimation : ProducerAnimation
{
[SerializeField]
private Transform _rotationTarget;
[SerializeField]
private float _rotationSpeed = 180;
[SerializeField]
private float _rampUpTime = 0.5f;
[SerializeField]
private float _slowDownTime = 0.5f;
private float _currentRotationSpeed;
public override void UpdateAnimation(in BuildingProductionState productionState)
{
if (!_rotationTarget) return;
var isWorking = productionState.State == ProducerState.Working;
var targetRotationSpeed = isWorking ? _rotationSpeed : 0;
var rampTime = isWorking ? _rampUpTime : _slowDownTime;
var maxDelta = rampTime > 0 ? _rotationSpeed / rampTime * Time.deltaTime : _rotationSpeed;
_currentRotationSpeed = Mathf.MoveTowards(_currentRotationSpeed, targetRotationSpeed, maxDelta);
var rotationDelta = _currentRotationSpeed * Time.deltaTime;
_rotationTarget.localRotation *= Quaternion.Euler(0, 0, rotationDelta);
}
}
}