bevy_lint/lints/nursery/
camera_modification_in_fixed_update.rs

1//! Checks for systems added to the `FixedUpdate` schedule that mutably query entities with a
2//! `Camera` component.
3//!
4//! # Motivation
5//!
6//! Modifying the camera in `FixedUpdate` can cause jittery, inconsistent, or laggy visuals because
7//! `FixedUpdate` may not run every render frame, especially on games with a high FPS.
8//!
9//! # Known Issues
10//!
11//! This lint only detects systems that explicitly use the `With<Camera>` query filter.
12//!
13//! # Example
14//!
15//! ```rust
16//! # use bevy::prelude::*;
17//! #
18//! fn move_camera(mut query: Query<&mut Transform, With<Camera>>) {
19//!     // ...
20//! }
21//!
22//! fn main() {
23//!     App::new()
24//!         // Uh oh! This could cause issues because the camera may not move every frame!
25//!         .add_systems(FixedUpdate, move_camera);
26//! }
27//! ```
28//!
29//! Use instead:
30//!
31//! ```rust
32//! # use bevy::prelude::*;
33//! #
34//! fn move_camera(mut query: Query<&mut Transform, With<Camera>>) {
35//!     // ...
36//! }
37//!
38//! fn main() {
39//!     App::new()
40//!         // Much better. This will run every frame.
41//!         .add_systems(Update, move_camera);
42//! }
43//! ```
44//!
45//! Any system that modifies the camera in a user-visible way should be run every render frame. The
46//! `Update` schedule is a good choice for this, but it notably runs _after_ `FixedUpdate`. You can
47//! use the `RunFixedMainLoop` schedule with the `RunFixedMainLoopSystem::BeforeFixedMainLoop`
48//! system set to run a system before `FixedUpdate`:
49//!
50//! ```
51//! # use bevy::prelude::*;
52//! #
53//! fn rotate_camera(mut query: Query<&mut Transform, With<Camera>>) {
54//!     // ...
55//! }
56//!
57//! fn main() {
58//!     App::new()
59//!         // In 3D games it is common for the player to move in the direction of the camera.
60//!         // Because of this, we must rotate the camera before running the physics logic in
61//!         // `FixedUpdate`. This will still run every render frame, though, so there won't be any
62//!         // lag!
63//!         .add_systems(
64//!             RunFixedMainLoop,
65//!             rotate_camera.in_set(RunFixedMainLoopSystem::BeforeFixedMainLoop),
66//!         );
67//! }
68//! ```
69//!
70//! For more information, check out the
71//! [physics in fixed timestep example](https://bevy.org/examples/movement/physics-in-fixed-timestep/).
72
73use clippy_utils::diagnostics::span_lint_and_help;
74use rustc_hir::{ExprKind, QPath, def::Res};
75use rustc_lint::{LateContext, LateLintPass};
76use rustc_middle::ty::{Adt, GenericArgKind, TyKind};
77
78use crate::{declare_bevy_lint, declare_bevy_lint_pass, sym, utils::hir_parse::MethodCall};
79
80declare_bevy_lint! {
81    pub(crate) CAMERA_MODIFICATION_IN_FIXED_UPDATE,
82    super::Nursery,
83    "camera modified in the `FixedUpdate` schedule",
84}
85
86declare_bevy_lint_pass! {
87    pub(crate) CameraModificationInFixedUpdate => [CAMERA_MODIFICATION_IN_FIXED_UPDATE],
88}
89
90impl<'tcx> LateLintPass<'tcx> for CameraModificationInFixedUpdate {
91    fn check_expr(&mut self, cx: &LateContext<'tcx>, expr: &'tcx rustc_hir::Expr<'tcx>) {
92        if expr.span.in_external_macro(cx.tcx.sess.source_map()) {
93            return;
94        }
95
96        let Some(MethodCall {
97            method_path,
98            args,
99            receiver,
100            ..
101        }) = MethodCall::try_from(cx, expr)
102        else {
103            return;
104        };
105
106        let receiver_ty = cx.typeck_results().expr_ty_adjusted(receiver).peel_refs();
107
108        // Match calls to `App::add_systems(schedule, systems)`
109        if !crate::paths::APP.matches_ty(cx, receiver_ty)
110            || method_path.ident.name != sym::add_systems
111        {
112            return;
113        }
114
115        let [schedule, systems] = args else {
116            return;
117        };
118
119        let schedule_ty = cx.typeck_results().expr_ty_adjusted(schedule).peel_refs();
120
121        // Skip if the schedule is not `FixedUpdate`
122        if !crate::paths::FIXED_UPDATE.matches_ty(cx, schedule_ty) {
123            return;
124        }
125
126        // Collect all added system expressions
127        let system_exprs = if let ExprKind::Tup(inner) = systems.kind {
128            inner.iter().collect()
129        } else {
130            vec![systems]
131        };
132
133        // Resolve the function definition for each system
134        for system_expr in system_exprs {
135            if let ExprKind::Path(QPath::Resolved(_, path)) = system_expr.kind
136                && let Res::Def(_, def_id) = path.res
137            {
138                let system_fn_sig = cx.tcx.fn_sig(def_id);
139                // Iterate over the function parameter types of the system function
140                for ty in system_fn_sig.skip_binder().inputs().skip_binder() {
141                    let Adt(adt_def_id, args) = ty.kind() else {
142                        continue;
143                    };
144
145                    // Check if the parameter is a `Query`
146                    let adt_ty = cx.tcx.type_of(adt_def_id.did()).skip_binder();
147
148                    if !crate::paths::QUERY.matches_ty(cx, adt_ty) {
149                        continue;
150                    }
151
152                    // Get the type arguments and ignore Lifetimes
153                    let mut query_type_arguments =
154                        args.iter()
155                            .filter_map(|generic_arg| match generic_arg.unpack() {
156                                GenericArgKind::Type(ty) => Some(ty),
157                                _ => None,
158                            });
159
160                    let Some(query_data) = query_type_arguments.next() else {
161                        return;
162                    };
163
164                    let Some(query_filters) = query_type_arguments.next() else {
165                        return;
166                    };
167
168                    // Determine mutability of each queried component
169                    let query_data_mutability = match query_data.kind() {
170                        TyKind::Tuple(tys) => tys
171                            .iter()
172                            .filter_map(|ty| match ty.kind() {
173                                TyKind::Ref(_, _, mutability) => Some(mutability),
174                                _ => None,
175                            })
176                            .collect(),
177                        TyKind::Ref(_, _, mutability) => vec![mutability],
178                        _ => return,
179                    };
180
181                    // collect all query filters
182                    let query_filters = if let TyKind::Tuple(inner) = query_filters.kind() {
183                        inner.iter().collect()
184                    } else {
185                        vec![query_filters]
186                    };
187
188                    // Check for `With<Camera>` filter on a mutable query
189                    for query_filter in query_filters {
190                        // Check if the `With` `QueryFilter` was used.
191                        if crate::paths::WITH.matches_ty(cx, query_filter)
192                        // Get the generic argument of the Filter
193                        && let TyKind::Adt(_, with_args) = query_filter.kind()
194                        // There can only be exactly one argument
195                        && let Some(filter_component_arg) = with_args.iter().next()
196                        // Get the type of the component the filter should filter for
197                        && let GenericArgKind::Type(filter_component_ty) =
198                            filter_component_arg.unpack()
199                        // Check if Filter is of type `Camera`
200                        && crate::paths::CAMERA.matches_ty(cx, filter_component_ty)
201                        // Emit lint if any `Camera` component is mutably borrowed
202                        && query_data_mutability.iter().any(|mutability|match mutability {
203                                rustc_ast::Mutability::Not => false,
204                                rustc_ast::Mutability::Mut => true,
205                            })
206                        {
207                            span_lint_and_help(
208                                cx,
209                                CAMERA_MODIFICATION_IN_FIXED_UPDATE,
210                                path.span,
211                                CAMERA_MODIFICATION_IN_FIXED_UPDATE.desc,
212                                None,
213                                "insert the system in the `Update` schedule instead",
214                            );
215                        }
216                    }
217                }
218            }
219        }
220    }
221}