---
title: "Game Objects"
description: "Worlds, objects, components, transforms, and queries — the GameObject model at NeoGameLib's core."
canonical_url: "https://docs.ctrl-neo.dev/en/neogamelib/game-objects"
---
# Game Objects

The heart of the library: worlds hold objects, objects hold components, and
components hold all your logic.

```csharp
NeoWorld world = WorldManager.CreateWorld("level1", w =>
{
    NeoObject player = w.Instantiate("Player", new Transform(new Vector2(100, 100)));
    player.AddComponent(new NeoSpriteRenderer(RenderBus, sheet));
});
WorldManager.Load("level1");
```

## NeoGame

`NeoGame` is your `Game` subclass. It creates all buses and managers in
`Initialize` (before any of your content loads), ticks them in a fixed order,
and draws the world/UI passes.

- `NeoGame.Singleton` — the static instance (set by the first constructed `NeoGame`).
- `NeoGame<T>` — for a typed singleton: `public class MyGame : NeoGame<MyGame>`
gives you `MyGame.Singleton` of type `MyGame`.

Manager properties available from anywhere via `NeoGame.Singleton`, and as
shorthands inside any `NeoComponent` (they are `null` only before the game
initializes, which is before any of your hooks run):

<table>
<thead>
  <tr>
    <th>
      Property
    </th>
    
    <th>
      Type
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        RenderBus
      </code>
    </td>
    
    <td>
      <code>
        NeoRenderBus
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        ColliderBus
      </code>
    </td>
    
    <td>
      <code>
        NeoColliderBus
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        TimerBus
      </code>
    </td>
    
    <td>
      <code>
        NeoTimerBus
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        InputBus
      </code>
    </td>
    
    <td>
      <code>
        NeoInputBus
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        UIBus
      </code>
    </td>
    
    <td>
      <code>
        NeoUIBus
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        WorldManager
      </code>
    </td>
    
    <td>
      <code>
        WorldManager
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Assets
      </code>
    </td>
    
    <td>
      <code>
        AssetManager
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GraphicsDeviceManager
      </code>
    </td>
    
    <td>
      <code>
        GraphicsDeviceManager
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GraphicsViewport
      </code>
    </td>
    
    <td>
      <code>
        Viewport
      </code>
      
       (graphics device viewport)
    </td>
  </tr>
</tbody>
</table>

Windowing and viewport properties (`ViewportAspect`, `ClearColor`,
`SetWindowMode`, ...) are documented in [Systems](https://docs.ctrl-neo.dev/8.systems#windowing).

## Worlds

A `NeoWorld` is a container of objects that ticks every frame while loaded.

### The registry model

`WorldManager` keeps a registry of named worlds. You **create** a world once
(optionally with a builder), then **load** it to build and run it, and
**unload** it to destroy its objects.

- Objects are built on **Load**, not Create. The builder callback runs every
load.
- Unloading destroys every object in the world. The world stays registered and
can be loaded again (which reruns the builder from scratch).
- Any number of worlds can be loaded at once; each ticks independently.
- There is no world state persistence: this is a registry, not a save system.
If a world needs initial state, put it in the builder.

```csharp
WorldManager.CreateWorld("ui", w => { /* build HUD objects */ });
WorldManager.CreateWorld("level1", w => { /* build gameplay objects */ });

WorldManager.Load("ui");
WorldManager.Load("level1");

NeoWorld hud = WorldManager.GetWorld("ui");          // also: WorldManager["ui"]
NeoObject obj = hud.FindObjectByComponent<HudManager>();

WorldManager.Unload("level1");   // destroys every object in level1
WorldManager.Load("level1");     // rebuilt from the builder
WorldManager.RenameWorld("ui", "hud");
WorldManager.UnloadAll();        // unloads everything; registrations stay
```

WorldManager API:

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

<tbody>
  <tr>
    <td>
      <code>
        CreateWorld(string name, Action<NeoWorld> build = null)
      </code>
    </td>
    
    <td>
      register a new world; throws if the name exists
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        CreateWorld(NeoWorld world, Action<NeoWorld> build = null)
      </code>
    </td>
    
    <td>
      register an existing instance (not recommended — see below)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Load(string name)
      </code>
    </td>
    
    <td>
      run builder + start ticking; no-op if already loaded
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Unload(string name)
      </code>
    </td>
    
    <td>
      stop ticking + destroy all objects; stays registered
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        UnloadAll()
      </code>
    </td>
    
    <td>
      unload every loaded world
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GetWorld(name)
      </code>
      
       / <code>
        this[name]
      </code>
      
       / <code>
        TryGetWorld(name, out w)
      </code>
    </td>
    
    <td>
      look up a registered world
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        IsLoaded(name)
      </code>
    </td>
    
    <td>
      is it currently ticking?
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        RenameWorld(name, newName)
      </code>
    </td>
    
    <td>
      rename registry entry (keeps collision groups)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        LoadedWorlds
      </code>
    </td>
    
    <td>
      loaded worlds in load order
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        GroupCollisions(group, params string[] worlds)
      </code>
    </td>
    
    <td>
      see <a href="https://docs.ctrl-neo.dev/5.collision#world-collision-groups">
        Collision
      </a>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        UngroupCollisions(world)
      </code>
    </td>
    
    <td>
      remove from any group
    </td>
  </tr>
</tbody>
</table>

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

Registering a world you created by hand (instead of via the name overload)
is allowed but discouraged: if you instantiated objects outside the builder,
they will be gone after an unload/reload cycle.

</callout>

## NeoObject

Objects are identity + transform + a component list. Put behavior in
components, not in object subclasses.

```csharp
NeoObject player = world.Instantiate("Player", new Transform(new Vector2(100, 100)));
player.AddComponent<PlayerController>();
```

### Creating objects

`NeoWorld.Instantiate` overloads:

- `Instantiate(string name)` — default transform (0,0), scale 1
- `Instantiate(string name, Vector2 position)`
- `Instantiate(string name, Transform transform)`
- `Instantiate<T>(T obj)` — keeps the object's current transform
- `Instantiate<T>(T obj, Vector2 position)` — overwrites position
- `Instantiate<T>(T obj, Transform transform)` — overwrites the whole transform

`Instantiate` assigns the object's `World`, adds it to the world list, and
fires `OnInstantiate()` — a virtual on `NeoObject` that runs before any
component `OnAwake`, and is the recommended place to add components instead of
the constructor.

### Destroying objects

- `obj.DestroyToQueue()` — the normal path. Queued; the object and its
components are destroyed at the end of the current world tick. Component
`OnDestroy` runs immediately when the queue flushes.
- `world.Destroy(obj)` — same queue, from the world side.
- `obj.DestroyImmediate()` — destroys right now, mid-callback. Mostly used
internally; prefer the queue unless you need instant teardown.

`DestroyImmediate` marks the object destroyed and runs `OnDestroy` on every
component once, in reverse add order.

### Components

```csharp
public T AddComponent<T>() where T : NeoComponent, new();
public T AddComponent<T>(T component) where T : NeoComponent;
public T GetComponent<T>() where T : NeoComponent;      // null if absent
public bool TryGetComponent<T>(out T component);
public bool HasComponent<T>();
public bool HasComponent(Type type);
public bool RemoveComponent(NeoComponent component);
public bool RemoveComponent<T>();
```

`AddComponent` fires `OnAwake` immediately. The added component becomes
visible to `GetComponent` right away, but components added after it are not
yet awake.

`Components` is the raw list. Iterate it with `PassSnapshot` (below).

## NeoComponent

The lifecycle of a component:

1. **OnAwake** — once, right when `AddComponent` attaches it. `ParentObject`,
`ParentTransform`, `ParentWorld` are all set. This is where the built-in
components register themselves with buses; use it for your own one-time
setup that needs the parent.
2. **OnStart** — once, on the first tick *while enabled*. By now the whole
component list is registered, so `GetComponent` sees everything.
3. **OnUpdate(GameTime)** — every world tick while `Enabled`.
4. **OnDestroy** — once, when removed from the object or the object dies.
The parent is still attached during the call — the place to clean up.

```csharp
public class PlayerController : NeoComponent
{
    private NeoSpriteRenderer _renderer;

    public override void OnAwake()
    {
        _renderer = ParentObject.GetComponent<NeoSpriteRenderer>();
    }

    public override void OnUpdate(GameTime gameTime)
    {
        float speed = 200f * (float)gameTime.ElapsedGameTime.TotalSeconds;
        if (Input.LeftMouseButton.IsDown())
            ParentTransform.Move(new Vector2(1, 0), gameTime);
    }
}
```

Component members:

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

<tbody>
  <tr>
    <td>
      <code>
        ParentObject
      </code>
      
       / <code>
        ParentTransform
      </code>
      
       / <code>
        ParentWorld
      </code>
    </td>
    
    <td>
      the owning object and its bits
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Enabled
      </code>
    </td>
    
    <td>
      off = skips <code>
        OnStart
      </code>
      
      /<code>
        OnUpdate
      </code>
      
       (default on)
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        IsAttached
      </code>
    </td>
    
    <td>
      true while on a live object; false after removal/destroy
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        HasRanOnStart
      </code>
    </td>
    
    <td>
      has <code>
        OnStart
      </code>
      
       already fired?
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Assets
      </code>
      
      , <code>
        WorldManager
      </code>
      
      , <code>
        RenderBus
      </code>
      
      , <code>
        ColliderBus
      </code>
      
      , <code>
        TimerBus
      </code>
      
      , <code>
        InputBus
      </code>
    </td>
    
    <td>
      manager shorthands
    </td>
  </tr>
</tbody>
</table>

`Enabled = false` also gates collision callbacks and render features for the
components that use those buses.

## Transform

`Transform` is a plain position/rotation/scale in world space.

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

<tbody>
  <tr>
    <td>
      <code>
        Position : Vector2
      </code>
    </td>
    
    <td>
      world position
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Rotation : float
      </code>
    </td>
    
    <td>
      radians
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Scale : Vector2
      </code>
    </td>
    
    <td>
      per-axis scale; negative values mirror (see <a href="https://docs.ctrl-neo.dev/4.rendering#negative-scales">
        Rendering
      </a>
      
      )
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Up
      </code>
      
       / <code>
        Down
      </code>
      
       / <code>
        Right
      </code>
      
       / <code>
        Left
      </code>
    </td>
    
    <td>
      unit vectors rotated by <code>
        Rotation
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        Move(Vector2 direction, GameTime)
      </code>
    </td>
    
    <td>
      <code>
        Position += direction * dt
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        FaceTowards(Direction2D side, Vector2/Transform/NeoObject target)
      </code>
    </td>
    
    <td>
      rotate so the given side points at the target; no-op at distance zero
    </td>
  </tr>
</tbody>
</table>

`NeoObject.Transform` setter copies `Position`/`Rotation`/`Scale` field by
field.

## Queries

`NeoWorld` find functions (all linear scans):

- `FindObjectByName(name)` — exact case-sensitive, first match; `FindObjectsByName` for all
- `FindObjectByComponent<T>()` / `FindObjectsByComponent<T>()`
- `FindObjectByHashCode(int)`
- `FindComponent<T>()` — shorthand for `FindObjectByComponent<T>()?.GetComponent<T>()`
- `FindComponents<T>()`
- each has a `Try...` variant that returns `false`/`null` instead of `null`

`NeoObject` also has `HasComponent`, `GetComponent`, `TryGetComponent`.

## Iterating yourself: PassSnapshot

Every loop in the library iterates a rented copy of a list so that mutation
during iteration is safe. When you write your own loops over objects or
components, do the same:

```csharp
using PassSnapshot<NeoObject> objects = new(world.Objects);
for (int i = 0; i < objects.Count; i++)
{
    NeoObject obj = objects[i];
    if (obj.IsDestroyed) continue;   // destroyed earlier this pass
    // ... can spawn or destroy anything here
}
```

Rules:

- The `using` is mandatory — it returns the rented array to the pool.
- Items are **not removed** from the snapshot when destroyed; check
`IsDestroyed` (objects) / `IsAttached` (components) each iteration.
- The snapshot is a one-frame copy; changes hit the real list immediately and
the snapshot next frame.
- Nesting works; each `PassSnapshot` rents its own array.
- Don't cache a snapshot across frames.
- Empty lists still rent a 0-length array; fine.

It lives in `NeoGameLib.Common`.

## A complete example

```csharp
public class MyGame : NeoGame
{
    protected override void OnLoadContent(ContentManager cm)
    {
        Texture2D sheet = cm.Load<Texture2D>("sprites/sheet");

        WorldManager.CreateWorld("level1", w =>
        {
            NeoObject player = w.Instantiate("Player", new Transform(new Vector2(320, 240)));
            player.AddComponent(new NeoSpriteRenderer(RenderBus, sheet));
            player.AddComponent<PlayerController>();
            player.AddComponent(new NeoBoxCollider { Size = new Vector2(32, 32) });
        });

        WorldManager.Load("level1");
    }
}

public class PlayerController : NeoComponent
{
    public override void OnUpdate(GameTime gameTime)
    {
        Vector2 input = Vector2.Zero;
        KeyboardState kb = Keyboard.GetState();
        if (kb.IsKeyDown(Keys.A)) input.X -= 1;
        if (kb.IsKeyDown(Keys.D)) input.X += 1;
        if (kb.IsKeyDown(Keys.W)) input.Y -= 1;
        if (kb.IsKeyDown(Keys.S)) input.Y += 1;

        ParentTransform.Move(input, gameTime);
    }
}
```

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

Raw `Keyboard.GetState()` works fine. For remappable, structured input see
[Input](https://docs.ctrl-neo.dev/6.input) (`NeoInputMap`), and for gravity/jumping/collider
resolution see `NeoMover` in [Systems](https://docs.ctrl-neo.dev/8.systems#neomover).

</callout>


## Sitemap

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