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,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class AppLinks
{
public const string DemoFeedbackUrl = "https://forms.gle/vA5owahuh8AUMbf87";
}
}

View File

@@ -0,0 +1,17 @@
using Sirenix.OdinInspector;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "BuildVersion", menuName = "Riversong Code Showcase/Build Version")]
public class BuildVersionAsset : ScriptableObject
{
public string CurrentVersion;
[ReadOnly]
public string LastBuildDate;
[ReadOnly]
public int LastBuildCounter;
}
}

View File

@@ -0,0 +1,492 @@
using System;
using System.Collections.Generic;
using Sirenix.OdinInspector;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.TextCore.Text;
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "GameConfig", menuName = "Riversong Code Showcase/Game Config")]
[Searchable]
public class GameConfig : ScriptableObject
{
[FoldoutGroup("General Settings")]
[InlineProperty]
[HideLabel]
public GeneralSettingsConfig GeneralSettings;
[FoldoutGroup("Audio")]
[InlineProperty]
[HideLabel]
public AudioConfig Audio;
[FoldoutGroup("Camera")]
[InlineProperty]
[HideLabel]
public CameraConfig Camera;
[FoldoutGroup("World Generation")]
[InlineProperty]
[HideLabel]
public WorldGenConfig WorldGen;
[FoldoutGroup("Time")]
[InlineProperty]
[HideLabel]
public TimeConfig Time;
[FoldoutGroup("Terrain")]
[InlineProperty]
[HideLabel]
public TerrainConfig Terrain;
[FoldoutGroup("Buildings")]
[InlineProperty]
[HideLabel]
public BuildingsConfig Buildings;
[FoldoutGroup("Roads")]
[InlineProperty]
[HideLabel]
public RoadsConfig Roads;
[FoldoutGroup("Agents")]
[InlineProperty]
[HideLabel]
public AgentsConfig Agents;
[FoldoutGroup("Population")]
[InlineProperty]
[HideLabel]
public PopulationConfig Population;
[FoldoutGroup("Economy")]
[InlineProperty]
[HideLabel]
public EconomyConfig Economy;
[FoldoutGroup("Onboarding")]
[InlineProperty]
[HideLabel]
public OnboardingConfig Onboarding;
[FoldoutGroup("UI")]
[InlineProperty]
[HideLabel]
public UIConfig UI;
[FoldoutGroup("VFX")]
[InlineProperty]
[HideLabel]
public VfxConfig Vfx;
[Serializable]
public class GeneralSettingsConfig
{
public float TileSize = 1;
public int BaseElevation = 2;
[TitleGroup("Demo Only")]
public int PopulationGoal = 50;
[Range(0, 1)]
public float HappinessGoal = 1;
}
[Serializable]
public class AudioConfig
{
public AssetReferenceT<AudioClip> MainThemeClip;
[Range(0, 1)]
public float MainThemeVolume = 1;
public AssetReferenceT<AudioClip> GameplayClip;
[Range(0, 1)]
public float GameplayVolume = 1;
public AssetReferenceT<SystemSoundLibrary> SystemSoundLibrary;
public AssetReferenceGameObject AudioSourcePrefab;
[TitleGroup("Spatial Audio")]
[LabelText("Horizontal Distance Range")]
public Vector2 SpatialAudioHorizontalDistanceRange = new(10, 20);
[LabelText("Zoom Range")]
public Vector2 SpatialAudioZoomRange = new(0, 0.3f);
}
[Serializable]
public class CameraConfig
{
[TitleGroup("Movement")]
public Vector2 MoveSpeed = new(10, 30);
[TitleGroup("Rotation")]
public Vector2 MouseRotationSpeed = new(70, 70);
public Vector2 KeyboardRotationSpeed = new(70, 70);
public Vector2 PitchRange = new(30, 70);
[TitleGroup("Zoom")]
public float ZoomSensitivity = 10;
public float ZoomSpeed = 40;
public Vector2 ZoomRange = new(10, 150);
}
[Serializable]
public class WorldGenConfig
{
public int ChunkSize = 16;
public int ChunkGenerationBatchCount = 16;
public int ChunksPerThread = 16;
public List<AssetReferenceT<Texture2D>> MapTextures = new();
public TreesConfig Trees = new();
public StoneConfig Stone = new();
public GrassConfig Grass = new();
public GrassConfig Crops = new();
public ProductStacksConfig ProductStacks = new();
[Serializable]
public class TreesConfig
{
public AssetReferenceT<ResourceNodeDefinition> TreeDefinition;
public float NoiseScale = 0.05f;
[Range(0, 1)]
public float Coverage = 0.3f;
public float Spacing = 2.5f;
public Vector2 OffsetRange = new(0, 0.25f);
[Range(0, 1)]
public float RandomTreeChance = 0.03f;
}
[Serializable]
public class StoneConfig
{
public AssetReferenceT<ResourceNodeDefinition> StoneDefinition;
public Vector2Int Count = new(3, 5);
public int Spacing = 10;
}
[Serializable]
public class GrassConfig
{
public float NoiseScale = 0.1f;
public float NoiseDiscardThreshold = 0.1f;
public float BladeWidth = 0.05f;
public float2 BladeHeightRange = new(0.3f, 1);
}
[Serializable]
public class ProductStacksConfig
{
public List<AssetReferenceT<ProductDefinition>> EligibleProducts;
[Range(0, 1)]
public float Chance = 0.05f;
public int ProductAmount = 1;
}
}
[Serializable]
public class TimeConfig
{
public float WeekDuration = 15;
[TitleGroup("Day Night Cycle")]
public float DayDuration = 120;
public float NightDuration = 30;
public float DayToNightDuration = 5;
public float NightToDayDuration = 5;
public DayNightLightingConfig DayLighting;
public DayNightLightingConfig NightLighting;
[MinMaxSlider(0, 1, true)]
public Vector2 WarmTintRampUp;
[MinMaxSlider(0, 1, true)]
public Vector2 WarmTintRampDown;
[Serializable]
public class DayNightLightingConfig
{
public Color AmbientColor = Color.black;
[Range(0, 1)]
public float ShadowStrength = 1;
}
}
[Serializable]
public class TerrainConfig
{
[TitleGroup("Materials")]
public Material GroundMaterial;
public Material CliffMaterial;
public Material GrassMaterial;
public Material CropsMaterial;
[TitleGroup("Grass and Crops")]
[LabelText("Growth Rate")]
public float GrassGrowthRate = 0.2f;
[LabelText("Grass LODs")]
public GrassLOD[] GrassLODs;
[LabelText("Crops LODs")]
public GrassLOD[] CropsLODs;
public float FertilityRegenarationRate = 0.05f;
[TitleGroup("Wind")]
[InlineProperty]
[HideLabel]
public WindConfig Wind;
[Serializable]
public class GrassLOD
{
public float Threshold = 1000;
public int Density = 16;
}
[Serializable]
public class WindConfig
{
public AssetReferenceT<Texture2D> Map;
public Vector3 BendDirection = Vector3.right;
public float MapScale = 0.1f;
public float Speed = 1;
}
}
[Serializable]
public class BuildingsConfig
{
public Material ConstructionSiteMaterial;
public int WeeksWithNeedsMetToUpgrade = 2;
}
[Serializable]
public class RoadsConfig
{
public float PlaceTileInterval = 0.2f;
public List<AssetReferenceT<GameObject>> Tiles;
public AssetReferenceT<GameObject> PlacementVfxPrefab;
}
[Serializable]
public class AgentsConfig
{
public AssetReferenceT<AgentDefinition> GenericAgent;
public AssetReferenceT<AgentDefinition> HunterAgent;
public AssetReferenceT<AgentDefinition> FarmerAgent;
public float MoveSpeed = 5;
public float RotationSpeed = 180;
}
[Serializable]
public class PopulationConfig
{
public AssetReferenceT<BuildingDefinition> TentBuilding;
[TitleGroup("Grace Period")]
[LabelText("Number of Weeks")]
public int GraceWeekCount = 3;
[LabelText("Min Happiness during Grace")]
[Range(0, 1)]
public float GraceMinHappiness = 0.8f;
[TitleGroup("Overall Happiness")]
[LabelText("Initial Value")]
[Range(0, 1)]
public float InitialeOverallHappiness = 0.8f;
[LabelText("Change Rate")]
public float OverallHappinessChangeRate = 0.5f;
[TitleGroup("Houses Happiness")]
[LabelText("Initial Value")]
[Range(0, 1)]
public float InitialHouseHappiness = 0.8f;
[LabelText("Weight Ramp Up (Weeks)")]
public int HouseWeightRampUpWeekCount = 3;
[TitleGroup("Growth Rate")]
[LabelText("Peak")]
public float GrowthRatePeakValue = 1;
[LabelText("Curve")]
public AnimationCurve GrowthRateCurve = AnimationCurve.Linear(0, -1, 1, 1);
}
[Serializable]
public class EconomyConfig
{
public float ProductionTickInterval = 0.1f;
public float RestedWorkersEfficiencyModifier = 0.5f;
public int RestedWorkersHouseMaxStepCount = 10;
public float PopulationToLaborFactor = 1;
public int PopulationPerMinLaborTierStep = 8;
public List<LaborTierConfig> LaborTiers = new()
{
new LaborTierConfig
{
Threshold = 0,
EfficiencyModifier = -0.3f
},
new LaborTierConfig
{
Threshold = 0.6f,
EfficiencyModifier = -0.15f
},
new LaborTierConfig
{
Threshold = 0.85f,
EfficiencyModifier = -0.05f
},
new LaborTierConfig
{
Threshold = 1,
EfficiencyModifier = 0
}
};
}
[Serializable]
public class LaborTierConfig
{
[Range(0, 1)]
public float Threshold;
public float EfficiencyModifier;
}
[Serializable]
public class OnboardingConfig
{
public int PopulationMilestone = 20;
public float MessageDuration = 30;
public OnboardingMessages Messages;
}
[Serializable]
public class UIConfig
{
public AssetReferenceT<GameObject> RootPrefab;
public AssetReferenceT<UITemplateLibrary> TemplateLibrary;
public DayNightUITheme Theme;
public AssetReferenceT<FontAsset> DebugFont;
public Material HighlightedGameObjectsMaterial;
public Material DeletedGameObjectsMaterial;
[TitleGroup("Tile Highlight")]
[InlineProperty]
[HideLabel]
public TileHighlightConfig TileHighlight;
[TitleGroup("Build Tool")]
[InlineProperty]
[HideLabel]
public BuildToolConfig BuildTool;
[Serializable]
public class TileHighlightConfig
{
public AssetReferenceT<GameObject> Prefab;
public Color ValidColor = Color.cyan;
public Color InvalidColor = Color.red;
public Color DeletePreviewColor = Color.red;
}
[Serializable]
public class BuildToolConfig
{
public float Height = 5;
public float LerpFactor = 10;
public float ImpulseScale = 1;
public float MaxTilt = 5;
public float MaxTiltAngle = 30;
public float Elasticity = 25;
[Range(0, 1)]
public float Damping = 0.9f;
}
}
[Serializable]
public class VfxConfig
{
[LabelText("AoE Material")]
public Material AoEMaterial;
}
}
}

View File

@@ -0,0 +1,30 @@
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IScene
{
Camera MainCamera { get; }
CinemachineCamera CinemachineCamera { get; }
SceneFolders SceneFolders { get; }
UIDocument LoadingOverlay { get; }
Transform LightRig { get; }
Light MainLight { get; }
Light NightLight { get; }
Volume NightVolume { get; }
Volume BloomVolume { get; }
Volume WarmTintVolume { get; }
}
}

View File

@@ -0,0 +1,21 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class SceneFolders : MonoBehaviour
{
public Transform AudioSources;
public Transform TerrainChunks;
public Transform RawResources;
public Transform Buildings;
public Transform RoadTiles;
public Transform ProductStacks;
public Transform Agents;
}
}

View File

@@ -0,0 +1,64 @@
using Sirenix.OdinInspector;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class UnityObjectInjector : MonoBehaviour, IScene, IServiceProvider
{
[field: TitleGroup("Config")]
[field: SerializeField]
public GameConfig GameConfig { get; private set; }
[field: SerializeField] public BuildVersionAsset BuildVersion { get; private set; }
[field: TitleGroup("Scene")]
[field: SerializeField]
public Camera MainCamera { get; private set; }
[field: TitleGroup("Scene")]
[field: SerializeField]
public CinemachineCamera CinemachineCamera { get; private set; }
[field: TitleGroup("Scene")]
[field: SerializeField]
public SceneFolders SceneFolders { get; private set; }
[field: TitleGroup("Scene")]
[field: SerializeField]
public UIDocument LoadingOverlay { get; private set; }
[field: FoldoutGroup("Scene/Day Night Cycle")]
[field: SerializeField]
public Transform LightRig { get; private set; }
[field: FoldoutGroup("Scene/Day Night Cycle")]
[field: SerializeField]
public Light MainLight { get; private set; }
[field: FoldoutGroup("Scene/Day Night Cycle")]
[field: SerializeField]
public Light NightLight { get; private set; }
[field: FoldoutGroup("Scene/Day Night Cycle")]
[field: SerializeField]
public Volume NightVolume { get; private set; }
[field: FoldoutGroup("Scene/Day Night Cycle")]
[field: SerializeField]
public Volume BloomVolume { get; private set; }
[field: FoldoutGroup("Scene/Day Night Cycle")]
[field: SerializeField]
public Volume WarmTintVolume { get; private set; }
public void RegisterServices(IServiceLocator serviceLocator)
{
serviceLocator.RegisterService(GameConfig);
serviceLocator.RegisterService(BuildVersion);
serviceLocator.RegisterService<IScene>(this);
}
}
}

View File

@@ -0,0 +1,8 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[InitializeAfter(typeof(DebugSystemGroup))]
public class FinalizeInitializationSystemGroup : GameSystemGroup
{
}
}

View File

@@ -0,0 +1,6 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct GameInitializationCompletedSignal
{
}
}

View File

@@ -0,0 +1,6 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct GameStartedSignal
{
}
}

View File

@@ -0,0 +1,29 @@
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(FinalizeInitializationSystemGroup))]
public class GameInitializationCompletedSignalSystem : GameSystem, IInitializable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IScene _scene;
public GameInitializationCompletedSignalSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public async UniTask InitializeAsync()
{
await UniTask.NextFrame();
_signalBus.Raise(new GameInitializationCompletedSignal());
await UniTask.NextFrame();
_scene.LoadingOverlay.enabled = false;
}
}
}

View File

@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine.AddressableAssets;
using Object = UnityEngine.Object;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
[InitializeAfter(typeof(GameDatabaseSystem))]
public class PreLoadAssetsSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private GameConfig _config;
[InjectService]
private IGameDatabase _gameDatabase;
private readonly HashSet<AssetReference> _loadedReferences = new();
public PreLoadAssetsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public async UniTask InitializeAsync()
{
var tasks = new List<UniTask>();
PreLoadAgents(tasks);
PreLoadProducts(tasks);
PreLoadBuildings(tasks);
await UniTask.WhenAll(tasks);
}
private void PreLoadAgents(List<UniTask> tasks)
{
Load(_config.Agents.GenericAgent, tasks);
Load(_config.Agents.HunterAgent, tasks);
Load(_config.Agents.FarmerAgent, tasks);
}
private void PreLoadProducts(List<UniTask> tasks)
{
foreach (var product in _gameDatabase.OfType<ProductDefinition>())
{
Load(product.ProductStackVisualization, tasks);
Load(product.CarriedVisualization, tasks);
}
}
private void PreLoadBuildings(List<UniTask> tasks)
{
Load(_config.Population.TentBuilding, tasks);
foreach (var building in _gameDatabase.OfType<BuildingDefinition>()) Load(building.Visualization, tasks);
}
private void Load<T>(AssetReferenceT<T> assetReference, List<UniTask> tasks) where T : Object
{
if (!assetReference.RuntimeKeyIsValid() || !_loadedReferences.Add(assetReference)) return;
tasks.Add(assetReference.LoadAssetAsync().ToUniTask());
}
public void Dispose()
{
foreach (var assetReference in _loadedReferences) assetReference.ReleaseAsset();
}
}
}

View File

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

View File

@@ -0,0 +1,82 @@
using System;
using Cysharp.Threading.Tasks;
using PrimeTween;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(AudioSystemGroup))]
public class BackgroundMusicSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private GameConfig _config;
[InjectService]
private IScene _scene;
[InjectService]
private ISignalBus _signalBus;
private AudioSource _audioSource;
private AudioClip _mainThemeClip;
private AudioClip _gameplayClip;
public BackgroundMusicSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public async UniTask InitializeAsync()
{
_audioSource = new GameObject("BGM").AddComponent<AudioSource>();
_audioSource.transform.SetParent(_scene.SceneFolders.AudioSources);
_audioSource.playOnAwake = false;
_audioSource.loop = true;
_mainThemeClip = await _config.Audio.MainThemeClip.LoadAssetAsync<AudioClip>();
_gameplayClip = await _config.Audio.GameplayClip.LoadAssetAsync<AudioClip>();
_signalBus.Subscribe<GameInitializationCompletedSignal>(OnGameInitializationCompleted);
_signalBus.Subscribe<GameStartedSignal>(OnGameStarted);
_signalBus.Subscribe<WorldReadySignal>(OnWorldReady);
}
public void Dispose()
{
_signalBus.Unsubscribe<GameInitializationCompletedSignal>(OnGameInitializationCompleted);
_signalBus.Unsubscribe<GameStartedSignal>(OnGameStarted);
_signalBus.Unsubscribe<WorldReadySignal>(OnWorldReady);
}
private void OnGameInitializationCompleted(GameInitializationCompletedSignal signal)
{
Play(_mainThemeClip, _config.Audio.MainThemeVolume);
}
private void OnGameStarted(GameStartedSignal signal)
{
_ = FadeOutAsync(2);
}
private async UniTask FadeOutAsync(float duration)
{
await Tween.Custom(_audioSource.volume, 0, duration, value => _audioSource.volume = value, Ease.Linear);
}
private void OnWorldReady(WorldReadySignal signal)
{
Play(_gameplayClip, _config.Audio.GameplayVolume);
}
private void Play(AudioClip clip, float volume)
{
_audioSource.Stop();
_audioSource.clip = clip;
_audioSource.volume = volume;
_audioSource.Play();
}
}
}

View File

@@ -0,0 +1,34 @@
using Unity.Mathematics;
using UnityEngine.Audio;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface ISoundPlayer
{
void Play(AudioResource resource);
void PlayAt(AudioResource resource, float3 position);
void PlayAt(AudioResource resource, int2 tile);
AudioResource GetSystemSound(SystemSoundId soundId);
}
public static class SoundPlayerExtensions
{
public static void Play(this ISoundPlayer soundPlayer, SystemSoundId soundId)
{
soundPlayer.Play(soundPlayer.GetSystemSound(soundId));
}
public static void PlayAt(this ISoundPlayer soundPlayer, SystemSoundId soundId, float3 position)
{
soundPlayer.PlayAt(soundPlayer.GetSystemSound(soundId), position);
}
public static void PlayAt(this ISoundPlayer soundPlayer, SystemSoundId soundId, int2 tile)
{
soundPlayer.PlayAt(soundPlayer.GetSystemSound(soundId), tile);
}
}
}

View File

@@ -0,0 +1,120 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using Time = UnityEngine.Time;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class PlaySoundOnEventSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private ISoundPlayer _soundPlayer;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private IGameDatabase _gameDatabase;
[InjectService]
private ITileSpace _tileSpace;
private Dictionary<int, float> _soundPlayedTimestamps = new();
public PlaySoundOnEventSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
_signalBus.Subscribe<BuildingPlacementAnimationCompletedSignal>(OnBuildingPlacementAnimationCompleted);
_signalBus.Subscribe<ConstructionSiteDeletedSignal>(OnConstructionSiteDeleted);
_signalBus.Subscribe<BuildingDeletedSignal>(OnBuildingDeleted);
_signalBus.Subscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
_signalBus.Subscribe<RawResourcesRemovedSignal>(OnRawResourcesRemoved);
_signalBus.Subscribe<RoadTileUpdatedSignal>(OnRoadTileUpdated);
_signalBus.Subscribe<BuildMenuButtonUnlockAnimationStartedSignal>(OnBuildMenuButtonUnlockAnimationStarted);
return UniTask.CompletedTask;
}
public void Dispose()
{
_signalBus.Unsubscribe<BuildingPlacementAnimationCompletedSignal>(OnBuildingPlacementAnimationCompleted);
_signalBus.Unsubscribe<ConstructionSiteDeletedSignal>(OnConstructionSiteDeleted);
_signalBus.Unsubscribe<BuildingDeletedSignal>(OnBuildingDeleted);
_signalBus.Unsubscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
_signalBus.Unsubscribe<RawResourcesRemovedSignal>(OnRawResourcesRemoved);
_signalBus.Unsubscribe<RoadTileUpdatedSignal>(OnRoadTileUpdated);
_signalBus.Unsubscribe<BuildMenuButtonUnlockAnimationStartedSignal>(OnBuildMenuButtonUnlockAnimationStarted);
}
private bool UpdatePlayedTimestamp(int key)
{
var timestamp = _soundPlayedTimestamps.GetValueOrDefault(key);
const float minInterval = 0.1f;
if (Time.unscaledTime - timestamp < minInterval) return false;
_soundPlayedTimestamps[key] = Time.unscaledTime;
return true;
}
private void OnBuildingPlacementAnimationCompleted(BuildingPlacementAnimationCompletedSignal signal)
{
PlaySystemSound(SystemSoundId.BuildingPlaced);
}
private void OnConstructionSiteDeleted(ConstructionSiteDeletedSignal signal)
{
PlaySystemSound(SystemSoundId.BuildingDeleted);
}
private void OnBuildingDeleted(BuildingDeletedSignal signal)
{
if (signal.Options == DeleteBuildingOptions.Silent) return;
PlaySystemSound(SystemSoundId.BuildingDeleted);
}
private void OnBuildingUpgraded(BuildingUpgradedSignal signal)
{
if (!signal.Building.Definition.IsHouse || !UpdatePlayedTimestamp((int)SystemSoundId.HouseUpgraded)) return;
var position = (float3)_tileSpace.GetRectWorldCenter(signal.Building.Rect);
_soundPlayer.PlayAt(SystemSoundId.HouseUpgraded, position);
}
private void OnRawResourcesRemoved(RawResourcesRemovedSignal signal)
{
foreach (var resourceNode in signal.ResourceNodes)
{
var definition = _gameDatabase.WithId<ResourceNodeDefinition>(resourceNode.DefinitionId);
if (!UpdatePlayedTimestamp(definition.RuntimeId)) continue;
if (signal.Reason == RawResourcesRemovedSignal.RemovalReason.Harvested)
_soundPlayer.PlayAt(definition.DeleteAudio, resourceNode.Tile);
else
_soundPlayer.Play(definition.DeleteAudio);
}
}
private void OnRoadTileUpdated(RoadTileUpdatedSignal signal)
{
PlaySystemSound(signal.RoadTileAdded ? SystemSoundId.RoadTilePlaced : SystemSoundId.RoadTileDeleted);
}
private void OnBuildMenuButtonUnlockAnimationStarted(BuildMenuButtonUnlockAnimationStartedSignal signal)
{
PlaySystemSound(SystemSoundId.UnlockNotification);
}
private void PlaySystemSound(SystemSoundId soundId)
{
if (UpdatePlayedTimestamp((int)soundId)) _soundPlayer.Play(soundId);
}
}
}

View File

@@ -0,0 +1,184 @@
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine;
using UnityEngine.Audio;
using Object = UnityEngine.Object;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(ISoundPlayer))]
[GameSystemGroup(typeof(AudioSystemGroup))]
[InitializeAfter(typeof(BackgroundMusicSystem))]
public class SoundPlayerSystem : GameSystem, IInitializable, IUpdatable, ISoundPlayer, IDrawGizmos
{
private const int ChannelCount = 16;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private IScene _scene;
[InjectService]
private ICameraProperties _cameraProperties;
[InjectService]
private GameConfig _config;
private AudioSourcePool _poolNonSpatial;
private AudioSourcePool _poolSpatial;
private SystemSoundLibrary _systemSoundLibrary;
public SoundPlayerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public async UniTask InitializeAsync()
{
var systemSoundLibraryTask = _config.Audio.SystemSoundLibrary.LoadAssetAsync<SystemSoundLibrary>().ToUniTask();
var audioSourcePrefabTask = _config.Audio.AudioSourcePrefab.LoadAssetAsync().ToUniTask();
_systemSoundLibrary = await systemSoundLibraryTask;
var audioSourcePrefab = (await audioSourcePrefabTask).GetComponent<AudioSource>();
await InitializePoolAsync(audioSourcePrefab);
}
private async UniTask InitializePoolAsync(AudioSource audioSourcePrefab)
{
_poolNonSpatial = InitializePool(audioSourcePrefab, "Sound_{0:00} (2D)");
await UniTask.NextFrame();
_poolSpatial = InitializePool(audioSourcePrefab, "Sound_{0:00} (3D)");
await UniTask.NextFrame();
}
private AudioSourcePool InitializePool(AudioSource audioSourcePrefab, string nameFormat)
{
var audioSources = new AudioSource[ChannelCount];
for (var i = 0; i < ChannelCount; i++)
{
var source = Object.Instantiate(audioSourcePrefab, _scene.SceneFolders.AudioSources);
source.name = string.Format(nameFormat, i);
audioSources[i] = source;
}
return new AudioSourcePool(audioSources);
}
public void Update()
{
UpdateSpatialAudioSources();
}
private void UpdateSpatialAudioSources()
{
var cameraPosition = ((float3)_scene.MainCamera.transform.position).xz;
var horizontalDistanceRange = _config.Audio.SpatialAudioHorizontalDistanceRange;
var zoomRange = _config.Audio.SpatialAudioZoomRange;
foreach (var audioSource in _poolSpatial.AudioSources)
{
#if !UNITY_EDITOR
if (!audioSource.isPlaying) continue;
#endif
var p = ((float3)audioSource.transform.position).xz;
var horizontalDistance = math.distance(cameraPosition, p);
var t = math.saturate(math.unlerp(horizontalDistanceRange.x, horizontalDistanceRange.y, horizontalDistance));
var attenuation = 1 - t * t;
var zoomFactor = 1 - math.unlerp(zoomRange.x, zoomRange.y, _cameraProperties.Zoom);
audioSource.volume = attenuation * zoomFactor;
}
}
public void Play(AudioResource resource)
{
var source = _poolNonSpatial.GetAudioSource();
source.resource = resource;
source.Play();
}
public void PlayAt(AudioResource resource, float3 position)
{
var source = _poolSpatial.GetAudioSource();
source.resource = resource;
source.transform.position = position;
source.Play();
}
public void PlayAt(AudioResource resource, int2 tile)
{
PlayAt(resource, _tileSpace.TileToWorld(tile));
}
public AudioResource GetSystemSound(SystemSoundId soundId)
{
return _systemSoundLibrary.Sounds.GetValueOrDefault(soundId);
}
public void DrawGizmos(bool selected)
{
#if UNITY_EDITOR
const int width = 60;
const int height = 8;
var camera = _scene.MainCamera;
foreach (var audioSource in _poolSpatial.AudioSources)
{
var screenPoint = camera.WorldToScreenPoint(audioSource.transform.position);
if (screenPoint.z < 0) continue;
var p = new Vector2(screenPoint.x - width * 0.5f, camera.pixelHeight - screenPoint.y);
Handles.BeginGUI();
EditorGUI.DrawRect(new Rect(p.x, p.y, width, height), new Color(0, 0, 0, 0.6f));
var volume = audioSource.volume;
var color = Color.Lerp(Color.red, Color.green, volume);
EditorGUI.DrawRect(new Rect(p.x + 0.5f * (width * (1 - volume)), p.y, width * volume, height), color);
Handles.EndGUI();
}
#endif
}
private class AudioSourcePool
{
private int _nextChannel;
public AudioSourcePool(AudioSource[] audioSources)
{
AudioSources = audioSources;
}
public AudioSource[] AudioSources { get; }
public AudioSource GetAudioSource()
{
var source = AudioSources[_nextChannel];
_nextChannel = (_nextChannel + 1) % ChannelCount;
return source;
}
}
}
}

View File

@@ -0,0 +1,21 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum SystemSoundId
{
UIClick,
BuildingPlaced,
BuildingDeleted,
HouseUpgraded,
RoadTilePlaced,
RoadTileDeleted,
UnlockNotification,
OnboardingMessage
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
using Sirenix.OdinInspector;
using UnityEngine;
using UnityEngine.Audio;
namespace DanieleMarotta.RiversongCodeShowcase
{
[CreateAssetMenu(fileName = "SystemSoundLibrary", menuName = "Riversong Code Showcase/System Sound Library")]
public class SystemSoundLibrary : SerializedScriptableObject
{
public Dictionary<SystemSoundId, AudioResource> Sounds;
}
}

View File

@@ -0,0 +1,145 @@
using Cysharp.Threading.Tasks;
using Unity.Cinemachine;
using UnityEngine;
using UnityEngine.InputSystem;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(ICameraProperties))]
public class CameraSystem : GameSystem, IInitializable, IUpdatable, ICameraProperties
{
[InjectService]
private GameConfig _gameConfig;
[InjectService]
private IScene _scene;
private GameConfig.CameraConfig _cameraConfig;
private CinemachineBrain _cinemachineBrain;
private CinemachineFollow _cinemachineFollow;
private Transform _target;
private float _targetZoom;
private Vector3? _terrainDragAnchor;
public CameraSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public float Zoom { get; private set; }
public UniTask InitializeAsync()
{
_cameraConfig = _gameConfig.Camera;
_cinemachineBrain = _scene.MainCamera.GetComponent<CinemachineBrain>();
_cinemachineFollow = (CinemachineFollow)_scene.CinemachineCamera.GetCinemachineComponent(CinemachineCore.Stage.Body);
_target = _cinemachineFollow.FollowTarget;
Zoom = _targetZoom = _cinemachineFollow.FollowOffset.magnitude;
return UniTask.CompletedTask;
}
public void Update()
{
var dt = Time.unscaledDeltaTime;
UpdateZoom(dt);
UpdatePosition(dt);
UpdateRotation(dt);
_cinemachineBrain.ManualUpdate();
}
private void UpdateZoom(float dt)
{
var zoomInput = -1 * Mouse.current.scroll.y.ReadValue();
_targetZoom += zoomInput * _cameraConfig.ZoomSensitivity;
_targetZoom = Mathf.Clamp(_targetZoom, _cameraConfig.ZoomRange.x, _cameraConfig.ZoomRange.y);
Zoom = Mathf.Lerp(Zoom, _targetZoom, _cameraConfig.ZoomSpeed * dt);
_cinemachineFollow.FollowOffset = _cinemachineFollow.FollowOffset.normalized * Zoom;
}
private void UpdatePosition(float dt)
{
var moveInput = Vector3.zero;
moveInput.x += Keyboard.current.dKey.isPressed ? 1 : 0;
moveInput.x += Keyboard.current.aKey.isPressed ? -1 : 0;
moveInput.z += Keyboard.current.wKey.isPressed ? 1 : 0;
moveInput.z += Keyboard.current.sKey.isPressed ? -1 : 0;
var right = _target.right;
var forward = _target.forward;
forward.y = 0;
forward.Normalize();
var normalizedZoom = Mathf.InverseLerp(_cameraConfig.ZoomRange.x, _cameraConfig.ZoomRange.y, Zoom);
var moveSpeed = Mathf.Lerp(_cameraConfig.MoveSpeed.x, _cameraConfig.MoveSpeed.y, normalizedZoom);
var delta = moveSpeed * dt;
_target.position += right * (moveInput.x * delta);
_target.position += forward * (moveInput.z * delta);
UpdateDraggingTerrain();
}
private void UpdateDraggingTerrain()
{
if (!Mouse.current.middleButton.isPressed)
{
_terrainDragAnchor = null;
return;
}
var plane = new Plane(Vector3.up, new Vector3(0, _gameConfig.GeneralSettings.BaseElevation, 0));
var ray = _scene.MainCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
if (!plane.Raycast(ray, out var enter)) return;
var hit = ray.GetPoint(enter);
if (_terrainDragAnchor == null)
{
_terrainDragAnchor = hit;
return;
}
_target.position += _terrainDragAnchor.Value - hit;
}
private void UpdateRotation(float dt)
{
var keyboard = Keyboard.current;
var rotation = _target.rotation.eulerAngles;
var keyboardRotationDelta = Vector2.zero;
keyboardRotationDelta.x += keyboard.eKey.isPressed ? 1 : 0;
keyboardRotationDelta.x += keyboard.qKey.isPressed ? -1 : 0;
keyboardRotationDelta.y += keyboard.rKey.isPressed ? 1 : 0;
keyboardRotationDelta.y += keyboard.fKey.isPressed ? -1 : 0;
rotation.y += keyboardRotationDelta.x * _cameraConfig.KeyboardRotationSpeed.x * dt;
rotation.x -= keyboardRotationDelta.y * _cameraConfig.KeyboardRotationSpeed.y * dt;
if (keyboard.leftAltKey.isPressed)
{
var mouseDelta = Mouse.current.delta;
rotation.x += mouseDelta.y.ReadValue() * _cameraConfig.MouseRotationSpeed.y * dt;
rotation.y += mouseDelta.x.ReadValue() * _cameraConfig.MouseRotationSpeed.x * dt;
}
rotation.x = Mathf.Clamp(rotation.x, _cameraConfig.PitchRange.x, _cameraConfig.PitchRange.y);
_target.forward = Quaternion.Euler(rotation) * Vector3.forward;
}
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface ICameraProperties
{
float Zoom { get; }
}
}

View File

@@ -0,0 +1,65 @@
using System;
using Unity.Collections;
using Unity.Collections.LowLevel.Unsafe;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class NativeGrid<T> : IDisposable where T : struct
{
private NativeArray<T> _data;
public NativeGrid(int2 size, Allocator allocator)
{
Size = size;
_data = new NativeArray<T>(Size.x * Size.y, allocator);
}
public int2 Size { get; }
protected int GetIndex(int2 point)
{
return math.mad(point.y, Size.x, point.x);
}
public T GetValue(int index)
{
return _data[index];
}
public T GetValue(int2 point)
{
return GetValue(GetIndex(point));
}
public unsafe ref T GetValueRW(int index)
{
return ref UnsafeUtility.ArrayElementAsRef<T>(_data.GetUnsafePtr(), index);
}
public ref T GetValueRW(int2 point)
{
return ref GetValueRW(GetIndex(point));
}
public void SetValue(int index, T value)
{
_data[index] = value;
}
public void SetValue(int2 point, T value)
{
SetValue(GetIndex(point), value);
}
public NativeArray<T> GetNativeArray()
{
return _data;
}
public void Dispose()
{
_data.Dispose();
}
}
}

View File

@@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using Unity.Mathematics;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class SpatialLookup<T>
{
private static readonly Predicate<T> NoopFilter = _ => true;
private int _cellSize;
private ListMultiDictionary<int2, Item> _lookup = new();
public SpatialLookup(int cellSize)
{
_cellSize = cellSize;
}
public void Add(T obj, TileRect rect, int key)
{
foreach (var cell in CellRange(rect)) _lookup.Add(cell, new Item(obj, rect, key));
}
public void Remove(TileRect rect, int key)
{
foreach (var cell in CellRange(rect)) _lookup.Remove(cell, new Item(key));
}
private TileRange CellRange(TileRect rect)
{
return TileRange.From(rect.Min / _cellSize, rect.Max / _cellSize);
}
public void RemoveAll(TileRect rect, Predicate<T> filter = null, List<T> result = null)
{
filter ??= NoopFilter;
using var closedScope = HashSetPool<int>.Get(out var closed);
using var toRemoveScope = ListPool<(int2, Item)>.Get(out var toRemove);
var cellMin = rect.Min / _cellSize;
var cellMax = rect.Max / _cellSize;
int2 cell;
for (cell.x = cellMin.x; cell.x <= cellMax.x; cell.x++)
for (cell.y = cellMin.y; cell.y <= cellMax.y; cell.y++)
{
if (!_lookup.TryGetValues(cell, out var list)) continue;
if (cell.x == cellMin.x || cell.x == cellMax.x || cell.y == cellMin.y || cell.y == cellMax.y)
{
foreach (var item in list)
{
if (!closed.Add(item.EqualityKey) || !rect.Intersects(item.Rect) || !filter.Invoke(item.Obj)) continue;
toRemove.Add((cell, item));
result?.Add(item.Obj);
}
}
else
{
if (result != null)
foreach (var item in list)
if (closed.Add(item.EqualityKey) || !filter.Invoke(item.Obj))
result.Add(item.Obj);
_lookup.Clear(cell);
}
}
foreach (var (key, value) in toRemove) _lookup.Remove(key, value);
}
public void Find(TileRect rect, List<T> result)
{
using var closedScope = HashSetPool<int>.Get(out var closed);
var cellMin = rect.Min / _cellSize;
var cellMax = rect.Max / _cellSize;
int2 cell;
for (cell.x = cellMin.x; cell.x <= cellMax.x; cell.x++)
for (cell.y = cellMin.y; cell.y <= cellMax.y; cell.y++)
{
if (!_lookup.TryGetValues(cell, out var list)) continue;
if (cell.x == cellMin.x || cell.x == cellMax.x || cell.y == cellMin.y || cell.y == cellMax.y)
foreach (var item in list)
{
if (!rect.Intersects(item.Rect) || !closed.Add(item.EqualityKey)) continue;
result.Add(item.Obj);
}
else
foreach (var item in list)
if (closed.Add(item.EqualityKey))
result.Add(item.Obj);
}
}
public bool FindMax(TileRect rect, Func<T, int> scoreFunc, out T max)
{
using var resultScope = ListPool<T>.Get(out var result);
Find(rect, result);
max = default;
var maxScore = int.MinValue;
foreach (var item in result)
{
var score = scoreFunc.Invoke(item);
if (score > maxScore)
{
maxScore = score;
max = item;
}
}
return maxScore > int.MinValue;
}
private struct Item : IEquatable<Item>
{
public T Obj;
public TileRect Rect;
public int EqualityKey;
public Item(T obj, TileRect rect, int equalityKey)
{
Obj = obj;
Rect = rect;
EqualityKey = equalityKey;
}
public Item(int equalityKey) : this(default, default, equalityKey)
{
}
public bool Equals(Item other)
{
return EqualityKey == other.EqualityKey;
}
}
}
}

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

View File

@@ -0,0 +1,44 @@
using System;
using Cysharp.Threading.Tasks;
using QFSW.QC;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DebugSystemGroup))]
public class DebugCommandsSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private IUnlocksService _unlocksService;
[InjectService]
private ISignalBus _signalBus;
public DebugCommandsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
QuantumRegistry.RegisterObject(this);
return UniTask.CompletedTask;
}
public void Dispose()
{
QuantumRegistry.DeregisterObject(this);
}
[Command("unlock-all", MonoTargetType.Registry)]
private void UnlockAll()
{
_unlocksService.UnlockAll();
}
[Command("complete-demo", MonoTargetType.Registry)]
private void CompleteDemo(float gameTime = 15 * 60)
{
_signalBus.Raise(new DemoCompletedSignal(gameTime));
}
}
}

View File

@@ -0,0 +1,8 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
[InitializeAfter(typeof(UISystemGroup))]
public class DebugSystemGroup : GameSystemGroup
{
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[GameSystemGroup(typeof(DebugSystemGroup))]
public class DrawGizmosSystem : GameSystem, IInitializable, IDisposable
{
[InjectService]
private IEngine _engine;
private DrawGizmosSceneProxy _sceneProxy;
private List<IDrawGizmos> _callbacks = new();
public DrawGizmosSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public UniTask InitializeAsync()
{
#if DEBUG
_sceneProxy = new GameObject(nameof(DrawGizmosSceneProxy)).AddComponent<DrawGizmosSceneProxy>();
_sceneProxy.DrawGizmos += OnDrawGizmos;
#endif
foreach (var system in _engine.Systems)
if (system is IDrawGizmos callback)
_callbacks.Add(callback);
return UniTask.CompletedTask;
}
public void Dispose()
{
_sceneProxy.DrawGizmos -= OnDrawGizmos;
}
private void OnDrawGizmos(bool selected)
{
foreach (var callback in _callbacks) callback.DrawGizmos(selected);
}
private class DrawGizmosSceneProxy : MonoBehaviour
{
public event Action<bool> DrawGizmos;
private void OnDrawGizmos()
{
DrawGizmos?.Invoke(false);
}
private void OnDrawGizmosSelected()
{
DrawGizmos?.Invoke(true);
}
}
}
}

View File

@@ -0,0 +1,7 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IDrawGizmos
{
void DrawGizmos(bool selected);
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct DemoCompletedSignal
{
public float GameTime;
public DemoCompletedSignal(float gameTime)
{
GameTime = gameTime;
}
}
}

View File

@@ -0,0 +1,41 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
[RequiresWorldReadyForUpdate]
public class DemoSystem : GameSystem, IUpdatable
{
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private GameConfig _config;
[InjectService]
private World _world;
private bool _demoCompleted;
private float _gameTime;
public DemoSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
if (_demoCompleted) return;
_gameTime += Time.unscaledDeltaTime;
var populationState = _world.PopulationState;
var generalSettings = _config.GeneralSettings;
if (populationState.Population < generalSettings.PopulationGoal || populationState.Happiness < generalSettings.HappinessGoal) return;
_demoCompleted = true;
_signalBus.Raise(new DemoCompletedSignal(_gameTime));
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
using System.Collections.Generic;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct CollectDeletedGameObjectsSignal
{
public DeletedGameObjectsFilter Filter;
public TileRect Rect;
public List<GameObject> GameObjects;
public CollectDeletedGameObjectsSignal(DeletedGameObjectsFilter filter, TileRect rect, List<GameObject> gameObjects)
{
Filter = filter;
Rect = rect;
GameObjects = gameObjects;
}
}
}

View File

@@ -0,0 +1,15 @@
using System.Collections.Generic;
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct CollectHighlightedGameObjectsSignal
{
public List<GameObject> GameObjects;
public CollectHighlightedGameObjectsSignal(List<GameObject> gameObjects)
{
GameObjects = gameObjects;
}
}
}

View File

@@ -0,0 +1,57 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class DeleteTool : DragTool
{
private ISignalBus _signalBus;
public DeleteTool(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override DeletedGameObjectsFilter DeletedGameObjectsFilter => DeletedGameObjectsFilter.All;
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_signalBus = ServiceLocator.GetService<ISignalBus>();
}
protected override bool Validate(ref int2 startTile, ref int2 endTile)
{
return true;
}
protected override void UpdateAffectedTiles(bool isValid, int2 startTile, int2 endTile)
{
foreach (var tile in TileRange.From(startTile, endTile)) AffectedTiles.Add((tile, TileHighlightType.DeletePreview));
}
protected override void DoTool(int2 startTile, int2 endTile)
{
_ = DoToolAsync(new TileRect(startTile, endTile));
}
private async UniTask DoToolAsync(TileRect rect)
{
const int count = 10;
var countX = (int)math.ceil((float)rect.Width / count);
var countY = (int)math.ceil((float)rect.Height / count);
for (var x = 0; x < countX; x++)
for (var y = 0; y < countY; y++)
{
var r = new TileRect(rect.Min + new int2(x, y) * count, count, count);
r.Max = math.min(r.Max, rect.Max);
_signalBus.Raise(new DoDeleteToolSignal(r));
await UniTask.NextFrame();
}
}
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct DoDeleteToolSignal
{
public TileRect Rect;
public DoDeleteToolSignal(TileRect rect)
{
Rect = rect;
}
}
}

View File

@@ -0,0 +1,18 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Flags]
public enum DeletedGameObjectsFilter
{
None = 0,
Buildings = 1,
RawResources = 1 << 1,
ProductStacks = 1 << 2,
All = ~None
}
}

View File

@@ -0,0 +1,96 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
using UnityEngine.InputSystem;
namespace DanieleMarotta.RiversongCodeShowcase
{
public abstract class DragTool : EditTool
{
private IPointerService _pointerService;
private ITileSpace _tileSpace;
private bool _isDragging;
private int2 _startTile;
private int2 _endTile;
private bool _isValid;
protected DragTool(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected virtual DeletedGameObjectsFilter DeletedGameObjectsFilter => DeletedGameObjectsFilter.None;
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_pointerService = ServiceLocator.GetService<IPointerService>();
_tileSpace = ServiceLocator.GetService<ITileSpace>();
}
public override void OnDisabled()
{
base.OnDisabled();
_isDragging = false;
}
public override void Update()
{
base.Update();
AffectedTiles.Clear();
var lmb = Mouse.current.leftButton;
var isPointerOnTerrain = _pointerService.TryGetPositionOnTerrain(out var position);
if (!isPointerOnTerrain)
{
_isDragging &= lmb.isPressed;
_isValid = false;
return;
}
if (!_isDragging)
{
_startTile = _tileSpace.WorldToTile(position);
_endTile = _startTile;
}
_isDragging |= _pointerService.TryConsumeLeftClick();
if (_isDragging && !_pointerService.IsPointerOverUI) _endTile = _tileSpace.WorldToTile(position);
_isValid = Validate(ref _startTile, ref _endTile);
UpdateAffectedTiles(_isValid, _startTile, _endTile);
if (lmb.wasReleasedThisFrame)
{
if (_isValid && !_pointerService.IsPointerOverUI) DoTool(_startTile, _endTile);
_isDragging = false;
}
}
protected abstract bool Validate(ref int2 startTile, ref int2 endTile);
protected abstract void UpdateAffectedTiles(bool isValid, int2 startTile, int2 endTile);
protected abstract void DoTool(int2 startTile, int2 endTile);
public override void GetDeleteGameObjectsPreviewInfo(out DeletedGameObjectsFilter filter, out TileRect rect)
{
if (!_isValid)
{
filter = DeletedGameObjectsFilter.None;
rect = TileRect.Empty;
return;
}
filter = DeletedGameObjectsFilter;
rect = new TileRect(_startTile, _endTile);
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.Collections.Generic;
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public abstract class EditTool : IDisposable
{
protected EditTool(IServiceLocator serviceLocator)
{
ServiceLocator = serviceLocator;
}
protected IServiceLocator ServiceLocator { get; }
public List<(int2, TileHighlightType)> AffectedTiles { get; } = new();
public virtual UniTask InitializeAsync()
{
return UniTask.CompletedTask;
}
public virtual void Dispose()
{
}
public virtual void OnEnabled()
{
}
public virtual void OnDisabled()
{
}
public virtual void Update()
{
}
public virtual void GetDeleteGameObjectsPreviewInfo(out DeletedGameObjectsFilter filter, out TileRect rect)
{
filter = DeletedGameObjectsFilter.None;
rect = TileRect.Empty;
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using Cysharp.Threading.Tasks;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class EditingState : IDisposable
{
public BuildTool BuildTool { get; set; }
public DeleteTool DeleteTool { get; set; }
public RoadTool RoadTool { get; set; }
public EditTool ActiveTool { get; set; }
public async UniTask InitializeAsync()
{
await BuildTool.InitializeAsync();
await DeleteTool.InitializeAsync();
await RoadTool.InitializeAsync();
}
public void Dispose()
{
BuildTool.Dispose();
DeleteTool.Dispose();
RoadTool.Dispose();
}
}
}

View File

@@ -0,0 +1,151 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Pool;
using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider;
namespace DanieleMarotta.RiversongCodeShowcase
{
[RequiresWorldReadyForUpdate]
public class EditingStateGameSystem : GameSystem, IServiceProvider, IInitializable, IDisposable, IUpdatable, IEditingService
{
[InjectService]
private ICancelAction _cancelAction;
[InjectService]
private WorldRenderingState _renderingState;
[InjectService]
private GameConfig _config;
[InjectService]
private World _world;
[InjectService]
private ISignalBus _signalBus;
[InjectService]
private MaterialReplacementCache _materialReplacementCache;
public EditingStateGameSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public EditingState EditingState { get; private set; }
public event Action<EditTool> ActiveToolChanged;
public void RegisterServices(IServiceLocator serviceLocator)
{
serviceLocator.RegisterService<IEditingService>(this);
EditingState = new EditingState();
serviceLocator.RegisterService(EditingState);
}
public async UniTask InitializeAsync()
{
EditingState.BuildTool = new BuildTool(ServiceLocator);
EditingState.DeleteTool = new DeleteTool(ServiceLocator);
EditingState.RoadTool = new RoadTool(ServiceLocator);
await EditingState.InitializeAsync();
_cancelAction.AddHandler(
(int)CancelActions.CancelEditTool,
_ =>
{
if (EditingState.ActiveTool == null) return false;
DeactivateTool();
return true;
});
}
public void Dispose()
{
EditingState?.Dispose();
EditingState = null;
}
public void ActivateTool(EditTool tool)
{
EditingState.ActiveTool?.OnDisabled();
EditingState.ActiveTool = tool;
EditingState.ActiveTool?.OnEnabled();
ActiveToolChanged?.Invoke(EditingState.ActiveTool);
}
public void DeactivateTool()
{
ActivateTool(null);
}
public void Update()
{
UpdateActiveTool();
UpdateHighlightedGameObjects();
}
private void UpdateActiveTool()
{
if (EditingState.ActiveTool == null) return;
EditingState.ActiveTool.Update();
UpdateAffectedTiles();
UpdateDeletedGameObjects();
}
private void UpdateAffectedTiles()
{
foreach (var (tile, type) in EditingState.ActiveTool.AffectedTiles)
{
if (_world.BlockMap.IsBlocked(tile, BlockReason.InvalidElevation)) continue;
Color32 color;
var config = _config.UI.TileHighlight;
switch (type)
{
case TileHighlightType.ValidTile:
color = config.ValidColor;
break;
case TileHighlightType.InvalidTile:
color = config.InvalidColor;
break;
case TileHighlightType.DeletePreview:
color = config.DeletePreviewColor;
break;
default:
color = Color.magenta;
break;
}
_renderingState.TileHighlight.AddTile(tile, color);
}
}
private void UpdateDeletedGameObjects()
{
EditingState.ActiveTool.GetDeleteGameObjectsPreviewInfo(out var filter, out var rect);
if (filter == DeletedGameObjectsFilter.None) return;
using var gameObjectsScope = ListPool<GameObject>.Get(out var gameObjects);
_signalBus.Raise(new CollectDeletedGameObjectsSignal(filter, rect, gameObjects));
_materialReplacementCache.ReplaceMaterials(gameObjects, _config.UI.DeletedGameObjectsMaterial);
}
private void UpdateHighlightedGameObjects()
{
using var gameObjectsScope = ListPool<GameObject>.Get(out var gameObjects);
_signalBus.Raise(new CollectHighlightedGameObjectsSignal(gameObjects));
_materialReplacementCache.ReplaceMaterials(gameObjects, _config.UI.HighlightedGameObjectsMaterial);
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IEditingService
{
EditingState EditingState { get; }
event Action<EditTool> ActiveToolChanged;
void ActivateTool(EditTool tool);
void DeactivateTool();
}
}

View File

@@ -0,0 +1,50 @@
using Cysharp.Threading.Tasks;
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
public class RoadTool : DragTool
{
private IEditToolValidatorService _validator;
private IRoadFactory _roadFactory;
public RoadTool(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
protected override DeletedGameObjectsFilter DeletedGameObjectsFilter => DeletedGameObjectsFilter.RawResources;
public override async UniTask InitializeAsync()
{
await base.InitializeAsync();
_validator = ServiceLocator.GetService<IEditToolValidatorService>();
_roadFactory = ServiceLocator.GetService<IRoadFactory>();
}
protected override bool Validate(ref int2 startTile, ref int2 endTile)
{
var dx = endTile.x - startTile.x;
var dy = endTile.y - startTile.y;
if (math.abs(dx) >= math.abs(dy))
endTile = new int2(endTile.x, startTile.y);
else
endTile = new int2(startTile.x, endTile.y);
return _validator.DoCommonValidation(new TileRect(startTile, endTile), BlockReason.CannotBuildRoad) == EditToolValidationResult.Success;
}
protected override void UpdateAffectedTiles(bool isValid, int2 startTile, int2 endTile)
{
var type = isValid ? TileHighlightType.ValidTile : TileHighlightType.InvalidTile;
foreach (var tile in TileRange.From(startTile, endTile)) AffectedTiles.Add((tile, type));
}
protected override void DoTool(int2 startTile, int2 endTile)
{
_roadFactory.CreateRoad(startTile, endTile);
}
}
}

View File

@@ -0,0 +1,13 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum EditToolValidationResult
{
Success,
BlockedTile,
CanOnlyBePlacedNearWater,
CanOnlyBePlacedOnFertileGround
}
}

View File

@@ -0,0 +1,56 @@
using Unity.Mathematics;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IEditToolValidatorService))]
public class EditToolValidatorSystem : GameSystem, IEditToolValidatorService
{
[InjectService]
private GameConfig _config;
[InjectService]
private World _world;
public EditToolValidatorSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public EditToolValidationResult DoCommonValidation(TileRect rect, BlockReason blockReason)
{
foreach (var tile in TileRange.From(rect))
if (!DoCommonTileValidation(tile, blockReason))
return EditToolValidationResult.BlockedTile;
return EditToolValidationResult.Success;
}
private bool DoCommonTileValidation(int2 tile, BlockReason blockReason)
{
return _world.Contains(tile) && !_world.BlockMap.IsBlocked(tile, blockReason);
}
public EditToolValidationResult ValidateBuildingPlacementRules(TileRect rect, BuildingDefinition buildingDefinition)
{
if (buildingDefinition.NearWater)
{
var waterMap = _world.WaterMap;
foreach (var tile in TileRange.From(rect))
if (!waterMap.IsNearWater(tile))
return EditToolValidationResult.CanOnlyBePlacedNearWater;
}
if (buildingDefinition.RequiresFertileTile)
{
foreach (var tile in TileRange.From(rect))
{
var fertility = _world.Fertility.GetValue(tile);
if (fertility.MaxFertility > 0) return EditToolValidationResult.Success;
}
return EditToolValidationResult.CanOnlyBePlacedOnFertileGround;
}
return EditToolValidationResult.Success;
}
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IEditToolValidatorService
{
EditToolValidationResult DoCommonValidation(TileRect rect, BlockReason blockReason);
public EditToolValidationResult ValidateBuildingPlacementRules(TileRect rect, BuildingDefinition buildingDefinition);
}
}

View File

@@ -0,0 +1,64 @@
using System;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.InputSystem;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IGameSpeed))]
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
[RequiresWorldReadyForUpdate]
[UpdateBefore(typeof(WorldTimeSystem))]
public class GameSpeedSystem : GameSystem, IGameSpeed, IInitializable, IUpdatable, IDisposable
{
private const float NormalSpeed = 1;
private const float FastSpeed = 2;
private const float VeryFastSpeed = 4;
public GameSpeedSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public int SpeedLevel { get; private set; }
public UniTask InitializeAsync()
{
SetSpeedLevel(0);
return UniTask.CompletedTask;
}
public void Dispose()
{
SetSpeedLevel(0);
}
public void Update()
{
if (Keyboard.current.digit1Key.wasPressedThisFrame)
SetSpeedLevel(0);
else if (Keyboard.current.digit2Key.wasPressedThisFrame)
SetSpeedLevel(1);
else if (Keyboard.current.digit3Key.wasPressedThisFrame) SetSpeedLevel(2);
Time.timeScale = GetTimeScale();
}
public void SetSpeedLevel(int speedLevel)
{
SpeedLevel = speedLevel;
}
private float GetTimeScale()
{
return SpeedLevel switch
{
1 => FastSpeed,
2 => VeryFastSpeed,
_ => NormalSpeed
};
}
}
}

View File

@@ -0,0 +1,9 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IGameSpeed
{
int SpeedLevel { get; }
void SetSpeedLevel(int speedLevel);
}
}

View File

@@ -0,0 +1,21 @@
using UnityEngine;
using UnityEngine.Pool;
namespace DanieleMarotta.RiversongCodeShowcase
{
public static class GameObjectLayers
{
public static readonly int Default = LayerMask.NameToLayer("Default");
public static readonly int Terrain = LayerMask.NameToLayer("Terrain");
public static readonly int IgnoreAoE = LayerMask.NameToLayer("Ignore AoE");
public static void SetLayerRecursively<T>(this GameObject gameObject, int layer, bool includeInactive = false) where T : Component
{
using var componentsScope = ListPool<T>.Get(out var components);
gameObject.GetComponentsInChildren(includeInactive, components);
foreach (var component in components) component.gameObject.layer = layer;
}
}
}

View File

@@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using UnityEngine.InputSystem;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(ICancelAction))]
[GameSystemGroup(typeof(LateGameSystemGroup))]
public class CancelActionSystem : GameSystem, IUpdatable, ICancelAction
{
private readonly List<(int, Func<CancelActionType, bool>)> _handlers = new();
private Comparison<(int, Func<CancelActionType, bool>)> _handlerComparison;
public CancelActionSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public void Update()
{
if (!TryGetCancelActionType(out var cancelActionType)) return;
foreach (var (_, handler) in _handlers)
if (handler.Invoke(cancelActionType))
return;
}
public void AddHandler(int priority, Func<CancelActionType, bool> cancelAction)
{
_handlerComparison ??= (handler, otherHandler) =>
{
var (handlerPriority, _) = handler;
var (otherHandlerPriority, _) = otherHandler;
return otherHandlerPriority.CompareTo(handlerPriority);
};
_handlers.Add((priority, cancelAction));
_handlers.Sort(_handlerComparison);
}
private static bool TryGetCancelActionType(out CancelActionType cancelActionType)
{
if (Keyboard.current.escapeKey.wasPressedThisFrame)
{
cancelActionType = CancelActionType.EscapeKey;
return true;
}
if (Mouse.current.rightButton.wasPressedThisFrame)
{
cancelActionType = CancelActionType.RightMouseButton;
return true;
}
cancelActionType = CancelActionType.None;
return false;
}
}
}

View File

@@ -0,0 +1,11 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum CancelActionType
{
None,
EscapeKey,
RightMouseButton
}
}

View File

@@ -0,0 +1,15 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public enum CancelActions
{
Invalid,
PauseMenu,
CloseBuildMenu,
CancelEditTool,
CancelSelection
}
}

View File

@@ -0,0 +1,9 @@
using System;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface ICancelAction
{
void AddHandler(int priority, Func<CancelActionType, bool> cancelAction);
}
}

View File

@@ -0,0 +1,13 @@
using UnityEngine;
namespace DanieleMarotta.RiversongCodeShowcase
{
public interface IPointerService
{
bool IsPointerOverUI { get; }
bool TryGetPositionOnTerrain(out Vector3 position);
bool TryConsumeLeftClick(int maxClickCount = 0);
}
}

View File

@@ -0,0 +1,82 @@
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.UIElements;
namespace DanieleMarotta.RiversongCodeShowcase
{
[Service(typeof(IPointerService))]
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
public class PointerSystem : GameSystem, IUpdatable, IPointerService
{
[InjectService]
private IScene _scene;
[InjectService]
private GameConfig _config;
[InjectService]
private ITileSpace _tileSpace;
[InjectService]
private World _world;
[InjectService]
private UIService _uiService;
private int _leftButtonClickCount;
private Vector3? _positionOnTerrain;
public PointerSystem(IServiceLocator serviceLocator) : base(serviceLocator)
{
}
public bool IsPointerOverUI { get; private set; }
public void Update()
{
_leftButtonClickCount = 0;
UpdateIsPointerOverUIFlag();
UpdatePositionOnTerrain();
}
private void UpdateIsPointerOverUIFlag()
{
var panel = _uiService.UIRoot.RootVisualElement.panel;
var position = Mouse.current.position.ReadValue();
position.y = Screen.height - position.y;
position = RuntimePanelUtils.ScreenToPanel(panel, position);
IsPointerOverUI = panel.Pick(position) != null;
}
private void UpdatePositionOnTerrain()
{
_positionOnTerrain = null;
var ray = _scene.MainCamera.ScreenPointToRay(Mouse.current.position.ReadValue());
if (Physics.Raycast(ray, out var hit, float.PositiveInfinity))
{
if (!Mathf.Approximately(Vector3.Dot(hit.normal, Vector3.up), 1)) return;
if (_tileSpace.GetElevation(hit.point.y) != _config.GeneralSettings.BaseElevation) return;
_positionOnTerrain = hit.point;
}
}
public bool TryGetPositionOnTerrain(out Vector3 position)
{
position = _positionOnTerrain ?? Vector3.zero;
return _positionOnTerrain != null;
}
public bool TryConsumeLeftClick(int maxClickCount = 0)
{
return !IsPointerOverUI && Mouse.current.leftButton.wasPressedThisFrame && _leftButtonClickCount++ <= maxClickCount;
}
}
}

View File

@@ -0,0 +1,12 @@
namespace DanieleMarotta.RiversongCodeShowcase
{
public struct OnboardingEventCompleted
{
public OnboardingEvents Event;
public OnboardingEventCompleted(OnboardingEvents @event)
{
Event = @event;
}
}
}

Some files were not shown because too many files have changed in this diff Show More