Implemented line clearing

master
hheik 2025-08-22 00:26:31 +03:00
parent 381609c261
commit c8fa0bf4ed
4 changed files with 222 additions and 71 deletions

View File

@ -9,7 +9,7 @@ pub fn game_area_gizmos(mut gizmos: Gizmos, game_area_query: Query<(&GameArea, &
Isometry2d::new(game_area.center() * grid.tile_size, Rot2::IDENTITY), Isometry2d::new(game_area.center() * grid.tile_size, Rot2::IDENTITY),
UVec2::new( UVec2::new(
(game_area.right_boundary - game_area.left_boundary) as u32 + 1, (game_area.right_boundary - game_area.left_boundary) as u32 + 1,
(game_area.top_boundary - game_area.bottom_boundary) as u32 + 1, (game_area.kill_height - game_area.bottom_boundary) as u32 + 1,
), ),
grid.tile_size, grid.tile_size,
Color::srgba(0.4, 0.1, 0.1, 0.6), Color::srgba(0.4, 0.1, 0.1, 0.6),

View File

@ -28,8 +28,5 @@ pub fn init(app: &mut App) {
.add_systems(Startup, systems::setup_game_scene) .add_systems(Startup, systems::setup_game_scene)
.add_systems(PreUpdate, (systems::block_control, systems::apply_gravity)) .add_systems(PreUpdate, (systems::block_control, systems::apply_gravity))
.add_systems(Update, (systems::demo_2d, systems::apply_piece_movement)) .add_systems(Update, (systems::demo_2d, systems::apply_piece_movement))
.add_systems( .add_systems(PostUpdate, systems::grid_positioning);
PostUpdate,
(systems::handle_piece_placed, systems::grid_positioning).chain(),
);
} }

View File

@ -1,3 +1,5 @@
use std::collections::{HashMap, HashSet};
use bevy::prelude::*; use bevy::prelude::*;
use crate::game::{ use crate::game::{
@ -10,23 +12,26 @@ pub fn setup_game_scene(world: &mut World) {
let game_area = GameArea::default(); let game_area = GameArea::default();
let mut next_piece = NextPiece::default(); let mut next_piece = NextPiece::default();
let starting_piece = next_piece.take_and_generate(); let starting_piece = next_piece.take_and_generate();
world.spawn(( world
Name::from("Game scene"), .spawn((
GameGravity::default(), Name::from("Game scene"),
game_area.clone(), GameGravity::default(),
next_piece, game_area.clone(),
children![ next_piece,
(Transform::from_xyz(-16.0, 88.0, 10.0), DemoCamera2d,), children![
// Create first piece (Transform::from_xyz(-16.0, 88.0, 10.0), DemoCamera2d,),
( // Create first piece
GridTransform { (
translation: game_area.block_spawn_point() GridTransform {
}, translation: game_area.block_spawn_point()
ControllablePiece, },
create_piece(starting_piece), ControllablePiece,
), create_piece(starting_piece),
], ),
)); ],
))
.observe(handle_placed_piece)
.observe(handle_line_clear);
world.flush(); world.flush();
} }
@ -85,8 +90,9 @@ pub fn apply_piece_movement(
mut piece_query: Query<(&mut PieceControls, Entity, &ChildOf, &Children)>, mut piece_query: Query<(&mut PieceControls, Entity, &ChildOf, &Children)>,
mut transform_query: Query<(&mut GridTransform, Has<Block>)>, mut transform_query: Query<(&mut GridTransform, Has<Block>)>,
mut input_repeat: Local<Option<InputRepeatState>>, mut input_repeat: Local<Option<InputRepeatState>>,
mut on_piece_placed: EventWriter<OnPiecePlaced>, // mut on_piece_placed: EventWriter<OnPiecePlaced>,
time: Res<Time>, time: Res<Time>,
mut commands: Commands,
) { ) {
// TODO: Handle piece collision // TODO: Handle piece collision
for (mut piece_controls, entity, parent, children) in piece_query.iter_mut() { for (mut piece_controls, entity, parent, children) in piece_query.iter_mut() {
@ -94,7 +100,8 @@ pub fn apply_piece_movement(
// TODO: replace with actual logic // TODO: replace with actual logic
if piece_controls.instant_drop { if piece_controls.instant_drop {
grid_transform.translation.y = 0; grid_transform.translation.y = 0;
on_piece_placed.write(OnPiecePlaced { piece: entity }); // on_piece_placed.write(OnPiecePlaced { piece: entity });
commands.trigger_targets(OnPiecePlaced { piece: entity }, parent.parent());
*input_repeat = None; *input_repeat = None;
continue; continue;
} }
@ -105,7 +112,8 @@ pub fn apply_piece_movement(
} }
if !can_move_down && piece_controls.cumulated_gravity >= 3.0 { if !can_move_down && piece_controls.cumulated_gravity >= 3.0 {
// Force piece placement // Force piece placement
on_piece_placed.write(OnPiecePlaced { piece: entity }); // on_piece_placed.write(OnPiecePlaced { piece: entity });
commands.trigger_targets(OnPiecePlaced { piece: entity }, parent.parent());
*input_repeat = None; *input_repeat = None;
continue; continue;
} }
@ -148,58 +156,184 @@ pub fn apply_piece_movement(
} }
} }
pub fn handle_piece_placed( pub fn handle_placed_piece(trigger: Trigger<OnPiecePlaced>, world: &mut World) {
mut on_piece_placed: EventReader<OnPiecePlaced>, let game_area = trigger.target();
let event = *trigger.event();
// for game_area in world
// .run_system_cached(place_blocks)
// .iter()
// .flatten()
// .cloned()
// {
world
.run_system_cached_with(place_blocks, (event, game_area))
.expect("Placing piece");
world.flush();
let cleared_rows = world
.run_system_cached_with(check_full_rows, game_area)
.expect("Checking for completed lines");
if !cleared_rows.is_empty() {
world.trigger_targets(
OnLinesCleared {
lines: cleared_rows,
},
game_area,
);
// world
// .run_system_cached_with(clear_rows, cleared_rows)
// .expect("Clearing full rows");
world.flush();
}
world
.run_system_cached_with(check_lose_condition, game_area)
.expect("Checking lose condition");
world
.run_system_cached_with(create_next_piece, game_area)
.expect("Creating new piece");
world.flush();
// }
// TODO: Finish up clearing the piece / lines
// TODO: Lose condition
}
/// Returns a vector of affected game area entities
fn place_blocks(
// mut piece_placed_events: EventReader<OnPiecePlaced>,
In((event, game_area)): In<(OnPiecePlaced, Entity)>,
mut commands: Commands, mut commands: Commands,
parent_query: Query<&ChildOf>,
child_query: Query<&Children>, child_query: Query<&Children>,
grid_transform_query: Query<&GridTransform>, grid_transform_query: Query<&GridTransform>,
block_query: Query<&GridTransform, With<Block>>, block_query: Query<&GridTransform, With<Block>>,
mut next_piece_query: Query<(&mut NextPiece, &GameArea)>,
) { ) {
for OnPiecePlaced { piece } in on_piece_placed.read() { let piece = event.piece;
let piece = *piece;
let parent = parent_query.get(piece).unwrap().0; // Move blocks from old piece to under game area
let (mut next_piece, game_area) = next_piece_query.get_mut(parent).unwrap(); let piece_position = grid_transform_query.get(piece).cloned().unwrap_or_default();
let new_piece = next_piece.take_and_generate(); for child in child_query
.get(piece)
.iter()
.flat_map(|children| children.iter())
{
if let Ok(block_pos) = block_query.get(child) {
let global_pos = piece_position.translation + block_pos.translation;
commands.entity(child).insert((
GridTransform {
translation: global_pos,
},
ChildOf(game_area),
));
}
}
// Create new piece // Despawn old piece
commands.entity(parent).with_child(( commands.entity(piece).despawn();
GridTransform { }
translation: game_area.block_spawn_point(),
},
ControllablePiece,
create_piece(new_piece),
));
// Move blocks from old piece to under game area fn check_full_rows(
let piece_position = grid_transform_query.get(piece).cloned().unwrap_or_default(); In(game_area_entity): In<Entity>,
for child in child_query game_area_query: Query<(&GameArea, &Children)>,
.get(piece) block_query: Query<&GridTransform, With<Block>>,
.iter() ) -> Vec<YPos> {
.flat_map(|children| children.iter()) let mut row_map: HashMap<YPos, HashSet<XPos>> = HashMap::new();
{ let (game_area, children) = game_area_query
if let Ok(block_pos) = block_query.get(child) { .get(game_area_entity)
let global_pos = piece_position.translation + block_pos.translation; .expect("Getting GameArea component to check for cleared rows");
commands.entity(child).insert((
GridTransform { for child in children.iter() {
translation: global_pos, let pos = match block_query.get(child) {
}, Ok(transform) => transform.translation,
ChildOf(parent), _ => continue,
)); };
if !game_area.is_point_inside(pos) {
warn!("Block outside of game bounds! {pos} ({child})");
continue;
}
if !row_map.entry(pos.y).or_default().insert(pos.x) {
warn!("Duplicate block at position {pos} ({child})");
}
}
// The Y-coordinates of cleared rows
let mut cleared_rows = vec![];
for (y, row_set) in row_map.iter() {
// Detect if the row is full
if row_set.len() as i32 == game_area.right_boundary - game_area.left_boundary + 1 {
cleared_rows.push(*y);
}
}
// Sort rows from up to down
cleared_rows.sort_unstable_by(|a, b| b.cmp(a));
cleared_rows
}
fn check_lose_condition(In(game_area_entity): In<Entity>) {}
fn create_next_piece(
In(game_area_entity): In<Entity>,
mut game_query: Query<(&mut NextPiece, &GameArea)>,
mut commands: Commands,
) {
let (mut next_piece, game_area) = game_query.get_mut(game_area_entity).unwrap();
commands.entity(game_area_entity).with_child((
GridTransform {
translation: game_area.block_spawn_point(),
},
ControllablePiece,
create_piece(next_piece.take_and_generate()),
));
}
fn handle_line_clear(
trigger: Trigger<OnLinesCleared>,
game_area_query: Query<&Children>,
mut block_query: Query<(Entity, &mut GridTransform), With<Block>>,
mut commands: Commands,
) {
let lines = trigger.event().lines.clone();
let children = game_area_query
.get(trigger.target())
.expect("Getting GameArea component to clear rows");
enum BlockOperation {
Nothing,
Move(YPos),
Remove,
}
let choose_block_operation = |y: YPos| {
let mut move_down = 0;
for cleared_y in lines.iter() {
if y == *cleared_y {
return BlockOperation::Remove;
} else if y > *cleared_y {
move_down += 1;
} }
} }
if move_down == 0 {
BlockOperation::Nothing
} else {
BlockOperation::Move(y - move_down)
}
};
// Despawn old piece let mut memoized: HashMap<YPos, BlockOperation> = HashMap::new();
commands.entity(piece).despawn();
// commands for child in children.iter() {
// .get_entity(event.entity) let (block_entity, mut transform) = match block_query.get_mut(child) {
// .unwrap() Ok(transform) => transform,
// .remove::<(PieceControls, ControllablePiece)>(); _ => continue,
};
// TODO: Finish up clearing the piece / lines let operation = memoized
// TODO: Lose condition .entry(transform.translation.y)
.or_insert(choose_block_operation(transform.translation.y));
match operation {
BlockOperation::Move(new_y) => transform.translation.y = *new_y,
BlockOperation::Remove => commands.entity(block_entity).despawn(),
_ => (),
};
} }
} }

View File

@ -4,12 +4,15 @@ use crate::util::Vector2I;
use super::{grid::*, prefab::*}; use super::{grid::*, prefab::*};
pub type YPos = i32;
pub type XPos = i32;
#[derive(Component, Clone, Debug, Reflect)] #[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)] #[reflect(Component)]
#[require(Grid, NextPiece)] #[require(Grid, NextPiece)]
pub struct GameArea { pub struct GameArea {
pub bottom_boundary: i32, pub bottom_boundary: i32,
pub top_boundary: i32, pub kill_height: i32,
pub left_boundary: i32, pub left_boundary: i32,
pub right_boundary: i32, pub right_boundary: i32,
} }
@ -18,7 +21,7 @@ impl Default for GameArea {
fn default() -> Self { fn default() -> Self {
Self { Self {
bottom_boundary: 0, bottom_boundary: 0,
top_boundary: 20, kill_height: 19,
left_boundary: 0, left_boundary: 0,
right_boundary: 9, right_boundary: 9,
} }
@ -29,16 +32,28 @@ impl GameArea {
pub fn block_spawn_point(&self) -> Vector2I { pub fn block_spawn_point(&self) -> Vector2I {
Vector2I::new( Vector2I::new(
(self.left_boundary + self.right_boundary) / 2, (self.left_boundary + self.right_boundary) / 2,
self.top_boundary + 2, self.kill_height + 2,
) )
} }
pub fn center(&self) -> Vec2 { pub fn center(&self) -> Vec2 {
Vec2 { Vec2 {
x: (self.left_boundary + self.right_boundary) as f32 / 2.0, x: (self.left_boundary + self.right_boundary) as f32 / 2.0,
y: (self.bottom_boundary + self.top_boundary) as f32 / 2.0, y: (self.bottom_boundary + self.kill_height) as f32 / 2.0,
} }
} }
/// Checks if the point is considered inside the game area.
/// This basically means the the point is not below the `bottom_boundary` or outside of the
/// horizontal boundaries.
///
/// The game area has no upper boundary, so points above the kill height are still considered
/// inside, unless it's beyond any of the other boundaries.
pub fn is_point_inside(&self, point: Vector2I) -> bool {
point.x >= self.left_boundary
&& point.x <= self.right_boundary
&& point.y >= self.bottom_boundary
}
} }
#[derive(Component, Debug, Reflect)] #[derive(Component, Debug, Reflect)]
@ -127,7 +142,12 @@ impl NextPiece {
#[reflect(Component)] #[reflect(Component)]
pub struct Block; pub struct Block;
#[derive(Event, Clone, Debug)] #[derive(Event, Clone, Copy, Debug)]
pub struct OnPiecePlaced { pub struct OnPiecePlaced {
pub piece: Entity, pub piece: Entity,
} }
#[derive(Event, Clone, Debug)]
pub struct OnLinesCleared {
pub lines: Vec<YPos>,
}