├── .gitignore ├── assets ├── mire-64x64.png ├── flappin-bird.png ├── flappin-clouds.png ├── flappin-pillars.png ├── flappin-bird.aseprite ├── flappin-clouds.aseprite └── flappin-pillars.aseprite ├── LICENSE-MIT ├── Cargo.toml ├── src ├── pixel_plugin.rs ├── pixel_border.rs ├── lib.rs ├── pixel_zoom.rs └── pixel_camera.rs ├── examples ├── mire.rs └── flappin.rs ├── README.md └── LICENSE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | -------------------------------------------------------------------------------- /assets/mire-64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakmaniso/bevy_pixel_camera/HEAD/assets/mire-64x64.png -------------------------------------------------------------------------------- /assets/flappin-bird.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakmaniso/bevy_pixel_camera/HEAD/assets/flappin-bird.png -------------------------------------------------------------------------------- /assets/flappin-clouds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakmaniso/bevy_pixel_camera/HEAD/assets/flappin-clouds.png -------------------------------------------------------------------------------- /assets/flappin-pillars.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakmaniso/bevy_pixel_camera/HEAD/assets/flappin-pillars.png -------------------------------------------------------------------------------- /assets/flappin-bird.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakmaniso/bevy_pixel_camera/HEAD/assets/flappin-bird.aseprite -------------------------------------------------------------------------------- /assets/flappin-clouds.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakmaniso/bevy_pixel_camera/HEAD/assets/flappin-clouds.aseprite -------------------------------------------------------------------------------- /assets/flappin-pillars.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drakmaniso/bevy_pixel_camera/HEAD/assets/flappin-pillars.aseprite -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining a copy of 2 | this software and associated documentation files (the "Software"), to deal in 3 | the Software without restriction, including without limitation the rights to 4 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 5 | of the Software, and to permit persons to whom the Software is furnished to do 6 | so, subject to the following conditions: 7 | 8 | The above copyright notice and this permission notice shall be included in all 9 | copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 | SOFTWARE. -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bevy_pixel_camera" 3 | version = "0.13.0" 4 | authors = ["drakmaniso "] 5 | edition = "2021" 6 | description = "A simple pixel-perfect camera plugin for Bevy, suitable for pixel-art" 7 | readme = "README.md" 8 | repository = "https://github.com/drakmaniso/bevy_pixel_camera" 9 | keywords = ["bevy", "graphics", "2d", "pixels", "pixel-art"] 10 | categories = ["graphics", "game-development"] 11 | exclude = ["assets/**", ".vscode/**"] 12 | license = "MIT OR Apache-2.0" 13 | 14 | [dependencies] 15 | bevy = { version = "0.13", default-features = false, features = [ 16 | "bevy_core_pipeline", 17 | "bevy_render", 18 | "bevy_sprite", 19 | ] } 20 | 21 | [dev-dependencies] 22 | bevy = { version = "0.13", default-features = false, features = [ 23 | "bevy_winit", 24 | "bevy_asset", 25 | "png", 26 | "multi-threaded", 27 | "x11", 28 | ] } 29 | 30 | [[example]] 31 | name = "flappin" 32 | required-features = ["bevy/bevy_winit", "bevy/bevy_asset", "bevy/png"] 33 | 34 | [[example]] 35 | name = "mire" 36 | required-features = ["bevy/bevy_winit", "bevy/bevy_asset", "bevy/png"] 37 | -------------------------------------------------------------------------------- /src/pixel_plugin.rs: -------------------------------------------------------------------------------- 1 | #[allow(deprecated)] 2 | use super::PixelProjection; 3 | 4 | use bevy::prelude::{App, IntoSystemConfigs, Plugin, PostUpdate}; 5 | use bevy::render::camera::{ 6 | self, Camera, OrthographicProjection, PerspectiveProjection, Projection, ScalingMode, 7 | }; 8 | use bevy::render::primitives::Aabb; 9 | use bevy::render::view::visibility; 10 | use bevy::render::view::{InheritedVisibility, Visibility, VisibleEntities}; 11 | use bevy::transform::TransformSystem; 12 | 13 | /// Provides the camera system. 14 | pub struct PixelCameraPlugin; 15 | 16 | #[allow(deprecated)] 17 | impl Plugin for PixelCameraPlugin { 18 | fn build(&self, app: &mut App) { 19 | app.register_type::() 20 | .register_type::() 21 | .register_type::() 22 | .register_type::() 23 | .register_type::() 24 | .register_type::() 25 | .register_type::() 26 | .add_systems(PostUpdate, super::update_pixel_camera_viewport) 27 | .add_systems(PostUpdate, camera::camera_system::) 28 | .add_systems( 29 | PostUpdate, 30 | visibility::update_frusta:: 31 | .in_set(visibility::VisibilitySystems::UpdateOrthographicFrusta) 32 | .after(camera::camera_system::) 33 | .after(TransformSystem::TransformPropagate) 34 | .ambiguous_with(visibility::update_frusta::) 35 | .ambiguous_with(visibility::update_frusta::) 36 | .ambiguous_with(visibility::update_frusta::), 37 | ) 38 | .add_systems( 39 | PostUpdate, 40 | super::pixel_zoom_system.after(camera::camera_system::), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /examples/mire.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy_pixel_camera::{PixelCameraPlugin, PixelViewport, PixelZoom}; 3 | 4 | const WIDTH: i32 = 320; 5 | const HEIGHT: i32 = 180; 6 | 7 | fn main() { 8 | App::new() 9 | .insert_resource(ClearColor(Color::rgb(0.2, 0.2, 0.2))) 10 | .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) 11 | .add_plugins(PixelCameraPlugin) 12 | .add_systems(Startup, setup) 13 | .add_systems(Update, bevy::window::close_on_esc) 14 | .run(); 15 | } 16 | 17 | fn setup(mut commands: Commands, asset_server: Res) { 18 | // Add a camera that will always fit the virtual resolution WIDTH x HEIGHT 19 | // inside the window. 20 | commands.spawn(( 21 | Camera2dBundle::default(), 22 | PixelZoom::FitSize { 23 | width: WIDTH, 24 | height: HEIGHT, 25 | }, 26 | PixelViewport, 27 | )); 28 | 29 | let mire_handle = asset_server.load("mire-64x64.png"); 30 | 31 | // Add a mire sprite in the center of the window. 32 | commands.spawn(SpriteBundle { 33 | texture: mire_handle.clone(), 34 | transform: Transform::from_translation(Vec3::new(0.0, 0.0, 0.0)), 35 | ..Default::default() 36 | }); 37 | 38 | // Add a mire sprite in the bottom-left corner of our virtual resolution. 39 | commands.spawn(SpriteBundle { 40 | texture: mire_handle.clone(), 41 | transform: Transform::from_translation(Vec3::new( 42 | -(WIDTH / 2) as f32, 43 | -(HEIGHT / 2) as f32, 44 | 0.0, 45 | )), 46 | ..Default::default() 47 | }); 48 | 49 | // Add a mire sprite in the bottom-right corner of our virtual resolution. 50 | commands.spawn(SpriteBundle { 51 | texture: mire_handle.clone(), 52 | transform: Transform::from_translation(Vec3::new( 53 | (WIDTH / 2) as f32, 54 | -(HEIGHT / 2) as f32, 55 | 0.0, 56 | )), 57 | ..Default::default() 58 | }); 59 | 60 | // Add a mire sprite in the top-left corner of our virtual resolution. 61 | commands.spawn(SpriteBundle { 62 | texture: mire_handle.clone(), 63 | transform: Transform::from_translation(Vec3::new( 64 | -(WIDTH / 2) as f32, 65 | (HEIGHT / 2) as f32, 66 | 0.0, 67 | )), 68 | ..Default::default() 69 | }); 70 | 71 | // Add a mire sprite in the top-right corner of our virtual resolution. 72 | commands.spawn(SpriteBundle { 73 | texture: mire_handle.clone(), 74 | transform: Transform::from_translation(Vec3::new( 75 | (WIDTH / 2) as f32, 76 | (HEIGHT / 2) as f32, 77 | 0.0, 78 | )), 79 | ..Default::default() 80 | }); 81 | } 82 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bevy_pixel_camera 2 | 3 | A simple camera plugin for the Bevy game engine, to help with the use of 4 | pixel-art sprites. 5 | 6 | This crates provides a plugin to automatically configure Bevy's 7 | `Camera2dBundle`. It works by setting the camera to an integer scaling 8 | factor (using Bevy's `ScalingMode::WindowSize`), and automatically updating 9 | the zoom level so that the specified target resolution fills as much of the 10 | sceen as possible. 11 | 12 | The plugin can also automatically set and resize the viewport of the camera 13 | to match the target resolution. 14 | 15 | ## Comparison with other methods 16 | 17 | There is two main methods to render pixel-art games: upscale each sprite 18 | independently, or render everything to an offscreen texture and only upscale 19 | this texture. This crate use the first method. There is advantages and 20 | drawbacks to both approaches. 21 | 22 | Advantages of the "upscale each sprite independently" method (i.e. this 23 | crate): 24 | 25 | - allows for smoother scrolling and movement of sprites, if you're willing 26 | to temporarily break the alignment on virtual pixels (this would be even 27 | more effective with a dedicated upscaling shader); 28 | - easier to mix pixel-art and high resolution graphics (for example for 29 | text, particles or effects). 30 | 31 | Advantages of the "offscreen texture" method: 32 | 33 | - always ensure perfect alignment on virtual pixels (authentic "retro" 34 | look); 35 | - may be more efficient (in most cases, the difference is probably 36 | negligible on modern computers). 37 | 38 | ## How to use 39 | 40 | Note that Bevy uses linear sampling by default for textures, which is not 41 | what you want for pixel art. The easiest way to change this is to configure 42 | Bevy's default plugins with `ImagePlugin::default_nearest()`. 43 | 44 | Also note that if either the width or the height of your sprite is not 45 | divisible by 2, you may need to change the anchor of the sprite (which is at 46 | the center by default), otherwise it won't be aligned with virtual pixels. 47 | 48 | ```rust 49 | use bevy::prelude::*; 50 | use bevy::sprite::Anchor; 51 | use bevy_pixel_camera::{ 52 | PixelCameraPlugin, PixelZoom, PixelViewport 53 | }; 54 | 55 | fn main() { 56 | App::new() 57 | .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) 58 | .add_plugins(PixelCameraPlugin) 59 | .add_systems(Startup, setup) 60 | .run(); 61 | } 62 | 63 | fn setup( 64 | mut commands: Commands, 65 | asset_server: Res, 66 | ) { 67 | commands.spawn(( 68 | Camera2dBundle::default(), 69 | PixelZoom::FitSize { 70 | width: 320, 71 | height: 180, 72 | }, 73 | PixelViewport, 74 | )); 75 | 76 | commands.spawn(SpriteBundle { 77 | texture: asset_server.load("my-pixel-art-sprite.png"), 78 | sprite: Sprite { 79 | anchor: Anchor::BottomLeft, 80 | ..Default::default() 81 | }, 82 | ..Default::default() 83 | }); 84 | } 85 | ``` 86 | 87 | A small example is included in the crate. Run it with: 88 | 89 | ```console 90 | cargo run --example flappin 91 | ``` 92 | 93 | ## Bevy versions supported 94 | 95 | | bevy | bevy_pixel_camera | 96 | |------|-------------------| 97 | | 0.13 | 0.13 | 98 | | 0.12 | 0.12 | 99 | | 0.11 | 0.5.2 | 100 | | 0.10 | 0.4.1 | 101 | | 0.9 | 0.3 | 102 | | 0.8 | 0.2 | 103 | 104 | ### Migration guide: 0.4 to 0.5 (Bevy 0.10 to 0.11) 105 | 106 | The `PixelBorderPlugin` has been deprecated. If you want a border around 107 | your virtual resolution, pass `true` to the `set_viewport` argument when 108 | creating the camera bundle (see example above). 109 | 110 | ### Migration guide: 0.5 to 0.12 (Bevy 0.11 to 0.12) 111 | 112 | The `PixelCameraBundle` has been deprecated. Replace it with a standard 113 | `Camera2dBundle`, to which you add the `PixelZoom` and `PixelViewport` 114 | components. 115 | 116 | ## License 117 | 118 | Licensed under either of 119 | 120 | - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 121 | ) 122 | - MIT license ([LICENSE-MIT](LICENSE-MIT) or 123 | ) 124 | 125 | at your option. 126 | 127 | License: MIT OR Apache-2.0 128 | -------------------------------------------------------------------------------- /src/pixel_border.rs: -------------------------------------------------------------------------------- 1 | #![deprecated(since = "0.5.0", note = "please use the `set_viewport` flag instead")] 2 | #![allow(deprecated)] 3 | 4 | use bevy::prelude::*; 5 | use bevy::sprite::Anchor; 6 | 7 | use crate::PixelProjection; 8 | 9 | /// Provides an opaque border around the desired resolution. 10 | pub struct PixelBorderPlugin { 11 | pub color: Color, 12 | } 13 | 14 | impl Plugin for PixelBorderPlugin { 15 | fn build(&self, app: &mut App) { 16 | app.insert_resource(BorderColor(self.color)) 17 | .add_systems(Startup, spawn_borders) 18 | .add_systems( 19 | PostUpdate, 20 | resize_borders 21 | .after(bevy::render::camera::camera_system::) 22 | .before(bevy::render::view::visibility::update_frusta::), 23 | ); 24 | } 25 | } 26 | 27 | /// Resource used to specify the color of the opaque border. 28 | #[derive(Clone, Debug, Resource)] 29 | pub struct BorderColor(Color); 30 | 31 | // Component 32 | #[derive(Component)] 33 | enum Border { 34 | Left, 35 | Right, 36 | Top, 37 | Bottom, 38 | } 39 | 40 | /// System to spawn the opaque border. Automatically added by the plugin as a 41 | /// startup system. 42 | pub fn spawn_borders(mut commands: Commands, color: Res) { 43 | let mut spawn_border = |name: &'static str, side: Border| -> Entity { 44 | commands 45 | .spawn(( 46 | Name::new(name), 47 | side, 48 | SpriteBundle { 49 | sprite: Sprite { 50 | anchor: Anchor::BottomLeft, 51 | color: color.0, 52 | ..Default::default() 53 | }, 54 | ..Default::default() 55 | }, 56 | )) 57 | .id() 58 | }; 59 | 60 | let left = spawn_border("Left", Border::Left); 61 | let right = spawn_border("Right", Border::Right); 62 | let top = spawn_border("Top", Border::Top); 63 | let bottom = spawn_border("Bottom", Border::Bottom); 64 | 65 | commands 66 | .spawn((SpatialBundle::default(), Name::new("Borders"))) 67 | .push_children(&[left, right, top, bottom]); 68 | } 69 | 70 | #[allow(clippy::type_complexity)] 71 | fn resize_borders( 72 | cameras: Query< 73 | (&PixelProjection, &GlobalTransform), 74 | Or<(Changed, Changed)>, 75 | >, 76 | mut borders: Query<(&mut Sprite, &mut Transform, &Border), Without>, 77 | ) { 78 | for (projection, transform) in cameras.iter() { 79 | let z = projection.far - 0.2; 80 | let width = projection.desired_width.map(|w| w as f32).unwrap_or(0.0); 81 | let height = projection.desired_height.map(|h| h as f32).unwrap_or(0.0); 82 | let left = transform.translation().x 83 | + if projection.centered { 84 | -(width / 2.0).round() 85 | } else { 86 | 0.0 87 | }; 88 | let right = left + width; 89 | let bottom = transform.translation().y 90 | + if projection.centered { 91 | (-height / 2.0).round() 92 | } else { 93 | 0.0 94 | }; 95 | let top = bottom + height; 96 | 97 | for (mut sprite, mut transform, border) in borders.iter_mut() { 98 | match border { 99 | Border::Left => { 100 | *transform = Transform::from_xyz(left - width, bottom - height, z); 101 | sprite.custom_size = Some(Vec2::new(width, 3.0 * height)); 102 | } 103 | Border::Right => { 104 | *transform = Transform::from_xyz(right, bottom - height, z); 105 | sprite.custom_size = Some(Vec2::new(width, 3.0 * height)); 106 | } 107 | Border::Top => { 108 | *transform = Transform::from_xyz(left - width, top, z); 109 | sprite.custom_size = Some(Vec2::new(3.0 * width, height)); 110 | } 111 | Border::Bottom => { 112 | *transform = Transform::from_xyz(left - width, bottom - height, z); 113 | sprite.custom_size = Some(Vec2::new(3.0 * width, height)); 114 | } 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! A simple camera plugin for the Bevy game engine, to help with the use of 2 | //! pixel-art sprites. 3 | //! 4 | //! This crates provides a plugin to automatically configure Bevy's 5 | //! `Camera2dBundle`. It works by setting the camera to an integer scaling 6 | //! factor (using Bevy's `ScalingMode::WindowSize`), and automatically updating 7 | //! the zoom level so that the specified target resolution fills as much of the 8 | //! sceen as possible. 9 | //! 10 | //! The plugin can also automatically set and resize the viewport of the camera 11 | //! to match the target resolution. 12 | //! 13 | //! # Comparison with other methods 14 | //! 15 | //! There is two main methods to render pixel-art games: upscale each sprite 16 | //! independently, or render everything to an offscreen texture and only upscale 17 | //! this texture. This crate use the first method. There is advantages and 18 | //! drawbacks to both approaches. 19 | //! 20 | //! Advantages of the "upscale each sprite independently" method (i.e. this 21 | //! crate): 22 | //! 23 | //! - allows for smoother scrolling and movement of sprites, if you're willing 24 | //! to temporarily break the alignment on virtual pixels (this would be even 25 | //! more effective with a dedicated upscaling shader); 26 | //! - easier to mix pixel-art and high resolution graphics (for example for 27 | //! text, particles or effects). 28 | //! 29 | //! Advantages of the "offscreen texture" method: 30 | //! 31 | //! - always ensure perfect alignment on virtual pixels (authentic "retro" 32 | //! look); 33 | //! - may be more efficient (in most cases, the difference is probably 34 | //! negligible on modern computers). 35 | //! 36 | //! # How to use 37 | //! 38 | //! Note that Bevy uses linear sampling by default for textures, which is not 39 | //! what you want for pixel art. The easiest way to change this is to configure 40 | //! Bevy's default plugins with `ImagePlugin::default_nearest()`. 41 | //! 42 | //! Also note that if either the width or the height of your sprite is not 43 | //! divisible by 2, you may need to change the anchor of the sprite (which is at 44 | //! the center by default), otherwise it won't be aligned with virtual pixels. 45 | //! 46 | //! ```no_run 47 | //! use bevy::prelude::*; 48 | //! use bevy::sprite::Anchor; 49 | //! use bevy_pixel_camera::{ 50 | //! PixelCameraPlugin, PixelZoom, PixelViewport 51 | //! }; 52 | //! 53 | //! fn main() { 54 | //! App::new() 55 | //! .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) 56 | //! .add_plugins(PixelCameraPlugin) 57 | //! .add_systems(Startup, setup) 58 | //! .run(); 59 | //! } 60 | //! 61 | //! fn setup( 62 | //! mut commands: Commands, 63 | //! asset_server: Res, 64 | //! ) { 65 | //! commands.spawn(( 66 | //! Camera2dBundle::default(), 67 | //! PixelZoom::FitSize { 68 | //! width: 320, 69 | //! height: 180, 70 | //! }, 71 | //! PixelViewport, 72 | //! )); 73 | //! 74 | //! commands.spawn(SpriteBundle { 75 | //! texture: asset_server.load("my-pixel-art-sprite.png"), 76 | //! sprite: Sprite { 77 | //! anchor: Anchor::BottomLeft, 78 | //! ..Default::default() 79 | //! }, 80 | //! ..Default::default() 81 | //! }); 82 | //! } 83 | //! ``` 84 | //! 85 | //! A small example is included in the crate. Run it with: 86 | //! 87 | //! ```console 88 | //! cargo run --example flappin 89 | //! ``` 90 | //! 91 | //! # Bevy versions supported 92 | //! 93 | //! | bevy | bevy_pixel_camera | 94 | //! |------|-------------------| 95 | //! | 0.13 | 0.13 | 96 | //! | 0.12 | 0.12 | 97 | //! | 0.11 | 0.5.2 | 98 | //! | 0.10 | 0.4.1 | 99 | //! | 0.9 | 0.3 | 100 | //! | 0.8 | 0.2 | 101 | //! 102 | //! ## Migration guide: 0.4 to 0.5 (Bevy 0.10 to 0.11) 103 | //! 104 | //! The `PixelBorderPlugin` has been deprecated. If you want a border around 105 | //! your virtual resolution, pass `true` to the `set_viewport` argument when 106 | //! creating the camera bundle (see example above). 107 | //! 108 | //! ## Migration guide: 0.5 to 0.12 (Bevy 0.11 to 0.12) 109 | //! 110 | //! The `PixelCameraBundle` has been deprecated. Replace it with a standard 111 | //! `Camera2dBundle`, to which you add the `PixelZoom` and `PixelViewport` 112 | //! components. 113 | //! 114 | //! # License 115 | //! 116 | //! Licensed under either of 117 | //! 118 | //! - Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or 119 | //! ) 120 | //! - MIT license ([LICENSE-MIT](LICENSE-MIT) or 121 | //! ) 122 | //! 123 | //! at your option. 124 | 125 | mod pixel_border; 126 | mod pixel_camera; 127 | mod pixel_plugin; 128 | mod pixel_zoom; 129 | 130 | #[allow(deprecated)] 131 | pub use pixel_border::*; 132 | #[allow(deprecated)] 133 | pub use pixel_camera::*; 134 | pub use pixel_plugin::*; 135 | pub use pixel_zoom::*; 136 | -------------------------------------------------------------------------------- /src/pixel_zoom.rs: -------------------------------------------------------------------------------- 1 | use bevy::{ 2 | prelude::*, 3 | render::camera::{NormalizedRenderTarget, ScalingMode, Viewport}, 4 | utils::HashSet, 5 | window::{PrimaryWindow, WindowCreated, WindowResized}, 6 | }; 7 | 8 | #[derive(Component, Debug, Clone, PartialEq)] 9 | /// Configure a `Camera2dBundle` to use integer scaling and automatically match 10 | /// a specified resolution. 11 | /// 12 | /// Note: when this component is present, a plugin system will automatically 13 | /// update the `ScalingMode` of the camera bundle. 14 | pub enum PixelZoom { 15 | /// Manually specify the camera zoom, i.e. the number of screen pixels 16 | /// (logical pixels) used to display one virtual pixel (world unit). 17 | Fixed(i32), 18 | /// Automatically set the camera zoom to fit the specified resolution inside 19 | /// the window. 20 | FitSize { width: i32, height: i32 }, 21 | /// Automatically set the camera zoom to fit the specified width inside the 22 | /// window. 23 | FitWidth(i32), 24 | /// Automatically set the camera zoom to fit the specified height inside the 25 | /// window. 26 | FitHeight(i32), 27 | } 28 | 29 | #[derive(Component, Debug, Clone, PartialEq)] 30 | /// Configure a `Camera2dBundle` to automatically set the viewport so that only 31 | /// pixels inside the desired resolution (as defined by the `PixelZoom` 32 | /// component) are displayed. 33 | pub struct PixelViewport; 34 | 35 | pub(crate) fn pixel_zoom_system( 36 | mut window_resized_events: EventReader, 37 | mut window_created_events: EventReader, 38 | mut image_asset_events: EventReader>, 39 | primary_window: Query>, 40 | mut cameras: Query<( 41 | &mut Camera, 42 | &PixelZoom, 43 | Option<&PixelViewport>, 44 | &mut OrthographicProjection, 45 | )>, 46 | ) { 47 | // Most of the change detection code is copied from `bevy_render/src/camera` 48 | 49 | // TODO: maybe this can be replaced with just monitoring 50 | // `OrthographicProjection` for changes? 51 | 52 | let primary_window = primary_window.iter().next(); 53 | 54 | let mut changed_window_ids = HashSet::new(); 55 | changed_window_ids.extend(window_created_events.read().map(|event| event.window)); 56 | changed_window_ids.extend(window_resized_events.read().map(|event| event.window)); 57 | 58 | let changed_image_handles: HashSet<&AssetId> = image_asset_events 59 | .read() 60 | .filter_map(|event| { 61 | if let AssetEvent::Modified { id } = event { 62 | Some(id) 63 | } else { 64 | None 65 | } 66 | }) 67 | .collect(); 68 | 69 | for (mut camera, pixel_zoom, pixel_viewport, mut projection) in &mut cameras { 70 | if let Some(normalized_target) = camera.target.normalize(primary_window) { 71 | if is_changed( 72 | &normalized_target, 73 | &changed_window_ids, 74 | &changed_image_handles, 75 | ) || camera.is_added() 76 | { 77 | let logical_size = match camera.logical_target_size() { 78 | Some(size) => size, 79 | None => continue, 80 | }; 81 | 82 | let physical_size = match camera.physical_target_size() { 83 | Some(size) => size, 84 | None => continue, 85 | }; 86 | 87 | let zoom = auto_zoom(pixel_zoom, logical_size) as f32; 88 | match projection.scaling_mode { 89 | ScalingMode::WindowSize(previous_zoom) => { 90 | if previous_zoom != zoom { 91 | projection.scaling_mode = ScalingMode::WindowSize(zoom) 92 | } 93 | } 94 | _ => projection.scaling_mode = ScalingMode::WindowSize(zoom), 95 | } 96 | 97 | if pixel_viewport.is_some() { 98 | set_viewport(&mut camera, pixel_zoom, zoom, physical_size, logical_size); 99 | } 100 | } 101 | } 102 | } 103 | } 104 | 105 | fn is_changed( 106 | render_target: &NormalizedRenderTarget, 107 | changed_window_ids: &HashSet, 108 | changed_image_handles: &HashSet<&AssetId>, 109 | ) -> bool { 110 | match render_target { 111 | NormalizedRenderTarget::Window(window_ref) => { 112 | changed_window_ids.contains(&window_ref.entity()) 113 | } 114 | NormalizedRenderTarget::Image(image_handle) => { 115 | changed_image_handles.contains(&image_handle.id()) 116 | } 117 | NormalizedRenderTarget::TextureView(_) => true, 118 | } 119 | } 120 | 121 | fn auto_zoom(mode: &PixelZoom, logical_size: Vec2) -> i32 { 122 | match mode { 123 | PixelZoom::FitSize { width, height } => { 124 | let zoom_x = (logical_size.x as i32) / i32::max(*width, 1); 125 | let zoom_y = (logical_size.y as i32) / i32::max(*height, 1); 126 | let zoom = i32::min(zoom_x, zoom_y); 127 | i32::max(zoom, 1) 128 | } 129 | PixelZoom::FitWidth(width) => { 130 | let zoom = (logical_size.x as i32) / i32::max(*width, 1); 131 | i32::max(zoom, 1) 132 | } 133 | PixelZoom::FitHeight(height) => { 134 | let zoom = (logical_size.y as i32) / i32::max(*height, 1); 135 | i32::max(zoom, 1) 136 | } 137 | PixelZoom::Fixed(zoom) => *zoom, 138 | } 139 | } 140 | 141 | fn set_viewport( 142 | camera: &mut Camera, 143 | mode: &PixelZoom, 144 | zoom: f32, 145 | physical_size: UVec2, 146 | logical_size: Vec2, 147 | ) { 148 | let (auto_width, auto_height) = match mode { 149 | PixelZoom::FitSize { width, height } => (Some(*width), Some(*height)), 150 | PixelZoom::FitWidth(width) => (Some(*width), None), 151 | PixelZoom::FitHeight(height) => (None, Some(*height)), 152 | PixelZoom::Fixed(..) => (None, None), 153 | }; 154 | 155 | let scale_factor = (physical_size.x as f32) / logical_size.x; 156 | 157 | let mut viewport_width = physical_size.x; 158 | let mut viewport_x = 0; 159 | if let Some(target_width) = auto_width { 160 | let logical_target_width = (target_width as f32) * zoom; 161 | viewport_width = (scale_factor * logical_target_width) as u32; 162 | viewport_x = (scale_factor * (logical_size.x - logical_target_width)) as u32 / 2; 163 | } 164 | 165 | let mut viewport_height = physical_size.y; 166 | let mut viewport_y = 0; 167 | if let Some(target_height) = auto_height { 168 | let logicat_target_height = (target_height as f32) * zoom; 169 | viewport_height = (scale_factor * logicat_target_height) as u32; 170 | viewport_y = (scale_factor * (logical_size.y - logicat_target_height)) as u32 / 2; 171 | } 172 | 173 | camera.viewport = Some(Viewport { 174 | physical_position: UVec2 { 175 | x: viewport_x, 176 | y: viewport_y, 177 | }, 178 | physical_size: UVec2 { 179 | x: viewport_width, 180 | y: viewport_height, 181 | }, 182 | ..Default::default() 183 | }); 184 | } 185 | -------------------------------------------------------------------------------- /src/pixel_camera.rs: -------------------------------------------------------------------------------- 1 | #![deprecated(since = "0.5.1", note = "please use the `PixelZoom` component instead")] 2 | #![allow(deprecated)] 3 | 4 | use bevy::math::Vec3A; 5 | use bevy::prelude::{ 6 | Bundle, Camera2d, Component, EventReader, GlobalTransform, Mat4, Query, Reflect, 7 | ReflectComponent, Transform, UVec2, With, 8 | }; 9 | use bevy::render::camera::{Camera, CameraProjection, CameraRenderGraph, Viewport}; 10 | use bevy::render::primitives::Frustum; 11 | use bevy::render::view::VisibleEntities; 12 | use bevy::window::{Window, WindowResized}; 13 | 14 | /// Provides the components for the camera entity. 15 | /// 16 | /// When using this camera, world coordinates are expressed using virtual 17 | /// pixels, which are always mapped to a multiple of actual screen pixels. 18 | #[derive(Bundle)] 19 | pub struct PixelCameraBundle { 20 | pub camera: Camera, 21 | pub camera_render_graph: CameraRenderGraph, 22 | pub pixel_projection: PixelProjection, 23 | pub visible_entities: VisibleEntities, 24 | pub frustum: Frustum, 25 | pub transform: Transform, 26 | pub global_transform: GlobalTransform, 27 | pub camera_2d: Camera2d, 28 | } 29 | 30 | impl PixelCameraBundle { 31 | /// Create a component bundle for a camera with the specified projection. 32 | pub fn new(pixel_projection: PixelProjection) -> Self { 33 | let transform = Transform::from_xyz(0.0, 0.0, 0.0); 34 | let view_projection = 35 | pixel_projection.get_projection_matrix() * transform.compute_matrix().inverse(); 36 | let frustum = Frustum::from_view_projection_custom_far( 37 | &view_projection, 38 | &transform.translation, 39 | &transform.back(), 40 | pixel_projection.far(), 41 | ); 42 | Self { 43 | camera_render_graph: CameraRenderGraph::new( 44 | bevy::core_pipeline::core_2d::graph::Core2d, 45 | ), 46 | pixel_projection, 47 | visible_entities: Default::default(), 48 | frustum, 49 | transform, 50 | global_transform: Default::default(), 51 | camera: Camera::default(), 52 | camera_2d: Camera2d, 53 | } 54 | } 55 | 56 | /// Create a component bundle for a camera where the size of virtual pixels 57 | /// are specified with `zoom`. 58 | pub fn from_zoom(zoom: i32) -> Self { 59 | Self::new(PixelProjection { 60 | zoom, 61 | ..Default::default() 62 | }) 63 | } 64 | 65 | /// Create a component bundle for a camera where the size of virtual pixels 66 | /// is automatically set to fit the specified resolution inside the window. 67 | /// 68 | /// If `set_viewport` is true, pixels outside of the desired resolution will 69 | /// not be displayed. This will automatically set the viewport of the 70 | /// camera, and resize it when necessary. 71 | pub fn from_resolution(width: i32, height: i32, set_viewport: bool) -> Self { 72 | Self::new(PixelProjection { 73 | desired_width: Some(width), 74 | desired_height: Some(height), 75 | set_viewport, 76 | ..Default::default() 77 | }) 78 | } 79 | 80 | /// Create a component bundle for a camera where the size of virtual pixels 81 | /// is automatically set to fit the specified width inside the window. 82 | /// 83 | /// If `set_viewport` is true, pixels outside of the desired width will 84 | /// not be displayed. This will automatically set the viewport of the 85 | /// camera, and resize it when necessary. 86 | pub fn from_width(width: i32, set_viewport: bool) -> Self { 87 | Self::new(PixelProjection { 88 | desired_width: Some(width), 89 | set_viewport, 90 | ..Default::default() 91 | }) 92 | } 93 | 94 | /// Create a component bundle for a camera where the size of virtual pixels 95 | /// is automatically set to fit the specified height inside the window. 96 | /// 97 | /// If `set_viewport` is true, pixels outside of the desired height will 98 | /// not be displayed. This will automatically set the viewport of the 99 | /// camera, and resize it when necessary. 100 | pub fn from_height(height: i32, set_viewport: bool) -> Self { 101 | Self::new(PixelProjection { 102 | desired_height: Some(height), 103 | set_viewport, 104 | ..Default::default() 105 | }) 106 | } 107 | } 108 | 109 | /// Component for a pixel-perfect orthographic projection. 110 | /// 111 | /// It is similar to Bevy's OrthographicProjection, except integral world 112 | /// coordinates are always aligned with virtual pixels (as defined by the zoom 113 | /// field). 114 | #[derive(Debug, Clone, Reflect, Component)] 115 | #[reflect(Component)] 116 | pub struct PixelProjection { 117 | pub left: f32, 118 | pub right: f32, 119 | pub bottom: f32, 120 | pub top: f32, 121 | pub near: f32, 122 | pub far: f32, 123 | 124 | /// If present, `zoom` will be automatically updated to always fit 125 | /// `desired_width` in the window as best as possible. 126 | pub desired_width: Option, 127 | 128 | /// If present, `zoom` will be automatically updated to always fit 129 | /// `desired_height` in the window as best as possible. 130 | pub desired_height: Option, 131 | 132 | /// If neither `desired_width` nor `desired_height` are present, zoom can be 133 | /// manually set. The value detemines the size of the virtual pixels. 134 | pub zoom: i32, 135 | 136 | /// If true, (0, 0) is the pixel closest to the center of the window, 137 | /// otherwise it's at bottom left. 138 | pub centered: bool, 139 | 140 | /// If true, pixels outside of the desired resolution will not be displayed. 141 | /// This will automatically set the viewport of the camera, and resize it 142 | /// when necessary. 143 | pub set_viewport: bool, 144 | } 145 | 146 | impl CameraProjection for PixelProjection { 147 | fn get_projection_matrix(&self) -> Mat4 { 148 | Mat4::orthographic_rh( 149 | self.left, 150 | self.right, 151 | self.bottom, 152 | self.top, 153 | // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0] 154 | // This is for interoperability with pipelines using infinite reverse perspective projections. 155 | self.far, 156 | self.near, 157 | ) 158 | } 159 | 160 | fn update(&mut self, width: f32, height: f32) { 161 | self.zoom = self.desired_zoom(width, height); 162 | 163 | let actual_width = width / (self.zoom as f32); 164 | let actual_height = height / (self.zoom as f32); 165 | if self.centered { 166 | self.left = -((actual_width as i32) / 2) as f32; 167 | self.right = self.left + actual_width; 168 | self.bottom = -((actual_height as i32) / 2) as f32; 169 | self.top = self.bottom + actual_height; 170 | } else { 171 | self.left = 0.0; 172 | self.right = actual_width; 173 | self.bottom = 0.0; 174 | self.top = actual_height; 175 | } 176 | } 177 | 178 | fn far(&self) -> f32 { 179 | self.far 180 | } 181 | 182 | fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { 183 | // NOTE: These vertices are in the specific order required by [`calculate_cascade`]. 184 | [ 185 | Vec3A::new(self.right, self.bottom, z_near), // bottom right 186 | Vec3A::new(self.right, self.top, z_near), // top right 187 | Vec3A::new(self.left, self.top, z_near), // top left 188 | Vec3A::new(self.left, self.bottom, z_near), // bottom left 189 | Vec3A::new(self.right, self.bottom, z_far), // bottom right 190 | Vec3A::new(self.right, self.top, z_far), // top right 191 | Vec3A::new(self.left, self.top, z_far), // top left 192 | Vec3A::new(self.left, self.bottom, z_far), // bottom left 193 | ] 194 | } 195 | } 196 | 197 | impl PixelProjection { 198 | pub fn desired_zoom(&self, width: f32, height: f32) -> i32 { 199 | let mut zoom_x = None; 200 | if let Some(desired_width) = self.desired_width { 201 | if desired_width > 0 { 202 | zoom_x = Some((width as i32) / desired_width); 203 | } 204 | } 205 | let mut zoom_y = None; 206 | if let Some(desired_height) = self.desired_height { 207 | if desired_height > 0 { 208 | zoom_y = Some((height as i32) / desired_height); 209 | } 210 | } 211 | match (zoom_x, zoom_y) { 212 | (Some(zoom_x), Some(zoom_y)) => zoom_x.min(zoom_y).max(1), 213 | (Some(zoom_x), None) => zoom_x.max(1), 214 | (None, Some(zoom_y)) => zoom_y.max(1), 215 | (None, None) => self.zoom, 216 | } 217 | } 218 | } 219 | 220 | impl Default for PixelProjection { 221 | fn default() -> Self { 222 | Self { 223 | left: -1.0, 224 | right: 1.0, 225 | bottom: -1.0, 226 | top: 1.0, 227 | near: -1000.0, 228 | far: 1000.0, 229 | desired_width: None, 230 | desired_height: None, 231 | zoom: 1, 232 | centered: true, 233 | set_viewport: false, 234 | } 235 | } 236 | } 237 | 238 | #[allow(clippy::type_complexity)] 239 | pub(crate) fn update_pixel_camera_viewport( 240 | mut resize_events: EventReader, 241 | windows: Query<&Window>, 242 | mut cameras: Query<(&mut Camera, &PixelProjection), With>, 243 | ) { 244 | for event in resize_events.read() { 245 | let window = windows.get(event.window).unwrap(); // TODO: better than unwrap? 246 | for (mut camera, projection) in cameras.iter_mut() { 247 | //TODO 248 | if projection.set_viewport { 249 | let zoom = projection.desired_zoom(event.width, event.height); 250 | let scale_factor = window.resolution.scale_factor() as f64; 251 | let viewport_width; 252 | let viewport_height; 253 | let viewport_x; 254 | let viewport_y; 255 | 256 | if let Some(target_width) = projection.desired_width { 257 | let logical_target_width = (target_width * zoom) as f64; 258 | viewport_width = (scale_factor * logical_target_width) as u32; 259 | viewport_x = 260 | (scale_factor * ((event.width as f64) - logical_target_width)) as u32 / 2; 261 | } else { 262 | viewport_width = window.physical_width(); 263 | viewport_x = 0; 264 | } 265 | if let Some(target_height) = projection.desired_height { 266 | let logicat_target_height = (target_height * zoom) as f64; 267 | viewport_height = (scale_factor * logicat_target_height) as u32; 268 | viewport_y = 269 | (scale_factor * ((event.height as f64) - logicat_target_height)) as u32 / 2; 270 | } else { 271 | viewport_height = window.physical_height(); 272 | viewport_y = 0; 273 | } 274 | camera.viewport = Some(Viewport { 275 | physical_position: UVec2 { 276 | x: viewport_x, 277 | y: viewport_y, 278 | }, 279 | physical_size: UVec2 { 280 | x: viewport_width, 281 | y: viewport_height, 282 | }, 283 | ..Default::default() 284 | }); 285 | } 286 | } 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /examples/flappin.rs: -------------------------------------------------------------------------------- 1 | use bevy::prelude::*; 2 | use bevy::sprite::Anchor; 3 | use bevy_pixel_camera::{PixelCameraPlugin, PixelViewport, PixelZoom}; 4 | 5 | // GAME CONSTANTS ///////////////////////////////////////////////////////////// 6 | 7 | const WIDTH: f32 = 240.0; 8 | const HEIGHT: f32 = 240.0; 9 | const LEFT: f32 = -WIDTH / 2.0; 10 | const RIGHT: f32 = LEFT + WIDTH; 11 | const BOTTOM: f32 = -HEIGHT / 2.0; 12 | const _TOP: f32 = BOTTOM + HEIGHT; 13 | 14 | const CLOUD_WIDTH: f32 = 66.0; 15 | const CLOUD_HEIGHT: f32 = 20.0; 16 | 17 | const PILLAR_WIDTH: f32 = 21.0; 18 | const PILLAR_HEIGHT: f32 = 482.0; 19 | const PILLAR_SPACING: f32 = 80.0; 20 | const PILLAR_GAP: f32 = 70.0; 21 | const PILLAR_RANGE: f32 = 105.0; 22 | 23 | const BIRD_X: f32 = -80.0; 24 | const BIRD_DX: f32 = 15.0; 25 | const BIRD_DY: f32 = 11.0; 26 | const BIRD_RADIUS: f32 = 6.0; 27 | 28 | const FALLING_JERK: f32 = -2300.0; 29 | const FLAP_VELOCITY: f32 = 100.0; 30 | const FLAP_ACCELERATION: f32 = 90.0; 31 | 32 | // SETUP ////////////////////////////////////////////////////////////////////// 33 | 34 | #[derive(States, Default, Clone, Eq, PartialEq, Hash, Debug)] 35 | enum GameState { 36 | #[default] 37 | StartScreen, 38 | Playing, 39 | GameOver, 40 | } 41 | 42 | fn main() { 43 | App::new() 44 | .init_state::() 45 | .add_plugins( 46 | DefaultPlugins 47 | .set(ImagePlugin::default_nearest()) 48 | .set(WindowPlugin { 49 | primary_window: Some(Window { 50 | title: "Flappin'".to_string(), 51 | // resolution: bevy::window::WindowResolution::default() 52 | // .with_scale_factor_override(1.0), 53 | ..default() 54 | }), 55 | ..default() 56 | }), 57 | ) 58 | .add_plugins(PixelCameraPlugin) 59 | .insert_resource(Rng { mz: 0, mw: 0 }) 60 | .insert_resource(ClearColor(Color::rgb(0.0, 0.0, 0.0))) 61 | .insert_resource(FlapTimer(Timer::from_seconds(0.5, TimerMode::Once))) 62 | .insert_resource(Action { 63 | just_pressed: false, 64 | }) 65 | .add_systems(Startup, setup) 66 | .add_systems(Startup, (spawn_bird, spawn_clouds).after(setup)) 67 | .add_systems(Update, bevy::window::close_on_esc) 68 | .add_systems(Update, on_press) 69 | .add_systems( 70 | Update, 71 | ( 72 | press_to_start, 73 | animate_flying_bird, 74 | animate_pillars, 75 | animate_clouds, 76 | ) 77 | .run_if(in_state(GameState::StartScreen)), 78 | ) 79 | .add_systems(OnEnter(GameState::Playing), spawn_pillars) 80 | .add_systems( 81 | Update, 82 | ( 83 | flap, 84 | animate_flappin_bird, 85 | collision_detection, 86 | animate_pillars, 87 | animate_clouds, 88 | ) 89 | .chain() 90 | .run_if(in_state(GameState::Playing)), 91 | ) 92 | .add_systems(OnEnter(GameState::GameOver), game_over) 93 | .add_systems(Update, press_to_start.run_if(in_state(GameState::GameOver))) 94 | .add_systems(OnExit(GameState::GameOver), despawn_pillars) 95 | .run(); 96 | } 97 | 98 | #[derive(Resource)] 99 | struct Textures { 100 | bird: Handle, 101 | bird_layout: Handle, 102 | pillars: Handle, 103 | clouds: Handle, 104 | clouds_layout: Handle, 105 | } 106 | 107 | fn setup( 108 | mut commands: Commands, 109 | time: Res