riversong code showcase
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct BuildingUpgradedSignal
|
||||
{
|
||||
public Building Building;
|
||||
|
||||
public BuildingUpgradedSignal(Building building)
|
||||
{
|
||||
Building = building;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class HouseTierCountTrackingSystem : GameSystem, IInitializable, IDisposable
|
||||
{
|
||||
[InjectService]
|
||||
private ISignalBus _signalBus;
|
||||
|
||||
[InjectService]
|
||||
private World _world;
|
||||
|
||||
public HouseTierCountTrackingSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync()
|
||||
{
|
||||
_signalBus.Subscribe<BuildingCreatedSignal>(OnBuildingCreated);
|
||||
_signalBus.Subscribe<BuildingDeletedSignal>(OnBuildingDeleted);
|
||||
_signalBus.Subscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_signalBus.Unsubscribe<BuildingCreatedSignal>(OnBuildingCreated);
|
||||
_signalBus.Unsubscribe<BuildingDeletedSignal>(OnBuildingDeleted);
|
||||
_signalBus.Unsubscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
|
||||
}
|
||||
|
||||
private void OnBuildingCreated(BuildingCreatedSignal signal)
|
||||
{
|
||||
var building = signal.Building;
|
||||
if (!building.Definition.IsHouse) return;
|
||||
|
||||
UpdateTierCount(building.TierIndex, 1);
|
||||
}
|
||||
|
||||
private void OnBuildingDeleted(BuildingDeletedSignal signal)
|
||||
{
|
||||
var building = signal.Building;
|
||||
if (!building.Definition.IsHouse) return;
|
||||
|
||||
UpdateTierCount(building.TierIndex, -1);
|
||||
}
|
||||
|
||||
private void OnBuildingUpgraded(BuildingUpgradedSignal signal)
|
||||
{
|
||||
var building = signal.Building;
|
||||
if (!building.Definition.IsHouse) return;
|
||||
|
||||
if (building.TierIndex > 0) UpdateTierCount(building.TierIndex - 1, -1);
|
||||
UpdateTierCount(building.TierIndex, 1);
|
||||
}
|
||||
|
||||
private void UpdateTierCount(int tierIndex, int delta)
|
||||
{
|
||||
if (!ValidateTierIndex(tierIndex)) return;
|
||||
|
||||
var countsByTier = _world.PopulationState.HouseCountsByTier;
|
||||
|
||||
var updatedCount = countsByTier[tierIndex] + delta;
|
||||
if (updatedCount < 0)
|
||||
{
|
||||
Debug.LogError($"Attempted to decrease house count for tier {tierIndex} that was already at 0");
|
||||
return;
|
||||
}
|
||||
|
||||
countsByTier[tierIndex] = updatedCount;
|
||||
}
|
||||
|
||||
private bool ValidateTierIndex(int tierIndex)
|
||||
{
|
||||
if (tierIndex is >= 0 and < WorldPopulationState.MaxBuildingTier) return true;
|
||||
|
||||
Debug.LogError($"Invalid tier index {tierIndex}");
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct PopulationChangedSignal
|
||||
{
|
||||
public int Population;
|
||||
|
||||
public PopulationChangedSignal(int population)
|
||||
{
|
||||
Population = population;
|
||||
}
|
||||
}
|
||||
}
|
||||
21
Source/Riversong/Game/World/Population/PopulationNeed.cs
Normal file
21
Source/Riversong/Game/World/Population/PopulationNeed.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct PopulationNeed
|
||||
{
|
||||
public byte TierIndex;
|
||||
|
||||
public PopulationNeedType Type;
|
||||
|
||||
public int ProductHandle;
|
||||
|
||||
public ushort HappinessScore;
|
||||
|
||||
public byte ConsumptionRate;
|
||||
|
||||
public byte FetchThreshold;
|
||||
|
||||
public ushort YieldOnFetch;
|
||||
|
||||
public ushort Current;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[Serializable]
|
||||
public class PopulationNeedAuthoring
|
||||
{
|
||||
public PopulationNeedType Type;
|
||||
|
||||
public ProductDefinition Product;
|
||||
|
||||
public int HappinessScore = 20;
|
||||
|
||||
public int ConsumptionRate;
|
||||
|
||||
public int FetchThreshold;
|
||||
|
||||
public int YieldOnFetch;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public enum PopulationNeedType : byte
|
||||
{
|
||||
Product
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
using Unity.Collections;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public struct PopulationNeedsState
|
||||
{
|
||||
public FixedList128Bytes<PopulationNeed> Needs;
|
||||
|
||||
public TierUpgradeState UpgradeState;
|
||||
|
||||
public bool AllNeedsMet;
|
||||
|
||||
public int NeedsMetForWeeks;
|
||||
|
||||
public float Happiness;
|
||||
|
||||
public float MaxHappinessScore;
|
||||
|
||||
public float OverallHappinessWeight;
|
||||
}
|
||||
}
|
||||
126
Source/Riversong/Game/World/Population/PopulationNeedsSystem.cs
Normal file
126
Source/Riversong/Game/World/Population/PopulationNeedsSystem.cs
Normal file
@@ -0,0 +1,126 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class PopulationNeedsSystem : GameSystem, IInitializable, IDisposable, IUpdatable
|
||||
{
|
||||
private const float MaxHappinessScoreChangeRate = 0.1f;
|
||||
|
||||
[InjectService]
|
||||
private ISignalBus _signalBus;
|
||||
|
||||
[InjectService]
|
||||
private IEntityCache _entityCache;
|
||||
|
||||
[InjectService]
|
||||
private World _world;
|
||||
|
||||
[InjectService]
|
||||
private GameConfig _config;
|
||||
|
||||
public PopulationNeedsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync()
|
||||
{
|
||||
_signalBus.Subscribe<EndOfWeekSignal>(OnEndOfWeek);
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_signalBus.Unsubscribe<EndOfWeekSignal>(OnEndOfWeek);
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (_world.TimeState.DayNightCycleStep != DayNightCycleStep.Day) return;
|
||||
|
||||
foreach (var house in _entityCache.GetHouses())
|
||||
{
|
||||
ref var needsState = ref house.GetNeedsStateRW();
|
||||
ref var storage = ref house.GetStorageRW();
|
||||
|
||||
var targetHappiness = 1f;
|
||||
var happinessScore = 0;
|
||||
var maxHappinessScore = 0;
|
||||
needsState.AllNeedsMet = true;
|
||||
|
||||
for (var i = 0; i < needsState.Needs.Length; i++)
|
||||
{
|
||||
var need = needsState.Needs[i];
|
||||
|
||||
if (need.TierIndex > house.TierIndex) break;
|
||||
if (need.Type != PopulationNeedType.Product) throw new NotImplementedException();
|
||||
|
||||
if (need.Current < need.YieldOnFetch)
|
||||
{
|
||||
var productCount = storage.AvailableNow(need.ProductHandle);
|
||||
if (productCount > 0)
|
||||
{
|
||||
storage.Take(need.ProductHandle, productCount);
|
||||
need.Current += (ushort)math.min(productCount * need.YieldOnFetch, ushort.MaxValue);
|
||||
}
|
||||
}
|
||||
|
||||
var needMet = need.Current > 0;
|
||||
if (needMet) happinessScore += need.HappinessScore;
|
||||
|
||||
maxHappinessScore += need.HappinessScore;
|
||||
|
||||
needsState.AllNeedsMet &= needMet;
|
||||
needsState.Needs[i] = need;
|
||||
}
|
||||
|
||||
if (maxHappinessScore > 0)
|
||||
{
|
||||
needsState.MaxHappinessScore += MaxHappinessScoreChangeRate * Time.deltaTime;
|
||||
needsState.MaxHappinessScore = math.min(needsState.MaxHappinessScore, maxHappinessScore);
|
||||
|
||||
targetHappiness = needsState.MaxHappinessScore > 0 ? math.saturate(happinessScore / needsState.MaxHappinessScore) : 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
needsState.MaxHappinessScore = 0;
|
||||
}
|
||||
|
||||
needsState.Happiness = Mathf.MoveTowards(needsState.Happiness, targetHappiness, _config.Population.OverallHappinessChangeRate * Time.deltaTime);
|
||||
needsState.Happiness = math.saturate(needsState.Happiness);
|
||||
needsState.OverallHappinessWeight = math.saturate((float)(_world.TimeState.TotalWeeks - house.WeekCreated) / _config.Population.HouseWeightRampUpWeekCount);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnEndOfWeek(EndOfWeekSignal signal)
|
||||
{
|
||||
foreach (var house in _entityCache.GetHouses())
|
||||
{
|
||||
ref var needsState = ref house.GetNeedsStateRW();
|
||||
|
||||
if (needsState.AllNeedsMet)
|
||||
needsState.NeedsMetForWeeks++;
|
||||
else
|
||||
needsState.NeedsMetForWeeks = 0;
|
||||
|
||||
for (var i = 0; i < needsState.Needs.Length; i++)
|
||||
{
|
||||
var need = needsState.Needs[i];
|
||||
|
||||
if (need.TierIndex > house.TierIndex) break;
|
||||
if (need.Type != PopulationNeedType.Product) continue;
|
||||
|
||||
if (need.Current >= need.ConsumptionRate)
|
||||
need.Current -= need.ConsumptionRate;
|
||||
else
|
||||
need.Current = 0;
|
||||
|
||||
needsState.Needs[i] = need;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Unity.Mathematics;
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[UpdateAfter(typeof(PopulationNeedsSystem))]
|
||||
public class PopulationUpdateSystem : GameSystem, IInitializable, IUpdatable
|
||||
{
|
||||
[InjectService]
|
||||
private ISignalBus _signalBus;
|
||||
|
||||
[InjectService]
|
||||
private World _world;
|
||||
|
||||
[InjectService]
|
||||
private IEntityCache _entityCache;
|
||||
|
||||
[InjectService]
|
||||
private GameConfig _config;
|
||||
|
||||
public PopulationUpdateSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync()
|
||||
{
|
||||
_world.PopulationState.Happiness = _config.Population.InitialeOverallHappiness;
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
if (_world.TimeState.DayNightCycleStep != DayNightCycleStep.Day) return;
|
||||
|
||||
UpdateOverallHappiness();
|
||||
UpdatePopulation();
|
||||
}
|
||||
|
||||
private void UpdateOverallHappiness()
|
||||
{
|
||||
var targetHappiness = 0f;
|
||||
var totalWeight = 0f;
|
||||
|
||||
foreach (var house in _entityCache.GetHouses())
|
||||
{
|
||||
ref var needsState = ref house.GetNeedsStateRW();
|
||||
targetHappiness += needsState.OverallHappinessWeight * needsState.Happiness;
|
||||
totalWeight += needsState.OverallHappinessWeight;
|
||||
}
|
||||
|
||||
if (totalWeight > 0) targetHappiness = math.saturate(targetHappiness / totalWeight);
|
||||
|
||||
var grace = _world.TimeState.TotalWeeks < _config.Population.GraceWeekCount;
|
||||
if (grace) targetHappiness = math.max(targetHappiness, _config.Population.GraceMinHappiness);
|
||||
|
||||
var populationState = _world.PopulationState;
|
||||
var delta = math.abs(targetHappiness - populationState.Happiness);
|
||||
const float halfOnePercent = 0.005f;
|
||||
populationState.Sentiment = delta > halfOnePercent ? math.sign(targetHappiness - populationState.Happiness) : 0;
|
||||
populationState.Happiness = Mathf.MoveTowards(populationState.Happiness, targetHappiness, _config.Population.OverallHappinessChangeRate * Time.deltaTime);
|
||||
populationState.Happiness = math.saturate(populationState.Happiness);
|
||||
}
|
||||
|
||||
private void UpdatePopulation()
|
||||
{
|
||||
var populationState = _world.PopulationState;
|
||||
|
||||
populationState.PopulationCapacity = 0;
|
||||
foreach (var house in _entityCache.GetHouses()) populationState.PopulationCapacity += house.PopulationCapacity;
|
||||
|
||||
var growthRate = _config.Population.GrowthRatePeakValue * _config.Population.GrowthRateCurve.Evaluate(populationState.Happiness);
|
||||
if (populationState.Sentiment > 0) growthRate = math.max(growthRate, 0);
|
||||
|
||||
populationState.GrowthAccumulator += growthRate * Time.deltaTime;
|
||||
if (math.abs(populationState.GrowthAccumulator) < 1) return;
|
||||
|
||||
var delta = (int)math.sign(populationState.GrowthAccumulator);
|
||||
populationState.GrowthAccumulator -= delta;
|
||||
|
||||
var updatedPopulation = math.clamp(populationState.Population + delta, 0, populationState.PopulationCapacity);
|
||||
if (updatedPopulation == populationState.Population) return;
|
||||
|
||||
populationState.Population = updatedPopulation;
|
||||
_signalBus.Raise(new PopulationChangedSignal(updatedPopulation));
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Source/Riversong/Game/World/Population/TierUpgradeState.cs
Normal file
13
Source/Riversong/Game/World/Population/TierUpgradeState.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public enum TierUpgradeState
|
||||
{
|
||||
NotReady,
|
||||
|
||||
FetchingMaterials,
|
||||
|
||||
AllMaterialsFetched,
|
||||
|
||||
MaxedOut
|
||||
}
|
||||
}
|
||||
93
Source/Riversong/Game/World/Population/TierUpgradeSystem.cs
Normal file
93
Source/Riversong/Game/World/Population/TierUpgradeSystem.cs
Normal file
@@ -0,0 +1,93 @@
|
||||
using System;
|
||||
using Cysharp.Threading.Tasks;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[InitializeAfter(typeof(PopulationNeedsSystem))]
|
||||
public class TierUpgradeSystem : GameSystem, IInitializable, IDisposable, IUpdatable
|
||||
{
|
||||
[InjectService]
|
||||
private IEntityCache _entityCache;
|
||||
|
||||
[InjectService]
|
||||
private IProductStorageCommonLogic _storageCommonLogic;
|
||||
|
||||
[InjectService]
|
||||
private ISignalBus _signalBus;
|
||||
|
||||
[InjectService]
|
||||
private GameConfig _config;
|
||||
|
||||
public TierUpgradeSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||
{
|
||||
}
|
||||
|
||||
public UniTask InitializeAsync()
|
||||
{
|
||||
_signalBus.Subscribe<EndOfWeekSignal>(OnEndOfWeek);
|
||||
|
||||
return UniTask.CompletedTask;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_signalBus.Unsubscribe<EndOfWeekSignal>(OnEndOfWeek);
|
||||
}
|
||||
|
||||
private void OnEndOfWeek(EndOfWeekSignal signal)
|
||||
{
|
||||
var config = _config.Buildings;
|
||||
|
||||
foreach (var house in _entityCache.GetHouses())
|
||||
{
|
||||
ref var needsState = ref house.GetNeedsStateRW();
|
||||
|
||||
if (needsState.UpgradeState != TierUpgradeState.NotReady || !needsState.AllNeedsMet || needsState.NeedsMetForWeeks < config.WeeksWithNeedsMetToUpgrade) continue;
|
||||
|
||||
needsState.UpgradeState = TierUpgradeState.FetchingMaterials;
|
||||
}
|
||||
}
|
||||
|
||||
public void Update()
|
||||
{
|
||||
foreach (var house in _entityCache.GetHouses())
|
||||
{
|
||||
ref var needsState = ref house.GetNeedsStateRW();
|
||||
|
||||
if (needsState.UpgradeState != TierUpgradeState.FetchingMaterials) continue;
|
||||
|
||||
var products = house.Definition.HouseTiers[house.TierIndex].UpgradeMaterials;
|
||||
|
||||
ref var storage = ref house.GetStorageRW();
|
||||
if (!_storageCommonLogic.TakeAllOrNothing(ref storage, products)) continue;
|
||||
|
||||
needsState.UpgradeState = TierUpgradeState.AllMaterialsFetched;
|
||||
|
||||
_ = UpgradeBuildingAsync(house);
|
||||
}
|
||||
}
|
||||
|
||||
private async UniTask UpgradeBuildingAsync(Building house)
|
||||
{
|
||||
await UniTask.WaitForSeconds(1);
|
||||
|
||||
house.TierIndex++;
|
||||
|
||||
var tiers = house.Definition.HouseTiers;
|
||||
house.PopulationCapacity = tiers[math.min(house.TierIndex, tiers.Count - 1)].Capacity;
|
||||
|
||||
_signalBus.Raise(new BuildingUpgradedSignal(house));
|
||||
|
||||
OnBuildingUpgraded(house);
|
||||
}
|
||||
|
||||
private void OnBuildingUpgraded(Building house)
|
||||
{
|
||||
var maxTier = house.Definition.HouseTiers.Count - 1;
|
||||
ref var needsState = ref house.GetNeedsStateRW();
|
||||
|
||||
needsState.UpgradeState = house.TierIndex < maxTier ? TierUpgradeState.NotReady : TierUpgradeState.MaxedOut;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class WorldPopulationState
|
||||
{
|
||||
public const int MaxBuildingTier = 10;
|
||||
|
||||
public int Population { get; set; }
|
||||
|
||||
public int PopulationCapacity { get; set; }
|
||||
|
||||
public float GrowthAccumulator { get; set; }
|
||||
|
||||
public float Happiness { get; set; }
|
||||
|
||||
public float Sentiment { get; set; }
|
||||
|
||||
public int[] HouseCountsByTier { get; } = new int[MaxBuildingTier];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user