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,61 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class Building : Entity, IBuildingShape, IProductStorageEntity, IAgentSourceEntity
{
private ProductStorage _storage;
private ProductStoragePolicyState _productStoragePolicy;
private BuildingProductionState _productionState;
private AgentSourceState _agentsState;
private PopulationNeedsState _needsState;
private BuildingSleepState _sleepState;
public BuildingDefinition Definition { get; set; }
public TileRect Rect { get; set; }
public Directions Orientation { get; set; }
public int WeekCreated { get; set; }
public int TierIndex { get; set; }
public int PopulationCapacity { get; set; }
public float SpawnCooldown => Definition.SpawnCooldown;
public ref ProductStorage GetStorageRW()
{
return ref _storage;
}
public ref ProductStoragePolicyState GetProductStoragePolicyRW()
{
return ref _productStoragePolicy;
}
public ref BuildingProductionState GetProductionStateRW()
{
return ref _productionState;
}
public ref AgentSourceState GetAgentSourceStateRW()
{
return ref _agentsState;
}
public ref PopulationNeedsState GetNeedsStateRW()
{
return ref _needsState;
}
public ref BuildingSleepState GetSleepStateRW()
{
return ref _sleepState;
}
}
}

View File

@@ -0,0 +1,55 @@
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UpdateAfter(typeof(EditingStateGameSystem))]
public class BuildingAoESystem : GameSystem, IUpdatable
{
[InjectService]
private EditingState _editingState;
[InjectService]
private UIState _uiState;
[InjectService]
private IBuildingSpatialQuery _buildingSpatialQuery;
[InjectService]
private IAoERenderingService _aoeRenderingService;
public BuildingAoESystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var tool = _editingState.ActiveTool;
if (tool is BuildTool buildTool && buildTool.Preview.IsVisible) AddAoE(buildTool.Building, buildTool.BuildingRect);
var selectedBuilding = _uiState.SelectedBuilding;
if (selectedBuilding != null) AddAoE(selectedBuilding.Definition, selectedBuilding.Rect);
}
private void AddAoE(BuildingDefinition definition, TileRect rect)
{
var range = definition.Range;
if (range > 0) _aoeRenderingService.Add(definition.AoELayer, rect.Inflate(range));
if (definition.IsHouse)
{
using var providersScope = ListPool<Building>.Get(out var providers);
_buildingSpatialQuery.FindProvidersForHouse(rect, providers);
foreach (var provider in providers) _aoeRenderingService.Add(provider.Definition.AoELayer, provider.Rect.Inflate(provider.Definition.Range));
}
if (definition.IsStorage)
{
using var providersScope = ListPool<Building>.Get(out var providers);
_buildingSpatialQuery.FindProvidersForStorage(rect, providers);
foreach (var provider in providers) _aoeRenderingService.Add(provider.Definition.AoELayer, provider.Rect.Inflate(provider.Definition.Range));
}
}
}
}

View File

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

View File

@@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "BuildingDefinition", menuName = "Riversong Code Showcase/Building Definition")]
public class BuildingDefinition : GameDataAsset, IComparable<BuildingDefinition>
{
public int UIOrder;
[PreviewField]
public Sprite Icon;
public string BuildingName = "Building";
[TextArea(5, 10)]
public string BuildingDescription = "Building";
public AssetReferenceGameObject Visualization;
public List<ProductAmountAuthoring> BuildingMaterials;
public int Width = 1;
public int Height = 1;
public int Range;
[LabelText("AoE Layer")]
public int AoELayer;
public bool CanBeDeleted = true;
[TitleGroup("Workers")]
public int WorkerCount;
public float SpawnCooldown;
[TitleGroup("Storage")]
public int StorageCapacity = 1000;
[LabelText("Show Storage Tooltip?")]
public bool ShowStorageTooltip;
[TitleGroup("Harvester")]
[LabelText("Resource")]
public ResourceNodeDefinition HarvestedResource;
[TitleGroup("Hunter")]
public CritterDefinition TargetCritter;
[TitleGroup("Farm")]
public bool IsFarm;
[LabelText("Product")]
public ProductDefinition FarmProduct;
[HorizontalGroup("Farm/Dropped Amount")]
[LabelText("Amt. Min")]
public int MinDroppedAmount = 1;
[HorizontalGroup("Farm/Dropped Amount")]
[LabelText("Amt. Max")]
public int MaxDroppedAmount = 1;
[TitleGroup("Producer")]
public RecipeDefinition Recipe;
[LabelText("Delivers Output?")]
public bool DeliversOutput = true;
[TitleGroup("Provider")]
[LabelText("Products")]
public List<ProductAmountAuthoring> ProvidedProducts;
[LabelText("Fetches Products?")]
public bool FetchesProducts = true;
[TitleGroup("House")]
[LabelText("Is House?")]
public bool IsHouse;
[ShowIf("IsHouse")]
public List<HouseTierAuthoring> HouseTiers;
[TitleGroup("Storage")]
[LabelText("Is Storage?")]
public bool IsStorage;
[TitleGroup("Placement Rules")]
[LabelText("Near Water?")]
public bool NearWater;
[LabelText("Requires Fertile Tile?")]
public bool RequiresFertileTile;
public int CompareTo(BuildingDefinition other)
{
return UIOrder.CompareTo(other.UIOrder);
}
[Serializable]
public class HouseTierAuthoring
{
public int Capacity = 1;
public List<ProductAmountAuthoring> UpgradeMaterials;
public List<PopulationNeedAuthoring> Needs;
}
}
}

View File

@@ -0,0 +1,15 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct BuildingDeletedSignal
{
public Building Building;
public DeleteBuildingOptions Options;
public BuildingDeletedSignal(Building building, DeleteBuildingOptions options)
{
Building = building;
Options = options;
}
}
}

View File

@@ -0,0 +1,323 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IBuildingFactory))]
[Service(typeof(IDeleteBuildingService))]
[Service(typeof(IBuildingSpatialQuery))]
public class BuildingManagerSystem : GameSystem, IInitializable, IDisposable, IBuildingFactory, IDeleteBuildingService, IBuildingSpatialQuery
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IProductStorageManager _productStorageManager;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IProductStackFactory _productStackFactory;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IAgentFactory _agentFactory;
[InjectService]
private IFailedPathCache _failedPathCache;
[InjectService]
private World _world;
[InjectService]
private GameConfig _config;
private Func<Building, int, bool> _fetchValidator;
private Func<Building, int, bool> _deliveryValidator;
private Func<Building, int, bool> _storageRequestValidator;
private Func<Building, int, int> _sourceRangeFunc;
private Func<Building, int, int> _candidateRangeFunc;
private Func<Building, int, int> _infiniteRangeFunc;
public BuildingManagerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
_fetchValidator = FetchValidator;
_deliveryValidator = DeliveryValidator;
_storageRequestValidator = StorageRequestValidator;
_sourceRangeFunc = SourceRange;
_candidateRangeFunc = CandidateRange;
_infiniteRangeFunc = InfiniteRange;
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<ConstructionCompletedSignal>(OnConstructionCompleted);
_signalBus.Subscribe<DoDeleteToolSignal>(OnDoDeleteTool);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<ConstructionCompletedSignal>(OnConstructionCompleted);
_signalBus.Unsubscribe<DoDeleteToolSignal>(OnDoDeleteTool);
}
private void OnConstructionCompleted(ConstructionCompletedSignal signal)
{
var constructionSite = signal.ConstructionSite;
Create(constructionSite.Building, constructionSite.Rect, constructionSite.Orientation);
}
public Building Create(BuildingDefinition definition, TileRect rect, Directions orientation)
{
var building = _entityCollection.Create<Building>();
building.Definition = definition;
building.Rect = rect;
building.Orientation = orientation;
building.WeekCreated = _world.TimeState.TotalWeeks;
InitializeStorage(building);
InitializeProductionState(building);
InitializeAgentSource(building);
InitializeHouse(building);
InitializeSleepState(building);
_entityCollection.Add(building);
_signalBus.Raise(new BuildingCreatedSignal(building));
return building;
}
private void InitializeStorage(Building building)
{
var capacity = building.Definition.StorageCapacity;
if (capacity <= 0) return;
_productStorageManager.InitializeProductStorage(building, capacity);
}
private void InitializeProductionState(Building building)
{
var recipeDefinition = building.Definition.Recipe;
if (!recipeDefinition) return;
ref var productionState = ref building.GetProductionStateRW();
productionState.State = ProducerState.NotWorking;
productionState.Recipe.Inputs.Length = recipeDefinition.Inputs.Count;
for (var i = 0; i < recipeDefinition.Inputs.Count; i++)
{
var input = recipeDefinition.Inputs[i];
var productHandle = _productCatalog.GetHandle(input.Product);
productionState.Recipe.Inputs[i] = (productHandle, input.Amount);
}
productionState.Recipe.OutputProductHandle = _productCatalog.GetHandle(recipeDefinition.Output.Product);
productionState.Recipe.OutputAmount = recipeDefinition.Output.Amount;
productionState.Recipe.ProductionTime = recipeDefinition.ProductionTime;
}
private void InitializeAgentSource(Building building)
{
_agentFactory.InitializeAgentSource(building);
}
private void InitializeHouse(Building building)
{
if (!building.Definition.IsHouse) return;
var tiers = building.Definition.HouseTiers;
building.PopulationCapacity = tiers.Count > 0 ? tiers[0].Capacity : 0;
InitializeHouseNeeds(building);
}
private void InitializeHouseNeeds(Building building)
{
ref var needsState = ref building.GetNeedsStateRW();
var tiers = building.Definition.HouseTiers;
for (var i = 0; i < tiers.Count; i++)
foreach (var need in tiers[i].Needs)
needsState.Needs.Add(
new PopulationNeed
{
TierIndex = (byte)i,
Type = need.Type,
ProductHandle = _productCatalog.GetHandle(need.Product),
HappinessScore = (ushort)need.HappinessScore,
ConsumptionRate = (byte)need.ConsumptionRate,
FetchThreshold = (byte)need.FetchThreshold,
YieldOnFetch = (ushort)need.YieldOnFetch
});
needsState.UpgradeState = tiers.Count > 1 ? TierUpgradeState.NotReady : TierUpgradeState.MaxedOut;
needsState.Happiness = _config.Population.InitialHouseHappiness;
}
private void InitializeSleepState(Building building)
{
ref var sleepState = ref building.GetSleepStateRW();
sleepState.HasHomelessWorkers = false;
sleepState.EfficiencyModifier = _config.Economy.RestedWorkersEfficiencyModifier;
}
private void OnDoDeleteTool(DoDeleteToolSignal signal)
{
using var toDeleteScope = HashSetPool<int>.Get(out var toDelete);
foreach (var tile in TileRange.From(signal.Rect))
foreach (Building building in _entityCollection.GetInternalEntityList(typeof(Building)))
{
if (!building.Definition.CanBeDeleted || !building.Rect.Contains(tile)) continue;
toDelete.Add(building.Id);
break;
}
foreach (var id in toDelete)
{
var building = _entityCollection.Get<Building>(id);
Delete(building, DeleteBuildingOptions.WithFeedback);
}
}
public void Delete(Building building, DeleteBuildingOptions options)
{
_entityCollection.Remove(building.Id);
_productStackFactory.CreateProductStacksFrom(building);
_signalBus.Raise(new BuildingDeletedSignal(building, options));
}
public bool FindStorageForFetch(int sourceId, TileRect sourceRect, int productHandle, out Building building)
{
return FindReachableBuilding(sourceId, sourceRect, 0, productHandle, _entityCache.GetStorageBuildings(), _fetchValidator, _infiniteRangeFunc, out building);
}
public bool FindStorageForFetch(int sourceId, TileRect sourceRect, int sourceRange, int productHandle, out Building building)
{
return FindReachableBuilding(sourceId, sourceRect, sourceRange, productHandle, _entityCache.GetStorageBuildings(), _fetchValidator, _sourceRangeFunc, out building);
}
public bool FindStorageForDelivery(int sourceId, TileRect sourceRect, int productHandle, out Building building)
{
return FindReachableBuilding(sourceId, sourceRect, 0, productHandle, _entityCache.GetStorageBuildings(), _deliveryValidator, _infiniteRangeFunc, out building);
}
public bool FindStorageForRequest(int sourceId, TileRect sourceRect, int sourceRange, int productHandle, out Building building)
{
return FindReachableBuilding(
sourceId,
sourceRect,
sourceRange,
productHandle,
_entityCache.GetStorageRequestBuildings(),
_storageRequestValidator,
_infiniteRangeFunc,
out building);
}
public bool FindProviderForFetch(int sourceId, TileRect sourceRect, int productHandle, out Building building)
{
return FindReachableBuilding(sourceId, sourceRect, 0, productHandle, _entityCache.GetProviders(), _fetchValidator, _candidateRangeFunc, out building);
}
public void FindProvidersForHouse(TileRect sourceRect, List<Building> providers)
{
foreach (var provider in _entityCache.GetProviders())
if (TileMath.StepCount(provider.Rect, sourceRect) <= provider.Definition.Range)
providers.Add(provider);
}
public void FindProvidersForStorage(TileRect sourceRect, List<Building> providers)
{
foreach (var provider in _entityCache.GetProviders())
if (provider.Definition.FetchesProducts && TileMath.StepCount(provider.Rect, sourceRect) <= provider.Definition.Range)
providers.Add(provider);
}
private bool FindReachableBuilding(int sourceId,
TileRect sourceRect,
int sourceRange,
int productHandle,
List<Building> candidates,
Func<Building, int, bool> validator,
Func<Building, int, int> rangeFunc,
out Building building)
{
building = null;
var best = int.MaxValue;
foreach (var candidate in candidates)
{
if (_failedPathCache.IsKnownFailedPath(sourceId, candidate.Id)) continue;
var stepCount = TileMath.StepCount(sourceRect, candidate.Rect);
if (stepCount >= best || stepCount > rangeFunc.Invoke(candidate, sourceRange)) continue;
if (!validator.Invoke(candidate, productHandle)) continue;
best = stepCount;
building = candidate;
}
return best < int.MaxValue;
}
private bool FetchValidator(Building candidate, int productHandle)
{
ref var storage = ref candidate.GetStorageRW();
return storage.AvailableNow(productHandle) > 0;
}
private bool DeliveryValidator(Building candidate, int productHandle)
{
ref var storage = ref candidate.GetStorageRW();
ref var storagePolicy = ref candidate.GetProductStoragePolicyRW();
return storage.FreeSpace() > 0 && storagePolicy.IsTakingAllowed(productHandle);
}
private bool StorageRequestValidator(Building candidate, int productHandle)
{
ref var storage = ref candidate.GetStorageRW();
ref var storagePolicy = ref candidate.GetProductStoragePolicyRW();
return storage.CountIncludingReservations(productHandle) > storagePolicy.GetRequestedAmount(productHandle) && storagePolicy.CanFulfillRequests(productHandle);
}
private int SourceRange(Building candidate, int sourceRange)
{
return sourceRange;
}
private int CandidateRange(Building candidate, int sourceRange)
{
return candidate.Definition.Range;
}
private int InfiniteRange(Building candidate, int sourceRange)
{
return int.MaxValue;
}
}
}

View File

@@ -0,0 +1,110 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UpdateAfter(typeof(EditingStateGameSystem))]
public class BuildingSelectionSystem : GameSystem, IInitializable, IUpdatable
{
[InjectService]
private GameConfig _gameConfig;
[InjectService]
private IPointerService _pointerService;
[InjectService]
private UIState _uiState;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private ICancelAction _cancelAction;
[InjectService]
private EditingState _editingState;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IBuildingVisualizationCollection _buildingVisualizationCollection;
[InjectService]
private World _world;
[InjectService]
private MaterialReplacementCache _materialReplacementCache;
public BuildingSelectionSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_cancelAction.AddHandler(
(int)CancelActions.CancelSelection,
_ =>
{
if (_uiState.SelectedBuilding == null) return false;
SetSelectedBuilding(null);
return true;
});
return UniTask.CompletedTask;
}
public void Update()
{
if (_uiState.SelectedBuilding != null && !_entityCollection.Exists(_uiState.SelectedBuilding.Id))
{
SetSelectedBuilding(null);
return;
}
if (_editingState.ActiveTool != null)
{
SetSelectedBuilding(null);
return;
}
var buildingUnderPointer = GetBuildingUnderPointer();
if (_pointerService.TryConsumeLeftClick()) SetSelectedBuilding(buildingUnderPointer);
HighlightBuilding(buildingUnderPointer);
HighlightBuilding(_uiState.SelectedBuilding);
}
private Building GetBuildingUnderPointer()
{
if (_pointerService.IsPointerOverUI) return null;
if (!_pointerService.TryGetPositionOnTerrain(out var position)) return null;
var tile = _tileSpace.WorldToTile(position);
ref var entityIdMapValue = ref _world.EntityIdMap.GetValueRW(tile);
if (entityIdMapValue.BuildingId == Entity.InvalidId) return null;
return _entityCollection.TryGet<Building>(entityIdMapValue.BuildingId, out var building) ? building : null;
}
private void HighlightBuilding(Building building)
{
if (building == null) return;
if (!_buildingVisualizationCollection.TryGetVisualization(building.Id, out var visualization)) return;
_materialReplacementCache.ReplaceMaterials(visualization.gameObject, _gameConfig.UI.HighlightedGameObjectsMaterial);
}
private void SetSelectedBuilding(Building selectedBuilding)
{
if (_uiState.SelectedBuilding == selectedBuilding) return;
var oldSelection = _uiState.SelectedBuilding;
_uiState.SelectedBuilding = selectedBuilding;
_signalBus.Raise(new SelectedBuildingChangedSignal(oldSelection, _uiState.SelectedBuilding));
}
}
}

View File

@@ -0,0 +1,13 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct BuildingSleepState
{
public int RestedWorkerCount;
public int AllocatedWorkerCount;
public bool HasHomelessWorkers;
public float EfficiencyModifier;
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct ConstructionCompletedSignal
{
public ConstructionSite ConstructionSite;
public ConstructionCompletedSignal(ConstructionSite constructionSite)
{
ConstructionSite = constructionSite;
}
}
}

View File

@@ -0,0 +1,34 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class ConstructionSite : Entity, IBuildingShape, IProductStorageEntity, IAgentSourceEntity
{
private ProductStorage _storage;
private ProductStoragePolicyState _productStoragePolicy;
private AgentSourceState _agentSourceState;
public BuildingDefinition Building { get; set; }
public TileRect Rect { get; set; }
public Directions Orientation { get; set; }
public float SpawnCooldown => 0;
public ref ProductStorage GetStorageRW()
{
return ref _storage;
}
public ref ProductStoragePolicyState GetProductStoragePolicyRW()
{
return ref _productStoragePolicy;
}
public ref AgentSourceState GetAgentSourceStateRW()
{
return ref _agentSourceState;
}
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct ConstructionSiteDeletedSignal
{
public ConstructionSite ConstructionSite;
public ConstructionSiteDeletedSignal(ConstructionSite constructionSite)
{
ConstructionSite = constructionSite;
}
}
}

View File

@@ -0,0 +1,171 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class ConstructionSiteManager : GameSystem, IInitializable, IDisposable, IUpdatable
{
private const int SpatialLookupCellSize = 5;
private const int StorageCapacity = 100;
[InjectService]
private GameConfig _config;
[InjectService]
private IScene _scene;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IProductStorageManager _productStorageManager;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IProductStackFactory _productStackFactory;
[InjectService]
private IAgentFactory _agentFactory;
private Dictionary<int, GameObject> _visualizations = new();
private SpatialLookup<(ConstructionSite, GameObject)> _spatialLookup = new(SpatialLookupCellSize);
private MaterialPropertyBlock _materialPropertyBlock;
public ConstructionSiteManager(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<BuildingPlacementAnimationCompletedSignal>(OnBuildingPlaced);
_signalBus.Subscribe<CollectDeletedGameObjectsSignal>(OnCollectDeletedGameObjects);
_signalBus.Subscribe<DoDeleteToolSignal>(OnDoDeleteTool);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<BuildingPlacementAnimationCompletedSignal>(OnBuildingPlaced);
_signalBus.Unsubscribe<CollectDeletedGameObjectsSignal>(OnCollectDeletedGameObjects);
_signalBus.Unsubscribe<DoDeleteToolSignal>(OnDoDeleteTool);
}
private void OnBuildingPlaced(BuildingPlacementAnimationCompletedSignal signal)
{
var constructionSite = _entityCollection.Create<ConstructionSite>();
constructionSite.Building = signal.Building;
constructionSite.Rect = signal.Rect;
constructionSite.Orientation = signal.Orientation;
_productStorageManager.InitializeProductStorage(constructionSite, StorageCapacity);
_agentFactory.InitializeAgentSource(constructionSite);
_entityCollection.Add(constructionSite);
_ = CreateVisualizationAsync(constructionSite);
}
private async UniTask CreateVisualizationAsync(ConstructionSite constructionSite)
{
var position = _tileSpace.GetRectWorldCenter(constructionSite.Rect);
var rotation = constructionSite.Orientation.ToQuaternion();
var folder = _scene.SceneFolders.Buildings;
var visualization = await constructionSite.Building.Visualization.InstantiateAsync(position, rotation, folder);
ReplaceMaterials(visualization);
_visualizations.Add(constructionSite.Id, visualization);
_spatialLookup.Add((constructionSite, visualization), constructionSite.Rect, constructionSite.Id);
}
private void ReplaceMaterials(GameObject gameObject)
{
var material = _config.Buildings.ConstructionSiteMaterial;
using var renderersScope = ListPool<Renderer>.Get(out var renderers);
gameObject.GetComponentsInChildren(renderers);
_materialPropertyBlock ??= new MaterialPropertyBlock();
foreach (var renderer in renderers) MaterialReplacementCache.ReplaceRendererMaterials(renderer, material, _materialPropertyBlock);
}
public void Update()
{
using var toCompleteScope = ListPool<ConstructionSite>.Get(out var toComplete);
foreach (ConstructionSite constructionSite in _entityCollection.GetInternalEntityList(typeof(ConstructionSite)))
if (_visualizations.ContainsKey(constructionSite.Id) && AllBuildingMaterialsFetched(constructionSite))
toComplete.Add(constructionSite);
foreach (var constructionSite in toComplete)
{
_entityCollection.Remove(constructionSite.Id);
_visualizations.Remove(constructionSite.Id, out var visualization);
_spatialLookup.Remove(constructionSite.Rect, constructionSite.Id);
constructionSite.Building.Visualization.ReleaseInstance(visualization);
_signalBus.Raise(new ConstructionCompletedSignal(constructionSite));
}
}
private bool AllBuildingMaterialsFetched(ConstructionSite constructionSite)
{
ref var storage = ref constructionSite.GetStorageRW();
foreach (var (product, amount) in constructionSite.Building.BuildingMaterials)
{
var handle = _productCatalog.GetHandle(product);
if (storage.AvailableNow(handle) < amount) return false;
}
return true;
}
private void OnCollectDeletedGameObjects(CollectDeletedGameObjectsSignal signal)
{
if ((signal.Filter & DeletedGameObjectsFilter.Buildings) == 0) return;
using var lookupResultScope = ListPool<(ConstructionSite, GameObject)>.Get(out var lookupResult);
_spatialLookup.Find(signal.Rect, lookupResult);
foreach (var (_, visualization) in lookupResult) signal.GameObjects.Add(visualization);
}
private void OnDoDeleteTool(DoDeleteToolSignal signal)
{
using var lookupResultScope = ListPool<(ConstructionSite, GameObject)>.Get(out var lookupResult);
_spatialLookup.Find(signal.Rect, lookupResult);
foreach (var (constructionSite, visualization) in lookupResult)
{
_productStackFactory.CreateProductStacksFrom(constructionSite);
_entityCollection.Remove(constructionSite.Id);
_visualizations.Remove(constructionSite.Id);
_spatialLookup.Remove(constructionSite.Rect, constructionSite.Id);
constructionSite.Building.Visualization.ReleaseInstance(visualization);
_signalBus.Raise(new ConstructionSiteDeletedSignal(constructionSite));
}
}
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum DeleteBuildingOptions
{
WithFeedback,
Silent
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IBuildingFactory
{
Building Create(BuildingDefinition definition, TileRect rect, Directions orientation);
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IBuildingShape
{
TileRect Rect { get; }
}
}

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IBuildingSpatialQuery
{
bool FindStorageForFetch(int sourceId, TileRect sourceRect, int productHandle, out Building building);
bool FindStorageForFetch(int sourceId, TileRect sourceRect, int sourceRange, int productHandle, out Building building);
bool FindStorageForDelivery(int sourceId, TileRect sourceRect, int productHandle, out Building building);
bool FindStorageForRequest(int sourceId, TileRect sourceRect, int sourceRange, int productHandle, out Building building);
bool FindProviderForFetch(int sourceId, TileRect sourceRect, int productHandle, out Building building);
void FindProvidersForHouse(TileRect sourceRect, List<Building> providers);
void FindProvidersForStorage(TileRect sourceRect, List<Building> providers);
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IDeleteBuildingService
{
void Delete(Building building, DeleteBuildingOptions options);
}
}

View File

@@ -0,0 +1,15 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct SelectedBuildingChangedSignal
{
public Building OldSelection;
public Building NewSelection;
public SelectedBuildingChangedSignal(Building oldSelection, Building newSelection)
{
OldSelection = oldSelection;
NewSelection = newSelection;
}
}
}

View File

@@ -0,0 +1,44 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TentRemovalSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IDeleteBuildingService _deleteBuildingService;
[InjectService]
private IEntityCache _entityCache;
public TentRemovalSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<DayStartedSignal>(OnDayStarted);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<DayStartedSignal>(OnDayStarted);
}
private void OnDayStarted(DayStartedSignal signal)
{
var tents = _entityCache.GetTentBuildings();
for (var i = tents.Count - 1; i >= 0; i--)
{
var tent = tents[i];
_deleteBuildingService.Delete(tent, DeleteBuildingOptions.Silent);
}
}
}
}

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