Open-Source Wikis

/

Bevy

/

How to contribute

/

Patterns and conventions

bevyengine/bevy

Patterns and conventions

Bevy has accumulated a set of in-house conventions enforced through clippy.toml, rustfmt.toml, and the workspace lints in the root Cargo.toml. This page describes them so a contributor knows what's idiomatic before review tells them.

Workspace lints

The root Cargo.toml declares warnings and denies that apply to every crate:

[workspace.lints.clippy]
doc_markdown = "warn"
manual_let_else = "warn"
match_same_arms = "warn"
redundant_closure_for_method_calls = "warn"
redundant_else = "warn"
semicolon_if_nothing_returned = "warn"
undocumented_unsafe_blocks = "warn"
unwrap_or_default = "warn"
nonstandard_macro_braces = "warn"
print_stdout = "warn"
print_stderr = "warn"
ptr_as_ptr = "warn"
ptr_cast_constness = "warn"
ref_as_ptr = "warn"
std_instead_of_core = "warn"
std_instead_of_alloc = "warn"
alloc_instead_of_core = "warn"
allow_attributes = "warn"
allow_attributes_without_reason = "warn"

[workspace.lints.rust]
missing_docs = "warn"
unsafe_code = "deny"
unsafe_op_in_unsafe_fn = "warn"
unused_qualifications = "warn"

Three of these matter the most in practice:

  • unsafe_code = "deny". Any unsafe block must add #![allow(unsafe_code)] at the file or module level with a reason attribute. New unsafe is always reviewed carefully.
  • missing_docs = "warn". Every pub item needs a doc comment. The CI flags a missing one as a warning, and the maintainer review treats those warnings as blockers.
  • std_instead_of_core / std_instead_of_alloc. Bevy supports no_std. Use core:: and alloc:: paths in library code wherever possible; reach for std:: only in platform-specific code or in bevy_winit.

Allow attributes always come with reasons

Because of allow_attributes_without_reason = "warn", every #[allow(...)] and #[expect(...)] must include a reason = "..." argument:

#[expect(
    clippy::doc_markdown,
    reason = "Clippy lints for un-backticked identifiers within the cargo features list, which we don't want."
)]

This is a Bevy-specific style — many Rust codebases skip it, but Bevy treats reason-less allows as a smell.

Math determinism

clippy.toml bans the standard-library f32 math functions across the workspace:

disallowed-methods = [
  { path = "f32::powf", reason = "use bevy_math::ops::powf instead for libm determinism" },
  { path = "f32::sin",  reason = "use bevy_math::ops::sin  instead for libm determinism" },
  ...
]

Use the wrappers in crates/bevy_math/src/ops.rs instead. They route through libm when the libm feature is enabled, which is what makes Bevy's math reproducible across platforms (important for replays and networked games).

ECS conventions

Read bevy_ecs for the full picture; the patterns to internalize are:

  • Composition over inheritance. Components are small (Position, Velocity, Health, Visibility). Bundles compose them. There is no class hierarchy.
  • Required components. When component A needs B to function, declare it with #[derive(Component)] #[require(B)] so spawning A automatically inserts B. New code prefers require over runtime checks.
  • Observers over events for cross-cutting effects. When some other system should react to a component being added, prefer a OnAdd observer over a custom event. Observers are the modern idiom.
  • Commands for deferred mutation. A system should not mutate the world structure (spawn, despawn, add components) directly through &mut World. Use Commands so the mutation is deferred to a sync point.
  • Local<T> for per-system state. When a system needs caching that doesn't belong on the world, Local<T> is preferred over a top-level resource.
  • Schedule labels are the API. When you publish a system, put it in a named SystemSet. Other plugins compose against the set, not against function pointers.

Naming

  • Crates: bevy_<thing>, snake_case.
  • Plugins: <Thing>Plugin, e.g. LogPlugin, WinitPlugin.
  • Plugin groups: <Thing>Plugins, e.g. DefaultPlugins, MinimalPlugins.
  • Component bundles (legacy, mostly replaced by required components): <Thing>Bundle.
  • Schedule labels: typed structs deriving ScheduleLabel. Examples: Update, PostUpdate, FixedUpdate.
  • System sets: typed structs deriving SystemSet with descriptive names like RenderSet::Queue.
  • Reflect-aware components: derive Reflect and add #[reflect(Component)].

Error handling

  • Library code returns Result<T, E> with a typed error. Many crates use thiserror to define the error enum (e.g. bevy_asset::AssetServerError).
  • Application-side code (examples, tests, bevy_app) uses anyhow only sparingly.
  • The bevy_ecs::error module provides per-system error reporting — systems that return a Result go through a registered error handler.

Reflection

If a type appears in scenes, the BRP, or the inspector, it needs #[derive(Reflect)]. The full pattern for a reflectable component:

use bevy_reflect::Reflect;

#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
pub struct Health(pub u32);

Plugins register reflected types via app.register_type::<Health>(). The reflect_auto_register feature uses a inventory-based mechanism to auto-register types so plugins don't have to remember every type — most newer code relies on this.

Macros

  • Derive macros live in dedicated *_macros crates (bevy_ecs_macros, bevy_reflect_derive, bevy_derive, etc).
  • Helper macros (children!, bsn!) follow specific brace conventions enforced by clippy.toml:
    • children! requires [ ] braces.
  • Procedural macros that go behind a cfg need to consider both branches (the macro should still expand to compilable code with the feature off).

Doc comments

  • The first line of a doc comment is the summary. Keep it under one line.
  • Include # Examples blocks for non-trivial APIs. Doctest discovery is wired up.
  • Cross-link to other items with backticked identifiers and [ ] paths: [World::spawn], [crate::component::Component].
  • For complex types, link to the relevant chapter of the Bevy book in addition to the rustdoc.

Formatting

rustfmt.toml:

use_field_init_shorthand = true
newline_style = "Unix"
style_edition = "2021"

That's it. Everything else uses the rustfmt defaults. Run cargo fmt --all before pushing.

File organization

  • One concept per file. The big files (crates/bevy_ecs/src/world/mod.rs at 4,600 lines) are exceptions; new modules stay focused.
  • Related types live in a module folder with a mod.rs that re-exports the public API.
  • Tests at the bottom of each file in #[cfg(test)] mod tests.

Re-exports

  • Each crate's lib.rs defines a prelude module re-exporting the things end users want without thinking about source paths.
  • bevy::prelude (in bevy_internal::prelude) re-exports each subordinate prelude. It's the canonical "I want everything" import for users.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Patterns and conventions – Bevy wiki | Factory