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),
UVec2::new(
(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,
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(PreUpdate, (systems::block_control, systems::apply_gravity))
.add_systems(Update, (systems::demo_2d, systems::apply_piece_movement))
.add_systems(
PostUpdate,
(systems::handle_piece_placed, systems::grid_positioning).chain(),
);
.add_systems(PostUpdate, systems::grid_positioning);
}

View File

@ -1,3 +1,5 @@
use std::collections::{HashMap, HashSet};
use bevy::prelude::*;
use crate::game::{
@ -10,23 +12,26 @@ pub fn setup_game_scene(world: &mut World) {
let game_area = GameArea::default();
let mut next_piece = NextPiece::default();
let starting_piece = next_piece.take_and_generate();
world.spawn((
Name::from("Game scene"),
GameGravity::default(),
game_area.clone(),
next_piece,
children![
(Transform::from_xyz(-16.0, 88.0, 10.0), DemoCamera2d,),
// Create first piece
(
GridTransform {
translation: game_area.block_spawn_point()
},
ControllablePiece,
create_piece(starting_piece),
),
],
));
world
.spawn((
Name::from("Game scene"),
GameGravity::default(),
game_area.clone(),
next_piece,
children![
(Transform::from_xyz(-16.0, 88.0, 10.0), DemoCamera2d,),
// Create first piece
(
GridTransform {
translation: game_area.block_spawn_point()
},
ControllablePiece,
create_piece(starting_piece),
),
],
))
.observe(handle_placed_piece)
.observe(handle_line_clear);
world.flush();
}
@ -85,8 +90,9 @@ pub fn apply_piece_movement(
mut piece_query: Query<(&mut PieceControls, Entity, &ChildOf, &Children)>,
mut transform_query: Query<(&mut GridTransform, Has<Block>)>,
mut input_repeat: Local<Option<InputRepeatState>>,
mut on_piece_placed: EventWriter<OnPiecePlaced>,
// mut on_piece_placed: EventWriter<OnPiecePlaced>,
time: Res<Time>,
mut commands: Commands,
) {
// TODO: Handle piece collision
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
if piece_controls.instant_drop {
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;
continue;
}
@ -105,7 +112,8 @@ pub fn apply_piece_movement(
}
if !can_move_down && piece_controls.cumulated_gravity >= 3.0 {
// 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;
continue;
}
@ -148,58 +156,184 @@ pub fn apply_piece_movement(
}
}
pub fn handle_piece_placed(
mut on_piece_placed: EventReader<OnPiecePlaced>,
pub fn handle_placed_piece(trigger: Trigger<OnPiecePlaced>, world: &mut World) {
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,
parent_query: Query<&ChildOf>,
child_query: Query<&Children>,
grid_transform_query: Query<&GridTransform>,
block_query: Query<&GridTransform, With<Block>>,
mut next_piece_query: Query<(&mut NextPiece, &GameArea)>,
) {
for OnPiecePlaced { piece } in on_piece_placed.read() {
let piece = *piece;
let piece = event.piece;
let parent = parent_query.get(piece).unwrap().0;
let (mut next_piece, game_area) = next_piece_query.get_mut(parent).unwrap();
let new_piece = next_piece.take_and_generate();
// Move blocks from old piece to under game area
let piece_position = grid_transform_query.get(piece).cloned().unwrap_or_default();
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
commands.entity(parent).with_child((
GridTransform {
translation: game_area.block_spawn_point(),
},
ControllablePiece,
create_piece(new_piece),
));
// Despawn old piece
commands.entity(piece).despawn();
}
// Move blocks from old piece to under game area
let piece_position = grid_transform_query.get(piece).cloned().unwrap_or_default();
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(parent),
));
fn check_full_rows(
In(game_area_entity): In<Entity>,
game_area_query: Query<(&GameArea, &Children)>,
block_query: Query<&GridTransform, With<Block>>,
) -> Vec<YPos> {
let mut row_map: HashMap<YPos, HashSet<XPos>> = HashMap::new();
let (game_area, children) = game_area_query
.get(game_area_entity)
.expect("Getting GameArea component to check for cleared rows");
for child in children.iter() {
let pos = match block_query.get(child) {
Ok(transform) => transform.translation,
_ => 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
commands.entity(piece).despawn();
let mut memoized: HashMap<YPos, BlockOperation> = HashMap::new();
// commands
// .get_entity(event.entity)
// .unwrap()
// .remove::<(PieceControls, ControllablePiece)>();
// TODO: Finish up clearing the piece / lines
// TODO: Lose condition
for child in children.iter() {
let (block_entity, mut transform) = match block_query.get_mut(child) {
Ok(transform) => transform,
_ => continue,
};
let operation = memoized
.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::*};
pub type YPos = i32;
pub type XPos = i32;
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)]
#[require(Grid, NextPiece)]
pub struct GameArea {
pub bottom_boundary: i32,
pub top_boundary: i32,
pub kill_height: i32,
pub left_boundary: i32,
pub right_boundary: i32,
}
@ -18,7 +21,7 @@ impl Default for GameArea {
fn default() -> Self {
Self {
bottom_boundary: 0,
top_boundary: 20,
kill_height: 19,
left_boundary: 0,
right_boundary: 9,
}
@ -29,16 +32,28 @@ impl GameArea {
pub fn block_spawn_point(&self) -> Vector2I {
Vector2I::new(
(self.left_boundary + self.right_boundary) / 2,
self.top_boundary + 2,
self.kill_height + 2,
)
}
pub fn center(&self) -> Vec2 {
Vec2 {
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)]
@ -127,7 +142,12 @@ impl NextPiece {
#[reflect(Component)]
pub struct Block;
#[derive(Event, Clone, Debug)]
#[derive(Event, Clone, Copy, Debug)]
pub struct OnPiecePlaced {
pub piece: Entity,
}
#[derive(Event, Clone, Debug)]
pub struct OnLinesCleared {
pub lines: Vec<YPos>,
}