---
title: "Systems"
description: "Timers, NeoMover, file storage, the asset cache, windowing, and the letterbox viewport."
canonical_url: "https://docs.ctrl-neo.dev/en/neogamelib/systems"
---
# Systems

The supporting cast: timers, the built-in movement handler, file storage, the
asset cache, windowing/viewport control, and two small utilities.

## Timers

`NeoTimer` is a standalone countdown (not a component). While running it
registers with `NeoGame.TimerBus`, which ticks it at the end of every frame;
on stop/finish it unregisters, so idle timers cost nothing.

```csharp
NeoTimer invuln = new(0.5f);                       // seconds
invuln.OnFinished = () => { /* end of invulnerability */ };
invuln.Start();                                    // Start while running restarts from full

// a looping timer:
spawnTimer.OnFinished = () => spawnTimer.Start();
```

<table>
<thead>
  <tr>
    <th>
      Member
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        Duration
      </code>
    </td>
    
    <td>
      seconds (settable)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        TimeLeft
      </code>
    </td>
    
    <td>
      remaining seconds (read-only)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        IsRunning
      </code>
      
       / <code>
        IsPaused
      </code>
    </td>
    
    <td>
      state
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Start()
      </code>
    </td>
    
    <td>
      (re)start from full duration; fires <code>
        OnStart
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Stop()
      </code>
    </td>
    
    <td>
      kill; zeroes <code>
        TimeLeft
      </code>
      
      , fires <code>
        OnStop
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Pause()
      </code>
      
       / <code>
        Resume()
      </code>
    </td>
    
    <td>
      freeze/unfreeze the countdown; fire <code>
        OnPause
      </code>
      
      /<code>
        OnResume
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        OnStart
      </code>
      
       / <code>
        OnStop
      </code>
      
       / <code>
        OnPause
      </code>
      
       / <code>
        OnResume
      </code>
      
       / <code>
        OnFinished
      </code>
    </td>
    
    <td>
      callbacks
    </td>
  </tr>
</tbody>
</table>

<callout color="primary" icon="i-lucide-info">

While `async`/`await` works in MonoGame, `Task.Delay` doesn't pause with the
game and cancelling is a pain — `NeoTimer` is the pause-friendly alternative.
Don't call `Start()` from inside an update loop that already has the timer
running.

</callout>

Timer callbacks can start/stop/restart any timer safely (the bus snapshots).
Ticks run last each frame, after collisions.

## NeoMover

One movement component covering keys, `Move()`, lerp smoothing, gravity,
jump, ground detection, and collider push-out. Everything is optional.

```csharp
NeoMover mover = player.AddComponent<NeoMover>();
mover.KeyLeft = Keys.A;
mover.KeyRight = Keys.D;
mover.KeyJump = Keys.Space;
mover.GroundComponentType = typeof(GroundTag);   // needs a NeoBoxCollider on the same object
```

<table>
<thead>
  <tr>
    <th>
      Member
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Meaning
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        KeyUp
      </code>
      
       / <code>
        KeyDown
      </code>
      
       / <code>
        KeyLeft
      </code>
      
       / <code>
        KeyRight
      </code>
      
       / <code>
        KeyJump
      </code>
    </td>
    
    <td>
      <code>
        null
      </code>
    </td>
    
    <td>
      bound keys; all null = keys never polled (pure <code>
        Move()
      </code>
      
       control)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Speed
      </code>
    </td>
    
    <td>
      200
    </td>
    
    <td>
      px/s horizontal
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Gravity
      </code>
    </td>
    
    <td>
      2000
    </td>
    
    <td>
      px/s²; 0 = fly
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        JumpVelocity
      </code>
    </td>
    
    <td>
      600
    </td>
    
    <td>
      px/s upward
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Omnidirectional
      </code>
    </td>
    
    <td>
      true
    </td>
    
    <td>
      false = vertical input ignored (platformer style)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        UseLerp
      </code>
    </td>
    
    <td>
      true
    </td>
    
    <td>
      smooth direction changes
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LerpSnappiness
      </code>
    </td>
    
    <td>
      10
    </td>
    
    <td>
      higher = snappier
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GroundComponentType
      </code>
    </td>
    
    <td>
      <code>
        null
      </code>
    </td>
    
    <td>
      component type marking ground (<code>
        typeof(GroundTag)
      </code>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Grounded
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      touching a ground-tagged object (read-only)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CollisionWhitelist
      </code>
      
       / <code>
        CollisionBlacklist
      </code>
    </td>
    
    <td>
      empty
    </td>
    
    <td>
      pass-through lists (below)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Velocity
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      current velocity (read-only; exposed for custom collision correction)
    </td>
  </tr>
</tbody>
</table>

API: `Move(Vector2 direction)` sets the target direction; `Jump()` jumps only
while grounded; `AddImpulse(Vector2)` adds a velocity kick;
`AddToWhitelist<T>()` / `RemoveFromWhitelist<T>()` / `AddToBlacklist<T>()` /
`RemoveFromBlacklist<T>()` manage the lists generically.

### Solid vs pass-through

The mover pushes itself out of overlapping colliders (per axis, so it slides
along walls). What counts as solid:

<table>
<thead>
  <tr>
    <th>
      Lists
    </th>
    
    <th>
      Behavior
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      both empty
    </td>
    
    <td>
      everything is solid (default)
    </td>
  </tr>
  
  <tr>
    <td>
      whitelist set
    </td>
    
    <td>
      only whitelisted components pass through; everything else solid
    </td>
  </tr>
  
  <tr>
    <td>
      blacklist set
    </td>
    
    <td>
      only blacklisted components are solid; everything else pass-through
    </td>
  </tr>
  
  <tr>
    <td>
      both set
    </td>
    
    <td>
      whitelist wins (whitelisted pass through)
    </td>
  </tr>
</tbody>
</table>

```csharp
mover.CollisionWhitelist.Add(typeof(TriggerTag));   // pass through triggers
mover.CollisionBlacklist.Add(typeof(SolidTag));     // ONLY these block
```

Requirements: a `NeoBoxCollider` on the same object; `GroundComponentType` for
grounding. The mover also contributes a fall velocity; with `Gravity = 0` and
no keys, `Move()` alone drives it.

## File storage

Tauri-style file-as-object stores in `NeoGameLib.NeoStorage`. Missing files
and folders are created on load.

### FileStore

```csharp
FileStore cfg = new FileStore(Path.Combine(Directory.GetCurrentDirectory(), "Data", "config.txt"));
cfg.Load();                       // reads file into buffer; creates file/folder if missing

string raw = cfg.Get();           // whole buffer, \n line breaks
List<string> lines = cfg.GetAsLines();  // split lines, \r\n normalized, no trailing break

cfg.Set("hello\nworld");          // replace buffer verbatim
cfg.Set(new List<string> { "hello", "world" });  // each entry = one line
cfg.Write();                      // push buffer to disk
```

### JsonStore

`FileStore` subclass whose buffer is a JSON object:

```csharp
JsonStore save = new JsonStore(Path.Combine(Directory.GetCurrentDirectory(), "Data", "save.json"));
save.Load();

save.Set("playerName", "Neo");
save.Set("gold", 420);

string name = save.Get<string>("playerName");   // "Neo"
int gold = save.Get<int>("gold");               // 420
int nope = save.Get<int>("missing");            // default(T): 0, null, etc.

save.Write();
```

- `Get<T>(key)` returns `default(T)` for missing keys; `Set(key, object)`.
- `Options` is a `JsonSerializerOptions` (default: `IncludeFields = true`,
required so MonoGame structs like `Vector2`/`Rectangle` — public fields —
serialize instead of becoming `{}`). Swap in your own for customization.
- Load parses the buffer into a `JsonObject`; Write serializes it back.

### FolderStore

Folder-as-store: enumerates top-level files and instantiates store objects
for them.

```csharp
FolderStore data = new FolderStore(Path.Combine(Directory.GetCurrentDirectory(), "Data"));
data.Load();                       // creates the folder if missing; lists top-level files

JsonStore json = data.Instantiate<JsonStore>("save.json");  // wraps the file
```

`Files` lists top-level paths; no recursion or glob filtering (yet).

## AssetManager

Cached content loading, keyed by asset path. Access via `NeoGame.Assets` (or
the `Assets` shorthand in components).

```csharp
Texture2D bot = Assets.Load<Texture2D>("Sprites/bot");  // loads once, caches, returns cached after
Assets.Unload("Sprites/bot");                            // removes cache + disposes if IDisposable
Assets.UnloadAll();
```

`Load<T>(path, forceLoad: false)` — `forceLoad: true` re-reads and re-caches.
It wraps the game's `ContentManager`, so `Content.RootDirectory` still
applies.

## Windowing

`NeoGame` exposes window control:

```csharp
NeoGame.Singleton.SetWindowMode(NeoWindowMode.Windowed);              // bordered; restores last windowed size
NeoGame.Singleton.SetWindowMode(NeoWindowMode.Fullscreen);            // exclusive fullscreen
NeoGame.Singleton.SetWindowMode(NeoWindowMode.BorderlessFullscreen);  // borderless, no mode change
NeoWindowMode mode = NeoGame.Singleton.WindowMode;
```

Call `SetWindowMode` **after** the game initializes. macOS has no borderless
fullscreen: `BorderlessFullscreen` becomes `Fullscreen` there, and `WindowMode`
reports `Fullscreen`.

## Letterbox viewport

The game viewport doesn't have to match the window. Set an aspect and the
game draws into the largest centered box with that ratio, with bars around it:

```csharp
ViewportAspect = 16f / 9f;        // null (default) = fill the whole window
LetterboxColor = Color.Black;     // the bars
ClearColor = Color.CornflowerBlue; // the viewport background

Rectangle box = ViewportBounds;   // window-pixel rectangle the game draws into
```

- Cameras, sprites, text, canvases, `OnDraw`, and `OnDrawUI` all operate
inside that box.
- `Input.MousePosition` stays in **window pixels**;
`RenderBus.ScreenToWorld`, `NeoCanvas.ScreenToCanvas`, and
`ContainsScreenPoint` remove the bar offset.
- This is not a virtual resolution — everything renders at native pixels.
- `GetScreenCenterCoords()` returns the viewport center.
- The viewport is re-applied on every update and draw, so resizes and window
mode switches stay correct.

A canvas with `ReferenceResolution = 1280x720` in a 16:9 viewport scales
exactly, with no `Match` compromise (see [UI](https://docs.ctrl-neo.dev/7.ui#scaling)).

## NeoSingleton

For your own singletons, `NeoSingleton<T>` gives a `Singleton` property and
throws if a second instance is constructed:

```csharp
public class GameSettings : NeoSingleton<GameSettings> { /* ... */ }
```

## Direction2D

`Direction2D` (`Left`, `Right`, `Up`, `Down`) with `ToVector2()` and
`Invert()` extensions. Used by `NeoSpriteRenderer.IsOverWindowBounds` and
`NeoAnimation.FromGrid`.

## EventBus (avoid)

`NeoGameLib.FuckingEvents.EventBus` is a minimal type-keyed publish/subscribe
helper from before the bus architecture existed. Nothing in the library uses
it, and its own comments warn it's tech debt — prefer buses and components.


## Sitemap

See the full [sitemap](https://docs.ctrl-neo.dev/sitemap.md) for all pages.
