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