riversong code showcase
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Source/Riversong/Game/CommonServices/CommonServicesSystem.cs
Normal file
58
Source/Riversong/Game/CommonServices/CommonServicesSystem.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
[InitializeBefore(typeof(EarlyGameSystemGroup))]
|
||||
public class CommonServicesSystemGroup : GameSystemGroup
|
||||
{
|
||||
}
|
||||
}
|
||||
14
Source/Riversong/Game/CommonServices/Entities/Entity.cs
Normal file
14
Source/Riversong/Game/CommonServices/Entities/Entity.cs
Normal 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 };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
Source/Riversong/Game/CommonServices/Entities/IEntity.cs
Normal file
7
Source/Riversong/Game/CommonServices/Entities/IEntity.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public interface IEntity
|
||||
{
|
||||
int Id { get; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace DanieleMarotta.RiversongCodeShowcase
|
||||
{
|
||||
public class PooledObject : MonoBehaviour
|
||||
{
|
||||
public int PoolKey { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
13
Source/Riversong/Game/CommonServices/Signals/ISignalBus.cs
Normal file
13
Source/Riversong/Game/CommonServices/Signals/ISignalBus.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
31
Source/Riversong/Game/CommonServices/Signals/SignalBus.cs
Normal file
31
Source/Riversong/Game/CommonServices/Signals/SignalBus.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
};
|
||||
}
|
||||
}
|
||||
134
Source/Riversong/Game/CommonServices/TileMath/Directions.cs
Normal file
134
Source/Riversong/Game/CommonServices/TileMath/Directions.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
18
Source/Riversong/Game/CommonServices/TileMath/ITileSpace.cs
Normal file
18
Source/Riversong/Game/CommonServices/TileMath/ITileSpace.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
136
Source/Riversong/Game/CommonServices/TileMath/TileMath.cs
Normal file
136
Source/Riversong/Game/CommonServices/TileMath/TileMath.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
88
Source/Riversong/Game/CommonServices/TileMath/TileRange.cs
Normal file
88
Source/Riversong/Game/CommonServices/TileMath/TileRange.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
71
Source/Riversong/Game/CommonServices/TileMath/TileRect.cs
Normal file
71
Source/Riversong/Game/CommonServices/TileMath/TileRect.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
Source/Riversong/Game/CommonServices/TileMath/TileSpace.cs
Normal file
38
Source/Riversong/Game/CommonServices/TileMath/TileSpace.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user