# Apple Hills Settings System Centralized, designer-friendly configuration using `ScriptableObject` assets, with runtime access via `SettingsProvider` (Addressables-backed) and editor/live-preview access via `SettingsAccess`. This page follows the style of other updated docs (TOC, inline code, code-first usage, case studies). ## Table of Contents - [What This Solves](#what-this-solves) - [Architecture at a Glance](#architecture-at-a-glance) - [Quick Start (Code-First)](#quick-start-code-first) - [Get Settings at Runtime](#get-settings-at-runtime) - [Use Editor-Time Values via `SettingsAccess`](#use-editor-time-values-via-settingsaccess) - [Access Example Fields](#access-example-fields) - [Authoring in the Editor](#authoring-in-the-editor) - [Creating/Editing Settings Assets](#creatingediting-settings-assets) - [Addressables Keys & Loading](#addressables-keys--loading) - [Available Settings Types](#available-settings-types) - [Case Studies](#case-studies) - [Tune Interaction Distances](#tune-interaction-distances) - [Follower Handling & Movement](#follower-handling--movement) - [Diving Minigame Tuning](#diving-minigame-tuning) - [Troubleshooting / FAQ](#troubleshooting--faq) - [Paths & Namespaces](#paths--namespaces) - [Design Opinion: Interface-per-Settings vs Simpler Alternatives](#design-opinion-interface-per-settings-vs-simpler-alternatives) - [Change Log](#change-log) ## What This Solves - Consistent, centralized configuration across gameplay systems. - Safe, designer-editable `ScriptableObject` assets with validation (`OnValidate`). - Simple, Addressables-based runtime loading and caching via `SettingsProvider`. - Editor-time overrides and scene gizmo feedback via `SettingsAccess` helpers. ## Architecture at a Glance - Base: `BaseSettings` (`ScriptableObject`) — common parent for all settings. Implements optional `OnValidate()`. - Access (runtime): `SettingsProvider` (singleton `MonoBehaviour`) — loads assets synchronously via Addressables at keys `Settings/` and caches them. Source: `Assets/Scripts/Core/Settings/SettingsProvider.cs`. - Access (editor/dev): `SettingsAccess` (static) — editor-friendly shim for reading selected values even when not in Play Mode, falling back to `GameManager` at runtime. Source: `Assets/Scripts/Core/SettingsAccess.cs`. - Editor glue (non-Play Mode): `EditorSettingsProvider` — initializes on script reload, loads assets from `Assets/Settings/*.asset`, wires delegates in `SettingsAccess` and repaints Scene views on changes. Source: `Assets/Editor/Settings/EditorSettingsProvider.cs`. - Contracts: `SettingsInterfaces` (`IPlayerFollowerSettings`, `IInteractionSettings`, `IDivingMinigameSettings`) used by systems to remain decoupled from concrete assets. Source: `Assets/Scripts/Core/Settings/SettingsInterfaces.cs`. - Concrete assets: `PlayerFollowerSettings`, `InteractionSettings`, `DivingMinigameSettings` — all derive from `BaseSettings` and implement their respective interfaces with validation in `OnValidate()`. - Editor tooling: `AppleHills/Settings Editor` — finds/creates assets under `Assets/Settings` and provides a tabbed UI; `AppleHills/Developer Settings Editor` — similar pattern for developer‑only settings under `Assets/Settings/Developer`. Sources: `Assets/Editor/Settings/SettingsEditorWindow.cs`, `Assets/Editor/Settings/DeveloperSettingsEditorWindow.cs`. ## Quick Start (Code-First) ### Get Settings at Runtime ```csharp using AppleHills.Core.Settings; var playerFollower = SettingsProvider.Instance.GetSettings(); float speed = playerFollower.MoveSpeed; ``` Or fetch interaction settings once and reuse: ```csharp using AppleHills.Core.Settings; private IInteractionSettings _interaction; void Awake() { _interaction = SettingsProvider.Instance.GetSettings(); } void UseIt() { float stopDist = _interaction.PlayerStopDistance; } ``` ### Use Editor-Time Values via `SettingsAccess` For scene tools, gizmos, or editors that should reflect current settings outside Play Mode: ```csharp // Returns editor-sourced values in Edit Mode; GameManager-backed in Play Mode float stopDist = AppleHills.SettingsAccess.GetPlayerStopDistance(); float directStop = AppleHills.SettingsAccess.GetPlayerStopDistanceDirectInteraction(); float puzzleRange = AppleHills.SettingsAccess.GetPuzzlePromptRange(); ``` ### Access Example Fields ```csharp using AppleHills.Core.Settings; var pf = SettingsProvider.Instance.GetSettings(); // Player float moveSpeed = pf.MoveSpeed; float accel = pf.MaxAcceleration; bool useRb = pf.UseRigidbody; // Follower float followDist = pf.FollowDistance; float near = pf.ThresholdNear; var inter = SettingsProvider.Instance.GetSettings(); LayerMask interactMask = inter.InteractableLayerMask; GameObject pickupPrefab = inter.BasePickupPrefab; float promptRange = inter.DefaultPuzzlePromptRange; ``` ## Authoring in the Editor ### Creating/Editing Settings Assets - Open via menu: `AppleHills/Settings Editor`. - The window discovers all assets of type `BaseSettings` and provides tabbed editing for: - `PlayerFollowerSettings` - `InteractionSettings` - `DivingMinigameSettings` - If an asset is missing, the tool auto-creates it under `Assets/Settings/`: - `Assets/Settings/PlayerFollowerSettings.asset` - `Assets/Settings/InteractionSettings.asset` - `Assets/Settings/DivingMinigameSettings.asset` - Click “Save All” to persist and refresh editor providers/gizmos. ### Addressables Keys & Loading At runtime, `SettingsProvider` synchronously loads settings via Addressables with keys constructed as `Settings/` where `` is the C# class name: - `Settings/PlayerFollowerSettings` - `Settings/InteractionSettings` - `Settings/DivingMinigameSettings` Notes: - Asset filenames can differ (e.g., `MinigameSettings.asset`), but Addressables keys should follow the type name to match `SettingsProvider`’s lookup. - Mark each settings asset as Addressable and set its Addressables key as above. The provider caches objects, so subsequent `GetSettings()` calls are fast. ## Available Settings Types - `PlayerFollowerSettings` (`IPlayerFollowerSettings`) - Player: `MoveSpeed`, `MaxAcceleration`, `StopDistance`, `UseRigidbody`, `DefaultHoldMovementMode`. - Follower: `FollowDistance`, `ManualMoveSmooth`, `ThresholdFar`, `ThresholdNear`, `StopThreshold`. - Backend: `FollowUpdateInterval`, `FollowerSpeedMultiplier`, `HeldIconDisplayHeight`. - `InteractionSettings` (`IInteractionSettings`) - Interactions: `PlayerStopDistance`, `PlayerStopDistanceDirectInteraction`, `FollowerPickupDelay`. - Input/Layering: `InteractableLayerMask`. - Prefabs: `BasePickupPrefab`, `LevelSwitchMenuPrefab`, `DefaultPuzzleIndicatorPrefab`. - Puzzle/UI: `DefaultPuzzlePromptRange`. - Items: `CombinationRules`, `SlotItemConfigs` plus helpers `GetCombinationRule(...)`, `GetSlotItemConfig(...)`. - `DivingMinigameSettings` (`IDivingMinigameSettings`) - Movement, spawning, scoring, surfacing, normalized movement, tile generation, obstacles, camera viewfinder settings, photo input mode (`PhotoInputModes`). ## Case Studies ### Tune Interaction Distances ```csharp using AppleHills.Core.Settings; public class InteractDistanceExample { private readonly IInteractionSettings _s = SettingsProvider.Instance.GetSettings(); public bool IsInRange(float dist) => dist <= _s.PlayerStopDistance; } ``` ### Follower Handling & Movement ```csharp using AppleHills.Core.Settings; public class FollowerMover { private readonly IPlayerFollowerSettings _pf = SettingsProvider.Instance.GetSettings(); public float TargetSpeed(float error) => Mathf.Clamp(error * _pf.FollowerSpeedMultiplier, 0f, _pf.MoveSpeed); } ``` ### Diving Minigame Tuning ```csharp using AppleHills.Core.Settings; public class SpawnController { private readonly IDivingMinigameSettings _m = SettingsProvider.Instance.GetSettings(); public float NextCooldown(float baseCooldown) => Mathf.Clamp(baseCooldown + Random.Range(-_m.ObstacleSpawnIntervalVariation, _m.ObstacleSpawnIntervalVariation), 0.1f, 99); } ``` ## Troubleshooting / FAQ - Settings return null at runtime: - Ensure assets are Addressable with keys `Settings/` and Addressables are initialized before first access. - Editor changes don’t reflect in scene gizmos: - Click “Save All” in `AppleHills/Settings Editor`; the editor provider refresh call updates views. - Which API to use: `SettingsProvider` vs `SettingsAccess`? - Use `SettingsProvider` in runtime code. Use `SettingsAccess` in editor tools/gizmos or shared code that runs both in Edit and Play Modes. ## Paths & Namespaces - Scripts: `Assets/Scripts/Core/Settings/` - `BaseSettings.cs` - `SettingsInterfaces.cs` - `SettingsProvider.cs` - `PlayerFollowerSettings.cs` - `InteractionSettings.cs` - `DivingMinigameSettings.cs` - Editor tooling: `Assets/Editor/Settings/` - `SettingsEditorWindow.cs` - `DeveloperSettingsEditorWindow.cs` - `EditorSettingsProvider.cs` - Editor-time facade: `Assets/Scripts/Core/SettingsAccess.cs` - Namespaces: - Runtime: `AppleHills.Core.Settings` - Editor windows: `AppleHills.Core.Settings.Editor` - Editor glue: `AppleHills.Editor` - Facade: `AppleHills` ## Design Opinion: Interface-per-Settings vs Simpler Alternatives Your current setup creates a dedicated interface and a concrete `ScriptableObject` class per settings domain (e.g., `IInteractionSettings` + `InteractionSettings`). Here’s an assessment based on the repository: Benefits - Decoupling and testability: Call sites can depend on `IInteractionSettings`/`IDivingMinigameSettings`, making it trivial to mock or swap implementations in tests or temporary experiments. - Contract discipline: Interfaces create a curated public surface for teams to converge on; designers can add fields to the asset without automatically expanding the contract. - Runtime safety during refactors: Systems compile against the interface even if you split a single asset into multiple specialized assets later. Costs - Boilerplate: Duplicated getters across the interface and class; more files to maintain. - Drift risk: If a field is added to the `ScriptableObject` but not reflected in the interface (or vice versa), consumers may not see it or may rely on the wrong surface. - Over-abstraction for small teams: When the same team owns both the consumer and the asset, interfaces can feel heavy until real polymorphism or mocking is needed. Pragmatic options - Keep interfaces where they pay off now: `IInteractionSettings`, `IDivingMinigameSettings` already gate many systems and benefit from abstraction. - Simplify where scope is narrow: For settings used in one place (or purely visual), rely on concrete classes until a second consumer appears. You can introduce an interface later without breaking Addressables keys (which use type names in lookup). - Aggregator interface: Introduce an `IGameSettings` façade that exposes only the subset most systems need, backed by a composite provider that reads from the three assets. This reduces type spread at call sites while preserving modular assets. - Codegen or source generators (optional): Generate interfaces from concrete classes (read-only properties only) to eliminate drift/boilerplate. Not required, just an idea if the pattern grows. Recommendation - Short term: Keep the three interfaces you have (interaction, follower, diving). They are already wired and provide value. Avoid adding new interfaces unless a setting has multiple independent consumers or needs mocking. - Medium term: Consider an `IGameSettings` façade (or a small static wrapper) for high-traffic reads like interaction distances to reduce repetitive `GetSettings()` calls around the codebase. ## Change Log - v1.2: Expanded architecture with `EditorSettingsProvider` and developer tooling; clarified Addressables key pattern vs asset filenames; added design opinion and recommendations. - v1.1: New page with TOC, code-first usage, authoring workflow, Addressables keys, case studies, troubleshooting, and paths.