diff --git a/Cargo.toml b/Cargo.toml index ca60300..c281ebd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,22 +21,22 @@ anyhow = "1.0" event-listener = "5.3" serde = "1.0" line_drawing = { version = "1.0", optional = true } -seldom_singleton = "0.2.0" +seldom_singleton = "0.3.0" bevy_turborand = { version = "0.9.0", optional = true } seldom_map_nav = { version = "0.7.0", optional = true } seldom_pixel_macros = { version = "0.2.0-dev", path = "macros" } seldom_state = { version = "0.11.0", optional = true } [dependencies.bevy] -version = "0.14.0" +version = "0.15.0" default-features = false features = ["bevy_asset", "bevy_core_pipeline", "bevy_render", "bevy_sprite"] [dev-dependencies] -bevy = { version = "0.14.0", features = ["png"] } -leafwing-input-manager = "0.14.0" +bevy = "0.15.0" +# leafwing-input-manager = "0.14.0" rand = "0.8.5" -seldom_state = { version = "0.11.0", features = ["leafwing_input"] } +# seldom_state = { version = "0.11.0", features = ["leafwing_input"] } [[example]] name = "line" diff --git a/README.md b/README.md index e46a77f..fbe2fc3 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ need improvement, feel free to submit an issue or pr! ## Philosophies -- Assets are created through images +- Assets are normal images All assets, including filters, are loaded from images. `seldom_pixel`'s scope is limited to rendered things, so this doesn't apply to things like levels and sounds. I recommend @@ -41,7 +41,7 @@ about [Aseprite](https://github.com/aseprite/aseprite/), which you can use for f can compile it. I've only used this plugin on `.png` files, so I recommend using that format, but feel free to try it on other lossless formats. -- It is what it looks like +- What you see is what you get This crate's position component, `PxPosition`, uses an `IVec2` (2-dimensional `i32` vector) to store positions. This means that entities are located at exact pixel positions. @@ -52,7 +52,7 @@ which I recommend using when possible. I also recommend resetting the `SubPxPosi to `PxPosition`'s value when it stops moving, so moving objects feel consistent to the player. This is less of a concern for games with smaller pixels. -- Sacrifice versatility for productivity +- Trade versatility for productivity If you are already interested in making a limited color palette pixel art game, this is an easy win for you. Filters in `seldom_pixel` are just maps from each color diff --git a/examples/anchors.rs b/examples/anchors.rs index a09a8e0..cfdf603 100644 --- a/examples/anchors.rs +++ b/examples/anchors.rs @@ -21,30 +21,27 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Centered - commands.spawn(PxSpriteBundle:: { - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::new(8, 16).into(), - ..default() - }); + commands.spawn(( + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::new(8, 16)), + )); // Bottom Left - commands.spawn(PxSpriteBundle:: { - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::splat(16).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }); + commands.spawn(( + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::splat(16)), + PxAnchor::BottomLeft, + )); // Custom. Values range from 0 to 1, with the origin at the bottom left corner. - commands.spawn(PxSpriteBundle:: { - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::new(24, 16).into(), - anchor: Vec2::new(0.2, 0.8).into(), - ..default() - }); + commands.spawn(( + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::new(24, 16)), + PxAnchor::Custom(Vec2::new(0.2, 0.8)), + )); } #[px_layer] diff --git a/examples/animated_filters.rs b/examples/animated_filters.rs index 72e1056..ee8f986 100644 --- a/examples/animated_filters.rs +++ b/examples/animated_filters.rs @@ -21,19 +21,18 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let mage = assets.load("sprite/mage.px_sprite.png"); // Spawn a bunch of sprites on different layers for layer in 0..8 { - commands.spawn(PxSpriteBundle { - sprite: mage.clone(), - position: IVec2::new(layer % 4 * 13, layer / 4 * 18).into(), - anchor: PxAnchor::BottomLeft, - layer: Layer(layer), - ..default() - }); + commands.spawn(( + PxSprite(mage.clone()), + PxPosition(IVec2::new(layer % 4 * 13, layer / 4 * 18)), + PxAnchor::BottomLeft, + Layer(layer), + )); } // Load the filter @@ -41,22 +40,16 @@ fn init(assets: Res, mut commands: Commands) { // Despawn at the end commands.spawn(( - PxFilterBundle { - filter: fade_to_black.clone(), - layers: PxFilterLayers::single_clip(Layer(0)), - ..default() - }, - PxAnimationBundle::default(), + PxFilter(fade_to_black.clone()), + PxFilterLayers::single_clip(Layer(0)), + PxAnimation::default(), )); // Add the `PxAnimationFinished` component at the end commands.spawn(( - PxFilterBundle { - filter: fade_to_black.clone(), - layers: PxFilterLayers::single_clip(Layer(1)), - ..default() - }, - PxAnimationBundle { + PxFilter(fade_to_black.clone()), + PxFilterLayers::single_clip(Layer(1)), + PxAnimation { on_finish: PxAnimationFinishBehavior::Mark, ..default() }, @@ -64,12 +57,9 @@ fn init(assets: Res, mut commands: Commands) { // Loop commands.spawn(( - PxFilterBundle { - filter: fade_to_black.clone(), - layers: PxFilterLayers::single_clip(Layer(2)), - ..default() - }, - PxAnimationBundle { + PxFilter(fade_to_black.clone()), + PxFilterLayers::single_clip(Layer(2)), + PxAnimation { on_finish: PxAnimationFinishBehavior::Loop, ..default() }, @@ -77,12 +67,9 @@ fn init(assets: Res, mut commands: Commands) { // Backward commands.spawn(( - PxFilterBundle { - filter: fade_to_black.clone(), - layers: PxFilterLayers::single_clip(Layer(3)), - ..default() - }, - PxAnimationBundle { + PxFilter(fade_to_black.clone()), + PxFilterLayers::single_clip(Layer(3)), + PxAnimation { direction: PxAnimationDirection::Backward, on_finish: PxAnimationFinishBehavior::Loop, ..default() @@ -91,12 +78,9 @@ fn init(assets: Res, mut commands: Commands) { // Faster commands.spawn(( - PxFilterBundle { - filter: fade_to_black.clone(), - layers: PxFilterLayers::single_clip(Layer(5)), - ..default() - }, - PxAnimationBundle { + PxFilter(fade_to_black.clone()), + PxFilterLayers::single_clip(Layer(5)), + PxAnimation { duration: PxAnimationDuration::millis_per_animation(500), on_finish: PxAnimationFinishBehavior::Loop, ..default() @@ -105,12 +89,9 @@ fn init(assets: Res, mut commands: Commands) { // Slower commands.spawn(( - PxFilterBundle { - filter: fade_to_black.clone(), - layers: PxFilterLayers::single_clip(Layer(4)), - ..default() - }, - PxAnimationBundle { + PxFilter(fade_to_black.clone()), + PxFilterLayers::single_clip(Layer(4)), + PxAnimation { duration: PxAnimationDuration::millis_per_animation(2000), on_finish: PxAnimationFinishBehavior::Loop, ..default() @@ -119,12 +100,9 @@ fn init(assets: Res, mut commands: Commands) { // Duration per frame commands.spawn(( - PxFilterBundle { - filter: fade_to_black.clone(), - layers: PxFilterLayers::single_clip(Layer(6)), - ..default() - }, - PxAnimationBundle { + PxFilter(fade_to_black.clone()), + PxFilterLayers::single_clip(Layer(6)), + PxAnimation { duration: PxAnimationDuration::millis_per_frame(1000), on_finish: PxAnimationFinishBehavior::Loop, ..default() @@ -133,12 +111,9 @@ fn init(assets: Res, mut commands: Commands) { // Dither between frames commands.spawn(( - PxFilterBundle { - filter: fade_to_black, - layers: PxFilterLayers::single_clip(Layer(7)), - ..default() - }, - PxAnimationBundle { + PxFilter(fade_to_black), + PxFilterLayers::single_clip(Layer(7)), + PxAnimation { on_finish: PxAnimationFinishBehavior::Loop, frame_transition: PxAnimationFrameTransition::Dither, ..default() diff --git a/examples/animated_sprites.rs b/examples/animated_sprites.rs index e42c729..820055b 100644 --- a/examples/animated_sprites.rs +++ b/examples/animated_sprites.rs @@ -21,30 +21,24 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Load an animated sprite with `add_animated` let runner = assets.load("sprite/runner.px_sprite.png"); // Despawn at the end commands.spawn(( - PxSpriteBundle:: { - sprite: runner.clone(), - anchor: PxAnchor::BottomLeft, - ..default() - }, - PxAnimationBundle::default(), + PxSprite(runner.clone()), + PxAnchor::BottomLeft, + PxAnimation::default(), )); // Add the `PxAnimationFinished` component at the end commands.spawn(( - PxSpriteBundle:: { - sprite: runner.clone(), - position: IVec2::new(13, 0).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }, - PxAnimationBundle { + PxSprite(runner.clone()), + PxPosition(IVec2::new(13, 0)), + PxAnchor::BottomLeft, + PxAnimation { on_finish: PxAnimationFinishBehavior::Mark, ..default() }, @@ -52,13 +46,10 @@ fn init(assets: Res, mut commands: Commands) { // Loop commands.spawn(( - PxSpriteBundle:: { - sprite: runner.clone(), - position: IVec2::new(26, 0).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }, - PxAnimationBundle { + PxSprite(runner.clone()), + PxPosition(IVec2::new(26, 0)), + PxAnchor::BottomLeft, + PxAnimation { on_finish: PxAnimationFinishBehavior::Loop, ..default() }, @@ -66,13 +57,10 @@ fn init(assets: Res, mut commands: Commands) { // Backward commands.spawn(( - PxSpriteBundle:: { - sprite: runner.clone(), - position: IVec2::new(39, 0).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }, - PxAnimationBundle { + PxSprite(runner.clone()), + PxPosition(IVec2::new(39, 0)), + PxAnchor::BottomLeft, + PxAnimation { direction: PxAnimationDirection::Backward, on_finish: PxAnimationFinishBehavior::Loop, ..default() @@ -81,13 +69,10 @@ fn init(assets: Res, mut commands: Commands) { // Faster commands.spawn(( - PxSpriteBundle:: { - sprite: runner.clone(), - position: IVec2::new(13, 18).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }, - PxAnimationBundle { + PxSprite(runner.clone()), + PxPosition(IVec2::new(13, 18)), + PxAnchor::BottomLeft, + PxAnimation { duration: PxAnimationDuration::millis_per_animation(500), on_finish: PxAnimationFinishBehavior::Loop, ..default() @@ -96,13 +81,10 @@ fn init(assets: Res, mut commands: Commands) { // Slower commands.spawn(( - PxSpriteBundle:: { - sprite: runner.clone(), - position: IVec2::new(0, 18).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }, - PxAnimationBundle { + PxSprite(runner.clone()), + PxPosition(IVec2::new(0, 18)), + PxAnchor::BottomLeft, + PxAnimation { duration: PxAnimationDuration::millis_per_animation(2000), on_finish: PxAnimationFinishBehavior::Loop, ..default() @@ -111,13 +93,10 @@ fn init(assets: Res, mut commands: Commands) { // Duration per frame commands.spawn(( - PxSpriteBundle:: { - sprite: runner.clone(), - position: IVec2::new(26, 18).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }, - PxAnimationBundle { + PxSprite(runner.clone()), + PxPosition(IVec2::new(26, 18)), + PxAnchor::BottomLeft, + PxAnimation { duration: PxAnimationDuration::millis_per_frame(1000), on_finish: PxAnimationFinishBehavior::Loop, ..default() @@ -126,13 +105,10 @@ fn init(assets: Res, mut commands: Commands) { // Dither between frames commands.spawn(( - PxSpriteBundle:: { - sprite: runner, - position: IVec2::new(39, 18).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }, - PxAnimationBundle { + PxSprite(runner), + PxPosition(IVec2::new(39, 18)), + PxAnchor::BottomLeft, + PxAnimation { on_finish: PxAnimationFinishBehavior::Loop, frame_transition: PxAnimationFrameTransition::Dither, ..default() diff --git a/examples/animated_text.rs b/examples/animated_text.rs index 96fbfe5..ba3d152 100644 --- a/examples/animated_text.rs +++ b/examples/animated_text.rs @@ -21,20 +21,19 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let typeface = assets.load("typeface/animated_typeface.px_typeface.png"); // Spawn text commands.spawn(( - PxTextBundle:: { - text: "LOOPED ANIMATION ⭐🙂⭐".into(), + PxText { + value: "LOOPED ANIMATION ⭐🙂⭐".to_string(), typeface: typeface.clone(), - rect: IRect::new(0, 0, 64, 64).into(), - alignment: PxAnchor::TopCenter, - ..default() }, - PxAnimationBundle { + PxRect(IRect::new(0, 0, 64, 64)), + PxAnchor::TopCenter, + PxAnimation { // Use millis_per_animation to have each character loop at the same time duration: PxAnimationDuration::millis_per_frame(333), on_finish: PxAnimationFinishBehavior::Loop, @@ -43,14 +42,13 @@ fn init(assets: Res, mut commands: Commands) { )); commands.spawn(( - PxTextBundle:: { - text: "DITHERED ANIMATION 🙂⭐🙂".into(), + PxText { + value: "DITHERED ANIMATION 🙂⭐🙂".to_string(), typeface, - rect: IRect::new(0, 0, 64, 64).into(), - alignment: PxAnchor::BottomCenter, - ..default() }, - PxAnimationBundle { + PxRect(IRect::new(0, 0, 64, 64)), + PxAnchor::BottomCenter, + PxAnimation { // Use millis_per_animation to have each character loop at the same time duration: PxAnimationDuration::millis_per_frame(333), on_finish: PxAnimationFinishBehavior::Loop, diff --git a/examples/animated_tilemap.rs b/examples/animated_tilemap.rs index 7b34eb2..b6d0308 100644 --- a/examples/animated_tilemap.rs +++ b/examples/animated_tilemap.rs @@ -22,22 +22,15 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); - let mut map = PxMap::new(UVec2::new(2, 4)); + let mut tiles = PxTiles::new(UVec2::new(2, 4)); let mut rng = thread_rng(); for x in 0..2 { for y in 0..4 { - map.set( - Some( - commands - .spawn(PxTileBundle { - tile: rng.gen_range(0..4).into(), - ..default() - }) - .id(), - ), + tiles.set( + Some(commands.spawn(PxTile::from(rng.gen_range(0..4))).id()), UVec2::new(x, y), ); } @@ -47,12 +40,11 @@ fn init(assets: Res, mut commands: Commands) { // Spawn the map commands.spawn(( - PxMapBundle:: { - map: map.clone(), + PxMap { + tiles: tiles.clone(), tileset: tileset.clone(), - ..default() }, - PxAnimationBundle { + PxAnimation { // Use millis_per_animation to have each tile loop at the same time duration: PxAnimationDuration::millis_per_frame(250), on_finish: PxAnimationFinishBehavior::Loop, @@ -61,13 +53,9 @@ fn init(assets: Res, mut commands: Commands) { )); commands.spawn(( - PxMapBundle:: { - map, - tileset, - position: IVec2::new(8, 0).into(), - ..default() - }, - PxAnimationBundle { + PxMap { tiles, tileset }, + PxPosition(IVec2::new(8, 0)), + PxAnimation { // Use millis_per_animation to have each tile loop at the same time duration: PxAnimationDuration::millis_per_frame(250), on_finish: PxAnimationFinishBehavior::Loop, diff --git a/examples/buttons.rs b/examples/buttons.rs index 74ded45..dd83423 100644 --- a/examples/buttons.rs +++ b/examples/buttons.rs @@ -22,7 +22,7 @@ fn main() { } fn init(mut cursor: ResMut, assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let idle = assets.load("filter/invert.px_filter.png"); @@ -37,32 +37,26 @@ fn init(mut cursor: ResMut, assets: Res, mut commands: Co // Sprite-based button commands.spawn(( - PxSpriteBundle:: { - sprite: button_idle.clone(), - position: IVec2::new(8, 4).into(), - ..default() - }, - PxButtonSpriteBundle { - bounds: UVec2::new(8, 4).into(), - idle: button_idle.clone().into(), - hover: assets.load("sprite/button_hover.px_sprite.png").into(), - click: assets.load("sprite/button_click.px_sprite.png").into(), + PxSprite(button_idle.clone()), + PxPosition(IVec2::new(8, 4)), + PxInteractBounds::from(UVec2::new(8, 4)), + PxButtonSprite { + idle: button_idle.clone(), + hover: assets.load("sprite/button_hover.px_sprite.png"), + click: assets.load("sprite/button_click.px_sprite.png"), }, Button, )); // Filter-based button commands.spawn(( - PxSpriteBundle:: { - sprite: button_idle, - position: IVec2::new(8, 12).into(), - ..default() - }, - PxButtonFilterBundle { - bounds: UVec2::new(8, 4).into(), - idle: assets.load("filter/identity.px_filter.png").into(), - hover: assets.load("filter/hover.px_filter.png").into(), - click: assets.load("filter/click.px_filter.png").into(), + PxSprite(button_idle), + PxPosition(IVec2::new(8, 12)), + PxInteractBounds::from(UVec2::new(8, 4)), + PxButtonFilter { + idle: assets.load("filter/identity.px_filter.png"), + hover: assets.load("filter/hover.px_filter.png"), + click: assets.load("filter/click.px_filter.png"), }, Button, )); diff --git a/examples/canvas.rs b/examples/canvas.rs index ebbc822..d062273 100644 --- a/examples/canvas.rs +++ b/examples/canvas.rs @@ -24,7 +24,7 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // `PxSubPosition` contains a `Vec2`. This is used // to represent the camera's sub-pixel position, which is rounded and applied @@ -34,11 +34,8 @@ fn init(assets: Res, mut commands: Commands) { // By default, the mage is on the world canvas, which means you see it in different positions // based on where the camera is commands.spawn(( - PxSpriteBundle:: { - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::splat(32).into(), - ..default() - }, + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::splat(32)), Mage, )); } @@ -62,7 +59,7 @@ fn move_camera( ) .as_vec2() .normalize_or_zero() - * time.delta_seconds() + * time.delta_secs() * CAMERA_SPEED; **camera = camera_pos.round().as_ivec2(); diff --git a/examples/cursor.rs b/examples/cursor.rs index af90e68..b0d880c 100644 --- a/examples/cursor.rs +++ b/examples/cursor.rs @@ -21,7 +21,7 @@ fn main() { } fn init(mut cursor: ResMut, assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let idle = assets.load("filter/invert.px_filter.png"); @@ -34,11 +34,10 @@ fn init(mut cursor: ResMut, assets: Res, mut commands: Co }; // Sprite to show how the cursor's filter applies - commands.spawn(PxSpriteBundle:: { - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::new(8, 8).into(), - ..default() - }); + commands.spawn(( + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::new(8, 8)), + )); } #[px_layer] diff --git a/examples/filter.rs b/examples/filter.rs index 26f03a2..d88263f 100644 --- a/examples/filter.rs +++ b/examples/filter.rs @@ -21,28 +21,20 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let mage = assets.load("sprite/mage.px_sprite.png"); // Spawn some sprites - commands.spawn(PxSpriteBundle:: { - sprite: mage.clone(), - position: IVec2::new(8, 16).into(), - ..default() - }); - - commands.spawn(PxSpriteBundle:: { - sprite: mage, - position: IVec2::new(24, 16).into(), - ..default() - }); + commands.spawn((PxSprite(mage.clone()), PxPosition(IVec2::new(8, 16)))); + + commands.spawn((PxSprite(mage), PxPosition(IVec2::new(24, 16)))); // Spawn a filter - commands.spawn(PxFilterBundle:: { - filter: assets.load("filter/invert.px_filter.png"), - ..default() - }); + commands.spawn(( + PxFilterLayers::::default(), + PxFilter(assets.load("filter/invert.px_filter.png")), + )); } #[px_layer] diff --git a/examples/filter_layers.rs b/examples/filter_layers.rs index 1443f66..9a74249 100644 --- a/examples/filter_layers.rs +++ b/examples/filter_layers.rs @@ -23,11 +23,11 @@ fn main() { #[derive(Resource)] struct GameAssets { - invert: Handle, + invert: Handle, } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.insert_resource(GameAssets { invert: assets.load("filter/invert.px_filter.png"), @@ -36,32 +36,21 @@ fn init(assets: Res, mut commands: Commands) { let mage = assets.load("sprite/mage.px_sprite.png"); // Spawn some sprites on different layers - commands.spawn(PxSpriteBundle:: { - sprite: mage.clone(), - position: IVec2::new(8, 16).into(), - ..default() - }); + commands.spawn((PxSprite(mage.clone()), PxPosition(IVec2::new(8, 16)))); - commands.spawn(PxSpriteBundle:: { - sprite: mage.clone(), - position: IVec2::new(24, 16).into(), - layer: Layer::Middle(-1), - ..default() - }); + commands.spawn(( + PxSprite(mage.clone()), + PxPosition(IVec2::new(24, 16)), + Layer::Middle(-1), + )); - commands.spawn(PxSpriteBundle:: { - sprite: mage.clone(), - position: IVec2::new(40, 16).into(), - layer: Layer::Middle(1), - ..default() - }); + commands.spawn(( + PxSprite(mage.clone()), + PxPosition(IVec2::new(40, 16)), + Layer::Middle(1), + )); - commands.spawn(PxSpriteBundle:: { - sprite: mage, - position: IVec2::new(56, 16).into(), - layer: Layer::Front, - ..default() - }); + commands.spawn((PxSprite(mage), PxPosition(IVec2::new(56, 16)), Layer::Front)); } #[derive(Deref, DerefMut)] @@ -76,7 +65,7 @@ impl Default for CurrentFilter { fn change_filter( mut commands: Commands, mut current_filter: Local, - filters: Query>>, + filters: Query>, assets: Res, keys: Res>, ) { @@ -87,9 +76,9 @@ fn change_filter( commands.entity(filter).despawn(); } - commands.spawn(PxFilterBundle { - filter: assets.invert.clone(), - layers: match **current_filter { + commands.spawn(( + PxFilter(assets.invert.clone()), + match **current_filter { // Filters the Middle(-1) layer specifically 0 => PxFilterLayers::single_clip(Layer::Middle(-1)), // Filters the screen's image after rendering Middle(0) @@ -101,8 +90,7 @@ fn change_filter( 3 => (|layer: &Layer| matches!(layer, Layer::Middle(layer) if *layer >= 0)).into(), _ => unreachable!(), }, - ..default() - }); + )); } } diff --git a/examples/layers.rs b/examples/layers.rs index b812587..8d94eeb 100644 --- a/examples/layers.rs +++ b/examples/layers.rs @@ -21,37 +21,26 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let mage = assets.load("sprite/mage.px_sprite.png"); // Spawn some sprites on different layers - commands.spawn(PxSpriteBundle:: { - sprite: mage.clone(), - position: IVec2::new(10, 22).into(), - ..default() - }); + commands.spawn((PxSprite(mage.clone()), PxPosition(IVec2::new(10, 22)))); - commands.spawn(PxSpriteBundle:: { - sprite: mage.clone(), - position: IVec2::new(14, 18).into(), - layer: Layer::Middle(-1), - ..default() - }); + commands.spawn(( + PxSprite(mage.clone()), + PxPosition(IVec2::new(14, 18)), + Layer::Middle(-1), + )); - commands.spawn(PxSpriteBundle:: { - sprite: mage.clone(), - position: IVec2::new(18, 14).into(), - layer: Layer::Middle(1), - ..default() - }); + commands.spawn(( + PxSprite(mage.clone()), + PxPosition(IVec2::new(18, 14)), + Layer::Middle(1), + )); - commands.spawn(PxSpriteBundle:: { - sprite: mage, - position: IVec2::new(22, 10).into(), - layer: Layer::Front, - ..default() - }); + commands.spawn((PxSprite(mage), PxPosition(IVec2::new(22, 10)), Layer::Front)); } // Layers are in render order: back to front diff --git a/examples/palette_change.rs b/examples/palette_change.rs index 6ef78b9..e46396a 100644 --- a/examples/palette_change.rs +++ b/examples/palette_change.rs @@ -38,7 +38,7 @@ struct GameAssets { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.insert_resource(GameAssets { palette_1: assets.load("palette/palette_1.palette.png"), @@ -49,15 +49,14 @@ fn init(assets: Res, mut commands: Commands) { fn spawn_mage(keys: Res>, assets: Res, mut commands: Commands) { if keys.just_pressed(KeyCode::Space) { let mut rng = thread_rng(); - commands.spawn(PxSpriteBundle:: { + commands.spawn(( // Usually, this sprite would be added in `init` to avoid duplicating data, // but it's here instead to show that loading assets is independent // of the current palette - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::new(rng.gen_range(0..56), rng.gen_range(0..48)).into(), - anchor: PxAnchor::BottomLeft, - ..default() - }); + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::new(rng.gen_range(0..56), rng.gen_range(0..48))), + PxAnchor::BottomLeft, + )); } } diff --git a/examples/sprite.rs b/examples/sprite.rs index 4878456..958d834 100644 --- a/examples/sprite.rs +++ b/examples/sprite.rs @@ -21,14 +21,13 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Spawn a sprite - commands.spawn(PxSpriteBundle:: { - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::splat(8).into(), - ..default() - }); + commands.spawn(( + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::splat(8)), + )); } #[px_layer] diff --git a/examples/sprite_filter.rs b/examples/sprite_filter.rs index dde6f94..8457b36 100644 --- a/examples/sprite_filter.rs +++ b/examples/sprite_filter.rs @@ -21,23 +21,19 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Spawn a sprite - commands.spawn(PxSpriteBundle:: { - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::new(8, 16).into(), - ..default() - }); + commands.spawn(( + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::new(8, 16)), + )); // Spawn a sprite with a filter commands.spawn(( - PxSpriteBundle:: { - sprite: assets.load("sprite/mage.px_sprite.png"), - position: IVec2::new(24, 16).into(), - ..default() - }, - assets.load::("filter/invert.px_filter.png"), + PxSprite(assets.load("sprite/mage.px_sprite.png")), + PxPosition(IVec2::new(24, 16)), + PxFilter(assets.load("filter/invert.px_filter.png")), )); } diff --git a/examples/text.rs b/examples/text.rs index a5c19f7..9ee5506 100644 --- a/examples/text.rs +++ b/examples/text.rs @@ -21,15 +21,16 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Spawn text - commands.spawn(PxTextBundle:: { - text: "THE MITOCHONDRIA IS THE POWERHOUSE OF THE CELL".into(), - typeface: assets.load("typeface/typeface.px_typeface.png"), - rect: IRect::new(0, 0, 64, 64).into(), - ..default() - }); + commands.spawn(( + PxText { + value: "THE MITOCHONDRIA IS THE POWERHOUSE OF THE CELL".to_string(), + typeface: assets.load("typeface/typeface.px_typeface.png"), + }, + PxRect(IRect::new(0, 0, 64, 64)), + )); } #[px_layer] diff --git a/examples/text_filter.rs b/examples/text_filter.rs index 39dc200..81c8575 100644 --- a/examples/text_filter.rs +++ b/examples/text_filter.rs @@ -21,17 +21,16 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Spawn text commands.spawn(( - PxTextBundle:: { - text: "THE MITOCHONDRIA IS THE POWERHOUSE OF THE CELL".into(), + PxText { + value: "THE MITOCHONDRIA IS THE POWERHOUSE OF THE CELL".to_string(), typeface: assets.load("typeface/typeface.px_typeface.png"), - rect: IRect::new(0, 0, 64, 64).into(), - ..default() }, - assets.load::("filter/dim.px_filter.png"), + PxRect(IRect::new(0, 0, 64, 64)), + PxFilter(assets.load("filter/dim.px_filter.png")), )); } diff --git a/examples/tilemap.rs b/examples/tilemap.rs index c6d4cd6..042f927 100644 --- a/examples/tilemap.rs +++ b/examples/tilemap.rs @@ -22,32 +22,24 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); - let mut map = PxMap::new(UVec2::splat(4)); + let mut tiles = PxTiles::new(UVec2::splat(4)); let mut rng = thread_rng(); for x in 0..4 { for y in 0..4 { - map.set( - Some( - commands - .spawn(PxTileBundle { - tile: rng.gen_range(0..4).into(), - ..default() - }) - .id(), - ), + tiles.set( + Some(commands.spawn(PxTile::from(rng.gen_range(0..4))).id()), UVec2::new(x, y), ); } } // Spawn the map - commands.spawn(PxMapBundle:: { - map, + commands.spawn(PxMap { + tiles, tileset: assets.load("tileset/tileset.px_tileset.png"), - ..default() }); } diff --git a/examples/tilemap_filter.rs b/examples/tilemap_filter.rs index 64e7032..a177f19 100644 --- a/examples/tilemap_filter.rs +++ b/examples/tilemap_filter.rs @@ -22,24 +22,23 @@ fn main() { } fn init(assets: Res, mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); - let mut map = PxMap::new(UVec2::splat(4)); - let dim = assets.load::("filter/dim.px_filter.png"); + let mut tiles = PxTiles::new(UVec2::splat(4)); + let dim = assets.load("filter/dim.px_filter.png"); let mut rng = thread_rng(); for x in 0..4 { for y in 0..4 { // Each tile must be added to the `TileStorage` - map.set( + tiles.set( Some( commands - .spawn(PxTileBundle { - tile: rng.gen_range(0..4).into(), - ..default() - }) - // Insert a filter on the tile - .insert(dim.clone()) + .spawn(( + PxTile::from(rng.gen_range(0..4)), + // Insert a filter on the tile + PxFilter(dim.clone()), + )) .id(), ), UVec2::new(x, y), @@ -48,14 +47,14 @@ fn init(assets: Res, mut commands: Commands) { } // Spawn the map - commands - .spawn(PxMapBundle:: { - map, + commands.spawn(( + PxMap { + tiles, tileset: assets.load("tileset/tileset.px_tileset.png"), - ..default() - }) + }, // Insert a filter on the map. This filter applies to all tiles in the map. - .insert(assets.load::("filter/invert.px_filter.png")); + PxFilter(assets.load("filter/invert.px_filter.png")), + )); } #[px_layer] diff --git a/src/animation.rs b/src/animation.rs index 7316df3..c1e52e0 100644 --- a/src/animation.rs +++ b/src/animation.rs @@ -20,15 +20,15 @@ pub(crate) fn plug(app: &mut App) { ( finish_animations::, finish_animations::, - finish_animations::, - finish_animations::, + finish_animations::, + finish_animations::, ) .in_set(PxSet::FinishAnimations), ); } /// Direction the animation plays -#[derive(Clone, Component, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub enum PxAnimationDirection { /// The animation plays foreward #[default] @@ -38,7 +38,7 @@ pub enum PxAnimationDirection { } /// Animation duration -#[derive(Clone, Component, Copy, Debug)] +#[derive(Clone, Copy, Debug)] pub enum PxAnimationDuration { /// Duration of the entire animation. When used on a tilemap, each tile's animation /// takes the same amount of time, but their frames may desync @@ -67,7 +67,7 @@ impl PxAnimationDuration { } /// Specifies what the animation does when it finishes -#[derive(Clone, Component, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub enum PxAnimationFinishBehavior { /// The entity is despawned when the animation finishes #[default] @@ -82,7 +82,7 @@ pub enum PxAnimationFinishBehavior { } /// Method the animation uses to interpolate between frames -#[derive(Clone, Component, Copy, Debug, Default)] +#[derive(Clone, Copy, Debug, Default)] pub enum PxAnimationFrameTransition { /// Frames are not interpolated #[default] @@ -91,35 +91,36 @@ pub enum PxAnimationFrameTransition { Dither, } -/// Time when the animation started -#[derive(Clone, Component, Copy, Debug, Deref, DerefMut)] -pub struct PxAnimationStart(pub Instant); - -impl Default for PxAnimationStart { - fn default() -> Self { - Self(Instant::now()) - } -} - /// Animates an entity. Works on sprites, filters, text, tilemaps, and lines. -#[derive(Bundle, Clone, Copy, Debug, Default)] -pub struct PxAnimationBundle { - /// A [`PxAnimationDirection`] component +#[derive(Component, Clone, Copy, Debug)] +pub struct PxAnimation { + /// A [`PxAnimationDirection`] pub direction: PxAnimationDirection, - /// A [`PxAnimationDuration`] component + /// A [`PxAnimationDuration`] pub duration: PxAnimationDuration, - /// A [`PxAnimationFinishBehavior`] component + /// A [`PxAnimationFinishBehavior`] pub on_finish: PxAnimationFinishBehavior, - /// A [`PxAnimationFrameTransition`] component + /// A [`PxAnimationFrameTransition`] pub frame_transition: PxAnimationFrameTransition, - /// A [`PxAnimationStart`] component - pub start: PxAnimationStart, + /// Time when the animation started + pub start: Instant, +} + +impl Default for PxAnimation { + fn default() -> Self { + Self { + direction: default(), + duration: default(), + on_finish: default(), + frame_transition: default(), + start: Instant::now(), + } + } } /// Marks an animation that has finished. Automatically added to animations /// with [`PxAnimationFinishBehavior::Mark`] #[derive(Component, Debug)] -#[component(storage = "SparseSet")] pub struct PxAnimationFinished; pub(crate) trait Animation { @@ -135,17 +136,12 @@ pub(crate) trait Animation { ); } -pub(crate) trait AnimationAsset: Asset { - fn max_frame_count(&self) -> usize; -} +pub(crate) trait AnimatedAssetComponent: Component { + type Asset: Asset; -pub(crate) type AnimationComponents = ( - &'static PxAnimationDirection, - &'static PxAnimationDuration, - &'static PxAnimationFinishBehavior, - &'static PxAnimationFrameTransition, - &'static PxAnimationStart, -); + fn handle(&self) -> &Handle; + fn max_frame_count(asset: &Self::Asset) -> usize; +} static DITHERING: &[u16] = &[ 0b0000_0000_0000_0000, @@ -230,7 +226,7 @@ pub(crate) fn draw_animation<'a, A: Animation>( PxAnimationFrameTransition, Duration, )>, - filters: impl IntoIterator, + filters: impl IntoIterator, ) { let mut filter: Box u8> = Box::new(|pixel| pixel); for filter_part in filters { @@ -273,7 +269,7 @@ pub(crate) fn draw_spatial<'a, A: Animation + Spatial>( PxAnimationFrameTransition, Duration, )>, - filters: impl IntoIterator, + filters: impl IntoIterator, camera: PxCamera, ) { let size = spatial.frame_size(); @@ -305,13 +301,7 @@ impl ExtractResource for LastUpdate { } pub(crate) fn copy_animation_params( - params: Option<( - &PxAnimationDirection, - &PxAnimationDuration, - &PxAnimationFinishBehavior, - &PxAnimationFrameTransition, - &PxAnimationStart, - )>, + animation: Option<&PxAnimation>, last_update: Instant, ) -> Option<( PxAnimationDirection, @@ -320,8 +310,14 @@ pub(crate) fn copy_animation_params( PxAnimationFrameTransition, Duration, )> { - params.map( - |(&direction, &duration, &on_finish, &frame_transition, &PxAnimationStart(start))| { + animation.map( + |&PxAnimation { + direction, + duration, + on_finish, + frame_transition, + start, + }| { ( direction, duration, @@ -333,30 +329,23 @@ pub(crate) fn copy_animation_params( ) } -fn finish_animations( +fn finish_animations( mut commands: Commands, - animations: Query<( - Entity, - &Handle, - &PxAnimationDuration, - &PxAnimationFinishBehavior, - &PxAnimationStart, - Option<&PxAnimationFinished>, - )>, - assets: Res>, + animations: Query<(Entity, &A, &PxAnimation, Option<&PxAnimationFinished>)>, + assets: Res>, time: Res>, ) { - for (entity, animation, duration, on_finish, spawn_time, finished) in &animations { - if let Some(animation) = assets.get(animation) { - let lifetime = match duration { - PxAnimationDuration::PerAnimation(duration) => *duration, + for (entity, asset_component, animation, finished) in &animations { + if let Some(asset) = assets.get(asset_component.handle()) { + let lifetime = match animation.duration { + PxAnimationDuration::PerAnimation(duration) => duration, PxAnimationDuration::PerFrame(duration) => { - *duration * animation.max_frame_count() as u32 + duration * A::max_frame_count(asset) as u32 } }; - if time.last_update().unwrap_or_else(|| time.startup()) - **spawn_time >= lifetime { - match on_finish { + if time.last_update().unwrap_or_else(|| time.startup()) - animation.start >= lifetime { + match animation.on_finish { PxAnimationFinishBehavior::Despawn => { commands.entity(entity).despawn(); } diff --git a/src/button.rs b/src/button.rs index 235149b..f693140 100644 --- a/src/button.rs +++ b/src/button.rs @@ -1,4 +1,7 @@ -use crate::{cursor::PxCursorPosition, math::RectExt, prelude::*, set::PxSet}; +use crate::{ + cursor::PxCursorPosition, filter::PxFilterAsset, math::RectExt, prelude::*, set::PxSet, + sprite::PxSpriteAsset, +}; pub(crate) fn plug(app: &mut App) { app.init_resource::() @@ -55,100 +58,48 @@ impl From for PxInteractBounds { } } -/// Sprite to use when the button is not being interacted -#[derive(Component, Debug, Default, Deref, DerefMut)] -pub struct PxIdleSprite(pub Handle); - -impl From> for PxIdleSprite { - fn from(image: Handle) -> Self { - Self(image) - } -} - -/// Sprite to use when the button is hovered -#[derive(Component, Debug, Default, Deref, DerefMut)] -pub struct PxHoverSprite(pub Handle); - -impl From> for PxHoverSprite { - fn from(image: Handle) -> Self { - Self(image) - } -} - -/// Sprite to use when the button is clicked -#[derive(Component, Debug, Default, Deref, DerefMut)] -pub struct PxClickSprite(pub Handle); - -impl From> for PxClickSprite { - fn from(image: Handle) -> Self { - Self(image) - } -} - /// Makes a sprite a button that changes sprite based on interaction -#[derive(Bundle, Debug, Default)] -pub struct PxButtonSpriteBundle { - /// A [`PxInteractBounds`] component - pub bounds: PxInteractBounds, - /// A [`PxIdleSprite`] component - pub idle: PxIdleSprite, - /// A [`PxHoverSprite`] component - pub hover: PxHoverSprite, - /// A [`PxClickSprite`] component - pub click: PxClickSprite, -} - -/// Filter to use when the button is not being interacted -#[derive(Component, Debug, Deref, DerefMut)] -pub struct PxIdleFilter(pub Handle); - -impl From> for PxIdleFilter { - fn from(image: Handle) -> Self { - Self(image) - } +#[derive(Component, Debug)] +#[require(PxSprite, PxInteractBounds)] +pub struct PxButtonSprite { + /// Sprite to use when the button is not being interacted + pub idle: Handle, + /// Sprite to use when the button is hovered + pub hover: Handle, + /// Sprite to use when the button is clicked + pub click: Handle, } -/// Filter to use when the button is hovered -#[derive(Component, Debug, Deref, DerefMut)] -pub struct PxHoverFilter(pub Handle); - -impl From> for PxHoverFilter { - fn from(image: Handle) -> Self { - Self(image) - } +/// Makes a sprite a button that changes filter based on interaction +#[derive(Component, Debug)] +#[require(PxSprite, PxInteractBounds)] +pub struct PxButtonFilter { + /// Filter to use when the button is not being interacted + pub idle: Handle, + /// Filter to use when the button is hovered + pub hover: Handle, + /// Filter to use when the button is clicked + pub click: Handle, } -/// Filter to use when the button is clicked -#[derive(Component, Debug, Deref, DerefMut)] -pub struct PxClickFilter(pub Handle); - -impl From> for PxClickFilter { - fn from(image: Handle) -> Self { - Self(image) +impl Default for PxButtonFilter { + fn default() -> Self { + Self { + idle: default(), + hover: default(), + click: default(), + } } } -/// Makes a sprite a button that changes filter based on interaction -#[derive(Bundle, Debug)] -pub struct PxButtonFilterBundle { - /// A [`PxInteractBounds`] component - pub bounds: PxInteractBounds, - /// A [`PxIdleFilter`] component - pub idle: PxIdleFilter, - /// A [`PxHoverFilter`] component - pub hover: PxHoverFilter, - /// A [`PxClickFilter`] component - pub click: PxClickFilter, -} +// TODO Migrate to observers /// Marks a button that is being hovered #[derive(Component, Debug)] -#[component(storage = "SparseSet")] pub struct PxHover; /// Marks a button that is being clicked. Always appears with [`PxHover`] #[derive(Component, Debug)] -#[component(storage = "SparseSet")] pub struct PxClick; /// Resource that determines whether buttons are enabled @@ -231,74 +182,54 @@ fn disable_buttons( fn add_button_sprites( mut commands: Commands, - buttons: Query<(Entity, &PxIdleSprite), Added>, + buttons: Query<(Entity, &PxButtonSprite), Added>, ) { - for (button, idle) in &buttons { - // `PxIdleSprite` derefs to a `Handle` - commands.entity(button).insert((**idle).clone()); + for (id, button) in &buttons { + commands.entity(id).insert(PxSprite(button.idle.clone())); } } fn update_button_sprites( - mut idle_buttons: Query< - (&mut Handle, &PxIdleSprite), - (Without, Without), - >, - mut hovered_buttons: Query< - (&mut Handle, &PxHoverSprite), - (With, Without), - >, - mut clicked_buttons: Query< - (&mut Handle, &PxClickSprite), - (With, With), - >, + mut idle_buttons: Query<(&mut PxSprite, &PxButtonSprite), (Without, Without)>, + mut hovered_buttons: Query<(&mut PxSprite, &PxButtonSprite), (With, Without)>, + mut clicked_buttons: Query<(&mut PxSprite, &PxButtonSprite), (With, With)>, ) { - for (mut sprite, idle_sprite) in &mut idle_buttons { - *sprite = (*idle_sprite).clone(); + for (mut sprite, button) in &mut idle_buttons { + **sprite = button.idle.clone(); } - for (mut sprite, hovered_sprite) in &mut hovered_buttons { - *sprite = (*hovered_sprite).clone(); + for (mut sprite, button) in &mut hovered_buttons { + **sprite = button.hover.clone(); } - for (mut sprite, clicked_sprite) in &mut clicked_buttons { - *sprite = (*clicked_sprite).clone(); + for (mut sprite, button) in &mut clicked_buttons { + **sprite = button.click.clone(); } } fn add_button_filters( mut commands: Commands, - buttons: Query<(Entity, &PxIdleFilter), Added>, + buttons: Query<(Entity, &PxButtonFilter), Added>, ) { - for (button, idle) in &buttons { - // `PxIdleFilter` derefs to a `Handle` - commands.entity(button).insert((**idle).clone()); + for (id, button) in &buttons { + commands.entity(id).insert(PxFilter(button.idle.clone())); } } fn update_button_filters( - mut idle_buttons: Query< - (&mut Handle, &PxIdleFilter), - (Without, Without), - >, - mut hovered_buttons: Query< - (&mut Handle, &PxHoverFilter), - (With, Without), - >, - mut clicked_buttons: Query< - (&mut Handle, &PxClickFilter), - (With, With), - >, + mut idle_buttons: Query<(&mut PxFilter, &PxButtonFilter), (Without, Without)>, + mut hovered_buttons: Query<(&mut PxFilter, &PxButtonFilter), (With, Without)>, + mut clicked_buttons: Query<(&mut PxFilter, &PxButtonFilter), (With, With)>, ) { - for (mut filter, idle_filter) in &mut idle_buttons { - *filter = (*idle_filter).clone(); + for (mut filter, button) in &mut idle_buttons { + **filter = button.idle.clone(); } - for (mut filter, hovered_filter) in &mut hovered_buttons { - *filter = (*hovered_filter).clone(); + for (mut filter, button) in &mut hovered_buttons { + **filter = button.hover.clone(); } - for (mut filter, clicked_filter) in &mut clicked_buttons { - *filter = (*clicked_filter).clone(); + for (mut filter, button) in &mut clicked_buttons { + **filter = button.click.clone(); } } diff --git a/src/cursor.rs b/src/cursor.rs index 544a4fa..4621450 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -6,7 +6,7 @@ use bevy::{ }; use crate::{ - filter::PxFilter, + filter::PxFilterAsset, prelude::*, screen::{screen_scale, Screen}, set::PxSet, @@ -37,11 +37,11 @@ pub enum PxCursor { /// consider using `bevy_framepace`. Filter { /// Filter to use when not clicking - idle: Handle, + idle: Handle, /// Filter to use when left clicking - left_click: Handle, + left_click: Handle, /// Filter to use when right clicking - right_click: Handle, + right_click: Handle, }, } @@ -76,7 +76,7 @@ fn update_cursor_position( return; }; - let Some(new_position) = camera.viewport_to_world_2d(tf, event.position) else { + let Ok(new_position) = camera.viewport_to_world_2d(tf, event.position) else { **position = None; return; }; @@ -107,7 +107,7 @@ fn change_cursor( return; }; - window.cursor.visible = cursor_pos.is_none() + window.cursor_options.visible = cursor_pos.is_none() || match *cursor { PxCursor::Os => true, PxCursor::Filter { .. } => false, diff --git a/src/filter.rs b/src/filter.rs index 81d2c9d..0b50557 100644 --- a/src/filter.rs +++ b/src/filter.rs @@ -5,15 +5,17 @@ use std::time::Duration; use anyhow::{Error, Result}; use bevy::{ asset::{io::Reader, AssetLoader, LoadContext}, + image::{CompressedImageFormats, ImageLoader, ImageLoaderSettings}, render::{ render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, - texture::{ImageLoader, ImageLoaderSettings}, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, Extract, RenderApp, }, }; use crate::{ - animation::{draw_animation, Animation, AnimationAsset, AnimationComponents}, + animation::{draw_animation, AnimatedAssetComponent, Animation, PxAnimation}, image::{PxImage, PxImageSliceMut}, palette::asset_palette, pixel::Pixel, @@ -22,34 +24,33 @@ use crate::{ }; pub(crate) fn plug(app: &mut App) { - app.add_plugins(RenderAssetPlugin::::default()) - .init_asset::() - .init_asset_loader::() - .sub_app_mut(RenderApp) - .add_systems(ExtractSchedule, extract_filters::); + app.add_plugins(( + RenderAssetPlugin::::default(), + SyncComponentPlugin::>::default(), + )) + .init_asset::() + .init_asset_loader::() + .sub_app_mut(RenderApp) + .add_systems(ExtractSchedule, extract_filters::); } -struct PxFilterLoader(ImageLoader); - -impl FromWorld for PxFilterLoader { - fn from_world(world: &mut World) -> Self { - Self(ImageLoader::from_world(world)) - } -} +#[derive(Default)] +struct PxFilterLoader; impl AssetLoader for PxFilterLoader { - type Asset = PxFilter; + type Asset = PxFilterAsset; type Settings = ImageLoaderSettings; type Error = Error; - async fn load<'a>( - &'a self, - reader: &'a mut Reader<'_>, - settings: &'a ImageLoaderSettings, - load_context: &'a mut LoadContext<'_>, - ) -> Result { - let Self(image_loader) = self; - let image = image_loader.load(reader, settings, load_context).await?; + async fn load( + &self, + reader: &mut dyn Reader, + settings: &ImageLoaderSettings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let image = ImageLoader::new(CompressedImageFormats::NONE) + .load(reader, settings, load_context) + .await?; let palette = asset_palette().await; let indices = PxImage::palette_indices(palette, &image)?; @@ -93,7 +94,7 @@ impl AssetLoader for PxFilterLoader { ); } - Ok(PxFilter(PxImage::new(filter, frame_area as usize))) + Ok(PxFilterAsset(PxImage::new(filter, frame_area as usize))) } fn extensions(&self) -> &[&str] { @@ -102,8 +103,9 @@ impl AssetLoader for PxFilterLoader { } /// Maps colors of an image to different colors. Filter a single sprite, text, or tilemap -/// by adding a [`Handle`] to it, or filter entire layers -/// by spawning a [`PxFilterBundle`]. Create a [`Handle`] with a [`PxAssets`] +/// by adding a [`PxFilter`] to it, or filter entire layers +/// by spawning a [`PxFilterLayers`]. Create a [`Handle`] with a +/// [`PxAssets`] /// and an image file. The image should have pixels in the same positions as the palette. /// The position of each pixel describes the mapping of colors. The image must only contain colors /// that are also in the palette. For animated filters, arrange a number of filters @@ -111,9 +113,9 @@ impl AssetLoader for PxFilterLoader { /// of the image. For examples, see the `assets/` directory in this repository. `fade_to_black.png` /// is an animated filter. #[derive(Asset, Clone, Reflect, Debug)] -pub struct PxFilter(pub(crate) PxImage); +pub struct PxFilterAsset(pub(crate) PxImage); -impl RenderAsset for PxFilter { +impl RenderAsset for PxFilterAsset { type SourceAsset = Self; type Param = (); @@ -125,7 +127,7 @@ impl RenderAsset for PxFilter { } } -impl Animation for PxFilter { +impl Animation for PxFilterAsset { type Param = (); fn frame_count(&self) -> usize { @@ -154,19 +156,29 @@ impl Animation for PxFilter { } } -impl AnimationAsset for PxFilter { - fn max_frame_count(&self) -> usize { - self.frame_count() - } -} - -impl PxFilter { +impl PxFilterAsset { pub(crate) fn as_fn(&self) -> impl '_ + Fn(u8) -> u8 { let Self(filter) = self; |pixel| filter.pixel(IVec2::new(pixel as i32, 0)) } } +/// Applies a [`PxFilterAsset`] to the entity +#[derive(Component, Deref, DerefMut, Default, Clone, Debug)] +pub struct PxFilter(pub Handle); + +impl AnimatedAssetComponent for PxFilter { + type Asset = PxFilterAsset; + + fn handle(&self) -> &Handle { + self + } + + fn max_frame_count(asset: &PxFilterAsset) -> usize { + asset.frame_count() + } +} + /// Function that can be used as a layer selection function in `PxFilterLayers`. Automatically /// implemented for types with the bounds and `Clone`. pub trait SelectLayerFn: 'static + Fn(&L) -> bool + Send + Sync { @@ -188,6 +200,7 @@ impl Clone for Box> { /// Determines which layers a filter appies to #[derive(Component, Clone)] +#[require(PxFilter, Visibility)] pub enum PxFilterLayers { /// Filter applies to a single layer Single { @@ -228,44 +241,36 @@ impl PxFilterLayers { } } -/// Makes a filter that applies to entire layers -#[derive(Bundle, Default)] -pub struct PxFilterBundle { - /// A [`Handle`] component - pub filter: Handle, - /// A [`PxFilterLayers`] component - pub layers: PxFilterLayers, - /// A [`Visibility`] component - pub visibility: Visibility, - /// An [`InheritedVisibility`] component - pub inherited_visibility: InheritedVisibility, -} - pub(crate) type FilterComponents = ( - &'static Handle, + &'static PxFilter, &'static PxFilterLayers, - Option, + Option<&'static PxAnimation>, ); fn extract_filters( - filters: Extract, &InheritedVisibility), Without>>, + filters: Extract< + Query<(FilterComponents, &InheritedVisibility, RenderEntity), Without>, + >, mut cmd: Commands, ) { - for ((filter, layers, animation), visibility) in &filters { + for ((filter, layers, animation), visibility, id) in &filters { if !visibility.get() { continue; } - let mut filter = cmd.spawn((filter.clone(), layers.clone())); + let mut entity = cmd.entity(id); + entity.insert((filter.clone(), layers.clone())); - if let Some((&direction, &duration, &on_finish, &frame_transition, &start)) = animation { - filter.insert((direction, duration, on_finish, frame_transition, start)); + if let Some(animation) = animation { + entity.insert(*animation); + } else { + entity.remove::(); } } } pub(crate) fn draw_filter( - filter: &PxFilter, + filter: &PxFilterAsset, animation: Option<( PxAnimationDirection, PxAnimationDuration, diff --git a/src/lib.rs b/src/lib.rs index 876a87b..6690c06 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -69,7 +69,7 @@ impl Plugin for PxPlugin { line::plug::, map::plug::, palette::plug(self.palette_path.clone()), - position::plug, + position::plug::, screen::Plug::::new(self.screen_size), sprite::plug::, text::plug::, diff --git a/src/map.rs b/src/map.rs index 06ca9c1..84e72a9 100644 --- a/src/map.rs +++ b/src/map.rs @@ -3,28 +3,35 @@ use std::mem::replace; use anyhow::{Error, Result}; use bevy::{ asset::{io::Reader, AssetLoader, LoadContext}, + image::{CompressedImageFormats, ImageLoader, ImageLoaderSettings}, render::{ render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, - texture::{ImageLoader, ImageLoaderSettings}, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, Extract, RenderApp, }, }; use serde::{Deserialize, Serialize}; use crate::{ - animation::{AnimationAsset, AnimationComponents}, + animation::{AnimatedAssetComponent, PxAnimation}, image::PxImage, palette::asset_palette, - position::{PxLayer, Spatial}, + position::{DefaultLayer, PxLayer, Spatial}, prelude::*, + sprite::PxSpriteAsset, }; pub(crate) fn plug(app: &mut App) { - app.add_plugins(RenderAssetPlugin::::default()) - .init_asset::() - .init_asset_loader::() - .sub_app_mut(RenderApp) - .add_systems(ExtractSchedule, (extract_maps::, extract_tiles)); + app.add_plugins(( + RenderAssetPlugin::::default(), + SyncComponentPlugin::::default(), + SyncComponentPlugin::::default(), + )) + .init_asset::() + .init_asset_loader::() + .sub_app_mut(RenderApp) + .add_systems(ExtractSchedule, (extract_maps::, extract_tiles)); } #[derive(Serialize, Deserialize)] @@ -42,27 +49,21 @@ impl Default for PxTilesetLoaderSettings { } } -struct PxTilesetLoader(ImageLoader); - -impl FromWorld for PxTilesetLoader { - fn from_world(world: &mut World) -> Self { - Self(ImageLoader::from_world(world)) - } -} +#[derive(Default)] +struct PxTilesetLoader; impl AssetLoader for PxTilesetLoader { type Asset = PxTileset; type Settings = PxTilesetLoaderSettings; type Error = Error; - async fn load<'a>( - &'a self, - reader: &'a mut Reader<'_>, - settings: &'a PxTilesetLoaderSettings, - load_context: &'a mut LoadContext<'_>, + async fn load( + &self, + reader: &mut dyn Reader, + settings: &PxTilesetLoaderSettings, + load_context: &mut LoadContext<'_>, ) -> Result { - let Self(image_loader) = self; - let image = image_loader + let image = ImageLoader::new(CompressedImageFormats::NONE) .load(reader, &settings.image_loader_settings, load_context) .await?; let palette = asset_palette().await; @@ -106,7 +107,7 @@ impl AssetLoader for PxTilesetLoader { max_frame_count = frame_count; } - tileset.push(PxSprite { + tileset.push(PxSpriteAsset { data: PxImage::new( replace(&mut tile, Vec::with_capacity(tile_area as usize)), tile_size.x as usize, @@ -135,7 +136,7 @@ impl AssetLoader for PxTilesetLoader { /// See `assets/tileset/tileset.png` for an example. #[derive(Asset, Clone, Reflect, Debug)] pub struct PxTileset { - pub(crate) tileset: Vec, + pub(crate) tileset: Vec, tile_size: UVec2, max_frame_count: usize, } @@ -152,12 +153,6 @@ impl RenderAsset for PxTileset { } } -impl AnimationAsset for PxTileset { - fn max_frame_count(&self) -> usize { - self.max_frame_count - } -} - impl PxTileset { /// The size of tiles in the tileset pub fn tile_size(&self) -> UVec2 { @@ -167,12 +162,12 @@ impl PxTileset { /// A tilemap #[derive(Component, Clone, Default, Debug)] -pub struct PxMap { +pub struct PxTiles { tiles: Vec>, width: usize, } -impl PxMap { +impl PxTiles { /// Creates a [`PxMap`] pub fn new(size: UVec2) -> Self { Self { @@ -219,34 +214,38 @@ impl PxMap { } } -impl<'a> Spatial for (&'a PxMap, &'a PxTileset) { +impl<'a> Spatial for (&'a PxTiles, &'a PxTileset) { fn frame_size(&self) -> UVec2 { - let (map, tileset) = self; - map.size() * tileset.tile_size + let (tiles, tileset) = self; + tiles.size() * tileset.tile_size } } /// Creates a tilemap -#[derive(Bundle, Debug, Default)] -pub struct PxMapBundle { - /// A [`PxMap`] component - pub map: PxMap, - /// A [`Handle`] component +#[derive(Component, Default, Clone, Debug)] +#[require(PxPosition, DefaultLayer, PxCanvas, Visibility)] +pub struct PxMap { + /// The map's tiles + pub tiles: PxTiles, + /// The map's tileset pub tileset: Handle, - /// A [`PxPosition`] component - pub position: PxPosition, - /// A layer component - pub layer: L, - /// A [`PxCanvas`] component - pub canvas: PxCanvas, - /// A [`Visibility`] component - pub visibility: Visibility, - /// An [`InheritedVisibility`] component - pub inherited_visibility: InheritedVisibility, +} + +impl AnimatedAssetComponent for PxMap { + type Asset = PxTileset; + + fn handle(&self) -> &Handle { + &self.tileset + } + + fn max_frame_count(tileset: &PxTileset) -> usize { + tileset.max_frame_count + } } /// A tile. Must be added to tiles added to [`PxMap`]. #[derive(Component, Clone, Default, Debug)] +#[require(Visibility)] pub struct PxTile { /// The index to the tile texture in the tileset pub texture: u32, @@ -258,58 +257,54 @@ impl From for PxTile { } } -/// Creates a tile -#[derive(Bundle, Debug, Default)] -pub struct PxTileBundle { - /// A [`PxTile`] component - pub tile: PxTile, - /// A [`Visibility`] component - pub visibility: Visibility, - /// An [`InheritedVisibility`] component - pub inherited_visibility: InheritedVisibility, -} - pub(crate) type MapComponents = ( &'static PxMap, - &'static Handle, &'static PxPosition, &'static L, &'static PxCanvas, - Option, - Option<&'static Handle>, + Option<&'static PxAnimation>, + Option<&'static PxFilter>, ); fn extract_maps( - maps: Extract, &InheritedVisibility)>>, + maps: Extract, &InheritedVisibility, RenderEntity)>>, + render_entities: Extract>, mut cmd: Commands, ) { - for ((map, tileset, &position, layer, &canvas, animation, filter), visibility) in &maps { + for ((map, &position, layer, &canvas, animation, filter), visibility, id) in &maps { if !visibility.get() { continue; } - let mut map = cmd.spawn(( - map.clone(), - tileset.clone(), - position, - layer.clone(), - canvas, - )); + let mut entity = cmd.entity(id); - if let Some((&direction, &duration, &on_finish, &frame_transition, &start)) = animation { - map.insert((direction, duration, on_finish, frame_transition, start)); + let mut map = map.clone(); + for opt_tile in &mut map.tiles.tiles { + if let &mut Some(tile) = opt_tile { + *opt_tile = render_entities.get(tile).ok(); + } + } + + entity.insert((map, position, layer.clone(), canvas)); + + if let Some(animation) = animation { + entity.insert(*animation); + } else { + entity.remove::(); } if let Some(filter) = filter { - map.insert(filter.clone()); + entity.insert(filter.clone()); + } else { + entity.remove::(); } } } -pub(crate) type TileComponents = (&'static PxTile, Option<&'static Handle>); +pub(crate) type TileComponents = (&'static PxTile, Option<&'static PxFilter>); fn extract_tiles( - tiles: Extract>, + tiles: Extract>, mut cmd: Commands, ) { for ((tile, filter), visibility, entity) in &tiles { @@ -317,11 +312,13 @@ fn extract_tiles( continue; } - let mut entity = cmd.get_or_spawn(entity); + let mut entity = cmd.entity(entity); entity.insert(tile.clone()); if let Some(filter) = filter { entity.insert(filter.clone()); + } else { + entity.remove::(); } } } diff --git a/src/palette.rs b/src/palette.rs index f458550..745870d 100644 --- a/src/palette.rs +++ b/src/palette.rs @@ -8,10 +8,8 @@ use std::{ use anyhow::{Error, Result}; use bevy::{ asset::{io::Reader, AssetLoader, LoadContext}, - render::{ - render_resource::TextureFormat, - texture::{ImageLoader, ImageLoaderSettings}, - }, + image::{CompressedImageFormats, ImageLoader, ImageLoaderSettings}, + render::render_resource::TextureFormat, utils::HashMap, }; use event_listener::Event; @@ -31,28 +29,24 @@ pub(crate) fn plug(palette_path: PathBuf) -> impl Fn(&mut App) { } } -struct PaletteLoader(ImageLoader); - -impl FromWorld for PaletteLoader { - fn from_world(world: &mut World) -> Self { - Self(ImageLoader::from_world(world)) - } -} +#[derive(Default)] +struct PaletteLoader; impl AssetLoader for PaletteLoader { type Asset = Palette; type Settings = ImageLoaderSettings; type Error = Error; - async fn load<'a>( - &'a self, - reader: &'a mut Reader<'_>, - settings: &'a ImageLoaderSettings, - load_context: &'a mut LoadContext<'_>, + async fn load( + &self, + reader: &mut dyn Reader, + settings: &ImageLoaderSettings, + load_context: &mut LoadContext<'_>, ) -> Result { - let Self(image_loader) = self; Ok(Palette::new( - &image_loader.load(reader, settings, load_context).await?, + &ImageLoader::new(CompressedImageFormats::NONE) + .load(reader, settings, load_context) + .await?, )) } diff --git a/src/position.rs b/src/position.rs index 23f48e3..4206b1e 100644 --- a/src/position.rs +++ b/src/position.rs @@ -2,9 +2,12 @@ use std::fmt::Debug; -use bevy::render::extract_component::ExtractComponent; +use bevy::{ + ecs::{component::ComponentId, world::DeferredWorld}, + render::{extract_component::ExtractComponent, RenderApp}, +}; -use crate::{math::Diagonal, prelude::*, screen::Screen, set::PxSet}; +use crate::{math::Diagonal, prelude::*, screen::Screen, set::PxSet, sprite::PxSpriteAsset}; // TODO Try to use traits instead of macros when we get the new solver macro_rules! align_to_screen { @@ -32,36 +35,39 @@ macro_rules! align_to_screen { }}; } -pub(crate) fn plug(app: &mut App) { - app.add_systems( - PreUpdate, - ( - update_sub_positions, - update_position_to_sub.in_set(PxSet::UpdatePosToSubPos), +pub(crate) fn plug(app: &mut App) { + app.insert_resource(InsertDefaultLayer::new::()) + .add_systems( + PreUpdate, + ( + update_sub_positions, + update_position_to_sub.in_set(PxSet::UpdatePosToSubPos), + ) + .chain(), ) - .chain(), - ) - .add_systems( - PostUpdate, - ( - align_to_screen!( - (&PxMap, &Handle), - Res>, - |(map, tileset), tilesets: &Res>| { - Some((map, tilesets.get(tileset)?).frame_size()) - } + .add_systems( + PostUpdate, + ( + align_to_screen!(&PxMap, Res>, |map: &PxMap, + tilesets: &Res< + Assets, + >| { + Some((&map.tiles, tilesets.get(&map.tileset)?).frame_size()) + }), + align_to_screen!( + &PxSprite, + Res>, + |sprite: &PxSprite, sprites: &Res>| { + Some(sprites.get(&**sprite)?.frame_size()) + } + ), + align_to_screen!(&PxRect, (), |rect: &PxRect, &()| Some(rect.frame_size())), + #[cfg(feature = "line")] + align_to_screen!(&PxLine, (), |line: &PxLine, &()| Some(line.frame_size())), ), - align_to_screen!(&Handle, Res>, |sprite, - sprites: &Res< - Assets, - >| { - Some(sprites.get(sprite)?.frame_size()) - }), - align_to_screen!(&PxRect, (), |rect: &PxRect, &()| Some(rect.frame_size())), - #[cfg(feature = "line")] - align_to_screen!(&PxLine, (), |line: &PxLine, &()| Some(line.frame_size())), - ), - ); + ) + .sub_app_mut(RenderApp) + .insert_resource(InsertDefaultLayer::new::()); } pub(crate) trait Spatial { @@ -92,6 +98,32 @@ pub trait PxLayer: ExtractComponent + Component + Ord + Clone + Default + Debug impl PxLayer for L {} +#[derive(Resource, Deref)] +struct InsertDefaultLayer(Box); + +impl InsertDefaultLayer { + fn new() -> Self { + Self(Box::new(|entity| { + entity.insert_if_new(L::default()); + })) + } +} + +#[derive(Component, Default)] +#[component(on_add = insert_default_layer)] +pub(crate) struct DefaultLayer; + +fn insert_default_layer(mut world: DeferredWorld, entity: Entity, _: ComponentId) { + world.commands().queue(move |world: &mut World| { + let insert_default_layer = world.remove_resource::().unwrap(); + if let Ok(mut entity) = world.get_entity_mut(entity) { + insert_default_layer(entity.remove::()); + } + world.insert_resource(insert_default_layer); + // That's what it's all about! + }) +} + /// How a sprite is positioned relative to its [`PxPosition`]. It defaults to [`PxAnchor::Center`]. #[derive(ExtractComponent, Component, Clone, Copy, Default, Debug)] pub enum PxAnchor { @@ -192,7 +224,7 @@ fn update_sub_positions(mut query: Query<(&mut PxSubPosition, &PxVelocity)>, tim **sub_position = new_position; } } else { - **sub_position += **velocity * time.delta_seconds(); + **sub_position += **velocity * time.delta_secs(); } } } diff --git a/src/prelude.rs b/src/prelude.rs index 276184f..4a5dfe9 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -17,24 +17,20 @@ pub use crate::particle::{ }; pub use crate::{ animation::{ - PxAnimationBundle, PxAnimationDirection, PxAnimationDuration, PxAnimationFinishBehavior, + PxAnimation, PxAnimationDirection, PxAnimationDuration, PxAnimationFinishBehavior, PxAnimationFinished, PxAnimationFrameTransition, }, - button::{ - PxButtonFilterBundle, PxButtonSpriteBundle, PxClick, PxClickFilter, PxClickSprite, - PxEnableButtons, PxHover, PxHoverFilter, PxHoverSprite, PxIdleFilter, PxIdleSprite, - PxInteractBounds, - }, + button::{PxButtonFilter, PxButtonSprite, PxClick, PxEnableButtons, PxHover, PxInteractBounds}, camera::{PxCamera, PxCanvas}, cursor::PxCursor, - filter::{PxFilter, PxFilterBundle, PxFilterLayers}, - map::{PxMap, PxMapBundle, PxTile, PxTileBundle, PxTileset}, + filter::{PxFilter, PxFilterAsset, PxFilterLayers}, + map::{PxMap, PxTile, PxTiles, PxTileset}, math::{Diagonal, Orthogonal}, position::{PxAnchor, PxLayer, PxPosition, PxScreenAlign, PxSubPosition, PxVelocity}, screen::ScreenSize, - sprite::{PxSprite, PxSpriteBundle}, - text::{PxText, PxTextBundle, PxTypeface}, - ui::{Align, PxLayout, PxLayoutBundle, PxRect}, + sprite::{PxSprite, PxSpriteAsset}, + text::{PxText, PxTypeface}, + ui::PxRect, PxPlugin, }; pub use seldom_pixel_macros::px_layer; diff --git a/src/screen.rs b/src/screen.rs index bfeec2b..ac9f1a4 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -4,6 +4,7 @@ use std::{collections::BTreeMap, marker::PhantomData}; use bevy::{ core_pipeline::core_2d::graph::{Core2d, Node2d}, + image::TextureFormatPixelInfo, render::{ extract_resource::{ExtractResource, ExtractResourcePlugin}, render_asset::RenderAssets, @@ -19,7 +20,6 @@ use bevy::{ TextureSampleType, TextureViewDescriptor, TextureViewDimension, VertexState, }, renderer::{RenderContext, RenderDevice, RenderQueue}, - texture::{BevyDefault, TextureFormatPixelInfo}, view::ViewTarget, Render, RenderApp, RenderSet, }, @@ -271,6 +271,7 @@ impl FromWorld for PxPipeline { depth_stencil: None, multisample: default(), push_constant_ranges: Vec::new(), + zero_initialize_workgroup_memory: true, }, ), layout, @@ -351,16 +352,14 @@ impl ViewNode for PxRenderNode { let mut layer_contents = BTreeMap::<_, (Vec<_>, Vec<_>, Vec<_>, (), Vec<_>, (), Vec<_>)>::default(); - for (map, tileset, position, layer, canvas, animation, filter) in - self.maps.iter_manual(world) - { + for (map, position, layer, canvas, animation, filter) in self.maps.iter_manual(world) { if let Some((maps, _, _, _, _, _, _)) = layer_contents.get_mut(layer) { - maps.push((map, tileset, position, canvas, animation, filter)); + maps.push((map, position, canvas, animation, filter)); } else { layer_contents.insert( layer.clone(), ( - vec![(map, tileset, position, canvas, animation, filter)], + vec![(map, position, canvas, animation, filter)], // default(), default(), default(), @@ -416,18 +415,18 @@ impl ViewNode for PxRenderNode { } } - for (text, typeface, rect, alignment, layer, canvas, animation, filter) in + for (text, rect, alignment, layer, canvas, animation, filter) in self.texts.iter_manual(world) { if let Some((_, _, texts, _, _, _, _)) = layer_contents.get_mut(layer) { - texts.push((text, typeface, rect, alignment, canvas, animation, filter)); + texts.push((text, rect, alignment, canvas, animation, filter)); } else { layer_contents.insert( layer.clone(), ( default(), default(), - vec![(text, typeface, rect, alignment, canvas, animation, filter)], + vec![(text, rect, alignment, canvas, animation, filter)], default(), default(), default(), @@ -539,9 +538,9 @@ impl ViewNode for PxRenderNode { let tilesets = world.resource::>(); // let images = world.resource::>(); - let sprite_assets = world.resource::>(); + let sprite_assets = world.resource::>(); let typefaces = world.resource::>(); - let filters = world.resource::>(); + let filters = world.resource::>(); let mut layer_image = PxImage::>::empty_from_image(&image); let mut image_slice = PxImageSliceMut::from_image_mut(&mut image); @@ -563,18 +562,19 @@ impl ViewNode for PxRenderNode { { layer_image.clear(); - for (map, tileset, position, canvas, animation, map_filter) in maps { - let Some(tileset) = tilesets.get(tileset) else { + for (map, position, canvas, animation, map_filter) in maps { + let Some(tileset) = tilesets.get(&map.tileset) else { continue; }; - let map_filter = map_filter.and_then(|map_filter| filters.get(map_filter)); - let size = map.size(); + let map_filter = map_filter.and_then(|map_filter| filters.get(&**map_filter)); + let size = map.tiles.size(); for x in 0..size.x { for y in 0..size.y { let pos = UVec2::new(x, y); - let Some(tile) = map.get(pos) else { + + let Some(tile) = map.tiles.get(pos) else { continue; }; @@ -598,7 +598,7 @@ impl ViewNode for PxRenderNode { *canvas, copy_animation_params(animation, last_update), [ - tile_filter.and_then(|tile_filter| filters.get(tile_filter)), + tile_filter.and_then(|tile_filter| filters.get(&**tile_filter)), map_filter, ] .into_iter() @@ -749,7 +749,7 @@ impl ViewNode for PxRenderNode { // } for (sprite, position, anchor, canvas, animation, filter) in sprites { - let Some(sprite) = sprite_assets.get(sprite) else { + let Some(sprite) = sprite_assets.get(&**sprite) else { continue; }; @@ -761,13 +761,13 @@ impl ViewNode for PxRenderNode { *anchor, *canvas, copy_animation_params(animation, last_update), - filter.and_then(|filter| filters.get(filter)), + filter.and_then(|filter| filters.get(&**filter)), camera, ); } - for (text, typeface, rect, alignment, canvas, animation, filter) in texts { - let Some(typeface) = typefaces.get(typeface) else { + for (text, rect, alignment, canvas, animation, filter) in texts { + let Some(typeface) = typefaces.get(&text.typeface) else { continue; }; @@ -785,7 +785,7 @@ impl ViewNode for PxRenderNode { let mut word_width = 0; let mut separator = Vec::default(); let mut separator_width = 0; - for character in text.chars() { + for character in text.value.chars() { let (character_width, is_separator) = typeface .characters .get(&character) @@ -902,7 +902,7 @@ impl ViewNode for PxRenderNode { PxAnchor::BottomLeft, PxCanvas::Camera, copy_animation_params(animation, last_update), - filter.and_then(|filter| filters.get(filter)), + filter.and_then(|filter| filters.get(&**filter)), camera, ); @@ -919,7 +919,7 @@ impl ViewNode for PxRenderNode { } if let Some(filter) = filter { - if let Some(PxFilter(filter)) = filters.get(filter) { + if let Some(PxFilterAsset(filter)) = filters.get(&**filter) { text_image.slice_all_mut().for_each_mut(|_, _, pixel| { if let Some(pixel) = pixel { *pixel = filter.pixel(IVec2::new(*pixel as i32, 0)); @@ -947,7 +947,7 @@ impl ViewNode for PxRenderNode { } for (filter, animation) in clip_filters { - if let Some(filter) = filters.get(filter) { + if let Some(filter) = filters.get(&**filter) { draw_filter( filter, copy_animation_params(animation, last_update), @@ -973,7 +973,7 @@ impl ViewNode for PxRenderNode { } for (filter, animation) in over_filters { - if let Some(filter) = filters.get(filter) { + if let Some(filter) = filters.get(&**filter) { draw_filter( filter, copy_animation_params(animation, last_update), @@ -992,7 +992,7 @@ impl ViewNode for PxRenderNode { } = world.resource() { if let Some(cursor_pos) = **world.resource::() { - if let Some(PxFilter(filter)) = filters.get(match cursor { + if let Some(PxFilterAsset(filter)) = filters.get(match cursor { CursorState::Idle => idle, CursorState::Left => left_click, CursorState::Right => right_click, diff --git a/src/sprite.rs b/src/sprite.rs index aebe517..6c1c396 100644 --- a/src/sprite.rs +++ b/src/sprite.rs @@ -3,35 +3,40 @@ use anyhow::{Error, Result}; use bevy::{ asset::{io::Reader, AssetLoader, LoadContext}, + image::{CompressedImageFormats, ImageLoader, ImageLoaderSettings}, render::{ render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, - texture::{ImageLoader, ImageLoaderSettings}, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, Extract, RenderApp, }, }; use serde::{Deserialize, Serialize}; use crate::{ - animation::{Animation, AnimationAsset, AnimationComponents}, + animation::{AnimatedAssetComponent, Animation}, image::{PxImage, PxImageSliceMut}, palette::asset_palette, pixel::Pixel, - position::{PxLayer, Spatial}, + position::{DefaultLayer, PxLayer, Spatial}, prelude::*, }; pub(crate) fn plug(app: &mut App) { - app.add_plugins(RenderAssetPlugin::::default()) - .init_asset::() - .init_asset_loader::() - .sub_app_mut(RenderApp) - .add_systems( - ExtractSchedule, - ( - extract_sprites::, - // extract_image_to_sprites:: - ), - ); + app.add_plugins(( + RenderAssetPlugin::::default(), + SyncComponentPlugin::::default(), + )) + .init_asset::() + .init_asset_loader::() + .sub_app_mut(RenderApp) + .add_systems( + ExtractSchedule, + ( + extract_sprites::, + // extract_image_to_sprites:: + ), + ); } #[derive(Serialize, Deserialize)] @@ -49,33 +54,27 @@ impl Default for PxSpriteLoaderSettings { } } -struct PxSpriteLoader(ImageLoader); - -impl FromWorld for PxSpriteLoader { - fn from_world(world: &mut World) -> Self { - Self(ImageLoader::from_world(world)) - } -} +#[derive(Default)] +struct PxSpriteLoader; impl AssetLoader for PxSpriteLoader { - type Asset = PxSprite; + type Asset = PxSpriteAsset; type Settings = PxSpriteLoaderSettings; type Error = Error; - async fn load<'a>( - &'a self, - reader: &'a mut Reader<'_>, - settings: &'a PxSpriteLoaderSettings, - load_context: &'a mut LoadContext<'_>, - ) -> Result { - let Self(image_loader) = self; - let image = image_loader + async fn load( + &self, + reader: &mut dyn Reader, + settings: &PxSpriteLoaderSettings, + load_context: &mut LoadContext<'_>, + ) -> Result { + let image = ImageLoader::new(CompressedImageFormats::NONE) .load(reader, &settings.image_loader_settings, load_context) .await?; let palette = asset_palette().await; let data = PxImage::palette_indices(palette, &image)?; - Ok(PxSprite { + Ok(PxSpriteAsset { frame_size: data.area() / settings.frame_count, data, }) @@ -86,17 +85,17 @@ impl AssetLoader for PxSpriteLoader { } } -/// A sprite. Create a [`Handle`] with a [`PxAssets`] and an image. -/// If the sprite is animated, the frames should be laid out from bottom to top. +/// A sprite. Create a [`Handle`] with a [`PxAssets`] and an image. +/// If the sprite is animated, the frames should be laid out from top to bottom. /// See `assets/sprite/runner.png` for an example of an animated sprite. #[derive(Asset, Serialize, Deserialize, Clone, Reflect, Debug)] -pub struct PxSprite { +pub struct PxSpriteAsset { // TODO Use 0 for transparency pub(crate) data: PxImage>, pub(crate) frame_size: usize, } -impl RenderAsset for PxSprite { +impl RenderAsset for PxSpriteAsset { type SourceAsset = Self; type Param = (); @@ -108,7 +107,7 @@ impl RenderAsset for PxSprite { } } -impl Animation for PxSprite { +impl Animation for PxSpriteAsset { type Param = (); fn frame_count(&self) -> usize { @@ -140,7 +139,7 @@ impl Animation for PxSprite { } } -impl Spatial for PxSprite { +impl Spatial for PxSpriteAsset { fn frame_size(&self) -> UVec2 { UVec2::new( self.data.width() as u32, @@ -149,29 +148,27 @@ impl Spatial for PxSprite { } } -impl AnimationAsset for PxSprite { - fn max_frame_count(&self) -> usize { - self.frame_count() +/// A sprite +#[derive(Component, Deref, DerefMut, Default, Clone, Debug)] +#[require(PxPosition, PxAnchor, DefaultLayer, PxCanvas, Visibility)] +pub struct PxSprite(pub Handle); + +impl From> for PxSprite { + fn from(value: Handle) -> Self { + Self(value) } } -/// Spawns a sprite -#[derive(Bundle, Debug, Default)] -pub struct PxSpriteBundle { - /// A [`Handle`] component - pub sprite: Handle, - /// A [`PxPosition`] component - pub position: PxPosition, - /// A [`PxAnchor`] component - pub anchor: PxAnchor, - /// A layer component - pub layer: L, - /// A [`PxCanvas`] component - pub canvas: PxCanvas, - /// A [`Visibility`] component - pub visibility: Visibility, - /// An [`InheritedVisibility`] component - pub inherited_visibility: InheritedVisibility, +impl AnimatedAssetComponent for PxSprite { + type Asset = PxSpriteAsset; + + fn handle(&self) -> &Handle { + self + } + + fn max_frame_count(sprite: &PxSpriteAsset) -> usize { + sprite.frame_count() + } } /// Size of threshold map to use for dithering. The image is tiled with dithering according to this @@ -396,32 +393,40 @@ pub struct Dither { // } pub(crate) type SpriteComponents = ( - &'static Handle, + &'static PxSprite, &'static PxPosition, &'static PxAnchor, &'static L, &'static PxCanvas, - Option, - Option<&'static Handle>, + Option<&'static PxAnimation>, + Option<&'static PxFilter>, ); fn extract_sprites( - sprites: Extract, &InheritedVisibility)>>, + // TODO Maybe calculate `ViewVisibility` + sprites: Extract, &InheritedVisibility, RenderEntity)>>, mut cmd: Commands, ) { - for ((sprite, &position, &anchor, layer, &canvas, animation, filter), visibility) in &sprites { + for ((sprite, &position, &anchor, layer, &canvas, animation, filter), visibility, id) in + &sprites + { if !visibility.get() { continue; } - let mut sprite = cmd.spawn((sprite.clone(), position, anchor, layer.clone(), canvas)); + let mut entity = cmd.entity(id); + entity.insert((sprite.clone(), position, anchor, layer.clone(), canvas)); - if let Some((&direction, &duration, &on_finish, &frame_transition, &start)) = animation { - sprite.insert((direction, duration, on_finish, frame_transition, start)); + if let Some(animation) = animation { + entity.insert(*animation); + } else { + entity.remove::(); } if let Some(filter) = filter { - sprite.insert(filter.clone()); + entity.insert(filter.clone()); + } else { + entity.remove::(); } } } diff --git a/src/text.rs b/src/text.rs index 4355944..af3b7aa 100644 --- a/src/text.rs +++ b/src/text.rs @@ -1,9 +1,11 @@ use anyhow::{anyhow, Error, Result}; use bevy::{ asset::{io::Reader, AssetLoader, LoadContext}, + image::{CompressedImageFormats, ImageLoader, ImageLoaderSettings}, render::{ render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin}, - texture::{ImageLoader, ImageLoaderSettings}, + sync_component::SyncComponentPlugin, + sync_world::RenderEntity, Extract, RenderApp, }, utils::HashMap, @@ -11,29 +13,19 @@ use bevy::{ use serde::{Deserialize, Serialize}; use crate::{ - animation::{AnimationAsset, AnimationComponents}, - image::PxImage, - palette::asset_palette, - position::PxLayer, - prelude::*, + animation::AnimatedAssetComponent, image::PxImage, palette::asset_palette, + position::DefaultLayer, position::PxLayer, prelude::*, }; pub(crate) fn plug(app: &mut App) { - app.add_plugins(RenderAssetPlugin::::default()) - .init_asset::() - .init_asset_loader::() - .sub_app_mut(RenderApp) - .add_systems(ExtractSchedule, extract_texts::); -} - -/// Text to be drawn on the screen -#[derive(Component, Clone, Deref, DerefMut, Default, Debug)] -pub struct PxText(pub String); - -impl> From for PxText { - fn from(t: T) -> Self { - Self(t.into()) - } + app.add_plugins(( + RenderAssetPlugin::::default(), + SyncComponentPlugin::::default(), + )) + .init_asset::() + .init_asset_loader::() + .sub_app_mut(RenderApp) + .add_systems(ExtractSchedule, extract_texts::); } #[derive(Serialize, Deserialize)] @@ -57,27 +49,21 @@ impl Default for PxTypefaceLoaderSettings { } } -struct PxTypefaceLoader(ImageLoader); - -impl FromWorld for PxTypefaceLoader { - fn from_world(world: &mut World) -> Self { - Self(ImageLoader::from_world(world)) - } -} +#[derive(Default)] +struct PxTypefaceLoader; impl AssetLoader for PxTypefaceLoader { type Asset = PxTypeface; type Settings = PxTypefaceLoaderSettings; type Error = Error; - async fn load<'a>( - &'a self, - reader: &'a mut Reader<'_>, - settings: &'a PxTypefaceLoaderSettings, - load_context: &'a mut LoadContext<'_>, + async fn load( + &self, + reader: &mut dyn Reader, + settings: &PxTypefaceLoaderSettings, + load_context: &mut LoadContext<'_>, ) -> Result { - let Self(image_loader) = self; - let image = image_loader + let image = ImageLoader::new(CompressedImageFormats::NONE) .load(reader, &settings.image_loader_settings, load_context) .await?; let palette = asset_palette().await; @@ -105,7 +91,7 @@ impl AssetLoader for PxTypefaceLoader { ( character, - PxSprite { + PxSpriteAsset { data: PxImage::from_parts_vert(image.split_horz(image_width / frames)) .unwrap(), frame_size: image_area / frames, @@ -164,7 +150,7 @@ pub(crate) struct PxSeparator { #[derive(Asset, Clone, Reflect, Debug)] pub struct PxTypeface { pub(crate) height: u32, - pub(crate) characters: HashMap, + pub(crate) characters: HashMap, pub(crate) separators: HashMap, pub(crate) max_frame_count: usize, } @@ -181,70 +167,60 @@ impl RenderAsset for PxTypeface { } } -impl AnimationAsset for PxTypeface { - fn max_frame_count(&self) -> usize { - self.max_frame_count - } -} - /// Spawns text to be rendered on-screen -#[derive(Bundle, Debug, Default)] -pub struct PxTextBundle { - /// A [`PxText`] component - pub text: PxText, - /// A [`Handle`] component +#[derive(Component, Default, Clone, Debug)] +#[require(PxRect, PxAnchor, DefaultLayer, PxCanvas, Visibility)] +pub struct PxText { + /// The contents of the text + pub value: String, + /// The typeface pub typeface: Handle, - /// A [`PxRect`] component - pub rect: PxRect, - /// A [`PxAnchor`] component - pub alignment: PxAnchor, - /// A layer component - pub layer: L, - /// A [`PxCanvas`] component - pub canvas: PxCanvas, - /// A [`Visibility`] component - pub visibility: Visibility, - /// An [`InheritedVisibility`] component - pub inherited_visibility: InheritedVisibility, +} + +impl AnimatedAssetComponent for PxText { + type Asset = PxTypeface; + + fn handle(&self) -> &Handle { + &self.typeface + } + + fn max_frame_count(typeface: &PxTypeface) -> usize { + typeface.max_frame_count + } } pub(crate) type TextComponents = ( &'static PxText, - &'static Handle, &'static PxRect, &'static PxAnchor, &'static L, &'static PxCanvas, - Option, - Option<&'static Handle>, + Option<&'static PxAnimation>, + Option<&'static PxFilter>, ); fn extract_texts( - texts: Extract, &InheritedVisibility)>>, + texts: Extract, &InheritedVisibility, RenderEntity)>>, mut cmd: Commands, ) { - for ((text, typeface, &rect, &alignment, layer, &canvas, animation, filter), visibility) in - &texts - { + for ((text, &rect, &alignment, layer, &canvas, animation, filter), visibility, id) in &texts { if !visibility.get() { continue; } - let mut text = cmd.spawn(( - text.clone(), - typeface.clone(), - rect, - alignment, - layer.clone(), - canvas, - )); - - if let Some((&direction, &duration, &on_finish, &frame_transition, &start)) = animation { - text.insert((direction, duration, on_finish, frame_transition, start)); + let mut entity = cmd.entity(id); + entity.insert((text.clone(), rect, alignment, layer.clone(), canvas)); + + if let Some(animation) = animation { + entity.insert(*animation); + } else { + entity.remove::(); } if let Some(filter) = filter { - text.insert(filter.clone()); + entity.insert(filter.clone()); + } else { + entity.remove::(); } } } diff --git a/src/ui.rs b/src/ui.rs index 8f3b047..6c0a2a2 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -15,45 +15,3 @@ impl Spatial for PxRect { self.size().as_uvec2() } } - -/// Cross-axis alignment -#[derive(Debug, Default)] -pub enum Align { - /// Align to the start of the space - #[default] - Start, - /// Align to the end of the space - End, -} - -/// Lays out a spatial entity's children in a direction -#[derive(Component, Debug)] -pub struct PxLayout { - /// Direction in which to lay out the children - pub direction: Orthogonal, - /// Cross-axis alignment - pub align: Align, - /// Space between each child - pub spacing: u16, -} - -impl Default for PxLayout { - fn default() -> Self { - Self { - direction: Orthogonal::Right, - align: default(), - spacing: 0, - } - } -} - -/// Lays out its children in a direction -#[derive(Bundle, Default, Debug)] -pub struct PxLayoutBundle { - /// A `PxLayout` component - pub layout: PxLayout, - /// A `PxRect` component - pub rect: PxRect, - /// A `PxPosition` component - pub position: PxPosition, -}