12 KiB
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
- Architecture at a Glance
- Quick Start (Code-First)
- Authoring in the Editor
- Available Settings Types
- Case Studies
- Troubleshooting / FAQ
- Paths & Namespaces
- Design Opinion: Interface-per-Settings vs Simpler Alternatives
- Change Log
What This Solves
- Consistent, centralized configuration across gameplay systems.
- Safe, designer-editable
ScriptableObjectassets with validation (OnValidate). - Simple, Addressables-based runtime loading and caching via
SettingsProvider. - Editor-time overrides and scene gizmo feedback via
SettingsAccesshelpers.
Architecture at a Glance
- Base:
BaseSettings(ScriptableObject) — common parent for all settings. Implements optionalOnValidate(). - Access (runtime):
SettingsProvider(singletonMonoBehaviour) — loads assets synchronously via Addressables at keysSettings/<TypeName>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 toGameManagerat runtime. Source:Assets/Scripts/Core/SettingsAccess.cs. - Editor glue (non-Play Mode):
EditorSettingsProvider— initializes on script reload, loads assets fromAssets/Settings/*.asset, wires delegates inSettingsAccessand 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 fromBaseSettingsand implement their respective interfaces with validation inOnValidate(). - Editor tooling:
AppleHills/Settings Editor— finds/creates assets underAssets/Settingsand provides a tabbed UI;AppleHills/Developer Settings Editor— similar pattern for developer‑only settings underAssets/Settings/Developer. Sources:Assets/Editor/Settings/SettingsEditorWindow.cs,Assets/Editor/Settings/DeveloperSettingsEditorWindow.cs.
Quick Start (Code-First)
Get Settings at Runtime
using AppleHills.Core.Settings;
var playerFollower = SettingsProvider.Instance.GetSettings<PlayerFollowerSettings>();
float speed = playerFollower.MoveSpeed;
Or fetch interaction settings once and reuse:
using AppleHills.Core.Settings;
private IInteractionSettings _interaction;
void Awake()
{
_interaction = SettingsProvider.Instance.GetSettings<InteractionSettings>();
}
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:
// 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
using AppleHills.Core.Settings;
var pf = SettingsProvider.Instance.GetSettings<PlayerFollowerSettings>();
// 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<InteractionSettings>();
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
BaseSettingsand provides tabbed editing for:PlayerFollowerSettingsInteractionSettingsDivingMinigameSettings
- If an asset is missing, the tool auto-creates it under
Assets/Settings/:Assets/Settings/PlayerFollowerSettings.assetAssets/Settings/InteractionSettings.assetAssets/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/<TypeName> where <TypeName> is the C# class name:
Settings/PlayerFollowerSettingsSettings/InteractionSettingsSettings/DivingMinigameSettings
Notes:
- Asset filenames can differ (e.g.,
MinigameSettings.asset), but Addressables keys should follow the type name to matchSettingsProvider’s lookup. - Mark each settings asset as Addressable and set its Addressables key as above. The provider caches objects, so subsequent
GetSettings<T>()calls are fast.
Available Settings Types
PlayerFollowerSettings(IPlayerFollowerSettings)- Player:
MoveSpeed,MaxAcceleration,StopDistance,UseRigidbody,DefaultHoldMovementMode. - Follower:
FollowDistance,ManualMoveSmooth,ThresholdFar,ThresholdNear,StopThreshold. - Backend:
FollowUpdateInterval,FollowerSpeedMultiplier,HeldIconDisplayHeight.
- Player:
InteractionSettings(IInteractionSettings)- Interactions:
PlayerStopDistance,PlayerStopDistanceDirectInteraction,FollowerPickupDelay. - Input/Layering:
InteractableLayerMask. - Prefabs:
BasePickupPrefab,LevelSwitchMenuPrefab,DefaultPuzzleIndicatorPrefab. - Puzzle/UI:
DefaultPuzzlePromptRange. - Items:
CombinationRules,SlotItemConfigsplus helpersGetCombinationRule(...),GetSlotItemConfig(...).
- Interactions:
DivingMinigameSettings(IDivingMinigameSettings)- Movement, spawning, scoring, surfacing, normalized movement, tile generation, obstacles, camera viewfinder settings, photo input mode (
PhotoInputModes).
- Movement, spawning, scoring, surfacing, normalized movement, tile generation, obstacles, camera viewfinder settings, photo input mode (
Case Studies
Tune Interaction Distances
using AppleHills.Core.Settings;
public class InteractDistanceExample
{
private readonly IInteractionSettings _s = SettingsProvider.Instance.GetSettings<InteractionSettings>();
public bool IsInRange(float dist) => dist <= _s.PlayerStopDistance;
}
Follower Handling & Movement
using AppleHills.Core.Settings;
public class FollowerMover
{
private readonly IPlayerFollowerSettings _pf = SettingsProvider.Instance.GetSettings<PlayerFollowerSettings>();
public float TargetSpeed(float error) => Mathf.Clamp(error * _pf.FollowerSpeedMultiplier, 0f, _pf.MoveSpeed);
}
Diving Minigame Tuning
using AppleHills.Core.Settings;
public class SpawnController
{
private readonly IDivingMinigameSettings _m = SettingsProvider.Instance.GetSettings<DivingMinigameSettings>();
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/<TypeName>and Addressables are initialized before first access.
- Ensure assets are Addressable with keys
- Editor changes don’t reflect in scene gizmos:
- Click “Save All” in
AppleHills/Settings Editor; the editor provider refresh call updates views.
- Click “Save All” in
- Which API to use:
SettingsProvidervsSettingsAccess?- Use
SettingsProviderin runtime code. UseSettingsAccessin editor tools/gizmos or shared code that runs both in Edit and Play Modes.
- Use
Paths & Namespaces
- Scripts:
Assets/Scripts/Core/Settings/BaseSettings.csSettingsInterfaces.csSettingsProvider.csPlayerFollowerSettings.csInteractionSettings.csDivingMinigameSettings.cs
- Editor tooling:
Assets/Editor/Settings/SettingsEditorWindow.csDeveloperSettingsEditorWindow.csEditorSettingsProvider.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
- Runtime:
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
ScriptableObjectbut 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,IDivingMinigameSettingsalready 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
IGameSettingsfaç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
IGameSettingsfaçade (or a small static wrapper) for high-traffic reads like interaction distances to reduce repetitiveGetSettings<T>()calls around the codebase.
Change Log
- v1.2: Expanded architecture with
EditorSettingsProviderand 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.