Migrate settings to a more manageable structure and implement Service Locator pattern for runtime Addressables retrieval

This commit is contained in:
Michal Pikulski
2025-09-23 14:43:02 +02:00
parent 4b206b9b2e
commit 197aedddb0
33 changed files with 1179 additions and 44 deletions

View File

@@ -1,4 +1,6 @@
using UnityEngine;
using AppleHills.Core.Settings;
using System.Collections;
/// <summary>
/// Singleton manager for global game state and settings. Provides accessors for various gameplay parameters.
@@ -29,53 +31,115 @@ public class GameManager : MonoBehaviour
}
}
[Header("Game Settings")]
public GameSettings gameSettings;
[Header("Legacy Game Settings (Deprecated)")]
[Tooltip("This is only used for migration to the new settings system")]
public GameSettings legacyGameSettings;
[Header("Settings Status")]
[SerializeField] private bool _settingsLoaded = false;
void Awake()
{
_instance = this;
if (gameSettings == null)
{
gameSettings = Resources.Load<GameSettings>("DefaultSettings");
if (gameSettings == null)
{
Debug.LogError("GameSettings asset not found in Resources!");
}
}
// Create settings provider if it doesn't exist
SettingsProvider.Instance.gameObject.name = "Settings Provider";
// Load all settings
StartCoroutine(InitializeSettings());
// DontDestroyOnLoad(gameObject);
}
private IEnumerator InitializeSettings()
{
// Initialize the settings provider
var initComplete = false;
SettingsProvider.Instance.PreloadAllSettings(() => initComplete = true);
// Wait for settings to be loaded
while (!initComplete)
{
yield return null;
}
// Register settings with service locator
ServiceLocator.Register<IPlayerFollowerSettings>(
SettingsProvider.Instance.GetSettings<PlayerFollowerSettings>());
ServiceLocator.Register<IInteractionSettings>(
SettingsProvider.Instance.GetSettings<InteractionSettings>());
ServiceLocator.Register<IMinigameSettings>(
SettingsProvider.Instance.GetSettings<MinigameSettings>());
// Log success
Debug.Log("All settings loaded and registered with ServiceLocator");
_settingsLoaded = true;
// Migrate settings if needed
if (legacyGameSettings != null)
{
MigrateFromLegacySettings();
}
}
private void MigrateFromLegacySettings()
{
// This method can be used to copy settings from the old GameSettings to the new system
// Implement if needed for your production environment
Debug.Log("Legacy settings migration available but not implemented.");
}
void OnApplicationQuit()
{
_isQuitting = true;
ServiceLocator.Clear();
}
// Accessors for game settings
public float PlayerStopDistance => gameSettings != null ? gameSettings.playerStopDistance : 1.0f;
public float FollowerPickupDelay => gameSettings != null ? gameSettings.followerPickupDelay : 0.2f;
public float FollowDistance => gameSettings != null ? gameSettings.followDistance : 1.5f;
public float ManualMoveSmooth => gameSettings != null ? gameSettings.manualMoveSmooth : 8f;
public float ThresholdFar => gameSettings != null ? gameSettings.thresholdFar : 2.5f;
public float ThresholdNear => gameSettings != null ? gameSettings.thresholdNear : 0.5f;
public float StopThreshold => gameSettings != null ? gameSettings.stopThreshold : 0.5f;
public float MoveSpeed => gameSettings != null ? gameSettings.moveSpeed : 5f;
public float StopDistance => gameSettings != null ? gameSettings.stopDistance : 0.1f;
public bool UseRigidbody => gameSettings != null ? gameSettings.useRigidbody : true;
public float FollowUpdateInterval => gameSettings != null ? gameSettings.followUpdateInterval : 0.1f;
public float FollowerSpeedMultiplier => gameSettings != null ? gameSettings.followerSpeedMultiplier : 1.2f;
public float HeldIconDisplayHeight => gameSettings != null ? gameSettings.heldIconDisplayHeight : 2.0f;
public GameObject BasePickupPrefab => gameSettings != null ? gameSettings.basePickupPrefab : null;
public LayerMask InteractableLayerMask => gameSettings != null ? gameSettings.interactableLayerMask : -1;
public float PlayerStopDistanceDirectInteraction => gameSettings != null ? gameSettings.playerStopDistanceDirectInteraction : 2.0f;
// Helper method to get settings
private T GetSettings<T>() where T : class
{
return ServiceLocator.Get<T>();
}
// PLAYER & FOLLOWER SETTINGS
// Player settings
public float MoveSpeed => GetSettings<IPlayerFollowerSettings>()?.MoveSpeed ?? 5f;
public float StopDistance => GetSettings<IPlayerFollowerSettings>()?.StopDistance ?? 0.1f;
public bool UseRigidbody => GetSettings<IPlayerFollowerSettings>()?.UseRigidbody ?? true;
public GameSettings.HoldMovementMode DefaultHoldMovementMode =>
GetSettings<IPlayerFollowerSettings>()?.DefaultHoldMovementMode ?? GameSettings.HoldMovementMode.Pathfinding;
// Follower settings
public float FollowDistance => GetSettings<IPlayerFollowerSettings>()?.FollowDistance ?? 1.5f;
public float ManualMoveSmooth => GetSettings<IPlayerFollowerSettings>()?.ManualMoveSmooth ?? 8f;
public float ThresholdFar => GetSettings<IPlayerFollowerSettings>()?.ThresholdFar ?? 2.5f;
public float ThresholdNear => GetSettings<IPlayerFollowerSettings>()?.ThresholdNear ?? 0.5f;
public float StopThreshold => GetSettings<IPlayerFollowerSettings>()?.StopThreshold ?? 0.1f;
public float FollowUpdateInterval => GetSettings<IPlayerFollowerSettings>()?.FollowUpdateInterval ?? 0.1f;
public float FollowerSpeedMultiplier => GetSettings<IPlayerFollowerSettings>()?.FollowerSpeedMultiplier ?? 1.2f;
public float HeldIconDisplayHeight => GetSettings<IPlayerFollowerSettings>()?.HeldIconDisplayHeight ?? 2.0f;
// INTERACTION SETTINGS
public float PlayerStopDistance => GetSettings<IInteractionSettings>()?.PlayerStopDistance ?? 6.0f;
public float PlayerStopDistanceDirectInteraction => GetSettings<IInteractionSettings>()?.PlayerStopDistanceDirectInteraction ?? 2.0f;
public float FollowerPickupDelay => GetSettings<IInteractionSettings>()?.FollowerPickupDelay ?? 0.2f;
public LayerMask InteractableLayerMask => GetSettings<IInteractionSettings>()?.InteractableLayerMask ?? -1;
public GameObject BasePickupPrefab => GetSettings<IInteractionSettings>()?.BasePickupPrefab;
public GameObject LevelSwitchMenuPrefab => GetSettings<IInteractionSettings>()?.LevelSwitchMenuPrefab;
/// <summary>
/// Returns the combination rule for two items, if any.
/// </summary>
public GameSettings.CombinationRule GetCombinationRule(PickupItemData item1, PickupItemData item2)
{
if (gameSettings == null || gameSettings.combinationRules == null) return null;
foreach (var rule in gameSettings.combinationRules)
var settings = GetSettings<IInteractionSettings>();
if (settings == null || settings.CombinationRules == null) return null;
foreach (var rule in settings.CombinationRules)
{
if ((PickupItemData.AreEquivalent(rule.itemA, item1) && PickupItemData.AreEquivalent(rule.itemB, item2)) ||
(PickupItemData.AreEquivalent(rule.itemA, item2) && PickupItemData.AreEquivalent(rule.itemB, item1)))
@@ -91,20 +155,23 @@ public class GameManager : MonoBehaviour
/// </summary>
public GameSettings.SlotItemConfig GetSlotItemConfig(PickupItemData slotItem)
{
if (gameSettings == null || gameSettings.slotItemConfigs == null || slotItem == null) return null;
foreach (var config in gameSettings.slotItemConfigs)
var settings = GetSettings<IInteractionSettings>();
if (settings == null || settings.SlotItemConfigs == null || slotItem == null) return null;
foreach (var config in settings.SlotItemConfigs)
{
if (PickupItemData.AreEquivalent(slotItem, config.slotItem))
return config;
}
return null;
}
// Add more accessors as needed
public float EndlessDescenderLerpSpeed => gameSettings != null ? gameSettings.endlessDescenderLerpSpeed : 12f;
public float EndlessDescenderMaxOffset => gameSettings != null ? gameSettings.endlessDescenderMaxOffset : 3f;
public float EndlessDescenderClampXMin => gameSettings != null ? gameSettings.endlessDescenderClampXMin : -5f;
public float EndlessDescenderClampXMax => gameSettings != null ? gameSettings.endlessDescenderClampXMax : 5f;
public float EndlessDescenderSpeedExponent => gameSettings != null ? gameSettings.endlessDescenderSpeedExponent : 2.5f;
public GameSettings.HoldMovementMode DefaultHoldMovementMode => gameSettings != null ? gameSettings.defaultHoldMovementMode : GameSettings.HoldMovementMode.Pathfinding;
public GameObject LevelSwitchMenuPrefab => gameSettings != null ? gameSettings.levelSwitchMenuPrefab : null;
// MINIGAME SETTINGS
// Endless Descender settings
public float EndlessDescenderLerpSpeed => GetSettings<IMinigameSettings>()?.EndlessDescenderLerpSpeed ?? 12f;
public float EndlessDescenderMaxOffset => GetSettings<IMinigameSettings>()?.EndlessDescenderMaxOffset ?? 3f;
public float EndlessDescenderClampXMin => GetSettings<IMinigameSettings>()?.EndlessDescenderClampXMin ?? -3.5f;
public float EndlessDescenderClampXMax => GetSettings<IMinigameSettings>()?.EndlessDescenderClampXMax ?? 3.5f;
public float EndlessDescenderSpeedExponent => GetSettings<IMinigameSettings>()?.EndlessDescenderSpeedExponent ?? 2.5f;
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: e380783135324fcd925048783e01d691
timeCreated: 1758619858

View File

@@ -0,0 +1,16 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Base class for all settings ScriptableObjects.
/// Provides common functionality for all settings types.
/// </summary>
public abstract class BaseSettings : ScriptableObject
{
public virtual void OnValidate()
{
// Override in derived classes to add validation
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: cd33ef6036eb49358acbbd50dfd9bb13
timeCreated: 1758619858

View File

@@ -0,0 +1,48 @@
using System.Collections.Generic;
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Settings related to interactions and items
/// </summary>
[CreateAssetMenu(fileName = "InteractionSettings", menuName = "AppleHills/Settings/Interaction & Items", order = 2)]
public class InteractionSettings : BaseSettings, IInteractionSettings
{
[Header("Interactions")]
[SerializeField] private float playerStopDistance = 6.0f;
[SerializeField] private float playerStopDistanceDirectInteraction = 2.0f;
[SerializeField] private float followerPickupDelay = 0.2f;
[Header("InputManager Settings")]
[Tooltip("Layer(s) to use for interactable objects.")]
[SerializeField] private LayerMask interactableLayerMask = -1; // Default to Everything
[Header("Default Prefabs")]
[SerializeField] private GameObject basePickupPrefab;
[SerializeField] private GameObject levelSwitchMenuPrefab;
[Header("Item Configuration")]
[SerializeField] private List<GameSettings.CombinationRule> combinationRules = new List<GameSettings.CombinationRule>();
[SerializeField] private List<GameSettings.SlotItemConfig> slotItemConfigs = new List<GameSettings.SlotItemConfig>();
// IInteractionSettings implementation
public float PlayerStopDistance => playerStopDistance;
public float PlayerStopDistanceDirectInteraction => playerStopDistanceDirectInteraction;
public float FollowerPickupDelay => followerPickupDelay;
public LayerMask InteractableLayerMask => interactableLayerMask;
public GameObject BasePickupPrefab => basePickupPrefab;
public GameObject LevelSwitchMenuPrefab => levelSwitchMenuPrefab;
public List<GameSettings.CombinationRule> CombinationRules => combinationRules;
public List<GameSettings.SlotItemConfig> SlotItemConfigs => slotItemConfigs;
public override void OnValidate()
{
base.OnValidate();
// Validate values
playerStopDistance = Mathf.Max(0.1f, playerStopDistance);
playerStopDistanceDirectInteraction = Mathf.Max(0.1f, playerStopDistanceDirectInteraction);
followerPickupDelay = Mathf.Max(0f, followerPickupDelay);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: ac22b092dc6f4db5b3dad35172b6e4c4
timeCreated: 1758619914

View File

@@ -0,0 +1,49 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Settings related to minigames
/// </summary>
[CreateAssetMenu(fileName = "MinigameSettings", menuName = "AppleHills/Settings/Minigames", order = 3)]
public class MinigameSettings : BaseSettings, IMinigameSettings
{
[Header("Endless Descender Settings")]
[Tooltip("How quickly the character follows the finger horizontally (higher = more responsive)")]
[SerializeField] private float endlessDescenderLerpSpeed = 12f;
[Tooltip("Maximum horizontal offset allowed between character and finger position")]
[SerializeField] private float endlessDescenderMaxOffset = 3f;
[Tooltip("Minimum allowed X position for endless descender movement")]
[SerializeField] private float endlessDescenderClampXMin = -3.5f;
[Tooltip("Maximum allowed X position for endless descender movement")]
[SerializeField] private float endlessDescenderClampXMax = 3.5f;
[Tooltip("Exponent for speed drop-off curve (higher = sharper drop near target)")]
[SerializeField] private float endlessDescenderSpeedExponent = 2.5f;
// IMinigameSettings implementation
public float EndlessDescenderLerpSpeed => endlessDescenderLerpSpeed;
public float EndlessDescenderMaxOffset => endlessDescenderMaxOffset;
public float EndlessDescenderClampXMin => endlessDescenderClampXMin;
public float EndlessDescenderClampXMax => endlessDescenderClampXMax;
public float EndlessDescenderSpeedExponent => endlessDescenderSpeedExponent;
public override void OnValidate()
{
base.OnValidate();
// Validate values
endlessDescenderLerpSpeed = Mathf.Max(0.1f, endlessDescenderLerpSpeed);
endlessDescenderMaxOffset = Mathf.Max(0.1f, endlessDescenderMaxOffset);
endlessDescenderSpeedExponent = Mathf.Max(0.1f, endlessDescenderSpeedExponent);
// Ensure min is less than max
if (endlessDescenderClampXMin >= endlessDescenderClampXMax)
{
endlessDescenderClampXMin = endlessDescenderClampXMax - 0.1f;
}
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0ce4dba7a1c54e73b1b3d7131a1c0570
timeCreated: 1758619927

View File

@@ -0,0 +1,53 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Settings related to player and follower behavior
/// </summary>
[CreateAssetMenu(fileName = "PlayerFollowerSettings", menuName = "AppleHills/Settings/Player & Follower", order = 1)]
public class PlayerFollowerSettings : BaseSettings, IPlayerFollowerSettings
{
[Header("Player Settings")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float stopDistance = 0.1f;
[SerializeField] private bool useRigidbody = true;
[SerializeField] private GameSettings.HoldMovementMode defaultHoldMovementMode = GameSettings.HoldMovementMode.Pathfinding;
[Header("Follower Settings")]
[SerializeField] private float followDistance = 1.5f;
[SerializeField] private float manualMoveSmooth = 8f;
[SerializeField] private float thresholdFar = 2.5f;
[SerializeField] private float thresholdNear = 0.5f;
[SerializeField] private float stopThreshold = 0.1f;
[Header("Backend Settings")]
[Tooltip("Technical parameters, not for design tuning")]
[SerializeField] private float followUpdateInterval = 0.1f;
[SerializeField] private float followerSpeedMultiplier = 1.2f;
[SerializeField] private float heldIconDisplayHeight = 2.0f;
// IPlayerFollowerSettings implementation
public float MoveSpeed => moveSpeed;
public float StopDistance => stopDistance;
public bool UseRigidbody => useRigidbody;
public GameSettings.HoldMovementMode DefaultHoldMovementMode => defaultHoldMovementMode;
public float FollowDistance => followDistance;
public float ManualMoveSmooth => manualMoveSmooth;
public float ThresholdFar => thresholdFar;
public float ThresholdNear => thresholdNear;
public float StopThreshold => stopThreshold;
public float FollowUpdateInterval => followUpdateInterval;
public float FollowerSpeedMultiplier => followerSpeedMultiplier;
public float HeldIconDisplayHeight => heldIconDisplayHeight;
public override void OnValidate()
{
base.OnValidate();
// Validate values
moveSpeed = Mathf.Max(0.1f, moveSpeed);
followDistance = Mathf.Max(0.1f, followDistance);
followerSpeedMultiplier = Mathf.Max(0.1f, followerSpeedMultiplier);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 32cd6d14d9304d5ba0fd590da1346654
timeCreated: 1758619904

View File

@@ -0,0 +1,51 @@
using System;
using System.Collections.Generic;
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Service Locator implementation for managing settings services.
/// Provides a central registry for all settings services.
/// </summary>
public static class ServiceLocator
{
private static readonly Dictionary<Type, object> _services = new Dictionary<Type, object>();
/// <summary>
/// Register a service with the service locator.
/// </summary>
/// <typeparam name="T">The interface type for the service</typeparam>
/// <param name="service">The service implementation</param>
public static void Register<T>(T service) where T : class
{
_services[typeof(T)] = service;
Debug.Log($"Service registered: {typeof(T).Name}");
}
/// <summary>
/// Get a service from the service locator.
/// </summary>
/// <typeparam name="T">The interface type for the service</typeparam>
/// <returns>The service implementation, or null if not found</returns>
public static T Get<T>() where T : class
{
if (_services.TryGetValue(typeof(T), out object service))
{
return service as T;
}
Debug.LogWarning($"Service of type {typeof(T).Name} not found!");
return null;
}
/// <summary>
/// Clear all registered services.
/// </summary>
public static void Clear()
{
_services.Clear();
Debug.Log("All services cleared");
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 16cc39d2f99b4e7fa65c4a8b39f3e87c
timeCreated: 1758619866

View File

@@ -0,0 +1,54 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Interface for player and follower settings
/// </summary>
public interface IPlayerFollowerSettings
{
// Player settings
float MoveSpeed { get; }
float StopDistance { get; }
bool UseRigidbody { get; }
GameSettings.HoldMovementMode DefaultHoldMovementMode { get; }
// Follower settings
float FollowDistance { get; }
float ManualMoveSmooth { get; }
float ThresholdFar { get; }
float ThresholdNear { get; }
float StopThreshold { get; }
float FollowUpdateInterval { get; }
float FollowerSpeedMultiplier { get; }
float HeldIconDisplayHeight { get; }
}
/// <summary>
/// Interface for interaction and item settings
/// </summary>
public interface IInteractionSettings
{
float PlayerStopDistance { get; }
float PlayerStopDistanceDirectInteraction { get; }
float FollowerPickupDelay { get; }
LayerMask InteractableLayerMask { get; }
GameObject BasePickupPrefab { get; }
GameObject LevelSwitchMenuPrefab { get; }
System.Collections.Generic.List<GameSettings.CombinationRule> CombinationRules { get; }
System.Collections.Generic.List<GameSettings.SlotItemConfig> SlotItemConfigs { get; }
}
/// <summary>
/// Interface for minigame settings
/// </summary>
public interface IMinigameSettings
{
// Endless Descender settings
float EndlessDescenderLerpSpeed { get; }
float EndlessDescenderMaxOffset { get; }
float EndlessDescenderClampXMin { get; }
float EndlessDescenderClampXMax { get; }
float EndlessDescenderSpeedExponent { get; }
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 54611ae012ab4455a53bd60961d9e7ea
timeCreated: 1758619892

View File

@@ -0,0 +1,106 @@
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.ResourceManagement.AsyncOperations;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Responsible for loading and caching settings from Addressables.
/// </summary>
public class SettingsProvider : MonoBehaviour
{
private static SettingsProvider _instance;
private Dictionary<string, BaseSettings> _settingsCache = new Dictionary<string, BaseSettings>();
// Singleton instance
public static SettingsProvider Instance
{
get
{
if (_instance == null)
{
GameObject go = new GameObject("Settings Provider");
_instance = go.AddComponent<SettingsProvider>();
DontDestroyOnLoad(go);
}
return _instance;
}
}
private void Awake()
{
if (_instance == null)
{
_instance = this;
DontDestroyOnLoad(gameObject);
}
else if (_instance != this)
{
Destroy(gameObject);
}
}
/// <summary>
/// Load settings asynchronously using Addressables
/// </summary>
public void LoadSettings<T>(Action<T> onLoaded) where T : BaseSettings
{
string key = typeof(T).Name;
// Return from cache if already loaded
if (_settingsCache.TryGetValue(key, out BaseSettings cachedSettings))
{
onLoaded?.Invoke(cachedSettings as T);
return;
}
// Load using Addressables
Addressables.LoadAssetAsync<T>($"Settings/{key}.asset").Completed += handle =>
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
_settingsCache[key] = handle.Result;
onLoaded?.Invoke(handle.Result);
}
else
{
Debug.LogError($"Failed to load settings: {key}");
onLoaded?.Invoke(null);
}
};
}
/// <summary>
/// Get cached settings
/// </summary>
public T GetSettings<T>() where T : BaseSettings
{
string key = typeof(T).Name;
if (_settingsCache.TryGetValue(key, out BaseSettings settings))
{
return settings as T;
}
return null;
}
/// <summary>
/// Preload all settings - call this at game startup
/// </summary>
public void PreloadAllSettings(Action onComplete)
{
// Load all necessary settings types
int pendingLoads = 3; // Number of settings types
Action decrementCounter = () => {
pendingLoads--;
if (pendingLoads <= 0)
onComplete?.Invoke();
};
LoadSettings<PlayerFollowerSettings>(settings => decrementCounter());
LoadSettings<InteractionSettings>(settings => decrementCounter());
LoadSettings<MinigameSettings>(settings => decrementCounter());
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 4d212b25192045d198f2bf42ef74f278
timeCreated: 1758619879