├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.toml ├── LICENSE.md ├── README.md ├── examples ├── basic.rs ├── key_bindings.rs └── scroll.rs ├── shell.nix └── src └── lib.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | update: 7 | name: Update 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: dtolnay/rust-toolchain@stable 12 | - run: cargo update 13 | 14 | check: 15 | name: Check 16 | runs-on: ubuntu-latest 17 | needs: update 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: dtolnay/rust-toolchain@stable 21 | - run: cargo check 22 | 23 | test: 24 | name: Test Suite 25 | runs-on: ubuntu-latest 26 | needs: update 27 | steps: 28 | - uses: actions/checkout@v3 29 | - name: Update apt 30 | run: sudo apt update 31 | - name: Install libudev 32 | run: sudo apt install libudev-dev 33 | - name: Install libxkbcommon 34 | run: sudo apt install libxkbcommon-dev 35 | - name: Install libwayland 36 | run: sudo apt install libwayland-dev 37 | - uses: dtolnay/rust-toolchain@stable 38 | - run: cargo test 39 | 40 | fmt: 41 | name: Rustfmt 42 | runs-on: ubuntu-latest 43 | steps: 44 | - uses: actions/checkout@v3 45 | - uses: dtolnay/rust-toolchain@stable 46 | - run: rustup component add rustfmt 47 | - run: cargo fmt --all -- --check 48 | 49 | clippy: 50 | name: Clippy 51 | runs-on: ubuntu-latest 52 | needs: update 53 | steps: 54 | - uses: actions/checkout@v3 55 | - uses: dtolnay/rust-toolchain@stable 56 | - run: rustup component add clippy 57 | - run: cargo clippy -- -D warnings 58 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target/ 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # IDE 13 | .idea/ 14 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_flycam" 3 | version = "0.15.0" 4 | authors = ["Spencer Burris "] 5 | edition = "2021" 6 | license = "ISC" 7 | description = "Basic first-person fly camera for the Bevy game engine" 8 | homepage = "https://github.com/sburris0/bevy_flycam/" 9 | repository = "https://github.com/sburris0/bevy_flycam/" 10 | readme = "README.md" 11 | keywords = ["gamedev", "bevy", "3d", "camera"] 12 | categories = ["game-engines", "game-development"] 13 | 14 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 15 | [dependencies] 16 | bevy = { version = "0.15", default-features = false, features = [ 17 | "bevy_asset", 18 | "bevy_core_pipeline", 19 | "bevy_render", 20 | "bevy_window", 21 | ] } 22 | 23 | [dev-dependencies] 24 | bevy = { version = "0.15", default-features = false, features = [ 25 | "bevy_asset", 26 | "bevy_core_pipeline", 27 | "bevy_pbr", 28 | "bevy_state", 29 | "bevy_window", 30 | "ktx2", 31 | "tonemapping_luts", 32 | "wayland", 33 | "x11", 34 | "zstd", 35 | ] } 36 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Spencer Burris 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. 4 | 5 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_flycam 2 | 3 | [![Crates.io](https://img.shields.io/crates/v/bevy_flycam)](https://crates.io/crates/bevy_flycam) 4 | ![Crates.io](https://img.shields.io/crates/l/bevy_flycam) 5 | ![docs.rs](https://img.shields.io/docsrs/bevy_flycam) 6 | 7 | A basic first-person fly camera for Bevy 0.14 8 | 9 | ## Controls 10 | 11 | - WASD to move horizontally 12 | - SPACE to ascend 13 | - LSHIFT to descend 14 | - ESC to grab/release cursor. 15 | 16 | ## Comparison 17 | 18 | There are a few notable differences from [bevy_fly_camera](https://github.com/mcpar-land/bevy_fly_camera)... 19 | 20 | - No linear interpolation 21 | - Cursor grabbing 22 | - Shorter code 23 | - Single-line setup 24 | - A tiny bit faster? 25 | 26 | ## Usage 27 | 28 | 1. Add to `Cargo.toml` or copy `lib.rs` to your own file 29 | 30 | ```toml 31 | [dependencies] 32 | bevy = "0.15" 33 | bevy_flycam = "*" 34 | ``` 35 | 36 | or 37 | 38 | ```toml 39 | [dependencies] 40 | bevy = "0.15" 41 | bevy_flycam = { git = "https://github.com/sburris0/bevy_flycam" } 42 | ``` 43 | 44 | 2. Include the prelude: 45 | 46 | ```rust 47 | use bevy_flycam::prelude::*; 48 | ``` 49 | 50 | 3. Add the `PlayerPlugin`: 51 | 52 | ```rust 53 | #[bevy_main] 54 | fn main() { 55 | App::new() 56 | .add_plugins(DefaultPlugins) 57 | .add_plugins(PlayerPlugin) 58 | .run(); 59 | } 60 | ``` 61 | 62 | Note that `PlayerPlugin` will spawn a camera for you. See [Using your own camera](#using-your-own-camera) for details on how to 63 | use a pre-existing one. 64 | 65 | Alternatively you can see the example `basic.rs` or `scroll.rs` located in the examples folder. 66 | You can run the example by cloning this repository and run the command: `cargo run --release --example basic` 67 | 68 | ## Customization 69 | 70 | ### Movement and keybindings 71 | 72 | To modify player movement speed or mouse sensitivity add it as a resource.
73 | Same thing goes for the keybindings used for moving the camera. 74 | 75 | ```Rust 76 | #[bevy_main] 77 | fn main() { 78 | App::new() 79 | .add_plugins(DefaultPlugins) 80 | .add_plugins(PlayerPlugin) 81 | .insert_resource(MovementSettings { 82 | sensitivity: 0.00015, // default: 0.00012 83 | speed: 12.0, // default: 12.0 84 | }) 85 | .insert_resource(KeyBindings { 86 | move_ascend: KeyCode::E, 87 | move_descend: KeyCode::Q, 88 | ..Default::default() 89 | }) 90 | .run(); 91 | } 92 | ``` 93 | 94 | ### Using your own camera 95 | 96 | You can also use `NoCameraPlayerPlugin` if you want to use your own camera. Be sure to add the `FlyCam` component to your own camera or else this plugin won't know what to move. 97 | 98 | ```Rust 99 | #[bevy_main] 100 | fn main() { 101 | App::new() 102 | .add_plugins(DefaultPlugins) 103 | .add_plugin(NoCameraPlayerPlugin) 104 | .add_systems(Startup, setup) 105 | .run(); 106 | } 107 | 108 | fn setup(mut commands: Commands) { 109 | commands.spawn(( 110 | Camera3dBundle { 111 | transform: Transform::from_xyz(0.0, 2.0, 0.5), 112 | ..default() 113 | }, 114 | FlyCam 115 | )); 116 | } 117 | ``` 118 | 119 | ## Support 120 | 121 | [![Bevy tracking](https://img.shields.io/badge/Bevy%20tracking-released%20version-lightblue)](https://github.com/bevyengine/bevy/blob/main/docs/plugins_guidelines.md#main-branch-tracking) 122 | 123 | bevy_flycam's crate version follows bevy's minor version as shown: 124 | | bevy | bevy_flycam | 125 | | :-- | :-- | 126 | | `0.15.0` | `0.15.0` | 127 | | `0.14.0` | `0.14.0` | 128 | | `0.13.0` | `0.13.0` | 129 | | `0.12.0` | `0.12.0` | 130 | | `0.11.0` | `0.11.0` | 131 | | `0.10.1` | `0.10.1` | 132 | 133 | ## Contributing 134 | 135 | PRs are very welcome. 136 | -------------------------------------------------------------------------------- /examples/basic.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_flycam::prelude::*; 3 | 4 | //From bevy examples: 5 | //https://github.com/bevyengine/bevy/blob/latest/examples/3d/3d_scene.rs 6 | 7 | fn main() { 8 | App::new() 9 | .add_plugins(DefaultPlugins) 10 | .add_plugins(PlayerPlugin) 11 | .insert_resource(MovementSettings { 12 | sensitivity: 0.00015, // default: 0.00012 13 | speed: 12.0, // default: 12.0 14 | }) 15 | .add_systems(Startup, setup) 16 | .run(); 17 | } 18 | 19 | /// set up a simple 3D scene 20 | fn setup( 21 | mut commands: Commands, 22 | mut meshes: ResMut>, 23 | mut materials: ResMut>, 24 | ) { 25 | // plane 26 | commands.spawn(( 27 | Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(2.5)))), 28 | MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), 29 | )); 30 | 31 | // cube 32 | commands.spawn(( 33 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 34 | MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), 35 | Transform::from_xyz(0.0, 0.5, 0.0), 36 | )); 37 | 38 | // light 39 | commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 8.0, 4.0))); 40 | 41 | info!("Move camera around by using WASD for lateral movement"); 42 | info!("Use Left Shift and Spacebar for vertical movement"); 43 | info!("Use the mouse to look around"); 44 | info!("Press Esc to hide or show the mouse cursor"); 45 | } 46 | -------------------------------------------------------------------------------- /examples/key_bindings.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_flycam::prelude::*; 3 | 4 | //From bevy examples: 5 | //https://github.com/bevyengine/bevy/blob/latest/examples/3d/3d_scene.rs 6 | 7 | fn main() { 8 | App::new() 9 | .add_plugins(DefaultPlugins) 10 | .add_plugins(PlayerPlugin) 11 | .insert_resource(MovementSettings { 12 | sensitivity: 0.00015, // default: 0.00012 13 | speed: 12.0, // default: 12.0 14 | }) 15 | // Unreal movement layout 16 | .insert_resource(KeyBindings { 17 | move_ascend: KeyCode::KeyE, 18 | move_descend: KeyCode::KeyQ, 19 | ..Default::default() 20 | }) 21 | .add_systems(Startup, setup) 22 | .run(); 23 | } 24 | 25 | /// set up a simple 3D scene 26 | fn setup( 27 | mut commands: Commands, 28 | mut meshes: ResMut>, 29 | mut materials: ResMut>, 30 | ) { 31 | // plane 32 | commands.spawn(( 33 | Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(2.5)))), 34 | MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), 35 | )); 36 | 37 | // cube 38 | commands.spawn(( 39 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 40 | MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), 41 | Transform::from_xyz(0.0, 0.5, 0.0), 42 | )); 43 | 44 | // light 45 | commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 8.0, 4.0))); 46 | } 47 | -------------------------------------------------------------------------------- /examples/scroll.rs: -------------------------------------------------------------------------------- 1 | use bevy::{input::mouse::MouseWheel, prelude::*}; 2 | use bevy_flycam::prelude::*; 3 | 4 | // From bevy examples: 5 | // https://github.com/bevyengine/bevy/blob/latest/examples/3d/3d_scene.rs 6 | 7 | #[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)] 8 | enum ScrollType { 9 | #[default] 10 | MovementSpeed, 11 | Zoom, 12 | } 13 | 14 | fn main() { 15 | App::new() 16 | .add_plugins(DefaultPlugins) 17 | //NoCameraPlayerPlugin as we provide the camera 18 | .add_plugins(NoCameraPlayerPlugin) 19 | .insert_resource(MovementSettings { 20 | ..Default::default() 21 | }) 22 | // Setting initial state 23 | .init_state::() 24 | .add_systems(Startup, setup) 25 | .add_systems(Update, switch_scroll_type) 26 | .add_systems(Update, scroll) 27 | .run(); 28 | } 29 | 30 | /// set up a simple 3D scene 31 | fn setup( 32 | mut commands: Commands, 33 | mut meshes: ResMut>, 34 | mut materials: ResMut>, 35 | ) { 36 | // plane 37 | commands.spawn(( 38 | Mesh3d(meshes.add(Plane3d::new(Vec3::Y, Vec2::splat(2.5)))), 39 | MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), 40 | )); 41 | 42 | // cube 43 | commands.spawn(( 44 | Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))), 45 | MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))), 46 | Transform::from_xyz(0.0, 0.5, 0.0), 47 | )); 48 | 49 | // light 50 | // light 51 | commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 8.0, 4.0))); 52 | 53 | // add camera 54 | commands.spawn(( 55 | Camera3d::default(), 56 | Transform::from_xyz(-2.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), 57 | FlyCam, 58 | )); 59 | 60 | info!("Press 'Z' to switch between Movement Speed and Zoom"); 61 | info!("Changing the selected value by scrolling the mousewheel"); 62 | } 63 | 64 | /// Listens for Z key being pressed and toggles between the two scroll-type states [`ScrollType`] 65 | fn switch_scroll_type( 66 | scroll_type: Res>, 67 | mut next_scroll_type: ResMut>, 68 | keyboard_input: Res>, 69 | ) { 70 | if keyboard_input.just_pressed(KeyCode::KeyZ) { 71 | let result = match scroll_type.get() { 72 | ScrollType::MovementSpeed => ScrollType::Zoom, 73 | ScrollType::Zoom => ScrollType::MovementSpeed, 74 | }; 75 | 76 | println!("{:?}", result); 77 | next_scroll_type.set(result); 78 | } 79 | } 80 | 81 | /// Depending on the state, the mouse-scroll changes either the movement speed or the field-of-view of the camera 82 | fn scroll( 83 | mut settings: ResMut, 84 | scroll_type: Res>, 85 | mut mouse_wheel_events: EventReader, 86 | mut query: Query<(&FlyCam, &mut Projection)>, 87 | ) { 88 | for event in mouse_wheel_events.read() { 89 | if *scroll_type.get() == ScrollType::MovementSpeed { 90 | settings.speed = (settings.speed + event.y * 0.1).abs(); 91 | println!("Speed: {:?}", settings.speed); 92 | } else { 93 | for (_camera, project) in query.iter_mut() { 94 | if let Projection::Perspective(perspective) = project.into_inner() { 95 | perspective.fov = (perspective.fov - event.y * 0.01).abs(); 96 | println!("FOV: {:?}", perspective.fov); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | { pkgs ? import { } }: 2 | 3 | with pkgs; 4 | 5 | mkShell rec { 6 | nativeBuildInputs = [ 7 | pkg-config 8 | ]; 9 | buildInputs = [ 10 | udev alsa-lib vulkan-loader 11 | xorg.libX11 xorg.libXcursor xorg.libXi xorg.libXrandr # To use the x11 feature 12 | libxkbcommon wayland # To use the wayland feature 13 | ]; 14 | LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; 15 | } 16 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use bevy::input::mouse::MouseMotion; 2 | use bevy::prelude::*; 3 | use bevy::window::{CursorGrabMode, PrimaryWindow}; 4 | 5 | pub mod prelude { 6 | pub use crate::*; 7 | } 8 | 9 | /// Mouse sensitivity and movement speed 10 | #[derive(Resource)] 11 | pub struct MovementSettings { 12 | pub sensitivity: f32, 13 | pub speed: f32, 14 | } 15 | 16 | impl Default for MovementSettings { 17 | fn default() -> Self { 18 | Self { 19 | sensitivity: 0.00012, 20 | speed: 12., 21 | } 22 | } 23 | } 24 | 25 | /// Key configuration 26 | #[derive(Resource)] 27 | pub struct KeyBindings { 28 | pub move_forward: KeyCode, 29 | pub move_backward: KeyCode, 30 | pub move_left: KeyCode, 31 | pub move_right: KeyCode, 32 | pub move_ascend: KeyCode, 33 | pub move_descend: KeyCode, 34 | pub toggle_grab_cursor: KeyCode, 35 | } 36 | 37 | impl Default for KeyBindings { 38 | fn default() -> Self { 39 | Self { 40 | move_forward: KeyCode::KeyW, 41 | move_backward: KeyCode::KeyS, 42 | move_left: KeyCode::KeyA, 43 | move_right: KeyCode::KeyD, 44 | move_ascend: KeyCode::Space, 45 | move_descend: KeyCode::ShiftLeft, 46 | toggle_grab_cursor: KeyCode::Escape, 47 | } 48 | } 49 | } 50 | 51 | /// Used in queries when you want flycams and not other cameras 52 | /// A marker component used in queries when you want flycams and not other cameras 53 | #[derive(Component)] 54 | pub struct FlyCam; 55 | 56 | /// Grabs/ungrabs mouse cursor 57 | fn toggle_grab_cursor(window: &mut Window) { 58 | match window.cursor_options.grab_mode { 59 | CursorGrabMode::None => { 60 | window.cursor_options.grab_mode = CursorGrabMode::Confined; 61 | window.cursor_options.visible = false; 62 | } 63 | _ => { 64 | window.cursor_options.grab_mode = CursorGrabMode::None; 65 | window.cursor_options.visible = true; 66 | } 67 | } 68 | } 69 | 70 | /// Grabs the cursor when game first starts 71 | fn initial_grab_cursor(mut primary_window: Query<&mut Window, With>) { 72 | if let Ok(mut window) = primary_window.get_single_mut() { 73 | toggle_grab_cursor(&mut window); 74 | } else { 75 | warn!("Primary window not found for `initial_grab_cursor`!"); 76 | } 77 | } 78 | 79 | /// Spawns the `Camera3dBundle` to be controlled 80 | fn setup_player(mut commands: Commands) { 81 | commands.spawn(( 82 | Camera3d::default(), 83 | FlyCam, 84 | Transform::from_xyz(-2.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), 85 | )); 86 | } 87 | 88 | /// Handles keyboard input and movement 89 | fn player_move( 90 | keys: Res>, 91 | time: Res