Getting Started

Overview

What the LeLa Framework template is and how it is organized.

The LeLa Framework (Level Labs) is a modular 2D Unity template built around clean assembly boundaries, ScriptableObject-driven data, the new Input System and a swappable audio backend (FMOD by default).

Design goals

  • Reusable, fully editable framework code under Assets/Scripts/Framework - not buried in Plugins.
  • Strict layering via assembly definitions for fast recompiles and clear dependencies.
  • Audio backend behind an interface so FMOD can be replaced (e.g. with Wwise) without touching game code.
  • Editor-first tooling: documentation CMS, finders, scene bootstrapper,
Everything in this panel is authored through the built-in CMS. Toggle Edit in the toolbar to manage pages, or open an article asset in the Inspector.

License

The Framework is provided by Level Labs GmbH.

All Tools and Templates in this project are licensed exclusively for use in projects in which Level Labs is actively participating as a contributor. Any use outside such a project, in particular in projects without the active participation of Level Labs, is not permitted and requires the prior written consent of Level Labs.

→ License

Getting Started

Quick Start

Get a playable loop running.

This Template allows a fresh generation of some Assets - BUT, fresh out of the box, it comes completely pre-configured.
  • Open PersistentLevel and press Play. GameBootstrap routes to the Main Menu.
  • Press Start in the menu - AppStateMachine loads the Game scene additively and GameSession begins a run.
  • Edit volumes in the Options menu; they persist via PlayerPrefs through the audio facade.
Getting Started

Project Structure

How the project is organized by responsibility.

The navigation tree on the left mirrors this documentation; the project source is organized along the same lines:

  • Assets/Scripts/Framework - reusable runtime systems (assembly LeLa.Framework).
  • Assets/Scripts/Framework/Editor - editor tooling incl. this CMS (LeLa.Framework.Editor).
  • Assets/Scripts/Audio.FMOD - FMOD implementation of the audio interface (LeLa.Audio.FMOD).
  • Assets/Scripts/Game - game-specific runtime (Game), with /Editor for its tools.
  • Assets/Plugins/FMOD - third-party audio middleware only.
The Plugins folder is reserved for genuine third-party assets - all of our own code lives under Scripts so it stays directly editable. See Assembly Layout for the dependency graph.
Getting Started

License

Copyright (c) 2026 Level Labs GmbH. All rights reserved.

LELA FRAMEWORK LICENSE

This Framework is provided by Level Labs GmbH and is licensed exclusively for use in Projects in which Level Labs is actively participating as a contributor. Any use outside such a Project is not permitted without the prior written consent of Level Labs.

1. DEFINITIONS

1.1 "Framework" means the software framework provided by Level Labs under this License, including all source code, binaries, assets, configuration and accompanying documentation, as well as any updates and modifications thereof. 1.2 "Level Labs" means Level Labs GmbH (Rietschelstr. 2, 04177 Leipzig, Germany), the holder of all rights in the Framework and the licensor under this License. 1.3 "Eligible Project" means a software or game project in which, at the relevant point in time, at least one current member of Level Labs is actively and demonstrably participating as a contributor. 1.4 "Licensee" means the natural or legal person who obtains or uses the Framework under this License.

2. GRANT OF LICENSE

2.1 Subject to the terms of this License, Level Labs grants the Licensee a limited, non-exclusive, non-transferable, non-sublicensable and revocable right to use, reproduce and modify the Framework solely for the purpose of developing and operating an Eligible Project. 2.2 No rights are granted beyond those expressly stated in this License. Any use of the Framework outside an Eligible Project requires the prior written consent of Level Labs.

3. RESTRICTIONS

The Licensee shall not: (a) use the Framework in any project that is not an Eligible Project; (b) distribute, sublicense, sell, rent, lend or otherwise make the Framework available to third parties, whether on a standalone basis or as part of another product, except as an integral part of an Eligible Project and only to other participants of that same Eligible Project; (c) use the Framework to create a competing framework or to provide it as a service to third parties; (d) remove, obscure or alter any copyright, trademark or other proprietary notices contained in the Framework.

4. TERM AND TERMINATION

4.1 The rights granted under this License with respect to a given Eligible Project end automatically, without the need for notice, as soon as Level Labs ceases to actively participate in that project (i.e. the project no longer qualifies as an Eligible Project). 4.2 Level Labs may revoke this License for good cause, in particular in the event of a material breach of its terms by the Licensee. 4.3 Upon termination or expiry, the Licensee shall immediately cease all use of the Framework and delete all copies of the Framework under its control, except for copies that must be retained under mandatory statutory provisions.

5. OWNERSHIP

The Framework is and remains the exclusive property of Level Labs and is protected by copyright and other intellectual property laws. This License does not transfer any ownership of intellectual property rights to the Licensee and constitutes the grant of a limited right of use only.

6. DISCLAIMER OF WARRANTY

To the extent permitted by applicable law, the Framework is provided "AS IS" and "AS AVAILABLE", without warranty of any kind, whether express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, or non-infringement. Mandatory statutory warranty rights remain unaffected.

7. LIMITATION OF LIABILITY

To the extent permitted by applicable law, Level Labs shall not be liable for any damages arising out of or in connection with the use of, or the inability to use, the Framework. Liability for intent (Vorsatz) and gross negligence (grobe Fahrlaessigkeit), liability for injury to life, body or health, liability under the Product Liability Act (Produkthaftungsgesetz) and any other liability that cannot be excluded by law remain unaffected.

8. GOVERNING LAW AND JURISDICTION

8.1 This License shall be governed by the laws of the Federal Republic of Germany, excluding the UN Convention on Contracts for the International Sale of Goods (CISG) and the rules of private international law. 8.2 The exclusive place of jurisdiction for all disputes arising out of or in connection with this License shall be in Germany, to the extent permitted by law.

9. SEVERABILITY

Should any provision of this License be or become invalid or unenforceable, the validity of the remaining provisions shall not be affected thereby.

Level Labs GmbH Contact: hello@levellabs.de Version 1.0 - 20.06.2026
Architecture

Assembly Layout

Six assemblies enforce a clean dependency graph.

LeLa.Framework            -> (UnityEngine.UI, TextMeshPro, InputSystem,
Tilemap.Extras)

LeLa.Framework.Editor     -> LeLa.Framework           
[Editor only]

LeLa.Audio.FMOD           -> LeLa.Framework, FMODUnity

Game                     
-> LeLa.Framework, LeLa.Audio.FMOD, FMODUnity, InputSystem

Game.Editor              
-> Game, LeLa.Framework(.Editor) [Editor only]

Game.Tests               
-> LeLa.Framework, Game        [tests]

The golden rule

Dependencies only point downward: Game may use Framework, but Framework must never reference Game. This keeps the framework reusable across projects.

If you find Framework code needing a Game type, the code belongs in the Game layer (as happened with WorldTimer and ScoreManager).
Architecture

Application Flow

How the persistent app layer and additive game scenes cooperate.

The template deliberately splits scene mechanics (Framework) from routing and run lifecycle (Game). Both live on the same persistent bootstrap scene; content scenes are loaded additively on top.

Two layers

  • App / persistent layer — always loaded (PersistentLevel). Hosts managers that survive menu/game transitions: scene loader, state router, session, prompts, notifications, input routing.
  • Game / content layer — loaded on demand. MainMenu, Game, Victory and other content scenes. UI and gameplay for the current screen only.

Who does what

  • ApplicationManager (Framework) — additive load/unload, raises LoadingStateChanged. Knows scene names, not game rules.
  • AppStateMachine (Game) — maps AppState to scene transitions via ApplicationManager. Starts a run when entering gameplay.
  • GameSession (Game) — lifecycle of one run (start, pause, game over, retry). Orthogonal to scene routing.
  • GameBootstrap (Game) — cold start from PersistentLevel only; applies saved settings and calls GoToMainMenu().

Related systems

→ Application Manager — scene loader (Framework)

→ App State Machine — scene routing (Game)

→ Game Session — run lifecycle (Game)

Cold start — step by step

1. Press Play from PersistentLevel

Always start from PersistentLevel. Playing a content scene directly skips bootstrap — AppStateMachine falls back to SceneManager.LoadScene without persistent UI or session setup.

2. Bootstrap initializes the app

  • GameBootstrap.Start() runs on the persistent scene.
  • Saved settings (audio volumes, etc.) are applied.
  • AppStateMachine.GoToMainMenu() is called — this is the intended first navigation.

3. AppStateMachine routes to Main Menu

  • AppState becomes Transitioning, then MainMenu.
  • ApplicationManager.SetScene("MainMenu") unloads other content scenes and loads MainMenu additively.
  • LoadingStateChanged(true/false) fires — the loading indicator can show a spinner.

4. Player starts a run

  • Main menu UI calls AppStateMachine.GoToGame() (or GoToLevel(sceneName)).
  • ApplicationManager loads the game scene additively.
  • AppState becomes InGame.
  • GameSession.StartNewRun() fires OnRunStarted — score, timer and other gameplay systems reset here.

5. During gameplay

  • GameSession handles pause/resume (Time.timeScale) independently of scene routing.
  • InputStateController enables PlayerInputChannel only while InGame + Running + no modal on ModalStack; otherwise UIInputChannel is active.
  • Victory or abort routes through AppStateMachine again (e.g. GoToVictory(), GoToMainMenu()).

Flow at a glance

PersistentLevel (Play)
  -> GameBootstrap.Start()
  -> AppStateMachine.GoToMainMenu()
       -> ApplicationManager.SetScene("MainMenu")
  -> [Main Menu] Start pressed
  -> AppStateMachine.GoToGame()
       -> ApplicationManager.SetScene("Game")
       -> GameSession.StartNewRun()

Why this split?

ApplicationManager stays game-agnostic and reusable. AppStateMachine and GameSession encode this game's flow and can evolve without touching the loader.

Always press Play from PersistentLevel. Content scenes are not entry points.

See also

→ Loading Indicator — subscribes to LoadingStateChanged

→ Input State Controller — gameplay vs UI input maps

Core

Application Manager

Framework additive scene loader \u2014 no game-specific routing.

Lives on the persistent bootstrap scene (PersistentLevel). Single owner of additive load/unload. Other systems request transitions; this class performs them and reports progress.

Responsibilities

  • SetScene(name) — load a content scene additively, set it active, unload every other loaded scene except the persistent bootstrap.
  • AddScene(name) — additive load without unloading (multi-scene setups).
  • LoadingStateChanged — event(bool) while a transition runs. UI subscribes; this class never references a concrete loading screen.
  • PersistentSceneName — bootstrap scene name from SceneField; never unloaded during transitions.

Explicit non-responsibilities

  • Does not know AppState, menus, runs, or victory conditions.
  • Does not call GameSession — that is AppStateMachine's job after the scene load is requested.

Related systems

→ Application Flow — full layer diagram

→ App State Machine — calls SetScene

→ Loading Indicator — listens to LoadingStateChanged

API

// Game layer (typical)
ApplicationManager.Instance.SetScene("Game");

// Optional additive without swap
ApplicationManager.Instance.AddScene("OverlayScene");

// UI subscription
ApplicationManager.Instance.LoadingStateChanged += isLoading =>
    loadingRoot.SetActive(isLoading);
_minFakeLoadTime keeps the loading state visible briefly so fast transitions do not flicker.
Core › Audio

Audio Abstraction

Game code talks to audio through an interface, never to FMOD directly.

IAppAudio defines the contract (set bus volume, play UI sound). AppAudio is a static facade/service locator. The FMOD backend registers itself at startup.

// Game / Framework code:

AppAudio.PlayUi(UiSoundId.Confirm);

AppAudio.SetBusVolume(AudioBus.Music,
0.5f);



// LeLa.Audio.FMOD/LLFMODManager implements IAppAudio

//
and calls AppAudio.Register(this) in Awake.
To switch to Wwise, write a WwiseAppAudio : IAppAudio in a new assembly and register it. No game code changes.
Core › Audio

FMOD Manager

The default IAppAudio implementation.

LLFMODManager implements IAppAudio, maps AudioBus values to FMOD VCA/bus parameters and UiSoundId values to FMOD EventReferences, and persists volumes via PlayerPrefs + ScriptableFloat.

// Lives in LeLa.Audio.FMOD. Registers in Awake:

AppAudio.Register(this);



//
Game code stays backend-agnostic:

AppAudio.SetBusVolume(AudioBus.Sfx,
0.7f);
Swap the whole backend by providing another IAppAudio and not loading the FMOD manager.
Core › Audio

UI Button Audio

One-click UI sounds on buttons.

LL_ButtonFMODAudio plays hover/click UI sounds for Unity UI Buttons through the audio facade.

Core › GUI › LLGUI

LLGUI Overview

Hub \u2014 what LLGUI is and where to read next.

LLGUI is the template's themed UI kit: prefab-based controls (buttons, panels, inputs, \u2026) that pull colors, sprites and typography from shared LLThemeSO assets. You author screens in the Hierarchy; the framework keeps visuals consistent and updatable in one place.

How it fits together

Think of three layers: Settings (project defaults) \u2192 Theme (palette + style slots) \u2192 Elements (LLButton, LLText, \u2026 on your canvas). Optional LLGUIThemeScope components swap the theme for a subtree; individual elements can override a single property without breaking the chain.

LLGUISettings (project)
  \u2514\u2500 DefaultTheme, Typography, Layout, Prefabs
LLGUIThemeScope (optional on canvas/subtree)
  \u2514\u2500 local LLThemeSO
LLButton / LLText / LLPanel / \u2026
  \u2514\u2500 style slot (Primary, Secondary, Background\u2026)
     \u2514\u2500 resolved tokens \u2192 Image, TMP, Selectable colors

Start here

  • New to LLGUI? \u2192 follow LLGUI Getting Started (5-minute hands-on path).
  • Changing look & feel? \u2192 LLGUI Themes and LLGUI Typography.
  • Which control do I need? \u2192 LLGUI Controls reference.
  • Menus, prefabs, project setup? \u2192 LLGUI Authoring.

Deep-dive articles

→ Hands-on \u2014 first canvas and button

→ Palettes, scopes, overrides, refresh

→ Every control and default style

→ Text styles and LLTextSO

→ Factory menu, settings, prefabs

Related GUI systems

→ Toast stack (separate from LLGUI kit)

→ Modal prompts

→ Scene-load spinner

→ Hover tooltips

LLGUI is runtime + editor in Assets/Scripts/Framework/GUI/LLGUI. Kenney UI Pack sprites ship as defaults; replace via theme or Settings sprite defaults.
Core › GUI › LLGUI

LLGUI Getting Started

Build your first themed screen in five steps.

This walkthrough builds a minimal menu screen using only editor menus \u2014 no custom editor scripts required.

1. Bootstrap project settings (once per project)

  • Menu: LeLa \u2192 GUI \u2192 Create Default Settings.
  • Creates Assets/Resources/LLGUI/LLGUISettings.asset with default theme, typography, layout and prefab registry.
  • Optional: Project Settings \u2192 LeLa \u2192 GUI to tweak defaults globally.

2. Create a canvas

  • GameObject \u2192 LeLa \u2192 GUI \u2192 Setups \u2192 Canvas \u2014 includes LLGUIThemeScope and CanvasScaler.
  • Unity creates an EventSystem with Input System UI module if none exists.

3. Add a view shell

  • GameObject \u2192 LeLa \u2192 GUI \u2192 Setups \u2192 Menu View (select the canvas first).
  • Gives Top / Center / Bottom regions \u2014 typical for main and pause menus.
  • Optional: assign an LLViewSO (Create \u2192 LeLa/GUI/View) for padding and spacing presets.

4. Place controls

  • Select a region, then GameObject \u2192 LeLa \u2192 GUI \u2192 Elements \u2192 Button (or Text, Panel, Toggle, \u2026).
  • Elements spawn from registered prefabs under LLGUI/Prefabs/Elements.

5. Wire behaviour

public class MainMenuScreen : MonoBehaviour
{
    [SerializeField] LLButton _startButton;

    void Awake()
    {
        _startButton.onClick.AddListener(() =>
            AppStateMachine.Instance.GoToGame());
    }
}

Change the look

  • Edit DefaultTheme (or your theme asset) \u2014 open scenes refresh automatically in the editor.
  • Per screen: enable Override Theme on the canvas LLGUIThemeScope and assign another LLThemeSO.
  • Per control: pick a different Theme Style slot (Primary / Secondary / Background).
Missing prefabs? Run LeLa \u2192 GUI \u2192 Regenerate Prefabs \u2014 rebuilds all element prefabs and re-registers them in Settings.

Next

→ How theme resolution and overrides work

→ Full control reference

Core › GUI › LLGUI

LLGUI Themes

Theme assets, scopes, style slots and overrides.

Themes centralize colors and background sprites. Typography is project-wide (see LLGUI Typography) but text colors come from the active theme slot.

Resolution order

  • 1. Element Use Local Theme + assigned LLThemeSO (if enabled).
  • 2. Nearest parent LLGUIThemeScope with override enabled.
  • 3. LLGUISettings.Instance.DefaultTheme.
// Pseudocode \u2014 see LLGUIThemeContext.ResolveTheme()
element override \u2192 theme scope \u2192 project default

Style slots (LLStyle)

Each theme contains a palette with six named slots. Elements pick one slot (or use their built-in default):

  • Primary \u2014 default buttons, toggles, sliders.
  • Secondary \u2014 tabs, input fields.
  • Tertiary / Accent \u2014 emphasis variants.
  • Background \u2014 panels, scroll views, view containers.
  • Muted \u2014 dividers, subtle chrome (LLDivider always uses Muted).

Tokens per slot (LLStyleTokens)

  • Background, Text, TextMuted, Border colors.
  • Optional BackgroundSprite \u2014 filled from Settings sprite defaults when null.

Theme Scope

Add LLGUIThemeScope (LeLa/GUI/Theme Scope or on Canvas prefab) to push a theme to all child ILLThemedElement components. Changing the scope re-applies the subtree in the editor.

Per-property overrides

When you manually tweak a color, sprite or font on a themed element, LLGUI records an override flag (BackgroundColor, TextTypography, \u2026). Overridden properties survive theme edits until you reset them.

  • Inspector: Custom Properties section shows active overrides with per-field Reset.
  • Reset All to Theme clears overrides and re-applies resolved tokens.

Live refresh

  • Editing a theme asset or Settings triggers LLGUIThemeRefreshBus \u2014 all themed elements in open scenes re-sync.
  • Context menu: GameObject \u2192 LeLa \u2192 GUI \u2192 Apply Active Theme To Selection for a targeted refresh.

Example \u2014 screen-specific theme

// On pause menu canvas:
// 1. LLGUIThemeScope \u2192 Override Theme = true
// 2. Assign PauseMenuTheme asset
// All child LLButtons/LLPanels pick up the pause palette.

Create a theme

  • Assets \u2192 Create \u2192 LeLa \u2192 GUI \u2192 Theme.
  • Duplicate DefaultTheme or GameJamTheme under LLGUI/ScriptableObjects/Themes as a starting point.

→ Back to hub

→ Fonts and text styles

Core › GUI › LLGUI

LLGUI Controls

Control reference \u2014 pick the right element.

All themed controls implement ILLThemedElement: call ApplyTheme() after structural changes, ResetToTheme() to discard manual overrides. Non-themed helpers (LLView, LLButtonList) compose layouts.

Quick picker

  • Click action \u2192 LLButton
  • Tab / segmented nav \u2192 LLTabButton
  • Label / heading \u2192 LLText
  • Container backdrop \u2192 LLPanel or LLView
  • Separator line \u2192 LLDivider
  • On/off \u2192 LLToggle
  • Numeric range \u2192 LLSlider ( exposes UnitySlider )
  • Text entry \u2192 LLInputField ( exposes InputField TMP )
  • Scrollable list \u2192 LLScrollView ( exposes ScrollRect, Content )
  • Vertical button stack \u2192 LLButtonList
  • Small icon \u2192 LLIcon

Themed controls (detail)

LLButton

  • Default style: Primary. Built on MultiGraphicsButton \u2014 tints background + child graphics together.
  • Label uses ButtonLabel typography. Wire onClick like Unity UI Button.

LLTabButton

  • Default style: Secondary. Tracks IsActiveTab for selected visual state.

LLText

  • Default style: Primary. Optional LLTextSO data source for copy + typography/style presets.
  • SetTypographyStyle() switches typography slot and clears typography overrides.

LLPanel / LLBackground

  • Default style: Background. Themed Image fill. LLPanel is the authoring entry point.

LLDivider

  • Always Muted slot \u2014 horizontal rule using Border color + divider sprite from Settings.

LLToggle / LLSlider / LLInputField / LLScrollView

  • Map uGUI/TMP parts to theme tokens (track, fill, handle, placeholder, scrollbar\u2026).
  • Expose underlying Unity components for gameplay wiring.

Layout helpers

LLView

  • Screen container with Top / Center / Bottom. Optional LLViewSO for padding, spacing, theme hint.
  • Init() runs on Awake \u2014 re-run from inspector Refresh after data changes.

LLButtonList

  • Configures VerticalLayoutGroup + child LayoutElement heights using Settings spacing.

Spawn menu

GameObject \u2192 LeLa \u2192 GUI \u2192 Elements \u2014 Button, Tab Button, Text, Panel, Divider, Toggle, Slider, Input Field, Scroll View, Button List.

LLIcon and low-level MultiGraphicsButton are Add Component only (no factory menu entry).

→ Step-by-step first screen

→ Style slots and overrides

Core › GUI › LLGUI

LLGUI Typography

Project typography, LLTextSO and text overrides.

Typography is configured once in LLGUISettings and shared across all themes. Themes supply text colors; Settings supply font, size, weight and spacing.

Where to edit

  • Project Settings \u2192 LeLa \u2192 GUI \u2192 Typography
  • Or open Assets/Resources/LLGUI/LLGUISettings.asset.

Built-in styles (LLTypographyStyle)

  • Title, Subtitle, Heading1\u20133
  • Body (default body copy)
  • Quote (italic), Caption (small)
  • ButtonLabel \u2014 used by LLButton and tab labels

Applying typography

  • LLText \u2014 pick Typography Style in inspector.
  • LLInputField \u2014 text + placeholder styles; SetTextTypographyStyle() at runtime.
  • LLTypographyUtility.Apply() pushes definition onto a TMP component.

LLTextSO (data-driven copy)

Create via Assets \u2192 Create \u2192 LeLa \u2192 GUI \u2192 Text. Assign to LLText._textData for reusable strings with baked typography/style hints. Useful for localization pipelines and designer-authored copy.

// LLText in scene references Title.asset:
// Text SO \u2192 string + TypographyStyle + ThemeStyle hint

Overrides

Manual font/size edits on a themed text element set the TextTypography override flag \u2014 reset from inspector when you want to follow Settings again.

→ Text colors from theme slots

→ LLText and LLInputField

Core › GUI › LLGUI

LLGUI Authoring

Factory menus, prefabs, settings and asset paths.

LLGUI is editor-first: prefabs, factory menus and project settings keep runtime code thin. Game code mostly wires onClick / Unity events \u2014 not theme plumbing.

Key menus

  • LeLa \u2192 GUI \u2192 Create Default Settings \u2014 bootstrap Resources + DefaultTheme.
  • LeLa \u2192 GUI \u2192 Regenerate Prefabs \u2014 rebuild all LLGUI prefabs from code (Kenney sprites).
  • GameObject \u2192 LeLa \u2192 GUI \u2192 \u2026 \u2014 spawn canvas, views, elements (see Getting Started).
  • Project Settings \u2192 LeLa \u2192 GUI \u2014 default theme, typography, layout, prefab registry.

LLGUIFactory

Spawning logic: load prefab from LLGUISettings.Prefabs by LLGUIElementType, parent under selection (or auto-create Canvas). Missing prefab \u2192 warning + optional regenerate.

LLGUIPrefabBuilder

Programmatically builds Canvas, EmptyView, MenuView and all element prefabs; wires serialized references; registers paths in Settings. Run after changing default control layout in code.

Asset locations

  • Assets/Scripts/Framework/GUI/LLGUI/Prefabs/ \u2014 Canvas, views, elements
  • \u2026/ScriptableObjects/Themes|Views|Texts/ \u2014 sample data assets
  • Assets/Resources/LLGUI/LLGUISettings.asset \u2014 runtime singleton

ScriptableObject create menus

  • LeLa/GUI/Theme \u2014 LLThemeSO
  • LeLa/GUI/View \u2014 LLViewSO (padding, spacing)
  • LeLa/GUI/Text \u2014 LLTextSO (copy + typography hints)

Inspectors

  • Themed elements share override UI via LLThemedElementEditor.
  • LLThemeSO / Settings editors trigger full-scene theme refresh on change.
  • MultiGraphicsButton \u2014 Collect Child Graphics for custom prefab layouts.
Template scene builder (BootstrapLLGUI in Game/Editor) uses the same prefab registry for automated scene setup.

→ First screen walkthrough

→ Back to hub

Core › GUI

Loading Indicator

Shows a spinner during scene loads.

LoadingIndicator subscribes to ApplicationManager.LoadingStateChanged and toggles its visuals automatically.

Core › GUI

Notifications

Transient in-game toasts.

NotificationController is a static entry point to push typed notifications rendered by a NotificationView.

NotificationController.Push("Checkpoint reached", NotificationType.Success);
Core › GUI

Prompt Service

Queued modal dialogs.

PromptService enqueues PromptRequests and shows them one at a time via a PromptView, integrating with the ModalStack.

PromptService.Show(new PromptRequest {
    Title = "Quit?",
   Message = "Unsaved progress will be lost.",
    Confirm = "Quit", Cancel = "Stay",
    OnConfirm = QuitGame
});
Core › GUI

Tooltips

Hover tooltips for UI.

The ToolTipSystem shows contextual tooltips for UI elements on hover, with a shared singleton tooltip renderer.

Core › Input › Input

Input Overview

"Map-scoped input channels \u2014 architecture, responsibilities, and

The System routes input through map-scoped channels: ScriptableObject assets where each channel wraps one action map from GameControls.inputactions and publishes typed C# events. Gameplay and UI code subscribe to those events; InputStateController on PersistentLevel decides which channel is active.

Responsibilities

  • Bindings \u2014 device \u2192 action mapping in GameControls.inputactions (Game Controls).
  • Channels \u2014 PlayerInputChannel and UIInputChannel translate actions into events (Input Channels).
  • Routing \u2014 InputStateController enables exactly one channel at a time (Input Map Routing).
  • Modal gate \u2014 ModalStack suppresses gameplay input while overlays hold focus (Modal Stack).

Architecture

GameControls.inputactions
        \u2502
        \u251C\u2500\u2500 PlayerInputChannel.asset   (Player map)
        \u2514\u2500\u2500 UIInputChannel.asset      (UI map)
                    \u2502  events
                    \u25BC
          Gameplay & UI subscribers
                    \u25B2  Enable / Disable
                   \u2502
           InputStateController
             \u2190 AppStateMachine, GameSession, ModalStack

Built-in channels

  • PlayerInputChannel \u2014 Move, Fire, Interact, Pause. Active during an unpaused in-game run with no modal open.
  • UIInputChannel \u2014 Navigate, Submit, Cancel, Point, Click. Active in menus, pause overlays, and whenever gameplay input is suppressed.
Pointer clicks on LLGUI buttons go through Unity's EventSystem. The UI channel covers gamepad/keyboard navigation in custom code.

Constraints

  • Channel Enable() / Disable() is owned exclusively by InputStateController.
  • Subscribe in OnEnable, unsubscribe in OnDisable \u2014 channels are shared assets.
  • Modal UI registers with ModalStack.Push(token) and clears with a matching Pop.

Documentation map

  • Input Getting Started \u2014 assets, scene wiring, first subscription.
  • Input Channels \u2014 events, lifecycle, custom channels.
  • Game Controls \u2014 bindings and new actions.
  • Input Map Routing \xB7 Modal Stack \xB7 Input Device Tracker \xB7 Input State Controller.

→ Setup walkthrough

→ Channel API

→ When each channel is active

Core › Input › Input

Input Getting Started

Channel assets, PersistentLevel wiring, first gameplay subscription.

This walkthrough sets up the default channel assets, verifies PersistentLevel wiring, and connects a gameplay script to PlayerInputChannel events.

Prerequisites

  • Unity Input System enabled (template default: activeInputHandler: Both).
  • Play mode starts from PersistentLevel \u2014 routing lives on the persistent scene.

Step 1 \u2014 Create channel assets

  • LeLa \u2192 Input \u2192 Create Default Channels
  • Assets/ScriptableObjects/Input/Channels/PlayerInputChannel.asset
  • Assets/ScriptableObjects/Input/Channels/UIInputChannel.asset
  • Both reference Assets/Scripts/Framework/Input/GameControls.inputactions.
LeLa \u2192 Setup \u2192 Build Template performs this step automatically.

Step 2 \u2014 Verify PersistentLevel

  • InputStateController \u2014 _playerChannel and _uiChannel assigned.
  • EventSystem + InputSystemUIInputModule for LLGUI pointer input.

Step 3 \u2014 Subscribe from gameplay

Assign the shared PlayerInputChannel asset. InputStateController enables it during gameplay; the script listens only:

using LeLa.Framework.Input;
using UnityEngine;

public class PlayerMotor : MonoBehaviour
{
    [SerializeField] PlayerInputChannel _player;
    Vector2 _move;

    void OnEnable()
    {
        _player.MoveEvent += OnMove;
       _player.MoveCanceledEvent += OnMoveCanceled;
    }

    void OnDisable()
   {
        _player.MoveEvent -= OnMove;
        _player.MoveCanceledEvent -= OnMoveCanceled;
    }

    void OnMove(Vector2 v) => _move = v;
   void OnMoveCanceled() => _move = Vector2.zero;
    void Update() => transform.position += (Vector3)(_move * 5f * Time.deltaTime);
}
Channel enablement is handled by InputStateController \u2014 see Input Map Routing.

Step 4 \u2014 Verify in Play Mode

  • Main menu \u2014 UIInputChannel active; mouse works via EventSystem.
  • In-game run \u2014 PlayerInputChannel active; movement input reaches the player.
  • Paused \u2014 player channel off, UI channel on (Input Map Routing).

→ Full event reference

→ Bindings

→ Routing rules

Core › Input › Input

Input Channels

PlayerInputChannel and UIInputChannel \u2014 events, lifecycle, extending.

An input channel is a ScriptableObject implementing IInputChannel. It owns one action map from the shared GameControls.inputactions asset, resolves actions by name on first Enable(), and exposes typed C# events.

Lifecycle

  • Enable() \u2014 locate map, call BindActions(), wire Hook(...), enable map.
  • While enabled \u2014 Input System callbacks invoke subscribed handlers.
  • Disable() \u2014 map disabled; handlers remain until removed in OnDisable.

PlayerInputChannel

  • Asset: PlayerInputChannel.asset \xB7 Map: Player
  • MoveEvent / MoveCanceledEvent \xB7 FirePerformed / FireCanceled
  • InteractEvent \xB7 PauseEvent

UIInputChannel

  • Asset: UIInputChannel.asset \xB7 Map: UI
  • NavigateEvent / NavigateCanceledEvent
  • SubmitEvent / CancelEvent \xB7 PointEvent / ClickEvent

IInputChannel

bool IsEnabled { get; }

void Enable();

void Disable();

Custom channel

  • Add a map in GameControls.inputactions.
  • Subclass InputChannelBase \u2014 set MapName, implement BindActions().
  • Create asset via CreateAssetMenu; extend routing in InputStateController.
public class InteractionInputChannel : InputChannelBase
{
    public event Action ConfirmEvent;
    protected override string MapName => "Interaction";
   protected override void BindActions()
    {
        var map = ActionAsset.FindActionMap(MapName, false);
        Hook(map, "Confirm", performed: () => ConfirmEvent?.Invoke());
   }
    protected override void ClearSubscribers() => ConfirmEvent = null;
}

→ Action definitions

→ Enable/disable rules

Core › Input › Input

Game Controls

GameControls.inputactions \u2014 bindings shared by all channels.

GameControls.inputactions at Assets/Scripts/Framework/Input/GameControls.inputactions is the single source of bindings. All channel assets reference this file; each channel exposes one map from it.

Player map \u2192 PlayerInputChannel

  • Move (Vector2) \u2014 WASD, left stick
  • Fire, Interact, Pause (Button)

UI map \u2192 UIInputChannel

  • Navigate (Vector2) \xB7 Submit, Cancel, Point, Click

Adding an action

  • Define action in the Input Actions editor.
  • Add event + Hook(...) in the channel class; clear in ClearSubscribers().
  • Subscribe from gameplay or UI code.
Action names must match between the .inputactions asset and Hook(...) calls.

→ Event wiring

Core › Input › Input

Modal Stack

Focus registry that suppresses gameplay input during overlays.

ModalStack tracks UI layers that currently hold focus. InputStateController reads IsAnyOpen and keeps PlayerInputChannel disabled while any entry is registered.

  • Push(object token) / Pop(object token)
  • IsAnyOpen \xB7 AnyOpenChanged(bool)
  • PauseMenuManager \u2014 Push on pause, Pop on resume/destroy.
  • PromptService \u2014 Push while a blocking prompt is visible.
void OnPaused()
{
    ModalStack.Push(this);
    _pauseRoot.SetActive(true);
}

void OnDestroy() => ModalStack.Pop(this);

→ How modals affect routing

Core › Input › Input

Input Device Tracker

Active device class for button prompts.

InputDeviceTracker reports the active device class \u2014 keyboard/mouse or gamepad \u2014 for button-prompt glyphs and cursor visibility. Map routing remains the responsibility of InputStateController.

Setup

  • One instance on PersistentLevel.
  • CurrentScheme \xB7 SchemeChanged

Example

void OnEnable()
{
    InputDeviceTracker.Instance.SchemeChanged += Apply;
   Apply(InputDeviceTracker.Instance.CurrentScheme);
}

void Apply(InputScheme scheme)
{
    bool kb = scheme == InputScheme.KeyboardMouse;
    _kbHints.SetActive(kb);
   _padHints.SetActive(!kb);
    Cursor.visible = kb;
}

→ Channel enablement

Core › Input › Input

Input Map Routing

When PlayerInputChannel vs UIInputChannel is enabled.

Exactly one channel is active at a time. InputStateController on PersistentLevel evaluates app state and calls Enable() / Disable() on the channel assets.

Decision rule

bool gameplay = AppState == InGame
             && GameSession.IsRunning
            && !ModalStack.IsAnyOpen;

ApplyChannel(_playerChannel, gameplay);
ApplyChannel(_uiChannel, !gameplay);

Channel states

  • PlayerInputChannel on \u2014 in-game, running, no modal.
  • UIInputChannel on \u2014 main menu, paused, prompt open, game over, transitions.

Pause sequence

  • PauseEvent \u2192 GameSession.TogglePause() \u2192 ModalStack.Push \u2192 router switches to UI channel.
  • Resume reverses: Pop \u2192 session running \u2192 player channel restored.

Refresh triggers

  • AppStateMachine.OnStateChanged \xB7 GameSession lifecycle events \xB7 ModalStack.AnyOpenChanged

→ Router implementation

→ Modal registration

Core

State Machine

Condition-driven FSM for local behaviour — AI, phases, UI flows, and entity logic.

The framework ships a small, condition-driven finite state machine (FSM) for local behaviour: enemy AI, weapon modes, tutorial steps, boss phases, or a multi-step UI flow on one screen. States are plain C# objects; transitions are Func<bool> guards evaluated every tick.

Not the same as AppStateMachine

AppStateMachine (Game layer) routes which scene is loaded — Main Menu, Game, Victory. The framework StateMachine drives behaviour inside a scene or entity. They solve different problems and are often used together.

→ App State Machine — top-level scene routing (Game layer)

Mental model

  • Each State implements OnStateEnter, OnStateTick, and OnStateExit.
  • A Transition pairs a target state with a condition (Func<bool>).
  • The StateMachine owns the current state and a transition table: Dictionary<State, List<Transition>>.
  • Call Tick() once per frame (or fixed step) from your MonoBehaviour, service, or test harness.

When to use it

  • Entity AI — patrol, chase, attack, stagger, flee with hysteresis thresholds.
  • Gameplay phases — prepare → combat → resolve loops, wave intermissions, turn-based steps.
  • Local UI flows — character creation steps, crafting wizards, modal sub-flows that stay on one canvas.
  • Weapon / tool modes — idle, charging, firing, cooldown without scattering boolean flags across Update().

When to use something else

  • Scene changes — use AppStateMachine + ApplicationManager, not this FSM.
  • Run lifecycle (pause, game over, retry) — use GameSession events.
  • Global app routing with only 3–4 screens — explicit methods or an app state enum is often enough; an FSM pays off when behaviour branches multiply.

Core types

  • State (abstract) — override OnStateTick(); optionally OnStateEnter() / OnStateExit() for one-shot setup and teardown.
  • Transitionnew Transition(targetState, () => condition).
  • StateMachine — constructed with a start state and the full transition map. No runtime AddState API — wire the graph up front (constructor clarity).

How Tick() works

  • 1. Evaluate transitions registered for the current state, in list order.
  • 2. The first transition whose condition returns true wins.
  • 3. If a target was chosen: call OnStateExit() on the old state, then OnStateEnter() on the new state, then switch.
  • 4. Always call OnStateTick() on the (possibly new) current state.
On the same frame as a transition, OnStateExit, OnStateEnter, and OnStateTick for the new state all run — design enter logic accordingly (avoid assuming tick always means "steady state").

Quick start

var idle = new IdleState();
var run = new RunState();

var fsm = new StateMachine(idle, new Dictionary<State, List<Transition>>
{
    [idle] = new List<Transition>
    {
        new Transition(run, () => _playerWantsMove),
    },
    [run] = new List<Transition>
    {
        new Transition(idle, () => !_playerWantsMove),
    },
});

void Update() => fsm.Tick();

Example — patrol / chase AI

Keep transition conditions cheap (distance checks, line-of-sight flags). Put movement and animation in OnStateTick; reset timers and play enter animations in OnStateEnter.

public sealed class PatrolState : State
{
    readonly Transform _self;
    readonly Transform _player;
    readonly float _aggroRange;

    public PatrolState(Transform self, Transform player, float aggroRange)
    {
        _self = self;
        _player = player;
        _aggroRange = aggroRange;
    }

    public bool ShouldChase =>
        Vector2.Distance(_self.position, _player.position) <= _aggroRange;

    public override void OnStateTick() => FollowWaypoints();
}

public sealed class ChaseState : State
{
    readonly Transform _self;
    readonly Transform _player;
    readonly float _loseRange;

    public ChaseState(Transform self, Transform player, float loseRange) { ... }

    public bool ShouldPatrol =>
        Vector2.Distance(_self.position, _player.position) >= _loseRange;

    public override void OnStateTick() => MoveTowards(_player.position);
}

// Awake — hysteresis: chase at 8m, drop aggro at 12m
var patrol = new PatrolState(transform, _player, aggroRange: 8f);
var chase = new ChaseState(transform, _player, loseRange: 12f);

_fsm = new StateMachine(patrol, new Dictionary<State, List<Transition>>
{
    [patrol] = new List<Transition> { new Transition(chase, () => patrol.ShouldChase) },
    [chase] = new List<Transition> { new Transition(patrol, () => chase.ShouldPatrol) },
});

Example — round phases

Phases map cleanly to states. Use OnStateEnter to kick off timers, UI, or spawners; use transitions to react to GameEvent raises or session signals.

public sealed class PreparePhase : State
{
    readonly Action _showBanner;
    public PreparePhase(Action showBanner) => _showBanner = showBanner;
    public override void OnStateEnter() => _showBanner();
    public override void OnStateTick() { }
}

public sealed class CombatPhase : State
{
    public bool RoundComplete { get; set; }
    public override void OnStateTick() => UpdateSpawners();
}

var prepare = new PreparePhase(ShowRoundBanner);
var combat = new CombatPhase();
var resolve = new ResolvePhase(ApplyScore);

_roundFsm = new StateMachine(prepare, new Dictionary<State, List<Transition>>
{
    [prepare] = new List<Transition>
    {
        new Transition(combat, () => _countdownFinished),
    },
    [combat] = new List<Transition>
    {
        new Transition(resolve, () => combat.RoundComplete),
    },
    [resolve] = new List<Transition>
    {
        new Transition(prepare, () => _continuePressed),
    },
});

Transition design rules

  • Order matters — put the most specific condition first; evaluation stops at the first match.
  • Register every state as a dictionary key, even if its transition list is empty (otherwise Tick() cannot look up outgoing edges).
  • Prefer hysteresis — different thresholds for enter vs leave (chase 8m / patrol 12m) to avoid flicker on the boundary.
  • Keep conditions side-effect free — they may run many times per second.
  • Share context via state instances — pass dependencies into state constructors instead of static globals.

Implementation tips

  • One FSM per entity or subsystem — do not build a single global machine for unrelated features.
  • Call Tick() from Update or a dedicated manager; for fixed-step gameplay, call from FixedUpdate consistently.
  • Unit-test states in isolation by invoking OnStateEnter/Tick/Exit directly; test transition tables with injected condition delegates.
  • Log transitions during development (Debug.Log($"{old} -> {new}") in a thin wrapper) — FSM bugs are usually wrong condition order or missing edges.

Template reference

  • Framework source: Assets/Scripts/Framework/StateMachine/
  • Minimal wiring sample: ExampleStateMachineHandler + ExampleState0/1/2 (illustrates transition lists; replace throws with real logic).
Utilities

MonoBehaviourSingleton

Typed MonoBehaviour singleton — Instance access, quit guard, duplicate protection.

The template uses MonoBehaviourSingleton<T> for scene-wide services that need a single live instance — ApplicationManager, GameSession, TimerManager, and others. The base class provides a typed Instance accessor, optional lazy creation, duplicate protection, and a quit guard so shutdown does not spawn ghost objects.

When to use

  • One manager per app/session — audio, notifications, scene loading, input device tracking.
  • Survives scene changes — enable dontDestroyOnLoad on persistent bootstrap objects.
  • Referenced from many systems — prefer HasInstance / ExistingInstance during teardown.

API

  • Instance — returns the live singleton; lazy-creates if missing (unless app is quitting).
  • HasInstance — true when an instance exists; safe gate before subscribe/unsubscribe.
  • ExistingInstance — returns instance without creating one; use in OnDestroy.
  • destroyFlag — set on duplicate components so subclass logic can bail in Awake.

Inspector settings

  • instantiationTypeLazy (default) or Instant (assign in Awake when placed in scene).
  • dontDestroyOnLoad — keep the GameObject across additive scene loads.

Example — gameplay manager

public class ScoreManager : MonoBehaviourSingleton<ScoreManager>
{
    [SerializeField] ScriptableInt _score;

    protected override void Awake()
    {
        base.Awake();
        if (destroyFlag) return;
        // init...
    }

    public void AddScore(int points) => _score.Value += points;
}

// Consumer
ScoreManager.Instance.AddScore(10);

Example — safe teardown

void OnEnable()
{
    if (GameSession.HasInstance)
        GameSession.Instance.OnRunStarted += ResetScore;
}

protected override void OnDestroy()
{
    if (GameSession.HasInstance)
        GameSession.Instance.OnRunStarted -= ResetScore;
    base.OnDestroy();
}
Always call base.Awake() and check destroyFlag before running subclass init. Use HasInstance in OnDestroy — never assume Instance still exists during quit.

Template singletons

  • ApplicationManager, AppStateMachine, GameSession, TimerManager, GameRandomizer, PromptService, NotificationController, InputDeviceTracker.
Utilities

Scene Field

Type-safe scene references in the inspector.

SceneField lets you drag a scene asset into a serialized field and get its name at runtime - no fragile string paths.

[SerializeField] SceneField gameScene;
Backed by SceneFieldPropertyDrawer in LeLa.Framework.Editor.
Utilities

Scriptable Events

GameEvent channels — Raise from code, receive via GameEventListener or Action bridges.

GameEvent is a shared ScriptableObject channel: publishers call Raise(), subscribers react without hard references to the publisher. The template ships two complementary ways to wire responses — GameEventListener (scene/prefab, UnityEvent) and Action delegates (code-first callbacks that call Raise() or handle timer bridges).

Create an event asset

  • Create → LeLa → Events → Game Event
  • Name by domain, e.g. TimerOverEvent, PlayerDiedEvent.
  • Assign the same asset on every publisher and listener.

Publishing

  • Call myEvent.Raise() from gameplay code when something happens.
  • Pass Raise() into an Action — common with Timer and OneShotEvent.
public class WorldTimer : MonoBehaviourSingleton<WorldTimer>
{
    [SerializeField] GameEvent _timerOverEvent;
    Timer _timer;

    void Start()
    {
        // Action bridge: timer callback raises the shared event
        _timer = new Timer(60f, true, () => _timerOverEvent?.Raise());
        _timer.Start();
    }
}

// Direct raise from gameplay
public void OnPlayerDied() => _playerDiedEvent.Raise();

Receiving — GameEventListener (UnityEvent)

Add a GameEventListener component to any GameObject. Assign the GameEvent asset and wire the Response UnityEvent in the Inspector to public methods on UI, audio, or gameplay objects. Registration happens automatically in OnEnable / OnDisable.

// Inspector setup:
// 1. Add GameEventListener to ResultsPanel
// 2. Assign TimerOverEvent asset
// 3. Response → ResultsPanel.ShowResults()

public class ResultsPanel : MonoBehaviour
{
    public void ShowResults() => gameObject.SetActive(true);
}
  • Supports multiple targets on one listener (UnityEvent list).
  • Ideal for prefabs, UI, and designer-authored reactions.

Receiving — Action delegates in code

When the reaction lives entirely in code, keep the listener component but target a method on the same or another script — that method is your Action-equivalent handler. For chained logic before broadcast, use an Action lambda that calls Raise() (see WorldTimer above).

public class DoorSequence : MonoBehaviour
{
    [SerializeField] GameEvent _doorOpenedEvent;

    public void OpenDoor()
    {
        PlayAnimation();
        _doorOpenedEvent.Raise(); // Action-style publisher
    }

    // Wired via GameEventListener Response:
    public void OnDoorOpenedPlaySound() => Audio.PlayOpenSfx();
}

Lifecycle

  • GameEventListener.OnEnableRegisterListener(this)
  • GameEventListener.OnDisableUnregisterListener(this)
  • Raise() iterates listeners in reverse order (safe for self-removal).

Editor tool

  • LeLa → Tools → Game Event Finder — lists every GameEventListener in open scenes and prefabs, grouped by event asset.
One GameEvent asset = one channel. Prefer many small events over one generic GameEvent with switch logic.

→ Action bridges with Timer and OneShotEvent

→ Shared ScriptableInt/Float used alongside events

Utilities

Scriptable Data

Shared ScriptableObject primitives, onValueChanged, and Reference types.

Scriptable primitives store live values in reusable assets so multiple systems share the same state without singletons or static fields. ScriptableInt, ScriptableFloat, and siblings expose a Value property with optional onValueChanged callbacks; IntReference / FloatReference let a field choose between a constant or a shared asset per instance.

Available types

  • ScriptableBool, ScriptableChar, ScriptableInt, ScriptableFloat
  • ScriptableString, ScriptableVector2, ScriptableVector3
  • Create via LeLa → Data → … (or matching CreateAssetMenu entry).

Shared runtime state

// Score.asset (ScriptableInt) referenced by HUD + ScoreManager
public class ScoreManager : MonoBehaviourSingleton<ScoreManager>
{
    [SerializeField] ScriptableInt _score;

    public void AddScore(int points) => _score.Value += points;
    public void ResetScore() => _score.Value = 0;
}

// Implicit cast: if (_score > highScore) ...
void UpdateHud() => _label.text = _score.Value.ToString();

Reacting to changes

void OnEnable()
{
    _musicVolume.onValueChanged += ApplyVolume;
    ApplyVolume(_musicVolume.Value);
}

void OnDisable() => _musicVolume.onValueChanged -= ApplyVolume;

void ApplyVolume(float v) => _audio.SetMusicVolume(v);
  • LLFMODManager persists bus volumes through ScriptableFloat + PlayerPrefs.
  • GameRandomizer stores the global RNG seed in a ScriptableInt.

Per-instance vs shared — References

IntReference and FloatReference (and other Reference types) add a useConstant toggle: designers pick a literal default or link to a shared Scriptable asset on each component.

[SerializeField] IntReference _damage; // constant 5 OR shared DamageTable asset

void Hit() => ApplyDamage(_damage.Value);

Editor tool

  • LeLa → Tools → Scriptable Primitive Finder — scan scenes and prefabs for references to a selected primitive asset.
Scriptable assets are shared at runtime — mutating Value affects every reference. Reset gameplay values on GameSession.OnRunStarted where needed.
Utilities

Object Pool

Generic component pool — Init, GetObject, ReturnObject.

ObjectPool<T> reuses prefab instances instead of calling Instantiate / Destroy every spawn cycle. Each closed generic type (ObjectPool<Bullet>, ObjectPool<VfxBurst>) gets its own lazy singleton queue.

Workflow

  • Init(prefab, initialSize) — pre-warm the pool (objects start inactive).
  • GetObject() — dequeue, activate, return instance (grows pool if empty).
  • ReturnObject(instance) — deactivate and enqueue for reuse.

Example — projectile spawner

public class BulletSpawner : MonoBehaviour
{
    [SerializeField] Bullet _bulletPrefab;
    ObjectPool<Bullet> _pool;

    void Awake()
    {
        _pool = ObjectPool<Bullet>.Instance;
        _pool.Init(_bulletPrefab, initialSize: 32);
    }

    public void Fire(Vector3 origin, Vector2 direction)
    {
        Bullet b = _pool.GetObject();
        b.transform.position = origin;
        b.Launch(direction);
    }
}

public class Bullet : MonoBehaviour
{
    public void Launch(Vector2 dir) { /* ... */ }

    void OnLifetimeExpired()
        => ObjectPool<Bullet>.Instance.ReturnObject(this);
}

Behaviour notes

  • Init rebuilds the pool if it was empty or contained destroyed references.
  • Returned objects must reset state in GetObject or an OnSpawn method.
  • Pool grows automatically when demand exceeds pre-warmed count.
Every GetObject() needs a matching ReturnObject() — leaks show up as active object count climbing during play.

→ Auto-return after delay

Utilities

Timer

Countdown timers, repeating intervals, and OneShotEvent milestones.

The timer system splits responsibilities: Timer holds countdown state and callbacks; TimerManager (singleton) ticks every registered timer each frame. Use it for delays, repeating intervals, and one-shot milestones without coroutines.

Core API — Timer

  • new Timer(waitTime, oneShot, params Action[]) — registers with TimerManager automatically.
  • Start() / Stop() / Pause() / Resume()
  • TimeLeft, NormalizedTimeLeft, TimeScale (independent of Time.timeScale unless you tie them).
  • OnTimeout event — additional subscribers after construction.

Example — one-shot delay

var delay = new Timer(2f, oneShot: true, () => Debug.Log("2s elapsed"));
delay.Start();

// Or subscribe after construction
var t = new Timer(5f);
t.OnTimeout += HideBanner;
t.Start();

Example — repeating timer

var pulse = new Timer(1f, oneShot: false, () => PulseUi());
pulse.Start(); // fires every second until Stop()

OneShotEvent milestones

OneShotEvent fires once when a threshold is crossed — percentage remaining or absolute seconds left. WorldTimer uses this to raise GameEvent assets at 30s and 10s.

_timer = new Timer(_maxTime.Value, true, () => _timerOverEvent?.Raise());

_timer.RegisterEvent(
    new OneShotEvent(30f, OneShotEventType.Absolute, () => _30SekEvent?.Raise()));
_timer.RegisterEvent(
    new OneShotEvent(0.25f, OneShotEventType.Percentage, WarnLowTime));

_timer.Start();
  • OneShotEventType.Absolute — threshold in seconds remaining.
  • OneShotEventType.Percentage — threshold as normalized time left (0–1).

TimerManager

  • Place on PersistentLevel (or rely on lazy singleton creation).
  • Calls Tick(Time.deltaTime) on all registered timers in Update.
TimerManager must exist in play mode — the template places it on the persistent scene. Constructing a Timer before the manager exists triggers lazy creation.

→ Raise GameEvent from timer callbacks

→ TimerManager base class

Utilities

Parallax

2D camera-linked scrolling with seamless sprite wrapping.

The Parallax component scrolls a sprite layer at a fraction of camera movement to create depth in 2D scenes. It supports horizontal and vertical wrapping using the attached SpriteRenderer bounds.

Setup

  • Add Parallax to a background layer GameObject with a SpriteRenderer.
  • Assign _cam — typically the main camera GameObject.
  • Set _parallaxEffect0 = fixed to world, 1 = moves with camera, values between = depth (e.g. 0.2 far, 0.8 near).

How it works

  • On Start, stores start position and sprite bounds size.
  • Each frame, offsets position by cameraPosition * parallaxEffect.
  • When the layer scrolls beyond one tile width/height, start anchor shifts — seamless loop.

Recommended layering

Sky         parallaxEffect = 0.0   (static or manual)
Mountains   parallaxEffect = 0.2
Trees       parallaxEffect = 0.5
Gameplay    parallaxEffect = 1.0   (no Parallax component)

Example scene hierarchy

BackgroundRoot
  ├── SkyLayer        (Parallax, effect 0.05)
  ├── MountainLayer   (Parallax, effect 0.25)
  └── ForestLayer     (Parallax, effect 0.55)
Main Camera  → assigned to each _cam field
Sprite width/height must match the repeating tile size for clean wrapping. Use power-of-two or evenly tileable art where possible.
Utilities

Randomizer

GameRandomizer — seeded System.Random shared across gameplay.

GameRandomizer is the template's seeded System.Random service. It reads and writes a shared ScriptableInt seed asset so runs can be reproduced in QA, replays stay deterministic, and gameplay systems share one RNG stream.

Setup

  • Place GameRandomizer on PersistentLevel (default execution order -20000).
  • Assign a ScriptableInt asset for _globalSeed.
  • _alwaysReseedOnStart — when enabled, generates a fresh seed every Play (debug/arcade). Disable for deterministic runs from a fixed seed.

Seed API

  • GetSeed() / SetSeed(int) — read or force a seed (0 is coerced to 1).
  • ReseedBySystemTime() / RegenerateGlobalSeed() — new seed from UTC ticks + GUID hash.

Random API

  • NextInt(minInclusive, maxExclusive)
  • NextFloat01() / NextDouble01()
  • NextSign() — returns -1 or +1
  • ShuffleInPlace<T>(IList<T>) — Fisher–Yates in place

Example — loot table roll

int RollLootIndex(IReadOnlyList<LootEntry> table)
{
    int roll = GameRandomizer.Instance.NextInt(0, table.Count);
    return roll;
}

void ShuffleDeck(List<Card> deck)
    => GameRandomizer.Instance.ShuffleInPlace(deck);

Example — fixed seed for QA

// Inspector: _alwaysReseedOnStart = false, ScriptableInt seed = 42
GameRandomizer.Instance.SetSeed(42);
int a = GameRandomizer.Instance.NextInt(0, 100);
GameRandomizer.Instance.SetSeed(42);
int b = GameRandomizer.Instance.NextInt(0, 100);
// a == b
Use GameRandomizer for gameplay randomness — avoid UnityEngine.Random when tests or replays need a controllable seed.

→ ScriptableInt seed asset

→ Deterministic scaling curves (separate utility)

Utilities

Fibonacci

FibonnaciSequence — memoized Fibonacci scaling curves.

FibonnaciSequence (static utility) returns Fibonacci numbers by index with memoization. Use it for deterministic scaling curves — spawn budgets, cost tables, wave difficulty — where exponential growth should stay predictable and allocation-free after warm-up.

API

  • FibonnaciSequence.GetValue(int index) — 0-based index; returns -1 for negative index.
  • Results are cached in a static dictionary — repeated lookups are O(1) after first compute.

Sequence values

index:  0  1  2  3  4  5  6  7  8
value:  1  1  2  3  5  8 13 21 34

Example — wave spawn count

int EnemiesForWave(int waveIndex)
{
    int fib = FibonnaciSequence.GetValue(waveIndex);
    return Mathf.Min(fib, 55); // cap difficulty
}

// wave 0 → 1 enemy, wave 5 → 8, wave 7 → 21

Example — upgrade cost curve

int CostForLevel(int level)
{
    int baseCost = 10;
    return baseCost * FibonnaciSequence.GetValue(level);
}
Class name in code is FibonnaciSequence (project spelling). For random rolls use GameRandomizer — Fibonacci is deterministic, not random.

→ Seeded gameplay randomness

Tech Art

Tileset Rules

SiblingRuleTile — cross-tile autotiling by SiblingGroup.

SiblingRuleTile extends Unity's RuleTile (2D Tilemap Extras) so autotiling matches tiles in the same SiblingGroup, not just the same tile asset. Different grass/dirt/stone tiles blend as one surface when they share a group — essential for modular terrain art.

When to use

  • Multiple tile assets represent the same material (variants, edges, seasons).
  • Standard RuleTile neighbour rules fail across visually related tiles.
  • Level artists paint with variety while keeping clean corner/edge blending.

Create a Sibling Rule Tile

  • Create → LeLa → Tiles → Sibling Rule Tile
  • Configure sprites and neighbour rules like any RuleTile.
  • Set siblingGroup — tiles with the same group autotile together.

SiblingGroup

  • Ground — built-in enum value; extend SiblingGroup in code for Water, Cliff, etc.
  • This neighbour — matches another SiblingRuleTile with the same group.
  • NotThis neighbour — matches anything outside the group.

Example workflow

Grass_A.asset   → SiblingRuleTile, siblingGroup = Ground
Grass_B.asset   → SiblingRuleTile, siblingGroup = Ground
Dirt_A.asset    → SiblingRuleTile, siblingGroup = Ground
Cliff_A.asset   → SiblingRuleTile, siblingGroup = Cliff

// Grass_A blends into Dirt_A at borders; Cliff_A stays isolated

RuleOverrideTile

  • Overrides are resolved before sibling comparison — works with Unity's override tiles.
Requires com.unity.2d.tilemap.extras (referenced by LeLa.Framework). Paint on a Tilemap with the Tile Palette as usual.
Game Layer

App State Machine

Top-level scene routing — which screen is active (Main Menu, Game, Victory).

AppStateMachine is the game-specific router for top-level screens. It decides which scene should be visible (Main Menu, Game, Victory) and delegates the actual async load/unload to ApplicationManager.

Role in the stack

  • FrameworkApplicationManager loads/unloads additive scenes and raises loading events. It does not know your game rules.
  • GameAppStateMachine maps high-level intent (GoToMainMenu(), GoToGame()) to those scene operations.
  • GameGameSession tracks the current run (pause, game over). Scene routing and run lifecycle are intentionally separate concerns.

AppState values

  • Booting — initial state before the first transition.
  • Transitioning — set while a scene change is in flight.
  • MainMenu — main menu content scene is active.
  • InGame — a gameplay content scene is active.
  • Victory — victory/results content scene is active.

Public API

  • GoToMainMenu() — load the configured main-menu scene; set state to MainMenu.
  • GoToGame() — load the default game scene; set InGame and call GameSession.StartNewRun().
  • GoToLevel(sceneName) — load any named gameplay scene (same as GoToGame but explicit).
  • GoToVictory() — load the victory/results scene.
  • CurrentState / PreviousState — read-only; subscribe to OnStateChanged(previous, next) for reactions (input maps, UI, analytics).

Example

// From UI or bootstrap:
AppStateMachine.Instance.GoToMainMenu();
AppStateMachine.Instance.GoToGame();
AppStateMachine.Instance.GoToVictory();

AppStateMachine.Instance.OnStateChanged += (prev, next) =>
    Debug.Log($"App {prev} -> {next}");

Integration details

  • Before every scene change, GameSession.RestoreTimeScaleForSceneChange() runs so a paused run never freezes the next scene.
  • If ApplicationManager is not available (e.g. you hit Play on a content scene in the editor), the machine falls back to SceneManager.LoadScene.
Configure scene references on the AppStateMachine component in PersistentLevel via SceneField assets.

Related systems

→ Application Flow — end-to-end cold start

→ Application Manager — additive scene loader

→ Game Session — run lifecycle (orthogonal to routing)

→ Input State Controller — reacts to AppState + session

Game Layer

Game Session

Lifecycle of a single gameplay run — start, pause, game over, retry.

GameSession owns the lifecycle of a single play run: start, pause, resume, game over, retry and return to menu. It replaces a monolithic GameManager with explicit states and events other systems can subscribe to.

AppState vs GameState

  • AppState (via AppStateMachine) — which screen/scene is active (Main Menu, In Game, Victory).
  • GameState (via GameSession) — what the current run is doing (Idle, Running, Paused, Ending).

Example: you can be AppState.InGame while GameState.Paused — same scene, frozen gameplay, UI map active.

GameState values

  • Idle — no active run (e.g. in menu or between runs).
  • Running — gameplay is live; Time.timeScale is 1.
  • Paused — run frozen; Time.timeScale is 0.
  • Ending — game-over reported; waiting for UI/navigation.

Events

  • OnRunStarted — new run began (reset score, timer, spawners here).
  • OnPaused / OnResumed — pause toggled.
  • OnGameOver — receives GameEndResult (victory, defeat, user abort).

Public API

  • StartNewRun() — called by AppStateMachine.GoToGame(); sets Running and fires OnRunStarted.
  • Pause() / Resume() / TogglePause() — time-scale pause handling.
  • ReportGameOver(result) — end the run; listeners show results or stats.
  • Retry() — reload current game scene via AppStateMachine.GoToGame().
  • AbortToMenu() — report user abort and route to main menu.

Example

GameSession.Instance.OnRunStarted += ResetScore;
GameSession.Instance.OnGameOver += result => ShowResults(result);
GameSession.Instance.TogglePause();

GameSession.Instance.ReportGameOver(GameEndResult.Victory);

Who listens?

  • ScoreManager / WorldTimer — reset on OnRunStarted, stop on OnGameOver.
  • PauseMenuManager — calls TogglePause().
  • InputStateControllerPlayerInputChannel only when InGame + Running + no modal.
Lives on PersistentLevel next to AppStateMachine — survives scene changes.

Related systems

→ Application Flow — when StartNewRun fires

→ App State Machine — triggers StartNewRun on GoToGame

→ Gameplay: Score & Timer — example listeners

→ Input State Controller — pause and modal aware input

Game Layer

Input State Controller

Routes app state to PlayerInputChannel and UIInputChannel enablement.

InputStateController lives on PersistentLevel and connects application state to channel enablement. It survives additive scene loads so routing stays consistent across MainMenu \u2194 Game transitions.

References

  • _playerChannel \u2192 PlayerInputChannel.asset
  • _uiChannel \u2192 UIInputChannel.asset

Inputs

  • AppStateMachine, GameSession, ModalStack

Implementation

[DefaultExecutionOrder(-17000)]
void Refresh()
{
    bool gameplay = inGame && running && !ModalStack.IsAnyOpen;
    ApplyChannel(_playerChannel, gameplay);
    ApplyChannel(_uiChannel, !gameplay);
}

Full routing table: Input Map Routing.

  • Consumers assign channel assets and subscribe to events; routing stays in this component.

→ Architecture

→ Decision rule and scenarios

Game Layer

Gameplay: Score & Timer

Example gameplay systems wired to the session.

ScoreManager resets on OnRunStarted; WorldTimer stops on OnGameOver. They demonstrate the decoupled session-event pattern.

Template Content

Menus

Main, Options and Pause menus.

  • MainMenuManager - drives panels, routes Start through AppStateMachine, plays UI sounds via AppAudio.
  • OptionsMenuManager - binds volume sliders to the AppAudio facade.
  • PauseMenuManager - in-game overlay using PlayerInputChannel + GameSession.
Editor Tools

Documentation CMS

This documentation system.

Open via LeLa -> Documentation. Content is stored as DocCategory and DocArticle ScriptableObjects under Assets/Documentation, rendered with UIToolkit (USS styling).

Authoring

  • Toggle Edit in the toolbar, then \uFF0B Article / \uFF0
  • Select an article and edit its blocks in the Inspector (Heading, Paragraph, Code, Bullet, Note, Image).
  • Paragraphs/headings support rich text: bold, italic, color.

Export

  • LeLa -> Documentation Tools -> Export to HTML writes a standalone page to Assets/Documentation/Generated/index.html.

Extending block types

Add a value to DocBlockType, then handle it in DocumentationWindow.RenderBlock and DocsHtmlExporter.RenderBlock.

Run Generate Default Docs again any time to restore these system pages.
Editor Tools

ToDo Finder

Scan the project for TODO markers.

LeLa -> Tools -> ToDo Finder aggregates TODO/HACK/FIXME comments into a browsable window.

Editor Tools

Attributes & Drawers

Inspector quality-of-life.

  • [Required] - warns/validates missing references (incl. play-mode check).
  • [BitMask] - enum mask field drawer.
  • [MethodButton] - call methods from the inspector.
  • Serializable dictionary/queue drawers and the SceneField drawer.
Editor Tools

Bootstrap Scene Builder

One-click persistent scene setup.

LeLa -> Setup -> Build Persistent Bootstrap Scene creates PersistentLevel with the core managers wired up and updates Build Settings.

Extending the Template

Add a New System

Where new code belongs.

  • Generic & reusable -> Assets/Scripts/Framework (LeLa.Framework).
  • Game-specific -> Assets/Scripts/Game (Game).
  • Editor-only tooling -> the matching /Editor folder.
Never let Framework depend on Game. Add assembly references only downward.
Extending the Template

Authoring Documentation

Keep these docs alive.

Document new systems here so the template stays self-explaining. Use Edit mode to add an article under the right category, or extend the generator (DefaultDocumentation.cs) so the page is recreated on regenerate.