
# Objective - Fixes #17960 ## Solution - Followed the [edition upgrade guide](https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-project-to-a-new-edition.html) ## Testing - CI --- ## Summary of Changes ### Documentation Indentation When using lists in documentation, proper indentation is now linted for. This means subsequent lines within the same list item must start at the same indentation level as the item. ```rust /* Valid */ /// - Item 1 /// Run-on sentence. /// - Item 2 struct Foo; /* Invalid */ /// - Item 1 /// Run-on sentence. /// - Item 2 struct Foo; ``` ### Implicit `!` to `()` Conversion `!` (the never return type, returned by `panic!`, etc.) no longer implicitly converts to `()`. This is particularly painful for systems with `todo!` or `panic!` statements, as they will no longer be functions returning `()` (or `Result<()>`), making them invalid systems for functions like `add_systems`. The ideal fix would be to accept functions returning `!` (or rather, _not_ returning), but this is blocked on the [stabilisation of the `!` type itself](https://doc.rust-lang.org/std/primitive.never.html), which is not done. The "simple" fix would be to add an explicit `-> ()` to system signatures (e.g., `|| { todo!() }` becomes `|| -> () { todo!() }`). However, this is _also_ banned, as there is an existing lint which (IMO, incorrectly) marks this as an unnecessary annotation. So, the "fix" (read: workaround) is to put these kinds of `|| -> ! { ... }` closuers into variables and give the variable an explicit type (e.g., `fn()`). ```rust // Valid let system: fn() = || todo!("Not implemented yet!"); app.add_systems(..., system); // Invalid app.add_systems(..., || todo!("Not implemented yet!")); ``` ### Temporary Variable Lifetimes The order in which temporary variables are dropped has changed. The simple fix here is _usually_ to just assign temporaries to a named variable before use. ### `gen` is a keyword We can no longer use the name `gen` as it is reserved for a future generator syntax. This involved replacing uses of the name `gen` with `r#gen` (the raw-identifier syntax). ### Formatting has changed Use statements have had the order of imports changed, causing a substantial +/-3,000 diff when applied. For now, I have opted-out of this change by amending `rustfmt.toml` ```toml style_edition = "2021" ``` This preserves the original formatting for now, reducing the size of this PR. It would be a simple followup to update this to 2024 and run `cargo fmt`. ### New `use<>` Opt-Out Syntax Lifetimes are now implicitly included in RPIT types. There was a handful of instances where it needed to be added to satisfy the borrow checker, but there may be more cases where it _should_ be added to avoid breakages in user code. ### `MyUnitStruct { .. }` is an invalid pattern Previously, you could match against unit structs (and unit enum variants) with a `{ .. }` destructuring. This is no longer valid. ### Pretty much every use of `ref` and `mut` are gone Pattern binding has changed to the point where these terms are largely unused now. They still serve a purpose, but it is far more niche now. ### `iter::repeat(...).take(...)` is bad New lint recommends using the more explicit `iter::repeat_n(..., ...)` instead. ## Migration Guide The lifetimes of functions using return-position impl-trait (RPIT) are likely _more_ conservative than they had been previously. If you encounter lifetime issues with such a function, please create an issue to investigate the addition of `+ use<...>`. ## Notes - Check the individual commits for a clearer breakdown for what _actually_ changed. --------- Co-authored-by: François Mockers <francois.mockers@vleue.com>
261 lines
10 KiB
Rust
261 lines
10 KiB
Rust
//! This example showcases a 3D first-person camera.
|
|
//!
|
|
//! The setup presented here is a very common way of organizing a first-person game
|
|
//! where the player can see their own arms. We use two industry terms to differentiate
|
|
//! the kinds of models we have:
|
|
//!
|
|
//! - The *view model* is the model that represents the player's body.
|
|
//! - The *world model* is everything else.
|
|
//!
|
|
//! ## Motivation
|
|
//!
|
|
//! The reason for this distinction is that these two models should be rendered with different field of views (FOV).
|
|
//! The view model is typically designed and animated with a very specific FOV in mind, so it is
|
|
//! generally *fixed* and cannot be changed by a player. The world model, on the other hand, should
|
|
//! be able to change its FOV to accommodate the player's preferences for the following reasons:
|
|
//! - *Accessibility*: How prone is the player to motion sickness? A wider FOV can help.
|
|
//! - *Tactical preference*: Does the player want to see more of the battlefield?
|
|
//! Or have a more zoomed-in view for precision aiming?
|
|
//! - *Physical considerations*: How well does the in-game FOV match the player's real-world FOV?
|
|
//! Are they sitting in front of a monitor or playing on a TV in the living room? How big is the screen?
|
|
//!
|
|
//! ## Implementation
|
|
//!
|
|
//! The `Player` is an entity holding two cameras, one for each model. The view model camera has a fixed
|
|
//! FOV of 70 degrees, while the world model camera has a variable FOV that can be changed by the player.
|
|
//!
|
|
//! We use different `RenderLayers` to select what to render.
|
|
//!
|
|
//! - The world model camera has no explicit `RenderLayers` component, so it uses the layer 0.
|
|
//! All static objects in the scene are also on layer 0 for the same reason.
|
|
//! - The view model camera has a `RenderLayers` component with layer 1, so it only renders objects
|
|
//! explicitly assigned to layer 1. The arm of the player is one such object.
|
|
//! The order of the view model camera is additionally bumped to 1 to ensure it renders on top of the world model.
|
|
//! - The light source in the scene must illuminate both the view model and the world model, so it is
|
|
//! assigned to both layers 0 and 1.
|
|
//!
|
|
//! ## Controls
|
|
//!
|
|
//! | Key Binding | Action |
|
|
//! |:---------------------|:--------------|
|
|
//! | mouse | Look around |
|
|
//! | arrow up | Decrease FOV |
|
|
//! | arrow down | Increase FOV |
|
|
|
|
use std::f32::consts::FRAC_PI_2;
|
|
|
|
use bevy::{
|
|
color::palettes::tailwind, input::mouse::AccumulatedMouseMotion, pbr::NotShadowCaster,
|
|
prelude::*, render::view::RenderLayers,
|
|
};
|
|
|
|
fn main() {
|
|
App::new()
|
|
.add_plugins(DefaultPlugins)
|
|
.add_systems(
|
|
Startup,
|
|
(
|
|
spawn_view_model,
|
|
spawn_world_model,
|
|
spawn_lights,
|
|
spawn_text,
|
|
),
|
|
)
|
|
.add_systems(Update, (move_player, change_fov))
|
|
.run();
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
struct Player;
|
|
|
|
#[derive(Debug, Component, Deref, DerefMut)]
|
|
struct CameraSensitivity(Vec2);
|
|
|
|
impl Default for CameraSensitivity {
|
|
fn default() -> Self {
|
|
Self(
|
|
// These factors are just arbitrary mouse sensitivity values.
|
|
// It's often nicer to have a faster horizontal sensitivity than vertical.
|
|
// We use a component for them so that we can make them user-configurable at runtime
|
|
// for accessibility reasons.
|
|
// It also allows you to inspect them in an editor if you `Reflect` the component.
|
|
Vec2::new(0.003, 0.002),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Component)]
|
|
struct WorldModelCamera;
|
|
|
|
/// Used implicitly by all entities without a `RenderLayers` component.
|
|
/// Our world model camera and all objects other than the player are on this layer.
|
|
/// The light source belongs to both layers.
|
|
const DEFAULT_RENDER_LAYER: usize = 0;
|
|
|
|
/// Used by the view model camera and the player's arm.
|
|
/// The light source belongs to both layers.
|
|
const VIEW_MODEL_RENDER_LAYER: usize = 1;
|
|
|
|
fn spawn_view_model(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
let arm = meshes.add(Cuboid::new(0.1, 0.1, 0.5));
|
|
let arm_material = materials.add(Color::from(tailwind::TEAL_200));
|
|
|
|
commands
|
|
.spawn((
|
|
Player,
|
|
CameraSensitivity::default(),
|
|
Transform::from_xyz(0.0, 1.0, 0.0),
|
|
Visibility::default(),
|
|
))
|
|
.with_children(|parent| {
|
|
parent.spawn((
|
|
WorldModelCamera,
|
|
Camera3d::default(),
|
|
Projection::from(PerspectiveProjection {
|
|
fov: 90.0_f32.to_radians(),
|
|
..default()
|
|
}),
|
|
));
|
|
|
|
// Spawn view model camera.
|
|
parent.spawn((
|
|
Camera3d::default(),
|
|
Camera {
|
|
// Bump the order to render on top of the world model.
|
|
order: 1,
|
|
..default()
|
|
},
|
|
Projection::from(PerspectiveProjection {
|
|
fov: 70.0_f32.to_radians(),
|
|
..default()
|
|
}),
|
|
// Only render objects belonging to the view model.
|
|
RenderLayers::layer(VIEW_MODEL_RENDER_LAYER),
|
|
));
|
|
|
|
// Spawn the player's right arm.
|
|
parent.spawn((
|
|
Mesh3d(arm),
|
|
MeshMaterial3d(arm_material),
|
|
Transform::from_xyz(0.2, -0.1, -0.25),
|
|
// Ensure the arm is only rendered by the view model camera.
|
|
RenderLayers::layer(VIEW_MODEL_RENDER_LAYER),
|
|
// The arm is free-floating, so shadows would look weird.
|
|
NotShadowCaster,
|
|
));
|
|
});
|
|
}
|
|
|
|
fn spawn_world_model(
|
|
mut commands: Commands,
|
|
mut meshes: ResMut<Assets<Mesh>>,
|
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
|
) {
|
|
let floor = meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(10.0)));
|
|
let cube = meshes.add(Cuboid::new(2.0, 0.5, 1.0));
|
|
let material = materials.add(Color::WHITE);
|
|
|
|
// The world model camera will render the floor and the cubes spawned in this system.
|
|
// Assigning no `RenderLayers` component defaults to layer 0.
|
|
|
|
commands.spawn((Mesh3d(floor), MeshMaterial3d(material.clone())));
|
|
|
|
commands.spawn((
|
|
Mesh3d(cube.clone()),
|
|
MeshMaterial3d(material.clone()),
|
|
Transform::from_xyz(0.0, 0.25, -3.0),
|
|
));
|
|
|
|
commands.spawn((
|
|
Mesh3d(cube),
|
|
MeshMaterial3d(material),
|
|
Transform::from_xyz(0.75, 1.75, 0.0),
|
|
));
|
|
}
|
|
|
|
fn spawn_lights(mut commands: Commands) {
|
|
commands.spawn((
|
|
PointLight {
|
|
color: Color::from(tailwind::ROSE_300),
|
|
shadows_enabled: true,
|
|
..default()
|
|
},
|
|
Transform::from_xyz(-2.0, 4.0, -0.75),
|
|
// The light source illuminates both the world model and the view model.
|
|
RenderLayers::from_layers(&[DEFAULT_RENDER_LAYER, VIEW_MODEL_RENDER_LAYER]),
|
|
));
|
|
}
|
|
|
|
fn spawn_text(mut commands: Commands) {
|
|
commands
|
|
.spawn(Node {
|
|
position_type: PositionType::Absolute,
|
|
bottom: Val::Px(12.0),
|
|
left: Val::Px(12.0),
|
|
..default()
|
|
})
|
|
.with_child(Text::new(concat!(
|
|
"Move the camera with your mouse.\n",
|
|
"Press arrow up to decrease the FOV of the world model.\n",
|
|
"Press arrow down to increase the FOV of the world model."
|
|
)));
|
|
}
|
|
|
|
fn move_player(
|
|
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
|
|
player: Single<(&mut Transform, &CameraSensitivity), With<Player>>,
|
|
) {
|
|
let (mut transform, camera_sensitivity) = player.into_inner();
|
|
|
|
let delta = accumulated_mouse_motion.delta;
|
|
|
|
if delta != Vec2::ZERO {
|
|
// Note that we are not multiplying by delta_time here.
|
|
// The reason is that for mouse movement, we already get the full movement that happened since the last frame.
|
|
// This means that if we multiply by delta_time, we will get a smaller rotation than intended by the user.
|
|
// This situation is reversed when reading e.g. analog input from a gamepad however, where the same rules
|
|
// as for keyboard input apply. Such an input should be multiplied by delta_time to get the intended rotation
|
|
// independent of the framerate.
|
|
let delta_yaw = -delta.x * camera_sensitivity.x;
|
|
let delta_pitch = -delta.y * camera_sensitivity.y;
|
|
|
|
let (yaw, pitch, roll) = transform.rotation.to_euler(EulerRot::YXZ);
|
|
let yaw = yaw + delta_yaw;
|
|
|
|
// If the pitch was ±¹⁄₂ π, the camera would look straight up or down.
|
|
// When the user wants to move the camera back to the horizon, which way should the camera face?
|
|
// The camera has no way of knowing what direction was "forward" before landing in that extreme position,
|
|
// so the direction picked will for all intents and purposes be arbitrary.
|
|
// Another issue is that for mathematical reasons, the yaw will effectively be flipped when the pitch is at the extremes.
|
|
// To not run into these issues, we clamp the pitch to a safe range.
|
|
const PITCH_LIMIT: f32 = FRAC_PI_2 - 0.01;
|
|
let pitch = (pitch + delta_pitch).clamp(-PITCH_LIMIT, PITCH_LIMIT);
|
|
|
|
transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll);
|
|
}
|
|
}
|
|
|
|
fn change_fov(
|
|
input: Res<ButtonInput<KeyCode>>,
|
|
mut world_model_projection: Single<&mut Projection, With<WorldModelCamera>>,
|
|
) {
|
|
let Projection::Perspective(perspective) = world_model_projection.as_mut() else {
|
|
unreachable!(
|
|
"The `Projection` component was explicitly built with `Projection::Perspective`"
|
|
);
|
|
};
|
|
|
|
if input.pressed(KeyCode::ArrowUp) {
|
|
perspective.fov -= 1.0_f32.to_radians();
|
|
perspective.fov = perspective.fov.max(20.0_f32.to_radians());
|
|
}
|
|
if input.pressed(KeyCode::ArrowDown) {
|
|
perspective.fov += 1.0_f32.to_radians();
|
|
perspective.fov = perspective.fov.min(160.0_f32.to_radians());
|
|
}
|
|
}
|