Files
AppleHillsProduction/docs/settings_readme.md
2025-10-21 15:08:48 +02:00

12 KiB
Raw Permalink Blame History

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

  • 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/<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 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 developeronly settings under Assets/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 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/<TypeName> where <TypeName> 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 SettingsProviders 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.
  • 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

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.
  • Editor changes dont 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). Heres 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<T>() 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.