Bloxel sandbox game similar to Minecraft "Classic" (2009) written in Rust with Bevy
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

148 lines
5.1 KiB

use std::f32::consts::TAU;
use bevy::prelude::*;
use bevy::input::mouse::AccumulatedMouseMotion;
use bevy::window::{CursorGrabMode, CursorOptions};
pub fn cursor_grab(
mut mouse_button_input: ResMut<ButtonInput<MouseButton>>,
key_input: Res<ButtonInput<KeyCode>>,
window: Single<(&mut Window, &mut CursorOptions)>,
) {
let (mut window, mut cursor) = window.into_inner();
let is_grabbed = cursor.grab_mode != CursorGrabMode::None;
let request_grab = mouse_button_input.any_just_pressed([MouseButton::Left, MouseButton::Right]);
let request_ungrab = !window.focused || key_input.just_pressed(KeyCode::Escape);
if !is_grabbed && request_grab && !request_ungrab {
cursor.grab_mode = CursorGrabMode::Locked;
// To prevent other systems (such as `place_break_blocks`)
// from seeing the mouse button inputs, clear the state here.
mouse_button_input.clear();
}
if is_grabbed && request_ungrab && !request_grab {
cursor.grab_mode = CursorGrabMode::None;
}
if is_grabbed && !request_ungrab {
// Set the cursor position to the center of the window, so that when
// it is ungrabbed, it will reappear there. Because `is_grabbed` is
// not updated on grab, this block is delayed by one frame.
//
// On Wayland, since the cursor is locked into place, this only needs
// to be done once. Unfortunately, for some reason this doesn't work
// in the same frame as setting `grab_mode`, and would log an error.
//
// On X11, the cursor can't be locked into place, only confined to the
// window bounds, so we repeatedly move the cursor back to the center
// while it's grabbed.
//
// On the web, the cursor can be locked, but setting its position is
// not supported at all, so this would instead log a bunch of errors.
let center = window.resolution.size() / 2.;
#[cfg(not(target_family = "wasm"))] // skip on web
window.set_cursor_position(Some(center));
}
// Keep cursor visbility in sync with `grab_mode`.
cursor.visible = cursor.grab_mode == CursorGrabMode::None;
}
pub fn is_cursor_grabbed(cursor: Single<&CursorOptions>) -> bool {
cursor.grab_mode != CursorGrabMode::None
}
#[derive(Component, Debug)]
pub struct CameraFreeLook {
/// The mouse sensitivity, in radians per pixel.
pub sensitivity: Vec2,
/// How far the camera can be tilted up and down.
pub pitch_limit: std::ops::RangeInclusive<f32>,
/// Upon initialization, `pitch` and `yaw` will
/// be set from the camera transform's rotation.
initialized: bool,
/// The current yaw (right/left) of the camera, in radians.
pub yaw: f32,
/// The current pitch (tilt) of the camera, in radians.
pub pitch: f32,
}
impl Default for CameraFreeLook {
fn default() -> Self {
Self {
sensitivity: Vec2::splat(0.2).map(f32::to_radians),
pitch_limit: -(TAU / 4.0)..=(TAU / 4.0),
initialized: false,
yaw: 0.0,
pitch: 0.0,
}
}
}
pub fn camera_look(
accumulated_mouse_motion: Res<AccumulatedMouseMotion>,
camera: Single<(&mut Transform, &mut CameraFreeLook)>,
) {
let (mut transform, mut look) = camera.into_inner();
// Ensure the yaw and pitch are initialized once
// from the camera transform's current rotation.
if !look.initialized {
(look.yaw, look.pitch, _) = transform.rotation.to_euler(EulerRot::YXZ);
look.initialized = true;
}
// Update the current camera state's internal yaw and pitch.
let motion = accumulated_mouse_motion.delta * look.sensitivity;
let (min, max) = look.pitch_limit.clone().into_inner();
look.yaw = (look.yaw - motion.x).rem_euclid(TAU); // keep within 0°..360°
look.pitch = (look.pitch - motion.y).clamp(min, max);
// Override the camera transform's rotation.
transform.rotation = Quat::from_euler(EulerRot::ZYX, 0.0, look.yaw, look.pitch);
}
#[derive(Component)]
pub struct Crosshair;
pub fn setup_crosshair(mut commands: Commands, assets: Res<AssetServer>) {
commands.spawn((
Node {
width: percent(100),
height: percent(100),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
children![(
Crosshair,
Node {
width: px(64),
height: px(64),
..default()
},
ImageNode {
image: assets.load("crosshair.png"),
..default()
},
// Hidden by default, because cursor shouldn't be grabbed at startup either.
Visibility::Hidden,
)],
));
}
pub fn update_crosshair_visibility(
cursor: Single<&CursorOptions, Changed<CursorOptions>>,
crosshair: Single<&mut Visibility, With<Crosshair>>,
) {
let is_grabbed = cursor.grab_mode != CursorGrabMode::None;
let mut crosshair_visibility = crosshair.into_inner();
*crosshair_visibility = (!is_grabbed || cursor.visible)
.then_some(Visibility::Hidden)
.unwrap_or_default();
}