Files
Oxide/docs/layers.md
T
Homer Simpson 9eead719b0 Import Oxide engine (Stages 0–10) under MIT license
Full project snapshot migrated to new Gitea remote without history:
engine, editor, physics, script, examples, tests, docs, and assets.
Relicensed from GPLv3 to MIT and updated repo URLs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-05 20:41:02 +02:00

156 lines
6.3 KiB
Markdown

# Layers, Groups & Tags
`oxide_engine::layer` is the engine's shared answer to *"which things interact
with which?"*. Instead of every subsystem inventing its own notion of
collision groups, render masks, or query filters, they all reference one
primitive — the [`LayerMask`] — so a layer named once is honored everywhere.
This system lands in Stage 5 (Engine Core Framework). The consumers below are
wired up in their own stages, but they all build on the types here.
## The model: one Layer, many Groups
Oxide follows the **Unity model**, separating two concepts:
- **[`Layer`]** — *single-valued* membership. Every entity is on exactly **one**
of 32 logical layers (its index `0..32`). This is the fast filter slot used by
rendering, physics, and queries.
- **[`Tags`]** — *multi-valued* gameplay grouping. An entity can be in **any
number** of named groups (`"Enemies"`, `"Interactables"`, `"SaveOnExit"`) that
game code and scripts look up by name.
So: *what kind of thing is this, for fast filtering?* → one **Layer**. *What
gameplay categories does it belong to?* → many **Groups** (tags). The two never
fight over the same slot.
## `LayerMask`: the filter primitive
A `LayerMask` is a 32-slot bitset packed into a `u32`. An entity's `Layer` is a
single index, but the things that *select* entities carry a **mask** — a
camera's visibility, a physics collision filter, a raycast's query mask — so a
filter can target several layers at once. An entity is selected when its layer
is one of the bits in the filter:
```rust
use oxide_engine::layer::{Layer, LayerMask};
let entity = Layer::on(2); // membership: layer 2
let filter = LayerMask::NONE.with(1).with(2); // a filter selecting 1 or 2
assert!(entity.matches(filter)); // layer 2 is in the mask → selected
```
`a.intersects(b)` (equivalently `(a & b) != 0`) is the universal mask
interaction test. Everything else (`with`/`without`/`union`/`intersection`/
`complement`, the `|`/`&`/`^`/`!` operators, `iter`) is convenience around that.
Layer indices run `0..32`. Passing an index `>= 32` panics in every build — a
mistake worth catching loudly rather than wrapping silently.
`Layer` is **node-baked**: `Scene::spawn` auto-attaches `Layer::DEFAULT` (index
0) to every entity, so a fresh entity is visible to broad "see everything"
filters out of the box.
## Naming layers: `LayerRegistry`
Bits are not self-documenting, so a project keeps a [`LayerRegistry`] mapping
indices to names. Index 0 is seeded as `"Default"`; the rest are unnamed until
assigned. The registry is project-level data (serialized with the project in a
later stage), and renaming a layer never moves an entity — only the label
changes.
```rust
use oxide_engine::layer::LayerRegistry;
let mut registry = LayerRegistry::new();
registry.set(1, "Player");
registry.set(2, "NPC");
// Author a filter by name instead of by magic number:
let visible_to_camera = registry.mask_of(["Player", "NPC"]);
assert_eq!(registry.index_of("Player"), Some(1));
```
The editor seeds a small, generally-useful starter set — `Default`, `UI`,
`Player`, `World` — and exposes the registry through its **Layer Names** editor
(opened from the inspector's `Layer` dropdown). Because both a camera's
visibility mask and a raycast's filter can be built from the same names, the
*named layer* is the single source of truth.
## Gameplay grouping: `Tags` + `GroupRegistry`
[`Tags`] is the per-entity, string-keyed set that holds an entity's **group**
membership — the multi-valued counterpart to its single `Layer`:
```rust
use oxide_engine::layer::Tags;
let mut tags = Tags::single("Enemy");
tags.insert("Flying");
assert!(tags.contains("Enemy"));
```
The set of *valid* group names is project-level data in a [`GroupRegistry`], so
the editor offers a fixed vocabulary to pick from (predefined, like layers)
rather than free-typed strings. Defining or deleting a group only changes that
vocabulary — it never touches the tags already on entities:
```rust
use oxide_engine::layer::GroupRegistry;
let mut groups = GroupRegistry::new();
groups.define("Enemies");
groups.define("Interactables");
assert!(groups.contains("Enemies"));
```
In the editor, the inspector's **Groups** dropdown is a multi-select of the
defined groups (each a checkbox toggling membership in the entity's `Tags`), with
an **Edit groups…** entry opening the Groups editor to manage the vocabulary.
Use a **layer** when something must filter quickly and en masse (rendering,
physics, queries). Use a **group** when you need to ask "what gameplay
categories is this in?" and an entity may be in several at once.
## Attaching to entities
`Layer` is auto-attached; `Tags` is an ordinary ECS component — attach it through
the scene's world and query it like anything else:
```rust
use oxide_engine::prelude::*;
use oxide_engine::layer::{LayerMask, Layer, Tags};
let mut scene = Scene::new();
let guard = scene.spawn("guard", Transform::IDENTITY); // Layer::DEFAULT auto-attached
scene.world_mut().insert_one(guard, Layer::on(2)).unwrap(); // move to layer 2
scene.world_mut().insert_one(guard, Tags::single("Enemy")).unwrap();
// Find everything a layer-2 query would hit:
let filter = LayerMask::layer(2);
for (entity, layer) in scene.world().query::<&Layer>().iter() {
if layer.matches(filter) {
// ... this entity is selected
}
}
```
All five types (`LayerMask`, `LayerRegistry`, `Layer`, `Tags`, `GroupRegistry`)
are `serde`-serializable, so layer/group data round-trips through RON and is
dual-editable from the editor and from scripts/AI agents like every other engine
component.
## Who consumes this
| Consumer | Stage | How it uses layers |
|----------|-------|--------------------|
| Render pass pipeline | 5 | A camera holds a visibility `LayerMask`; only entities whose `Layer` is in it are drawn |
| Physics | 9 | A collider's membership + filter masks drive collision groups and sensor/trigger filtering |
| Scene queries | 9 | A raycast/shape-cast carries a filter mask tested against candidates' layer |
| Editor | 6+ | Single-select `Layer` dropdown + Layer Names editor; multi-select `Groups` dropdown + Groups editor |
[`LayerMask`]: ../engine/src/layer/mask.rs
[`LayerRegistry`]: ../engine/src/layer/registry.rs
[`Layer`]: ../engine/src/layer/components.rs
[`Tags`]: ../engine/src/layer/components.rs
[`GroupRegistry`]: ../engine/src/layer/groups.rs