Added piece collision

master
hheik 2025-08-23 00:38:10 +03:00
parent 8d091fec47
commit db238c3248
7 changed files with 285 additions and 158 deletions

View File

@ -17,16 +17,19 @@ pub fn init(app: &mut App) {
app.add_event::<tetris::OnPiecePlaced>()
.register_type::<tetris::GameArea>()
.register_type::<tetris::GameGravity>()
.register_type::<tetris::BlockSet>()
.register_type::<tetris::NextPiece>()
.register_type::<tetris::Piece>()
.register_type::<tetris::PieceControls>()
.register_type::<tetris::ControllablePiece>()
.register_type::<tetris::Gravity>()
.register_type::<tetris::Block>()
.register_type::<prefab::PieceType>()
.register_type::<grid::Grid>()
.register_type::<grid::GridTransform>()
.add_systems(Startup, systems::setup_game_scene)
.add_systems(PreUpdate, (systems::block_control, systems::apply_gravity))
.add_systems(PreUpdate, (systems::repeat_inputs, systems::apply_gravity))
.add_systems(Update, (systems::demo_2d, systems::apply_piece_movement))
.add_systems(PostUpdate, systems::grid_positioning);
}

View File

@ -1,7 +1,11 @@
mod demo;
mod game_scene;
mod grid;
mod input;
mod movement;
pub use demo::*;
pub use game_scene::*;
pub use grid::*;
pub use input::*;
pub use movement::*;

View File

@ -2,11 +2,7 @@ use std::collections::{HashMap, HashSet};
use bevy::prelude::*;
use crate::game::{
grid::*,
prefab::*,
tetris::{piece_input, *},
};
use crate::game::{grid::*, prefab::*, tetris::*};
pub fn setup_game_scene(world: &mut World) {
let game_area = GameArea::default();
@ -35,127 +31,6 @@ pub fn setup_game_scene(world: &mut World) {
world.flush();
}
pub fn block_control(
time: Res<Time>,
key_input: Res<ButtonInput<KeyCode>>,
mut piece_query: Query<&mut PieceControls, With<ControllablePiece>>,
) {
const SOFT_DROP_SPEED: f32 = 10.0;
for mut piece in piece_query.iter_mut() {
piece.reset_inputs();
if key_input.pressed(KeyCode::ArrowDown) {
piece.cumulated_gravity += SOFT_DROP_SPEED * time.delta_secs();
}
if key_input.just_pressed(KeyCode::Space) {
piece.instant_drop = true;
}
if key_input.just_pressed(KeyCode::ArrowUp) {
piece.rotation = Some(piece_input::Rotation::Clockwise);
}
if key_input.just_pressed(KeyCode::ShiftLeft) {
piece.rotation = Some(piece_input::Rotation::CounterClockwise);
}
match (
key_input.pressed(KeyCode::ArrowLeft),
key_input.pressed(KeyCode::ArrowRight),
) {
(true, false) => piece.movement = Some(piece_input::Move::Left),
(false, true) => piece.movement = Some(piece_input::Move::Right),
_ => (),
}
}
}
pub fn apply_gravity(
mut piece_query: Query<(&mut PieceControls, &ChildOf), With<ControllablePiece>>,
time: Res<Time>,
gravity_query: Query<&GameGravity>,
) {
for (mut piece, parent) in piece_query.iter_mut() {
if let Ok(gravity) = gravity_query.get(parent.0) {
piece.cumulated_gravity += gravity.current * time.delta_secs();
}
}
}
pub struct InputRepeatState {
pub input_type: piece_input::Move,
pub cumulated_time: f32,
pub next_interval: f32,
pub interval_reduction: f32,
pub min_interval: f32,
}
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>,
time: Res<Time>,
mut commands: Commands,
) {
// TODO: Handle piece collision
for (mut piece_controls, entity, parent, children) in piece_query.iter_mut() {
let (mut grid_transform, _) = transform_query.get_mut(entity).unwrap();
// TODO: replace with actual logic
if piece_controls.instant_drop {
grid_transform.translation.y = 0;
// on_piece_placed.write(OnPiecePlaced { piece: entity });
commands.trigger_targets(OnPiecePlaced { piece: entity }, parent.parent());
*input_repeat = None;
continue;
}
let can_move_down = grid_transform.translation.y > 0;
if can_move_down && piece_controls.cumulated_gravity >= 1.0 {
piece_controls.cumulated_gravity -= 1.0;
grid_transform.translation.y -= 1;
}
if !can_move_down && piece_controls.cumulated_gravity >= 3.0 {
// Force piece placement
// on_piece_placed.write(OnPiecePlaced { piece: entity });
commands.trigger_targets(OnPiecePlaced { piece: entity }, parent.parent());
*input_repeat = None;
continue;
}
if let Some(movement) = piece_controls.movement {
let mut move_piece = false;
if input_repeat
.as_ref()
.is_some_and(|state| state.input_type == movement)
{
if let Some(input_repeat) = input_repeat.as_mut() {
input_repeat.cumulated_time += time.delta_secs();
if input_repeat.cumulated_time >= input_repeat.next_interval {
input_repeat.cumulated_time = 0.0;
input_repeat.next_interval = (input_repeat.next_interval
* input_repeat.interval_reduction)
.max(input_repeat.min_interval);
move_piece = true;
}
}
} else {
*input_repeat = Some(InputRepeatState {
input_type: movement,
cumulated_time: 0.0,
next_interval: 0.2,
interval_reduction: 0.0,
min_interval: 0.03,
});
move_piece = true;
}
if move_piece {
grid_transform.translation.x += match movement {
piece_input::Move::Left => -1,
piece_input::Move::Right => 1,
}
}
} else {
*input_repeat = None;
}
}
}
pub fn handle_placed_piece(trigger: Trigger<OnPiecePlaced>, world: &mut World) {
let game_area = trigger.target();
let event = *trigger.event();
@ -178,6 +53,10 @@ pub fn handle_placed_piece(trigger: Trigger<OnPiecePlaced>, world: &mut World) {
world.flush();
}
world
.run_system_cached_with(rebuild_collisions, game_area)
.expect("Rebuilding collisions");
world
.run_system_cached_with(check_lose_condition, game_area)
.expect("Checking lose condition");
@ -187,13 +66,28 @@ pub fn handle_placed_piece(trigger: Trigger<OnPiecePlaced>, world: &mut World) {
.expect("Creating new piece");
world.flush();
// TODO: Finish up clearing the piece / lines
// TODO: Lose condition
}
fn rebuild_collisions(
In(game_entity): In<Entity>,
game_query: Query<&Children>,
block_query: Query<&GridTransform, With<Block>>,
mut commands: Commands,
) {
if let Ok(children) = game_query.get(game_entity) {
let mut blocks = HashSet::new();
for child in children.iter() {
if let Ok(transform) = block_query.get(child) {
blocks.insert(transform.translation);
}
}
commands.entity(game_entity).insert(BlockSet::new(blocks));
}
}
/// 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,
child_query: Query<&Children>,

53
src/game/systems/input.rs Normal file
View File

@ -0,0 +1,53 @@
use bevy::prelude::*;
use crate::game::tetris::*;
pub fn repeat_inputs(
key_input: Res<ButtonInput<KeyCode>>,
mut repeat_move: Local<input::InputRepeat>,
mut repeat_rotate: Local<input::InputRepeat>,
time: Res<Time>,
mut controllable_query: Query<&mut PieceControls, With<ControllablePiece>>,
) {
for mut controls in controllable_query.iter_mut() {
*controls = PieceControls::default();
let delta_secs = time.delta_secs();
let move_dir = match (
key_input.pressed(KeyCode::ArrowLeft),
key_input.pressed(KeyCode::ArrowRight),
) {
(true, false) => Some(input::Move::Left),
(false, true) => Some(input::Move::Right),
_ => None,
};
controls.movement = if repeat_move.should_repeat(move_dir.is_some(), delta_secs) {
move_dir
} else {
None
};
let rotate_dir = match (
key_input.pressed(KeyCode::ArrowUp),
key_input.pressed(KeyCode::ShiftLeft),
) {
(true, false) => Some(input::Rotation::Clockwise),
(false, true) => Some(input::Rotation::CounterClockwise),
_ => None,
};
controls.rotation = if repeat_rotate.should_repeat(rotate_dir.is_some(), delta_secs) {
rotate_dir
} else {
None
};
if key_input.pressed(KeyCode::ArrowDown) {
controls.fast_drop = true;
}
if key_input.just_pressed(KeyCode::Space) {
controls.instant_drop = true;
}
}
}

View File

@ -0,0 +1,99 @@
use bevy::prelude::*;
use crate::{
game::{grid::*, tetris::*},
util::Vector2I,
};
pub fn apply_gravity(
mut piece_query: Query<(&mut Gravity, &PieceControls, &ChildOf)>,
time: Res<Time>,
game_gravity_query: Query<&GameGravity>,
) {
const SOFT_DROP_SPEED: f32 = 10.0;
for (mut piece_gravity, controls, parent) in piece_query.iter_mut() {
if let Ok(game_gravity) = game_gravity_query.get(parent.0) {
piece_gravity.cumulated += game_gravity.current * time.delta_secs();
}
if controls.fast_drop {
piece_gravity.cumulated += SOFT_DROP_SPEED * time.delta_secs();
}
}
}
pub fn apply_piece_movement(
mut piece_query: Query<(
&PieceControls,
Option<&mut Gravity>,
Entity,
&ChildOf,
&Children,
)>,
game_query: Query<(&BlockSet, &GameArea)>,
mut transform_query: Query<(&mut GridTransform, Has<Block>)>,
mut commands: Commands,
) {
for (controls, maybe_gravity, entity, parent, children) in piece_query.iter_mut() {
let game_entity = parent.parent();
let (collisions, game_area) = game_query.get(game_entity).unwrap();
let blocks: Vec<Vector2I> = {
let mut blocks = vec![];
for child in children.iter() {
let block = transform_query
.get(child)
.iter()
.filter_map(|(transform, has_block)| {
if *has_block {
Some(transform.translation)
} else {
None
}
})
.next();
if let Some(pos) = block {
blocks.push(pos);
}
}
blocks
};
let cast_shape =
|pos| collisions.cast_shape(pos, &blocks) || !game_area.is_shape_inside(pos, &blocks);
let piece_pos = &mut transform_query.get_mut(entity).unwrap().0.translation;
if controls.instant_drop {
let mut drop_pos = *piece_pos;
while !cast_shape(drop_pos + Vector2I::DOWN) {
drop_pos = drop_pos + Vector2I::DOWN;
}
*piece_pos = drop_pos;
commands.trigger_targets(OnPiecePlaced { piece: entity }, game_entity);
continue;
}
let can_move_down = !cast_shape(*piece_pos + Vector2I::DOWN);
if let Some(mut gravity) = maybe_gravity {
if gravity.cumulated >= 1.0 && can_move_down {
gravity.cumulated -= 1.0;
*piece_pos = *piece_pos + Vector2I::DOWN;
}
if gravity.cumulated >= 3.0 && !can_move_down {
commands.trigger_targets(OnPiecePlaced { piece: entity }, game_entity);
continue;
}
}
if let Some(movement) = controls.movement {
let dir = match movement {
input::Move::Left => Vector2I::LEFT,
input::Move::Right => Vector2I::RIGHT,
};
if !cast_shape(*piece_pos + dir) {
*piece_pos = *piece_pos + dir;
}
}
}
}

View File

@ -1,15 +1,42 @@
use std::collections::HashSet;
use bevy::prelude::*;
use crate::util::Vector2I;
use super::{grid::*, prefab::*};
pub mod input;
pub type YPos = i32;
pub type XPos = i32;
#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
pub struct BlockSet {
blocks: HashSet<Vector2I>,
}
impl BlockSet {
pub fn new(blocks: HashSet<Vector2I>) -> Self {
Self { blocks }
}
pub fn cast_point(&self, pos: &Vector2I) -> bool {
self.blocks.contains(pos)
}
pub fn cast_shape(&self, shape_pos: Vector2I, local_blocks: &[Vector2I]) -> bool {
local_blocks
.iter()
.map(|local_pos| shape_pos + *local_pos)
.any(|global_pos| self.cast_point(&global_pos))
}
}
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)]
#[require(Grid, NextPiece)]
#[require(Grid, NextPiece, BlockSet)]
pub struct GameArea {
pub bottom_boundary: i32,
pub kill_height: i32,
@ -54,6 +81,13 @@ impl GameArea {
&& point.x <= self.right_boundary
&& point.y >= self.bottom_boundary
}
pub fn is_shape_inside(&self, shape_pos: Vector2I, local_blocks: &[Vector2I]) -> bool {
local_blocks
.iter()
.map(|local_pos| shape_pos + *local_pos)
.all(|global_pos| self.is_point_inside(global_pos))
}
}
#[derive(Component, Debug, Reflect)]
@ -68,22 +102,6 @@ impl Default for GameGravity {
}
}
pub mod piece_input {
use bevy::prelude::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum Rotation {
Clockwise,
CounterClockwise,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum Move {
Left,
Right,
}
}
#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
#[require(Grid, GridTransform)]
@ -91,20 +109,19 @@ pub struct Piece;
#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
#[require(Piece)]
pub struct PieceControls {
pub cumulated_gravity: f32,
pub instant_drop: bool,
pub movement: Option<piece_input::Move>,
pub rotation: Option<piece_input::Rotation>,
pub struct Gravity {
pub cumulated: f32,
}
impl PieceControls {
pub fn reset_inputs(&mut self) {
self.instant_drop = false;
self.movement = None;
self.rotation = None;
}
/// Dictates the intention of piece's movement
#[derive(Component, Debug, Default, Reflect)]
#[reflect(Component)]
#[require(Piece, Gravity)]
pub struct PieceControls {
pub instant_drop: bool,
pub fast_drop: bool,
pub movement: Option<input::Move>,
pub rotation: Option<input::Rotation>,
}
#[derive(Component, Debug, Reflect)]

57
src/game/tetris/input.rs Normal file
View File

@ -0,0 +1,57 @@
use bevy::prelude::*;
#[derive(Debug, Clone, Reflect)]
pub struct InputRepeat {
pub currently_pressed: bool,
pub cumulated_time: f32,
pub next_interval: f32,
pub interval_mult: f32,
pub min_interval: f32,
}
impl Default for InputRepeat {
fn default() -> Self {
Self {
currently_pressed: false,
cumulated_time: 0.0,
next_interval: 0.2,
interval_mult: 0.0,
min_interval: 0.03,
}
}
}
impl InputRepeat {
/// Returns if the input should be repeated this frame.
pub fn should_repeat(&mut self, pressed: bool, delta_seconds: f32) -> bool {
if !pressed {
*self = Self::default();
return false;
}
if !self.currently_pressed {
self.currently_pressed = true;
true
} else {
self.cumulated_time += delta_seconds;
if self.cumulated_time >= self.next_interval {
self.cumulated_time = 0.0;
self.next_interval =
(self.next_interval * self.interval_mult).max(self.min_interval);
return true;
}
false
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum Rotation {
Clockwise,
CounterClockwise,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Reflect)]
pub enum Move {
Left,
Right,
}