From 1c99950e655482cb8439baacc0f7252d003a7e46 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Wed, 29 Oct 2025 22:31:56 -0700 Subject: [PATCH] Rendering to images works, images are ui nodes Lots of improvements we need to make but the bones are there! --- DESIGN.md => src/bin/tetris/DESIGN.md | 0 src/bin/tetris/main.rs | 220 +++++++++++++++++++++++--- 2 files changed, 197 insertions(+), 23 deletions(-) rename DESIGN.md => src/bin/tetris/DESIGN.md (100%) diff --git a/DESIGN.md b/src/bin/tetris/DESIGN.md similarity index 100% rename from DESIGN.md rename to src/bin/tetris/DESIGN.md diff --git a/src/bin/tetris/main.rs b/src/bin/tetris/main.rs index 045b95c..f5975fb 100644 --- a/src/bin/tetris/main.rs +++ b/src/bin/tetris/main.rs @@ -2,17 +2,27 @@ // Bevy basically forces "complex types" with Querys #![allow(clippy::type_complexity)] +use bevy::{ + asset::RenderAssetUsages, + math::FloatOrd, + render::{ + camera::{ImageRenderTarget, RenderTarget}, + render_resource::{Extent3d, TextureDimension, TextureFormat, TextureUsages}, + view::RenderLayers, + }, +}; use games::*; use itertools::Itertools; #[cfg(test)] mod test; -// TODO: Space key: skip to end +const TETRIS: RenderLayers = RenderLayers::layer(1); +const BATTLER: RenderLayers = RenderLayers::layer(2); + // TODO: When piece is near wall and rotates, move it over if it fits // TODO: Make falling based on a timer resource ticking // This allows us to tune the falling rate over time -// TODO: Preview next batch of pieces that will drop fn main() { App::new() @@ -26,7 +36,16 @@ fn main() { .init_resource::() .init_resource::() .init_resource::() - .add_systems(Startup, (init_world, init_debug_ui, init_ui)) + .init_resource::() + .add_systems( + Startup, + ( + init_world, + init_debug_ui, + init_cameras, + init_ui.after(init_cameras), + ), + ) // Input and basic systems .add_systems( Update, @@ -48,8 +67,11 @@ fn main() { .run_if(clock_cycle(1.0)), update_position.run_if(any_component_changed::), update_shape_blocks - .run_if(any_component_added::.or(any_component_changed::)).after(update_position), - deactivate_shape.run_if(any_component_removed::).after(update_shape_blocks), + .run_if(any_component_added::.or(any_component_changed::)) + .after(update_position), + deactivate_shape + .run_if(any_component_removed::) + .after(update_shape_blocks), // Clearing lines systems clear_line.run_if(any_component_changed::), adjust_block_lines @@ -246,7 +268,7 @@ impl Display for ShapeStore { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.0 { Some(inner) => write!(f, "{}", inner.as_ascii()), - None => write!(f, "---") + None => write!(f, "---"), } } } @@ -266,11 +288,96 @@ fn init_world( (0..Y_MAX).for_each(|i| { info!("Spawning line {i}"); - commands.spawn((Line(i), LineBlocks::default())); + commands.spawn((Line(i), LineBlocks::default(), TETRIS)); }); } -fn init_ui(mut commands: Commands) { +#[derive(Resource, Default)] +struct OutputImages { + tetris: Handle, + battler: Handle, +} + +#[derive(Component)] +struct TetrisCamera; + +#[derive(Component)] +struct BattlerCamera; + +fn init_cameras( + mut commands: Commands, + mut images: ResMut>, + mut output_images: ResMut, +) { + fn render_target_image() -> Image { + let (width, height) = (1028, 1028); + let size = Extent3d { + width, + height, + ..default() + }; + let dimension = TextureDimension::D2; + let format = TextureFormat::Bgra8UnormSrgb; + let asset_usage = RenderAssetUsages::all(); + let mut image = Image::new_uninit(size, dimension, format, asset_usage); + + image.texture_descriptor.usage = TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::RENDER_ATTACHMENT; + + image + } + + { + let img = render_target_image(); + let target = { + let handle = images.add(img); + output_images.tetris = handle.clone(); + let scale_factor = FloatOrd(1.0); + + RenderTarget::Image(ImageRenderTarget { + handle, + scale_factor, + }) + }; + commands.spawn(( + Camera2d, + Camera { + order: 1, + target, + ..default() + }, + TETRIS, + TetrisCamera, + )); + } + + { + let img = render_target_image(); + let target = { + let handle = images.add(img); + output_images.battler = handle.clone(); + let scale_factor = FloatOrd(1.0); + + RenderTarget::Image(ImageRenderTarget { + handle, + scale_factor, + }) + }; + commands.spawn(( + Camera2d, + Camera { + order: 2, + target, + ..default() + }, + BATTLER, + BattlerCamera, + )); + } +} + +fn init_ui(mut commands: Commands, output_images: Res) { commands .spawn(( Node { @@ -279,7 +386,6 @@ fn init_ui(mut commands: Commands) { flex_direction: FlexDirection::Column, ..default() }, - BackgroundColor(BLACK.into()), )) .with_children(|parent| { parent @@ -314,6 +420,50 @@ fn init_ui(mut commands: Commands) { }); }); + commands.spawn(( + Node { + align_self: AlignSelf::End, + justify_self: JustifySelf::Center, + border: UiRect::all(Val::Px(5.0)), + ..default() + }, + BorderColor(BLACK.into()), + children![( + Node { + width: Val::Px(256.0), + height: Val::Px(256.0), + ..default() + }, + ImageNode { + image: output_images.tetris.clone(), + image_mode: NodeImageMode::Stretch, + ..default() + }, + )] + )); + + commands.spawn(( + Node { + align_self: AlignSelf::Start, + justify_self: JustifySelf::Center, + border: UiRect::all(Val::Px(5.0)), + ..default() + }, + BorderColor(BLACK.into()), + children![( + Node { + width: Val::Px(256.0), + height: Val::Px(256.0), + ..default() + }, + ImageNode { + image: output_images.battler.clone(), + image_mode: NodeImageMode::Stretch, + ..default() + }, + )] + )); + commands .spawn(( Node { @@ -341,9 +491,7 @@ fn init_debug_ui(mut commands: Commands) { .with_children(|parent| { parent.spawn(( Node::default(), - children![ - (Text::new("SHAPE"), SyncSingleton::::default()), - ], + children![(Text::new("SHAPE"), SyncSingleton::::default()),], )); }); } @@ -555,7 +703,15 @@ fn update_position( // TODO: Move to trigger fn update_shape_blocks( - query: Query<(Entity, &Shape, &GridPosition), Or<(Added, Changed, Added, Changed)>>, + query: Query< + (Entity, &Shape, &GridPosition), + Or<( + Added, + Changed, + Added, + Changed, + )>, + >, mut blocks: Query<&mut GridPosition, (With, Without)>, mut commands: Commands, visuals: Res, @@ -571,7 +727,7 @@ fn update_shape_blocks( .with_related_entities::(|parent| { s.coordinates(center).for_each(|gp| { parent - .spawn((mesh.clone(), mat.clone(), gp.unwrap(), Block)) + .spawn((mesh.clone(), mat.clone(), gp.unwrap(), Block, TETRIS)) .observe(movement); }); }); @@ -673,8 +829,12 @@ fn add_piece(mut commands: Commands, mut shapes: ResMut) { let this_shape = shapes.next.pop_front().unwrap(); commands .spawn(( - GridPosition { y: Y_MAX - this_shape.height(), ..default() }, + GridPosition { + y: Y_MAX - this_shape.height(), + ..default() + }, this_shape, + TETRIS, )) .observe(movement) .observe(swap); @@ -690,10 +850,13 @@ fn clear_line( ) { let mut cleared_lines: Vec = changed_lines .iter() - .filter_map(|e| try { (Some(e)?, line_blocks.get(e).ok()?, lines.get(e).ok()?) } ) + .filter_map(|e| try { (Some(e)?, line_blocks.get(e).ok()?, lines.get(e).ok()?) }) .filter_map(|(e, lb, Line(i))| { if lb.0.len() == 10 { - commands.entity(e).despawn_related::().insert(LineBlocks::default()); + commands + .entity(e) + .despawn_related::() + .insert(LineBlocks::default()); score.0 += 1; info!("New score: {:?}", score.0); Some(*i) @@ -711,9 +874,14 @@ fn clear_line( { debug_assert_eq!(lines.iter().count(), 20, "There should be 20 lines"); // Check that all line numbers are present - lines.iter().map(|Line(i)| i).sorted().enumerate().for_each(|(i, line_num)| { - debug_assert_eq!(i, *line_num, "Line numbers should match their sorted index"); - }); + lines + .iter() + .map(|Line(i)| i) + .sorted() + .enumerate() + .for_each(|(i, line_num)| { + debug_assert_eq!(i, *line_num, "Line numbers should match their sorted index"); + }); } let original_cleared_lines_len = cleared_lines.len(); @@ -785,7 +953,9 @@ fn movement( Movement::Left => vec![center.with_offset(-1, 0)], Movement::Right => vec![center.with_offset(1, 0)], Movement::Rotate => vec![Ok(*center)], - Movement::Skip => (1..=center.y).map(|i| center.with_offset(0, -(i as isize))).collect(), + Movement::Skip => (1..=center.y) + .map(|i| center.with_offset(0, -(i as isize))) + .collect(), }; let new_shape = match trigger.event() { Movement::Down | Movement::Left | Movement::Right | Movement::Skip => *this_shape, @@ -860,7 +1030,10 @@ fn swap( // Copy current shape into store // De-activate entity store.0 = Some(*shapes.get(trigger.target()).unwrap()); - commands.entity(trigger.target()).despawn_related::().despawn(); + commands + .entity(trigger.target()) + .despawn_related::() + .despawn(); } Some(inner) => { // Copy current shape into store @@ -883,7 +1056,8 @@ fn deactivate_shape( let GridPosition { y, .. } = grid_positions.get(block).unwrap(); if let Some(parent_line) = lines .iter() - .find_map(|(e, Line(i))| (*y == *i).then_some(e)) { + .find_map(|(e, Line(i))| (*y == *i).then_some(e)) + { commands .entity(parent_line) .add_one_related::(block);