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}