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.
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.
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
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().
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()).
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.
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.
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.
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.
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.
LLGUI is runtime + editor in Assets/Scripts/Framework/GUI/LLGUI.
Kenney UI Pack sprites ship as defaults; replace via theme or Settings sprite
defaults.
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.
Typography is configured once in LLGUISettings and shared across
all themes. Themes supply text colors; Settings supply font, size, weight
and spacing.
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.
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
"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).
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.
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.
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.
API
Push(object token) / Pop(object token)
IsAnyOpen \xB7 AnyOpenChanged(bool)
Usage in template
PauseMenuManager \u2014 Push on pause, Pop on resume/destroy.
PromptService \u2014 Push while a blocking prompt is visible.
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.
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.
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.
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.
Transition — new 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.
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
instantiationType — Lazy (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);
Always call base.Awake() and check destroyFlag before running subclass init. Use HasInstance in OnDestroy — never assume Instance still exists during quit.
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();
}
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.
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.
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.
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.
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 _parallaxEffect — 0 = 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.
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.
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.
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.
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
Framework — ApplicationManager loads/unloads additive scenes and raises loading events. It does not know your game rules.
Game — AppStateMachine maps high-level intent (GoToMainMenu(), GoToGame()) to those scene operations.
Game — GameSession 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.
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().
InputStateController — PlayerInputChannel only when InGame + Running + no modal.
Lives on PersistentLevel next to AppStateMachine — survives scene changes.
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.
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.
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.