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". Anyunsafeblock must add#![allow(unsafe_code)]at the file or module level with areasonattribute. Newunsafeis always reviewed carefully.missing_docs = "warn". Everypubitem 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 supportsno_std. Usecore::andalloc::paths in library code wherever possible; reach forstd::only in platform-specific code or inbevy_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
AneedsBto function, declare it with#[derive(Component)] #[require(B)]so spawningAautomatically insertsB. New code prefersrequireover runtime checks. - Observers over events for cross-cutting effects. When some other system should react to a component being added, prefer a
OnAddobserver 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. UseCommandsso 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
SystemSetwith descriptive names likeRenderSet::Queue. - Reflect-aware components: derive
Reflectand add#[reflect(Component)].
Error handling
- Library code returns
Result<T, E>with a typed error. Many crates usethiserrorto define the error enum (e.g.bevy_asset::AssetServerError). - Application-side code (examples, tests,
bevy_app) usesanyhowonly sparingly. - The
bevy_ecs::errormodule provides per-system error reporting — systems that return aResultgo 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
*_macroscrates (bevy_ecs_macros,bevy_reflect_derive,bevy_derive, etc). - Helper macros (
children!,bsn!) follow specific brace conventions enforced byclippy.toml:children!requires[ ]braces.
- Procedural macros that go behind a
cfgneed 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
# Examplesblocks 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.rsat 4,600 lines) are exceptions; new modules stay focused. - Related types live in a module folder with a
mod.rsthat re-exports the public API. - Tests at the bottom of each file in
#[cfg(test)] mod tests.
Re-exports
- Each crate's
lib.rsdefines apreludemodule re-exporting the things end users want without thinking about source paths. bevy::prelude(inbevy_internal::prelude) re-exports each subordinateprelude. 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.