Bevy Forge 0.1.0: visual scene editor for Bevy 0.20 BSN
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
# Bevy Forge architecture
|
||||
|
||||
This page describes how the Bevy Forge workspace is put together: the crates, the scene model, the
|
||||
editor's panel/action model, persistence, code generation and the testing strategy. It documents the
|
||||
architecture the code is being built against; individual modules may still be in progress in a given
|
||||
checkout.
|
||||
|
||||
- Product overview and quick start: [README](../README.md)
|
||||
- Scene file reference: [Scene format](format.md)
|
||||
- Export templates: [BSN export](bsn-export.md)
|
||||
|
||||
## Design constraints
|
||||
|
||||
1. **No Bevy dependency in the editor.** `bevy_forge_core` and `bevy_forge_editor` depend only on
|
||||
serde/RON/IndexMap/thiserror/toml in core and eframe/egui/image in the editor. The only crate that
|
||||
links Bevy is the excluded `compat/bsn-smoke` verification crate. This keeps editor builds fast
|
||||
and independent of Bevy's release cadence.
|
||||
2. **Export is source code.** A `.forge.ron` scene stores editor data only. BSN or imperative Rust
|
||||
is generated on demand and never stored in the scene file. Code generation is deterministic for a
|
||||
given document and options.
|
||||
3. **One scene format.** `.forge.ron` (RON via serde) is the only scene extension. It is Forge's own
|
||||
format, not Bevy's `.scn.ron` dynamic-scene format.
|
||||
4. **Snapshot undo.** The document is small enough that full snapshots are simpler and safer than
|
||||
inverse operations. Every undoable mutation goes through one session API.
|
||||
5. **Immediate-mode UI with deferred actions.** Panels render and return `Action` values. Only the
|
||||
app shell performs file I/O, spawning, saving and exporting.
|
||||
|
||||
## Workspace at a glance
|
||||
|
||||
```text
|
||||
+--------------------------------+
|
||||
| bevy_forge_core |
|
||||
| model - project - validation |
|
||||
| history - assets - codegen |
|
||||
+---------------+----------------+
|
||||
^
|
||||
depends on | depends on
|
||||
+--------------------+--------------------+
|
||||
| |
|
||||
+------------+-------------+ +------------+-------------+
|
||||
| bevy_forge_editor | | bevy_forge_cli |
|
||||
| eframe/egui application | | clap binary `forge` |
|
||||
| binary: `bevy-forge` | | |
|
||||
+--------------------------+ +--------------------------+
|
||||
|
||||
compat/bsn-smoke (excluded from the workspace, depends on Bevy 0.20.0)
|
||||
compiles representative generated BSN to prove the export is valid Rust.
|
||||
```
|
||||
|
||||
| Crate / directory | Role | Binary / artifact |
|
||||
|---|---|---|
|
||||
| `crates/bevy_forge_core` | Document model, structural ops, project and scene persistence, undo, asset index, validation, code generation | library |
|
||||
| `crates/bevy_forge_editor` | Native editor (eframe/egui): panels, viewports, dialogs, palette, toasts | `bevy-forge` |
|
||||
| `crates/bevy_forge_cli` | `forge new`, `validate`, `export`, `info`, `scenes` | `forge` |
|
||||
| `examples/demo-project` | Real Bevy 0.20 project used as the first-run demo and screenshot subject | not built by the workspace |
|
||||
| `compat/bsn-smoke` | Compiles generated BSN against real Bevy 0.20.0; excluded from the workspace | standalone crate |
|
||||
| `scripts/` | End-to-end generated-code check | shell script |
|
||||
|
||||
The root `Cargo.toml` declares the three workspace members and excludes `compat/bsn-smoke` so Bevy
|
||||
is never built as part of the normal workspace.
|
||||
|
||||
## Core model
|
||||
|
||||
All types below are public and frozen for this release.
|
||||
|
||||
| Type | Module | Meaning |
|
||||
|---|---|---|
|
||||
| `EntityId` (`u64`) | `scene` | Stable entity identity inside one scene file |
|
||||
| `SceneDoc` | `scene` | `format_version`, `name`, ordered `entities`, ordered `roots`, `next_id` |
|
||||
| `EntityNode` | `scene` | `id`, `name`, `parent`, ordered `children`, mandatory `transform`, `components`, editor-only `locked` |
|
||||
| `Transform` | `math` | `translation: Vec3`, `rotation: Vec3` (XYZ Euler **degrees**), `scale: Vec3` |
|
||||
| `ComponentData` | `components` | Tagged enum of built-in components plus `Custom(CustomComponent)` |
|
||||
| `Session` | `history` | Document + undo/redo history + dirty flag |
|
||||
| `History` / `Snapshot` | `history` | Bounded stack of full-document snapshots (default limit 100) |
|
||||
| `Project` | `project` | Root, package name, Bevy version, assets dir; scene load/save/list |
|
||||
| `AssetIndex` / `AssetEntry` / `AssetKind` | `assets` | Recursive index of the project's asset files |
|
||||
| `ValidationReport` / `Issue` / `Severity` | `validation` | Deterministic validation output |
|
||||
| `ExportFormat` / `CodegenOptions` | `codegen` | Export dispatch and options (function name, header) |
|
||||
| `EditorConfig` | `config` | Recent projects and editor preferences |
|
||||
|
||||
### Scene identity and ordering
|
||||
|
||||
- IDs are `u64`, start at 1 and are never reused within a scene; `next_id` is monotonic.
|
||||
- Each entity has exactly one parent (`None` for roots). Cycles are rejected by `reparent`.
|
||||
- `roots` and every `children` vector define display order and are preserved by save/load.
|
||||
- `SceneDoc::entities` is an `IndexMap`; insertion order is stable for serialization.
|
||||
- `EntityNode.id` must equal its map key; `SceneDoc::check_integrity()` reports mismatches, dangling
|
||||
parents/children, duplicate references and wrong root/child links.
|
||||
- Every structural edit must leave `check_integrity() == []`.
|
||||
- `locked` is editor-only metadata: it blocks viewport transforms, not deletion or hierarchy edits.
|
||||
|
||||
### Transform semantics
|
||||
|
||||
`Transform.rotation` stores XYZ Euler angles in degrees because that is what the inspector edits.
|
||||
Hierarchy composition converts degrees to a quaternion (`quat_from_euler_deg`), composes parent and
|
||||
child, and converts back to Euler degrees. Export converts the other way with
|
||||
`Quat::from_euler(EulerRot::XYZ, x.to_radians(), y.to_radians(), z.to_radians())`. There is no
|
||||
ambiguity between "editor degrees" and "Bevy radians" because conversion only happens at the
|
||||
composition and code-generation boundaries.
|
||||
|
||||
`Name` and `Transform` are node fields in `.forge.ron`, not entries in `components`; the BSN and Rust
|
||||
generators always emit them for every entity.
|
||||
|
||||
### Components
|
||||
|
||||
`ComponentData` is a serde-tagged enum:
|
||||
|
||||
- `Sprite(SpriteData)`, `Text2d(Text2dData)`, `Camera2d`, `Camera3d`,
|
||||
`PointLight(PointLightData)`, `DirectionalLight(DirectionalLightData)`, `Mesh3d(Mesh3dData)`,
|
||||
`Visibility(VisibilityMode)`, `Custom(CustomComponent)`.
|
||||
- `ComponentData::key()` is the uniqueness key: `"sprite"`, `"text2d"`, `"camera2d"`, `"camera3d"`,
|
||||
`"point_light"`, `"directional_light"`, `"mesh3d"`, `"visibility"`, and
|
||||
`"custom:<type_path>"`. `add_component` refuses duplicate keys; custom components are unique per
|
||||
type path, so two different custom types can coexist.
|
||||
- `Mesh3dData` holds `MeshSource` (`Primitive(PrimitiveShape)` or `Asset(String)`) and
|
||||
`MaterialData`. Primitives are cuboid, sphere, plane, cylinder and capsule.
|
||||
- `CustomComponent` stores a Rust type path plus a list of `(name, expr)` fields. Expressions are
|
||||
raw Rust/BSN text, validated only for identifier shape and non-emptiness.
|
||||
|
||||
## Undo/redo
|
||||
|
||||
`Session` wraps the document and owns the history:
|
||||
|
||||
```rust
|
||||
// One undo entry, recorded only if the document actually changed.
|
||||
session.edit("Rename entity", |doc| {
|
||||
doc.rename(id, "Player");
|
||||
});
|
||||
|
||||
// Continuous interactions (gizmo drags): snapshot before, mutate every frame,
|
||||
// commit once on release.
|
||||
let before = session.doc().clone();
|
||||
// ... update transforms during the drag ...
|
||||
session.commit(before, "Move entities");
|
||||
```
|
||||
|
||||
- `edit` clones the document before running the closure and records a snapshot only when the result
|
||||
differs.
|
||||
- `commit` accepts a caller-supplied pre-edit snapshot and records it once at the end of a drag.
|
||||
- `doc_mut` is the escape hatch for read-modify-write UI code: it marks the session dirty but adds
|
||||
no undo point, so it must not be used for user-visible edits.
|
||||
- `undo`/`redo` swap snapshots and return the action label; the app puts that label in the status
|
||||
bar.
|
||||
- `mark_saved` clears the dirty flag, `replace` swaps in a new document and clears history (used
|
||||
when opening or creating a scene), and `is_dirty` drives the title dot, autosave and quit guard.
|
||||
- A new edit clears the redo stack. The default history limit is 100 snapshots.
|
||||
|
||||
## Persistence
|
||||
|
||||
### Opening a project
|
||||
|
||||
`Project::open(root)` requires an existing directory with a `Cargo.toml` containing a `bevy`
|
||||
dependency (normal, workspace or dev dependencies; a string or a `{ version = ... }` table). It
|
||||
canonicalizes the root, reads the package name, and records the Bevy version string. The assets
|
||||
directory is `<root>/assets` when it exists, otherwise `<root>` (so projects without a dedicated
|
||||
assets folder still work).
|
||||
|
||||
Scenes live in `<root>/scenes/*.forge.ron`. `Project::load_scene` parses RON and normalizes a legacy
|
||||
`format_version: 0` to the current version in memory. `Project::save_scene` accepts an optional
|
||||
path (defaulting to a slugified scene name), refuses paths outside the project root, creates parent
|
||||
directories, serializes with the pretty RON printer and writes atomically: a temporary file
|
||||
`.<name>.tmp-<pid>` in the target directory, then `rename` over the destination.
|
||||
|
||||
### Autosave and configuration
|
||||
|
||||
When autosave is enabled (`autosave_secs > 0`, default 60), the app writes a dirty document to the
|
||||
project's `.forge/autosave.forge.ron` no more often than that interval. Autosave never marks the
|
||||
session saved and never replaces an explicit save; `.forge/` is ignored by version control.
|
||||
|
||||
Editor preferences (recent projects, last project, grid/snap/show-assets flags, autosave interval)
|
||||
are stored as RON in the platform config directory, honouring `XDG_CONFIG_HOME` on Unix:
|
||||
`<config>/bevy-forge/config.ron`.
|
||||
|
||||
## Assets and validation
|
||||
|
||||
`AssetIndex::scan(assets_dir)` recursively indexes regular files under the assets directory, skipping
|
||||
hidden entries, symlinks, and `target` / `build` / `node_modules` directories, with a depth cap.
|
||||
Paths are `/`-separated and relative to the assets directory and are sorted for determinism. Files
|
||||
are classified by extension into `Image`, `Mesh`, `Audio`, `Font`, `Scene`, `Shader`, `Material` and
|
||||
`Other`. A missing or unreadable assets directory yields an empty index with an `error` string; the
|
||||
editor shows that state instead of failing.
|
||||
|
||||
`validate_scene(doc, assets: Option<&AssetIndex>) -> ValidationReport` runs structural checks,
|
||||
range checks and (only when an index is supplied) asset existence/kind checks. Severities order as
|
||||
`Info < Warning < Error`; the report offers `is_ok`, `errors`, `warnings`, `count`, `by_entity` and
|
||||
`sorted`. Validation must never panic on malformed forests and is deterministic (severity
|
||||
descending, then entity, then message). See [the format reference](format.md#validation) for the
|
||||
rule list.
|
||||
|
||||
## Code generation
|
||||
|
||||
```rust
|
||||
pub enum ExportFormat { Bsn, Rust }
|
||||
|
||||
pub struct CodegenOptions {
|
||||
pub function_name: String, // a Rust identifier that is not a keyword
|
||||
pub include_header: bool,
|
||||
}
|
||||
```
|
||||
|
||||
The dispatch method is `ExportFormat::generate(self, doc: &SceneDoc, options: &CodegenOptions) ->
|
||||
Result<String, CodegenError>`, implemented in `codegen.rs` and forwarded to `codegen::bsn` or
|
||||
`codegen::rust`.
|
||||
|
||||
- `codegen.rs` owns the shared options and dispatch; `codegen/bsn.rs` emits Bevy 0.20 BSN and
|
||||
`codegen/rust.rs` emits imperative `Commands::spawn` code.
|
||||
- Both generators walk roots and children in document order and emit `Name` and `Transform` for every
|
||||
entity, followed by components in stored order.
|
||||
- Generation is pure: no file access, no globals, no timestamps. The same document and options
|
||||
produce identical text.
|
||||
- Errors are explicit: `EmptyScene`, `InvalidIdentifier`, `NonFinite` (transform, material or light
|
||||
value) and `InvalidCustomComponent`. Invalid scenes must not panic the generator.
|
||||
- The BSN generator conditionally adds asset parameters (`meshes`, `materials`) to the function
|
||||
signature, based on what the document uses, and chooses `bsn!` (single root, `impl Scene`) or
|
||||
`bsn_list!` (multiple roots, `impl SceneList`).
|
||||
- The CLI and editor are thin wrappers around `generate`: the CLI prints to stdout or writes `-o`;
|
||||
the editor shows a preview, can copy to the clipboard and can save to a file.
|
||||
|
||||
## Editor shell
|
||||
|
||||
`ForgeApp` owns all long-lived state: `Session`, `UiState`, `Project`, `AssetIndex`, `TextureCache`,
|
||||
`Toasts`, `EditorConfig`, `ExportState`, every panel state and the current scene path. Panels never
|
||||
own the document.
|
||||
|
||||
### Frame order
|
||||
|
||||
Each frame, `ForgeApp` builds a borrowed `EditorContext` and runs, in order:
|
||||
|
||||
1. `menu::show` - top menu bar.
|
||||
2. `shortcuts::handle` - global keyboard shortcuts.
|
||||
3. `status_bar::show` - counts, selection, dirty state, mode, cursor position.
|
||||
4. `assets_panel::show` - asset browser (side panel).
|
||||
5. `hierarchy::show` / `inspector::show` - left/right side panels when visible.
|
||||
6. The central panel: `viewport2d::show` or `viewport3d::show` according to the view mode.
|
||||
7. `palette::show`, then `dialogs::show` - overlays.
|
||||
8. The collected `Vec<Action>` is dispatched by the app; autosave runs afterwards.
|
||||
|
||||
Menus, shortcuts, panels and dialogs all return actions instead of performing operations.
|
||||
|
||||
### Context and panels
|
||||
|
||||
```rust
|
||||
pub struct EditorContext<'a> {
|
||||
pub session: &'a mut Session,
|
||||
pub project: Option<&'a Project>,
|
||||
pub assets: &'a AssetIndex,
|
||||
pub textures: &'a mut TextureCache,
|
||||
pub ui: &'a mut UiState,
|
||||
pub toasts: &'a mut Toasts,
|
||||
pub export: &'a mut ExportState,
|
||||
}
|
||||
```
|
||||
|
||||
```text
|
||||
// Frozen panel entry points (types abridged; see the module sources).
|
||||
hierarchy::show(ctx: &egui::Context, cx: &mut EditorContext<'_>, state: &mut HierarchyPanelState) -> Vec<Action>
|
||||
inspector::show(ctx: &egui::Context, cx: &mut EditorContext<'_>, state: &mut InspectorPanelState) -> Vec<Action>
|
||||
assets_panel::show(ctx: &egui::Context, cx: &mut EditorContext<'_>, state: &mut AssetsPanelState) -> Vec<Action>
|
||||
viewport2d::show(ui: &mut egui::Ui, cx: &mut EditorContext<'_>, state: &mut Viewport2dState) -> Vec<Action>
|
||||
viewport3d::show(ui: &mut egui::Ui, cx: &mut EditorContext<'_>, state: &mut Viewport3dState) -> Vec<Action>
|
||||
menu::show(ctx: &egui::Context, cx: &EditorContext<'_>) -> Vec<Action>
|
||||
palette::show(ctx: &egui::Context, cx: &mut EditorContext<'_>, state: &mut PaletteState) -> Vec<Action>
|
||||
dialogs::show(ctx: &egui::Context, cx: &mut EditorContext<'_>, state: &mut DialogsState) -> Vec<Action>
|
||||
welcome::show(ctx: &egui::Context, state: &mut WelcomeState, config: &EditorConfig) -> Vec<Action>
|
||||
shortcuts::handle(ctx: &egui::Context, session: &Session) -> Vec<Action>
|
||||
status_bar::show(ctx: &egui::Context, cx: &EditorContext<'_>)
|
||||
```
|
||||
|
||||
Rules that keep panels independent and testable:
|
||||
|
||||
- Selection changes are immediate (`cx.ui.selection.select_only/add/toggle/...`); app-level
|
||||
operations become actions.
|
||||
- Scene mutations go through `cx.session.edit(...)` (or snapshot + `commit` for drags).
|
||||
- Panels never save files, spawn processes, open dialogs themselves or change the active screen.
|
||||
- Each panel owns a `Default`-constructible state struct stored by `ForgeApp`.
|
||||
- Input focus is not stolen except by the command palette and dialogs.
|
||||
- User feedback uses `cx.ui.status` for routine messages and `cx.toasts` for successes/errors.
|
||||
|
||||
### Actions
|
||||
|
||||
`Action` (in `action.rs`) is the vocabulary between UI and app. It covers scene lifecycle (new,
|
||||
open, save, save-as), export (BSN/Rust, copy, write), editing (undo, redo, duplicate, delete,
|
||||
focus), view state (2D/3D, grid, snap, panels, fit), and shell operations (palette, shortcuts,
|
||||
about, quit, reload assets). `ForgeApp::handle_actions` is the only place that executes them.
|
||||
Adding an action is a shared-contract change: the variant must be added to `action.rs` and handled
|
||||
in `app.rs`, never handled inside a panel.
|
||||
|
||||
### UI state
|
||||
|
||||
`UiState` holds multi-selection (ordered set plus a primary entity), view mode, panel visibility,
|
||||
grid/snap settings, per-panel search strings and asset filter, cursor world position, viewport
|
||||
rectangle, status text, error banner and the one-frame `fit_view_requested` flag. Panel-specific
|
||||
state (zoom, pan, drag origins, inline-rename buffer, filters, dialog fields) stays in that panel's
|
||||
own state struct.
|
||||
|
||||
## Error handling and robustness
|
||||
|
||||
- Core errors are `thiserror` enums (`SceneError`, `ProjectError`, `CodegenError`, `ConfigError`).
|
||||
User input, file I/O and RON parsing never use `unwrap()`/`expect()`.
|
||||
- Editor failures are surfaced as toasts or an error banner and leave the previous state usable;
|
||||
opening a malformed scene must not panic the app.
|
||||
- CLI user errors are printed to stderr with a non-zero exit code; no panics.
|
||||
- Asset scanning and texture loading are failure-safe: missing files produce empty/placeholder
|
||||
states, not crashes.
|
||||
- Structural operations and validation bound parent-chain walks so malformed forests cannot hang
|
||||
the UI.
|
||||
|
||||
## Testing strategy
|
||||
|
||||
The project separates pure logic from UI and from real-Bevy compilation so each layer can fail
|
||||
independently:
|
||||
|
||||
| Layer | What it proves | Mechanism |
|
||||
|---|---|---|
|
||||
| Core unit tests | Model, hierarchy, undo, project, config, assets, validation, both generators | `#[cfg(test)]` modules and `crates/bevy_forge_core/tests/` |
|
||||
| CLI tests | Argument parsing, exit codes, file output, JSON shape | `std::process::Command` with `env!("CARGO_BIN_EXE_forge")` and `tempfile` project fixtures |
|
||||
| Editor tests | Panels render and emit the expected actions without a window | Headless `egui::Context` frames driving `ForgeApp::new_for_test` / `ForgeApp::ui` and panel functions |
|
||||
| Generated-code gate | Exported BSN is valid Rust against Bevy 0.20.0 | `compat/bsn-smoke` (`cargo check`) plus `scripts/check-generated-code.sh` |
|
||||
| Manual/visual | The actual desktop app is usable and looks intentional | Launch with the demo project and capture `docs/screenshot.png` |
|
||||
|
||||
Representative commands (run inside a checkout):
|
||||
|
||||
```bash
|
||||
cargo test -p bevy_forge_core
|
||||
cargo test -p bevy_forge_cli
|
||||
cargo test -p bevy_forge_editor
|
||||
cargo check --workspace
|
||||
cd compat/bsn-smoke && cargo check
|
||||
```
|
||||
|
||||
Snapshot assumptions are asserted directly: tests create documents through the public structural API
|
||||
and check `check_integrity()`, round-trip scenes through RON, compare undo/redo states, and compare
|
||||
generated text against expected templates. The generated-code gate is the only test that links Bevy,
|
||||
which is why it lives in an excluded crate.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- No live in-game editor plugin, remote protocol or hot reload in 0.1.
|
||||
- No Bevy dependency in the editor or core crates (and therefore no Bevy types in the scene model).
|
||||
- No runtime `.bsn` asset format; BSN is generated Rust source.
|
||||
- No animation editor, `bevy_ui` editor, material graph, terrain tools, collaboration or asset import
|
||||
pipeline.
|
||||
- No GPU-rendered viewport; the 3D view is an egui-painted preview.
|
||||
- No support for Bevy versions other than 0.20 BSN, and no legacy `.scn.ron` dynamic scenes.
|
||||
- No native file dialogs.
|
||||
|
||||
## Extension points
|
||||
|
||||
- **New built-in component.** Add a `ComponentData` variant plus its data struct, a `ComponentKind`
|
||||
entry and `key`, default construction, asset references, validation rules, both generator arms,
|
||||
the inspector's add/remove UI and tests. This is a contract change because `ComponentData` is
|
||||
frozen.
|
||||
- **New primitive shape.** Extend `PrimitiveShape` and `half_extents`, emit the verified Bevy
|
||||
constructor in the BSN/Rust generators, and add inspector fields. Verify the constructor against
|
||||
Bevy 0.20 before documenting it.
|
||||
- **New export format.** Add an `ExportFormat` variant, a generator module and dispatch arm, a CLI
|
||||
value, an editor menu entry and compile/test coverage.
|
||||
- **New panel or action.** Panels follow the `show(...) -> Vec<Action>` pattern and keep their state
|
||||
in a `Default` struct owned by `ForgeApp`. New actions require editing `action.rs` and handling
|
||||
them in `app.rs`; panels must not acquire file I/O.
|
||||
- **New CLI command.** Add a clap subcommand in `crates/bevy_forge_cli`, reuse core APIs, define exit
|
||||
codes, and add process tests.
|
||||
- **Project-specific components.** Prefer the custom-component escape hatch over adding built-ins:
|
||||
store a type path and raw field expressions, and document the required derives. This keeps the
|
||||
editor independent of game code.
|
||||
- **Asset kinds.** Extend `AssetKind::from_path` and the asset filter when a new file type needs
|
||||
first-class browsing.
|
||||
|
||||
Public signatures are frozen for this release. Changing one requires updating the contract and all
|
||||
call sites rather than editing a panel in isolation.
|
||||
|
||||
## Further reading
|
||||
|
||||
- [Scene format](format.md) - the `.forge.ron` reference.
|
||||
- [BSN export](bsn-export.md) - templates, derive requirements, Bevy integration.
|
||||
- [README](../README.md) - quick start, CLI, shortcuts, limitations.
|
||||
@@ -0,0 +1,486 @@
|
||||
# BSN export
|
||||
|
||||
Bevy Forge's primary export target is **Bevy 0.20 BSN**: Rust source code written with the `bsn!`
|
||||
and `bsn_list!` macros from `bevy::scene`. This is the modern scene notation in Bevy 0.20 - not the
|
||||
legacy `DynamicScene` / `.scn.ron` format. The output is ordinary Rust that your game compiles; there
|
||||
is no runtime `.bsn` asset to load.
|
||||
|
||||
Related pages: [scene format](format.md), [architecture](architecture.md), [README](../README.md).
|
||||
|
||||
## Where the code comes from
|
||||
|
||||
- **Editor:** File > Export > BSN scene. The dialog shows a preview with the generated function name,
|
||||
lets you copy it to the clipboard, and can write it to a file (default `export/<function>.rs`).
|
||||
- **CLI:**
|
||||
|
||||
```bash
|
||||
cargo run -p bevy_forge_cli -- export examples/demo-project/scenes/garden.forge.ron \
|
||||
--format bsn -o /tmp/garden.rs
|
||||
```
|
||||
|
||||
Without `-o`, the code is printed to stdout. `--function <name>` sets the generated function name;
|
||||
the default is `scene`. The name must be a valid Rust identifier that is not a Rust keyword,
|
||||
otherwise the exporter returns an error.
|
||||
|
||||
Generated files begin with `use bevy::prelude::*;` (plus a macro import when needed). With
|
||||
`CodegenOptions::include_header` enabled - the editor's default - a short "generated by Bevy Forge"
|
||||
comment precedes the imports; the CLI exports without that header, so its stdout and `-o` files start
|
||||
directly with the import. Generated files end with exactly one newline. Generation is deterministic
|
||||
for a given scene and options.
|
||||
|
||||
## Output shape
|
||||
|
||||
One root scene:
|
||||
|
||||
```rust
|
||||
// Generated by Bevy Forge. Paste into a Bevy 0.20 project.
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn scene() -> impl Scene {
|
||||
bsn! {
|
||||
Name("Player")
|
||||
Transform {
|
||||
translation: Vec3::new(0.0, 0.0, 0.0),
|
||||
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
}
|
||||
Sprite { image: "textures/player.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(64.0, 64.0)) }, flip_x: false, flip_y: false }
|
||||
Children [
|
||||
Name("Hat")
|
||||
Transform {
|
||||
translation: Vec3::new(0.0, 1.0, 0.0),
|
||||
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
}
|
||||
Sprite { color: Color::srgba(1.0, 0.5, 0.2, 1.0), flip_x: false, flip_y: false }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Multiple roots use `bsn_list!` and return `impl SceneList`. Roots (and children) are separated by
|
||||
`--`:
|
||||
|
||||
```rust
|
||||
// Generated by Bevy Forge. Paste into a Bevy 0.20 project.
|
||||
use bevy::prelude::*;
|
||||
use bevy::scene::bsn_list;
|
||||
|
||||
pub fn scene() -> impl SceneList {
|
||||
bsn_list! {
|
||||
Name("Left")
|
||||
Transform {
|
||||
translation: Vec3::new(-2.0, 0.0, 0.0),
|
||||
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
}
|
||||
Sprite { color: Color::srgba(0.4, 0.7, 1.0, 1.0), flip_x: false, flip_y: false }
|
||||
--
|
||||
Name("Right")
|
||||
Transform {
|
||||
translation: Vec3::new(2.0, 0.0, 0.0),
|
||||
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
}
|
||||
Sprite { color: Color::srgba(1.0, 0.6, 0.4, 1.0), flip_x: false, flip_y: false }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rules the generator follows:
|
||||
|
||||
- `bsn!` accepts exactly one root. Two roots inside one `bsn!` are a parse error; use `bsn_list!`.
|
||||
- `--` separates sibling scenes inside `bsn_list!` and `Children [ ... ]`. The editor's scene order is
|
||||
preserved.
|
||||
- Every entity emits `Name("<escaped display name>")` and a complete `Transform` block with all
|
||||
three fields. Display names are emitted as escaped Rust strings, never as `#Identifier` references.
|
||||
- Rotation is converted from stored XYZ Euler **degrees** to a quaternion:
|
||||
`Quat::from_euler(EulerRot::XYZ, 45.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians())`.
|
||||
The `f32` suffix is part of the generated output; it keeps the literal type unambiguous.
|
||||
- Components are emitted in stored order after `Transform`; children appear as a nested `Children`
|
||||
block.
|
||||
- Macro imports are normal `use` statements. Do not fully qualify the macros
|
||||
(`bevy::scene::bsn_list! { ... }` compiles but prints a build-time macro-root warning).
|
||||
|
||||
## Component templates
|
||||
|
||||
### Name
|
||||
|
||||
```rust
|
||||
Name("Player")
|
||||
```
|
||||
|
||||
The string is Rust-escaped. `Name` takes a string-like value; `Name(42)` does not compile.
|
||||
|
||||
### Transform
|
||||
|
||||
```rust
|
||||
Transform {
|
||||
translation: Vec3::new(1.0, 0.5, 0.0),
|
||||
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 45.0f32.to_radians(), 0.0f32.to_radians()),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
}
|
||||
```
|
||||
|
||||
All three fields are always present. (Here `45.0f32.to_radians()` is 45 degrees.) BSN component patches
|
||||
do not support struct-update syntax: `Transform { ..default() }` fails with a type inference error,
|
||||
so the generator writes every field.
|
||||
|
||||
### Sprite
|
||||
|
||||
```rust
|
||||
Sprite { image: "textures/player.png", color: Color::srgba(1.0, 1.0, 1.0, 1.0), custom_size: { Some(Vec2::new(64.0, 32.0)) }, flip_x: false, flip_y: false }
|
||||
```
|
||||
|
||||
- `image` is omitted when the sprite has no image; `custom_size` is omitted when it is `None`.
|
||||
- `color` is always emitted.
|
||||
- `Some(...)` **must be braced** inside a BSN patch; bare `Some(..)` is a parse error. Bare `None`
|
||||
is accepted, but the generator simply omits optional fields instead.
|
||||
- For a solid-colour sprite the output is `Sprite { color: ..., flip_x: false, flip_y: false }`.
|
||||
|
||||
### Text2d
|
||||
|
||||
```rust
|
||||
Text2d("Hello, Bevy!")
|
||||
TextFont { font_size: FontSize::Px(32.0) }
|
||||
TextColor(Color::srgba(1.0, 1.0, 1.0, 1.0))
|
||||
```
|
||||
|
||||
Text is a Rust string literal; newlines and quotes are escaped normally. `font_size` is in pixels.
|
||||
|
||||
### Cameras
|
||||
|
||||
```rust
|
||||
Camera2d
|
||||
```
|
||||
|
||||
```rust
|
||||
Camera3d::default()
|
||||
```
|
||||
|
||||
`Camera2d` is emitted bare. `Camera3d` is a constructor call, not a bare identifier.
|
||||
|
||||
### Point light
|
||||
|
||||
```rust
|
||||
PointLight { color: Color::srgba(1.0, 0.8, 0.6, 1.0), intensity: 800.0, range: 20.0, shadow_maps_enabled: true }
|
||||
```
|
||||
|
||||
### Directional light
|
||||
|
||||
```rust
|
||||
DirectionalLight { color: Color::srgba(1.0, 0.95, 0.85, 1.0), illuminance: 10000.0, shadow_maps_enabled: true }
|
||||
```
|
||||
|
||||
**Negative literal caveat:** BSN patches do not accept a leading `-` in a direct field value. A
|
||||
negative number must be wrapped in a block and suffixed:
|
||||
`intensity: { -5.0f32 }`. Values produced by calls or paths (`Vec3::new(-1.0, 0.0, 0.0)`,
|
||||
`f32::NEG_INFINITY`) are fine. The generator uses the braced form when a direct literal is negative.
|
||||
|
||||
### Visibility
|
||||
|
||||
```rust
|
||||
Visibility::Inherited
|
||||
```
|
||||
|
||||
```rust
|
||||
Visibility::Visible
|
||||
```
|
||||
|
||||
```rust
|
||||
Visibility::Hidden
|
||||
```
|
||||
|
||||
An entity without a visibility component emits nothing; Bevy treats it as inherited.
|
||||
|
||||
### Mesh3d and materials
|
||||
|
||||
Primitive meshes and materials need asset handles. The generated function takes parameters for the
|
||||
asset collections the scene uses:
|
||||
|
||||
```rust
|
||||
pub fn scene(
|
||||
meshes: &mut Assets<Mesh>,
|
||||
materials: &mut Assets<StandardMaterial>,
|
||||
) -> impl Scene {
|
||||
bsn! {
|
||||
Name("Ground")
|
||||
Transform {
|
||||
translation: Vec3::new(0.0, 0.0, 0.0),
|
||||
rotation: Quat::from_euler(EulerRot::XYZ, 0.0f32.to_radians(), 0.0f32.to_radians(), 0.0f32.to_radians()),
|
||||
scale: Vec3::new(1.0, 1.0, 1.0),
|
||||
}
|
||||
Mesh3d({ meshes.add(Cuboid::new(12.0, 0.2, 12.0)) })
|
||||
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
|
||||
base_color: Color::srgba(0.35, 0.55, 0.3, 1.0),
|
||||
metallic: 0.0,
|
||||
perceptual_roughness: 0.9,
|
||||
unlit: false,
|
||||
..default()
|
||||
}) })
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- Primitive mesh handles must be **braced** because method calls are not accepted as direct BSN
|
||||
values: `Mesh3d({ meshes.add(..) })`, not `Mesh3d(meshes.add(..))`.
|
||||
- `MeshMaterial3d` requires the turbofish `::<StandardMaterial>`; without it the compiler reports
|
||||
missing generics.
|
||||
- `..default()` is accepted inside the raw Rust `StandardMaterial` literal (it is a constructor
|
||||
argument), but **not** inside a BSN component patch.
|
||||
- The generator includes `meshes` only when a primitive mesh exists, and `materials` whenever a
|
||||
`Mesh3d` exists. A scene with no meshes has no asset parameters.
|
||||
- Primitive constructors (each wrapped in `Mesh3d({ meshes.add(...) })`):
|
||||
- Cuboid: `Cuboid::new(x, y, z)`
|
||||
- Sphere: `Sphere::new(radius)`
|
||||
- Plane: `Plane3d::new(Vec3::Y, Vec2::new(width * 0.5, height * 0.5))` - the scene stores the full
|
||||
size, while Bevy's `Plane3d` takes half extents along X and Z
|
||||
- Cylinder: `Cylinder::new(radius, height)`
|
||||
- Capsule: `Capsule3d::new(radius, length)`
|
||||
|
||||
An asset mesh is addressed by path instead of an added handle:
|
||||
|
||||
```rust
|
||||
Mesh3d("models/robot.gltf#Mesh0/Primitive0")
|
||||
MeshMaterial3d::<StandardMaterial>({ materials.add(StandardMaterial {
|
||||
base_color: Color::srgba(0.8, 0.8, 0.85, 1.0),
|
||||
metallic: 0.1,
|
||||
perceptual_roughness: 0.4,
|
||||
unlit: false,
|
||||
..default()
|
||||
}) })
|
||||
```
|
||||
|
||||
An asset mesh still needs `materials`, but not `meshes`. The path is resolved by Bevy's `AssetServer`
|
||||
at spawn time; see [runtime asset caveats](#runtime-asset-caveats).
|
||||
|
||||
### Custom components
|
||||
|
||||
```rust
|
||||
forge_demo::Orbit {
|
||||
radius: 2.0,
|
||||
speed: { -0.5f32 },
|
||||
}
|
||||
```
|
||||
|
||||
A custom component exports as a struct literal: the stored `type_path`, then each `name: expr` pair.
|
||||
A custom component with zero fields is emitted as the bare type path:
|
||||
|
||||
```rust
|
||||
forge_demo::Marker
|
||||
```
|
||||
|
||||
Field expressions are copied from the scene file. The generator wraps an expression in
|
||||
`{ ... }` when it starts with `-` or is a bare enum-variant call such as `Some(..)`, because neither
|
||||
form is accepted as a direct BSN field value. Duplicate field names are rejected. In the example
|
||||
above the scene expression is `-0.5f32`, so the `f32` suffix survives bracing.
|
||||
|
||||
## Custom component requirements
|
||||
|
||||
The generated code must compile in the game crate that defines the component. Bevy 0.20's BSN macro
|
||||
needs a `FromTemplate` implementation, provided either by the blanket `Default + Clone + Unpin`
|
||||
implementation or by a derived `FromTemplate`.
|
||||
|
||||
| Component shape | Minimum derives |
|
||||
|---|---|
|
||||
| Simple data fields (`f32`, `Color`, `Vec3`, ...) | `#[derive(Component, Default, Clone)]` |
|
||||
| Unit marker | `#[derive(Component, Default, Clone)]` |
|
||||
| Fields like `Handle<Image>` or `Entity` | `#[derive(Component, FromTemplate)]` |
|
||||
| `Option<Handle<T>>` or other `#[template(built_in)]` fields | `#[derive(Component, FromTemplate)]` plus `#[template(built_in)]` on that field |
|
||||
|
||||
Notes:
|
||||
|
||||
- `FromTemplate` is in Bevy's prelude. The derive generates a companion template type, so it can be
|
||||
combined with `Default + Clone` in 0.20.
|
||||
- A component with `Handle<Image>` and only `Default + Clone` fails to compile because
|
||||
`Handle<Image>` does not convert from a string path through the blanket template; use
|
||||
`FromTemplate`.
|
||||
- Add `#[template(built_in)]` in your Rust source where a field type needs it. The editor does not
|
||||
introspect your types, so it cannot infer this option.
|
||||
- The exported `type_path` must be importable where the generated file is compiled. Use a qualified
|
||||
path such as `my_game::Health` if the type is not in scope.
|
||||
- The generated BSN binds no entity references (`#Child`), so custom components that link to other
|
||||
entities should use whatever handle/entity value your scene code provides; a raw `Entity` field
|
||||
expression is the author's responsibility.
|
||||
|
||||
Example component definitions:
|
||||
|
||||
```rust
|
||||
use bevy::prelude::*;
|
||||
|
||||
#[derive(Component, Default, Clone)]
|
||||
pub struct Health {
|
||||
pub current: f32,
|
||||
pub max: f32,
|
||||
}
|
||||
|
||||
#[derive(Component, FromTemplate)]
|
||||
pub struct Icon {
|
||||
pub image: Handle<Image>,
|
||||
pub tint: Color,
|
||||
}
|
||||
```
|
||||
|
||||
## Pasting generated code into a Bevy app
|
||||
|
||||
1. Export the file, for example into `src/scenes/garden.rs` in your game.
|
||||
2. Declare the module and call the generated function from a startup system:
|
||||
|
||||
```rust
|
||||
use bevy::prelude::*;
|
||||
|
||||
mod scenes {
|
||||
pub mod garden;
|
||||
}
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.add_systems(Startup, spawn_garden)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn spawn_garden(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||
) {
|
||||
commands.spawn_scene(scenes::garden::scene(&mut meshes, &mut materials));
|
||||
}
|
||||
```
|
||||
|
||||
For a scene whose generated signature has no asset parameters:
|
||||
|
||||
```rust
|
||||
commands.spawn_scene(scenes::garden::scene());
|
||||
```
|
||||
|
||||
For multiple roots:
|
||||
|
||||
```rust
|
||||
commands.spawn_scene_list(scenes::garden::scene());
|
||||
```
|
||||
|
||||
- `spawn_scene`, `spawn_scene_list` and the related traits are in Bevy's prelude.
|
||||
- Matching the generated parameter list is required: passing unused asset collections is harmless,
|
||||
but omitting a required one does not compile.
|
||||
- On `World` (for example in exclusive systems or tests), use
|
||||
`world.spawn_scene(scene)` or `world.spawn_scene_list(scene)`; `spawn_scene_list` returns a
|
||||
`Result<Vec<Entity>, _>`.
|
||||
- `commands.queue_spawn_scene(..)` (and the list variant) defers the spawn. Handles created by
|
||||
`meshes.add` / `materials.add` are evaluated when `scene()` is called and are owned by the scene,
|
||||
so deferred spawning is safe.
|
||||
- Use `DefaultPlugins` or the specific plugins your components need (`ScenePlugin`, `SpritePlugin`,
|
||||
`PbrPlugin`, ...). A headless test harness needs at least task pool, asset and scene plugins.
|
||||
|
||||
## Runtime asset caveats
|
||||
|
||||
- **Primitives and inline materials work immediately.** `meshes.add(Cuboid::new(..))` and
|
||||
`materials.add(StandardMaterial { .. })` create values when `scene()` runs; the entity renders as
|
||||
soon as it is spawned and the asset is registered.
|
||||
- **Sprite images and asset meshes are path handles.** A string like `"textures/player.png"` becomes
|
||||
a path handled by `AssetServer` at spawn time. If the file is missing, or the relevant asset loader
|
||||
is not enabled in your game (`bevy_gltf` for `.gltf`, for example), the handle silently stays
|
||||
`Loading` or `Failed` - spawning does not error.
|
||||
- **Material asset paths need a loader.** `MeshMaterial3d::<StandardMaterial>("materials/cube.toml")`
|
||||
compiles, but unless your project registers a `StandardMaterial` asset loader it never resolves.
|
||||
This is why Forge emits inline material values instead of material asset paths. Do not replace the
|
||||
generated material expression with a bare path and expect it to render.
|
||||
- **No runtime BSN scene assets.** The generated code is compiled into your crate. Bevy's scene
|
||||
asset/caching features are not used by this exporter.
|
||||
|
||||
## Determinism and export errors
|
||||
|
||||
`ExportFormat::generate` is pure and deterministic. It returns errors instead of panicking:
|
||||
|
||||
| Error | Cause |
|
||||
|---|---|
|
||||
| `EmptyScene` | The scene has no root entities (including an entirely empty scene); there is nothing to generate. |
|
||||
| `InvalidIdentifier` | The function name is not a valid Rust identifier, or is a Rust keyword. |
|
||||
| `NonFinite` | A transform, material or light value is `NaN` or infinite. |
|
||||
| `InvalidCustomComponent` | A custom type path or field name is invalid, or a field expression is empty. The BSN generator additionally rejects duplicate field names. |
|
||||
|
||||
The CLI exits `1` for these codegen errors and `2` for I/O, RON parse or usage errors. The editor
|
||||
shows the error in the export dialog instead of writing a file.
|
||||
|
||||
## Troubleshooting compile errors
|
||||
|
||||
| Compiler message | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `error: Unexpected input after function name` | Method call used as a direct BSN value, e.g. `Mesh3d(meshes.add(..))` | Brace it: `Mesh3d({ meshes.add(..) })` |
|
||||
| `error[E0107]: missing generics for struct MeshMaterial3d` | Missing turbofish | Use `MeshMaterial3d::<StandardMaterial>(...)` |
|
||||
| `error: expected type, found variant 'Some'` | Unbraced `Some(..)` in a patch | Use `custom_size: { Some(Vec2::new(..)) }` |
|
||||
| `error: expected identifier` at a negative number | Negative direct field literal | Use `intensity: { -5.0f32 }` (or a call/path value) |
|
||||
| `error: expected f32, found integer` | Integer literal in an `f32` field | Write `10.0`, not `10` |
|
||||
| `error[E0282]: type annotations needed` | `..default()` or `..Default::default()` inside a BSN component patch | Not supported; write the fields or omit them to keep defaults. `..default()` is only valid inside raw Rust literals in `{}` arguments |
|
||||
| `error: expected HashedStr, found integer` | `Name(42)` | `Name` takes a string: `Name("42")` |
|
||||
| `error: unexpected token` at `--` inside `bsn!` | Multiple roots in a single-root macro | Use `bsn_list!` and return `impl SceneList` |
|
||||
| `cannot find macro bsn_list` | Missing import | Add `use bevy::scene::bsn_list;` (or `use bevy::prelude::*;`) |
|
||||
| `Unknown macro root!` printed to stderr | Fully qualified macro path | Import the macro with `use` instead |
|
||||
| `warning: unused variable: meshes` | Asset parameter present but unused (e.g. hand-edited) | Let the exporter include only needed parameters, or allow the warning |
|
||||
| `warning: float_literal_f32_fallback` | Unsuffixed negative float in a bracing block | Use an `f32` suffix: `{ -5.0f32 }` |
|
||||
| Duplicate sibling entities visually merged | Missing `--` between scenes | Separate sibling patches with `--` |
|
||||
|
||||
If an exported scene fails to compile, first check that the pasted file is in a crate that depends on
|
||||
Bevy 0.20 and that custom component types and their derives are present. Then compare against the
|
||||
templates above. The repository's `compat/bsn-smoke` crate compiles representative generated output
|
||||
against real Bevy 0.20 and is the reference for supported syntax.
|
||||
|
||||
## Imperative Rust alternative
|
||||
|
||||
`forge export --format rust` (or File > Export > Rust spawn code) produces a function that spawns the
|
||||
scene immediately with `Commands`. It targets the same Bevy 0.20 API surface but does not use BSN.
|
||||
|
||||
```rust
|
||||
// Generated by Bevy Forge. Paste into a Bevy 0.20 project.
|
||||
// Imperative `Commands::spawn` code: call this function from a startup system.
|
||||
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn scene(
|
||||
commands: &mut Commands,
|
||||
asset_server: &AssetServer,
|
||||
) {
|
||||
commands.spawn((
|
||||
Name::new("Player"),
|
||||
Transform::from_xyz(0.0, 0.0, 0.0)
|
||||
.with_rotation(Quat::from_euler(EulerRot::XYZ, 0.0_f32.to_radians(), 0.0_f32.to_radians(), 0.0_f32.to_radians()))
|
||||
.with_scale(Vec3::new(1.0, 1.0, 1.0)),
|
||||
Sprite { image: asset_server.load("textures/player.png"), color: Color::srgba(1.0, 1.0, 1.0, 1.0), flip_x: false, flip_y: false, ..default() },
|
||||
)).with_children(|parent| {
|
||||
parent.spawn((
|
||||
Name::new("Hat"),
|
||||
Transform::from_xyz(0.0, 1.0, 0.0)
|
||||
.with_rotation(Quat::from_euler(EulerRot::XYZ, 0.0_f32.to_radians(), 0.0_f32.to_radians(), 0.0_f32.to_radians()))
|
||||
.with_scale(Vec3::new(1.0, 1.0, 1.0)),
|
||||
Sprite { color: Color::srgba(1.0, 0.5, 0.2, 1.0), flip_x: false, flip_y: false, ..default() },
|
||||
));
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
Usage in a system:
|
||||
|
||||
```rust
|
||||
fn spawn_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
scene(&mut commands, &asset_server);
|
||||
}
|
||||
```
|
||||
|
||||
Rules for the imperative generator:
|
||||
|
||||
- `commands` is always a parameter. `asset_server` appears only when a sprite image or asset mesh is
|
||||
used. `meshes` and `materials` are both emitted whenever the scene contains any `Mesh3d`
|
||||
(primitive or asset), because the mesh handle and the inline material are created at the same
|
||||
time.
|
||||
- Each entity spawns a tuple of `Name::new(..)`, a `Transform`, then its components. Omitted `Sprite`
|
||||
fields use `..default()`.
|
||||
- Hierarchy uses `.with_children(|parent| { ... })`; sibling order is preserved.
|
||||
- Primitive meshes and materials use the same constructors as BSN. Text2d uses `Text2d::new`,
|
||||
`TextFont { font_size: FontSize::Px(..), ..default() }` and `TextColor(..)`.
|
||||
- Custom components are emitted as struct literals; the type must be imported where the generated
|
||||
code compiles.
|
||||
- Empty scenes are an error, exactly as for BSN.
|
||||
|
||||
The imperative form is useful when a project cannot use BSN macros or when code must run against an
|
||||
API where a scene is spawned imperatively. BSN remains the primary, recommended export.
|
||||
+569
@@ -0,0 +1,569 @@
|
||||
# Scene format (`.forge.ron`)
|
||||
|
||||
Bevy Forge stores scenes as human-readable [RON](https://github.com/ron-rs/ron) documents with the
|
||||
`.forge.ron` extension. A scene file contains **editor data only** - entities, hierarchy, transforms
|
||||
and components. It never contains generated code and is not Bevy's `.scn.ron` dynamic-scene format.
|
||||
BSN or imperative Rust is generated from a scene on demand; see the [BSN export guide](bsn-export.md).
|
||||
|
||||
This page documents format version **1** as written by the editor and understood by the core crate.
|
||||
Related pages: [architecture](architecture.md), [README](../README.md).
|
||||
|
||||
## Files and naming
|
||||
|
||||
- Scenes live in `<project>/scenes/`. `Project::list_scenes()` lists the `*.forge.ron` files there.
|
||||
- The editor's suggested filename is the scene display name run through `slugify`: ASCII
|
||||
letters/digits are lowercased, whitespace and `-`, `_`, `.` collapse to `-`, and an empty result
|
||||
becomes `scene` (for example `Level 01` -> `level-01.forge.ron`). The name inside the file is
|
||||
separate and can be any UTF-8 string.
|
||||
- The editor refuses to save under any other extension, and `Project::save_scene` refuses paths
|
||||
outside the project root.
|
||||
- Files are UTF-8 RON produced with a two-space pretty printer. Writers use `\n` line endings.
|
||||
RON comments are legal in the format but the editor does not write them.
|
||||
- Whitespace and line breaks are not significant. The editor's writer puts one struct field per
|
||||
line and expands nested vectors/colours the same way; the examples on this page are written more
|
||||
compactly for readability and parse identically.
|
||||
|
||||
## Complete example
|
||||
|
||||
A small 3D scene with two roots, a directional light and a primitive mesh:
|
||||
|
||||
```ron
|
||||
(
|
||||
format_version: 1,
|
||||
name: "Garden",
|
||||
entities: {
|
||||
1: (
|
||||
id: 1,
|
||||
name: "Sun",
|
||||
parent: None,
|
||||
children: [],
|
||||
transform: (
|
||||
translation: (x: -4.0, y: 8.0, z: -6.0),
|
||||
rotation: (x: 45.0, y: 30.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(
|
||||
type: directional_light,
|
||||
data: (
|
||||
color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0),
|
||||
illuminance: 10000.0,
|
||||
shadows: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
2: (
|
||||
id: 2,
|
||||
name: "Ground",
|
||||
parent: None,
|
||||
children: [],
|
||||
transform: (
|
||||
translation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
rotation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(
|
||||
type: mesh3d,
|
||||
data: (
|
||||
mesh: (
|
||||
source: primitive,
|
||||
value: cuboid(size: (x: 12.0, y: 0.2, z: 12.0)),
|
||||
),
|
||||
material: (
|
||||
base_color: (r: 0.35, g: 0.55, b: 0.3, a: 1.0),
|
||||
metallic: 0.0,
|
||||
perceptual_roughness: 0.9,
|
||||
unlit: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
},
|
||||
roots: [1, 2],
|
||||
next_id: 3,
|
||||
)
|
||||
```
|
||||
|
||||
A 2D scene with hierarchy, a sprite, text, a camera, a hidden component and a custom component:
|
||||
|
||||
```ron
|
||||
(
|
||||
format_version: 1,
|
||||
name: "Platformer",
|
||||
entities: {
|
||||
1: (
|
||||
id: 1,
|
||||
name: "Camera",
|
||||
parent: None,
|
||||
children: [],
|
||||
transform: (
|
||||
translation: (x: 0.0, y: 0.0, z: 10.0),
|
||||
rotation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(type: camera2d),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
2: (
|
||||
id: 2,
|
||||
name: "Player",
|
||||
parent: None,
|
||||
children: [3],
|
||||
transform: (
|
||||
translation: (x: 0.0, y: 1.0, z: 0.0),
|
||||
rotation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(
|
||||
type: sprite,
|
||||
data: (
|
||||
image: Some("textures/player.png"),
|
||||
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||
custom_size: Some((x: 32.0, y: 48.0)),
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
),
|
||||
),
|
||||
(type: visibility, data: visible),
|
||||
(
|
||||
type: custom,
|
||||
data: (
|
||||
type_path: "forge_demo::Orbit",
|
||||
fields: [
|
||||
(name: "radius", expr: "2.0"),
|
||||
(name: "speed", expr: "-0.5"),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
3: (
|
||||
id: 3,
|
||||
name: "Label",
|
||||
parent: Some(2),
|
||||
children: [],
|
||||
transform: (
|
||||
translation: (x: 0.0, y: 32.0, z: 0.0),
|
||||
rotation: (x: 0.0, y: 0.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
),
|
||||
components: [
|
||||
(
|
||||
type: text2d,
|
||||
data: (
|
||||
text: "Bevy Forge",
|
||||
font_size: 24.0,
|
||||
color: (r: 0.9, g: 0.9, b: 1.0, a: 1.0),
|
||||
),
|
||||
),
|
||||
],
|
||||
locked: false,
|
||||
),
|
||||
},
|
||||
roots: [1, 2],
|
||||
next_id: 4,
|
||||
)
|
||||
```
|
||||
|
||||
## Top-level fields
|
||||
|
||||
`SceneDoc` is a RON struct with these fields, in this order:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `format_version` | `u32` | Schema version. Currently `1`; see [Versioning](#versioning). |
|
||||
| `name` | `String` | Scene display name, independent of the filename. May be empty (validation warns). |
|
||||
| `entities` | map `u64 -> EntityNode` | All entities, keyed by ID. Map order is insertion order and is stable across saves. |
|
||||
| `roots` | `[u64]` | Root entity IDs in display order. Every entity is either a root or a descendant of one. |
|
||||
| `next_id` | `u64` | Next identity to allocate. Must be greater than every ID in `entities`; IDs are never reused. |
|
||||
|
||||
## Entity fields
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `id` | `u64` | Must equal the key in `entities`. |
|
||||
| `name` | `String` | Display name; arbitrary UTF-8. Used for hierarchy labels and generated `Name(...)`. |
|
||||
| `parent` | `Option<u64>` | `None` for roots, otherwise the containing entity. |
|
||||
| `children` | `[u64]` | Ordered child IDs. Order is preserved in the hierarchy and in generated code. |
|
||||
| `transform` | `Transform` | Mandatory local transform; see below. |
|
||||
| `components` | `[ComponentData]` | Ordered component list; at most one component per `key()`. |
|
||||
| `locked` | `bool` | Editor-only. `true` blocks viewport transforms and gizmos, not hierarchy edits or deletion. Missing in old files defaults to `false`. |
|
||||
|
||||
## Transforms and coordinates
|
||||
|
||||
`Transform` mirrors Bevy's `Transform`:
|
||||
|
||||
```ron
|
||||
transform: (
|
||||
translation: (x: 1.0, y: 2.0, z: 0.5),
|
||||
rotation: (x: 0.0, y: 45.0, z: 0.0),
|
||||
scale: (x: 1.0, y: 1.0, z: 1.0),
|
||||
)
|
||||
```
|
||||
|
||||
- `translation` is the local position. 2D scenes use X/Y; Z is used as the draw-order axis by the
|
||||
2D viewport. 3D scenes use all three axes.
|
||||
- `rotation` is **XYZ Euler angles in degrees**, not radians and not a quaternion. This is the value
|
||||
the inspector edits. Export converts it with
|
||||
`Quat::from_euler(EulerRot::XYZ, x.to_radians(), y.to_radians(), z.to_radians())`.
|
||||
- `scale` is a per-axis multiplier. A zero scale is legal in the file but produces a validation
|
||||
warning.
|
||||
- World transforms are computed by composing local transforms down the parent chain. Parents affect
|
||||
children; moving a parent moves its subtree in the viewports. Scene files always store local
|
||||
transforms, never world transforms.
|
||||
- All numbers are `f32` RON floats. Non-finite values (`NaN`, `inf`) are representable in RON but
|
||||
rejected by validation and code generation.
|
||||
|
||||
Colours use the four-field `Color` struct with channels in `0.0..=1.0`:
|
||||
|
||||
```ron
|
||||
color: (r: 0.35, g: 0.55, b: 0.95, a: 1.0)
|
||||
```
|
||||
|
||||
The editor edits these as sRGB colours; the generated code emits `Color::srgba(r, g, b, a)`.
|
||||
|
||||
## Components
|
||||
|
||||
`components` is an ordered list of serde-tagged values. Every entry has a `type` tag and, except for
|
||||
unit variants, a `data` payload. Tags and enum payloads are **bare RON identifiers**, not strings -
|
||||
write `type: sprite`, not `type: "sprite"`:
|
||||
|
||||
```ron
|
||||
(
|
||||
type: sprite,
|
||||
data: (
|
||||
image: Some("textures/player.png"),
|
||||
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||
custom_size: Some((x: 32.0, y: 48.0)),
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
The tag values and uniqueness keys are:
|
||||
|
||||
| Tag | Payload | `key()` | Notes |
|
||||
|---|---|---|---|
|
||||
| `sprite` | `SpriteData` | `sprite` | 2D sprite |
|
||||
| `text2d` | `Text2dData` | `text2d` | world-space text |
|
||||
| `camera2d` | none | `camera2d` | unit: `(type: camera2d)` |
|
||||
| `camera3d` | none | `camera3d` | unit: `(type: camera3d)` |
|
||||
| `point_light` | `PointLightData` | `point_light` | |
|
||||
| `directional_light` | `DirectionalLightData` | `directional_light` | |
|
||||
| `mesh3d` | `Mesh3dData` | `mesh3d` | primitive or asset mesh plus material |
|
||||
| `visibility` | `VisibilityMode` | `visibility` | `inherited`, `visible` or `hidden` |
|
||||
| `custom` | `CustomComponent` | `custom:<type_path>` | one per custom type path |
|
||||
|
||||
`Name` and `Transform` are **not** components in this format - they are entity fields. Adding a
|
||||
component whose key already exists is refused; use the inspector's edit controls to change values.
|
||||
|
||||
### Sprite
|
||||
|
||||
```ron
|
||||
(
|
||||
type: sprite,
|
||||
data: (
|
||||
image: Some("textures/player.png"),
|
||||
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||
custom_size: Some((x: 64.0, y: 64.0)),
|
||||
flip_x: false,
|
||||
flip_y: false,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `image` | `Option<String>` | Path relative to `assets/`; omit `Some` for a solid-colour sprite |
|
||||
| `color` | `Color` | Tint; defaults to white |
|
||||
| `custom_size` | `Option<Vec2>` | Explicit size; `None` uses the texture's native size |
|
||||
| `flip_x`, `flip_y` | `bool` | Mirror the texture on that axis |
|
||||
|
||||
A sprite with no image and no custom size renders nothing; validation reports a warning.
|
||||
|
||||
### Text2d
|
||||
|
||||
```ron
|
||||
(
|
||||
type: text2d,
|
||||
data: (
|
||||
text: "Hello\nBevy",
|
||||
font_size: 32.0,
|
||||
color: (r: 1.0, g: 1.0, b: 1.0, a: 1.0),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
`font_size` is in pixels and must be greater than zero. The text string is escaped as a normal RON
|
||||
string and becomes a Rust string literal on export. Empty text is allowed but produces a warning.
|
||||
|
||||
### Cameras
|
||||
|
||||
```ron
|
||||
(type: camera2d)
|
||||
```
|
||||
|
||||
```ron
|
||||
(type: camera3d)
|
||||
```
|
||||
|
||||
A scene can contain both, but the 2D and 3D camera mix produces a warning. Optional cameras are
|
||||
reported as info; more than one camera is also info. Neither variant stores projection fields in
|
||||
this version.
|
||||
|
||||
### Point light
|
||||
|
||||
```ron
|
||||
(
|
||||
type: point_light,
|
||||
data: (
|
||||
color: (r: 1.0, g: 0.8, b: 0.6, a: 1.0),
|
||||
intensity: 800.0,
|
||||
range: 20.0,
|
||||
shadows: true,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
`intensity` must be non-negative and `range` must be positive; `shadows` maps to
|
||||
`shadow_maps_enabled` in generated BSN.
|
||||
|
||||
### Directional light
|
||||
|
||||
```ron
|
||||
(
|
||||
type: directional_light,
|
||||
data: (
|
||||
color: (r: 1.0, g: 0.95, b: 0.85, a: 1.0),
|
||||
illuminance: 10000.0,
|
||||
shadows: true,
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
`illuminance` must be non-negative. The light's direction comes from the entity's rotation.
|
||||
|
||||
### Mesh3d
|
||||
|
||||
```ron
|
||||
(
|
||||
type: mesh3d,
|
||||
data: (
|
||||
mesh: (
|
||||
source: primitive,
|
||||
value: cuboid(size: (x: 1.0, y: 1.0, z: 1.0)),
|
||||
),
|
||||
material: (
|
||||
base_color: (r: 0.45, g: 0.62, b: 0.95, a: 1.0),
|
||||
metallic: 0.0,
|
||||
perceptual_roughness: 0.5,
|
||||
unlit: false,
|
||||
),
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
`MeshSource` is tagged `source` / `value`:
|
||||
|
||||
- `source: primitive` with one of the five `PrimitiveShape` values below.
|
||||
- `source: asset` with a string mesh path, for example
|
||||
`value: "models/robot.gltf#Mesh0/Primitive0"`.
|
||||
|
||||
Primitive shapes (RON value for `value`):
|
||||
|
||||
| Shape | RON |
|
||||
|---|---|
|
||||
| Cuboid | `cuboid(size: (x: 2.0, y: 1.0, z: 0.5))` |
|
||||
| Sphere | `sphere(radius: 0.5)` |
|
||||
| Plane | `plane(size: (x: 10.0, y: 10.0))` - width and depth in X/Z |
|
||||
| Cylinder | `cylinder(radius: 0.5, height: 1.0)` |
|
||||
| Capsule | `capsule(radius: 0.5, length: 1.0)` |
|
||||
|
||||
`MaterialData` fields:
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `base_color` | `Color` | Albedo / base colour |
|
||||
| `metallic` | `f32` | 0 = dielectric, 1 = metal; values outside `0..=1` warn |
|
||||
| `perceptual_roughness` | `f32` | 0 = mirror, 1 = rough; values outside `0..=1` warn |
|
||||
| `unlit` | `bool` | Emit an unlit material |
|
||||
|
||||
Asset mesh paths are validated for existence and kind when an asset index is available, and are
|
||||
emitted as `Mesh3d("<path>")` in BSN. The material is always emitted inline, so no material asset
|
||||
loader is required.
|
||||
|
||||
### Visibility
|
||||
|
||||
```ron
|
||||
(type: visibility, data: hidden)
|
||||
```
|
||||
|
||||
The payload is one of the bare RON identifiers `inherited`, `visible` or `hidden`. `hidden` is
|
||||
equivalent to Bevy's `Visibility::Hidden`; toggling the hierarchy eye adds/removes this component.
|
||||
An entity without a visibility component is treated as `inherited`.
|
||||
|
||||
### Custom components
|
||||
|
||||
```ron
|
||||
(
|
||||
type: custom,
|
||||
data: (
|
||||
type_path: "forge_demo::Orbit",
|
||||
fields: [
|
||||
(name: "radius", expr: "2.0"),
|
||||
(name: "speed", expr: "-0.5"),
|
||||
(name: "axis", expr: "Vec3::Y"),
|
||||
],
|
||||
),
|
||||
)
|
||||
```
|
||||
|
||||
| Field | Type | Meaning |
|
||||
|---|---|---|
|
||||
| `type_path` | `String` | A Rust type path such as `Health`, `my_game::Health` or `my_game::components::Health` |
|
||||
| `fields` | `[CustomField]` | Ordered field initialisers |
|
||||
| `fields[].name` | `String` | Rust field identifier |
|
||||
| `fields[].expr` | `String` | Raw Rust/BSN expression, stored verbatim and emitted after `name:` |
|
||||
|
||||
Custom components are the escape hatch for project-specific types. The editor validates only the
|
||||
shape of the type path and field names, and that expressions are non-empty; it does not parse Rust.
|
||||
The component type must derive `Component` plus `Default + Clone` or `FromTemplate` - see
|
||||
[custom-component requirements](bsn-export.md#custom-component-requirements). Two custom components
|
||||
with different type paths can coexist on one entity; the same path may appear only once.
|
||||
|
||||
## Ids, forests and ordering
|
||||
|
||||
Every scene is a forest of entities:
|
||||
|
||||
- Each entity has exactly one parent (`None` for roots) and appears in exactly one place: either
|
||||
`roots` or one parent's `children` list.
|
||||
- `parent` and `children` must agree (`child.parent == Some(parent_id)`). `roots` entries must have
|
||||
`parent: None`.
|
||||
- Cycles are invalid and are rejected by `reparent`; a hand-edited file with a cycle fails
|
||||
structural validation.
|
||||
- `id` is unique and stable for the lifetime of the scene; IDs are never reused after deletion.
|
||||
`next_id` must remain greater than every entity ID. Gaps are fine.
|
||||
- Order matters: `roots` controls root order, each `children` list controls sibling order, and both
|
||||
are preserved by duplicate/reparent and by code generation.
|
||||
- Deleting an entity removes its whole subtree. Duplicating deep-copies the subtree, inserts the
|
||||
copy immediately after the original among its siblings, and suffixes the root copy's name with
|
||||
`" Copy"`.
|
||||
- `locked` is editor metadata and has no effect on export.
|
||||
|
||||
`SceneDoc::check_integrity()` returns a list of structural problems (empty when valid). Validation
|
||||
includes these checks.
|
||||
|
||||
## Versioning
|
||||
|
||||
`format_version` is the scene schema version, not the Bevy Forge application version. It is currently
|
||||
`1` (`FORMAT_VERSION`).
|
||||
|
||||
- The editor writes `1`.
|
||||
- On load, version `0` is normalized to `1` in memory and the data is kept (a permissive path for
|
||||
early files).
|
||||
- There is no migration framework yet: the loader otherwise parses the document as the current
|
||||
schema. Treat version `1` as the only supported schema.
|
||||
- Additive fields should be serde-defaultable so older files still load; breaking changes bump
|
||||
`format_version`.
|
||||
- Generated code is not stored in the file and is not versioned by this field. Export behaviour is
|
||||
governed by the codegen contract and the target Bevy version (0.20).
|
||||
|
||||
## Asset paths
|
||||
|
||||
Sprite images and asset meshes use paths **relative to the project's Bevy assets directory**, with
|
||||
`/` separators:
|
||||
|
||||
```ron
|
||||
image: Some("textures/player.png")
|
||||
```
|
||||
|
||||
```ron
|
||||
value: "models/robot.gltf#Mesh0/Primitive0"
|
||||
```
|
||||
|
||||
Rules (violations are validation errors):
|
||||
|
||||
- No absolute paths.
|
||||
- No `..` path components.
|
||||
- No backslashes.
|
||||
- With an asset index, the file must exist and its kind must match the use (for example, an `.ogg`
|
||||
cannot be used as a sprite image; `.png`, `.jpg`, `.jpeg`, `.webp`, `.bmp`, `.tga`, `.ktx2` and
|
||||
`.dds` are images, while `.gltf`, `.glb`, `.obj` and `.fbx` are meshes).
|
||||
|
||||
Projects with an `assets/` directory use it directly. Projects without one treat the project root as
|
||||
the assets directory. The editor resolves asset paths through the project, never the process working
|
||||
directory.
|
||||
|
||||
## Saving and autosave
|
||||
|
||||
- `Project::save_scene` serializes with the pretty RON printer and writes **atomically**: it writes
|
||||
a temporary `.<filename>.tmp-<pid>` file in the destination directory and renames it over the
|
||||
target. A crash during save leaves the previous file intact.
|
||||
- The editor refuses non-`.forge.ron` filenames and `Project::save_scene` refuses paths outside the
|
||||
project root. Parent directories are created for save-as paths.
|
||||
- Saving marks the session clean; the title dot and quit guard use that flag.
|
||||
- When autosave is enabled (`autosave_secs > 0`, default 60 seconds), a dirty document is written to
|
||||
`<project>/.forge/autosave.forge.ron` at most once per interval. Autosave is best-effort: it does
|
||||
not mark the session saved and is not a substitute for saving. Set `autosave_secs` to 0 to disable
|
||||
it.
|
||||
- Editor preferences are stored separately in `bevy-forge/config.ron` under the platform config
|
||||
directory; that file is not a scene.
|
||||
|
||||
## Validation
|
||||
|
||||
`validate_scene(doc, assets: Option<&AssetIndex>)` returns a `ValidationReport` with `Error`,
|
||||
`Warning` and `Info` issues. `forge validate` prints them and uses the exit code to distinguish
|
||||
"valid" from "has errors"; the editor shows per-entity issues and summary counts. Passing `None` for
|
||||
the asset index disables missing-asset checks.
|
||||
|
||||
**Errors**
|
||||
|
||||
- Structural invariant failures: missing parent/child/root, cycles, mismatched node ID, duplicate
|
||||
references.
|
||||
- Non-finite transform, rotation or scale values.
|
||||
- Non-finite light or material values.
|
||||
- Custom component: invalid type path, invalid field name, empty field expression.
|
||||
- Asset reference containing an absolute path, a `..` component or a backslash.
|
||||
- With an asset index: referenced sprite image or mesh asset missing, or the wrong asset kind.
|
||||
- Sprite `custom_size` with a non-positive component.
|
||||
- Text `font_size <= 0`.
|
||||
- `PointLight.intensity < 0` or `range <= 0`.
|
||||
- `DirectionalLight.illuminance < 0`.
|
||||
- An entity is its own parent or introduces a cycle.
|
||||
|
||||
**Warnings**
|
||||
|
||||
- Duplicate entity names.
|
||||
- A sprite with no image and no custom size (it renders nothing).
|
||||
- A zero scale component.
|
||||
- Material `metallic` or `perceptual_roughness` outside `0..=1`.
|
||||
- Empty `Text2d` string.
|
||||
- Both 2D and 3D cameras in one scene.
|
||||
- An empty or whitespace-only entity name.
|
||||
|
||||
**Info**
|
||||
|
||||
- No camera in the scene.
|
||||
- No light in a scene that contains meshes.
|
||||
- An entity with only a `Transform`.
|
||||
- More than one camera.
|
||||
|
||||
Validation is deterministic (severity descending, then entity ID, then message) and must not panic
|
||||
on malformed forests.
|
||||
|
||||
## Relationship to BSN
|
||||
|
||||
The `.forge.ron` file is the source of truth in the editor. Exporting walks the forest and emits
|
||||
`Name`, `Transform` and the component list for every entity, converting Euler degrees to a quaternion
|
||||
and choosing asset parameters for mesh scenes. The generated file is portable Rust, not a scene asset
|
||||
that Bevy loads at runtime. See the [BSN export guide](bsn-export.md).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 791 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 476 KiB |
Reference in New Issue
Block a user