From 8a9b47be6b1c8f96e7692c5cd00895ca54185b43 Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Thu, 12 Oct 2023 23:51:17 -0700 Subject: [PATCH] Drastic rework of movement * Added `moves` ledger to Board * This should help with future work like undo, automated tests, net-play. * Refactored 2d and 3d displays to use BoardIndex -> Transform * This means we can have a generic system set the position of Entities based on moves on the Board. * Refactored 2d view to have centered board * (instead of shifting root entity which had poor behavior characteristics) --- src/display2d.rs | 180 ++++++++++++++++++++++++++--------------------- src/display3d.rs | 88 +---------------------- src/game.rs | 71 ++++++++++++++++++- 3 files changed, 167 insertions(+), 172 deletions(-) diff --git a/src/display2d.rs b/src/display2d.rs index 833878b..42f7be1 100644 --- a/src/display2d.rs +++ b/src/display2d.rs @@ -12,7 +12,7 @@ use bevy::{ /// TODO: Handle Cursor! /// use crate::{ - game::{ActiveTile, Board, BoardIndex, Piece}, + game::{ActiveTile, Board, BoardIndex, Piece, Tile}, prelude::*, }; @@ -42,15 +42,17 @@ impl Plugin for Display2dPlugin { .run_if(any_with_component::()), cancel_place .run_if(in_state(GameState::Display2d)) - .run_if(any_with_component::()), + .run_if(on_event::()), + snap_back_cancel + .run_if(in_state(GameState::Display2d)) + .run_if(any_component_removed::()), update_background.run_if(on_event::()), - draw_board - .run_if(resource_exists::()) - .run_if(any_component_removed::()) // trigger if item was de-selected - .run_if(any_with_component::()), + // PERF: Only need to run when piece2d or tile2d added + set_sprite, + set_transform, ), ) - .add_systems(OnEnter(GameState::Display2d), (activate, draw_board)) + .add_systems(OnEnter(GameState::Display2d), activate) .add_systems(OnExit(GameState::Display2d), deactivate); } } @@ -67,7 +69,10 @@ struct Board2d; /// Marker for 2d piece entities #[derive(Debug, Component)] -struct Piece2d; +pub(crate) struct Piece2d; + +#[derive(Debug, Component)] +struct Tile2d; #[derive(Debug, Component)] struct Display2dCamera; @@ -151,16 +156,11 @@ fn update_background( } /// STARTUP: Initialize the board for representation -fn initialize_board(sprite_sheet: Option>, mut commands: Commands) { - if let Some(sprite_sheet) = sprite_sheet { +fn initialize_board(board: Option>, mut commands: Commands) { + if let Some(board) = board { commands .spawn(( SpatialBundle { - transform: Transform::from_xyz( - -SCALE * TILE_SIZE * 7.0 / 2.0, // TODO: WHY??? - -SCALE * TILE_SIZE * 3.0 / 2.0, // Why 7 and 3?? - 0.0, - ), visibility: Visibility::Hidden, ..default() }, @@ -170,82 +170,78 @@ fn initialize_board(sprite_sheet: Option>, mut commands: Comman for i in 0..32 { let x = i % 8; let y = i / 8; - let s = (x % 2) ^ (y % 2); - - let transform = Transform::from_scale(Vec3::splat(SCALE)).with_translation( - Vec3::new(SCALE * 16.0 * x as f32, SCALE * 16.0 * y as f32, 0.0), - ); - - let sprite = TextureAtlasSprite::new(s); - - let texture_atlas = sprite_sheet.handle.clone(); - let index = BoardIndex { x, y }; - // Rectangle - parent.spawn(( - SpriteSheetBundle { - texture_atlas, - sprite, - transform, - ..default() - }, - index, - )); + let s = (x % 2) ^ (y % 2); + let tile = if s == 0 { Tile::Dark } else { Tile::Light }; + + parent.spawn((tile, index, Tile2d, SpriteSheetBundle { ..default() })); } - }); - } -} -/// Update the location of pieces on the board -fn draw_board( - board: Option>, - sprite_sheet: Option>, - root: Query>, - tiles: Query<(&Transform, &BoardIndex)>, - pieces: Query>, - mut commands: Commands, -) { - if let (Some(board), Some(sprite_sheet)) = (board, sprite_sheet) { - pieces - .iter() - .for_each(|entity| commands.entity(entity).despawn_recursive()); - commands.entity(root.single()).with_children(|parent| { - board - .pieces() - .iter() - .filter_map(|(board_index, piece)| { - tiles.iter().find_map(|(transform, this_index)| { - (*this_index == *board_index).then(|| (piece, transform, this_index)) - }) - }) - .for_each(|(piece, transform, index)| { - let texture_atlas = sprite_sheet.handle.clone(); - let s = match piece { - Piece::Queen => 2, - Piece::Drone => 3, - Piece::Pawn => 4, - }; - let sprite = TextureAtlasSprite::new(s); + board.pieces().iter().for_each(|(piece, index)| { // TODO: transform is slightly different, set sprite parent.spawn(( piece.clone(), - SpriteSheetBundle { - texture_atlas, - sprite, - transform: Transform { - ..transform.clone() - }, - ..default() - }, Piece2d, index.clone(), + SpriteSheetBundle { ..default() }, )); }); - }); + }); } } +fn set_sprite( + mut events: Query< + ( + &mut TextureAtlasSprite, + &mut Handle, + Option<&Piece>, + Option<&Tile>, + ), + Or<(Added, Added)>, + >, + sprite_sheet: Option>, +) { + if let Some(sprite_sheet) = sprite_sheet { + events + .iter_mut() + .for_each(|(mut sprite, mut texture_atlas, piece, tile)| { + *texture_atlas = sprite_sheet.handle.clone(); + let s = match piece { + Some(Piece::Queen) => 2, + Some(Piece::Drone) => 3, + Some(Piece::Pawn) => 4, + None => match tile { + Some(Tile::Dark) => 0, + Some(Tile::Light) => 1, + None => 99, + }, + }; + *sprite = TextureAtlasSprite::new(s); + debug!("Setting sprite for {:?} {:?}", piece, tile); + }); + } +} + +/// Sets a piece location given it's board index +fn set_transform( + mut events: Query< + (&mut Transform, &BoardIndex), + ( + Or<(With, With)>, + Or<(Changed, Added)>, + ), + >, +) { + events.iter_mut().for_each(|(mut t, i)| { + let x = SCALE * 16.0 * ((i.x as f32) - 3.5); + let y = SCALE * 16.0 * ((i.y as f32) - 1.5); + *t = Transform::from_scale(Vec3::splat(SCALE)).with_translation(Vec3::new(x, y, 0.0)); + info!("setting position of {:?} to {:?}", i, t); + }); +} + fn activate( mut cameras: Query<&mut Camera, With>, mut boards: Query<&mut Visibility, With>, @@ -296,7 +292,7 @@ fn active_tile( let size = { let sprite_size = atlases .get(handle) - .map(|atlas| atlas.textures.get(*index).expect("Get this rect")) + .map(|atlas| atlas.textures.get(*index).expect("Get this rect texture")) .expect("get this rect") .size(); let (transform_scale, _, _) = transform.to_scale_rotation_translation(); @@ -372,7 +368,8 @@ fn place_piece( active: Res, mut board: ResMut, mut commands: Commands, - mut writer: EventWriter, + mut game_events: EventWriter, + mut move_events: EventWriter, ) { events .iter() @@ -391,11 +388,14 @@ fn place_piece( )) }) .for_each(|(entity, from, to)| match board.move_piece(from, to) { - Ok(()) => { + Ok(moves) => { commands.entity(entity).remove::(); - writer.send(game::GameEvent::PlacePiece); + moves.iter().for_each(|m| { + move_events.send(m.clone()); + }); + game_events.send(game::GameEvent::PlacePiece); } - Err(game::GameError::NullMove) => writer.send(game::GameEvent::PlacePiece), + Err(game::GameError::NullMove) => game_events.send(game::GameEvent::PlacePiece), Err(game::GameError::InvalidMove) => warn!("Invalid move!"), }) } @@ -417,3 +417,19 @@ fn cancel_place( writer.send(game::GameEvent::PlacePiece); }) } + +fn snap_back_cancel( + mut events: RemovedComponents, + query: Query<&game::BoardIndex, With>, + mut move_events: EventWriter, +) { + events.iter().for_each(|entity| { + if let Ok(idx) = query.get(entity) { + move_events.send(game::Move { + epoch: 0, + from: idx.clone(), + to: Some(idx.clone()), + }); + } + }) +} diff --git a/src/display3d.rs b/src/display3d.rs index 754c7da..8af92c5 100644 --- a/src/display3d.rs +++ b/src/display3d.rs @@ -11,12 +11,6 @@ impl Plugin for Display3dPlugin { Update, menu::exit_to_menu.run_if(in_state(GameState::Display3d)), ) - .add_systems( - Update, - update_board - // .run_if(in_state(GameState::Display3d)) - .run_if(resource_changed::()), - ) .add_systems( Update, set_piece_position.run_if(in_state(GameState::Display3d)), @@ -36,7 +30,7 @@ impl Plugin for Display3dPlugin { struct Board3d; #[derive(Debug, Component)] -struct Piece3d; +pub(crate) struct Piece3d; fn initialize_camera(mut commands: Commands) { commands.spawn(( @@ -157,86 +151,6 @@ fn initialize_board( } } -/// Update the location of pieces on the 3d board -fn update_board( - board: Option>, - mut pieces: Query<(Entity, &mut BoardIndex, &game::Piece), With>, - mut commands: Commands, -) { - if let Some(board) = board { - let board_pieces = board.pieces(); - - { - // Get a list of all Queen piece entities - let mut queen_entities = pieces - .iter_mut() - .filter_map(|(entity, idx, p)| (*p == game::Piece::Queen).then_some((entity, idx))); - - // Compare each board piece with an entity piece - // Set the board index for this particular queen - board_pieces - .iter() - .filter_map(|(index, p)| (*p == game::Piece::Queen).then_some(index)) - .for_each(|index| { - if let Some((_, mut idx)) = queen_entities.next() { - *idx = index.clone(); - } - }); - // If we have one left that means this queen has been "destroyed" so we remove the - // BoardIndex component from the entitiy, removing it from play. - if let Some((entity, _)) = queen_entities.next() { - commands.entity(entity).remove::(); - } - } - - { - // Get a list of all drone piece entities - let mut drone_entities = pieces - .iter_mut() - .filter_map(|(entity, idx, p)| (*p == game::Piece::Drone).then_some((entity, idx))); - - // Compare each board piece with an entity piece - // Set the board index for this particular drone - board_pieces - .iter() - .filter_map(|(index, p)| (*p == game::Piece::Drone).then_some(index)) - .for_each(|index| { - if let Some((_, mut idx)) = drone_entities.next() { - *idx = index.clone(); - } - }); - // If we have one left that means this drone has been "destroyed" so we remove the - // BoardIndex component from the entitiy, removing it from play. - if let Some((entity, _)) = drone_entities.next() { - commands.entity(entity).remove::(); - } - } - - { - // Get a list of all pawn piece entities - let mut pawn_entities = pieces - .iter_mut() - .filter_map(|(entity, idx, p)| (*p == game::Piece::Pawn).then_some((entity, idx))); - - // Compare each board piece with an entity piece - // Set the board index for this particular pawn - board_pieces - .iter() - .filter_map(|(index, p)| (*p == game::Piece::Pawn).then_some(index)) - .for_each(|index| { - if let Some((_, mut idx)) = pawn_entities.next() { - *idx = index.clone(); - } - }); - // If we have one left that means this pawn has been "destroyed" so we remove the - // BoardIndex component from the entitiy, removing it from play. - if let Some((entity, _)) = pawn_entities.next() { - commands.entity(entity).remove::(); - } - } - } -} - /// Sets a piece location given it's board index fn set_piece_position( mut events: Query<(&mut Transform, &BoardIndex), (With, Changed)>, diff --git a/src/game.rs b/src/game.rs index c88591f..01ea063 100644 --- a/src/game.rs +++ b/src/game.rs @@ -5,8 +5,17 @@ pub(crate) struct GamePlugin; impl Plugin for GamePlugin { fn build(&self, app: &mut App) { app.add_event::() + .add_event::() .init_resource::() .add_systems(Startup, setup_board) + .add_systems( + Update, + update_board::.run_if(on_event::()), + ) + .add_systems( + Update, + update_board::.run_if(on_event::()), + ) .add_systems( PostUpdate, ( @@ -26,6 +35,12 @@ pub(crate) enum Piece { Queen, } +#[derive(Debug, Component, Clone, PartialEq)] +pub(crate) enum Tile { + Dark, + Light, +} + // manually for the type. impl std::fmt::Display for Piece { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { @@ -65,9 +80,17 @@ pub(crate) enum GameEvent { #[derive(Debug, Resource)] pub(crate) struct Board { inner: Vec>>, + moves: Vec, } -#[derive(Debug, Component, PartialEq, Clone)] +#[derive(Debug, Default, Event, Clone)] +pub(crate) struct Move { + pub epoch: usize, + pub from: BoardIndex, + pub to: Option, +} + +#[derive(Debug, Component, PartialEq, Clone, Default)] pub(crate) struct BoardIndex { pub x: usize, pub y: usize, @@ -96,15 +119,35 @@ impl Board { &mut self, from: &BoardIndex, to: &BoardIndex, - ) -> Result<(), GameError> { + ) -> Result, GameError> { if from == to { Err(GameError::NullMove) } else if self.at(to).is_none() { self.at(from).map_or(Err(GameError::NullMove), |from_val| { + // The current epoch is the last epoch + 1 + let epoch = self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1; + // Local moves vec we can return + let mut moves = vec![]; + // If the position we are moving to is occupied, capture the removal in the ledger + if self.inner[to.y][to.x].is_some() { + moves.push(Move { + epoch, + from: to.clone(), + to: None, + }); + } + // Capture the intened move in the moves ledger + moves.push(Move { + epoch, + from: from.clone(), + to: Some(to.clone()), + }); + self.moves.extend(moves.clone()); // TODO: We can self.inner.swap(to, from) if board is single vec + // Update board to reflect move self.inner[to.y][to.x] = Some(from_val); self.inner[from.y][from.x] = None; - Ok(()) + Ok(moves) }) } else { Err(GameError::InvalidMove) @@ -141,6 +184,7 @@ pub(crate) struct Selected; fn setup_board(mut commands: Commands) { use Piece::*; commands.insert_resource(Board { + moves: vec![], inner: vec![ vec![ Some(Queen), @@ -204,3 +248,24 @@ fn debug_hovering( fn debug_board(board: Res, mut debug_info: ResMut) { debug_info.set("board".into(), format!("\n{}", *board)); } + +/// Update this method to use a diff between the board and the state of the 2d/3d worlds +/// Only update the tiles without a corresponding board piece +fn update_board( + mut events: EventReader, + mut pieces: Query<(Entity, &mut BoardIndex), With>, + mut commands: Commands, +) { + events.iter().for_each(|Move { from, to, .. }| { + pieces + .iter_mut() + .filter(|(_, index)| **index == *from) + .for_each(|(entity, mut index)| { + if let Some(idx) = to { + *index = idx.clone(); + } else { + commands.entity(entity).remove::(); + } + }) + }) +}