Revamp the settings system (#7)

- A Settings Provider system to utilize addressables for loading settings at runtime
- An editor UI for easy modifications of the settings objects
- A split out developer settings functionality to keep gameplay and nitty-gritty details separately
- Most settings migrated out of game objects and into the new system
- An additional Editor utility for fetching the settings at editor runtime, for gizmos, visualization etc

Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com>
Co-authored-by: AlexanderT <alexander@foolhardyhorizons.com>
Reviewed-on: #7
This commit is contained in:
2025-09-24 13:33:43 +00:00
parent 4b206b9b2e
commit 63cb3f1a8c
77 changed files with 2795 additions and 978 deletions

View File

@@ -0,0 +1,19 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Base abstract class for all developer settings.
/// Developer settings are intended for technical configuration rather than gameplay/design values.
/// </summary>
public abstract class BaseDeveloperSettings : ScriptableObject
{
/// <summary>
/// Called to validate settings values when they are changed in the inspector.
/// </summary>
public virtual void OnValidate()
{
// Base implementation does nothing, override in derived classes
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 50def2e6e95a4830b57f3e1b76a4df51
timeCreated: 1758707161

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,116 @@
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using System;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Provides access to developer settings for technical configuration rather than gameplay parameters.
/// Follows the singleton pattern for global access.
/// </summary>
public class DeveloperSettingsProvider : MonoBehaviour
{
private static DeveloperSettingsProvider _instance;
/// <summary>
/// Singleton instance of the provider.
/// </summary>
public static DeveloperSettingsProvider Instance
{
get
{
if (_instance == null && Application.isPlaying)
{
_instance = FindFirstObjectByType<DeveloperSettingsProvider>();
if (_instance == null)
{
GameObject go = new GameObject("DeveloperSettingsProvider");
_instance = go.AddComponent<DeveloperSettingsProvider>();
// Don't destroy between scenes
DontDestroyOnLoad(go);
}
}
return _instance;
}
}
// Dictionary to cache loaded settings
private Dictionary<System.Type, BaseDeveloperSettings> _settingsCache = new Dictionary<System.Type, BaseDeveloperSettings>();
// Path prefix for addressable developer settings
[SerializeField] private string _addressablePath = "Settings/Developer";
private void Awake()
{
if (_instance != null && _instance != this)
{
Destroy(gameObject);
return;
}
_instance = this;
DontDestroyOnLoad(gameObject);
// Initialize settings cache
_settingsCache = new Dictionary<System.Type, BaseDeveloperSettings>();
}
/// <summary>
/// Gets or loads developer settings of the specified type.
/// </summary>
/// <typeparam name="T">Type of developer settings to retrieve</typeparam>
/// <returns>The settings instance or null if not found</returns>
public T GetSettings<T>() where T : BaseDeveloperSettings
{
System.Type type = typeof(T);
// Return from cache if available
if (_settingsCache.TryGetValue(type, out BaseDeveloperSettings cachedSettings))
{
return cachedSettings as T;
}
// Load from Addressables if not cached
string key = $"{_addressablePath}/{type.Name}";
try
{
T settings = Addressables.LoadAssetAsync<T>(key).WaitForCompletion();
if (settings != null)
{
_settingsCache[type] = settings;
return settings;
}
}
catch (Exception e)
{
Debug.LogError($"Failed to load developer settings at '{key}': {e.Message}");
}
Debug.LogWarning($"Developer settings of type {type.Name} not found at addressable path '{key}'");
// Fallback to Resources for backward compatibility
T resourcesSettings = Resources.Load<T>($"{_addressablePath}/{type.Name}");
if (resourcesSettings != null)
{
Debug.Log($"Found developer settings in Resources instead of Addressables at '{_addressablePath}/{type.Name}'");
_settingsCache[type] = resourcesSettings;
return resourcesSettings;
}
return null;
}
/// <summary>
/// Clears the settings cache, forcing settings to be reloaded.
/// </summary>
public void ClearCache()
{
_settingsCache.Clear();
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: f9945aa4a563434e973ab49176259150
timeCreated: 1758707186

View File

@@ -0,0 +1,246 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Enum defining the type of bump response when player collides with obstacles
/// </summary>
public enum BumpMode
{
Impulse = 0,
SmoothToCenter = 1
}
/// <summary>
/// Developer settings for the diving minigame technical configuration.
/// These settings are separate from gameplay/design settings and focus on technical implementation details.
/// </summary>
[CreateAssetMenu(fileName = "DivingDeveloperSettings", menuName = "AppleHills/Developer Settings/Diving", order = 1)]
public class DivingDeveloperSettings : BaseDeveloperSettings
{
[Header("Bubble System")]
[Tooltip("Object pooling enabled for bubbles")]
[SerializeField] private bool bubbleUseObjectPooling = true;
[Tooltip("Initial number of bubbles to pre-allocate in pool")]
[SerializeField] private int bubbleInitialPoolSize = 10;
[Tooltip("Maximum number of bubbles allowed in pool")]
[SerializeField] private int bubbleMaxPoolSize = 30;
[Tooltip("Default spawn interval for bubbles in seconds")]
[SerializeField] private float bubbleSpawnInterval = 0.3f;
[Tooltip("Range of possible bubble movement speeds (min, max)")]
[SerializeField] private Vector2 bubbleSpeedRange = new Vector2(0.5f, 2f);
[Tooltip("Range of possible bubble scale factors (min, max)")]
[SerializeField] private Vector2 bubbleScaleRange = new Vector2(0.3f, 0.7f);
[Tooltip("Range of possible bubble wobble speeds (min, max)")]
[SerializeField] private Vector2 bubbleWobbleSpeedRange = new Vector2(1f, 3f);
[Tooltip("Range of possible bubble wobble amounts (min, max)")]
[SerializeField] private Vector2 bubbleWobbleAmountRange = new Vector2(0.05f, 0.15f);
[Tooltip("Minimum X position for bubble spawning")]
[SerializeField] private float bubbleSpawnXMin = -3.5f;
[Tooltip("Maximum X position for bubble spawning")]
[SerializeField] private float bubbleSpawnXMax = 3.5f;
[Tooltip("Y position for bubble spawning")]
[SerializeField] private float bubbleSpawnY = -5f;
[Tooltip("Minimum scale factor during wobble animation")]
[SerializeField] private float bubbleWobbleMinScale = 0.2f;
[Tooltip("Maximum scale factor during wobble animation")]
[SerializeField] private float bubbleWobbleMaxScale = 1.2f;
[Tooltip("Factor to multiply bubble speed by when surfacing")]
[SerializeField] private float bubbleSurfacingSpeedFactor = 0.5f;
[Header("Obstacle System")]
[Tooltip("Layer for obstacles to be placed on")]
[Layer]
[SerializeField] private int obstacleLayer = 9;
[Tooltip("Whether to use object pooling for obstacles")]
[SerializeField] private bool obstacleUseObjectPooling = true;
[Tooltip("Maximum objects per prefab type in obstacle pool")]
[SerializeField] private int obstacleMaxPerPrefabPoolSize = 3;
[Tooltip("Total maximum size of obstacle pool across all prefab types")]
[SerializeField] private int obstacleTotalMaxPoolSize = 15;
[Header("Trench Tile System")]
[Tooltip("Layer for trench tiles to be placed on")]
[Layer]
[SerializeField] private int trenchTileLayer = 13; // QuarryTrenchTile layer
[Tooltip("Whether to use object pooling for trench tiles")]
[SerializeField] private bool trenchTileUseObjectPooling = true;
[Tooltip("Maximum objects per prefab type in trench tile pool")]
[SerializeField] private int trenchTileMaxPerPrefabPoolSize = 2;
[Tooltip("Total maximum size of trench tile pool across all prefab types")]
[SerializeField] private int trenchTileTotalMaxPoolSize = 10;
[Header("Player Blink Behavior")]
[Tooltip("Color to blink to when taking damage (typically red for damage indication)")]
[SerializeField] private Color playerBlinkDamageColor = Color.red;
[Tooltip("How fast to blink between normal and damage colors (seconds between color changes)")]
[SerializeField] private float playerBlinkRate = 0.15f;
[Tooltip("Alpha value for the damage color (0 = transparent, 1 = opaque)")]
[Range(0f, 1f)]
[SerializeField] private float playerDamageColorAlpha = 0.7f;
[Header("Player Wobble Behavior")]
[Tooltip("Frequency of wobble (higher = faster rocking)")]
[SerializeField] private float playerWobbleFrequency = 1.5f;
[Tooltip("Base wobble amplitude in degrees from horizontal")]
[SerializeField] private float playerBaseWobbleAmplitude = 8f;
[Tooltip("How much speed affects amplitude")]
[SerializeField] private float playerSpeedToAmplitude = 2f;
[Tooltip("Maximum allowed rotation in degrees")]
[SerializeField] private float playerMaxRotationLimit = 45f;
[Tooltip("Frequency of vertical bobbing")]
[SerializeField] private float playerVerticalFrequency = 0.5f;
[Tooltip("How far the object moves up/down")]
[SerializeField] private float playerVerticalAmplitude = 0.5f;
[Tooltip("How quickly velocity changes are smoothed")]
[SerializeField] private float playerVelocitySmoothing = 10f;
[Tooltip("How quickly rotation is smoothed")]
[SerializeField] private float playerRotationSmoothing = 10f;
[Header("Collision Settings")]
[Tooltip("Layer mask for obstacle detection - configure which layers contain obstacles")]
[LayerMask]
[SerializeField] private LayerMask playerObstacleLayerMask = -1;
[Tooltip("Whether to block player input during damage immunity period")]
[SerializeField] private bool blockInputDuringImmunity = true;
[Tooltip("Type of bump response: 0=Impulse, 1=SmoothToCenter")]
[SerializeField] private BumpMode bumpMode = BumpMode.Impulse;
[Tooltip("Animation curve controlling bump movement over time")]
[SerializeField] private AnimationCurve bumpCurve = new AnimationCurve(
new Keyframe(0f, 0f, 0f, 2f),
new Keyframe(1f, 1f, 0f, 0f));
// Bubble properties access
public bool BubbleUseObjectPooling => bubbleUseObjectPooling;
public int BubbleInitialPoolSize => bubbleInitialPoolSize;
public int BubbleMaxPoolSize => bubbleMaxPoolSize;
public float BubbleSpawnInterval => bubbleSpawnInterval;
public Vector2 BubbleSpeedRange => bubbleSpeedRange;
public Vector2 BubbleScaleRange => bubbleScaleRange;
public Vector2 BubbleWobbleSpeedRange => bubbleWobbleSpeedRange;
public Vector2 BubbleWobbleAmountRange => bubbleWobbleAmountRange;
public float BubbleSpawnXMin => bubbleSpawnXMin;
public float BubbleSpawnXMax => bubbleSpawnXMax;
public float BubbleSpawnY => bubbleSpawnY;
public float BubbleWobbleMinScale => bubbleWobbleMinScale;
public float BubbleWobbleMaxScale => bubbleWobbleMaxScale;
public float BubbleSurfacingSpeedFactor => bubbleSurfacingSpeedFactor;
// Obstacle properties access
public int ObstacleLayer => obstacleLayer;
public bool ObstacleUseObjectPooling => obstacleUseObjectPooling;
public int ObstacleMaxPerPrefabPoolSize => obstacleMaxPerPrefabPoolSize;
public int ObstacleTotalMaxPoolSize => obstacleTotalMaxPoolSize;
// Trench Tile System properties
public int TrenchTileLayer => trenchTileLayer;
public bool TrenchTileUseObjectPooling => trenchTileUseObjectPooling;
public int TrenchTileMaxPerPrefabPoolSize => trenchTileMaxPerPrefabPoolSize;
public int TrenchTileTotalMaxPoolSize => trenchTileTotalMaxPoolSize;
// Player Blink Behavior properties
public Color PlayerBlinkDamageColor => playerBlinkDamageColor;
public float PlayerBlinkRate => playerBlinkRate;
public float PlayerDamageColorAlpha => playerDamageColorAlpha;
// Player Wobble Behavior properties
public float PlayerWobbleFrequency => playerWobbleFrequency;
public float PlayerBaseWobbleAmplitude => playerBaseWobbleAmplitude;
public float PlayerSpeedToAmplitude => playerSpeedToAmplitude;
public float PlayerMaxRotationLimit => playerMaxRotationLimit;
public float PlayerVerticalFrequency => playerVerticalFrequency;
public float PlayerVerticalAmplitude => playerVerticalAmplitude;
public float PlayerVelocitySmoothing => playerVelocitySmoothing;
public float PlayerRotationSmoothing => playerRotationSmoothing;
// Collision Settings properties
public LayerMask PlayerObstacleLayerMask => playerObstacleLayerMask;
public bool BlockInputDuringImmunity => blockInputDuringImmunity;
public BumpMode BumpMode => bumpMode;
public AnimationCurve BumpCurve => bumpCurve;
public override void OnValidate()
{
base.OnValidate();
// Validate bubble settings
bubbleInitialPoolSize = Mathf.Max(1, bubbleInitialPoolSize);
bubbleMaxPoolSize = Mathf.Max(bubbleInitialPoolSize, bubbleMaxPoolSize);
bubbleSpawnInterval = Mathf.Max(0.05f, bubbleSpawnInterval);
bubbleSpeedRange = new Vector2(
Mathf.Max(0.1f, bubbleSpeedRange.x),
Mathf.Max(bubbleSpeedRange.x, bubbleSpeedRange.y)
);
bubbleScaleRange = new Vector2(
Mathf.Max(0.1f, bubbleScaleRange.x),
Mathf.Max(bubbleScaleRange.x, bubbleScaleRange.y)
);
bubbleWobbleSpeedRange = new Vector2(
Mathf.Max(0.1f, bubbleWobbleSpeedRange.x),
Mathf.Max(bubbleWobbleSpeedRange.x, bubbleWobbleSpeedRange.y)
);
bubbleWobbleAmountRange = new Vector2(
Mathf.Max(0.01f, bubbleWobbleAmountRange.x),
Mathf.Max(bubbleWobbleAmountRange.x, bubbleWobbleAmountRange.y)
);
bubbleWobbleMinScale = Mathf.Max(0.01f, bubbleWobbleMinScale);
bubbleWobbleMaxScale = Mathf.Max(bubbleWobbleMinScale, bubbleWobbleMaxScale);
bubbleSurfacingSpeedFactor = Mathf.Max(0.01f, bubbleSurfacingSpeedFactor);
// Validate obstacle settings
obstacleMaxPerPrefabPoolSize = Mathf.Max(1, obstacleMaxPerPrefabPoolSize);
obstacleTotalMaxPoolSize = Mathf.Max(obstacleMaxPerPrefabPoolSize, obstacleTotalMaxPoolSize);
// Validate Trench Tile settings
trenchTileMaxPerPrefabPoolSize = Mathf.Max(1, trenchTileMaxPerPrefabPoolSize);
trenchTileTotalMaxPoolSize = Mathf.Max(trenchTileMaxPerPrefabPoolSize, trenchTileTotalMaxPoolSize);
// Validate Player Blink settings
playerBlinkRate = Mathf.Max(0.01f, playerBlinkRate);
playerDamageColorAlpha = Mathf.Clamp01(playerDamageColorAlpha);
// Validate Player Wobble settings
playerWobbleFrequency = Mathf.Max(0.01f, playerWobbleFrequency);
playerBaseWobbleAmplitude = Mathf.Max(0f, playerBaseWobbleAmplitude);
playerMaxRotationLimit = Mathf.Max(0f, playerMaxRotationLimit);
playerVerticalFrequency = Mathf.Max(0.01f, playerVerticalFrequency);
playerVerticalAmplitude = Mathf.Max(0f, playerVerticalAmplitude);
playerVelocitySmoothing = Mathf.Max(0.1f, playerVelocitySmoothing);
playerRotationSmoothing = Mathf.Max(0.1f, playerRotationSmoothing);
// Validate Collision settings
bumpMode = (BumpMode)Mathf.Clamp((int)bumpMode, 0, 1);
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 033961b12e7b4289838d554c2264bacd
timeCreated: 1758707215

View File

@@ -0,0 +1,234 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Settings related to minigames
/// </summary>
[CreateAssetMenu(fileName = "MinigameSettings", menuName = "AppleHills/Settings/Minigames", order = 3)]
public class DivingMinigameSettings : BaseSettings, IDivingMinigameSettings
{
[Header("Basic Movement")]
[Tooltip("How quickly the character follows the finger horizontally (higher = more responsive)")]
[SerializeField] private float lerpSpeed = 12f;
[Tooltip("Maximum horizontal offset allowed between character and finger position")]
[SerializeField] private float maxOffset = 3f;
[Tooltip("Minimum allowed X position for movement")]
[SerializeField] private float clampXMin = -3.5f;
[Tooltip("Maximum allowed X position for movement")]
[SerializeField] private float clampXMax = 3.5f;
[Tooltip("Exponent for speed drop-off curve (higher = sharper drop near target)")]
[SerializeField] private float speedExponent = 2.5f;
[Header("Player Movement")]
[Tooltip("Maximum distance the player can move from a single tap")]
[SerializeField] private float tapMaxDistance = 0.5f;
[Tooltip("How quickly the tap impulse fades (higher = faster stop)")]
[SerializeField] private float tapDecelerationRate = 5.0f;
[Header("Monster Spawning")]
[Tooltip("Base chance (0-1) of spawning a monster on each tile")]
[SerializeField] private float baseSpawnProbability = 0.2f;
[Tooltip("Maximum chance (0-1) of spawning a monster")]
[SerializeField] private float maxSpawnProbability = 0.5f;
[Tooltip("How fast the probability increases per second")]
[SerializeField] private float probabilityIncreaseRate = 0.01f;
[Tooltip("Force a spawn after this many seconds without spawns")]
[SerializeField] private float guaranteedSpawnTime = 30f;
[Tooltip("Minimum time between monster spawns")]
[SerializeField] private float spawnCooldown = 5f;
[Header("Scoring")]
[Tooltip("Base points for taking a picture")]
[SerializeField] private int basePoints = 100;
[Tooltip("Additional points per depth unit")]
[SerializeField] private int depthMultiplier = 10;
[Header("Surfacing")]
[Tooltip("Duration in seconds for speed transition when surfacing")]
[SerializeField] private float speedTransitionDuration = 2.0f;
[Tooltip("Factor to multiply speed by when surfacing (usually 1.0 for same speed)")]
[SerializeField] private float surfacingSpeedFactor = 3.0f;
[Tooltip("How long to continue spawning tiles after surfacing begins (seconds)")]
[SerializeField] private float surfacingSpawnDelay = 5.0f;
[Header("Tile Generation")]
[Tooltip("Initial number of tiles to create at start")]
[SerializeField] private int initialTileCount = 3;
[Tooltip("Buffer distance for spawning new tiles")]
[SerializeField] private float tileSpawnBuffer = 1f;
[Tooltip("Base movement speed for tiles")]
[SerializeField] private float moveSpeed = 3f;
[Tooltip("Factor to increase speed by each interval")]
[SerializeField] private float speedUpFactor = 0.2f;
[Tooltip("Time interval between speed increases (seconds)")]
[SerializeField] private float speedUpInterval = 10f;
[Tooltip("Maximum movement speed allowed")]
[SerializeField] private float maxMoveSpeed = 12f;
[Tooltip("Interval for velocity calculations (seconds)")]
[SerializeField] private float velocityCalculationInterval = 0.5f;
[Header("Obstacles")]
[Tooltip("Time interval between obstacle spawn attempts (in seconds)")]
[SerializeField] private float obstacleSpawnInterval = 2f;
[Tooltip("Random variation in obstacle spawn timing (+/- seconds)")]
[SerializeField] private float obstacleSpawnIntervalVariation = 0.5f;
[Tooltip("Maximum number of obstacle spawn position attempts before skipping")]
[SerializeField] private int obstacleMaxSpawnAttempts = 10;
[Tooltip("Radius around obstacle spawn point to check for tile collisions")]
[SerializeField] private float obstacleSpawnCollisionRadius = 1f;
[Tooltip("Minimum movement speed for spawned obstacles")]
[SerializeField] private float obstacleMinMoveSpeed = 1f;
[Tooltip("Maximum movement speed for spawned obstacles")]
[SerializeField] private float obstacleMaxMoveSpeed = 4f;
[Header("Collision Handling")]
[Tooltip("Duration in seconds of damage immunity after being hit")]
[SerializeField] private float damageImmunityDuration = 1.0f;
[Tooltip("Force strength for impulse bumps - higher values push further toward center")]
[SerializeField] private float bumpForce = 5.0f;
[Tooltip("Speed for smooth movement to center (units per second)")]
[SerializeField] private float smoothMoveSpeed = 8.0f;
[Tooltip("Whether to block player input during bump movement")]
[SerializeField] private bool blockInputDuringBump = true;
// IDivingMinigameSettings implementation - Basic Movement
public float LerpSpeed => lerpSpeed;
public float MaxOffset => maxOffset;
public float ClampXMin => clampXMin;
public float ClampXMax => clampXMax;
public float SpeedExponent => speedExponent;
// IDivingMinigameSettings implementation - Player Movement
public float TapMaxDistance => tapMaxDistance;
public float TapDecelerationRate => tapDecelerationRate;
// IDivingMinigameSettings implementation - Monster Spawning
public float BaseSpawnProbability => baseSpawnProbability;
public float MaxSpawnProbability => maxSpawnProbability;
public float ProbabilityIncreaseRate => probabilityIncreaseRate;
public float GuaranteedSpawnTime => guaranteedSpawnTime;
public float SpawnCooldown => spawnCooldown;
// IDivingMinigameSettings implementation - Scoring
public int BasePoints => basePoints;
public int DepthMultiplier => depthMultiplier;
// IDivingMinigameSettings implementation - Surfacing
public float SpeedTransitionDuration => speedTransitionDuration;
public float SurfacingSpeedFactor => surfacingSpeedFactor;
public float SurfacingSpawnDelay => surfacingSpawnDelay;
// IDivingMinigameSettings implementation - Tile Generation
public int InitialTileCount => initialTileCount;
public float TileSpawnBuffer => tileSpawnBuffer;
public float MoveSpeed => moveSpeed;
public float SpeedUpFactor => speedUpFactor;
public float SpeedUpInterval => speedUpInterval;
public float MaxMoveSpeed => maxMoveSpeed;
public float VelocityCalculationInterval => velocityCalculationInterval;
// IDivingMinigameSettings implementation - Obstacles
public float ObstacleSpawnInterval => obstacleSpawnInterval;
public float ObstacleSpawnIntervalVariation => obstacleSpawnIntervalVariation;
public int ObstacleMaxSpawnAttempts => obstacleMaxSpawnAttempts;
public float ObstacleSpawnCollisionRadius => obstacleSpawnCollisionRadius;
public float ObstacleMinMoveSpeed => obstacleMinMoveSpeed;
public float ObstacleMaxMoveSpeed => obstacleMaxMoveSpeed;
// IDivingMinigameSettings implementation - Collision Handling
public float DamageImmunityDuration => damageImmunityDuration;
public float BumpForce => bumpForce;
public float SmoothMoveSpeed => smoothMoveSpeed;
public bool BlockInputDuringBump => blockInputDuringBump;
public override void OnValidate()
{
base.OnValidate();
// Validate basic movement values
lerpSpeed = Mathf.Max(0.1f, lerpSpeed);
maxOffset = Mathf.Max(0.1f, maxOffset);
speedExponent = Mathf.Max(0.1f, speedExponent);
// Ensure min is less than max for clamping
if (clampXMin >= clampXMax)
{
clampXMin = clampXMax - 0.1f;
}
// Validate player movement
tapMaxDistance = Mathf.Max(0.01f, tapMaxDistance);
tapDecelerationRate = Mathf.Max(0.1f, tapDecelerationRate);
// Validate probability values
baseSpawnProbability = Mathf.Clamp01(baseSpawnProbability);
maxSpawnProbability = Mathf.Clamp01(maxSpawnProbability);
probabilityIncreaseRate = Mathf.Max(0f, probabilityIncreaseRate);
// Ensure max probability is at least base probability
if (maxSpawnProbability < baseSpawnProbability)
{
maxSpawnProbability = baseSpawnProbability;
}
// Validate time values
guaranteedSpawnTime = Mathf.Max(0.1f, guaranteedSpawnTime);
spawnCooldown = Mathf.Max(0.1f, spawnCooldown);
speedTransitionDuration = Mathf.Max(0.1f, speedTransitionDuration);
surfacingSpawnDelay = Mathf.Max(0f, surfacingSpawnDelay);
// Validate scoring
basePoints = Mathf.Max(0, basePoints);
depthMultiplier = Mathf.Max(0, depthMultiplier);
// Validate tile generation
initialTileCount = Mathf.Max(1, initialTileCount);
tileSpawnBuffer = Mathf.Max(0f, tileSpawnBuffer);
moveSpeed = Mathf.Max(0.1f, moveSpeed);
speedUpFactor = Mathf.Max(0f, speedUpFactor);
speedUpInterval = Mathf.Max(0.1f, speedUpInterval);
maxMoveSpeed = Mathf.Max(moveSpeed, maxMoveSpeed);
velocityCalculationInterval = Mathf.Max(0.01f, velocityCalculationInterval);
// Validate obstacle values
obstacleSpawnInterval = Mathf.Max(0.1f, obstacleSpawnInterval);
obstacleSpawnIntervalVariation = Mathf.Max(0f, obstacleSpawnIntervalVariation);
obstacleMaxSpawnAttempts = Mathf.Max(1, obstacleMaxSpawnAttempts);
obstacleSpawnCollisionRadius = Mathf.Max(0.1f, obstacleSpawnCollisionRadius);
obstacleMinMoveSpeed = Mathf.Max(0.1f, obstacleMinMoveSpeed);
obstacleMaxMoveSpeed = Mathf.Max(obstacleMinMoveSpeed, obstacleMaxMoveSpeed);
// Validate collision settings
damageImmunityDuration = Mathf.Max(0.1f, damageImmunityDuration);
bumpForce = Mathf.Max(0.1f, bumpForce);
smoothMoveSpeed = Mathf.Max(0.1f, smoothMoveSpeed);
}
}
}

View File

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

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<CombinationRule> combinationRules = new List<CombinationRule>();
[SerializeField] private List<SlotItemConfig> slotItemConfigs = new List<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<CombinationRule> CombinationRules => combinationRules;
public List<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,27 @@
using System.Collections.Generic;
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Defines a rule for combining two items
/// </summary>
[System.Serializable]
public class CombinationRule
{
public PickupItemData itemA;
public PickupItemData itemB;
public GameObject resultPrefab; // The prefab to spawn as the result
}
/// <summary>
/// Configuration for items that can be placed in slots
/// </summary>
[System.Serializable]
public class SlotItemConfig
{
public PickupItemData slotItem; // The slot object (SO reference)
public List<PickupItemData> allowedItems;
public List<PickupItemData> forbiddenItems; // Items that cannot be placed in this slot
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 9f9547445fd84c7db30533b7ee9d81dd
timeCreated: 1758699048

View File

@@ -0,0 +1,20 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Attribute to indicate a field should be drawn using the layer selector dropdown
/// </summary>
public class LayerAttribute : PropertyAttribute
{
// No properties needed - this is a marker attribute
}
/// <summary>
/// Attribute to indicate a field should be drawn using the layer mask selector dropdown
/// </summary>
public class LayerMaskAttribute : PropertyAttribute
{
// No properties needed - this is a marker attribute
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 7c64dbd728524f23bda766b57a388207
timeCreated: 1758711688

View File

@@ -0,0 +1,13 @@
using UnityEngine;
namespace AppleHills.Core.Settings
{
/// <summary>
/// Enum defining different movement modes for player movement
/// </summary>
public enum HoldMovementMode
{
Pathfinding,
Direct
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b6b1454235ab476dae09e99238d6c7ce
timeCreated: 1758699033

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 HoldMovementMode defaultHoldMovementMode = 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 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,98 @@
using UnityEngine;
using System.Collections.Generic;
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; }
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; }
List<CombinationRule> CombinationRules { get; }
List<SlotItemConfig> SlotItemConfigs { get; }
}
/// <summary>
/// Interface for minigame settings
/// </summary>
public interface IDivingMinigameSettings
{
// Basic Movement
float LerpSpeed { get; }
float MaxOffset { get; }
float ClampXMin { get; }
float ClampXMax { get; }
float SpeedExponent { get; }
// Player Movement
float TapMaxDistance { get; }
float TapDecelerationRate { get; }
// Monster Spawning
float BaseSpawnProbability { get; }
float MaxSpawnProbability { get; }
float ProbabilityIncreaseRate { get; }
float GuaranteedSpawnTime { get; }
float SpawnCooldown { get; }
// Scoring
int BasePoints { get; }
int DepthMultiplier { get; }
// Surfacing
float SpeedTransitionDuration { get; }
float SurfacingSpeedFactor { get; }
float SurfacingSpawnDelay { get; }
// Tile Generation
int InitialTileCount { get; }
float TileSpawnBuffer { get; }
float MoveSpeed { get; }
float SpeedUpFactor { get; }
float SpeedUpInterval { get; }
float MaxMoveSpeed { get; }
float VelocityCalculationInterval { get; }
// Obstacles
float ObstacleSpawnInterval { get; }
float ObstacleSpawnIntervalVariation { get; }
int ObstacleMaxSpawnAttempts { get; }
float ObstacleSpawnCollisionRadius { get; }
float ObstacleMinMoveSpeed { get; }
float ObstacleMaxMoveSpeed { get; }
// Collision Handling
float DamageImmunityDuration { get; }
float BumpForce { get; }
float SmoothMoveSpeed { get; }
bool BlockInputDuringBump { get; }
}
}

View File

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

View File

@@ -0,0 +1,100 @@
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.
/// Uses synchronous loading to ensure settings are available immediately.
/// </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 synchronously using Addressables - blocks until complete
/// </summary>
public T LoadSettingsSynchronous<T>() where T : BaseSettings
{
string key = typeof(T).Name;
// Return from cache if already loaded
if (_settingsCache.TryGetValue(key, out BaseSettings cachedSettings))
{
return cachedSettings as T;
}
// Load using Addressables synchronously
try
{
// WaitForCompletion blocks until the asset is loaded
T settings = Addressables.LoadAssetAsync<T>($"Settings/{key}").WaitForCompletion();
if (settings != null)
{
_settingsCache[key] = settings;
return settings;
}
else
{
Debug.LogError($"Failed to load settings: {key}");
}
}
catch (Exception e)
{
Debug.LogError($"Error loading settings {key}: {e.Message}");
}
return null;
}
/// <summary>
/// Get cached settings or load them synchronously if not cached
/// </summary>
public T GetSettings<T>() where T : BaseSettings
{
string key = typeof(T).Name;
// Return from cache if already loaded
if (_settingsCache.TryGetValue(key, out BaseSettings cachedSettings))
{
return cachedSettings as T;
}
// Load synchronously if not in cache
return LoadSettingsSynchronous<T>();
}
}
}

View File

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