Compare commits

...

5 Commits

Author SHA1 Message Date
Elijah Voigt e09ee105ea bounds checking works 2 months ago
Elijah Voigt 1dfac5dd61 Fix rotation bug 2 months ago
Elijah Voigt fc0f665afc Slight debug menu styling 2 months ago
Elijah Voigt b01a7f73e5 Add cross for entities in the world with no mesh 2 months ago
Elijah Voigt f61b5761a8 Added gizmo outlines to blocks 2 months ago

@ -0,0 +1,11 @@
# Tetris Game
## Bugs
* Piece should stop moving when it hits the edge of the grid
## Features
* Placing pieces in the grid
* Placing multiple pieces stacking
* Clearing lines when they are full

@ -32,7 +32,10 @@ impl Plugin for BlocksPlugin {
.init_asset_loader::<ShapeAssetLoader>() .init_asset_loader::<ShapeAssetLoader>()
.add_message::<Movement>() .add_message::<Movement>()
.add_systems(OnEnter(Loading(true)), load_assets.run_if(run_once)) .add_systems(OnEnter(Loading(true)), load_assets.run_if(run_once))
.add_systems(OnEnter(GameState::Setup), (setup_camera, setup_blocks)) .add_systems(
OnEnter(GameState::Setup),
(setup_camera, setup_blocks, setup_grid),
)
.add_systems( .add_systems(
Update, Update,
( (
@ -43,6 +46,7 @@ impl Plugin for BlocksPlugin {
), ),
propogate_orientation.run_if(any_component_changed::<Orientation>), propogate_orientation.run_if(any_component_changed::<Orientation>),
propogate_grid_position.run_if(any_component_changed::<GridPosition>), propogate_grid_position.run_if(any_component_changed::<GridPosition>),
propogate_relative_position.run_if(any_component_changed::<RelativePosition>),
handle_kb_input.run_if(on_message::<KeyboardInput>), handle_kb_input.run_if(on_message::<KeyboardInput>),
handle_movement.run_if(on_message::<Movement>), handle_movement.run_if(on_message::<Movement>),
), ),
@ -52,7 +56,7 @@ impl Plugin for BlocksPlugin {
} }
} }
const SCALE: f32 = 20.0; pub(crate) const SCALE: f32 = 20.0;
const X_MAX: usize = 10; const X_MAX: usize = 10;
const Y_MAX: usize = 20; const Y_MAX: usize = 20;
@ -101,11 +105,42 @@ struct GridPosition {
pub y: usize, pub y: usize,
} }
#[derive(Debug, Error)]
enum GridPositionError {
#[error("X > X_MAX")]
XOver,
#[error("X < 0")]
XUnder,
#[error("Y < 0")]
YUnder,
#[error("Y < 0 && X < 0")]
BothUnder,
}
impl GridPosition { impl GridPosition {
fn with_relative_offset(&self, other: &RelativePosition) -> GridPosition { fn with_relative_offset(&self, other: &RelativePosition) -> GridPosition {
GridPosition { self.add(*other).unwrap()
x: self.x.checked_add_signed(other.x).unwrap(), }
y: self.y.checked_add_signed(other.y).unwrap(),
fn add(&self, other: RelativePosition) -> Result<GridPosition, GridPositionError> {
let x = self.x.checked_add_signed(other.x);
let y = self.y.checked_add_signed(other.y);
match (x, y) {
// Both underflow
(None, None) => Err(GridPositionError::BothUnder),
// Just y underflows
(None, Some(_)) => Err(GridPositionError::XUnder),
// y underflow or x overflow
(Some(x), None) => match x {
0..X_MAX => Err(GridPositionError::YUnder),
_ => Err(GridPositionError::XOver),
}
// both good, may x overflow
(Some(x), Some(y)) => match x {
0..X_MAX => Ok(GridPosition { x, y }),
_ => Err(GridPositionError::XOver),
}
} }
} }
} }
@ -118,31 +153,6 @@ impl Into<Vec3> for &GridPosition {
} }
} }
impl std::ops::AddAssign<RelativePosition> for GridPosition {
fn add_assign(&mut self, rhs: RelativePosition) {
self.x = self.x.strict_add_signed(rhs.x);
self.y = self.y.strict_add_signed(rhs.y);
}
}
impl std::ops::SubAssign<RelativePosition> for GridPosition {
fn sub_assign(&mut self, rhs: RelativePosition) {
self.x = self.x.strict_sub_signed(rhs.x);
self.y = self.y.strict_sub_signed(rhs.y);
}
}
impl std::ops::Add<RelativePosition> for GridPosition {
type Output = GridPosition;
fn add(self, rhs: RelativePosition) -> GridPosition {
GridPosition {
x: self.x.strict_add_signed(rhs.x),
y: self.y.strict_add_signed(rhs.y),
}
}
}
/// Block positions relative to the shape's center /// Block positions relative to the shape's center
#[derive(Component, PartialEq, Debug, Clone, Copy)] #[derive(Component, PartialEq, Debug, Clone, Copy)]
pub(crate) struct RelativePosition { pub(crate) struct RelativePosition {
@ -257,8 +267,9 @@ impl ShapeLayout {
// TODO: Just hard-code this // TODO: Just hard-code this
ShapeLayout { ShapeLayout {
matrix: self.rotated_matrix(Orientation::Right), matrix: self.rotated_matrix(Orientation::Right),
symmetrical: false symmetrical: false,
}.rotated_matrix(Orientation::Right) }
.rotated_matrix(Orientation::Right)
} }
Orientation::Left => { Orientation::Left => {
// The main algorithm // The main algorithm
@ -352,6 +363,7 @@ fn setup_blocks(
server: Res<AssetServer>, server: Res<AssetServer>,
mut checklist: ResMut<SetupChecklist>, mut checklist: ResMut<SetupChecklist>,
) { ) {
// TODO: WARN: THIS WILL CAUSE ISSUES WHEN WE LOAD MULTIPLE SHAPES
let h: Handle<ShapeAsset> = server let h: Handle<ShapeAsset> = server
.get_handle(all_assets.handles[0].path().unwrap()) .get_handle(all_assets.handles[0].path().unwrap())
.unwrap(); .unwrap();
@ -361,6 +373,30 @@ fn setup_blocks(
checklist.spawn_shape = true; checklist.spawn_shape = true;
} }
fn setup_grid(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<ColorMaterial>>,
mut checklist: ResMut<SetupChecklist>,
) {
let m: Mesh = Rectangle::new(SCALE, SCALE).into();
let mh: Handle<Mesh> = meshes.add(m);
let m2d = Mesh2d(mh);
let c: Color = WHITE.with_alpha(0.5).into();
let ch: Handle<ColorMaterial> = materials.add(c);
let mat = MeshMaterial2d(ch);
let t = Transform::from_xyz(0.0, 0.0, -1.0);
for x in 0..X_MAX {
for y in 0..Y_MAX {
let gp = GridPosition { x, y };
commands.spawn((gp, m2d.clone(), mat.clone(), t.clone()));
}
}
checklist.spawn_grid = true;
}
/// Blocks <- Shape Relationship /// Blocks <- Shape Relationship
#[derive(Component)] #[derive(Component)]
#[relationship(relationship_target = ShapeBlocks)] #[relationship(relationship_target = ShapeBlocks)]
@ -427,7 +463,9 @@ fn update_grid_position_transform(
>, >,
) { ) {
query.iter_mut().for_each(|(gp, mut t)| { query.iter_mut().for_each(|(gp, mut t)| {
t.translation = gp.into(); let new_t: Vec3 = gp.into();
t.translation.x = new_t.x;
t.translation.y = new_t.y;
}); });
} }
@ -439,28 +477,42 @@ fn propogate_grid_position(
parent.iter().for_each(|(parent_gp, sbs)| { parent.iter().for_each(|(parent_gp, sbs)| {
sbs.iter().for_each(|e| { sbs.iter().for_each(|e| {
let (mut gp, rp) = children.get_mut(e).unwrap(); let (mut gp, rp) = children.get_mut(e).unwrap();
*gp = parent_gp.clone() + *rp; *gp = parent_gp.with_relative_offset(rp);
}); });
}); });
} }
/// When a shape's orientation changes, the blocks need to move /// When a block's relative position changes, update it's grid position
fn propogate_relative_position(
mut children: Query<
(&mut GridPosition, &RelativePosition, &ShapeBlock),
Changed<RelativePosition>,
>,
parent: Query<&GridPosition, Without<ShapeBlock>>,
) {
children.iter_mut().for_each(|(mut gp, rp, sb)| {
let parent_gp = parent.get(sb.0).unwrap();
*gp = parent_gp.with_relative_offset(rp);
});
}
/// When a shape's orientation changes, the blocks need to move so assign new relative positions
fn propogate_orientation( fn propogate_orientation(
parent: Query<(&GridPosition, &Orientation, &ShapeLayout, &ShapeBlocks), Changed<Orientation>>, parent: Query<(&Orientation, &ShapeLayout, &ShapeBlocks), Changed<Orientation>>,
mut children: Query<&mut GridPosition, Without<ShapeBlocks>>, mut children: Query<&mut RelativePosition>,
) { ) {
parent.iter().for_each(|(parent_gp, parent_o, sl, sbs)| { parent.iter().for_each(|(parent_o, sl, sbs)| {
let new_layout = sl.positions(*parent_o); let new_layout = sl.positions(*parent_o);
sbs.iter().zip(new_layout).for_each(|(e, rp)| { sbs.iter().zip(new_layout).for_each(|(e, rp)| {
let mut gp = children.get_mut(e).unwrap(); let mut this_rp = children.get_mut(e).unwrap();
*gp = parent_gp.clone() + rp; *this_rp = rp;
}); });
}); });
} }
/// Movement message used to propose/plan a move /// Movement message used to propose/plan a move
#[derive(Debug, Message)] #[derive(Debug, Message, PartialEq)]
enum Movement { enum Movement {
Rotate, Rotate,
Left, Left,
@ -520,33 +572,41 @@ impl Orientation {
fn handle_movement( fn handle_movement(
mut moves: MessageReader<Movement>, mut moves: MessageReader<Movement>,
mut grid_positions: Query<&mut GridPosition, With<ShapeBlocks>>, mut shapes: Query<(&ShapeLayout, &mut GridPosition, &mut Orientation)>,
mut orientations: Query<&mut Orientation, With<ShapeBlocks>>,
) { ) {
moves.read().for_each(|m| match m { moves.read().for_each(|m| {
Movement::Left => { shapes.iter_mut().for_each(|(shape_layout, mut gp, mut o)| {
grid_positions.iter_mut().for_each(|mut gp| { // First determine if proposed positions are valid
debug!("moving shape left"); // Determine next orientation
*gp -= RelativePosition::new(1, 0); let proposed_orientation = if *m == Movement::Rotate {
}); o.rotated()
} } else {
Movement::Right => { *o
grid_positions.iter_mut().for_each(|mut gp| { };
debug!("moving shape right");
*gp += RelativePosition::new(1, 0); // And next position
}); let proposed_position = match m {
} Movement::Left => gp.add((-1, 0).into()),
Movement::Down => { Movement::Right => gp.add((1, 0).into()),
grid_positions.iter_mut().for_each(|mut gp| { Movement::Down => gp.add((0, -1).into()),
debug!("moving shape down"); Movement::Rotate => Ok(gp.clone()),
*gp -= RelativePosition::new(0, 1); };
});
} if let Ok(pp) = proposed_position {
Movement::Rotate => { // Check that new positions are all valid
orientations.iter_mut().for_each(|mut o| { if shape_layout
debug!("rotating shape"); .positions(proposed_orientation)
*o = o.rotated(); .iter()
}); .all(|rp| pp.add(*rp).is_ok()) {
} // then commit the movement
}) *gp = pp;
*o = proposed_orientation;
} else {
// invalid position!
}
} else {
// Invalid move!
}
});
});
} }

@ -25,7 +25,7 @@ impl Plugin for DebugPlugin {
log_transition::<DebugState>.run_if(state_changed::<DebugState>), log_transition::<DebugState>.run_if(state_changed::<DebugState>),
log_transition::<Loading>.run_if(state_changed::<Loading>), log_transition::<Loading>.run_if(state_changed::<Loading>),
log_transition::<GameState>.run_if(state_changed::<GameState>), log_transition::<GameState>.run_if(state_changed::<GameState>),
log_transition::<DebugOutlines>.run_if(state_changed::<DebugOutlines>) log_transition::<DebugOutlines>.run_if(state_changed::<DebugOutlines>),
), ),
) )
.add_systems( .add_systems(
@ -45,11 +45,11 @@ impl Plugin for DebugPlugin {
sync_state_to_ui::<DebugOutlines>, sync_state_to_ui::<DebugOutlines>,
) )
.run_if(state_changed::<DebugOutlines>), .run_if(state_changed::<DebugOutlines>),
) ),
) )
.add_systems( .add_systems(
Update, Update,
draw_outline_gizmos.run_if(in_state(DebugOutlines(true))) draw_outline_gizmos.run_if(in_state(DebugOutlines(true))),
); );
} }
} }
@ -78,6 +78,8 @@ fn setup_ui(mut commands: Commands) {
flex_direction: FlexDirection::Row, flex_direction: FlexDirection::Row,
align_items: AlignItems::Center, align_items: AlignItems::Center,
justify_content: JustifyContent::Start, justify_content: JustifyContent::Start,
justify_self: JustifySelf::Center,
align_self: AlignSelf::Start,
column_gap: px(8), column_gap: px(8),
..default() ..default()
}, },
@ -93,26 +95,20 @@ fn setup_ui(mut commands: Commands) {
commands.spawn(( commands.spawn((
Node { Node {
justify_self: JustifySelf::Center, justify_self: JustifySelf::Start,
align_self: AlignSelf::Center, align_self: AlignSelf::Start,
..default() ..default()
}, },
DebugState(true), DebugState(true),
children![ children![(
( Node { ..default() },
Node { children![(
..default() DebugState(true),
}, checkbox((), Spawn((Text::new("outlines"), ThemedText))),
children![ observe(toggle_outline_state),
Text::new("Outlines"), observe(check_box),
( ),]
toggle_switch((),), ),],
observe(checkbox_self_update),
observe(toggle_outline_state),
),
]
),
]
)); ));
commands.spawn(( commands.spawn((
@ -167,15 +163,43 @@ impl fmt::Display for DebugOutlines {
} }
} }
fn toggle_outline_state( fn toggle_outline_state(event: On<ValueChange<bool>>, mut next: ResMut<NextState<DebugOutlines>>) {
event: On<ValueChange<bool>>,
mut next: ResMut<NextState<DebugOutlines>>,
) {
next.set(DebugOutlines(event.event().value)); next.set(DebugOutlines(event.event().value));
} }
fn draw_debug_outlines( fn draw_outline_gizmos(
gizmos: Gizmos mut gizmos: Gizmos,
query: Query<(Entity, &GlobalTransform), Without<Camera>>,
meshed: Query<&Mesh2d>,
meshes: Res<Assets<Mesh>>,
) { ) {
todo!("Draw outlines here") query.iter().for_each(|(e, gt)| {
let entity_center = gt.translation().truncate();
let entity_size = gt.scale().truncate();
if let Ok(Mesh2d(h)) = meshed.get(e) {
let mesh = meshes.get(h).unwrap();
let aabb = mesh.compute_aabb().unwrap();
let mesh_half_extents = aabb.half_extents.truncate();
let mesh_center = aabb.center.truncate();
let size = 2.0 * mesh_half_extents * entity_size;
let center = entity_center + mesh_center;
gizmos.rect_2d(center, size, MAGENTA);
} else {
let size = SCALE * 0.6;
gizmos.cross_2d(entity_center, size, MAGENTA);
}
});
}
fn check_box(change: On<ValueChange<bool>>, mut commands: Commands) {
let mut checkbox = commands.entity(change.source);
if change.value {
checkbox.insert(Checked);
} else {
checkbox.remove::<Checked>();
}
} }

@ -97,14 +97,17 @@ struct AllAssets {
} }
/// A "checklist" to know if we can progress from setup to the game /// A "checklist" to know if we can progress from setup to the game
/// TODO: Turn this into a hash-map
/// Systems on sartup insert `key:false` and update to `key;true`
#[derive(Default, Resource)] #[derive(Default, Resource)]
struct SetupChecklist { struct SetupChecklist {
spawn_shape: bool, spawn_shape: bool,
spawn_grid: bool,
} }
impl SetupChecklist { impl SetupChecklist {
fn done(&self) -> bool { fn done(&self) -> bool {
self.spawn_shape self.spawn_shape && self.spawn_grid
} }
} }

@ -26,7 +26,7 @@ fn test_shape_layout_01c() {
let actual = ShapeLayout::new(vec![vec![0, 1, 0], vec![1, 1, 1]]).positions(Orientation::Down); let actual = ShapeLayout::new(vec![vec![0, 1, 0], vec![1, 1, 1]]).positions(Orientation::Down);
let expected: [RelativePosition; 4] = let expected: [RelativePosition; 4] =
[(0, -1).into(), (-1, 0).into(), (0, 0).into(), (1, 0).into(),]; [(0, -1).into(), (-1, 0).into(), (0, 0).into(), (1, 0).into()];
debug_assert_eq!(expected, actual); debug_assert_eq!(expected, actual);
} }

Loading…
Cancel
Save