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,65 @@
using System;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class Agent : Entity, IDisposable
{
private IntentQueue _intentQueue;
private TilePath _path;
private AgentJobState _agentJobState;
private CritterState _critterState;
public int CarriedProductHandle;
public AgentDefinition Definition { get; set; }
public int HomeId { get; set; }
public float3 Position { get; set; }
public float3 Heading { get; set; }
public float3 Velocity { get; set; }
public AgentLifecycleState LifecycleState { get; set; } = AgentLifecycleState.New;
public IntentExecutionState IntentExecutionState { get; set; } = IntentExecutionState.WaitingIntent;
public float IntentExecutionTime { get; set; }
public int PathQueryHandle { get; set; }
public int PathIndex { get; set; }
public int2 WanderingCenter { get; set; }
public ref IntentQueue GetIntentQueueRW()
{
return ref _intentQueue;
}
public ref TilePath GetPathRW()
{
return ref _path;
}
public ref AgentJobState GetJobStateRW()
{
return ref _agentJobState;
}
public ref CritterState GetCritterStateRW()
{
return ref _critterState;
}
public void Dispose()
{
_path.Dispose();
}
}
}

View File

@@ -0,0 +1,14 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
// ReSharper disable InconsistentNaming
public enum AgentAnimation
{
None,
ChopTree,
Farming,
FireProjectile
}
}

View File

@@ -0,0 +1,21 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class AgentAnimationEventHandler : MonoBehaviour
{
private bool _actionAnimationCompleted;
public void ActionAnimationCompleted()
{
_actionAnimationCompleted = true;
}
public bool WaitAnimationCompleted()
{
var result = _actionAnimationCompleted;
_actionAnimationCompleted = false;
return result;
}
}
}

View File

@@ -0,0 +1,68 @@
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DefaultAgentsSystemGroup))]
[UpdateAfter(typeof(IntentExecutionSystem))]
public class AgentAnimationSystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IAgentVisualizationCollection _agentVisualizationCollection;
[InjectService]
private IProductCatalog _productCatalog;
public AgentAnimationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (Agent agent in _entityCollection.GetInternalEntityList(typeof(Agent)))
{
if (!_agentVisualizationCollection.TryGetVisualization(agent.Id, out var visualization)) continue;
const float minVelocitySq = 0.01f;
visualization.UpdateAnimationState(math.lengthsq(agent.Velocity) > minVelocitySq);
UpdateCarriedProduct(agent, visualization);
}
}
private void UpdateCarriedProduct(Agent agent, AgentVisualization visualization)
{
var carriedProduct = visualization.CarriedProductVisualization;
if (!carriedProduct) return;
var isCarryingProduct = agent.CarriedProductHandle != IProductCatalog.InvalidHandle;
var targetWeight = isCarryingProduct ? 1 : 0;
var weight = Mathf.MoveTowards(carriedProduct.GetAnimatorLayerWeight(), targetWeight, 10 * Time.deltaTime);
carriedProduct.SetAnimatorLayerWeight(weight);
if (isCarryingProduct)
SetProductVisualization(carriedProduct, agent.CarriedProductHandle);
else
ClearProductVisualization(carriedProduct);
}
private void SetProductVisualization(AgentCarriedProductVisualization carriedProduct, int productHandle)
{
if (carriedProduct.IsShowingCarriedProduct()) return;
var product = _productCatalog.GetProduct(productHandle);
var productVisualization = (GameObject)Object.Instantiate(product.CarriedVisualization.Asset);
carriedProduct.SetProductVisualization(productVisualization);
}
private void ClearProductVisualization(AgentCarriedProductVisualization carriedProduct)
{
if (!carriedProduct.ClearProductVisualization(out var productVisualization)) return;
Object.Destroy(productVisualization);
}
}
}

View File

@@ -0,0 +1,55 @@
using Sirenix.OdinInspector;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class AgentCarriedProductVisualization : MonoBehaviour
{
private const string CarryingProductLayer = "CarryingProduct";
[Required]
[SerializeField]
private Animator _animator;
[Required]
[SerializeField]
private Transform _carriedProductAnchor;
private int _carryingProductLayerIndex;
private GameObject _carriedProductVisualization;
private void Awake()
{
_carryingProductLayerIndex = _animator.GetLayerIndex(CarryingProductLayer);
}
public float GetAnimatorLayerWeight()
{
return _animator.GetLayerWeight(_carryingProductLayerIndex);
}
public void SetAnimatorLayerWeight(float weight)
{
_animator.SetLayerWeight(_carryingProductLayerIndex, weight);
}
public void SetProductVisualization(GameObject visualization)
{
_carriedProductVisualization = visualization;
visualization.transform.SetParent(_carriedProductAnchor, false);
}
public bool ClearProductVisualization(out GameObject visualization)
{
visualization = _carriedProductVisualization;
_carriedProductVisualization = null;
return visualization;
}
public bool IsShowingCarriedProduct()
{
return _carriedProductVisualization;
}
}
}

View File

@@ -0,0 +1,179 @@
using Unity.Burst;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
[BurstCompile]
[Service(typeof(IAgentCommonLogic))]
[GameSystemGroup(typeof(AgentsSystemGroup))]
public class AgentCommonLogicSystem : GameSystem, IAgentCommonLogic
{
private const float RotationSpeed = 270 * math.TORADIANS;
[InjectService]
private GameConfig _config;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IBuildingSpatialQuery _buildingSpatialQuery;
[InjectService]
private IAgentFactory _agentFactory;
public AgentCommonLogicSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public bool MoveAgent(Agent agent, float3 targetPosition, float dt)
{
var position = agent.Position;
var heading = agent.Heading;
var oldPosition = position;
var targetReached = MoveAgent(targetPosition, dt, _config.Agents.MoveSpeed, ref position, ref heading);
agent.Position = position;
agent.Heading = heading;
agent.Velocity = dt > 0 ? (position - oldPosition) / dt : 0;
return targetReached;
}
[BurstCompile]
private static bool MoveAgent(in float3 targetPosition, float dt, float speed, ref float3 position, ref float3 heading)
{
var positionDelta = targetPosition - position;
var targetHeading = math.normalizesafe(positionDelta, heading);
var movementLength = speed * dt;
const float targetOvershoot = 1.1f;
var targetReachedThreshold = movementLength * targetOvershoot;
var snapPosition = math.lengthsq(positionDelta) < targetReachedThreshold * targetReachedThreshold;
position = math.select(position + targetHeading * movementLength, targetPosition, snapPosition);
var snapHeading = UpdateHeading(ref heading, targetHeading, RotationSpeed * dt);
heading = math.select(heading, targetHeading, snapHeading);
return snapPosition && snapHeading;
}
public bool UpdateHeading(Agent agent, in float3 targetHeading, float dt)
{
var heading = agent.Heading;
var headingReached = UpdateHeading(ref heading, targetHeading, RotationSpeed * dt);
agent.Heading = heading;
return headingReached;
}
[BurstCompile]
private static bool UpdateHeading(ref float3 heading, in float3 targetHeading, float maxRadiansDelta)
{
var q0 = quaternion.LookRotationSafe(heading, math.up());
var q1 = quaternion.LookRotationSafe(targetHeading, math.up());
var angle = math.angle(q0, q1);
if (angle < maxRadiansDelta)
{
heading = targetHeading;
return true;
}
var t = math.min(1, maxRadiansDelta / angle);
var q = math.slerp(q0, q1, t);
heading = math.rotate(q, math.forward());
return false;
}
public bool TryFetchProductFromStorage(AgentDefinition agentDefinition,
Building source,
int productHandle,
int neededAmount,
int maxAgentCount = int.MaxValue,
AgentJob job = AgentJob.None)
{
ref var sourceStorage = ref source.GetStorageRW();
return sourceStorage.CountIncludingReservations(productHandle) < neededAmount &&
TryFindFetchStorage(source, productHandle, out var fetchBuilding) &&
TryFetchProduct(agentDefinition, source, fetchBuilding, productHandle, 1, maxAgentCount, job);
}
public bool TryFetchProductFromProvider(AgentDefinition agentDefinition,
Building source,
int productHandle,
int neededAmount,
int maxAgentCount = int.MaxValue,
AgentJob job = AgentJob.None)
{
ref var sourceStorage = ref source.GetStorageRW();
return sourceStorage.CountIncludingReservations(productHandle) < neededAmount &&
_buildingSpatialQuery.FindProviderForFetch(source.Id, source.Rect, productHandle, out var fetchBuilding) &&
TryFetchProduct(agentDefinition, source, fetchBuilding, productHandle, 1, maxAgentCount, job);
}
public bool TryFetchProduct(AgentDefinition agentDefinition,
Building source,
Building fetchBuilding,
int productHandle,
int amount,
int maxAgentCount = int.MaxValue,
AgentJob job = AgentJob.None)
{
if (!_agentFactory.CanSpawnAgent(source, maxAgentCount, job)) return false;
ref var sourceStorage = ref source.GetStorageRW();
if (sourceStorage.FreeSpace() < amount) return false;
ref var fetchStorage = ref fetchBuilding.GetStorageRW();
if (fetchStorage.AvailableNow(productHandle) < amount) return false;
sourceStorage.ReservePutting(productHandle, amount);
fetchStorage.ReserveTaking(productHandle, amount);
var position = _tileSpace.TileToWorld(source.Rect.Center);
var agent = _agentFactory.CreateVillager(agentDefinition, source, position, job);
IntentQueue.Fetch(ref agent.GetIntentQueueRW(), source.Rect, source.Id, fetchBuilding.Rect, fetchBuilding.Id, productHandle, amount, PathTraversalRules.TransportAgent);
return true;
}
public bool TryDeliverProduct(AgentDefinition agentDefinition, Building source, int productHandle, int maxAgentCount = int.MaxValue, AgentJob job = AgentJob.None)
{
if (!_agentFactory.CanSpawnAgent(source, maxAgentCount, job)) return false;
ref var sourceStorage = ref source.GetStorageRW();
if (sourceStorage.AvailableNow(productHandle) <= 0) return false;
if (!_buildingSpatialQuery.FindStorageForDelivery(source.Id, source.Rect, productHandle, out var deliveryStorage)) return false;
sourceStorage.ReserveTaking(productHandle, 1);
ref var destinationStorage = ref deliveryStorage.GetStorageRW();
destinationStorage.ReservePutting(productHandle, 1);
var position = _tileSpace.TileToWorld(source.Rect.Center);
var deliveryAgent = _agentFactory.CreateVillager(agentDefinition, source, position, job);
IntentQueue.Deliver(
ref deliveryAgent.GetIntentQueueRW(),
source.Rect,
source.Id,
deliveryStorage.Rect,
deliveryStorage.Id,
productHandle,
1,
PathTraversalRules.TransportAgent);
return true;
}
private bool TryFindFetchStorage(Building source, int productHandle, out Building fetchBuilding)
{
if (source.Definition.ProvidedProducts.Count > 0)
return _buildingSpatialQuery.FindStorageForFetch(source.Id, source.Rect, source.Definition.Range, productHandle, out fetchBuilding);
return _buildingSpatialQuery.FindStorageForFetch(source.Id, source.Rect, productHandle, out fetchBuilding);
}
}
}

View File

@@ -0,0 +1,19 @@
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.AddressableAssets;
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "AgentDefinition", menuName = "Riversong Code Showcase/Agent Definition")]
public class AgentDefinition : GameDataAsset
{
[Required]
public AssetReferenceGameObject Visualization;
public AssetReferenceGameObject Projectile;
public int WanderingRadius = 10;
public Vector2 WanderingIdleTime = new(3, 10);
}
}

View File

@@ -0,0 +1,17 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum AgentJob
{
None,
Harvester,
Hunter,
Farmer,
StorageRequestHauler,
Count
}
}

View File

@@ -0,0 +1,34 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct AgentJobState
{
public AgentJob Job;
public AgentStateMachineStep StateMachineStep;
public HarvesterJobState Harvester;
public FarmingJobState Farming;
public HunterJobState Hunter;
public struct HarvesterJobState
{
public float3 TargetPosition;
public int ResourceNodeId;
}
public struct HunterJobState
{
public int TargetHerdId;
}
public struct FarmingJobState
{
public int2? LockedTile;
}
}
}

View File

@@ -0,0 +1,11 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum AgentLifecycleState
{
New,
Live,
DeSpawning
}
}

View File

@@ -0,0 +1,217 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IAgentFactory))]
[Service(typeof(IAgentVisualizationCollection))]
[GameSystemGroup(typeof(DefaultAgentsSystemGroup))]
public class AgentManagerSystem : GameSystem, IUpdatable, IAgentFactory, IAgentVisualizationCollection
{
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IScene _scene;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IProductStackFactory _productStackFactory;
[InjectService]
private World _world;
private readonly Dictionary<int, (Agent, AgentVisualization)> _visualizations = new();
private readonly HashSet<int> _pendingVisualizations = new();
public AgentManagerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void InitializeAgentSource(IAgentSourceEntity source)
{
ref var agentSourceState = ref source.GetAgentSourceStateRW();
agentSourceState.MaxAgentCount = agentSourceState.AgentIds.Capacity;
agentSourceState.AgentCount.Length = (int)AgentJob.Count;
agentSourceState.SpawnCooldown.Length = (int)AgentJob.Count;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool CanSpawnAgent(IAgentSourceEntity source, int maxAgentCount = int.MaxValue, AgentJob job = AgentJob.None)
{
ref var agentSourceState = ref source.GetAgentSourceStateRW();
return agentSourceState.AgentIds.Length < agentSourceState.MaxAgentCount &&
agentSourceState.AgentCount[(int)job] < maxAgentCount &&
(job == AgentJob.None || agentSourceState.SpawnCooldown[(int)job] <= 0);
}
public Agent CreateVillager(AgentDefinition definition, IAgentSourceEntity source, float3 position, AgentJob job = AgentJob.None)
{
var villager = CreateAgent(definition, source, position, job);
FinalizeAgentCreation(villager);
return villager;
}
public Agent CreateCritter(CritterDefinition critterDefinition, IAgentSourceEntity source, float3 position)
{
var critter = CreateAgent(critterDefinition.AgentDefinition, source, position, AgentJob.None);
ref var critterState = ref critter.GetCritterStateRW();
critterState.IsCritter = true;
critterState.CritterDefinitionId = critterDefinition.RuntimeId;
FinalizeAgentCreation(critter);
return critter;
}
private Agent CreateAgent(AgentDefinition definition, IAgentSourceEntity source, float3 position, AgentJob job)
{
var agent = _entityCollection.Create<Agent>();
agent.Definition = definition;
agent.HomeId = source.Id;
agent.Position = position;
agent.Heading = math.forward();
agent.CarriedProductHandle = IProductCatalog.InvalidHandle;
ref var path = ref agent.GetPathRW();
path = TilePath.Initialize();
ref var jobState = ref agent.GetJobStateRW();
jobState.Job = job;
ref var agentSourceState = ref source.GetAgentSourceStateRW();
agentSourceState.AgentIds.Add(agent.Id);
agentSourceState.AgentCount[(int)job]++;
if (job != AgentJob.None) agentSourceState.SpawnCooldown[(int)job] = source.SpawnCooldown;
return agent;
}
private void FinalizeAgentCreation(Agent agent)
{
_entityCollection.Add(agent);
_ = CreateVisualizationAsync(agent.Definition, agent);
}
private async UniTask CreateVisualizationAsync(AgentDefinition definition, Agent agent)
{
var folder = _scene.SceneFolders.Agents;
_pendingVisualizations.Add(agent.Id);
var visualizationGameObject = await definition.Visualization.InstantiateAsync(agent.Position, Quaternion.identity, folder).ToUniTask();
if (!_pendingVisualizations.Remove(agent.Id))
{
definition.Visualization.ReleaseInstance(visualizationGameObject);
return;
}
var visualization = visualizationGameObject.GetComponent<AgentVisualization>();
if (!visualization)
{
Debug.LogError($"Invalid agent visualization '{visualization.name}'");
definition.Visualization.ReleaseInstance(visualizationGameObject);
return;
}
_visualizations.Add(agent.Id, (agent, visualization));
}
public bool TryGetVisualization(int id, out AgentVisualization visualization)
{
if (_visualizations.TryGetValue(id, out var value))
{
(_, visualization) = value;
return true;
}
visualization = null;
return false;
}
public void Update()
{
DeSpawnAgents();
UpdateVisualizations();
}
private void DeSpawnAgents()
{
var agents = _entityCollection.GetInternalEntityList(typeof(Agent));
using var deSpawnScope = ListPool<Agent>.Get(out var deSpawn);
foreach (Agent agent in agents)
if (agent.LifecycleState == AgentLifecycleState.DeSpawning)
deSpawn.Add(agent);
foreach (var agent in deSpawn) DeSpawnAgent(agent);
foreach (Agent agent in agents)
if (agent.IntentExecutionState == IntentExecutionState.EmptyQueue || !_entityCollection.Exists(agent.HomeId))
agent.LifecycleState = AgentLifecycleState.DeSpawning;
}
private void DeSpawnAgent(Agent agent)
{
var product = _productCatalog.GetProduct(agent.CarriedProductHandle);
if (product)
{
var tile = _tileSpace.WorldToTile(agent.Position);
ref var entityIdValue = ref _world.EntityIdMap.GetValueRW(tile);
if (entityIdValue.BuildingId == agent.HomeId)
{
var home = _entityCollection.Get<Building>(agent.HomeId);
ref var storagePolicy = ref home.GetProductStoragePolicyRW();
if (storagePolicy.IsTakingAllowed(agent.CarriedProductHandle))
{
ref var storage = ref home.GetStorageRW();
storage.Put(agent.CarriedProductHandle, 1);
}
}
else
{
_productStackFactory.CreateOrMerge(tile, product, 1);
}
}
agent.Dispose();
_entityCollection.Remove(agent.Id);
if (_pendingVisualizations.Remove(agent.Id)) return;
var (_, visualization) = _visualizations[agent.Id];
_visualizations.Remove(agent.Id);
agent.Definition.Visualization.ReleaseInstance(visualization.gameObject);
}
private void UpdateVisualizations()
{
foreach (var (agent, visualization) in _visualizations.Values)
{
visualization.SetVisible(agent.LifecycleState == AgentLifecycleState.Live);
if (agent.LifecycleState != AgentLifecycleState.Live) continue;
var heading = math.normalizesafe(agent.Heading, math.forward());
visualization.transform.SetPositionAndRotation(agent.Position, Quaternion.LookRotation(heading));
}
}
}
}

View File

@@ -0,0 +1,15 @@
using Unity.Collections;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct AgentSourceState
{
public int MaxAgentCount;
public FixedList512Bytes<int> AgentIds;
public FixedList32Bytes<byte> AgentCount;
public FixedList128Bytes<float> SpawnCooldown;
}
}

View File

@@ -0,0 +1,40 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EarlyAgentsSystemGroup))]
[UpdateBefore(typeof(AgentsSpawnTickSystem))]
public class AgentSpawnCooldownSystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private World _world;
public AgentSpawnCooldownSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var laborEfficiencyModifier = _world.ProductionState.LaborEfficiencyModifier;
foreach (Building building in _entityCollection.GetInternalEntityList(typeof(Building)))
{
ref var agentSourceState = ref building.GetAgentSourceStateRW();
ref var sleepState = ref building.GetSleepStateRW();
for (var jobIndex = 1; jobIndex < (int)AgentJob.Count; jobIndex++)
{
var cooldown = agentSourceState.SpawnCooldown[jobIndex];
if (cooldown <= 0) continue;
var efficiencyMultiplier = Mathf.Max(0, 1 + sleepState.EfficiencyModifier + laborEfficiencyModifier);
var dt = Time.deltaTime * efficiencyMultiplier;
agentSourceState.SpawnCooldown[jobIndex] = Mathf.MoveTowards(cooldown, 0, dt);
}
}
}
}
}

View File

@@ -0,0 +1,20 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
// ReSharper disable InconsistentNaming
public enum AgentStateMachineStep
{
None,
#region Generic
ReturningHome,
#endregion
#region Hunter
Hunter_SeekingPrey
#endregion
}
}

View File

@@ -0,0 +1,57 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class AgentVisualization : MonoBehaviour
{
private static readonly int IsMovingParamID = Animator.StringToHash("IsMoving");
[Required]
[SerializeField]
private Animator _animator;
[SerializeField]
private AgentAnimationEventHandler _animationEventHandler;
[field: SerializeField] public AgentCarriedProductVisualization CarriedProductVisualization { get; private set; }
private bool _isVisible;
private List<Renderer> _renderers;
private void Awake()
{
_renderers = new List<Renderer>(1);
GetComponentsInChildren(_renderers);
_isVisible = true;
SetVisible(false);
}
public void SetVisible(bool isVisible)
{
if (_isVisible == isVisible) return;
foreach (var r in _renderers) r.enabled = isVisible;
_isVisible = isVisible;
}
public void UpdateAnimationState(bool isMoving)
{
_animator.SetBool(IsMovingParamID, isMoving);
}
public void SetAnimatorTrigger(int triggerID)
{
_animator.SetTrigger(triggerID);
}
public bool WaitAnimationCompleted()
{
return _animationEventHandler.WaitAnimationCompleted();
}
}
}

View File

@@ -0,0 +1,52 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DefaultAgentsSystemGroup))]
[UpdateAfter(typeof(AgentManagerSystem))]
public class AgentsCleanUpSystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCollection _entityCollection;
public AgentsCleanUpSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (IAgentSourceEntity agentSource in _entityCollection.GetInternalEntityList(typeof(ConstructionSite))) CleanUpAgentSource(agentSource);
foreach (IAgentSourceEntity agentSource in _entityCollection.GetInternalEntityList(typeof(Building))) CleanUpAgentSource(agentSource);
foreach (IAgentSourceEntity agentSource in _entityCollection.GetInternalEntityList(typeof(CritterHerd))) CleanUpAgentSource(agentSource);
}
private void CleanUpAgentSource(IAgentSourceEntity agentSource)
{
ref var agentSourceState = ref agentSource.GetAgentSourceStateRW();
for (var i = agentSourceState.AgentIds.Length - 1; i >= 0; i--)
{
var id = agentSourceState.AgentIds[i];
var agent = _entityCollection.Get<Agent>(id);
if (agent is not { LifecycleState: AgentLifecycleState.DeSpawning }) continue;
ref var jobState = ref agent.GetJobStateRW();
var job = jobState.Job;
var count = agentSourceState.AgentCount[(int)job];
if (count > 0)
{
agentSourceState.AgentCount[(int)job] = (byte)(count - 1);
}
else
{
var home = _entityCollection.Get<Building>(agent.HomeId);
Debug.LogError($"Agent count <= 0 when de-spawning agent. Home: '{home.Definition.name}', Job: '{job}'");
}
agentSourceState.AgentIds.RemoveAtSwapBack(i);
}
}
}
}

View File

@@ -0,0 +1,28 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EarlyAgentsSystemGroup))]
public class AgentsSpawnTickSystem : GameSystem, IUpdatable
{
private const float SpawnInterval = 1;
[InjectService]
private World _world;
public AgentsSpawnTickSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var state = _world.AgentsState;
state.SpawnTickTimer += Time.deltaTime;
if (state.SpawnTickTimer < SpawnInterval) return;
state.SpawnTickTimer = 0;
state.SpawnTickNow = true;
}
}
}

View File

@@ -0,0 +1,114 @@
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DayOnlyAgentSpawnSystemsGroup))]
public class ConstructionAgentSystem : GameSystem, IUpdatable
{
private const int FetchRange = 20;
[InjectService]
private GameConfig _config;
[InjectService]
private World _world;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IAgentFactory _agentFactory;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IBuildingSpatialQuery _buildingSpatialQuery;
public ConstructionAgentSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var agentDefinition = (AgentDefinition)_config.Agents.GenericAgent.Asset;
foreach (ConstructionSite constructionSite in _entityCollection.GetInternalEntityList(typeof(ConstructionSite)))
TryFetch(constructionSite, constructionSite.Rect, constructionSite.Building.BuildingMaterials, agentDefinition);
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;
TryFetch(house, house.Rect, products, agentDefinition);
}
}
private void TryFetch<T>(T source, TileRect sourceRect, List<ProductAmountAuthoring> products, AgentDefinition agentDefinition)
where T : class, IProductStorageEntity, IAgentSourceEntity
{
if (!_agentFactory.CanSpawnAgent(source)) return;
ref var sourceStorage = ref source.GetStorageRW();
foreach (var (product, needed) in products)
{
var productHandle = _productCatalog.GetHandle(product);
sourceStorage.Get(productHandle, out var count, out var putReservations, out _);
var remaining = needed - count - putReservations;
if (remaining <= 0) continue;
if (!FindFetchDestination(source.Id, sourceRect, productHandle, out var destinationRect, out var destinationEntity)) continue;
sourceStorage.ReservePutting(productHandle, 1);
ref var destinationStorage = ref destinationEntity.GetStorageRW();
destinationStorage.ReserveTaking(productHandle, 1);
var position = _tileSpace.TileToWorld(sourceRect.Center);
var agent = _agentFactory.CreateVillager(agentDefinition, source, position);
IntentQueue.Fetch(
ref agent.GetIntentQueueRW(),
sourceRect,
source.Id,
destinationRect,
destinationEntity.Id,
productHandle,
1,
PathTraversalRules.GenericAgent | PathTraversalRules.UpdateFailedPathCache);
break;
}
}
private bool FindFetchDestination(int sourceId, TileRect sourceRect, int productHandle, out TileRect destinationRect, out IProductStorageEntity destinationEntity)
{
var productStackFound = _world.ProductStacks.FindClosest(sourceRect, FetchRange, productHandle, 1, out var productStack);
var storageFound = _buildingSpatialQuery.FindStorageForFetch(sourceId, sourceRect, FetchRange, productHandle, out var storageBuilding);
if (productStackFound || storageFound)
{
var productStackDistance = productStackFound ? TileMath.StepCount(sourceRect, productStack.Tile) : int.MaxValue;
var storageDistance = storageFound ? TileMath.StepCount(sourceRect, storageBuilding.Rect) : int.MaxValue;
destinationRect = productStackDistance < storageDistance ? TileRect.OneTile(productStack.Tile) : storageBuilding.Rect;
destinationEntity = productStackDistance < storageDistance ? productStack : storageBuilding;
return true;
}
destinationRect = TileRect.Empty;
destinationEntity = null;
return false;
}
}
}

View File

@@ -0,0 +1,24 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "CritterDefinition", menuName = "Riversong Code Showcase/Critter Definition")]
public class CritterDefinition : GameDataAsset
{
public AgentDefinition AgentDefinition;
public int MinCritterCount = 5;
public int MaxCritterCount = 15;
public float MinSpawningInterval = 10;
public float MaxSpawningInterval = 15;
public ProductDefinition Product;
public int MinProductAmount = 3;
public int MaxProductAmount = 5;
}
}

View File

@@ -0,0 +1,24 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class CritterHerd : Entity, IAgentSourceEntity
{
private AgentSourceState _agentsState;
public CritterDefinition CritterDefinition { get; set; }
public int2 Center { get; set; }
public int MaxCritterCount { get; set; }
public float SpawnTimer { get; set; }
public float SpawnCooldown => 0;
public ref AgentSourceState GetAgentSourceStateRW()
{
return ref _agentsState;
}
}
}

View File

@@ -0,0 +1,37 @@
using Random = UnityEngine.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DefaultAgentsSystemGroup))]
public class CritterHerdMoveCenterSystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private World _world;
[InjectService]
private ISignalBus _signalBus;
public CritterHerdMoveCenterSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (CritterHerd herd in _entityCollection.GetInternalEntityList(typeof(CritterHerd)))
{
if (!_world.BlockMap.IsBlocked(herd.Center, BlockReason.BlocksAgents)) continue;
var oldCenter = herd.Center;
var moveDirection = ((Directions)Random.Range(0, 8)).ToVector();
var center = oldCenter + 4 * moveDirection;
if (!_world.Contains(center) || _world.BlockMap.IsBlocked(center, BlockReason.BlocksAgents)) continue;
herd.Center = center;
}
}
}
}

View File

@@ -0,0 +1,80 @@
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Pool;
using Random = UnityEngine.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EarlyAgentsSystemGroup))]
public class CritterSpawnSystem : GameSystem, IUpdatable
{
[InjectService]
private World _world;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IAgentFactory _agentFactory;
[InjectService]
private ITileSpace _tileSpace;
public CritterSpawnSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (CritterHerd herd in _entityCollection.GetInternalEntityList(typeof(CritterHerd)))
{
ref var agentSourceState = ref herd.GetAgentSourceStateRW();
if (agentSourceState.AgentCount[(int)AgentJob.None] >= herd.MaxCritterCount)
{
herd.SpawnTimer = float.MinValue;
continue;
}
var critterDefinition = herd.CritterDefinition;
if (herd.SpawnTimer < 0) herd.SpawnTimer = Random.Range(critterDefinition.MinSpawningInterval, critterDefinition.MaxSpawningInterval);
herd.SpawnTimer -= Time.deltaTime;
if (herd.SpawnTimer <= 0)
{
herd.SpawnTimer = float.MinValue;
var wanderingRadius = critterDefinition.AgentDefinition.WanderingRadius;
if (!TryFindRandomFreeTile(herd.Center - wanderingRadius, herd.Center + wanderingRadius, out var tile)) continue;
var position = _tileSpace.TileToWorld(tile);
var critter = _agentFactory.CreateCritter(critterDefinition, herd, position);
critter.WanderingCenter = tile;
ref var intentQueue = ref critter.GetIntentQueueRW();
intentQueue.EnqueueIntent(AgentIntent.MakeLive());
IntentQueue.LoopWandering(ref intentQueue, 0);
}
}
}
private bool TryFindRandomFreeTile(int2 min, int2 max, out int2 tile)
{
using var candidatesScope = ListPool<int2>.Get(out var candidates);
foreach (var candidate in TileRange.From(min, max))
if (!_world.BlockMap.IsBlocked(candidate, BlockReason.BlocksAgents))
candidates.Add(candidate);
if (candidates.Count == 0)
{
tile = int2.zero;
return false;
}
tile = candidates[Random.Range(0, candidates.Count)];
return true;
}
}
}

View File

@@ -0,0 +1,11 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct CritterState
{
public bool IsCritter;
public int CritterDefinitionId;
public int LockId;
}
}

View File

@@ -0,0 +1,48 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DefaultAgentsSystemGroup))]
[UpdateAfter(typeof(AgentManagerSystem))]
[UpdateBefore(typeof(AgentsCleanUpSystem))]
public class SpawnProductStackAfterCritterDeathSystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IProductStackFactory _productStackFactory;
[InjectService]
private World _world;
public SpawnProductStackAfterCritterDeathSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (CritterHerd herd in _entityCollection.GetInternalEntityList(typeof(CritterHerd)))
{
ref var agentSourceState = ref herd.GetAgentSourceStateRW();
foreach (var critterId in agentSourceState.AgentIds)
{
var critter = _entityCollection.Get<Agent>(critterId);
if (critter.LifecycleState != AgentLifecycleState.DeSpawning) continue;
var tile = _tileSpace.WorldToTile(critter.Position);
if (_world.BlockMap.IsBlocked(tile, BlockReason.BlocksAgents)) continue;
var product = herd.CritterDefinition.Product;
var amount = Random.Range(herd.CritterDefinition.MinProductAmount, herd.CritterDefinition.MaxProductAmount + 1);
_productStackFactory.CreateOrMerge(tile, product, amount);
}
}
}
}
}

View File

@@ -0,0 +1,37 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DefaultAgentsSystemGroup))]
public class UnlockCrittersSystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IIntentLogicExecutor _intentLogicExecutor;
public UnlockCrittersSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (var critter in _entityCache.GetCritterAgents())
{
ref var critterState = ref critter.GetCritterStateRW();
if (critterState.LockId == Entity.InvalidId || _entityCollection.Exists(critterState.LockId)) continue;
Debug.LogError($"Invalid lock id {critterState.LockId} on critter {critter.Id}");
critterState.LockId = Entity.InvalidId;
ref var intentQueue = ref _intentLogicExecutor.CancelRemainingIntents(critter);
IntentQueue.LoopWandering(ref intentQueue, 0);
}
}
}
}

View File

@@ -0,0 +1,10 @@
using System.Collections.Generic;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class WorldCritterHerdsState
{
public List<int2> EligibleCenters { get; } = new();
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DefaultAgentsSystemGroup))]
public class DeSpawnAgentsAtNightSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IIntentLogicExecutor _intentLogicExecutor;
public DeSpawnAgentsAtNightSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<NightStartedSignal>(OnNightStarted);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<NightStartedSignal>(OnNightStarted);
}
private void OnNightStarted(NightStartedSignal signal)
{
foreach (Agent agent in _entityCollection.GetInternalEntityList(typeof(Agent)))
{
if (agent.LifecycleState == AgentLifecycleState.DeSpawning) continue;
if (agent.CarriedProductHandle != IProductCatalog.InvalidHandle) continue;
ref var jobState = ref agent.GetJobStateRW();
if (jobState.Job == AgentJob.StorageRequestHauler) continue;
ref var critterState = ref agent.GetCritterStateRW();
if (critterState.IsCritter) continue;
_intentLogicExecutor.CancelRemainingIntents(agent);
}
}
}
}

View File

@@ -0,0 +1,40 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DayOnlyAgentSpawnSystemsGroup))]
public class FarmingAgentSystem : GameSystem, IUpdatable
{
private const int MaxAgentCount = 1;
[InjectService]
private GameConfig _config;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IAgentFactory _agentFactory;
public FarmingAgentSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var agentDefinition = (AgentDefinition)_config.Agents.FarmerAgent.Asset;
foreach (var farm in _entityCache.GetFarmBuildings())
{
if (!_agentFactory.CanSpawnAgent(farm, MaxAgentCount, AgentJob.Farmer)) continue;
var position = _tileSpace.TileToWorld(farm.Rect.Center);
var agent = _agentFactory.CreateVillager(agentDefinition, farm, position, AgentJob.Farmer);
IntentQueue.WorkFertileTile(ref agent.GetIntentQueueRW(), farm.Rect, farm.Id, agent.Id, farm.Definition.Range);
}
}
}
}

View File

@@ -0,0 +1,3 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
}

View File

@@ -0,0 +1,47 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DayOnlyAgentSpawnSystemsGroup))]
public class HarvesterAgentSystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IAgentFactory _agentFactory;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IBuildingSpatialQuery _buildingSpatialQuery;
[InjectService]
private IAgentCommonLogic _agentCommonLogic;
public HarvesterAgentSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (var harvester in _entityCache.GetHarvesterBuildings())
{
if (!_agentFactory.CanSpawnAgent(harvester, 1, AgentJob.Harvester)) continue;
var resourceDefinition = harvester.Definition.HarvestedResource;
var position = _tileSpace.TileToWorld(harvester.Rect.Center);
var agent = _agentFactory.CreateVillager(resourceDefinition.HarvesterAgent, harvester, position, AgentJob.Harvester);
var range = harvester.Definition.Range;
IntentQueue.Harvest(ref agent.GetIntentQueueRW(), harvester.Rect, harvester.Id, resourceDefinition, range);
}
}
}
}

View File

@@ -0,0 +1,47 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DayOnlyAgentSpawnSystemsGroup))]
public class HouseAgentSystem : GameSystem, IUpdatable
{
[InjectService]
private GameConfig _config;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IAgentCommonLogic _agentCommonLogic;
public HouseAgentSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var agentDefinition = (AgentDefinition)_config.Agents.GenericAgent.Asset;
foreach (var house in _entityCache.GetHouses())
{
ref var storage = ref house.GetStorageRW();
ref var needsState = ref house.GetNeedsStateRW();
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 || need.Current > need.FetchThreshold) continue;
storage.Get(need.ProductHandle, out var count, out var putReservations, out _);
if (count > 0 || putReservations > 0) continue;
if (_agentCommonLogic.TryFetchProductFromProvider(agentDefinition, house, need.ProductHandle, 1)) break;
}
}
}
}
}

View File

@@ -0,0 +1,78 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EarlyAgentsSystemGroup))]
[UpdateAfter(typeof(AgentSpawnSystemsGroup))]
public class HunterBehaviorSystem : GameSystem, IUpdatable
{
private const int HunterFireRange = 8;
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IIntentLogicExecutor _intentLogicExecutor;
[InjectService]
private IProjectileManager _projectileManager;
public HunterBehaviorSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (var hunter in _entityCache.GetHunterAgents())
{
if (hunter.LifecycleState != AgentLifecycleState.Live) continue;
ref var jobState = ref hunter.GetJobStateRW();
if (jobState.StateMachineStep != AgentStateMachineStep.Hunter_SeekingPrey) continue;
if (UpdateSeekingPrey(hunter)) jobState.StateMachineStep = AgentStateMachineStep.ReturningHome;
}
}
private bool UpdateSeekingPrey(Agent hunter)
{
const float hunterRangeSq = HunterFireRange * HunterFireRange;
if (!_entityCollection.TryGet<Building>(hunter.HomeId, out var hunterBuilding)) return false;
var hunterTile = _tileSpace.WorldToTile(hunter.Position);
var critterDefinitionId = hunterBuilding.Definition.TargetCritter.RuntimeId;
foreach (var critter in _entityCache.GetCritterAgents())
{
ref var critterState = ref critter.GetCritterStateRW();
if (critterState.CritterDefinitionId != critterDefinitionId || critter.LifecycleState != AgentLifecycleState.Live || critterState.LockId != Entity.InvalidId)
continue;
var critterTile = _tileSpace.WorldToTile(critter.Position);
if (math.distancesq(hunterTile, critterTile) > hunterRangeSq) continue;
critterState.LockId = hunter.Id;
ref var hunterIntentQueue = ref _intentLogicExecutor.CancelRemainingIntents(hunter);
var projectileTravelTime = _projectileManager.ComputeProjectileTravelTime(hunter, critter);
IntentQueue.FireAtPreyAndReturnHome(ref hunterIntentQueue, hunterBuilding.Rect, hunterBuilding.Id, critterTile, critter.Id, projectileTravelTime);
ref var critterIntentQueue = ref _intentLogicExecutor.CancelRemainingIntents(critter);
critterIntentQueue.EnqueueIntent(AgentIntent.WaitForever());
return true;
}
return false;
}
}
}

View File

@@ -0,0 +1,45 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DayOnlyAgentSpawnSystemsGroup))]
public class HunterSpawnSystem : GameSystem, IUpdatable
{
[InjectService]
private GameConfig _config;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IAgentFactory _agentFactory;
public HunterSpawnSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var agentDefinition = (AgentDefinition)_config.Agents.HunterAgent.Asset;
foreach (var hunterBuilding in _entityCache.GetHunterBuildings()) SpawnHunterAgent(hunterBuilding, agentDefinition);
}
private void SpawnHunterAgent(Building hunterBuilding, AgentDefinition agentDefinition)
{
if (!_agentFactory.CanSpawnAgent(hunterBuilding, 1, AgentJob.Hunter)) return;
var position = _tileSpace.TileToWorld(hunterBuilding.Rect.Center);
var critterId = hunterBuilding.Definition.TargetCritter.RuntimeId;
var agent = _agentFactory.CreateVillager(agentDefinition, hunterBuilding, position, AgentJob.Hunter);
ref var jobState = ref agent.GetJobStateRW();
jobState.StateMachineStep = AgentStateMachineStep.Hunter_SeekingPrey;
IntentQueue.SeekPrey(ref agent.GetIntentQueueRW(), critterId, hunterBuilding.Definition.Range);
}
}
}

View File

@@ -0,0 +1,35 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IAgentCommonLogic
{
bool MoveAgent(Agent agent, float3 targetPosition, float dt);
bool UpdateHeading(Agent agent, in float3 targetHeading, float dt);
bool TryFetchProductFromStorage(AgentDefinition agentDefinition,
Building source,
int productHandle,
int neededAmount,
int maxAgentCount = int.MaxValue,
AgentJob job = AgentJob.None);
bool TryFetchProductFromProvider(AgentDefinition agentDefinition,
Building source,
int productHandle,
int neededAmount,
int maxAgentCount = int.MaxValue,
AgentJob job = AgentJob.None);
bool TryFetchProduct(AgentDefinition agentDefinition,
Building source,
Building fetchBuilding,
int productHandle,
int amount,
int maxAgentCount = int.MaxValue,
AgentJob job = AgentJob.None);
bool TryDeliverProduct(AgentDefinition agentDefinition, Building source, int productHandle, int maxAgentCount = int.MaxValue, AgentJob job = AgentJob.None);
}
}

View File

@@ -0,0 +1,15 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IAgentFactory
{
void InitializeAgentSource(IAgentSourceEntity source);
bool CanSpawnAgent(IAgentSourceEntity source, int maxAgentCount = int.MaxValue, AgentJob job = AgentJob.None);
Agent CreateVillager(AgentDefinition definition, IAgentSourceEntity source, float3 position, AgentJob job = AgentJob.None);
Agent CreateCritter(CritterDefinition critterDefinition, IAgentSourceEntity source, float3 position);
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IAgentSourceEntity : IEntity
{
float SpawnCooldown { get; }
ref AgentSourceState GetAgentSourceStateRW();
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IAgentVisualizationCollection
{
bool TryGetVisualization(int id, out AgentVisualization visualization);
}
}

View File

@@ -0,0 +1,183 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct AgentIntent
{
public static readonly AgentIntent None = new() { Type = AgentIntentType.None };
public AgentIntentType Type;
public TileRect Rect;
public int TargetId;
public PathTraversalRules PathTraversalRules;
public ushort PathMaxDistance;
public byte StopBeforeGoalStepCount;
public int StorageId;
public byte ProductHandle;
public byte ProductAmount;
public float GenericFloatParam0;
public static AgentIntent MakeLive()
{
return new AgentIntent { Type = AgentIntentType.MakeLive };
}
public static AgentIntent WaitSeconds(float time)
{
return new AgentIntent
{
Type = AgentIntentType.WaitSeconds,
GenericFloatParam0 = time
};
}
public static AgentIntent WaitForever()
{
return new AgentIntent { Type = AgentIntentType.WaitForever };
}
public static AgentIntent FindPath(TileRect rect, int targetId = Entity.InvalidId, PathTraversalRules traversalRules = PathTraversalRules.None)
{
return new AgentIntent
{
Type = AgentIntentType.FindPath,
Rect = rect,
TargetId = targetId,
PathTraversalRules = traversalRules
};
}
public static AgentIntent SearchResourceNode(int targetId, PathTraversalRules traversalRules = PathTraversalRules.None, int maxDistance = 0)
{
var intent = new AgentIntent { Type = AgentIntentType.SearchResourceNode };
SetupSearchTargetIntent(ref intent, targetId, traversalRules, maxDistance);
return intent;
}
public static AgentIntent SearchFertileTile(PathTraversalRules traversalRules = PathTraversalRules.None, int maxDistance = 0)
{
var intent = new AgentIntent { Type = AgentIntentType.SearchFertileTile };
SetupSearchTargetIntent(ref intent, Entity.InvalidId, traversalRules, maxDistance);
return intent;
}
public static AgentIntent SearchCritter(int targetId, PathTraversalRules traversalRules = PathTraversalRules.None, int maxDistance = 0)
{
var intent = new AgentIntent { Type = AgentIntentType.SearchCritter };
SetupSearchTargetIntent(ref intent, targetId, traversalRules, maxDistance);
return intent;
}
private static void SetupSearchTargetIntent(ref AgentIntent intent, int targetId, PathTraversalRules traversalRules, int maxDistance)
{
intent.TargetId = targetId;
intent.PathTraversalRules = traversalRules;
intent.PathMaxDistance = (ushort)maxDistance;
}
public static AgentIntent FollowPath(int stopBeforeGoalStepCount = 0)
{
return new AgentIntent
{
Type = AgentIntentType.FollowPath,
StopBeforeGoalStepCount = (byte)stopBeforeGoalStepCount
};
}
public static AgentIntent MoveToHarvestPosition(float positionOffset)
{
return new AgentIntent
{
Type = AgentIntentType.MoveToHarvestPosition,
GenericFloatParam0 = positionOffset
};
}
public static AgentIntent HarvestResource()
{
return new AgentIntent { Type = AgentIntentType.HarvestResource };
}
public static AgentIntent HarvestFertileTile()
{
return new AgentIntent { Type = AgentIntentType.HarvestFertileTile };
}
public static AgentIntent SetFertileTileLockState(int lockId)
{
return new AgentIntent
{
Type = AgentIntentType.SetFertileTileLockState,
TargetId = lockId
};
}
public static AgentIntent FireProjectile(int targetId)
{
return new AgentIntent
{
Type = AgentIntentType.FireProjectile,
TargetId = targetId
};
}
public static AgentIntent Take(int storageId, int productHandle, int productAmount)
{
return new AgentIntent
{
Type = AgentIntentType.TakeProduct,
StorageId = storageId,
ProductHandle = (byte)productHandle,
ProductAmount = (byte)productAmount
};
}
public static AgentIntent Put(int storageId, int productHandle, int productAmount)
{
return new AgentIntent
{
Type = AgentIntentType.PutProduct,
StorageId = storageId,
ProductHandle = (byte)productHandle,
ProductAmount = (byte)productAmount
};
}
public static AgentIntent Wander()
{
return new AgentIntent { Type = AgentIntentType.Wander };
}
public static AgentIntent LoopWandering()
{
return new AgentIntent { Type = AgentIntentType.LoopWandering };
}
public static AgentIntent LookAt(int2 tile)
{
return new AgentIntent
{
Type = AgentIntentType.LookAt,
Rect = TileRect.OneTile(tile)
};
}
public static AgentIntent PlayAnimation(AgentAnimation animation)
{
return new AgentIntent
{
Type = AgentIntentType.PlayAnimation,
TargetId = (int)animation
};
}
}
}

View File

@@ -0,0 +1,47 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum AgentIntentType : byte
{
MakeLive,
WaitSeconds,
WaitForever,
FindPath,
SearchResourceNode,
SearchFertileTile,
SearchCritter,
FollowPath,
MoveToHarvestPosition,
HarvestResource,
HarvestFertileTile,
SetFertileTileLockState,
FireProjectile,
TakeProduct,
PutProduct,
Wander,
LoopWandering,
LookAt,
PlayAnimation,
Count,
None = byte.MaxValue
}
}

View File

@@ -0,0 +1,26 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class FireProjectileExecutionLogic : IntentExecutionLogic
{
private IEntityCollection _entityCollection;
private IProjectileManager _projectileManager;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_entityCollection = serviceLocator.GetService<IEntityCollection>();
_projectileManager = serviceLocator.GetService<IProjectileManager>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
var critter = _entityCollection.Get<Agent>(intent.TargetId);
_projectileManager.FireProjectile(agent, critter);
return IntentExecutionResult.Success;
}
}
}

View File

@@ -0,0 +1,41 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class FollowPathIntentExecutionLogic : IntentExecutionLogic
{
private ITileSpace _tileSpace;
private IAgentCommonLogic _agentCommonLogic;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_tileSpace = serviceLocator.GetService<ITileSpace>();
_agentCommonLogic = serviceLocator.GetService<IAgentCommonLogic>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult OnStartingIntent(Agent agent, in AgentIntent intent)
{
agent.PathIndex = 0;
return IntentExecutionResult.Success;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
ref var path = ref agent.GetPathRW();
if (path.StepCount <= 1) return IntentExecutionResult.Success;
var nextTile = path.Steps[agent.PathIndex + 1];
var targetPosition = (float3)_tileSpace.TileToWorld(nextTile);
var targetReached = _agentCommonLogic.MoveAgent(agent, targetPosition, Time.deltaTime);
var lastStepReached = targetReached && ++agent.PathIndex >= path.StepCount - 1 - intent.StopBeforeGoalStepCount;
return lastStepReached ? IntentExecutionResult.Success : IntentExecutionResult.InProgress;
}
}
}

View File

@@ -0,0 +1,42 @@
using Cysharp.Threading.Tasks;
using Random = UnityEngine.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HarvestFertileTileIntentExecutionLogic : IntentExecutionLogic
{
private IEntityCollection _entityCollection;
private IProductStackFactory _productStackFactory;
private World _world;
private ITileSpace _tileSpace;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_entityCollection = serviceLocator.GetService<IEntityCollection>();
_productStackFactory = serviceLocator.GetService<IProductStackFactory>();
_world = serviceLocator.GetService<World>();
_tileSpace = serviceLocator.GetService<ITileSpace>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
if (!_entityCollection.TryGet<Building>(agent.HomeId, out var farm)) return IntentExecutionResult.Failure;
var tile = _tileSpace.WorldToTile(agent.Position);
ref var fertility = ref _world.Fertility.GetValueRW(tile);
fertility.CurrentFertility = 0;
var product = farm.Definition.FarmProduct;
var amount = Random.Range(farm.Definition.MinDroppedAmount, farm.Definition.MaxDroppedAmount + 1);
_productStackFactory.CreateOrMerge(tile, product, amount);
return IntentExecutionResult.Success;
}
}
}

View File

@@ -0,0 +1,31 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HarvestResourceIntentExecutionLogic : IntentExecutionLogic
{
private World _world;
private ISignalBus _signalBus;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_world = serviceLocator.GetService<World>();
_signalBus = serviceLocator.GetService<ISignalBus>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
ref var jobState = ref agent.GetJobStateRW();
var resourceNodeId = jobState.Harvester.ResourceNodeId;
if (!_world.RawResources.Exists(resourceNodeId)) return IntentExecutionResult.Failure;
_signalBus.Raise(new RawResourceHarvestedSignal(resourceNodeId));
return IntentExecutionResult.Success;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IIntentLogicExecutor
{
ref IntentQueue CancelRemainingIntents(Agent agent);
}
}

View File

@@ -0,0 +1,23 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public abstract class IntentExecutionLogic
{
public virtual UniTask InitializeAsync(IServiceLocator serviceLocator)
{
return UniTask.CompletedTask;
}
public virtual IntentExecutionResult OnStartingIntent(Agent agent, in AgentIntent intent)
{
return IntentExecutionResult.Success;
}
public virtual void OnCanceled(Agent agent, AgentIntent intent)
{
}
public abstract IntentExecutionResult Execute(Agent agent, in AgentIntent intent);
}
}

View File

@@ -0,0 +1,11 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum IntentExecutionResult
{
InProgress,
Failure,
Success
}
}

View File

@@ -0,0 +1,11 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum IntentExecutionState
{
WaitingIntent,
ExecutingIntent,
EmptyQueue
}
}

View File

@@ -0,0 +1,108 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IIntentLogicExecutor))]
[GameSystemGroup(typeof(DefaultAgentsSystemGroup))]
[UpdateAfter(typeof(AgentManagerSystem))]
public class IntentExecutionSystem : GameSystem, IInitializable, IUpdatable, IIntentLogicExecutor
{
[InjectService]
private IEntityCollection _entityCollection;
private readonly IntentExecutionLogic[] _executionLogic = new IntentExecutionLogic[(int)AgentIntentType.Count];
public IntentExecutionSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public async UniTask InitializeAsync()
{
var pathfindingLogic = new PathfindingIntentExecutionLogic();
var wanderingLogic = new WanderingExecutionLogic();
_executionLogic[(int)AgentIntentType.MakeLive] = new MakeLiveIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.WaitSeconds] = new WaitSecondsIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.WaitForever] = new WaitForeverIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.FindPath] = pathfindingLogic;
_executionLogic[(int)AgentIntentType.SearchResourceNode] = pathfindingLogic;
_executionLogic[(int)AgentIntentType.SearchFertileTile] = pathfindingLogic;
_executionLogic[(int)AgentIntentType.SearchCritter] = pathfindingLogic;
_executionLogic[(int)AgentIntentType.FollowPath] = new FollowPathIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.MoveToHarvestPosition] = new MoveToHarvestPositionIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.HarvestResource] = new HarvestResourceIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.HarvestFertileTile] = new HarvestFertileTileIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.SetFertileTileLockState] = new SetFertileTileLockStateIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.FireProjectile] = new FireProjectileExecutionLogic();
_executionLogic[(int)AgentIntentType.TakeProduct] = new TakeProductIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.PutProduct] = new PutProductIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.Wander] = wanderingLogic;
_executionLogic[(int)AgentIntentType.LoopWandering] = wanderingLogic;
_executionLogic[(int)AgentIntentType.LookAt] = new LookAtIntentExecutionLogic();
_executionLogic[(int)AgentIntentType.PlayAnimation] = new PlayAnimationIntentExecutionLogic();
foreach (var executionLogic in _executionLogic) await executionLogic.InitializeAsync(ServiceLocator);
}
public void Update()
{
foreach (Agent agent in _entityCollection.GetInternalEntityList(typeof(Agent))) UpdateAgent(agent);
}
private void UpdateAgent(Agent agent)
{
if (agent.LifecycleState == AgentLifecycleState.DeSpawning)
{
CancelRemainingIntents(agent);
return;
}
ref var intentQueue = ref agent.GetIntentQueueRW();
if (!intentQueue.TryPeekIntent(out var intent))
{
agent.IntentExecutionState = IntentExecutionState.EmptyQueue;
return;
}
var executionLogic = _executionLogic[(int)intent.Type];
if (agent.IntentExecutionState == IntentExecutionState.WaitingIntent)
{
if (executionLogic.OnStartingIntent(agent, intent) == IntentExecutionResult.Failure)
{
intentQueue.TryDequeueIntent(out _);
CancelRemainingIntents(agent);
return;
}
agent.IntentExecutionState = IntentExecutionState.ExecutingIntent;
agent.IntentExecutionTime = 0;
}
var result = executionLogic.Execute(agent, intent);
if (result == IntentExecutionResult.InProgress)
{
agent.IntentExecutionTime += Time.deltaTime;
return;
}
intentQueue.TryDequeueIntent(out _);
if (result == IntentExecutionResult.Failure) CancelRemainingIntents(agent);
agent.IntentExecutionState = IntentExecutionState.WaitingIntent;
}
public ref IntentQueue CancelRemainingIntents(Agent agent)
{
ref var intentQueue = ref agent.GetIntentQueueRW();
while (intentQueue.TryDequeueIntent(out var intent)) _executionLogic[(int)intent.Type].OnCanceled(agent, intent);
agent.IntentExecutionState = IntentExecutionState.WaitingIntent;
return ref intentQueue;
}
}
}

View File

@@ -0,0 +1,156 @@
using Unity.Collections;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct IntentQueue
{
private FixedList512Bytes<AgentIntent> _intentQueue;
public int Count => _intentQueue.Length;
public void EnqueueIntent(in AgentIntent intent)
{
_intentQueue.Add(intent);
}
public bool TryDequeueIntent(out AgentIntent intent)
{
if (_intentQueue.Length > 0)
{
intent = _intentQueue[0];
_intentQueue.RemoveAt(0);
return true;
}
intent = AgentIntent.None;
return false;
}
public bool TryPeekIntent(out AgentIntent intent)
{
if (_intentQueue.Length > 0)
{
intent = _intentQueue[0];
return true;
}
intent = AgentIntent.None;
return false;
}
public void Clear()
{
_intentQueue.Clear();
}
public static void Fetch(ref IntentQueue intentQueue,
TileRect sourceRect,
int sourceId,
TileRect destinationRect,
int destinationId,
int productHandle,
int amount,
PathTraversalRules traversalRules)
{
intentQueue.EnqueueIntent(AgentIntent.FindPath(destinationRect, destinationId, traversalRules));
intentQueue.EnqueueIntent(AgentIntent.MakeLive());
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
intentQueue.EnqueueIntent(AgentIntent.Take(destinationId, productHandle, amount));
intentQueue.EnqueueIntent(AgentIntent.FindPath(sourceRect, sourceId, traversalRules));
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
intentQueue.EnqueueIntent(AgentIntent.Put(sourceId, productHandle, amount));
}
public static void Deliver(ref IntentQueue intentQueue,
TileRect sourceRect,
int sourceId,
TileRect destinationRect,
int destinationId,
int productHandle,
int amount,
PathTraversalRules traversalRules)
{
intentQueue.EnqueueIntent(AgentIntent.FindPath(destinationRect, destinationId, traversalRules));
intentQueue.EnqueueIntent(AgentIntent.MakeLive());
intentQueue.EnqueueIntent(AgentIntent.Take(sourceId, productHandle, amount));
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
intentQueue.EnqueueIntent(AgentIntent.Put(destinationId, productHandle, amount));
intentQueue.EnqueueIntent(AgentIntent.FindPath(sourceRect, sourceId, traversalRules));
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
}
public static void MoveProduct(ref IntentQueue intentQueue,
TileRect sourceRect,
int sourceId,
TileRect fetchRect,
int fetchId,
TileRect deliverRect,
int deliverId,
int productHandle,
int amount,
PathTraversalRules traversalRules)
{
intentQueue.EnqueueIntent(AgentIntent.FindPath(fetchRect, fetchId, traversalRules));
intentQueue.EnqueueIntent(AgentIntent.MakeLive());
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
intentQueue.EnqueueIntent(AgentIntent.Take(fetchId, productHandle, amount));
intentQueue.EnqueueIntent(AgentIntent.FindPath(deliverRect, deliverId, traversalRules));
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
intentQueue.EnqueueIntent(AgentIntent.Put(deliverId, productHandle, amount));
intentQueue.EnqueueIntent(AgentIntent.FindPath(sourceRect, sourceId, traversalRules));
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
}
public static void Harvest(ref IntentQueue intentQueue, TileRect sourceRect, int sourceId, ResourceNodeDefinition resource, int maxDistance)
{
intentQueue.EnqueueIntent(AgentIntent.SearchResourceNode(resource.RuntimeId, PathTraversalRules.GenericAgent, maxDistance));
intentQueue.EnqueueIntent(AgentIntent.MakeLive());
intentQueue.EnqueueIntent(AgentIntent.FollowPath(1));
intentQueue.EnqueueIntent(AgentIntent.MoveToHarvestPosition(resource.HarvestPositionOffset));
intentQueue.EnqueueIntent(AgentIntent.PlayAnimation(resource.HarvestAnimation));
intentQueue.EnqueueIntent(AgentIntent.HarvestResource());
intentQueue.EnqueueIntent(AgentIntent.WaitSeconds(1.5f));
intentQueue.EnqueueIntent(AgentIntent.FindPath(sourceRect, sourceId));
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
}
public static void SeekPrey(ref IntentQueue intentQueue, int critterDefinitionId, int maxDistance)
{
intentQueue.EnqueueIntent(AgentIntent.SearchCritter(critterDefinitionId, PathTraversalRules.GenericAgent, maxDistance));
intentQueue.EnqueueIntent(AgentIntent.MakeLive());
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
}
public static void FireAtPreyAndReturnHome(ref IntentQueue intentQueue, TileRect homeRect, int homeId, int2 critterTile, int critterId, float projectileTravelTime)
{
intentQueue.EnqueueIntent(AgentIntent.LookAt(critterTile));
intentQueue.EnqueueIntent(AgentIntent.PlayAnimation(AgentAnimation.FireProjectile));
intentQueue.EnqueueIntent(AgentIntent.FireProjectile(critterId));
intentQueue.EnqueueIntent(AgentIntent.WaitSeconds(projectileTravelTime + 1));
intentQueue.EnqueueIntent(AgentIntent.FindPath(homeRect, homeId));
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
}
public static void WorkFertileTile(ref IntentQueue intentQueue, TileRect sourceRect, int sourceId, int agentId, int maxDistance)
{
intentQueue.EnqueueIntent(AgentIntent.SearchFertileTile(PathTraversalRules.GenericAgent | PathTraversalRules.FertileTilesOnly, maxDistance));
intentQueue.EnqueueIntent(AgentIntent.SetFertileTileLockState(agentId));
intentQueue.EnqueueIntent(AgentIntent.MakeLive());
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
intentQueue.EnqueueIntent(AgentIntent.PlayAnimation(AgentAnimation.Farming));
intentQueue.EnqueueIntent(AgentIntent.HarvestFertileTile());
intentQueue.EnqueueIntent(AgentIntent.WaitSeconds(2.5f));
intentQueue.EnqueueIntent(AgentIntent.SetFertileTileLockState(Entity.InvalidId));
intentQueue.EnqueueIntent(AgentIntent.FindPath(sourceRect, sourceId));
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
}
public static void LoopWandering(ref IntentQueue intentQueue, float idleTime)
{
intentQueue.EnqueueIntent(AgentIntent.WaitSeconds(idleTime));
intentQueue.EnqueueIntent(AgentIntent.Wander());
intentQueue.EnqueueIntent(AgentIntent.FollowPath());
intentQueue.EnqueueIntent(AgentIntent.LoopWandering());
}
}
}

View File

@@ -0,0 +1,63 @@
using System.Runtime.CompilerServices;
using Cysharp.Threading.Tasks;
using Unity.Burst;
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[BurstCompile]
public class LookAtIntentExecutionLogic : IntentExecutionLogic
{
private ITileSpace _tileSpace;
private float _rotationSpeed;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_tileSpace = serviceLocator.GetService<ITileSpace>();
_rotationSpeed = serviceLocator.GetService<GameConfig>().Agents.RotationSpeed;
return UniTask.CompletedTask;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
var lookAt = math.normalizesafe((float3)_tileSpace.TileToWorld(intent.Rect.Center) - agent.Position);
if (math.all(lookAt == 0)) return IntentExecutionResult.Success;
var step = math.radians(_rotationSpeed) * Time.deltaTime;
var heading = agent.Heading;
LookAt(ref heading, lookAt, step);
agent.Heading = heading;
return math.dot(heading, lookAt) > 0.995f ? IntentExecutionResult.Success : IntentExecutionResult.InProgress;
}
[BurstCompile]
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void LookAt(ref float3 heading, in float3 lookAt, float maxRadiansDelta)
{
var currentRotation = quaternion.LookRotationSafe(heading, math.up());
var targetRotation = quaternion.LookRotationSafe(lookAt, math.up());
var dot = math.dot(currentRotation, targetRotation);
if (dot < 0)
{
dot = -dot;
targetRotation.value = -targetRotation.value;
}
dot = math.clamp(dot, -1, 1);
var angle = math.acos(dot) * 2;
if (angle <= maxRadiansDelta)
{
heading = lookAt;
return;
}
currentRotation = math.slerp(currentRotation, targetRotation, maxRadiansDelta / angle);
heading = math.mul(currentRotation, math.forward());
}
}
}

View File

@@ -0,0 +1,50 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class MakeLiveIntentExecutionLogic : IntentExecutionLogic
{
private ITileSpace _tileSpace;
private IAgentVisualizationCollection _visualizationCollection;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_tileSpace = serviceLocator.GetService<ITileSpace>();
_visualizationCollection = serviceLocator.GetService<IAgentVisualizationCollection>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
switch (agent.LifecycleState)
{
case AgentLifecycleState.DeSpawning:
return IntentExecutionResult.Failure;
case AgentLifecycleState.Live:
return IntentExecutionResult.Success;
}
if (!_visualizationCollection.TryGetVisualization(agent.Id, out _)) return IntentExecutionResult.InProgress;
agent.LifecycleState = AgentLifecycleState.Live;
SetInitialHeading(agent);
return IntentExecutionResult.Success;
}
private void SetInitialHeading(Agent agent)
{
ref var path = ref agent.GetPathRW();
if (path.StepCount <= 1) return;
var p0 = _tileSpace.TileToWorld(path.Steps[0]);
var p1 = _tileSpace.TileToWorld(path.Steps[1]);
agent.Heading = math.normalizesafe(p1 - p0, math.forward());
}
}
}

View File

@@ -0,0 +1,58 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class MoveToHarvestPositionIntentExecutionLogic : IntentExecutionLogic
{
private GameConfig _config;
private World _world;
private IAgentCommonLogic _agentCommonLogic;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_config = serviceLocator.GetService<GameConfig>();
_world = serviceLocator.GetService<World>();
_agentCommonLogic = serviceLocator.GetService<IAgentCommonLogic>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult OnStartingIntent(Agent agent, in AgentIntent intent)
{
ref var path = ref agent.GetPathRW();
var tile = path.Steps[^1];
using var resourceNodesScope = ListPool<(int, bool)>.Get(out var resourceNodeIds);
_world.RawResources.GetResourceNodes(TileRect.OneTile(tile), _config.GeneralSettings.BaseElevation, resourceNodeIds);
if (resourceNodeIds.Count <= 0) return IntentExecutionResult.Failure;
var (id, _) = resourceNodeIds[0];
_world.RawResources.TryGetResourceNode(id, out var resourceNode);
ref var jobState = ref agent.GetJobStateRW();
jobState.Harvester.ResourceNodeId = id;
jobState.Harvester.TargetPosition = resourceNode.Position + math.normalizesafe(agent.Position - resourceNode.Position) * intent.GenericFloatParam0;
return IntentExecutionResult.Success;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
ref var jobState = ref agent.GetJobStateRW();
var targetReached = _agentCommonLogic.MoveAgent(agent, jobState.Harvester.TargetPosition, Time.deltaTime);
if (!targetReached) return IntentExecutionResult.InProgress;
if (!_world.RawResources.TryGetResourceNode(jobState.Harvester.ResourceNodeId, out var resourceNode)) return IntentExecutionResult.Failure;
var desiredHeading = math.normalizesafe(resourceNode.Position - agent.Position, agent.Heading);
return _agentCommonLogic.UpdateHeading(agent, desiredHeading, Time.deltaTime) ? IntentExecutionResult.Success : IntentExecutionResult.InProgress;
}
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct PathQueryFailedSignal
{
public PathTraversalRules TraversalRules;
public PathQueryFailedSignal(PathTraversalRules traversalRules)
{
TraversalRules = traversalRules;
}
}
}

View File

@@ -0,0 +1,82 @@
using System;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PathfindingIntentExecutionLogic : IntentExecutionLogic
{
private IPathfinder _pathfinder;
private ITileSpace _tileSpace;
private IFailedPathCache _failedPathCache;
private ISignalBus _signalBus;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_pathfinder = serviceLocator.GetService<IPathfinder>();
_tileSpace = serviceLocator.GetService<ITileSpace>();
_failedPathCache = serviceLocator.GetService<IFailedPathCache>();
_signalBus = serviceLocator.GetService<ISignalBus>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult OnStartingIntent(Agent agent, in AgentIntent intent)
{
ref var path = ref agent.GetPathRW();
path.Steps.Clear();
var searchType = intent.Type switch
{
AgentIntentType.FindPath => PathSearchType.FindPath,
AgentIntentType.SearchResourceNode => PathSearchType.SearchResourceNode,
AgentIntentType.SearchFertileTile => PathSearchType.SearchFertileTile,
AgentIntentType.SearchCritter => PathSearchType.SearchCritterHerd,
_ => throw new ArgumentOutOfRangeException()
};
var query = new PathQuery
{
SearchType = searchType,
SourceTile = _tileSpace.WorldToTile(agent.Position),
DestinationRect = intent.Rect,
TargetId = intent.TargetId,
TraversalRules = intent.PathTraversalRules,
MaxDistance = intent.PathMaxDistance
};
agent.PathQueryHandle = _pathfinder.FindPath(path, ref query);
return IntentExecutionResult.Success;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
switch (_pathfinder.CompletePath(agent.PathQueryHandle))
{
case PathQueryResult.InProgress:
return IntentExecutionResult.InProgress;
case PathQueryResult.InvalidQuery:
return IntentExecutionResult.Failure;
case PathQueryResult.Failure:
if ((intent.PathTraversalRules & PathTraversalRules.UpdateFailedPathCache) != 0) _failedPathCache.FailPath(agent.HomeId, intent.TargetId);
_signalBus.Raise(new PathQueryFailedSignal(intent.PathTraversalRules));
return IntentExecutionResult.Failure;
}
ref var path = ref agent.GetPathRW();
var agentTile = _tileSpace.WorldToTile(agent.Position);
if (path.Steps.Length > 0 && math.any(agentTile != path.Steps[0]))
{
agent.Position = _tileSpace.TileToWorld(path.Steps[0]);
agent.Velocity = float3.zero;
}
return IntentExecutionResult.Success;
}
}
}

View File

@@ -0,0 +1,52 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PlayAnimationIntentExecutionLogic : IntentExecutionLogic
{
private static readonly int ChopTree = Animator.StringToHash("ChopTree");
private static readonly int Farming = Animator.StringToHash("Farming");
private static readonly int FireProjectile = Animator.StringToHash("FireProjectile");
private IAgentVisualizationCollection _agentVisualizationCollection;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_agentVisualizationCollection = serviceLocator.GetService<IAgentVisualizationCollection>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult OnStartingIntent(Agent agent, in AgentIntent intent)
{
if (!_agentVisualizationCollection.TryGetVisualization(agent.Id, out var visualization)) return IntentExecutionResult.Failure;
var triggerID = GetAnimatorTriggerID((AgentAnimation)intent.TargetId);
visualization.SetAnimatorTrigger(triggerID);
return IntentExecutionResult.Success;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
if (!_agentVisualizationCollection.TryGetVisualization(agent.Id, out var visualization)) return IntentExecutionResult.Failure;
return !visualization.WaitAnimationCompleted() ? IntentExecutionResult.InProgress : IntentExecutionResult.Success;
}
private int GetAnimatorTriggerID(AgentAnimation animation)
{
return animation switch
{
AgentAnimation.ChopTree => ChopTree,
AgentAnimation.Farming => Farming,
AgentAnimation.FireProjectile => FireProjectile,
_ => throw new ArgumentOutOfRangeException()
};
}
}
}

View File

@@ -0,0 +1,44 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PutProductIntentExecutionLogic : IntentExecutionLogic
{
private IProductStorageManager _productStorageManager;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_productStorageManager = serviceLocator.GetService<IProductStorageManager>();
return UniTask.CompletedTask;
}
public override void OnCanceled(Agent agent, AgentIntent intent)
{
base.OnCanceled(agent, intent);
if (!_productStorageManager.TryGetProductStorageEntity(intent.StorageId, out var storageEntity)) return;
ref var storage = ref storageEntity.GetStorageRW();
storage.ReleasePutReservation(intent.ProductHandle, intent.ProductAmount);
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
if (!_productStorageManager.TryGetProductStorageEntity(intent.StorageId, out var storageEntity)) return IntentExecutionResult.Failure;
ref var storage = ref storageEntity.GetStorageRW();
if (!storage.FulfillPut(intent.ProductHandle, intent.ProductAmount))
{
storage.ReleasePutReservation(intent.ProductHandle, intent.ProductAmount);
Debug.LogError("Agent could not fulfill putting product");
return IntentExecutionResult.Failure;
}
agent.CarriedProductHandle = IProductCatalog.InvalidHandle;
return IntentExecutionResult.Success;
}
}
}

View File

@@ -0,0 +1,61 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class SetFertileTileLockStateIntentExecutionLogic : IntentExecutionLogic
{
private World _world;
private ITileSpace _tileSpace;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_world = serviceLocator.GetService<World>();
_tileSpace = serviceLocator.GetService<ITileSpace>();
return UniTask.CompletedTask;
}
public override void OnCanceled(Agent agent, AgentIntent intent)
{
if (intent.TargetId != Entity.InvalidId) return;
Unlock(agent);
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
return intent.TargetId == Entity.InvalidId ? Unlock(agent) : Lock(agent);
}
private IntentExecutionResult Lock(Agent agent)
{
ref var path = ref agent.GetPathRW();
var tile = path.StepCount > 0 ? path.Steps[^1] : _tileSpace.WorldToTile(agent.Position);
ref var fertility = ref _world.Fertility.GetValueRW(tile);
if (fertility.MaxFertility <= 0 || fertility.CurrentFertility < fertility.MaxFertility || fertility.LockId != Entity.InvalidId) return IntentExecutionResult.Failure;
fertility.LockId = agent.Id;
ref var farming = ref agent.GetJobStateRW().Farming;
farming.LockedTile = tile;
return IntentExecutionResult.Success;
}
private IntentExecutionResult Unlock(Agent agent)
{
ref var farming = ref agent.GetJobStateRW().Farming;
if (farming.LockedTile == null) return IntentExecutionResult.Success;
var tile = farming.LockedTile.Value;
ref var fertility = ref _world.Fertility.GetValueRW(tile);
if (fertility.LockId == agent.Id) fertility.LockId = Entity.InvalidId;
farming.LockedTile = null;
return IntentExecutionResult.Success;
}
}
}

View File

@@ -0,0 +1,43 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TakeProductIntentExecutionLogic : IntentExecutionLogic
{
private IProductStorageManager _productStorageManager;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_productStorageManager = serviceLocator.GetService<IProductStorageManager>();
return UniTask.CompletedTask;
}
public override void OnCanceled(Agent agent, AgentIntent intent)
{
base.OnCanceled(agent, intent);
if (!_productStorageManager.TryGetProductStorageEntity(intent.StorageId, out var storageEntity)) return;
ref var storage = ref storageEntity.GetStorageRW();
storage.ReleaseTakeReservation(intent.ProductHandle, intent.ProductAmount);
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
if (!_productStorageManager.TryGetProductStorageEntity(intent.StorageId, out var storageEntity)) return IntentExecutionResult.Failure;
ref var storage = ref storageEntity.GetStorageRW();
if (!storage.FulfillTake(intent.ProductHandle, intent.ProductAmount))
{
Debug.LogError("Agent could not fulfill taking product");
return IntentExecutionResult.Failure;
}
agent.CarriedProductHandle = intent.ProductHandle;
return IntentExecutionResult.Success;
}
}
}

View File

@@ -0,0 +1,10 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class WaitForeverIntentExecutionLogic : IntentExecutionLogic
{
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
return IntentExecutionResult.InProgress;
}
}
}

View File

@@ -0,0 +1,10 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class WaitSecondsIntentExecutionLogic : IntentExecutionLogic
{
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
return agent.IntentExecutionTime < intent.GenericFloatParam0 ? IntentExecutionResult.InProgress : IntentExecutionResult.Success;
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine.Pool;
using Random = UnityEngine.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class WanderingExecutionLogic : IntentExecutionLogic
{
private ITileSpace _tileSpace;
private World _world;
public override UniTask InitializeAsync(IServiceLocator serviceLocator)
{
_tileSpace = serviceLocator.GetService<ITileSpace>();
_world = serviceLocator.GetService<World>();
return UniTask.CompletedTask;
}
public override IntentExecutionResult Execute(Agent agent, in AgentIntent intent)
{
switch (intent.Type)
{
case AgentIntentType.Wander:
return Wander(agent);
case AgentIntentType.LoopWandering:
LoopWandering(agent);
return IntentExecutionResult.Success;
}
throw new InvalidOperationException();
}
private IntentExecutionResult Wander(Agent agent)
{
using var candidatesScope = ListPool<int2>.Get(out var candidates);
var tile = _tileSpace.WorldToTile(agent.Position);
for (var i = 0; i < 8; i++)
{
var candidate = ((Directions)i).ToVector();
if (!_world.BlockMap.IsBlocked(tile + candidate, BlockReason.BlocksAgents)) candidates.Add(candidate);
}
if (candidates.Count <= 0) return IntentExecutionResult.Failure;
var direction = candidates[Random.Range(0, candidates.Count)];
if (agent.Definition.WanderingRadius > 0)
{
var distance = TileMath.StepCount(agent.WanderingCenter, tile + direction);
if (distance > agent.Definition.WanderingRadius) direction = math.sign(agent.WanderingCenter - tile);
}
ref var path = ref agent.GetPathRW();
path.Steps.Clear();
path.Steps.Add(tile);
path.Steps.Add(tile + direction);
return IntentExecutionResult.Success;
}
private void LoopWandering(Agent agent)
{
var idleTimeRange = agent.Definition.WanderingIdleTime;
var idleTime = Random.Range(idleTimeRange.x, idleTimeRange.y);
IntentQueue.LoopWandering(ref agent.GetIntentQueueRW(), idleTime);
}
}
}

View File

@@ -0,0 +1,39 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DayOnlyAgentSpawnSystemsGroup))]
public class ProducerDeliveryAgentSystem : GameSystem, IUpdatable
{
[InjectService]
private GameConfig _config;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IAgentCommonLogic _agentCommonLogic;
public ProducerDeliveryAgentSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var agentDefinition = (AgentDefinition)_config.Agents.GenericAgent.Asset;
foreach (var producer in _entityCache.GetProducers())
{
if (!producer.Definition.DeliversOutput) continue;
ref var productionState = ref producer.GetProductionStateRW();
ref var recipe = ref productionState.Recipe;
foreach (var (productHandle, amount) in recipe.Inputs)
if (_agentCommonLogic.TryFetchProductFromStorage(agentDefinition, producer, productHandle, amount))
return;
_agentCommonLogic.TryDeliverProduct(agentDefinition, producer, recipe.OutputProductHandle);
}
}
}
}

View File

@@ -0,0 +1,36 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DayOnlyAgentSpawnSystemsGroup))]
public class ProviderAgentSystem : GameSystem, IUpdatable
{
private const int MaxAgentCount = 10;
[InjectService]
private GameConfig _config;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IAgentCommonLogic _agentCommonLogic;
public ProviderAgentSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var agentDefinition = (AgentDefinition)_config.Agents.GenericAgent.Asset;
foreach (var provider in _entityCache.GetProviders())
if (provider.Definition.FetchesProducts)
foreach (var (product, amount) in provider.Definition.ProvidedProducts)
if (_agentCommonLogic.TryFetchProductFromStorage(agentDefinition, provider, _productCatalog.GetHandle(product), amount, MaxAgentCount))
break;
}
}
}

View File

@@ -0,0 +1,21 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(AgentsSystemGroup))]
[UpdateBefore(typeof(IntentExecutionSystem))]
[UpdateBefore(typeof(AgentAnimationSystem))]
public class ResetAgentVelocitySystem : GameSystem, IUpdatable
{
[InjectService]
private IEntityCollection _entityCollection;
public ResetAgentVelocitySystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
foreach (Agent agent in _entityCollection.GetInternalEntityList(typeof(Agent))) agent.Velocity = 0;
}
}
}

View File

@@ -0,0 +1,19 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(LateAgentsSystemGroup))]
public class ResetAgentsSpawnTickSystem : GameSystem, IUpdatable
{
[InjectService]
private World _world;
public ResetAgentsSpawnTickSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
_world.AgentsState.SpawnTickNow = false;
}
}
}

View File

@@ -0,0 +1,63 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DayOnlyAgentSpawnSystemsGroup))]
[UpdateBefore(typeof(ConstructionAgentSystem))]
public class StorageBuildingFetchProductStacksAgentSystem : GameSystem, IUpdatable
{
private const int MaxAgentCount = 10;
[InjectService]
private GameConfig _config;
[InjectService]
private World _world;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IAgentFactory _agentFactory;
[InjectService]
private ITileSpace _tileSpace;
public StorageBuildingFetchProductStacksAgentSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var agentDefinition = (AgentDefinition)_config.Agents.GenericAgent.Asset;
foreach (var storageBuilding in _entityCache.GetStorageBuildings())
{
if (!_agentFactory.CanSpawnAgent(storageBuilding, MaxAgentCount)) continue;
ref var storage = ref storageBuilding.GetStorageRW();
if (storage.FreeSpace() <= 0) continue;
ref var storagePolicy = ref storageBuilding.GetProductStoragePolicyRW();
if (!_world.ProductStacks.FindClosest(storageBuilding.Rect, storageBuilding.Definition.Range, 1, storagePolicy, out var productStack)) continue;
storage.ReservePutting(productStack.ProductHandle, 1);
ref var productStackStorage = ref productStack.GetStorageRW();
productStackStorage.ReserveTaking(productStack.ProductHandle, 1);
var position = _tileSpace.TileToWorld(storageBuilding.Rect.Center);
var agent = _agentFactory.CreateVillager(agentDefinition, storageBuilding, position);
IntentQueue.Fetch(
ref agent.GetIntentQueueRW(),
storageBuilding.Rect,
storageBuilding.Id,
TileRect.OneTile(productStack.Tile),
productStack.Id,
productStack.ProductHandle,
1,
PathTraversalRules.GenericAgent | PathTraversalRules.UpdateFailedPathCache);
}
}
}
}

View File

@@ -0,0 +1,63 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(AgentSpawnSystemsGroup))]
public class StorageBuildingRequestProductAgentSystem : GameSystem, IUpdatable
{
private const int MaxAgentCount = 20;
[InjectService]
private GameConfig _config;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IProductCatalog _productCatalog;
[InjectService]
private IBuildingSpatialQuery _spatialQuery;
[InjectService]
private IAgentCommonLogic _agentCommonLogic;
public StorageBuildingRequestProductAgentSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
var agentDefinition = (AgentDefinition)_config.Agents.GenericAgent.Asset;
var productTypeCount = _productCatalog.ProductTypeCount;
if (productTypeCount <= 0) return;
foreach (var storageBuilding in _entityCache.GetStorageRequestBuildings())
{
ref var storage = ref storageBuilding.GetStorageRW();
if (storage.FreeSpace() <= 0) continue;
ref var storagePolicy = ref storageBuilding.GetProductStoragePolicyRW();
var startProductHandle = storagePolicy.NextProductToFetch;
for (var i = 0; i < productTypeCount; i++)
{
var productHandle = (startProductHandle + i) % productTypeCount;
var requestedAmount = storagePolicy.GetRequestedAmount(productHandle);
if (requestedAmount <= 0) continue;
if (storage.CountIncludingReservations(productHandle) >= requestedAmount) continue;
if (!_spatialQuery.FindStorageForRequest(storageBuilding.Id, storageBuilding.Rect, storageBuilding.Definition.Range, productHandle, out var fetchBuilding))
continue;
if (!_agentCommonLogic.TryFetchProduct(agentDefinition, storageBuilding, fetchBuilding, productHandle, 1, MaxAgentCount, AgentJob.StorageRequestHauler)) continue;
storagePolicy.NextProductToFetch = (productHandle + 1) % productTypeCount;
break;
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EarlyAgentsSystemGroup))]
[UpdateAfter(typeof(AgentsSpawnTickSystem))]
public class AgentSpawnSystemsGroup : GameSystemGroup
{
}
}

View File

@@ -0,0 +1,10 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[RequiresWorldReadyForUpdate]
[GameSystemGroup(typeof(DefaultGameSystemGroup))]
[UpdateAfter(typeof(EconomySystemGroup))]
public class AgentsSystemGroup : GameSystemGroup
{
}
}

View File

@@ -0,0 +1,18 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(AgentSpawnSystemsGroup))]
public class DayOnlyAgentSpawnSystemsGroup : GameSystemGroup
{
[InjectService]
private World _world;
public override void Update()
{
if (!_world.AgentsState.SpawnTickNow) return;
if (_world.TimeState.DayNightCycleStep != DayNightCycleStep.Day) return;
base.Update();
}
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(AgentsSystemGroup))]
[UpdateAfter(typeof(EarlyAgentsSystemGroup))]
public class DefaultAgentsSystemGroup : GameSystemGroup
{
}
}

View File

@@ -0,0 +1,8 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(AgentsSystemGroup))]
public class EarlyAgentsSystemGroup : GameSystemGroup
{
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(AgentsSystemGroup))]
[UpdateAfter(typeof(DefaultAgentsSystemGroup))]
public class LateAgentsSystemGroup : GameSystemGroup
{
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class WorldAgentsState
{
public float SpawnTickTimer { get; set; }
public bool SpawnTickNow { get; set; }
}
}

View File

@@ -0,0 +1,37 @@
using Unity.Collections;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BlockMap : NativeGrid<BlockReason>
{
public BlockMap(int2 size) : base(size, Allocator.Persistent)
{
}
public void AddReason(int2 tile, BlockReason reason)
{
SetValue(tile, GetValue(tile) | reason);
}
public void AddReason(int2 min, int2 max, BlockReason reason)
{
foreach (var tile in TileRange.From(min, max)) AddReason(tile, reason);
}
public void RemoveReason(int2 tile, BlockReason reason)
{
SetValue(tile, GetValue(tile) & ~reason);
}
public void RemoveReason(int2 min, int2 max, BlockReason reason)
{
foreach (var tile in TileRange.From(min, max)) RemoveReason(tile, reason);
}
public bool IsBlocked(int2 tile, BlockReason reason = BlockReason.Any)
{
return (GetValue(tile) & reason) != 0;
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BlockMapUpdateSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private World _world;
[InjectService]
private GameConfig _config;
public BlockMapUpdateSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<BuildingPlacementAnimationStartedSignal>(OnBuildingPlacementAnimationStarted);
_signalBus.Subscribe<BuildingCreatedSignal>(OnBuildingCreated);
_signalBus.Subscribe<BuildingDeleteAnimationCompletedSignal>(OnBuildingDeleteAnimationCompleted);
_signalBus.Subscribe<ConstructionSiteDeletedSignal>(OnConstructionSiteDeleted);
_signalBus.Subscribe<RoadTileUpdatedSignal>(OnRoadTileUpdated);
_signalBus.Subscribe<RawResourcesRemovedSignal>(OnRawResourcesRemoved);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<BuildingPlacementAnimationStartedSignal>(OnBuildingPlacementAnimationStarted);
_signalBus.Unsubscribe<BuildingCreatedSignal>(OnBuildingCreated);
_signalBus.Unsubscribe<BuildingDeleteAnimationCompletedSignal>(OnBuildingDeleteAnimationCompleted);
_signalBus.Unsubscribe<ConstructionSiteDeletedSignal>(OnConstructionSiteDeleted);
_signalBus.Unsubscribe<RoadTileUpdatedSignal>(OnRoadTileUpdated);
_signalBus.Unsubscribe<RawResourcesRemovedSignal>(OnRawResourcesRemoved);
}
private void OnBuildingPlacementAnimationStarted(BuildingPlacementAnimationStartedSignal signal)
{
_world.BlockMap.AddReason(signal.Rect.Min, signal.Rect.Max, BlockReason.Building);
}
private void OnBuildingCreated(BuildingCreatedSignal signal)
{
var rect = signal.Building.Rect;
_world.BlockMap.AddReason(rect.Min, rect.Max, BlockReason.Building);
}
private void OnBuildingDeleteAnimationCompleted(BuildingDeleteAnimationCompletedSignal signal)
{
var rect = signal.Building.Rect;
_world.BlockMap.RemoveReason(rect.Min, rect.Max, BlockReason.Building);
}
private void OnConstructionSiteDeleted(ConstructionSiteDeletedSignal signal)
{
var rect = signal.ConstructionSite.Rect;
_world.BlockMap.RemoveReason(rect.Min, rect.Max, BlockReason.Building);
}
private void OnRoadTileUpdated(RoadTileUpdatedSignal signal)
{
if (_world.RoadNetwork.IsRoadTile(signal.Tile))
_world.BlockMap.AddReason(signal.Tile, BlockReason.Road);
else
_world.BlockMap.RemoveReason(signal.Tile, BlockReason.Road);
}
private void OnRawResourcesRemoved(RawResourcesRemovedSignal signal)
{
foreach (var resourceNode in signal.ResourceNodes) _world.BlockMap.RemoveReason(resourceNode.Tile, BlockReason.RawResource);
}
}
}

View File

@@ -0,0 +1,32 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Flags]
public enum BlockReason
{
None,
InvalidElevation = 1 << 0,
RawResource = 1 << 1,
ProductStack = 1 << 2,
Building = 1 << 3,
Road = 1 << 4,
CannotBuild = InvalidElevation | Building | Road,
CannotBuildRoad = InvalidElevation | Building,
ClearGrassMask = ProductStack | Building | Road,
CannotMakeCamp = RawResource | ProductStack | CannotBuild,
BlocksAgents = InvalidElevation | Building,
Any = ~None
}
}

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

Some files were not shown because too many files have changed in this diff Show More