1
Layers
Jaroslav Beneš edited this page 2026-08-08 18:11:58 +02:00

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:

  • Layersingle-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.
  • Tagsmulti-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:

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.

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:

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:

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:

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