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,9 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IDestroyRawResourceAnimation
{
UniTask PlayAsync();
}
}

View File

@@ -0,0 +1,9 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IRawResourceVisualizationCollection
{
bool TryGetVisualization(int id, out GameObject visualization);
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct RawResourceHarvestedSignal
{
public int RawResourceId;
public RawResourceHarvestedSignal(int rawResourceId)
{
RawResourceId = rawResourceId;
}
}
}

View File

@@ -0,0 +1,128 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Pool;
using Object = UnityEngine.Object;
using Random = UnityEngine.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IRawResourceVisualizationCollection))]
public class RawResourceRenderingSystem : GameSystem, IInitializable, IDisposable, IRawResourceVisualizationCollection, IOnWorldGenerationCompletedCallback
{
private const int MaxCreatedPerUpdate = 16;
[InjectService]
private IScene _scene;
[InjectService]
private IGameDatabase _gameDatabase;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private World _world;
[InjectService]
private GameConfig _config;
private Dictionary<int, GameObject> _visualizationsById = new();
public RawResourceRenderingSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<WorldGenerationCompletedSignal>(OnWorldGenerationCompleted);
_signalBus.Subscribe<CollectDeletedGameObjectsSignal>(OnCollectDeletedGameObjects);
_signalBus.Subscribe<RawResourcesRemovedSignal>(OnRawResourcesRemoved);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<WorldGenerationCompletedSignal>(OnWorldGenerationCompleted);
_signalBus.Unsubscribe<CollectDeletedGameObjectsSignal>(OnCollectDeletedGameObjects);
_signalBus.Unsubscribe<RawResourcesRemovedSignal>(OnRawResourcesRemoved);
}
private void OnWorldGenerationCompleted(WorldGenerationCompletedSignal signal)
{
signal.Callbacks.Add(this);
}
public async UniTask OnWorldGenerationCompletedAsync(World world)
{
await CreateVisualizationsAsync();
}
private async UniTask CreateVisualizationsAsync()
{
var visualizationCount = 0;
foreach (var resourceNode in _world.RawResources)
{
var definition = _gameDatabase.WithId<ResourceNodeDefinition>(resourceNode.DefinitionId);
var rotation = definition.RandomRotation ? Quaternion.Euler(0, Random.Range(0, 360), 0) : resourceNode.Orientation.ToQuaternion();
var scale = Vector3.one * Random.Range(definition.ScaleRange.x, definition.ScaleRange.y);
GameObject prefab;
if (definition.Prefab.IsValid())
prefab = (GameObject)definition.Prefab.Asset;
else
prefab = await definition.Prefab.LoadAssetAsync<GameObject>();
var visualization = Object.Instantiate(prefab, _scene.SceneFolders.RawResources);
visualization.transform.SetPositionAndRotation(resourceNode.Position, rotation);
visualization.transform.localScale = Vector3.Scale(visualization.transform.localScale, scale);
_visualizationsById.Add(resourceNode.Id, visualization);
if (++visualizationCount % MaxCreatedPerUpdate == 0) await UniTask.NextFrame();
}
}
private void OnCollectDeletedGameObjects(CollectDeletedGameObjectsSignal signal)
{
if ((signal.Filter & DeletedGameObjectsFilter.RawResources) == 0) return;
using var resourceNodesScope = ListPool<(int, bool)>.Get(out var resourceNodeIds);
_world.RawResources.GetResourceNodes(signal.Rect, _config.GeneralSettings.BaseElevation, resourceNodeIds);
foreach (var (id, canBeDeleted) in resourceNodeIds)
{
if (!canBeDeleted) continue;
var visualization = _visualizationsById[id];
signal.GameObjects.Add(visualization.gameObject);
}
}
private void OnRawResourcesRemoved(RawResourcesRemovedSignal signal)
{
foreach (var resourceNode in signal.ResourceNodes)
{
if (!_visualizationsById.Remove(resourceNode.Id, out var visualization)) return;
_ = DestroyVisualizationAsync(visualization);
}
}
private async UniTask DestroyVisualizationAsync(GameObject visualization)
{
if (visualization.TryGetComponent<IDestroyRawResourceAnimation>(out var animation)) await animation.PlayAsync();
Object.Destroy(visualization);
}
public bool TryGetVisualization(int id, out GameObject visualization)
{
return _visualizationsById.TryGetValue(id, out visualization);
}
}
}

View File

@@ -0,0 +1,63 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class RawResourcesRemovalSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private World _world;
[InjectService]
private GameConfig _config;
public RawResourcesRemovalSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<BuildingPlacementAnimationStartedSignal>(OnBuildingPlacementAnimationStarted);
_signalBus.Subscribe<RoadTileUpdatedSignal>(OnRoadTileUpdated);
_signalBus.Subscribe<DoDeleteToolSignal>(OnDoDeleteTool);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<BuildingPlacementAnimationStartedSignal>(OnBuildingPlacementAnimationStarted);
_signalBus.Unsubscribe<RoadTileUpdatedSignal>(OnRoadTileUpdated);
_signalBus.Unsubscribe<DoDeleteToolSignal>(OnDoDeleteTool);
}
private void OnBuildingPlacementAnimationStarted(BuildingPlacementAnimationStartedSignal signal)
{
RemoveResourceNodesAt(signal.Rect);
}
private void OnRoadTileUpdated(RoadTileUpdatedSignal signal)
{
RemoveResourceNodesAt(TileRect.OneTile(signal.Tile));
}
private void OnDoDeleteTool(DoDeleteToolSignal signal)
{
RemoveResourceNodesAt(signal.Rect);
}
private void RemoveResourceNodesAt(TileRect rect)
{
using var _ = ListPool<ResourceNode>.Get(out var deletedResourceNodes);
_world.RawResources.DeleteResourceNodes(rect, _config.GeneralSettings.BaseElevation, deletedResourceNodes);
if (deletedResourceNodes.Count == 0) return;
_signalBus.Raise(new RawResourcesRemovedSignal(deletedResourceNodes, RawResourcesRemovedSignal.RemovalReason.PlayerAction));
}
}
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct RawResourcesRemovedSignal
{
public List<ResourceNode> ResourceNodes;
public RemovalReason Reason;
public RawResourcesRemovedSignal(List<ResourceNode> resourceNodes, RemovalReason reason)
{
ResourceNodes = resourceNodes;
Reason = reason;
}
public enum RemovalReason
{
PlayerAction,
Harvested
}
}
}

View File

@@ -0,0 +1,90 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class RawResourcesState : IEnumerable<ResourceNode>
{
private const int SpatialLookupCellSize = 5;
private Dictionary<int, ResourceNode> _resourceNodesById = new();
private Dictionary<int, SpatialLookup<(int, bool)>> _spatialLookups = new();
public int Count => _resourceNodesById.Count;
public void AddResourceNode(in ResourceNode node)
{
_resourceNodesById.Add(node.Id, node);
GetLookup(node.Elevation).Add((node.Id, node.CanBeDeleted), TileRect.OneTile(node.Tile), node.Id);
}
public bool Exists(int id)
{
return _resourceNodesById.ContainsKey(id);
}
public bool TryGetResourceNode(int id, out ResourceNode node)
{
return _resourceNodesById.TryGetValue(id, out node);
}
public void GetResourceNodes(TileRect rect, int elevation, List<(int, bool)> resourceNodeIds)
{
GetLookup(elevation).Find(rect, resourceNodeIds);
}
public bool DeleteResourceNode(int id, int elevation, out ResourceNode resourceNode)
{
var result = _resourceNodesById.Remove(id, out resourceNode);
if (result) GetLookup(elevation).Remove(TileRect.OneTile(resourceNode.Tile), id);
return result;
}
public void DeleteResourceNodes(TileRect rect, int elevation, List<ResourceNode> deletedResourceNodes = null)
{
using var removeIdsScope = ListPool<(int, bool)>.Get(out var removeIds);
GetLookup(elevation)
.RemoveAll(
rect,
value =>
{
var (_, canBeDeleted) = value;
return canBeDeleted;
},
removeIds);
foreach (var (id, canBeDeleted) in removeIds)
{
_resourceNodesById.Remove(id, out var resourceNode);
deletedResourceNodes?.Add(resourceNode);
}
}
private SpatialLookup<(int, bool)> GetLookup(int elevation)
{
if (!_spatialLookups.TryGetValue(elevation, out var lookup))
{
lookup = new SpatialLookup<(int, bool)>(SpatialLookupCellSize);
_spatialLookups.Add(elevation, lookup);
}
return lookup;
}
public Dictionary<int, ResourceNode>.ValueCollection.Enumerator GetEnumerator()
{
return _resourceNodesById.Values.GetEnumerator();
}
IEnumerator<ResourceNode> IEnumerable<ResourceNode>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@@ -0,0 +1,21 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct ResourceNode
{
public int Id;
public int DefinitionId;
public float3 Position;
public int2 Tile;
public Directions Orientation;
public int Elevation;
public bool CanBeDeleted;
}
}

View File

@@ -0,0 +1,37 @@
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Audio;
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "ResourceNodeDefinition", menuName = "Riversong Code Showcase/Resource Node Definition")]
public class ResourceNodeDefinition : GameDataAsset
{
public AssetReferenceT<GameObject> Prefab;
public AudioResource DeleteAudio;
public bool RandomRotation = true;
public Vector2 ScaleRange = new(1, 1);
[TitleGroup("Harvesting")]
[LabelText("Agent")]
public AgentDefinition HarvesterAgent;
public ProductDefinition Product;
[HorizontalGroup("Dropped Amount")]
[LabelText("Dropped Min")]
public int MinDroppedAmount = 10;
[HorizontalGroup("Dropped Amount")]
[LabelText("Dropped Max")]
public int MaxDroppedAmount = 15;
public AgentAnimation HarvestAnimation;
public float HarvestPositionOffset = 0.5f;
}
}

View File

@@ -0,0 +1,59 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Pool;
using Random = UnityEngine.Random;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class ResourceNodeHarvestingSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private GameConfig _config;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IProductStackFactory _productStackFactory;
[InjectService]
private World _world;
[InjectService]
private IGameDatabase _gameDatabase;
public ResourceNodeHarvestingSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<RawResourceHarvestedSignal>(OnRawResourceHarvested);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<RawResourceHarvestedSignal>(OnRawResourceHarvested);
}
private void OnRawResourceHarvested(RawResourceHarvestedSignal signal)
{
if (!_world.RawResources.DeleteResourceNode(signal.RawResourceId, _config.GeneralSettings.BaseElevation, out var resourceNode))
{
Debug.LogError("Could not find harvested Resource Node");
return;
}
var definition = _gameDatabase.WithId<ResourceNodeDefinition>(resourceNode.DefinitionId);
var amount = Random.Range(definition.MinDroppedAmount, definition.MaxDroppedAmount + 1);
_productStackFactory.CreateOrMerge(resourceNode.Tile, definition.Product, amount);
using var resourceNodesScope = ListPool<ResourceNode>.Get(out var resourceNodes);
resourceNodes.Add(resourceNode);
_signalBus.Raise(new RawResourcesRemovedSignal(resourceNodes, RawResourcesRemovedSignal.RemovalReason.Harvested));
}
}
}

View File

@@ -0,0 +1,71 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TreeFallAnimation : MonoBehaviour, IDestroyRawResourceAnimation
{
[SerializeField]
private float _fallDelay;
[SerializeField]
private float _fallDuration = 1;
[SerializeField]
private AnimationCurve _fallRotationCurve = AnimationCurve.Linear(0, 0, 1, 1);
[SerializeField]
private float _sinkDelay = 0.5f;
[SerializeField]
private float _sinkDepth = 1;
[SerializeField]
private float _sinkSpeed = 1;
public async UniTask PlayAsync()
{
await FallAsync();
await SinkAsync();
}
private async UniTask FallAsync()
{
await UniTask.WaitForSeconds(_fallDelay);
var axis = Quaternion.Euler(0, Random.value * 360, 0) * Vector3.forward;
var initialRotation = transform.rotation;
var finalRotation = initialRotation * Quaternion.AngleAxis(90, axis);
var t = 0f;
while (t < 1)
{
t += Time.deltaTime / _fallDuration;
transform.rotation = Quaternion.Lerp(initialRotation, finalRotation, _fallRotationCurve.Evaluate(t));
await UniTask.NextFrame();
}
transform.rotation = finalRotation;
}
private async UniTask SinkAsync()
{
await UniTask.WaitForSeconds(_sinkDelay);
var initialY = transform.position.y;
var finalY = initialY - _sinkDepth;
while (transform.position.y > finalY)
{
transform.position += Vector3.down * _sinkSpeed * Time.deltaTime;
await UniTask.NextFrame();
}
transform.position = new Vector3(transform.position.x, finalY, transform.position.z);
}
}
}