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,126 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.InputSystem;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildTool : EditTool
{
private IPointerService _pointerService;
private ITileSpace _tileSpace;
private IEditToolValidatorService _validator;
private IBuildToolPreviewManager _previewManager;
private Directions _buildingOrientation;
private EditToolValidationResult _validationResult;
public BuildTool(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public BuildingDefinition Building { get; set; }
public BuildToolPreview Preview { get; private set; }
public TileRect BuildingRect { get; private set; }
public override UniTask InitializeAsync()
{
var config = ServiceLocator.GetService<GameConfig>();
_pointerService = ServiceLocator.GetService<IPointerService>();
_tileSpace = ServiceLocator.GetService<ITileSpace>();
_validator = ServiceLocator.GetService<IEditToolValidatorService>();
_previewManager = ServiceLocator.GetService<IBuildToolPreviewManager>();
Preview = new BuildToolPreview(config.UI.BuildTool, _tileSpace);
return UniTask.CompletedTask;
}
public override void OnEnabled()
{
base.OnEnabled();
if (!Building)
{
Debug.LogError("Building not set when activating Build Tool");
return;
}
Preview.PrepareForBuilding(Building);
_buildingOrientation = Directions.North;
}
public override void OnDisabled()
{
base.OnDisabled();
Preview.Release();
}
public override void Update()
{
base.Update();
var pointerOnTerrain = _pointerService.TryGetPositionOnTerrain(out var position);
var keyboard = Keyboard.current;
if (keyboard.tKey.wasPressedThisFrame) _buildingOrientation = (Directions)(((int)_buildingOrientation + 6) % 8);
var buildingCenter = _tileSpace.WorldToTile(position);
BuildingRect = TileMath.GetBuildingRect(buildingCenter, _buildingOrientation, Building.Width, Building.Height);
_validationResult = ValidatePlacement(pointerOnTerrain);
var isValid = _validationResult == EditToolValidationResult.Success;
AffectedTiles.Clear();
if (pointerOnTerrain)
{
var highlightType = isValid ? TileHighlightType.ValidTile : TileHighlightType.InvalidTile;
foreach (var tile in TileRange.From(BuildingRect)) AffectedTiles.Add((tile, highlightType));
}
Preview.Update(isValid, position, _buildingOrientation);
if (isValid && _pointerService.TryConsumeLeftClick()) Build();
}
private EditToolValidationResult ValidatePlacement(bool pointerOnTerrain)
{
if (!pointerOnTerrain) return EditToolValidationResult.BlockedTile;
var commonValidation = _validator.DoCommonValidation(BuildingRect, BlockReason.CannotBuild);
if (commonValidation != EditToolValidationResult.Success) return commonValidation;
return _validator.ValidateBuildingPlacementRules(BuildingRect, Building);
}
public EditToolValidationResult GetLastValidationResult()
{
return _validationResult;
}
private void Build()
{
_previewManager.PlayPlacementAnimationAndBuild(Preview, Building, BuildingRect, _buildingOrientation);
}
public override void GetDeleteGameObjectsPreviewInfo(out DeletedGameObjectsFilter filter, out TileRect rect)
{
if (_validationResult != EditToolValidationResult.Success)
{
filter = DeletedGameObjectsFilter.None;
rect = TileRect.Empty;
return;
}
filter = DeletedGameObjectsFilter.RawResources | DeletedGameObjectsFilter.ProductStacks;
rect = BuildingRect;
}
}
}

View File

@@ -0,0 +1,126 @@
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Pool;
using Object = UnityEngine.Object;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildToolPreview
{
private readonly GameConfig.UIConfig.BuildToolConfig _config;
private readonly ITileSpace _tileSpace;
private BuildingDefinition _building;
private Vector3 _tilt;
private Vector3 _tiltVelocity;
private float _yaw;
private float _yawVelocity;
public BuildToolPreview(GameConfig.UIConfig.BuildToolConfig config, ITileSpace tileSpace)
{
_config = config;
_tileSpace = tileSpace;
}
public GameObject PreviewObject { get; private set; }
public bool IsVisible => PreviewObject && PreviewObject.activeSelf;
public float3 Position => PreviewObject ? PreviewObject.transform.position : Vector3.zero;
public void PrepareForBuilding(BuildingDefinition building)
{
_building = building;
}
public void Release()
{
ClearPreviewObject();
}
public void Update(bool isValid, Vector3 pointer, Directions orientation)
{
if (!PreviewObject)
{
if (_building.Visualization.IsDone)
CreatePreviewObject();
else
return;
}
var snap = !PreviewObject.activeSelf && isValid;
if (snap)
{
_tilt = Vector3.zero;
_tiltVelocity = Vector3.zero;
}
PreviewObject.SetActive(isValid);
if (!isValid) return;
var rect = TileMath.GetBuildingRect(_tileSpace.WorldToTile(pointer), orientation, _building.Width, _building.Height);
var position = _tileSpace.GetRectWorldCenter(rect);
position.y += _config.Height;
var dt = Time.unscaledDeltaTime;
position = snap ? position : Vector3.Lerp(PreviewObject.transform.position, position, _config.LerpFactor * dt);
var yaw = orientation.ToQuaternion().eulerAngles.y;
_yaw = snap ? yaw : Mathf.SmoothDampAngle(_yaw, yaw, ref _yawVelocity, 0.1f, Mathf.Infinity, dt);
var rotation = Quaternion.Euler(0, _yaw, 0);
if (!snap) rotation = SimulateSpring(position, dt) * rotation;
PreviewObject.transform.SetPositionAndRotation(position, rotation);
}
private Quaternion SimulateSpring(Vector3 targetPosition, float dt)
{
var positionDelta = targetPosition - PreviewObject.transform.position;
_tilt -= positionDelta * _config.ImpulseScale;
_tilt = _tilt.normalized * Mathf.Min(_tilt.magnitude, _config.MaxTilt);
_tiltVelocity -= _tilt * (_config.Elasticity * dt);
_tiltVelocity *= Mathf.Pow(1 - _config.Damping, dt);
_tilt += _tiltVelocity * dt;
if (_tilt.sqrMagnitude > 0.001f)
{
var axis = Vector3.Cross(Vector3.down, _tilt.normalized);
var angle = _tilt.magnitude / _config.MaxTilt * _config.MaxTiltAngle;
return Quaternion.AngleAxis(angle, axis);
}
return Quaternion.identity;
}
private void CreatePreviewObject()
{
PreviewObject = Object.Instantiate((GameObject)_building.Visualization.Asset);
PreviewObject.SetLayerRecursively<Renderer>(GameObjectLayers.IgnoreAoE);
using var _ = ListPool<HideOnBuildingPreview>.Get(out var hide);
PreviewObject.GetComponentsInChildren(hide);
foreach (var obj in hide) obj.gameObject.SetActive(false);
// Start the preview in the disabled state, causes the position to be snapped when enabled
PreviewObject.SetActive(false);
}
public void ClearPreviewObject()
{
if (PreviewObject)
{
Object.Destroy(PreviewObject);
PreviewObject = null;
}
}
}
}

View File

@@ -0,0 +1,52 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IBuildToolPreviewManager))]
public class BuildToolPreviewManager : GameSystem, IBuildToolPreviewManager
{
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private ISignalBus _signalBus;
public BuildToolPreviewManager(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void PlayPlacementAnimationAndBuild(BuildToolPreview preview, BuildingDefinition definition, TileRect rect, Directions orientation)
{
_ = PlayPlacementAnimationAndBuildAsync(preview, definition, rect, orientation);
}
private async UniTask PlayPlacementAnimationAndBuildAsync(BuildToolPreview preview, BuildingDefinition definition, TileRect rect, Directions orientation)
{
_signalBus.Raise(new BuildingPlacementAnimationStartedSignal(definition, rect, orientation));
var animatedObject = Object.Instantiate(preview.PreviewObject);
preview.ClearPreviewObject();
var placementAnimation = animatedObject.GetComponent<BuildingPlacementAnimation>();
if (placementAnimation)
{
var finalPosition = _tileSpace.GetRectWorldCenter(rect);
var finalRotation = orientation.ToQuaternion();
await placementAnimation.PlayAsync(definition, animatedObject.transform.position, finalPosition, finalRotation);
}
else
{
Debug.LogError($"Prefab '{definition.name}' has no {nameof(BuildingPlacementAnimation)} component");
}
_signalBus.Raise(new BuildingPlacementAnimationCompletedSignal(definition, rect, orientation));
await UniTask.NextFrame();
Object.Destroy(animatedObject);
}
}
}

View File

@@ -0,0 +1,30 @@
using System.Collections.Generic;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class BuildToolValidator
{
private readonly World _world;
private readonly int _baseElevation;
public BuildToolValidator(World world, int baseElevation)
{
_world = world;
_baseElevation = baseElevation;
}
public bool ValidatePlacement(List<int2> affectedTiles)
{
foreach (var tile in affectedTiles)
{
if (_world.Heightmap.GetValue(tile) != _baseElevation) return false;
if (_world.BlockMap.IsBlocked(tile)) return false;
}
return true;
}
}
}

View File

@@ -0,0 +1,191 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
[UpdateAfter(typeof(EditingStateGameSystem))]
public class GameObjectsHighlightingSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private GameConfig _gameConfig;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private EditingState _editingState;
[InjectService]
private UIState _uiState;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private IBuildingSpatialQuery _buildingSpatialQuery;
[InjectService]
private IProductStackVisualizationCollection _productStackVisualizationCollection;
[InjectService]
private IRawResourceVisualizationCollection _rawResourceVisualizationCollection;
[InjectService]
private IAgentVisualizationCollection _agentVisualizationCollection;
[InjectService]
private IBuildingVisualizationCollection _buildingVisualizationCollection;
[InjectService]
private World _world;
public GameObjectsHighlightingSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<CollectHighlightedGameObjectsSignal>(OnCollectHighlightedGameObjects);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<CollectHighlightedGameObjectsSignal>(OnCollectHighlightedGameObjects);
}
private bool TryGetSourceBuilding(out BuildingDefinition building, out TileRect sourceRect)
{
var tool = _editingState.ActiveTool;
if (tool is BuildTool buildTool && buildTool.Preview.IsVisible)
{
building = buildTool.Building;
sourceRect = buildTool.BuildingRect;
return true;
}
var selectedBuilding = _uiState.SelectedBuilding;
if (selectedBuilding != null)
{
building = selectedBuilding.Definition;
sourceRect = selectedBuilding.Rect;
return true;
}
building = null;
sourceRect = TileRect.Empty;
return false;
}
private void OnCollectHighlightedGameObjects(CollectHighlightedGameObjectsSignal signal)
{
if (!TryGetSourceBuilding(out var building, out var sourceRect)) return;
CollectBuildings(signal.GameObjects, building, sourceRect);
CollectProductStacks(signal.GameObjects, building, sourceRect);
CollectResources(signal.GameObjects, building, sourceRect);
CollectCritters(signal.GameObjects, building, sourceRect);
}
private void CollectBuildings(List<GameObject> gameObjects, BuildingDefinition sourceBuilding, TileRect sourceRect)
{
if (sourceBuilding.ProvidedProducts.Count > 0)
{
foreach (var house in _entityCache.GetHouses())
{
if (TileMath.StepCount(sourceRect, house.Rect) > sourceBuilding.Range) continue;
AddHighlightedBuilding(gameObjects, house);
}
if (sourceBuilding.FetchesProducts)
foreach (var storage in _entityCache.GetStorageBuildings())
{
if (TileMath.StepCount(sourceRect, storage.Rect) > sourceBuilding.Range) continue;
AddHighlightedBuilding(gameObjects, storage);
}
}
if (sourceBuilding.IsHouse)
{
using var providersScope = ListPool<Building>.Get(out var providers);
_buildingSpatialQuery.FindProvidersForHouse(sourceRect, providers);
foreach (var provider in providers) AddHighlightedBuilding(gameObjects, provider);
}
if (sourceBuilding.IsStorage)
{
using var providersScope = ListPool<Building>.Get(out var providers);
_buildingSpatialQuery.FindProvidersForStorage(sourceRect, providers);
foreach (var provider in providers) AddHighlightedBuilding(gameObjects, provider);
}
}
private void AddHighlightedBuilding(List<GameObject> gameObjects, Building building)
{
if (_buildingVisualizationCollection.TryGetVisualization(building.Id, out var visualization)) gameObjects.Add(visualization.gameObject);
}
private void CollectProductStacks(List<GameObject> gameObjects, BuildingDefinition sourceBuilding, TileRect sourceRect)
{
if (!sourceBuilding.IsStorage) return;
var rangeRect = sourceRect.Inflate(sourceBuilding.Range);
using var productStacksScope = ListPool<ProductStack>.Get(out var productStacks);
_world.ProductStacks.Find(rangeRect, productStacks);
foreach (var productStack in productStacks)
{
if (!_productStackVisualizationCollection.TryGetVisualization(productStack.Id, out var visualization)) continue;
gameObjects.Add(visualization);
}
}
private void CollectResources(List<GameObject> gameObjects, BuildingDefinition sourceBuilding, TileRect sourceRect)
{
if (!sourceBuilding.HarvestedResource) return;
var rangeRect = sourceRect.Inflate(sourceBuilding.Range);
using var resourceIdsScope = ListPool<(int, bool)>.Get(out var resourceIds);
_world.RawResources.GetResourceNodes(rangeRect, _gameConfig.GeneralSettings.BaseElevation, resourceIds);
foreach (var (id, _) in resourceIds)
{
_world.RawResources.TryGetResourceNode(id, out var resourceNode);
if (resourceNode.DefinitionId != sourceBuilding.HarvestedResource.RuntimeId) continue;
if (!_rawResourceVisualizationCollection.TryGetVisualization(resourceNode.Id, out var visualization)) continue;
gameObjects.Add(visualization);
}
}
private void CollectCritters(List<GameObject> gameObjects, BuildingDefinition sourceBuilding, TileRect sourceRect)
{
if (!sourceBuilding.TargetCritter) return;
var rangeRect = sourceRect.Inflate(sourceBuilding.Range);
foreach (var critter in _entityCache.GetCritterAgents())
{
ref var critterState = ref critter.GetCritterStateRW();
if (critterState.CritterDefinitionId != sourceBuilding.TargetCritter.RuntimeId) continue;
var critterTile = _tileSpace.WorldToTile(critter.Position);
if (!rangeRect.Contains(critterTile)) continue;
if (!_agentVisualizationCollection.TryGetVisualization(critter.Id, out var visualization)) continue;
gameObjects.Add(visualization.gameObject);
}
}
}
}

View File

@@ -0,0 +1,8 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class HideOnBuildingPreview : MonoBehaviour
{
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IBuildToolPreviewManager
{
void PlayPlacementAnimationAndBuild(BuildToolPreview preview, BuildingDefinition definition, TileRect rect, Directions orientation);
}
}