riversong code showcase
This commit is contained in:
24
README.md
Normal file
24
README.md
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Riversong Code Showcase
|
||||||
|
|
||||||
|
This repository is a client-facing code sample for **Riversong**, a **Unity 6** project.
|
||||||
|
|
||||||
|
Its purpose is to provide a focused view of my engineering work, architecture, and coding style without including the full production project.
|
||||||
|
|
||||||
|
## What Is Included
|
||||||
|
|
||||||
|
- Selected C# source code only
|
||||||
|
- Gameplay, UI, systems, and supporting engine/framework code used in the project
|
||||||
|
|
||||||
|
## What Is Not Included
|
||||||
|
|
||||||
|
- Unity assets
|
||||||
|
- Scenes, prefabs, materials, and other content files
|
||||||
|
- Project settings and the full runnable Unity project
|
||||||
|
|
||||||
|
This repository is intentionally trimmed down so clients can review the code clearly and without unrelated project files.
|
||||||
|
|
||||||
|
## Project Page
|
||||||
|
|
||||||
|
The public itch.io page for the project is here:
|
||||||
|
|
||||||
|
[Riversong on itch.io](https://danis-workshop.itch.io/riversong)
|
||||||
35
Source/Engine/Collections/IMultiDictionary.cs
Normal file
35
Source/Engine/Collections/IMultiDictionary.cs
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IMultiDictionary<K, V, C> : IEnumerable<KeyValuePair<K, V>> where C : ICollection<V>
|
||||||
|
{
|
||||||
|
C this[K key] { get; }
|
||||||
|
|
||||||
|
Dictionary<K, C>.KeyCollection Keys { get; }
|
||||||
|
|
||||||
|
int KeyCount { get; }
|
||||||
|
|
||||||
|
int ValueCount { get; }
|
||||||
|
|
||||||
|
int Count(K key);
|
||||||
|
|
||||||
|
bool Add(K key, V value);
|
||||||
|
|
||||||
|
bool Remove(K key, V value);
|
||||||
|
|
||||||
|
bool Remove(V value);
|
||||||
|
|
||||||
|
void Clear(K key);
|
||||||
|
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
bool ContainsKey(K key);
|
||||||
|
|
||||||
|
bool ContainsValue(K key, V value);
|
||||||
|
|
||||||
|
bool ContainsValue(V value);
|
||||||
|
|
||||||
|
bool TryGetValues(K key, out C values);
|
||||||
|
}
|
||||||
|
}
|
||||||
22
Source/Engine/Collections/ListMultiDictionary.cs
Normal file
22
Source/Engine/Collections/ListMultiDictionary.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class ListMultiDictionary<K, V> : MultiDictionary<K, V, List<V>>
|
||||||
|
{
|
||||||
|
public ListMultiDictionary(IEqualityComparer<K> keyComparer = null) : base(keyComparer)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override List<V> CreateCollection()
|
||||||
|
{
|
||||||
|
return new List<V>();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override bool AddToCollection(V value, List<V> collection)
|
||||||
|
{
|
||||||
|
collection.Add(value);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
Source/Engine/Collections/MultiDictionary.cs
Normal file
137
Source/Engine/Collections/MultiDictionary.cs
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public abstract class MultiDictionary<K, V, C> : IMultiDictionary<K, V, C> where C : ICollection<V>
|
||||||
|
{
|
||||||
|
private readonly Dictionary<K, C> _collections;
|
||||||
|
|
||||||
|
protected MultiDictionary(IEqualityComparer<K> keyComparer = null)
|
||||||
|
{
|
||||||
|
_collections = keyComparer != null ? new Dictionary<K, C>(keyComparer) : new Dictionary<K, C>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public C this[K key] => _collections[key];
|
||||||
|
|
||||||
|
public Dictionary<K, C>.KeyCollection Keys => _collections.Keys;
|
||||||
|
|
||||||
|
public int KeyCount => _collections.Count;
|
||||||
|
|
||||||
|
public int ValueCount { get; private set; }
|
||||||
|
|
||||||
|
protected abstract C CreateCollection();
|
||||||
|
|
||||||
|
protected abstract bool AddToCollection(V value, C collection);
|
||||||
|
|
||||||
|
public int Count(K key)
|
||||||
|
{
|
||||||
|
return _collections.TryGetValue(key, out var collection) ? collection.Count : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Add(K key, V value)
|
||||||
|
{
|
||||||
|
if (!_collections.TryGetValue(key, out var collection))
|
||||||
|
{
|
||||||
|
collection = CreateCollection();
|
||||||
|
_collections.Add(key, collection);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AddToCollection(value, collection))
|
||||||
|
{
|
||||||
|
ValueCount++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(K key, V value)
|
||||||
|
{
|
||||||
|
if (_collections.TryGetValue(key, out var collection) && collection.Remove(value))
|
||||||
|
{
|
||||||
|
ValueCount--;
|
||||||
|
if (collection.Count == 0) _collections.Remove(key);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Remove(V value)
|
||||||
|
{
|
||||||
|
var valueRemoved = false;
|
||||||
|
var removeCollection = false;
|
||||||
|
K collectionKeyToRemove = default;
|
||||||
|
foreach (var kvp in _collections)
|
||||||
|
{
|
||||||
|
var collection = kvp.Value;
|
||||||
|
if (collection.Remove(value))
|
||||||
|
{
|
||||||
|
valueRemoved = true;
|
||||||
|
ValueCount--;
|
||||||
|
if (collection.Count == 0)
|
||||||
|
{
|
||||||
|
removeCollection = true;
|
||||||
|
collectionKeyToRemove = kvp.Key;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (removeCollection) _collections.Remove(collectionKeyToRemove);
|
||||||
|
return valueRemoved;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear(K key)
|
||||||
|
{
|
||||||
|
if (_collections.TryGetValue(key, out var collection))
|
||||||
|
{
|
||||||
|
ValueCount -= collection.Count;
|
||||||
|
collection.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_collections.Clear();
|
||||||
|
ValueCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsKey(K key)
|
||||||
|
{
|
||||||
|
return _collections.ContainsKey(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsValue(K key, V value)
|
||||||
|
{
|
||||||
|
return _collections.TryGetValue(key, out var collection) && collection.Contains(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool ContainsValue(V value)
|
||||||
|
{
|
||||||
|
foreach (var collection in _collections.Values)
|
||||||
|
if (collection.Contains(value))
|
||||||
|
return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryGetValues(K key, out C values)
|
||||||
|
{
|
||||||
|
return _collections.TryGetValue(key, out values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
|
||||||
|
{
|
||||||
|
foreach (var kvp in _collections)
|
||||||
|
{
|
||||||
|
var key = kvp.Key;
|
||||||
|
var collection = kvp.Value;
|
||||||
|
foreach (var value in collection) yield return new KeyValuePair<K, V>(key, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class DisableDiscoveryAttribute : Attribute
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Source/Engine/Core/Attributes/GameSystemGroupAttribute.cs
Normal file
15
Source/Engine/Core/Attributes/GameSystemGroupAttribute.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class)]
|
||||||
|
public class GameSystemGroupAttribute : Attribute
|
||||||
|
{
|
||||||
|
public GameSystemGroupAttribute(Type systemGroupType)
|
||||||
|
{
|
||||||
|
SystemGroupType = systemGroupType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type SystemGroupType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Source/Engine/Core/Attributes/InjectServiceAttribute.cs
Normal file
15
Source/Engine/Core/Attributes/InjectServiceAttribute.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Field)]
|
||||||
|
public class InjectServiceAttribute : Attribute
|
||||||
|
{
|
||||||
|
public InjectServiceAttribute(Type serviceType = null)
|
||||||
|
{
|
||||||
|
ServiceType = serviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ServiceType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Source/Engine/Core/Attributes/ServiceAttribute.cs
Normal file
15
Source/Engine/Core/Attributes/ServiceAttribute.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||||
|
public class ServiceAttribute : Attribute
|
||||||
|
{
|
||||||
|
public ServiceAttribute(Type serviceType = null)
|
||||||
|
{
|
||||||
|
ServiceType = serviceType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type ServiceType { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
57
Source/Engine/Core/Attributes/SortingAttributes.cs
Normal file
57
Source/Engine/Core/Attributes/SortingAttributes.cs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
|
||||||
|
public class SortingAttribute : Attribute
|
||||||
|
{
|
||||||
|
protected SortingAttribute(Type systemType)
|
||||||
|
{
|
||||||
|
SystemType = systemType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type SystemType { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InitializeAfterAttribute : SortingAttribute
|
||||||
|
{
|
||||||
|
public InitializeAfterAttribute(Type systemType) : base(systemType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class InitializeBeforeAttribute : SortingAttribute
|
||||||
|
{
|
||||||
|
public InitializeBeforeAttribute(Type systemType) : base(systemType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateAfterAttribute : SortingAttribute
|
||||||
|
{
|
||||||
|
public UpdateAfterAttribute(Type systemType) : base(systemType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class UpdateBeforeAttribute : SortingAttribute
|
||||||
|
{
|
||||||
|
public UpdateBeforeAttribute(Type systemType) : base(systemType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DisposeAfterAttribute : SortingAttribute
|
||||||
|
{
|
||||||
|
public DisposeAfterAttribute(Type systemType) : base(systemType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DisposeBeforeAttribute : SortingAttribute
|
||||||
|
{
|
||||||
|
public DisposeBeforeAttribute(Type systemType) : base(systemType)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
155
Source/Engine/Core/EngineRunner.cs
Normal file
155
Source/Engine/Core/EngineRunner.cs
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class EngineRunner : MonoBehaviour, IEngine
|
||||||
|
{
|
||||||
|
private ServiceLocator _serviceLocator;
|
||||||
|
|
||||||
|
private RootGameSystemGroup _rootSystemGroup;
|
||||||
|
|
||||||
|
private List<GameSystemGroup> _systemGroups = new();
|
||||||
|
|
||||||
|
private Dictionary<Type, GameSystemGroup> _systemGroupsByType = new();
|
||||||
|
|
||||||
|
private EngineUpdateFilter _updateFilter = new();
|
||||||
|
|
||||||
|
public bool IsInitialized { get; private set; }
|
||||||
|
|
||||||
|
public List<IGameSystem> Systems { get; } = new();
|
||||||
|
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
StartAsync().Forget(Debug.LogException);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTask StartAsync()
|
||||||
|
{
|
||||||
|
Debug.Log("Engine started");
|
||||||
|
|
||||||
|
_serviceLocator = new ServiceLocator();
|
||||||
|
|
||||||
|
_serviceLocator.RegisterService(typeof(IEngine), this);
|
||||||
|
|
||||||
|
var serviceProviders = GetComponentsInChildren<IServiceProvider>();
|
||||||
|
foreach (var serviceProvider in serviceProviders) serviceProvider.RegisterServices(_serviceLocator);
|
||||||
|
|
||||||
|
await UniTask.NextFrame();
|
||||||
|
|
||||||
|
_rootSystemGroup = new RootGameSystemGroup();
|
||||||
|
_systemGroups.Add(_rootSystemGroup);
|
||||||
|
_systemGroupsByType.Add(_rootSystemGroup.GetType(), _rootSystemGroup);
|
||||||
|
|
||||||
|
Debug.Log("Systems discovery started");
|
||||||
|
DiscoverSystems();
|
||||||
|
Debug.Log($"Systems discovery completed. Discovered {_rootSystemGroup.Systems.Count} system groups and {Systems.Count} systems");
|
||||||
|
|
||||||
|
await UniTask.NextFrame();
|
||||||
|
|
||||||
|
foreach (var systemGroup in _systemGroups) _serviceLocator.Inject(systemGroup);
|
||||||
|
foreach (var system in Systems) _serviceLocator.Inject(system);
|
||||||
|
|
||||||
|
await UniTask.NextFrame();
|
||||||
|
|
||||||
|
var types = Systems.Select(obj => obj.GetType()).Concat(_systemGroups.Select(obj => obj.GetType())).Distinct().ToList();
|
||||||
|
SystemSorter.InitializeSorters(types);
|
||||||
|
|
||||||
|
await UniTask.NextFrame();
|
||||||
|
|
||||||
|
Debug.Log("Systems initialization started");
|
||||||
|
await InitializeAsync();
|
||||||
|
Debug.Log("Systems initialization completed");
|
||||||
|
|
||||||
|
IsInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DiscoverSystems()
|
||||||
|
{
|
||||||
|
var assemblies = AppDomain.CurrentDomain.GetAssemblies();
|
||||||
|
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
foreach (var type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
if (type.IsAbstract || !type.IsSubclassOf(typeof(GameSystemGroup)) || type.GetCustomAttribute<DisableDiscoveryAttribute>() != null) continue;
|
||||||
|
|
||||||
|
var systemGroup = (GameSystemGroup)Activator.CreateInstance(type);
|
||||||
|
systemGroup.UpdateFilter = _updateFilter;
|
||||||
|
|
||||||
|
_systemGroups.Add(systemGroup);
|
||||||
|
_systemGroupsByType.Add(type, systemGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var systemGroup in _systemGroups)
|
||||||
|
{
|
||||||
|
if (systemGroup == _rootSystemGroup) continue;
|
||||||
|
|
||||||
|
var parent = GetContainingSystemGroup(systemGroup.GetType(), typeof(RootGameSystemGroup));
|
||||||
|
parent.Add(systemGroup);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
foreach (var type in assembly.GetTypes())
|
||||||
|
{
|
||||||
|
if (type.IsAbstract || !type.IsSubclassOf(typeof(GameSystem)) || type.GetCustomAttribute<DisableDiscoveryAttribute>() != null) continue;
|
||||||
|
|
||||||
|
var system = CreateSystem(type);
|
||||||
|
Systems.Add(system);
|
||||||
|
|
||||||
|
var systemGroup = GetContainingSystemGroup(type, typeof(DefaultGameSystemGroup));
|
||||||
|
systemGroup.Add(system);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameSystem CreateSystem(Type systemType)
|
||||||
|
{
|
||||||
|
var system = (GameSystem)Activator.CreateInstance(systemType, _serviceLocator);
|
||||||
|
|
||||||
|
var serviceAttributes = systemType.GetCustomAttributes<ServiceAttribute>();
|
||||||
|
foreach (var serviceAttribute in serviceAttributes)
|
||||||
|
{
|
||||||
|
var serviceType = serviceAttribute?.ServiceType ?? systemType;
|
||||||
|
_serviceLocator.RegisterService(serviceType, system);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (system is IServiceProvider serviceProvider) serviceProvider.RegisterServices(_serviceLocator);
|
||||||
|
|
||||||
|
return system;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GameSystemGroup GetContainingSystemGroup(Type type, Type defaultGroup)
|
||||||
|
{
|
||||||
|
var systemGroupAttribute = type.GetCustomAttribute<GameSystemGroupAttribute>();
|
||||||
|
var systemGroupType = systemGroupAttribute?.SystemGroupType ?? defaultGroup;
|
||||||
|
return _systemGroupsByType[systemGroupType];
|
||||||
|
}
|
||||||
|
|
||||||
|
private async UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
await _rootSystemGroup.InitializeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (!IsInitialized) return;
|
||||||
|
|
||||||
|
_rootSystemGroup.Update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDestroy()
|
||||||
|
{
|
||||||
|
Debug.Log("Systems disposing started");
|
||||||
|
_rootSystemGroup.Dispose();
|
||||||
|
Debug.Log("Systems disposing completed");
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterUpdateFilter(IUpdateFilter filter)
|
||||||
|
{
|
||||||
|
_updateFilter.Filters.Add(filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Source/Engine/Core/EngineUpdateFilter.cs
Normal file
17
Source/Engine/Core/EngineUpdateFilter.cs
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class EngineUpdateFilter : IUpdateFilter
|
||||||
|
{
|
||||||
|
public List<IUpdateFilter> Filters { get; } = new();
|
||||||
|
|
||||||
|
public bool CanUpdate(IUpdatable updatable)
|
||||||
|
{
|
||||||
|
foreach (var updateFilter in Filters)
|
||||||
|
if (!updateFilter.CanUpdate(updatable))
|
||||||
|
return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Source/Engine/Core/GameSystem.cs
Normal file
14
Source/Engine/Core/GameSystem.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public abstract class GameSystem : IGameSystem
|
||||||
|
{
|
||||||
|
protected GameSystem(IServiceLocator serviceLocator)
|
||||||
|
{
|
||||||
|
ServiceLocator = serviceLocator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual string Name => GetType().Name;
|
||||||
|
|
||||||
|
protected IServiceLocator ServiceLocator { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
83
Source/Engine/Core/GameSystemGroup.cs
Normal file
83
Source/Engine/Core/GameSystemGroup.cs
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
using Debug = UnityEngine.Debug;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public abstract class GameSystemGroup : IGameSystem, IInitializable, IUpdatable, IDisposable
|
||||||
|
{
|
||||||
|
private List<IInitializable> _initializables = new();
|
||||||
|
|
||||||
|
private List<IUpdatable> _updatables = new();
|
||||||
|
|
||||||
|
private List<IDisposable> _disposables = new();
|
||||||
|
|
||||||
|
public virtual string Name => GetType().Name;
|
||||||
|
|
||||||
|
public List<IGameSystem> Systems { get; } = new();
|
||||||
|
|
||||||
|
public IUpdateFilter UpdateFilter { get; set; }
|
||||||
|
|
||||||
|
public void Add(IGameSystem system)
|
||||||
|
{
|
||||||
|
Systems.Add(system);
|
||||||
|
if (system is IInitializable initializable) _initializables.Add(initializable);
|
||||||
|
if (system is IUpdatable updatable) _updatables.Add(updatable);
|
||||||
|
if (system is IDisposable disposable) _disposables.Add(disposable);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual async UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
SystemSorter.InitializableSorter.Sort(_initializables);
|
||||||
|
SystemSorter.UpdatableSorter.Sort(_updatables);
|
||||||
|
SystemSorter.DisposableSorter.Sort(_disposables);
|
||||||
|
|
||||||
|
var batches = SystemSorter.InitializableSorter.CreateExecutionBatches(_initializables);
|
||||||
|
foreach (var batch in batches)
|
||||||
|
{
|
||||||
|
var tasks = new UniTask[batch.Count];
|
||||||
|
for (var i = 0; i < batch.Count; i++) tasks[i] = InitializeAndLogAsync(batch[i]);
|
||||||
|
|
||||||
|
await UniTask.WhenAll(tasks);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async UniTask InitializeAndLogAsync(IInitializable initializable)
|
||||||
|
{
|
||||||
|
var startTime = Time.unscaledTime;
|
||||||
|
|
||||||
|
await initializable.InitializeAsync();
|
||||||
|
|
||||||
|
LogInitializationTime(initializable, startTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void LogInitializationTime(IInitializable initializable, float startTime)
|
||||||
|
{
|
||||||
|
if (initializable is GameSystemGroup) return;
|
||||||
|
|
||||||
|
var elapsed = Time.unscaledTime - startTime;
|
||||||
|
|
||||||
|
var log = $"Initialized {((IGameSystem)initializable).Name} in {(int)(elapsed * 1000)} ms";
|
||||||
|
if (elapsed > 0.3f) log = $"<color=yellow>{log}</color>";
|
||||||
|
|
||||||
|
Debug.Log(log);
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Update()
|
||||||
|
{
|
||||||
|
foreach (var updatable in _updatables)
|
||||||
|
{
|
||||||
|
if (UpdateFilter != null && !UpdateFilter.CanUpdate(updatable)) continue;
|
||||||
|
|
||||||
|
updatable.Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual void Dispose()
|
||||||
|
{
|
||||||
|
foreach (var disposable in _disposables) disposable.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Source/Engine/Core/Groups/DefaultGameSystemGroup.cs
Normal file
6
Source/Engine/Core/Groups/DefaultGameSystemGroup.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class DefaultGameSystemGroup : GameSystemGroup
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Source/Engine/Core/Groups/EarlyGameSystemGroup.cs
Normal file
9
Source/Engine/Core/Groups/EarlyGameSystemGroup.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[InitializeBefore(typeof(DefaultGameSystemGroup))]
|
||||||
|
[UpdateBefore(typeof(DefaultGameSystemGroup))]
|
||||||
|
[DisposeBefore(typeof(DefaultGameSystemGroup))]
|
||||||
|
public class EarlyGameSystemGroup : GameSystemGroup
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Source/Engine/Core/Groups/LateGameSystemGroup.cs
Normal file
9
Source/Engine/Core/Groups/LateGameSystemGroup.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[InitializeAfter(typeof(DefaultGameSystemGroup))]
|
||||||
|
[UpdateAfter(typeof(DefaultGameSystemGroup))]
|
||||||
|
[DisposeAfter(typeof(DefaultGameSystemGroup))]
|
||||||
|
public class LateGameSystemGroup : GameSystemGroup
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Engine/Core/Groups/RootGameSystemGroup.cs
Normal file
7
Source/Engine/Core/Groups/RootGameSystemGroup.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[DisableDiscovery]
|
||||||
|
public class RootGameSystemGroup : GameSystemGroup
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Source/Engine/Core/IEngine.cs
Normal file
13
Source/Engine/Core/IEngine.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IEngine
|
||||||
|
{
|
||||||
|
public bool IsInitialized { get; }
|
||||||
|
|
||||||
|
public List<IGameSystem> Systems { get; }
|
||||||
|
|
||||||
|
public void RegisterUpdateFilter(IUpdateFilter filter);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Engine/Core/IGameSystem.cs
Normal file
7
Source/Engine/Core/IGameSystem.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IGameSystem
|
||||||
|
{
|
||||||
|
public string Name { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Source/Engine/Core/IInitializable.cs
Normal file
9
Source/Engine/Core/IInitializable.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IInitializable
|
||||||
|
{
|
||||||
|
UniTask InitializeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Source/Engine/Core/IServiceLocator.cs
Normal file
13
Source/Engine/Core/IServiceLocator.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IServiceLocator
|
||||||
|
{
|
||||||
|
void RegisterService(Type serviceType, object service);
|
||||||
|
|
||||||
|
object GetService(Type serviceType);
|
||||||
|
|
||||||
|
void Inject(object target);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Engine/Core/IServiceProvider.cs
Normal file
7
Source/Engine/Core/IServiceProvider.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IServiceProvider
|
||||||
|
{
|
||||||
|
void RegisterServices(IServiceLocator serviceLocator);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Engine/Core/IUpdatable.cs
Normal file
7
Source/Engine/Core/IUpdatable.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IUpdatable
|
||||||
|
{
|
||||||
|
void Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Engine/Core/IUpdateFilter.cs
Normal file
7
Source/Engine/Core/IUpdateFilter.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IUpdateFilter
|
||||||
|
{
|
||||||
|
bool CanUpdate(IUpdatable updatable);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
Source/Engine/Core/ServiceLocator.cs
Normal file
53
Source/Engine/Core/ServiceLocator.cs
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class ServiceLocator : IServiceLocator
|
||||||
|
{
|
||||||
|
private Dictionary<Type, object> _services = new();
|
||||||
|
|
||||||
|
public void RegisterService(Type serviceType, object service)
|
||||||
|
{
|
||||||
|
_services.Add(serviceType, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
public object GetService(Type serviceType)
|
||||||
|
{
|
||||||
|
_services.TryGetValue(serviceType, out var service);
|
||||||
|
return service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Inject(object target)
|
||||||
|
{
|
||||||
|
var type = target.GetType();
|
||||||
|
|
||||||
|
while (type != typeof(object))
|
||||||
|
{
|
||||||
|
var bindingAttr = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||||
|
var fields = type.GetFields(bindingAttr);
|
||||||
|
|
||||||
|
foreach (var field in fields)
|
||||||
|
{
|
||||||
|
var serviceAttribute = field.GetCustomAttribute<InjectServiceAttribute>();
|
||||||
|
if (serviceAttribute == null) continue;
|
||||||
|
|
||||||
|
var serviceType = serviceAttribute.ServiceType ?? field.FieldType;
|
||||||
|
|
||||||
|
var service = GetService(serviceType);
|
||||||
|
if (service == null)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Could not resolve service of type {serviceType} when injecting {type}");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
field.SetValue(target, service);
|
||||||
|
}
|
||||||
|
|
||||||
|
type = type.BaseType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
15
Source/Engine/Core/ServiceLocatorExtensions.cs
Normal file
15
Source/Engine/Core/ServiceLocatorExtensions.cs
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public static class ServiceLocatorExtensions
|
||||||
|
{
|
||||||
|
public static void RegisterService<T>(this IServiceLocator serviceLocator, T service)
|
||||||
|
{
|
||||||
|
serviceLocator.RegisterService(typeof(T), service);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T GetService<T>(this IServiceLocator serviceLocator)
|
||||||
|
{
|
||||||
|
return (T)serviceLocator.GetService(typeof(T));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
93
Source/Engine/Core/SystemSorter.cs
Normal file
93
Source/Engine/Core/SystemSorter.cs
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class SystemSorter
|
||||||
|
{
|
||||||
|
public static DependencySorter<InitializeBeforeAttribute, InitializeAfterAttribute, IInitializable> InitializableSorter { get; } = new();
|
||||||
|
|
||||||
|
public static DependencySorter<UpdateBeforeAttribute, UpdateAfterAttribute, IUpdatable> UpdatableSorter { get; } = new();
|
||||||
|
|
||||||
|
public static DependencySorter<DisposeBeforeAttribute, DisposeAfterAttribute, IDisposable> DisposableSorter { get; } = new();
|
||||||
|
|
||||||
|
public static void InitializeSorters(List<Type> types)
|
||||||
|
{
|
||||||
|
InitializableSorter.Initialize(types);
|
||||||
|
UpdatableSorter.Initialize(types);
|
||||||
|
DisposableSorter.Initialize(types);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public class DependencySorter<TBeforeAttr, TAfterAttr, TObj> where TBeforeAttr : SortingAttribute where TAfterAttr : SortingAttribute
|
||||||
|
{
|
||||||
|
private Dictionary<Type, int> _indexLookup = new();
|
||||||
|
|
||||||
|
private Dictionary<Type, List<Type>> _dependencyLookup = new();
|
||||||
|
|
||||||
|
private Comparison<TObj> _comparison;
|
||||||
|
|
||||||
|
public DependencySorter()
|
||||||
|
{
|
||||||
|
_comparison = (lhs, rhs) => _indexLookup[lhs.GetType()].CompareTo(_indexLookup[rhs.GetType()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Initialize(List<Type> types)
|
||||||
|
{
|
||||||
|
var dependencies = new ListMultiDictionary<Type, Type>();
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
var beforeAttributes = type.GetCustomAttributes<TBeforeAttr>(true);
|
||||||
|
foreach (var beforeAttribute in beforeAttributes) dependencies.Add(beforeAttribute.SystemType, type);
|
||||||
|
|
||||||
|
var afterAttributes = type.GetCustomAttributes<TAfterAttr>(true);
|
||||||
|
foreach (var afterAttribute in afterAttributes) dependencies.Add(type, afterAttribute.SystemType);
|
||||||
|
}
|
||||||
|
|
||||||
|
_dependencyLookup = types.ToDictionary(type => type, type => dependencies.TryGetValues(type, out var d) ? d : new List<Type>());
|
||||||
|
_indexLookup.Clear();
|
||||||
|
|
||||||
|
var sortedTypes = TopologicalSort<Type>.Default.Sort(types, t => dependencies.TryGetValues(t, out var d) ? d : null);
|
||||||
|
|
||||||
|
var index = 0;
|
||||||
|
foreach (var type in sortedTypes) _indexLookup.Add(type, index++);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Sort(List<TObj> list)
|
||||||
|
{
|
||||||
|
list.Sort(_comparison);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<List<TObj>> CreateExecutionBatches(List<TObj> list)
|
||||||
|
{
|
||||||
|
var batches = new List<List<TObj>>();
|
||||||
|
|
||||||
|
if (list.Count == 0) return batches;
|
||||||
|
|
||||||
|
var remainingTypes = new HashSet<Type>(list.Select(item => item.GetType()));
|
||||||
|
while (remainingTypes.Count > 0)
|
||||||
|
{
|
||||||
|
var batch = new List<TObj>();
|
||||||
|
foreach (var item in list)
|
||||||
|
{
|
||||||
|
var itemType = item.GetType();
|
||||||
|
if (!remainingTypes.Contains(itemType)) continue;
|
||||||
|
|
||||||
|
if (_dependencyLookup.TryGetValue(itemType, out var dependencies) && dependencies.Exists(remainingTypes.Contains)) continue;
|
||||||
|
|
||||||
|
batch.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.Count == 0) throw new InvalidOperationException($"Unable to build execution batch for {typeof(TObj).Name}");
|
||||||
|
|
||||||
|
batches.Add(batch);
|
||||||
|
|
||||||
|
foreach (var item in batch) remainingTypes.Remove(item.GetType());
|
||||||
|
}
|
||||||
|
|
||||||
|
return batches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
9
Source/Engine/GameData/GameDataAsset.cs
Normal file
9
Source/Engine/GameData/GameDataAsset.cs
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public abstract class GameDataAsset : ScriptableObject, IGameDataRuntimeId
|
||||||
|
{
|
||||||
|
public int RuntimeId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Source/Engine/GameData/GameDatabase.cs
Normal file
61
Source/Engine/GameData/GameDatabase.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class GameDatabase : IGameDatabase
|
||||||
|
{
|
||||||
|
private Dictionary<int, object> _idLookup = new();
|
||||||
|
|
||||||
|
private Dictionary<Type, object> _typeLookup = new();
|
||||||
|
|
||||||
|
public void Add<T>(int id, T asset) where T : class
|
||||||
|
{
|
||||||
|
_idLookup.Add(id, asset);
|
||||||
|
|
||||||
|
InvokeAddToTypeLookupWithType(asset, asset.GetType());
|
||||||
|
|
||||||
|
if (asset is IGameDataRuntimeId runtimeId) runtimeId.RuntimeId = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InvokeAddToTypeLookupWithType(object asset, Type type)
|
||||||
|
{
|
||||||
|
var bindingAttr = BindingFlags.Instance | BindingFlags.NonPublic;
|
||||||
|
var method = GetType().GetMethod(nameof(AddToTypeLookup), bindingAttr)!.MakeGenericMethod(type);
|
||||||
|
method.Invoke(this, new[] { asset });
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AddToTypeLookup<T>(T asset)
|
||||||
|
{
|
||||||
|
var type = typeof(T);
|
||||||
|
|
||||||
|
if (!_typeLookup.TryGetValue(type, out var list))
|
||||||
|
{
|
||||||
|
list = new List<T>();
|
||||||
|
_typeLookup.Add(type, list);
|
||||||
|
}
|
||||||
|
((List<T>)list).Add(asset);
|
||||||
|
|
||||||
|
var nextType = type.BaseType;
|
||||||
|
if (nextType == typeof(object)) return;
|
||||||
|
|
||||||
|
InvokeAddToTypeLookupWithType(asset, nextType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public T WithId<T>(int id) where T : class
|
||||||
|
{
|
||||||
|
return _idLookup.TryGetValue(id, out var asset) ? (T)asset : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> OfType<T>() where T : class
|
||||||
|
{
|
||||||
|
if (!_typeLookup.TryGetValue(typeof(T), out var assets))
|
||||||
|
{
|
||||||
|
assets = new List<T>();
|
||||||
|
_typeLookup.Add(typeof(T), assets);
|
||||||
|
}
|
||||||
|
return (List<T>)assets;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
37
Source/Engine/GameData/GameDatabaseSystem.cs
Normal file
37
Source/Engine/GameData/GameDatabaseSystem.cs
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine.AddressableAssets;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
|
||||||
|
public class GameDatabaseSystem : GameSystem, IInitializable, IServiceProvider
|
||||||
|
{
|
||||||
|
public const string GameDataAddressablesKey = "GameData";
|
||||||
|
|
||||||
|
private GameDatabase _gameDatabase;
|
||||||
|
|
||||||
|
public GameDatabaseSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterServices(IServiceLocator serviceLocator)
|
||||||
|
{
|
||||||
|
_gameDatabase = new GameDatabase();
|
||||||
|
ServiceLocator.RegisterService<IGameDatabase>(_gameDatabase);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
await Addressables.LoadAssetsAsync<Object>(GameDataAddressablesKey, OnAssetLoaded);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAssetLoaded(Object asset)
|
||||||
|
{
|
||||||
|
var id = asset.GetInstanceID();
|
||||||
|
id = unchecked(id + 0x40000000);
|
||||||
|
|
||||||
|
_gameDatabase.Add(id, asset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Engine/GameData/IGameDataRuntimeId.cs
Normal file
7
Source/Engine/GameData/IGameDataRuntimeId.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IGameDataRuntimeId
|
||||||
|
{
|
||||||
|
int RuntimeId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Source/Engine/GameData/IGameDatabase.cs
Normal file
13
Source/Engine/GameData/IGameDatabase.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IGameDatabase
|
||||||
|
{
|
||||||
|
void Add<T>(int id, T asset) where T : class;
|
||||||
|
|
||||||
|
T WithId<T>(int id) where T : class;
|
||||||
|
|
||||||
|
List<T> OfType<T>() where T : class;
|
||||||
|
}
|
||||||
|
}
|
||||||
32
Source/Engine/Helpers/AsyncBudget.cs
Normal file
32
Source/Engine/Helpers/AsyncBudget.cs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class AsyncBudget
|
||||||
|
{
|
||||||
|
private int _max;
|
||||||
|
|
||||||
|
private int _counter;
|
||||||
|
|
||||||
|
public AsyncBudget(int max)
|
||||||
|
{
|
||||||
|
_max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask TickAsync()
|
||||||
|
{
|
||||||
|
if (++_counter > _max)
|
||||||
|
{
|
||||||
|
_counter = 0;
|
||||||
|
await UniTask.NextFrame();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset(int max)
|
||||||
|
{
|
||||||
|
_max = max;
|
||||||
|
_counter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
63
Source/Engine/Helpers/TopologicalSort.cs
Normal file
63
Source/Engine/Helpers/TopologicalSort.cs
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class TopologicalSort<T>
|
||||||
|
{
|
||||||
|
private HashSet<T> _currentDependencies;
|
||||||
|
|
||||||
|
private HashSet<T> _closed;
|
||||||
|
|
||||||
|
public TopologicalSort(IEqualityComparer<T> comparer)
|
||||||
|
{
|
||||||
|
_currentDependencies = new HashSet<T>(comparer);
|
||||||
|
_closed = new HashSet<T>(comparer);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TopologicalSort() : this(EqualityComparer<T>.Default)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TopologicalSort<T> Default { get; } = new();
|
||||||
|
|
||||||
|
public IEnumerable<T> Sort(IEnumerable<T> source, Func<T, List<T>> dependenciesGetter, ICollection<T> sorted = null)
|
||||||
|
{
|
||||||
|
sorted ??= new List<T>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var item in source) Visit(item, dependenciesGetter, sorted);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_currentDependencies.Clear();
|
||||||
|
_closed.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Visit(T item, Func<T, List<T>> dependenciesGetter, ICollection<T> sorted)
|
||||||
|
{
|
||||||
|
if (!_currentDependencies.Add(item))
|
||||||
|
{
|
||||||
|
var ex = new StringBuilder();
|
||||||
|
ex.AppendLine(item.ToString());
|
||||||
|
foreach (var dependency in _currentDependencies) ex.AppendLine(dependency.ToString());
|
||||||
|
|
||||||
|
throw new InvalidOperationException(ex.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
var dependencies = dependenciesGetter.Invoke(item);
|
||||||
|
if (dependencies != null)
|
||||||
|
foreach (var dependency in dependencies)
|
||||||
|
Visit(dependency, dependenciesGetter, sorted);
|
||||||
|
|
||||||
|
_currentDependencies.Remove(item);
|
||||||
|
|
||||||
|
if (_closed.Add(item)) sorted.Add(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Riversong/Config/AppLinks.cs
Normal file
7
Source/Riversong/Config/AppLinks.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public static class AppLinks
|
||||||
|
{
|
||||||
|
public const string DemoFeedbackUrl = "https://forms.gle/vA5owahuh8AUMbf87";
|
||||||
|
}
|
||||||
|
}
|
||||||
17
Source/Riversong/Config/BuildVersionAsset.cs
Normal file
17
Source/Riversong/Config/BuildVersionAsset.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
492
Source/Riversong/Config/GameConfig.cs
Normal file
492
Source/Riversong/Config/GameConfig.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
30
Source/Riversong/Config/IScene.cs
Normal file
30
Source/Riversong/Config/IScene.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Source/Riversong/Config/SceneFolders.cs
Normal file
21
Source/Riversong/Config/SceneFolders.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
64
Source/Riversong/Config/UnityObjectInjector.cs
Normal file
64
Source/Riversong/Config/UnityObjectInjector.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[InitializeAfter(typeof(DebugSystemGroup))]
|
||||||
|
public class FinalizeInitializationSystemGroup : GameSystemGroup
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public struct GameInitializationCompletedSignal
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
6
Source/Riversong/Game/AppLifecycle/GameStartedSignal.cs
Normal file
6
Source/Riversong/Game/AppLifecycle/GameStartedSignal.cs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public struct GameStartedSignal
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Source/Riversong/Game/AssetsLoading/PreLoadAssetsSystem.cs
Normal file
71
Source/Riversong/Game/AssetsLoading/PreLoadAssetsSystem.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Source/Riversong/Game/Audio/AudioSystemGroup.cs
Normal file
8
Source/Riversong/Game/Audio/AudioSystemGroup.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
|
||||||
|
public class AudioSystemGroup : GameSystemGroup
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
82
Source/Riversong/Game/Audio/BackgroundMusicSystem.cs
Normal file
82
Source/Riversong/Game/Audio/BackgroundMusicSystem.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Source/Riversong/Game/Audio/ISoundPlayer.cs
Normal file
34
Source/Riversong/Game/Audio/ISoundPlayer.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
120
Source/Riversong/Game/Audio/PlaySoundOnEventSystem.cs
Normal file
120
Source/Riversong/Game/Audio/PlaySoundOnEventSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
184
Source/Riversong/Game/Audio/SoundPlayerSystem.cs
Normal file
184
Source/Riversong/Game/Audio/SoundPlayerSystem.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
21
Source/Riversong/Game/Audio/SystemSoundId.cs
Normal file
21
Source/Riversong/Game/Audio/SystemSoundId.cs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public enum SystemSoundId
|
||||||
|
{
|
||||||
|
UIClick,
|
||||||
|
|
||||||
|
BuildingPlaced,
|
||||||
|
|
||||||
|
BuildingDeleted,
|
||||||
|
|
||||||
|
HouseUpgraded,
|
||||||
|
|
||||||
|
RoadTilePlaced,
|
||||||
|
|
||||||
|
RoadTileDeleted,
|
||||||
|
|
||||||
|
UnlockNotification,
|
||||||
|
|
||||||
|
OnboardingMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Source/Riversong/Game/Audio/SystemSoundLibrary.cs
Normal file
13
Source/Riversong/Game/Audio/SystemSoundLibrary.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
145
Source/Riversong/Game/Camera/CameraSystem.cs
Normal file
145
Source/Riversong/Game/Camera/CameraSystem.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Riversong/Game/Camera/ICameraProperties.cs
Normal file
7
Source/Riversong/Game/Camera/ICameraProperties.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface ICameraProperties
|
||||||
|
{
|
||||||
|
float Zoom { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
65
Source/Riversong/Game/Collections/NativeGrid.cs
Normal file
65
Source/Riversong/Game/Collections/NativeGrid.cs
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
147
Source/Riversong/Game/Collections/SpatialLookup.cs
Normal file
147
Source/Riversong/Game/Collections/SpatialLookup.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[GameSystemGroup(typeof(CommonServicesSystemGroup))]
|
||||||
|
[InitializeAfter(typeof(CommonServicesSystem))]
|
||||||
|
public class AnalyticsInitializationSystem : GameSystem, IServiceProvider, IInitializable
|
||||||
|
{
|
||||||
|
private IAnalyticsService _analyticsService;
|
||||||
|
|
||||||
|
public AnalyticsInitializationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterServices(IServiceLocator serviceLocator)
|
||||||
|
{
|
||||||
|
_analyticsService = AnalyticsServiceFactory.Create();
|
||||||
|
serviceLocator.RegisterService(_analyticsService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
await _analyticsService.InitializeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public static class AnalyticsServiceFactory
|
||||||
|
{
|
||||||
|
public static IAnalyticsService Create()
|
||||||
|
{
|
||||||
|
#if UNITY_EDITOR && !ENABLE_EDITOR_ANALYTICS
|
||||||
|
return new NoOpAnalyticsService();
|
||||||
|
#else
|
||||||
|
return new UnityAnalyticsService();
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
using System;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class AnalyticsSessionState
|
||||||
|
{
|
||||||
|
private SessionStatus _status;
|
||||||
|
|
||||||
|
public string SessionId { get; private set; }
|
||||||
|
|
||||||
|
public float StartRealtimeSeconds { get; private set; }
|
||||||
|
|
||||||
|
public int HeartbeatIndex { get; private set; }
|
||||||
|
|
||||||
|
public int TotalBuildingsPlaced { get; private set; }
|
||||||
|
|
||||||
|
public bool Started => _status != SessionStatus.NotStarted;
|
||||||
|
|
||||||
|
public bool DemoCompleted => _status == SessionStatus.DemoCompleted;
|
||||||
|
|
||||||
|
public void Begin(float realtimeSeconds)
|
||||||
|
{
|
||||||
|
_status = SessionStatus.Started;
|
||||||
|
SessionId = Guid.NewGuid().ToString("N");
|
||||||
|
StartRealtimeSeconds = realtimeSeconds;
|
||||||
|
HeartbeatIndex = 0;
|
||||||
|
TotalBuildingsPlaced = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryRecordBuildingConstruction(BuildingDefinition definition)
|
||||||
|
{
|
||||||
|
if (!Started || !definition) return false;
|
||||||
|
|
||||||
|
TotalBuildingsPlaced++;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanRecordHouseUpgrade(Building building)
|
||||||
|
{
|
||||||
|
return Started && building.Definition.IsHouse;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryAdvanceHeartbeat(float realtimeSeconds, float heartbeatIntervalSeconds, out int heartbeatIndex, out float playtimeSeconds)
|
||||||
|
{
|
||||||
|
heartbeatIndex = 0;
|
||||||
|
playtimeSeconds = 0;
|
||||||
|
|
||||||
|
if (!Started || DemoCompleted) return false;
|
||||||
|
|
||||||
|
var nextHeartbeatTime = StartRealtimeSeconds + (HeartbeatIndex + 1) * heartbeatIntervalSeconds;
|
||||||
|
if (realtimeSeconds < nextHeartbeatTime) return false;
|
||||||
|
|
||||||
|
heartbeatIndex = ++HeartbeatIndex;
|
||||||
|
playtimeSeconds = GetPlaytimeSeconds(realtimeSeconds);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryMarkDemoCompleted()
|
||||||
|
{
|
||||||
|
if (!Started || DemoCompleted) return false;
|
||||||
|
|
||||||
|
_status = SessionStatus.DemoCompleted;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public float GetPlaytimeSeconds(float realtimeSeconds)
|
||||||
|
{
|
||||||
|
if (!Started) return 0;
|
||||||
|
|
||||||
|
return Mathf.Max(realtimeSeconds - StartRealtimeSeconds, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum SessionStatus
|
||||||
|
{
|
||||||
|
NotStarted,
|
||||||
|
|
||||||
|
Started,
|
||||||
|
|
||||||
|
DemoCompleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using System;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[GameSystemGroup(typeof(CommonServicesSystemGroup))]
|
||||||
|
[InitializeAfter(typeof(AnalyticsInitializationSystem))]
|
||||||
|
public class DemoAnalyticsSystem : GameSystem, IInitializable, IDisposable, IUpdatable
|
||||||
|
{
|
||||||
|
private const float HeartbeatIntervalSeconds = 300;
|
||||||
|
|
||||||
|
[InjectService]
|
||||||
|
private IAnalyticsService _analyticsService;
|
||||||
|
|
||||||
|
[InjectService]
|
||||||
|
private ISignalBus _signalBus;
|
||||||
|
|
||||||
|
[InjectService]
|
||||||
|
private World _world;
|
||||||
|
|
||||||
|
private AnalyticsSessionState _sessionState = new();
|
||||||
|
|
||||||
|
public DemoAnalyticsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
_signalBus.Subscribe<WorldReadySignal>(OnWorldReady);
|
||||||
|
_signalBus.Subscribe<BuildingCreatedSignal>(OnBuildingCreated);
|
||||||
|
_signalBus.Subscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
|
||||||
|
_signalBus.Subscribe<DemoCompletedSignal>(OnDemoCompleted);
|
||||||
|
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_signalBus.Unsubscribe<WorldReadySignal>(OnWorldReady);
|
||||||
|
_signalBus.Unsubscribe<BuildingCreatedSignal>(OnBuildingCreated);
|
||||||
|
_signalBus.Unsubscribe<BuildingUpgradedSignal>(OnBuildingUpgraded);
|
||||||
|
_signalBus.Unsubscribe<DemoCompletedSignal>(OnDemoCompleted);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
var realtimeSeconds = Time.realtimeSinceStartup;
|
||||||
|
|
||||||
|
while (_sessionState.TryAdvanceHeartbeat(realtimeSeconds, HeartbeatIntervalSeconds, out var heartbeatIndex, out var playtimeSeconds))
|
||||||
|
_analyticsService.RecordHeartbeat(_sessionState.SessionId, heartbeatIndex, playtimeSeconds, _sessionState.TotalBuildingsPlaced, _world.PopulationState.Population);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWorldReady(WorldReadySignal signal)
|
||||||
|
{
|
||||||
|
_sessionState.Begin(Time.realtimeSinceStartup);
|
||||||
|
_analyticsService.RecordSessionStarted(_sessionState.SessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBuildingCreated(BuildingCreatedSignal signal)
|
||||||
|
{
|
||||||
|
var definition = signal.Building?.Definition;
|
||||||
|
if (!_sessionState.TryRecordBuildingConstruction(definition)) return;
|
||||||
|
|
||||||
|
_analyticsService.RecordBuildingConstructionCompleted(_sessionState.SessionId, definition.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnBuildingUpgraded(BuildingUpgradedSignal signal)
|
||||||
|
{
|
||||||
|
var building = signal.Building;
|
||||||
|
if (!_sessionState.CanRecordHouseUpgrade(building)) return;
|
||||||
|
|
||||||
|
_analyticsService.RecordHouseUpgraded(_sessionState.SessionId, building.Definition.name, building.TierIndex, building.TierIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDemoCompleted(DemoCompletedSignal signal)
|
||||||
|
{
|
||||||
|
if (!_sessionState.TryMarkDemoCompleted()) return;
|
||||||
|
|
||||||
|
var playtimeSeconds = _sessionState.GetPlaytimeSeconds(Time.realtimeSinceStartup);
|
||||||
|
_analyticsService.RecordDemoCompleted(
|
||||||
|
_sessionState.SessionId,
|
||||||
|
playtimeSeconds,
|
||||||
|
_world.TimeState.TotalWeeks,
|
||||||
|
_world.PopulationState.Population,
|
||||||
|
_world.PopulationState.Happiness);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using Unity.Services.Analytics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public sealed class BuildingConstructionCompletedAnalyticsEvent : Event
|
||||||
|
{
|
||||||
|
public BuildingConstructionCompletedAnalyticsEvent() : base("building_construction_completed")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SessionId
|
||||||
|
{
|
||||||
|
set => SetParameter("session_id", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildingName
|
||||||
|
{
|
||||||
|
set => SetParameter("building_name", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Unity.Services.Analytics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public sealed class DemoCompletedAnalyticsEvent : Event
|
||||||
|
{
|
||||||
|
public DemoCompletedAnalyticsEvent() : base("demo_completed")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SessionId
|
||||||
|
{
|
||||||
|
set => SetParameter("session_id", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float PlaytimeSeconds
|
||||||
|
{
|
||||||
|
set => SetParameter("playtime_seconds", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TotalWeeks
|
||||||
|
{
|
||||||
|
set => SetParameter("total_weeks", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Population
|
||||||
|
{
|
||||||
|
set => SetParameter("population", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float Happiness
|
||||||
|
{
|
||||||
|
set => SetParameter("happiness", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Unity.Services.Analytics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public sealed class HouseUpgradedAnalyticsEvent : Event
|
||||||
|
{
|
||||||
|
public HouseUpgradedAnalyticsEvent() : base("house_upgraded")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SessionId
|
||||||
|
{
|
||||||
|
set => SetParameter("session_id", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildingName
|
||||||
|
{
|
||||||
|
set => SetParameter("building_name", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int FromTier
|
||||||
|
{
|
||||||
|
set => SetParameter("from_tier", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int ToTier
|
||||||
|
{
|
||||||
|
set => SetParameter("to_tier", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using Unity.Services.Analytics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public sealed class SessionHeartbeatAnalyticsEvent : Event
|
||||||
|
{
|
||||||
|
public SessionHeartbeatAnalyticsEvent() : base("session_heartbeat")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SessionId
|
||||||
|
{
|
||||||
|
set => SetParameter("session_id", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int HeartbeatIndex
|
||||||
|
{
|
||||||
|
set => SetParameter("heartbeat_index", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public float PlaytimeSeconds
|
||||||
|
{
|
||||||
|
set => SetParameter("playtime_seconds", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int TotalBuildingsPlaced
|
||||||
|
{
|
||||||
|
set => SetParameter("total_buildings_placed", value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Population
|
||||||
|
{
|
||||||
|
set => SetParameter("population", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using Unity.Services.Analytics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public sealed class SessionStartedAnalyticsEvent : Event
|
||||||
|
{
|
||||||
|
public SessionStartedAnalyticsEvent() : base("session_started")
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public string SessionId
|
||||||
|
{
|
||||||
|
set => SetParameter("session_id", value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IAnalyticsService
|
||||||
|
{
|
||||||
|
UniTask InitializeAsync();
|
||||||
|
|
||||||
|
void RecordSessionStarted(string sessionId);
|
||||||
|
|
||||||
|
void RecordBuildingConstructionCompleted(string sessionId, string buildingName);
|
||||||
|
|
||||||
|
void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier);
|
||||||
|
|
||||||
|
void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population);
|
||||||
|
|
||||||
|
void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class NoOpAnalyticsService : IAnalyticsService
|
||||||
|
{
|
||||||
|
public UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordSessionStarted(string sessionId)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordBuildingConstructionCompleted(string sessionId, string buildingName)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using Unity.Services.Analytics;
|
||||||
|
using Unity.Services.Core;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class UnityAnalyticsService : IAnalyticsService
|
||||||
|
{
|
||||||
|
private bool _canRecord;
|
||||||
|
|
||||||
|
public async UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
if (_canRecord) return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (UnityServices.State == ServicesInitializationState.Uninitialized) await UnityServices.InitializeAsync();
|
||||||
|
|
||||||
|
AnalyticsService.Instance.StartDataCollection();
|
||||||
|
|
||||||
|
_canRecord = true;
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Debug.LogError($"Failed to initialize analytics. Analytics will be disabled.\n{exception}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordSessionStarted(string sessionId)
|
||||||
|
{
|
||||||
|
if (!_canRecord) return;
|
||||||
|
|
||||||
|
var analyticsEvent = new SessionStartedAnalyticsEvent { SessionId = sessionId };
|
||||||
|
|
||||||
|
AnalyticsService.Instance.RecordEvent(analyticsEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordBuildingConstructionCompleted(string sessionId, string buildingName)
|
||||||
|
{
|
||||||
|
if (!_canRecord) return;
|
||||||
|
|
||||||
|
var analyticsEvent = new BuildingConstructionCompletedAnalyticsEvent
|
||||||
|
{
|
||||||
|
SessionId = sessionId,
|
||||||
|
BuildingName = buildingName
|
||||||
|
};
|
||||||
|
|
||||||
|
AnalyticsService.Instance.RecordEvent(analyticsEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordHouseUpgraded(string sessionId, string buildingName, int fromTier, int toTier)
|
||||||
|
{
|
||||||
|
if (!_canRecord) return;
|
||||||
|
|
||||||
|
var analyticsEvent = new HouseUpgradedAnalyticsEvent
|
||||||
|
{
|
||||||
|
SessionId = sessionId,
|
||||||
|
BuildingName = buildingName,
|
||||||
|
FromTier = fromTier,
|
||||||
|
ToTier = toTier
|
||||||
|
};
|
||||||
|
|
||||||
|
AnalyticsService.Instance.RecordEvent(analyticsEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordHeartbeat(string sessionId, int heartbeatIndex, float playtimeSeconds, int totalBuildingsPlaced, int population)
|
||||||
|
{
|
||||||
|
if (!_canRecord) return;
|
||||||
|
|
||||||
|
var analyticsEvent = new SessionHeartbeatAnalyticsEvent
|
||||||
|
{
|
||||||
|
SessionId = sessionId,
|
||||||
|
HeartbeatIndex = heartbeatIndex,
|
||||||
|
PlaytimeSeconds = playtimeSeconds,
|
||||||
|
TotalBuildingsPlaced = totalBuildingsPlaced,
|
||||||
|
Population = population
|
||||||
|
};
|
||||||
|
|
||||||
|
AnalyticsService.Instance.RecordEvent(analyticsEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RecordDemoCompleted(string sessionId, float playtimeSeconds, int totalWeeks, int population, float happiness)
|
||||||
|
{
|
||||||
|
if (!_canRecord) return;
|
||||||
|
|
||||||
|
var analyticsEvent = new DemoCompletedAnalyticsEvent
|
||||||
|
{
|
||||||
|
SessionId = sessionId,
|
||||||
|
PlaytimeSeconds = playtimeSeconds,
|
||||||
|
TotalWeeks = totalWeeks,
|
||||||
|
Population = population,
|
||||||
|
Happiness = Mathf.Clamp01(happiness)
|
||||||
|
};
|
||||||
|
|
||||||
|
AnalyticsService.Instance.RecordEvent(analyticsEvent);
|
||||||
|
AnalyticsService.Instance.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
58
Source/Riversong/Game/CommonServices/CommonServicesSystem.cs
Normal file
58
Source/Riversong/Game/CommonServices/CommonServicesSystem.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using IServiceProvider = DanieleMarotta.RiversongCodeShowcase.IServiceProvider;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[GameSystemGroup(typeof(CommonServicesSystemGroup))]
|
||||||
|
public class CommonServicesSystem : GameSystem, IServiceProvider, IInitializable, IDisposable
|
||||||
|
{
|
||||||
|
[InjectService]
|
||||||
|
private GameConfig _config;
|
||||||
|
|
||||||
|
[InjectService]
|
||||||
|
private World _world;
|
||||||
|
|
||||||
|
private SignalBus _signalBus;
|
||||||
|
|
||||||
|
private TileSpace _tileSpace;
|
||||||
|
|
||||||
|
private MaterialReplacementCache _materialReplacementCache;
|
||||||
|
|
||||||
|
public CommonServicesSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void RegisterServices(IServiceLocator serviceLocator)
|
||||||
|
{
|
||||||
|
serviceLocator.RegisterService<IEntityCollection>(new EntityCollection());
|
||||||
|
serviceLocator.RegisterService<IEntityCache>(new EntityCache());
|
||||||
|
|
||||||
|
_signalBus = new SignalBus();
|
||||||
|
serviceLocator.RegisterService<ISignalBus>(_signalBus);
|
||||||
|
|
||||||
|
_tileSpace = new TileSpace();
|
||||||
|
serviceLocator.RegisterService<ITileSpace>(_tileSpace);
|
||||||
|
|
||||||
|
serviceLocator.RegisterService<IPoolingService>(new PoolingService());
|
||||||
|
|
||||||
|
_materialReplacementCache = new MaterialReplacementCache();
|
||||||
|
serviceLocator.RegisterService(_materialReplacementCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
public UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
_tileSpace.TileSize = _config.GeneralSettings.TileSize;
|
||||||
|
_tileSpace.World = _world;
|
||||||
|
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_signalBus?.Dispose();
|
||||||
|
_signalBus = null;
|
||||||
|
_materialReplacementCache = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[InitializeBefore(typeof(EarlyGameSystemGroup))]
|
||||||
|
public class CommonServicesSystemGroup : GameSystemGroup
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
14
Source/Riversong/Game/CommonServices/Entities/Entity.cs
Normal file
14
Source/Riversong/Game/CommonServices/Entities/Entity.cs
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class Entity
|
||||||
|
{
|
||||||
|
public const int InvalidId = 0;
|
||||||
|
|
||||||
|
public int Id { get; private set; }
|
||||||
|
|
||||||
|
public static T Create<T>(int id) where T : Entity, new()
|
||||||
|
{
|
||||||
|
return new T { Id = id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class EntityCache : IEntityCache
|
||||||
|
{
|
||||||
|
private Dictionary<int, Cache> _caches = new();
|
||||||
|
|
||||||
|
public void CreateCache<T>(int key, Predicate<T> filter) where T : Entity
|
||||||
|
{
|
||||||
|
if (!_caches.TryGetValue(key, out var cache))
|
||||||
|
{
|
||||||
|
cache = new Cache<T>();
|
||||||
|
_caches.Add(key, cache);
|
||||||
|
}
|
||||||
|
((Cache<T>)cache).Filter = filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> Get<T>(int key) where T : Entity
|
||||||
|
{
|
||||||
|
if (!_caches.TryGetValue(key, out var cache))
|
||||||
|
{
|
||||||
|
cache = new Cache<T>();
|
||||||
|
_caches.Add(key, cache);
|
||||||
|
}
|
||||||
|
return ((Cache<T>)cache).Entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnAdded(Entity entity)
|
||||||
|
{
|
||||||
|
foreach (var cache in _caches.Values) cache.TryAdd(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnRemoved(Entity entity)
|
||||||
|
{
|
||||||
|
foreach (var cache in _caches.Values) cache.TryRemove(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class Cache
|
||||||
|
{
|
||||||
|
public void TryAdd(Entity entity)
|
||||||
|
{
|
||||||
|
if (FilterEntity(entity)) Add(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void TryRemove(Entity entity)
|
||||||
|
{
|
||||||
|
if (FilterEntity(entity)) Remove(entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract bool FilterEntity(Entity entity);
|
||||||
|
|
||||||
|
protected abstract void Add(Entity entity);
|
||||||
|
|
||||||
|
protected abstract void Remove(Entity entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Cache<T> : Cache where T : Entity
|
||||||
|
{
|
||||||
|
public Predicate<T> Filter { get; set; }
|
||||||
|
|
||||||
|
public List<T> Entities { get; } = new();
|
||||||
|
|
||||||
|
protected override bool FilterEntity(Entity entity)
|
||||||
|
{
|
||||||
|
return entity is T typedEntity && Filter.Invoke(typedEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Add(Entity entity)
|
||||||
|
{
|
||||||
|
Entities.Add((T)entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void Remove(Entity entity)
|
||||||
|
{
|
||||||
|
Entities.Remove((T)entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public static class EntityCacheExtensions
|
||||||
|
{
|
||||||
|
public static List<Building> GetHarvesterBuildings(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.HarvesterBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetHunterBuildings(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.HunterBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetFarmBuildings(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.FarmBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetProducers(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.ProducerBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetProviders(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.ProviderBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetBuildingsWithWorkers(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.BuildingsWithWorkers);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetHouses(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.HouseBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetTentBuildings(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.TentBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetStorageBuildings(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.StorageBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Building> GetStorageRequestBuildings(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Building>((int)EntityCacheKeys.StorageRequestBuildings);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Agent> GetHunterAgents(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Agent>((int)EntityCacheKeys.HunterAgents);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Agent> GetCritterAgents(this IEntityCache entityCache)
|
||||||
|
{
|
||||||
|
return entityCache.Get<Agent>((int)EntityCacheKeys.CritterAgents);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public enum EntityCacheKeys
|
||||||
|
{
|
||||||
|
Invalid,
|
||||||
|
|
||||||
|
#region Buildings
|
||||||
|
|
||||||
|
HarvesterBuildings,
|
||||||
|
|
||||||
|
HunterBuildings,
|
||||||
|
|
||||||
|
FarmBuildings,
|
||||||
|
|
||||||
|
ProducerBuildings,
|
||||||
|
|
||||||
|
ProviderBuildings,
|
||||||
|
|
||||||
|
BuildingsWithWorkers,
|
||||||
|
|
||||||
|
HouseBuildings,
|
||||||
|
|
||||||
|
StorageBuildings,
|
||||||
|
|
||||||
|
StorageRequestBuildings,
|
||||||
|
|
||||||
|
TentBuildings,
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region Agents
|
||||||
|
|
||||||
|
HunterAgents,
|
||||||
|
|
||||||
|
CritterAgents
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
using System;
|
||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
|
||||||
|
[InitializeAfter(typeof(PreLoadAssetsSystem))]
|
||||||
|
public class EntityCacheSystem : GameSystem, IInitializable, IDisposable
|
||||||
|
{
|
||||||
|
[InjectService]
|
||||||
|
private IEntityCollection _entityCollection;
|
||||||
|
|
||||||
|
[InjectService]
|
||||||
|
private IEntityCache _entityCache;
|
||||||
|
|
||||||
|
[InjectService]
|
||||||
|
private GameConfig _config;
|
||||||
|
|
||||||
|
public EntityCacheSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
CreateCaches();
|
||||||
|
|
||||||
|
foreach (var entity in _entityCollection.GetInternalEntityList(typeof(Entity))) _entityCache.OnAdded(entity);
|
||||||
|
|
||||||
|
var callbacks = _entityCollection.On<Entity>();
|
||||||
|
callbacks.Added += _entityCache.OnAdded;
|
||||||
|
callbacks.Removed += _entityCache.OnRemoved;
|
||||||
|
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateCaches()
|
||||||
|
{
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.HarvesterBuildings, b => b.Definition.HarvestedResource);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.HunterBuildings, b => b.Definition.TargetCritter);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.FarmBuildings, b => b.Definition.IsFarm);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.ProducerBuildings, b => b.Definition.Recipe);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.ProviderBuildings, b => b.Definition.ProvidedProducts.Count > 0);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.BuildingsWithWorkers, b => b.Definition.WorkerCount > 0);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.HouseBuildings, b => b.Definition.IsHouse);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.StorageBuildings, b => b.Definition.IsStorage);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.StorageRequestBuildings, b => b.Definition.IsStorage);
|
||||||
|
_entityCache.CreateCache<Building>((int)EntityCacheKeys.TentBuildings, b => b.Definition == _config.Population.TentBuilding.Asset);
|
||||||
|
_entityCache.CreateCache<Agent>(
|
||||||
|
(int)EntityCacheKeys.HunterAgents,
|
||||||
|
a =>
|
||||||
|
{
|
||||||
|
ref var jobState = ref a.GetJobStateRW();
|
||||||
|
return jobState.Job == AgentJob.Hunter;
|
||||||
|
});
|
||||||
|
_entityCache.CreateCache<Agent>(
|
||||||
|
(int)EntityCacheKeys.CritterAgents,
|
||||||
|
a =>
|
||||||
|
{
|
||||||
|
ref var critterState = ref a.GetCritterStateRW();
|
||||||
|
return critterState.IsCritter;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
var callbacks = _entityCollection.On<Entity>();
|
||||||
|
callbacks.Added -= _entityCache.OnAdded;
|
||||||
|
callbacks.Removed -= _entityCache.OnRemoved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IEntityCache
|
||||||
|
{
|
||||||
|
void CreateCache<T>(int key, Predicate<T> filter) where T : Entity;
|
||||||
|
|
||||||
|
List<T> Get<T>(int key) where T : Entity;
|
||||||
|
|
||||||
|
void OnAdded(Entity entity);
|
||||||
|
|
||||||
|
void OnRemoved(Entity entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class EntityCollection : IEntityCollection
|
||||||
|
{
|
||||||
|
private static readonly List<Entity> Empty = new();
|
||||||
|
|
||||||
|
private int _nextId = Entity.InvalidId + 1;
|
||||||
|
|
||||||
|
private Dictionary<int, Entity> _entitiesById = new();
|
||||||
|
|
||||||
|
private ListMultiDictionary<Type, Entity> _entitiesByType = new();
|
||||||
|
|
||||||
|
private Dictionary<Type, IEntityCollectionCallbacks> _callbacks = new();
|
||||||
|
|
||||||
|
public T Create<T>() where T : Entity, new()
|
||||||
|
{
|
||||||
|
return Entity.Create<T>(_nextId++);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(Entity entity)
|
||||||
|
{
|
||||||
|
_entitiesById.Add(entity.Id, entity);
|
||||||
|
OnAdded(entity.GetType(), entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAdded(Type type, Entity entity)
|
||||||
|
{
|
||||||
|
_entitiesByType.Add(type, entity);
|
||||||
|
|
||||||
|
if (_callbacks.TryGetValue(type, out var callbacks)) callbacks.OnAdded(entity);
|
||||||
|
|
||||||
|
if (type == typeof(Entity)) return;
|
||||||
|
|
||||||
|
OnAdded(type.BaseType, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entity Remove(int id)
|
||||||
|
{
|
||||||
|
if (!_entitiesById.Remove(id, out var entity)) return null;
|
||||||
|
|
||||||
|
OnRemoved(entity.GetType(), entity);
|
||||||
|
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRemoved(Type type, Entity entity)
|
||||||
|
{
|
||||||
|
_entitiesByType.Remove(type, entity);
|
||||||
|
|
||||||
|
if (_callbacks.TryGetValue(type, out var callbacks)) callbacks.OnRemoved(entity);
|
||||||
|
|
||||||
|
if (type == typeof(Entity)) return;
|
||||||
|
|
||||||
|
OnRemoved(type.BaseType, entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists(int id)
|
||||||
|
{
|
||||||
|
return _entitiesById.ContainsKey(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Entity Get(int id)
|
||||||
|
{
|
||||||
|
return _entitiesById.TryGetValue(id, out var entity) ? entity : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Type GetEntityType(int id)
|
||||||
|
{
|
||||||
|
return Get(id)?.GetType();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Entity> GetInternalEntityList(Type type)
|
||||||
|
{
|
||||||
|
return _entitiesByType.TryGetValues(type, out var entityList) ? entityList : Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEntityCollectionCallbacks<T> On<T>() where T : Entity
|
||||||
|
{
|
||||||
|
if (!_callbacks.TryGetValue(typeof(T), out var callbacks))
|
||||||
|
{
|
||||||
|
callbacks = new EntityCollectionCallbacks<T>();
|
||||||
|
_callbacks.Add(typeof(T), callbacks);
|
||||||
|
}
|
||||||
|
return (IEntityCollectionCallbacks<T>)callbacks;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class EntityCollectionCallbacks<T> : IEntityCollectionCallbacks<T> where T : Entity
|
||||||
|
{
|
||||||
|
public event Action<T> Added;
|
||||||
|
|
||||||
|
public event Action<T> Removed;
|
||||||
|
|
||||||
|
public void OnAdded(Entity entity)
|
||||||
|
{
|
||||||
|
Added?.Invoke((T)entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void OnRemoved(Entity entity)
|
||||||
|
{
|
||||||
|
Removed?.Invoke((T)entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public static class EntityCollectionExtensions
|
||||||
|
{
|
||||||
|
public static T Get<T>(this IEntityCollection entityCollection, int id) where T : Entity
|
||||||
|
{
|
||||||
|
return (T)entityCollection.Get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T CreateAndAdd<T>(this IEntityCollection entityCollection) where T : Entity, new()
|
||||||
|
{
|
||||||
|
var entity = entityCollection.Create<T>();
|
||||||
|
entityCollection.Add(entity);
|
||||||
|
return entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGet<T>(this IEntityCollection entityCollection, int id, out T entity) where T : Entity
|
||||||
|
{
|
||||||
|
entity = entityCollection.Get(id) as T;
|
||||||
|
return entity != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
Source/Riversong/Game/CommonServices/Entities/IEntity.cs
Normal file
7
Source/Riversong/Game/CommonServices/Entities/IEntity.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IEntity
|
||||||
|
{
|
||||||
|
int Id { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IEntityCollection
|
||||||
|
{
|
||||||
|
T Create<T>() where T : Entity, new();
|
||||||
|
|
||||||
|
void Add(Entity entity);
|
||||||
|
|
||||||
|
Entity Remove(int id);
|
||||||
|
|
||||||
|
bool Exists(int id);
|
||||||
|
|
||||||
|
Entity Get(int id);
|
||||||
|
|
||||||
|
Type GetEntityType(int id);
|
||||||
|
|
||||||
|
public List<Entity> GetInternalEntityList(Type type);
|
||||||
|
|
||||||
|
public IEntityCollectionCallbacks<T> On<T>() where T : Entity;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IEntityCollectionCallbacks
|
||||||
|
{
|
||||||
|
public void OnAdded(Entity entity);
|
||||||
|
|
||||||
|
public void OnRemoved(Entity entity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface IEntityCollectionCallbacks<out T> : IEntityCollectionCallbacks
|
||||||
|
{
|
||||||
|
event Action<T> Added;
|
||||||
|
|
||||||
|
event Action<T> Removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
using UnityEngine.Pool;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface IPoolingService
|
||||||
|
{
|
||||||
|
void AddPool<T>(int key, IObjectPool<T> pool) where T : class;
|
||||||
|
|
||||||
|
IObjectPool<T> GetPool<T>(int key) where T : class;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class PooledObject : MonoBehaviour
|
||||||
|
{
|
||||||
|
public int PoolKey { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using UnityEngine.Pool;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class PoolingService : IPoolingService
|
||||||
|
{
|
||||||
|
private Dictionary<int, object> _poolLookup = new();
|
||||||
|
|
||||||
|
public void AddPool<T>(int key, IObjectPool<T> pool) where T : class
|
||||||
|
{
|
||||||
|
_poolLookup.Add(key, pool);
|
||||||
|
}
|
||||||
|
|
||||||
|
public IObjectPool<T> GetPool<T>(int key) where T : class
|
||||||
|
{
|
||||||
|
return _poolLookup.TryGetValue(key, out var pool) ? (IObjectPool<T>)pool : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.Pool;
|
||||||
|
using Object = UnityEngine.Object;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public static class PoolingServiceExtensions
|
||||||
|
{
|
||||||
|
public static IObjectPool<GameObject> GetOrCreatePool(this IPoolingService poolingService, GameObject prefab, Transform folder = null)
|
||||||
|
{
|
||||||
|
var poolKey = prefab.GetInstanceID();
|
||||||
|
|
||||||
|
var pool = poolingService.GetPool<GameObject>(poolKey);
|
||||||
|
if (pool != null) return pool;
|
||||||
|
|
||||||
|
pool = new ObjectPool<GameObject>(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var go = Object.Instantiate(prefab, folder);
|
||||||
|
OnCreate(go, poolKey);
|
||||||
|
return go;
|
||||||
|
},
|
||||||
|
OnGet,
|
||||||
|
go => OnRelease(go, folder));
|
||||||
|
|
||||||
|
poolingService.AddPool(poolKey, pool);
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IObjectPool<T> GetOrCreatePool<T>(this IPoolingService poolingService, T prefab, Transform folder = null) where T : Component
|
||||||
|
{
|
||||||
|
var poolKey = prefab.GetInstanceID();
|
||||||
|
|
||||||
|
var pool = poolingService.GetPool<T>(poolKey);
|
||||||
|
if (pool != null) return pool;
|
||||||
|
|
||||||
|
pool = new ObjectPool<T>(
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
var component = Object.Instantiate(prefab, folder);
|
||||||
|
OnCreate(component.gameObject, poolKey);
|
||||||
|
return component;
|
||||||
|
},
|
||||||
|
component => OnGet(component.gameObject),
|
||||||
|
component => OnRelease(component.gameObject, folder));
|
||||||
|
|
||||||
|
poolingService.AddPool(poolKey, pool);
|
||||||
|
|
||||||
|
return pool;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnCreate(GameObject go, int poolKey)
|
||||||
|
{
|
||||||
|
go.AddComponent<PooledObject>().PoolKey = poolKey;
|
||||||
|
go.SetActive(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnGet(GameObject go)
|
||||||
|
{
|
||||||
|
go.transform.SetParent(null);
|
||||||
|
go.SetActive(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void OnRelease(GameObject go, Transform folder)
|
||||||
|
{
|
||||||
|
go.SetActive(false);
|
||||||
|
go.transform.SetParent(folder);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static T Get<T>(this IPoolingService poolingService, int poolKey) where T : class
|
||||||
|
{
|
||||||
|
return poolingService.GetPool<T>(poolKey).Get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Release(this IPoolingService poolingService, GameObject go)
|
||||||
|
{
|
||||||
|
var poolKey = go.GetComponent<PooledObject>().PoolKey;
|
||||||
|
poolingService.GetPool<GameObject>(poolKey).Release(go);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Release<T>(this IPoolingService poolingService, T component) where T : Component
|
||||||
|
{
|
||||||
|
var poolKey = component.GetComponent<PooledObject>().PoolKey;
|
||||||
|
poolingService.GetPool<T>(poolKey).Release(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
using Cysharp.Threading.Tasks;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[GameSystemGroup(typeof(EarlyGameSystemGroup))]
|
||||||
|
[InitializeAfter(typeof(PreLoadAssetsSystem))]
|
||||||
|
public class PoolsInitializationSystem : GameSystem, IInitializable
|
||||||
|
{
|
||||||
|
[InjectService]
|
||||||
|
private IPoolingService _poolingService;
|
||||||
|
|
||||||
|
[InjectService]
|
||||||
|
private IGameDatabase _gameDatabase;
|
||||||
|
|
||||||
|
public PoolsInitializationSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public UniTask InitializeAsync()
|
||||||
|
{
|
||||||
|
foreach (var product in _gameDatabase.OfType<ProductDefinition>())
|
||||||
|
{
|
||||||
|
_poolingService.GetOrCreatePool((GameObject)product.ProductStackVisualization.Asset);
|
||||||
|
_poolingService.GetOrCreatePool((GameObject)product.CarriedVisualization.Asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
return UniTask.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[GameSystemGroup(typeof(DefaultGameSystemGroup))]
|
||||||
|
[UpdateBefore(typeof(EditingStateGameSystem))]
|
||||||
|
[UpdateBefore(typeof(BuildingSelectionSystem))]
|
||||||
|
public class RestoreTemporaryMaterialsSystem : GameSystem, IUpdatable
|
||||||
|
{
|
||||||
|
[InjectService]
|
||||||
|
private MaterialReplacementCache _materialReplacementCache;
|
||||||
|
|
||||||
|
public RestoreTemporaryMaterialsSystem(IServiceLocator serviceLocator) : base(serviceLocator)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Update()
|
||||||
|
{
|
||||||
|
_materialReplacementCache.RestoreMaterials();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
Source/Riversong/Game/CommonServices/Signals/ISignalBus.cs
Normal file
13
Source/Riversong/Game/CommonServices/Signals/ISignalBus.cs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface ISignalBus
|
||||||
|
{
|
||||||
|
void Raise<T>(T signal);
|
||||||
|
|
||||||
|
void Subscribe<T>(Action<T> handler);
|
||||||
|
|
||||||
|
void Unsubscribe<T>(Action<T> handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
Source/Riversong/Game/CommonServices/Signals/SignalBus.cs
Normal file
31
Source/Riversong/Game/CommonServices/Signals/SignalBus.cs
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class SignalBus : ISignalBus, IDisposable
|
||||||
|
{
|
||||||
|
private readonly ListMultiDictionary<Type, Delegate> _subscribers = new();
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_subscribers.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Raise<T>(T signal)
|
||||||
|
{
|
||||||
|
if (!_subscribers.TryGetValues(typeof(T), out var handlers)) return;
|
||||||
|
|
||||||
|
foreach (var handler in handlers) ((Action<T>)handler).Invoke(signal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Subscribe<T>(Action<T> handler)
|
||||||
|
{
|
||||||
|
_subscribers.Add(typeof(T), handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unsubscribe<T>(Action<T> handler)
|
||||||
|
{
|
||||||
|
_subscribers.Remove(typeof(T), handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public static class DirectionVectors
|
||||||
|
{
|
||||||
|
public static readonly int2[] Directions4 =
|
||||||
|
{
|
||||||
|
new(1, 0),
|
||||||
|
new(-1, 0),
|
||||||
|
new(0, 1),
|
||||||
|
new(0, -1)
|
||||||
|
};
|
||||||
|
|
||||||
|
public static readonly int2[] Directions8 =
|
||||||
|
{
|
||||||
|
new(1, 0),
|
||||||
|
new(-1, 0),
|
||||||
|
new(0, 1),
|
||||||
|
new(0, -1),
|
||||||
|
new(1, 1),
|
||||||
|
new(1, -1),
|
||||||
|
new(-1, 1),
|
||||||
|
new(-1, -1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
134
Source/Riversong/Game/CommonServices/TileMath/Directions.cs
Normal file
134
Source/Riversong/Game/CommonServices/TileMath/Directions.cs
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
using System;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public enum Directions
|
||||||
|
{
|
||||||
|
North,
|
||||||
|
|
||||||
|
NorthWest,
|
||||||
|
|
||||||
|
West,
|
||||||
|
|
||||||
|
SouthWest,
|
||||||
|
|
||||||
|
South,
|
||||||
|
|
||||||
|
SouthEast,
|
||||||
|
|
||||||
|
East,
|
||||||
|
|
||||||
|
NorthEast
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DirectionsMask4
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
North = 1 << 0,
|
||||||
|
|
||||||
|
West = 1 << 1,
|
||||||
|
|
||||||
|
South = 1 << 2,
|
||||||
|
|
||||||
|
East = 1 << 3
|
||||||
|
}
|
||||||
|
|
||||||
|
[Flags]
|
||||||
|
public enum DirectionsMask8
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
North = 1 << 0,
|
||||||
|
|
||||||
|
NorthWest = 1 << 1,
|
||||||
|
|
||||||
|
West = 1 << 2,
|
||||||
|
|
||||||
|
SouthWest = 1 << 3,
|
||||||
|
|
||||||
|
South = 1 << 4,
|
||||||
|
|
||||||
|
SouthEast = 1 << 5,
|
||||||
|
|
||||||
|
East = 1 << 6,
|
||||||
|
|
||||||
|
NorthEast = 1 << 7
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class DirectionsExtensions
|
||||||
|
{
|
||||||
|
public static int2 ToVector(this Directions direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Directions.North:
|
||||||
|
return new int2(0, 1);
|
||||||
|
|
||||||
|
case Directions.NorthWest:
|
||||||
|
return new int2(-1, 1);
|
||||||
|
|
||||||
|
case Directions.West:
|
||||||
|
return new int2(-1, 0);
|
||||||
|
|
||||||
|
case Directions.SouthWest:
|
||||||
|
return new int2(-1, -1);
|
||||||
|
|
||||||
|
case Directions.South:
|
||||||
|
return new int2(0, -1);
|
||||||
|
|
||||||
|
case Directions.SouthEast:
|
||||||
|
return new int2(1, -1);
|
||||||
|
|
||||||
|
case Directions.East:
|
||||||
|
return new int2(1, 0);
|
||||||
|
|
||||||
|
case Directions.NorthEast:
|
||||||
|
return new int2(1, 1);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Quaternion ToQuaternion(this Directions direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Directions.North:
|
||||||
|
return Quaternion.identity;
|
||||||
|
|
||||||
|
case Directions.NorthWest:
|
||||||
|
return Quaternion.LookRotation(new Vector3(-1, 0, 1));
|
||||||
|
|
||||||
|
case Directions.West:
|
||||||
|
return Quaternion.LookRotation(Vector3.left);
|
||||||
|
|
||||||
|
case Directions.SouthWest:
|
||||||
|
return Quaternion.LookRotation(new Vector3(-1, 0, -1));
|
||||||
|
|
||||||
|
case Directions.South:
|
||||||
|
return Quaternion.LookRotation(Vector3.back);
|
||||||
|
|
||||||
|
case Directions.SouthEast:
|
||||||
|
return Quaternion.LookRotation(new Vector3(1, 0, -1));
|
||||||
|
|
||||||
|
case Directions.East:
|
||||||
|
return Quaternion.LookRotation(Vector3.right);
|
||||||
|
|
||||||
|
case Directions.NorthEast:
|
||||||
|
return Quaternion.LookRotation(new Vector3(1, 0, 1));
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int2 Rotate(this Directions direction, int2 v)
|
||||||
|
{
|
||||||
|
return TileMath.Rotate(v, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
18
Source/Riversong/Game/CommonServices/TileMath/ITileSpace.cs
Normal file
18
Source/Riversong/Game/CommonServices/TileMath/ITileSpace.cs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
using Unity.Mathematics;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public interface ITileSpace
|
||||||
|
{
|
||||||
|
float TileSize { get; }
|
||||||
|
|
||||||
|
int2 WorldToTile(Vector3 position);
|
||||||
|
|
||||||
|
Vector3 TileToWorld(int2 tile, float tileX = 0.5f, float tileY = 0.5f);
|
||||||
|
|
||||||
|
int GetElevation(float y);
|
||||||
|
|
||||||
|
Vector3 GetRectWorldCenter(in TileRect rect);
|
||||||
|
}
|
||||||
|
}
|
||||||
136
Source/Riversong/Game/CommonServices/TileMath/TileMath.cs
Normal file
136
Source/Riversong/Game/CommonServices/TileMath/TileMath.cs
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
using System;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public static class TileMath
|
||||||
|
{
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static int2 Rotate(int2 v, Directions direction)
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Directions.North:
|
||||||
|
return new int2(v.x, v.y);
|
||||||
|
|
||||||
|
case Directions.West:
|
||||||
|
return new int2(v.y, -v.x);
|
||||||
|
|
||||||
|
case Directions.South:
|
||||||
|
return new int2(-v.x, -v.y);
|
||||||
|
|
||||||
|
case Directions.East:
|
||||||
|
return new int2(-v.y, v.x);
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TileRect GetBuildingRect(int2 rectCenter, Directions direction, int width, int height)
|
||||||
|
{
|
||||||
|
int dx;
|
||||||
|
int dy;
|
||||||
|
|
||||||
|
var sx = (width - 1) >> 1;
|
||||||
|
var sy = (height - 1) >> 1;
|
||||||
|
|
||||||
|
if (width == height || direction == Directions.North)
|
||||||
|
{
|
||||||
|
dx = -sx;
|
||||||
|
dy = -sy;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
switch (direction)
|
||||||
|
{
|
||||||
|
case Directions.West:
|
||||||
|
dx = sy - (height - 1);
|
||||||
|
dy = -sx;
|
||||||
|
(width, height) = (height, width);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Directions.South:
|
||||||
|
dx = sx - (width - 1);
|
||||||
|
dy = sy - (height - 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case Directions.East:
|
||||||
|
dx = -sy;
|
||||||
|
dy = sx - (width - 1);
|
||||||
|
(width, height) = (height, width);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new ArgumentOutOfRangeException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new TileRect(rectCenter + new int2(dx, dy), width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void WalkLine(int2 startTile, int2 endTile, Action<int2> tileAction)
|
||||||
|
{
|
||||||
|
var dx = endTile.x - startTile.x;
|
||||||
|
var dy = endTile.y - startTile.y;
|
||||||
|
if (math.abs(dx) >= math.abs(dy))
|
||||||
|
endTile.y = startTile.y;
|
||||||
|
else
|
||||||
|
endTile.x = startTile.x;
|
||||||
|
|
||||||
|
var tile = startTile;
|
||||||
|
var d = math.sign(endTile - startTile);
|
||||||
|
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
tileAction.Invoke(tile);
|
||||||
|
|
||||||
|
if (math.all(tile == endTile)) break;
|
||||||
|
|
||||||
|
tile += d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static int StepCount(int2 tile, int2 otherTile)
|
||||||
|
{
|
||||||
|
var delta = math.abs(tile - otherTile);
|
||||||
|
return math.max(delta.x, delta.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static int StepCount(int2 tile, in TileRect rect)
|
||||||
|
{
|
||||||
|
var left = math.max(rect.Min.x - tile.x, 0);
|
||||||
|
var right = math.max(tile.x - rect.Max.x, 0);
|
||||||
|
var dx = math.max(left, right);
|
||||||
|
|
||||||
|
var bottom = math.max(rect.Min.y - tile.y, 0);
|
||||||
|
var top = math.max(tile.y - rect.Max.y, 0);
|
||||||
|
var dy = math.max(bottom, top);
|
||||||
|
|
||||||
|
return math.max(dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static int StepCount(in TileRect rect, int2 tile)
|
||||||
|
{
|
||||||
|
return StepCount(tile, rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
public static int StepCount(in TileRect rect, in TileRect otherRect)
|
||||||
|
{
|
||||||
|
var left = math.max(otherRect.Min.x - rect.Max.x, 0);
|
||||||
|
var right = math.max(rect.Min.x - otherRect.Max.x, 0);
|
||||||
|
var dx = math.max(left, right);
|
||||||
|
|
||||||
|
var bottom = math.max(otherRect.Min.y - rect.Max.y, 0);
|
||||||
|
var top = math.max(rect.Min.y - otherRect.Max.y, 0);
|
||||||
|
var dy = math.max(bottom, top);
|
||||||
|
|
||||||
|
return math.max(dx, dy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
Source/Riversong/Game/CommonServices/TileMath/TileRange.cs
Normal file
88
Source/Riversong/Game/CommonServices/TileMath/TileRange.cs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public struct TileRange : IEnumerable<int2>
|
||||||
|
{
|
||||||
|
public int2 Min;
|
||||||
|
|
||||||
|
public int2 Max;
|
||||||
|
|
||||||
|
private TileRange(int2 min, int2 max)
|
||||||
|
{
|
||||||
|
Min = min;
|
||||||
|
Max = max;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TileRange From(in TileRect rect)
|
||||||
|
{
|
||||||
|
return new TileRange(rect.Min, rect.Max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TileRange From(int2 min, int2 max)
|
||||||
|
{
|
||||||
|
return From(new TileRect(min, max));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Enumerator GetEnumerator()
|
||||||
|
{
|
||||||
|
return new Enumerator(Min, Max);
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator<int2> IEnumerable<int2>.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
IEnumerator IEnumerable.GetEnumerator()
|
||||||
|
{
|
||||||
|
return GetEnumerator();
|
||||||
|
}
|
||||||
|
|
||||||
|
public struct Enumerator : IEnumerator<int2>
|
||||||
|
{
|
||||||
|
public int2 Min;
|
||||||
|
|
||||||
|
public int2 Max;
|
||||||
|
|
||||||
|
public int2 Current { get; private set; }
|
||||||
|
|
||||||
|
object IEnumerator.Current => Current;
|
||||||
|
|
||||||
|
public Enumerator(int2 min, int2 max) : this()
|
||||||
|
{
|
||||||
|
Min = min;
|
||||||
|
Max = max;
|
||||||
|
Reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool MoveNext()
|
||||||
|
{
|
||||||
|
if (Current.x < Max.x)
|
||||||
|
{
|
||||||
|
Current = new int2(Current.x + 1, Current.y);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Current.y < Max.y)
|
||||||
|
{
|
||||||
|
Current = new int2(Min.x, Current.y + 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Reset()
|
||||||
|
{
|
||||||
|
Current = new int2(Min.x - 1, Min.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
71
Source/Riversong/Game/CommonServices/TileMath/TileRect.cs
Normal file
71
Source/Riversong/Game/CommonServices/TileMath/TileRect.cs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public struct TileRect
|
||||||
|
{
|
||||||
|
public static readonly TileRect Empty = new(int2.zero, int2.zero);
|
||||||
|
|
||||||
|
public static readonly TileRect Everything = new(int.MinValue, int.MaxValue);
|
||||||
|
|
||||||
|
private int2 _min;
|
||||||
|
|
||||||
|
private int2 _max;
|
||||||
|
|
||||||
|
public int2 Min
|
||||||
|
{
|
||||||
|
readonly get => _min;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_min = value;
|
||||||
|
_max = math.max(_min, _max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int2 Max
|
||||||
|
{
|
||||||
|
readonly get => _max;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
_max = value;
|
||||||
|
_min = math.min(_min, _max);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Width => _max.x - _min.x + 1;
|
||||||
|
|
||||||
|
public int Height => _max.y - _min.y + 1;
|
||||||
|
|
||||||
|
public int2 Center => (_min + _max) / 2;
|
||||||
|
|
||||||
|
public TileRect(int2 min, int2 max)
|
||||||
|
{
|
||||||
|
_min = math.min(min, max);
|
||||||
|
_max = math.max(min, max);
|
||||||
|
}
|
||||||
|
|
||||||
|
public TileRect(int2 min, int width, int height) : this(min, min + new int2(width - 1, height - 1))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly bool Contains(int2 tile)
|
||||||
|
{
|
||||||
|
return tile.x >= Min.x && tile.x <= Max.x && tile.y >= Min.y && tile.y <= Max.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly bool Intersects(TileRect other)
|
||||||
|
{
|
||||||
|
return !(other.Max.x < Min.x || other.Min.x > Max.x || other.Max.y < Min.y || other.Min.y > Max.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public readonly TileRect Inflate(int amount)
|
||||||
|
{
|
||||||
|
return amount == int.MaxValue ? Everything : new TileRect(Min - amount, Max + amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TileRect OneTile(int2 tile)
|
||||||
|
{
|
||||||
|
return new TileRect(tile, 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
38
Source/Riversong/Game/CommonServices/TileMath/TileSpace.cs
Normal file
38
Source/Riversong/Game/CommonServices/TileMath/TileSpace.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using Unity.Mathematics;
|
||||||
|
using UnityEngine;
|
||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
public class TileSpace : ITileSpace
|
||||||
|
{
|
||||||
|
public float TileSize { get; set; }
|
||||||
|
|
||||||
|
public World World { get; set; }
|
||||||
|
|
||||||
|
public int2 WorldToTile(Vector3 position)
|
||||||
|
{
|
||||||
|
var tileX = Mathf.FloorToInt(position.x / TileSize);
|
||||||
|
var tileY = Mathf.FloorToInt(position.z / TileSize);
|
||||||
|
return new int2(tileX, tileY);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 TileToWorld(int2 tile, float tileX = 0.5f, float tileY = 0.5f)
|
||||||
|
{
|
||||||
|
tileX = Mathf.Clamp01(tileX);
|
||||||
|
tileY = Mathf.Clamp01(tileY);
|
||||||
|
return new Vector3((tile.x + tileX) * TileSize, World.Heightmap.GetValue(tile), (tile.y + tileY) * TileSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int GetElevation(float y)
|
||||||
|
{
|
||||||
|
return Mathf.RoundToInt(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Vector3 GetRectWorldCenter(in TileRect rect)
|
||||||
|
{
|
||||||
|
var worldCenter = TileToWorld(rect.Min, 0, 0);
|
||||||
|
worldCenter += new Vector3(rect.Width, 0, rect.Height) * (0.5f * TileSize);
|
||||||
|
return worldCenter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
44
Source/Riversong/Game/DebugCommands/DebugCommandsSystem.cs
Normal file
44
Source/Riversong/Game/DebugCommands/DebugCommandsSystem.cs
Normal 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
8
Source/Riversong/Game/DebugCommands/DebugSystemGroup.cs
Normal file
8
Source/Riversong/Game/DebugCommands/DebugSystemGroup.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
|
||||||
|
namespace DanieleMarotta.RiversongCodeShowcase
|
||||||
|
{
|
||||||
|
[InitializeAfter(typeof(UISystemGroup))]
|
||||||
|
public class DebugSystemGroup : GameSystemGroup
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
61
Source/Riversong/Game/DebugCommands/DrawGizmosSystem.cs
Normal file
61
Source/Riversong/Game/DebugCommands/DrawGizmosSystem.cs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user