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,27 @@
using Cysharp.Threading.Tasks;
using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(CommonServicesSystemGroup))]
[InitializeAfter(typeof(CommonServicesSystem))]
public class AnalyticsInitializationSystem : GameSystem, IServiceProvider, IInitializable
{
private IAnalyticsService _analyticsService;
public AnalyticsInitializationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void RegisterServices(IServiceLocator serviceLocator)
{
_analyticsService = AnalyticsServiceFactory.Create();
serviceLocator.RegisterService(_analyticsService);
}
public async UniTask InitializeAsync()
{
await _analyticsService.InitializeAsync();
}
}
}

View File

@@ -0,0 +1,14 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class AnalyticsServiceFactory
{
public static IAnalyticsService Create()
{
#if UNITY_EDITOR && !ENABLE_EDITOR_ANALYTICS
return new NoOpAnalyticsService();
#else
return new UnityAnalyticsService();
#endif
}
}
}

View File

@@ -0,0 +1,86 @@
using System;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class AnalyticsSessionState
{
private SessionStatus _status;
public string SessionId { get; private set; }
public float StartRealtimeSeconds { get; private set; }
public int HeartbeatIndex { get; private set; }
public int TotalBuildingsPlaced { get; private set; }
public bool Started => _status != SessionStatus.NotStarted;
public bool DemoCompleted => _status == SessionStatus.DemoCompleted;
public void Begin(float realtimeSeconds)
{
_status = SessionStatus.Started;
SessionId = Guid.NewGuid().ToString("N");
StartRealtimeSeconds = realtimeSeconds;
HeartbeatIndex = 0;
TotalBuildingsPlaced = 0;
}
public bool TryRecordBuildingConstruction(BuildingDefinition definition)
{
if (!Started || !definition) return false;
TotalBuildingsPlaced++;
return true;
}
public bool CanRecordHouseUpgrade(Building building)
{
return Started && building.Definition.IsHouse;
}
public bool TryAdvanceHeartbeat(float realtimeSeconds, float heartbeatIntervalSeconds, out int heartbeatIndex, out float playtimeSeconds)
{
heartbeatIndex = 0;
playtimeSeconds = 0;
if (!Started || DemoCompleted) return false;
var nextHeartbeatTime = StartRealtimeSeconds + (HeartbeatIndex + 1) * heartbeatIntervalSeconds;
if (realtimeSeconds < nextHeartbeatTime) return false;
heartbeatIndex = ++HeartbeatIndex;
playtimeSeconds = GetPlaytimeSeconds(realtimeSeconds);
return true;
}
public bool TryMarkDemoCompleted()
{
if (!Started || DemoCompleted) return false;
_status = SessionStatus.DemoCompleted;
return true;
}
public float GetPlaytimeSeconds(float realtimeSeconds)
{
if (!Started) return 0;
return Mathf.Max(realtimeSeconds - StartRealtimeSeconds, 0);
}
private enum SessionStatus
{
NotStarted,
Started,
DemoCompleted
}
}
}

View File

@@ -0,0 +1,89 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(CommonServicesSystemGroup))]
[InitializeAfter(typeof(AnalyticsInitializationSystem))]
public class DemoAnalyticsSystem : GameSystem, IInitializable, IDisposable, IUpdatable
{
private const float HeartbeatIntervalSeconds = 300;
[InjectService]
private IAnalyticsService _analyticsService;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private World _world;
private AnalyticsSessionState _sessionState = new();
public DemoAnalyticsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<WorldReadySignal>(OnWorldReady);
_signalBus.Subscribe<BuildingCreatedSignal>(OnBuildingCreated);
_signalBus.Subscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
_signalBus.Subscribe<DemoCompletedSignal>(OnDemoCompleted);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<WorldReadySignal>(OnWorldReady);
_signalBus.Unsubscribe<BuildingCreatedSignal>(OnBuildingCreated);
_signalBus.Unsubscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
_signalBus.Unsubscribe<DemoCompletedSignal>(OnDemoCompleted);
}
public void Update()
{
var realtimeSeconds = Time.realtimeSinceStartup;
while (_sessionState.TryAdvanceHeartbeat(realtimeSeconds, HeartbeatIntervalSeconds, out var heartbeatIndex, out var playtimeSeconds))
_analyticsService.RecordHeartbeat(_sessionState.SessionId, heartbeatIndex, playtimeSeconds, _sessionState.TotalBuildingsPlaced, _world.PopulationState.Population);
}
private void OnWorldReady(WorldReadySignal signal)
{
_sessionState.Begin(Time.realtimeSinceStartup);
_analyticsService.RecordSessionStarted(_sessionState.SessionId);
}
private void OnBuildingCreated(BuildingCreatedSignal signal)
{
var definition = signal.Building?.Definition;
if (!_sessionState.TryRecordBuildingConstruction(definition)) return;
_analyticsService.RecordBuildingConstructionCompleted(_sessionState.SessionId, definition.name);
}
private void OnBuildingUpgraded(BuildingUpgradedSignal signal)
{
var building = signal.Building;
if (!_sessionState.CanRecordHouseUpgrade(building)) return;
_analyticsService.RecordHouseUpgraded(_sessionState.SessionId, building.Definition.name, building.TierIndex, building.TierIndex + 1);
}
private void OnDemoCompleted(DemoCompletedSignal signal)
{
if (!_sessionState.TryMarkDemoCompleted()) return;
var playtimeSeconds = _sessionState.GetPlaytimeSeconds(Time.realtimeSinceStartup);
_analyticsService.RecordDemoCompleted(
_sessionState.SessionId,
playtimeSeconds,
_world.TimeState.TotalWeeks,
_world.PopulationState.Population,
_world.PopulationState.Happiness);
}
}
}

View File

@@ -0,0 +1,21 @@
using Unity.Services.Analytics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public sealed class BuildingConstructionCompletedAnalyticsEvent : Event
{
public BuildingConstructionCompletedAnalyticsEvent() : base("building_construction_completed")
{
}
public string SessionId
{
set => SetParameter("session_id", value);
}
public string BuildingName
{
set => SetParameter("building_name", value);
}
}
}

View File

@@ -0,0 +1,36 @@
using Unity.Services.Analytics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public sealed class DemoCompletedAnalyticsEvent : Event
{
public DemoCompletedAnalyticsEvent() : base("demo_completed")
{
}
public string SessionId
{
set => SetParameter("session_id", value);
}
public float PlaytimeSeconds
{
set => SetParameter("playtime_seconds", value);
}
public int TotalWeeks
{
set => SetParameter("total_weeks", value);
}
public int Population
{
set => SetParameter("population", value);
}
public float Happiness
{
set => SetParameter("happiness", value);
}
}
}

View File

@@ -0,0 +1,31 @@
using Unity.Services.Analytics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public sealed class HouseUpgradedAnalyticsEvent : Event
{
public HouseUpgradedAnalyticsEvent() : base("house_upgraded")
{
}
public string SessionId
{
set => SetParameter("session_id", value);
}
public string BuildingName
{
set => SetParameter("building_name", value);
}
public int FromTier
{
set => SetParameter("from_tier", value);
}
public int ToTier
{
set => SetParameter("to_tier", value);
}
}
}

View File

@@ -0,0 +1,36 @@
using Unity.Services.Analytics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public sealed class SessionHeartbeatAnalyticsEvent : Event
{
public SessionHeartbeatAnalyticsEvent() : base("session_heartbeat")
{
}
public string SessionId
{
set => SetParameter("session_id", value);
}
public int HeartbeatIndex
{
set => SetParameter("heartbeat_index", value);
}
public float PlaytimeSeconds
{
set => SetParameter("playtime_seconds", value);
}
public int TotalBuildingsPlaced
{
set => SetParameter("total_buildings_placed", value);
}
public int Population
{
set => SetParameter("population", value);
}
}
}

View File

@@ -0,0 +1,16 @@
using Unity.Services.Analytics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public sealed class SessionStartedAnalyticsEvent : Event
{
public SessionStartedAnalyticsEvent() : base("session_started")
{
}
public string SessionId
{
set => SetParameter("session_id", value);
}
}
}

View File

@@ -0,0 +1,19 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IAnalyticsService
{
UniTask InitializeAsync();
void RecordSessionStarted(string sessionId);
void RecordBuildingConstructionCompleted(string sessionId, string buildingName);
void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier);
void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population);
void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness);
}
}

View File

@@ -0,0 +1,32 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class NoOpAnalyticsService : IAnalyticsService
{
public UniTask InitializeAsync()
{
return UniTask.CompletedTask;
}
public void RecordSessionStarted(string sessionId)
{
}
public void RecordBuildingConstructionCompleted(string sessionId, string buildingName)
{
}
public void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier)
{
}
public void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population)
{
}
public void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness)
{
}
}
}

View File

@@ -0,0 +1,101 @@
using System;
using Cysharp.Threading.Tasks;
using Unity.Services.Analytics;
using Unity.Services.Core;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class UnityAnalyticsService : IAnalyticsService
{
private bool _canRecord;
public async UniTask InitializeAsync()
{
if (_canRecord) return;
try
{
if (UnityServices.State == ServicesInitializationState.Uninitialized) await UnityServices.InitializeAsync();
AnalyticsService.Instance.StartDataCollection();
_canRecord = true;
}
catch (Exception exception)
{
Debug.LogError($"Failed to initialize analytics. Analytics will be disabled.\n{exception}");
}
}
public void RecordSessionStarted(string sessionId)
{
if (!_canRecord) return;
var analyticsEvent = new SessionStartedAnalyticsEvent { SessionId = sessionId };
AnalyticsService.Instance.RecordEvent(analyticsEvent);
}
public void RecordBuildingConstructionCompleted(string sessionId, string buildingName)
{
if (!_canRecord) return;
var analyticsEvent = new BuildingConstructionCompletedAnalyticsEvent
{
SessionId = sessionId,
BuildingName = buildingName
};
AnalyticsService.Instance.RecordEvent(analyticsEvent);
}
public void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier)
{
if (!_canRecord) return;
var analyticsEvent = new HouseUpgradedAnalyticsEvent
{
SessionId = sessionId,
BuildingName = buildingName,
FromTier = fromTier,
ToTier = toTier
};
AnalyticsService.Instance.RecordEvent(analyticsEvent);
}
public void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population)
{
if (!_canRecord) return;
var analyticsEvent = new SessionHeartbeatAnalyticsEvent
{
SessionId = sessionId,
HeartbeatIndex = heartbeatIndex,
PlaytimeSeconds = playtimeSeconds,
TotalBuildingsPlaced = totalBuildingsPlaced,
Population = population
};
AnalyticsService.Instance.RecordEvent(analyticsEvent);
}
public void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness)
{
if (!_canRecord) return;
var analyticsEvent = new DemoCompletedAnalyticsEvent
{
SessionId = sessionId,
PlaytimeSeconds = playtimeSeconds,
TotalWeeks = totalWeeks,
Population = population,
Happiness = Mathf.Clamp01(happiness)
};
AnalyticsService.Instance.RecordEvent(analyticsEvent);
AnalyticsService.Instance.Flush();
}
}
}

View File

@@ -0,0 +1,58 @@
using System;
using Cysharp.Threading.Tasks;
using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(CommonServicesSystemGroup))]
public class CommonServicesSystem : GameSystem, IServiceProvider, IInitializable, IDisposable
{
[InjectService]
private GameConfig _config;
[InjectService]
private World _world;
private SignalBus _signalBus;
private TileSpace _tileSpace;
private MaterialReplacementCache _materialReplacementCache;
public CommonServicesSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void RegisterServices(IServiceLocator serviceLocator)
{
serviceLocator.RegisterService<IEntityCollection>(new EntityCollection());
serviceLocator.RegisterService<IEntityCache>(new EntityCache());
_signalBus = new SignalBus();
serviceLocator.RegisterService<ISignalBus>(_signalBus);
_tileSpace = new TileSpace();
serviceLocator.RegisterService<ITileSpace>(_tileSpace);
serviceLocator.RegisterService<IPoolingService>(new PoolingService());
_materialReplacementCache = new MaterialReplacementCache();
serviceLocator.RegisterService(_materialReplacementCache);
}
public UniTask InitializeAsync()
{
_tileSpace.TileSize = _config.GeneralSettings.TileSize;
_tileSpace.World = _world;
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus?.Dispose();
_signalBus = null;
_materialReplacementCache = null;
}
}
}

View File

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

View File

@@ -0,0 +1,14 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public class Entity
{
public const int InvalidId = 0;
public int Id { get; private set; }
public static T Create<T>(int id) where T : Entity, new()
{
return new T { Id = id };
}
}
}

View File

@@ -0,0 +1,81 @@
using System;
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class EntityCache : IEntityCache
{
private Dictionary<int, Cache> _caches = new();
public void CreateCache<T>(int key, Predicate<T> filter) where T : Entity
{
if (!_caches.TryGetValue(key, out var cache))
{
cache = new Cache<T>();
_caches.Add(key, cache);
}
((Cache<T>)cache).Filter = filter;
}
public List<T> Get<T>(int key) where T : Entity
{
if (!_caches.TryGetValue(key, out var cache))
{
cache = new Cache<T>();
_caches.Add(key, cache);
}
return ((Cache<T>)cache).Entities;
}
public void OnAdded(Entity entity)
{
foreach (var cache in _caches.Values) cache.TryAdd(entity);
}
public void OnRemoved(Entity entity)
{
foreach (var cache in _caches.Values) cache.TryRemove(entity);
}
private abstract class Cache
{
public void TryAdd(Entity entity)
{
if (FilterEntity(entity)) Add(entity);
}
public void TryRemove(Entity entity)
{
if (FilterEntity(entity)) Remove(entity);
}
protected abstract bool FilterEntity(Entity entity);
protected abstract void Add(Entity entity);
protected abstract void Remove(Entity entity);
}
private class Cache<T> : Cache where T : Entity
{
public Predicate<T> Filter { get; set; }
public List<T> Entities { get; } = new();
protected override bool FilterEntity(Entity entity)
{
return entity is T typedEntity && Filter.Invoke(typedEntity);
}
protected override void Add(Entity entity)
{
Entities.Add((T)entity);
}
protected override void Remove(Entity entity)
{
Entities.Remove((T)entity);
}
}
}
}

View File

@@ -0,0 +1,67 @@
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class EntityCacheExtensions
{
public static List<Building> GetHarvesterBuildings(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.HarvesterBuildings);
}
public static List<Building> GetHunterBuildings(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.HunterBuildings);
}
public static List<Building> GetFarmBuildings(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.FarmBuildings);
}
public static List<Building> GetProducers(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.ProducerBuildings);
}
public static List<Building> GetProviders(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.ProviderBuildings);
}
public static List<Building> GetBuildingsWithWorkers(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.BuildingsWithWorkers);
}
public static List<Building> GetHouses(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.HouseBuildings);
}
public static List<Building> GetTentBuildings(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.TentBuildings);
}
public static List<Building> GetStorageBuildings(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.StorageBuildings);
}
public static List<Building> GetStorageRequestBuildings(this IEntityCache entityCache)
{
return entityCache.Get<Building>((int)EntityCacheKeys.StorageRequestBuildings);
}
public static List<Agent> GetHunterAgents(this IEntityCache entityCache)
{
return entityCache.Get<Agent>((int)EntityCacheKeys.HunterAgents);
}
public static List<Agent> GetCritterAgents(this IEntityCache entityCache)
{
return entityCache.Get<Agent>((int)EntityCacheKeys.CritterAgents);
}
}
}

View File

@@ -0,0 +1,39 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum EntityCacheKeys
{
Invalid,
#region Buildings
HarvesterBuildings,
HunterBuildings,
FarmBuildings,
ProducerBuildings,
ProviderBuildings,
BuildingsWithWorkers,
HouseBuildings,
StorageBuildings,
StorageRequestBuildings,
TentBuildings,
#endregion
#region Agents
HunterAgents,
CritterAgents
#endregion
}
}

View File

@@ -0,0 +1,71 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
[InitializeAfter(typeof(PreLoadAssetsSystem))]
public class EntityCacheSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private IEntityCollection _entityCollection;
[InjectService]
private IEntityCache _entityCache;
[InjectService]
private GameConfig _config;
public EntityCacheSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
CreateCaches();
foreach (var entity in _entityCollection.GetInternalEntityList(typeof(Entity))) _entityCache.OnAdded(entity);
var callbacks = _entityCollection.On<Entity>();
callbacks.Added += _entityCache.OnAdded;
callbacks.Removed += _entityCache.OnRemoved;
return UniTask.CompletedTask;
}
private void CreateCaches()
{
_entityCache.CreateCache<Building>((int)EntityCacheKeys.HarvesterBuildings, b => b.Definition.HarvestedResource);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.HunterBuildings, b => b.Definition.TargetCritter);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.FarmBuildings, b => b.Definition.IsFarm);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.ProducerBuildings, b => b.Definition.Recipe);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.ProviderBuildings, b => b.Definition.ProvidedProducts.Count > 0);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.BuildingsWithWorkers, b => b.Definition.WorkerCount > 0);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.HouseBuildings, b => b.Definition.IsHouse);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.StorageBuildings, b => b.Definition.IsStorage);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.StorageRequestBuildings, b => b.Definition.IsStorage);
_entityCache.CreateCache<Building>((int)EntityCacheKeys.TentBuildings, b => b.Definition == _config.Population.TentBuilding.Asset);
_entityCache.CreateCache<Agent>(
(int)EntityCacheKeys.HunterAgents,
a =>
{
ref var jobState = ref a.GetJobStateRW();
return jobState.Job == AgentJob.Hunter;
});
_entityCache.CreateCache<Agent>(
(int)EntityCacheKeys.CritterAgents,
a =>
{
ref var critterState = ref a.GetCritterStateRW();
return critterState.IsCritter;
});
}
public void Dispose()
{
var callbacks = _entityCollection.On<Entity>();
callbacks.Added -= _entityCache.OnAdded;
callbacks.Removed -= _entityCache.OnRemoved;
}
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IEntityCache
{
void CreateCache<T>(int key, Predicate<T> filter) where T : Entity;
List<T> Get<T>(int key) where T : Entity;
void OnAdded(Entity entity);
void OnRemoved(Entity entity);
}
}

View File

@@ -0,0 +1,90 @@
using System;
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class EntityCollection : IEntityCollection
{
private static readonly List<Entity> Empty = new();
private int _nextId = Entity.InvalidId + 1;
private Dictionary<int, Entity> _entitiesById = new();
private ListMultiDictionary<Type, Entity> _entitiesByType = new();
private Dictionary<Type, IEntityCollectionCallbacks> _callbacks = new();
public T Create<T>() where T : Entity, new()
{
return Entity.Create<T>(_nextId++);
}
public void Add(Entity entity)
{
_entitiesById.Add(entity.Id, entity);
OnAdded(entity.GetType(), entity);
}
private void OnAdded(Type type, Entity entity)
{
_entitiesByType.Add(type, entity);
if (_callbacks.TryGetValue(type, out var callbacks)) callbacks.OnAdded(entity);
if (type == typeof(Entity)) return;
OnAdded(type.BaseType, entity);
}
public Entity Remove(int id)
{
if (!_entitiesById.Remove(id, out var entity)) return null;
OnRemoved(entity.GetType(), entity);
return entity;
}
private void OnRemoved(Type type, Entity entity)
{
_entitiesByType.Remove(type, entity);
if (_callbacks.TryGetValue(type, out var callbacks)) callbacks.OnRemoved(entity);
if (type == typeof(Entity)) return;
OnRemoved(type.BaseType, entity);
}
public bool Exists(int id)
{
return _entitiesById.ContainsKey(id);
}
public Entity Get(int id)
{
return _entitiesById.TryGetValue(id, out var entity) ? entity : null;
}
public Type GetEntityType(int id)
{
return Get(id)?.GetType();
}
public List<Entity> GetInternalEntityList(Type type)
{
return _entitiesByType.TryGetValues(type, out var entityList) ? entityList : Empty;
}
public IEntityCollectionCallbacks<T> On<T>() where T : Entity
{
if (!_callbacks.TryGetValue(typeof(T), out var callbacks))
{
callbacks = new EntityCollectionCallbacks<T>();
_callbacks.Add(typeof(T), callbacks);
}
return (IEntityCollectionCallbacks<T>)callbacks;
}
}
}

View File

@@ -0,0 +1,21 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class EntityCollectionCallbacks<T> : IEntityCollectionCallbacks<T> where T : Entity
{
public event Action<T> Added;
public event Action<T> Removed;
public void OnAdded(Entity entity)
{
Added?.Invoke((T)entity);
}
public void OnRemoved(Entity entity)
{
Removed?.Invoke((T)entity);
}
}
}

View File

@@ -0,0 +1,23 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class EntityCollectionExtensions
{
public static T Get<T>(this IEntityCollection entityCollection, int id) where T : Entity
{
return (T)entityCollection.Get(id);
}
public static T CreateAndAdd<T>(this IEntityCollection entityCollection) where T : Entity, new()
{
var entity = entityCollection.Create<T>();
entityCollection.Add(entity);
return entity;
}
public static bool TryGet<T>(this IEntityCollection entityCollection, int id, out T entity) where T : Entity
{
entity = entityCollection.Get(id) as T;
return entity != null;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IEntity
{
int Id { get; }
}
}

View File

@@ -0,0 +1,24 @@
using System;
using System.Collections.Generic;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IEntityCollection
{
T Create<T>() where T : Entity, new();
void Add(Entity entity);
Entity Remove(int id);
bool Exists(int id);
Entity Get(int id);
Type GetEntityType(int id);
public List<Entity> GetInternalEntityList(Type type);
public IEntityCollectionCallbacks<T> On<T>() where T : Entity;
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IEntityCollectionCallbacks
{
public void OnAdded(Entity entity);
public void OnRemoved(Entity entity);
}
public interface IEntityCollectionCallbacks<out T> : IEntityCollectionCallbacks
{
event Action<T> Added;
event Action<T> Removed;
}
}

View File

@@ -0,0 +1,11 @@
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IPoolingService
{
void AddPool<T>(int key, IObjectPool<T> pool) where T : class;
IObjectPool<T> GetPool<T>(int key) where T : class;
}
}

View File

@@ -0,0 +1,9 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PooledObject : MonoBehaviour
{
public int PoolKey { get; set; }
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PoolingService : IPoolingService
{
private Dictionary<int, object> _poolLookup = new();
public void AddPool<T>(int key, IObjectPool<T> pool) where T : class
{
_poolLookup.Add(key, pool);
}
public IObjectPool<T> GetPool<T>(int key) where T : class
{
return _poolLookup.TryGetValue(key, out var pool) ? (IObjectPool<T>)pool : null;
}
}
}

View File

@@ -0,0 +1,88 @@
using UnityEngine;
using UnityEngine.Pool;
using Object = UnityEngine.Object;
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class PoolingServiceExtensions
{
public static IObjectPool<GameObject> GetOrCreatePool(this IPoolingService poolingService, GameObject prefab, Transform folder = null)
{
var poolKey = prefab.GetInstanceID();
var pool = poolingService.GetPool<GameObject>(poolKey);
if (pool != null) return pool;
pool = new ObjectPool<GameObject>(
() =>
{
var go = Object.Instantiate(prefab, folder);
OnCreate(go, poolKey);
return go;
},
OnGet,
go => OnRelease(go, folder));
poolingService.AddPool(poolKey, pool);
return pool;
}
public static IObjectPool<T> GetOrCreatePool<T>(this IPoolingService poolingService, T prefab, Transform folder = null) where T : Component
{
var poolKey = prefab.GetInstanceID();
var pool = poolingService.GetPool<T>(poolKey);
if (pool != null) return pool;
pool = new ObjectPool<T>(
() =>
{
var component = Object.Instantiate(prefab, folder);
OnCreate(component.gameObject, poolKey);
return component;
},
component => OnGet(component.gameObject),
component => OnRelease(component.gameObject, folder));
poolingService.AddPool(poolKey, pool);
return pool;
}
private static void OnCreate(GameObject go, int poolKey)
{
go.AddComponent<PooledObject>().PoolKey = poolKey;
go.SetActive(false);
}
private static void OnGet(GameObject go)
{
go.transform.SetParent(null);
go.SetActive(true);
}
private static void OnRelease(GameObject go, Transform folder)
{
go.SetActive(false);
go.transform.SetParent(folder);
}
public static T Get<T>(this IPoolingService poolingService, int poolKey) where T : class
{
return poolingService.GetPool<T>(poolKey).Get();
}
public static void Release(this IPoolingService poolingService, GameObject go)
{
var poolKey = go.GetComponent<PooledObject>().PoolKey;
poolingService.GetPool<GameObject>(poolKey).Release(go);
}
public static void Release<T>(this IPoolingService poolingService, T component) where T : Component
{
var poolKey = component.GetComponent<PooledObject>().PoolKey;
poolingService.GetPool<T>(poolKey).Release(component);
}
}
}

View File

@@ -0,0 +1,31 @@
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
[InitializeAfter(typeof(PreLoadAssetsSystem))]
public class PoolsInitializationSystem : GameSystem, IInitializable
{
[InjectService]
private IPoolingService _poolingService;
[InjectService]
private IGameDatabase _gameDatabase;
public PoolsInitializationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
foreach (var product in _gameDatabase.OfType<ProductDefinition>())
{
_poolingService.GetOrCreatePool((GameObject)product.ProductStackVisualization.Asset);
_poolingService.GetOrCreatePool((GameObject)product.CarriedVisualization.Asset);
}
return UniTask.CompletedTask;
}
}
}

View File

@@ -0,0 +1,21 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DefaultGameSystemGroup))]
[UpdateBefore(typeof(EditingStateGameSystem))]
[UpdateBefore(typeof(BuildingSelectionSystem))]
public class RestoreTemporaryMaterialsSystem : GameSystem, IUpdatable
{
[InjectService]
private MaterialReplacementCache _materialReplacementCache;
public RestoreTemporaryMaterialsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
_materialReplacementCache.RestoreMaterials();
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface ISignalBus
{
void Raise<T>(T signal);
void Subscribe<T>(Action<T> handler);
void Unsubscribe<T>(Action<T> handler);
}
}

View File

@@ -0,0 +1,31 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class SignalBus : ISignalBus, IDisposable
{
private readonly ListMultiDictionary<Type, Delegate> _subscribers = new();
public void Dispose()
{
_subscribers.Clear();
}
public void Raise<T>(T signal)
{
if (!_subscribers.TryGetValues(typeof(T), out var handlers)) return;
foreach (var handler in handlers) ((Action<T>)handler).Invoke(signal);
}
public void Subscribe<T>(Action<T> handler)
{
_subscribers.Add(typeof(T), handler);
}
public void Unsubscribe<T>(Action<T> handler)
{
_subscribers.Remove(typeof(T), handler);
}
}
}

View File

@@ -0,0 +1,27 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class DirectionVectors
{
public static readonly int2[] Directions4 =
{
new(1, 0),
new(-1, 0),
new(0, 1),
new(0, -1)
};
public static readonly int2[] Directions8 =
{
new(1, 0),
new(-1, 0),
new(0, 1),
new(0, -1),
new(1, 1),
new(1, -1),
new(-1, 1),
new(-1, -1)
};
}
}

View File

@@ -0,0 +1,134 @@
using System;
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum Directions
{
North,
NorthWest,
West,
SouthWest,
South,
SouthEast,
East,
NorthEast
}
public enum DirectionsMask4
{
None = 0,
North = 1 << 0,
West = 1 << 1,
South = 1 << 2,
East = 1 << 3
}
[Flags]
public enum DirectionsMask8
{
None = 0,
North = 1 << 0,
NorthWest = 1 << 1,
West = 1 << 2,
SouthWest = 1 << 3,
South = 1 << 4,
SouthEast = 1 << 5,
East = 1 << 6,
NorthEast = 1 << 7
}
public static class DirectionsExtensions
{
public static int2 ToVector(this Directions direction)
{
switch (direction)
{
case Directions.North:
return new int2(0, 1);
case Directions.NorthWest:
return new int2(-1, 1);
case Directions.West:
return new int2(-1, 0);
case Directions.SouthWest:
return new int2(-1, -1);
case Directions.South:
return new int2(0, -1);
case Directions.SouthEast:
return new int2(1, -1);
case Directions.East:
return new int2(1, 0);
case Directions.NorthEast:
return new int2(1, 1);
default:
throw new ArgumentOutOfRangeException();
}
}
public static Quaternion ToQuaternion(this Directions direction)
{
switch (direction)
{
case Directions.North:
return Quaternion.identity;
case Directions.NorthWest:
return Quaternion.LookRotation(new Vector3(-1, 0, 1));
case Directions.West:
return Quaternion.LookRotation(Vector3.left);
case Directions.SouthWest:
return Quaternion.LookRotation(new Vector3(-1, 0, -1));
case Directions.South:
return Quaternion.LookRotation(Vector3.back);
case Directions.SouthEast:
return Quaternion.LookRotation(new Vector3(1, 0, -1));
case Directions.East:
return Quaternion.LookRotation(Vector3.right);
case Directions.NorthEast:
return Quaternion.LookRotation(new Vector3(1, 0, 1));
default:
throw new ArgumentOutOfRangeException();
}
}
public static int2 Rotate(this Directions direction, int2 v)
{
return TileMath.Rotate(v, direction);
}
}
}

View File

@@ -0,0 +1,18 @@
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface ITileSpace
{
float TileSize { get; }
int2 WorldToTile(Vector3 position);
Vector3 TileToWorld(int2 tile, float tileX = 0.5f, float tileY = 0.5f);
int GetElevation(float y);
Vector3 GetRectWorldCenter(in TileRect rect);
}
}

View File

@@ -0,0 +1,136 @@
using System;
using System.Runtime.CompilerServices;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class TileMath
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int2 Rotate(int2 v, Directions direction)
{
switch (direction)
{
case Directions.North:
return new int2(v.x, v.y);
case Directions.West:
return new int2(v.y, -v.x);
case Directions.South:
return new int2(-v.x, -v.y);
case Directions.East:
return new int2(-v.y, v.x);
default:
throw new ArgumentOutOfRangeException();
}
}
public static TileRect GetBuildingRect(int2 rectCenter, Directions direction, int width, int height)
{
int dx;
int dy;
var sx = (width - 1) >> 1;
var sy = (height - 1) >> 1;
if (width == height || direction == Directions.North)
{
dx = -sx;
dy = -sy;
}
else
{
switch (direction)
{
case Directions.West:
dx = sy - (height - 1);
dy = -sx;
(width, height) = (height, width);
break;
case Directions.South:
dx = sx - (width - 1);
dy = sy - (height - 1);
break;
case Directions.East:
dx = -sy;
dy = sx - (width - 1);
(width, height) = (height, width);
break;
default:
throw new ArgumentOutOfRangeException();
}
}
return new TileRect(rectCenter + new int2(dx, dy), width, height);
}
public static void WalkLine(int2 startTile, int2 endTile, Action<int2> tileAction)
{
var dx = endTile.x - startTile.x;
var dy = endTile.y - startTile.y;
if (math.abs(dx) >= math.abs(dy))
endTile.y = startTile.y;
else
endTile.x = startTile.x;
var tile = startTile;
var d = math.sign(endTile - startTile);
while (true)
{
tileAction.Invoke(tile);
if (math.all(tile == endTile)) break;
tile += d;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int StepCount(int2 tile, int2 otherTile)
{
var delta = math.abs(tile - otherTile);
return math.max(delta.x, delta.y);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int StepCount(int2 tile, in TileRect rect)
{
var left = math.max(rect.Min.x - tile.x, 0);
var right = math.max(tile.x - rect.Max.x, 0);
var dx = math.max(left, right);
var bottom = math.max(rect.Min.y - tile.y, 0);
var top = math.max(tile.y - rect.Max.y, 0);
var dy = math.max(bottom, top);
return math.max(dx, dy);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int StepCount(in TileRect rect, int2 tile)
{
return StepCount(tile, rect);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static int StepCount(in TileRect rect, in TileRect otherRect)
{
var left = math.max(otherRect.Min.x - rect.Max.x, 0);
var right = math.max(rect.Min.x - otherRect.Max.x, 0);
var dx = math.max(left, right);
var bottom = math.max(otherRect.Min.y - rect.Max.y, 0);
var top = math.max(rect.Min.y - otherRect.Max.y, 0);
var dy = math.max(bottom, top);
return math.max(dx, dy);
}
}
}

View File

@@ -0,0 +1,88 @@
using System.Collections;
using System.Collections.Generic;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct TileRange : IEnumerable<int2>
{
public int2 Min;
public int2 Max;
private TileRange(int2 min, int2 max)
{
Min = min;
Max = max;
}
public static TileRange From(in TileRect rect)
{
return new TileRange(rect.Min, rect.Max);
}
public static TileRange From(int2 min, int2 max)
{
return From(new TileRect(min, max));
}
public Enumerator GetEnumerator()
{
return new Enumerator(Min, Max);
}
IEnumerator<int2> IEnumerable<int2>.GetEnumerator()
{
return GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public struct Enumerator : IEnumerator<int2>
{
public int2 Min;
public int2 Max;
public int2 Current { get; private set; }
object IEnumerator.Current => Current;
public Enumerator(int2 min, int2 max) : this()
{
Min = min;
Max = max;
Reset();
}
public bool MoveNext()
{
if (Current.x < Max.x)
{
Current = new int2(Current.x + 1, Current.y);
return true;
}
if (Current.y < Max.y)
{
Current = new int2(Min.x, Current.y + 1);
return true;
}
return false;
}
public void Reset()
{
Current = new int2(Min.x - 1, Min.y);
}
public void Dispose()
{
}
}
}
}

View File

@@ -0,0 +1,71 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct TileRect
{
public static readonly TileRect Empty = new(int2.zero, int2.zero);
public static readonly TileRect Everything = new(int.MinValue, int.MaxValue);
private int2 _min;
private int2 _max;
public int2 Min
{
readonly get => _min;
set
{
_min = value;
_max = math.max(_min, _max);
}
}
public int2 Max
{
readonly get => _max;
set
{
_max = value;
_min = math.min(_min, _max);
}
}
public int Width => _max.x - _min.x + 1;
public int Height => _max.y - _min.y + 1;
public int2 Center => (_min + _max) / 2;
public TileRect(int2 min, int2 max)
{
_min = math.min(min, max);
_max = math.max(min, max);
}
public TileRect(int2 min, int width, int height) : this(min, min + new int2(width - 1, height - 1))
{
}
public readonly bool Contains(int2 tile)
{
return tile.x >= Min.x && tile.x <= Max.x && tile.y >= Min.y && tile.y <= Max.y;
}
public readonly bool Intersects(TileRect other)
{
return !(other.Max.x < Min.x || other.Min.x > Max.x || other.Max.y < Min.y || other.Min.y > Max.y);
}
public readonly TileRect Inflate(int amount)
{
return amount == int.MaxValue ? Everything : new TileRect(Min - amount, Max + amount);
}
public static TileRect OneTile(int2 tile)
{
return new TileRect(tile, 1, 1);
}
}
}

View File

@@ -0,0 +1,38 @@
using Unity.Mathematics;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class TileSpace : ITileSpace
{
public float TileSize { get; set; }
public World World { get; set; }
public int2 WorldToTile(Vector3 position)
{
var tileX = Mathf.FloorToInt(position.x / TileSize);
var tileY = Mathf.FloorToInt(position.z / TileSize);
return new int2(tileX, tileY);
}
public Vector3 TileToWorld(int2 tile, float tileX = 0.5f, float tileY = 0.5f)
{
tileX = Mathf.Clamp01(tileX);
tileY = Mathf.Clamp01(tileY);
return new Vector3((tile.x + tileX) * TileSize, World.Heightmap.GetValue(tile), (tile.y + tileY) * TileSize);
}
public int GetElevation(float y)
{
return Mathf.RoundToInt(y);
}
public Vector3 GetRectWorldCenter(in TileRect rect)
{
var worldCenter = TileToWorld(rect.Min, 0, 0);
worldCenter += new Vector3(rect.Width, 0, rect.Height) * (0.5f * TileSize);
return worldCenter;
}
}
}