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>()
.add_message::<Movement>()
.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(
Update,
(
@ -43,6 +46,7 @@ impl Plugin for BlocksPlugin {
),
propogate_orientation.run_if(any_component_changed::<Orientation>),
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_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 Y_MAX: usize = 20;
@ -101,11 +105,42 @@ struct GridPosition {
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 {
fn with_relative_offset(&self, other: &RelativePosition) -> GridPosition {
GridPosition {
x: self.x.checked_add_signed(other.x).unwrap(),
y: self.y.checked_add_signed(other.y).unwrap(),
self.add(*other).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
#[derive(Component, PartialEq, Debug, Clone, Copy)]
pub(crate) struct RelativePosition {
@ -257,8 +267,9 @@ impl ShapeLayout {
// TODO: Just hard-code this
ShapeLayout {
matrix: self.rotated_matrix(Orientation::Right),
symmetrical: false
}.rotated_matrix(Orientation::Right)
symmetrical: false,
}
.rotated_matrix(Orientation::Right)
}
Orientation::Left => {
// The main algorithm
@ -352,6 +363,7 @@ fn setup_blocks(
server: Res<AssetServer>,
mut checklist: ResMut<SetupChecklist>,
) {
// TODO: WARN: THIS WILL CAUSE ISSUES WHEN WE LOAD MULTIPLE SHAPES
let h: Handle<ShapeAsset> = server
.get_handle(all_assets.handles[0].path().unwrap())
.unwrap();
@ -361,6 +373,30 @@ fn setup_blocks(
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
#[derive(Component)]
#[relationship(relationship_target = ShapeBlocks)]
@ -427,7 +463,9 @@ fn update_grid_position_transform(
>,
) {
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)| {
sbs.iter().for_each(|e| {
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(
parent: Query<(&GridPosition, &Orientation, &ShapeLayout, &ShapeBlocks), Changed<Orientation>>,
mut children: Query<&mut GridPosition, Without<ShapeBlocks>>,
parent: Query<(&Orientation, &ShapeLayout, &ShapeBlocks), Changed<Orientation>>,
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);
sbs.iter().zip(new_layout).for_each(|(e, rp)| {
let mut gp = children.get_mut(e).unwrap();
*gp = parent_gp.clone() + rp;
let mut this_rp = children.get_mut(e).unwrap();
*this_rp = rp;
});
});
}
/// Movement message used to propose/plan a move
#[derive(Debug, Message)]
#[derive(Debug, Message, PartialEq)]
enum Movement {
Rotate,
Left,
@ -520,33 +572,41 @@ impl Orientation {
fn handle_movement(
mut moves: MessageReader<Movement>,
mut grid_positions: Query<&mut GridPosition, With<ShapeBlocks>>,
mut orientations: Query<&mut Orientation, With<ShapeBlocks>>,
mut shapes: Query<(&ShapeLayout, &mut GridPosition, &mut Orientation)>,
) {
moves.read().for_each(|m| match m {
Movement::Left => {
grid_positions.iter_mut().for_each(|mut gp| {
debug!("moving shape left");
*gp -= RelativePosition::new(1, 0);
});
moves.read().for_each(|m| {
shapes.iter_mut().for_each(|(shape_layout, mut gp, mut o)| {
// First determine if proposed positions are valid
// Determine next orientation
let proposed_orientation = if *m == Movement::Rotate {
o.rotated()
} else {
*o
};
// And next position
let proposed_position = match m {
Movement::Left => gp.add((-1, 0).into()),
Movement::Right => gp.add((1, 0).into()),
Movement::Down => gp.add((0, -1).into()),
Movement::Rotate => Ok(gp.clone()),
};
if let Ok(pp) = proposed_position {
// Check that new positions are all valid
if shape_layout
.positions(proposed_orientation)
.iter()
.all(|rp| pp.add(*rp).is_ok()) {
// then commit the movement
*gp = pp;
*o = proposed_orientation;
} else {
// invalid position!
}
Movement::Right => {
grid_positions.iter_mut().for_each(|mut gp| {
debug!("moving shape right");
*gp += RelativePosition::new(1, 0);
});
} else {
// Invalid move!
}
Movement::Down => {
grid_positions.iter_mut().for_each(|mut gp| {
debug!("moving shape down");
*gp -= RelativePosition::new(0, 1);
});
}
Movement::Rotate => {
orientations.iter_mut().for_each(|mut o| {
debug!("rotating shape");
*o = o.rotated();
});
}
})
}

@ -25,7 +25,7 @@ impl Plugin for DebugPlugin {
log_transition::<DebugState>.run_if(state_changed::<DebugState>),
log_transition::<Loading>.run_if(state_changed::<Loading>),
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(
@ -45,11 +45,11 @@ impl Plugin for DebugPlugin {
sync_state_to_ui::<DebugOutlines>,
)
.run_if(state_changed::<DebugOutlines>),
)
),
)
.add_systems(
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,
align_items: AlignItems::Center,
justify_content: JustifyContent::Start,
justify_self: JustifySelf::Center,
align_self: AlignSelf::Start,
column_gap: px(8),
..default()
},
@ -93,26 +95,20 @@ fn setup_ui(mut commands: Commands) {
commands.spawn((
Node {
justify_self: JustifySelf::Center,
align_self: AlignSelf::Center,
justify_self: JustifySelf::Start,
align_self: AlignSelf::Start,
..default()
},
DebugState(true),
children![
(
Node {
..default()
},
children![
Text::new("Outlines"),
(
toggle_switch((),),
observe(checkbox_self_update),
children![(
Node { ..default() },
children![(
DebugState(true),
checkbox((), Spawn((Text::new("outlines"), ThemedText))),
observe(toggle_outline_state),
),
]
),
]
observe(check_box),
),]
),],
));
commands.spawn((
@ -167,15 +163,43 @@ impl fmt::Display for DebugOutlines {
}
}
fn toggle_outline_state(
event: On<ValueChange<bool>>,
mut next: ResMut<NextState<DebugOutlines>>,
) {
fn toggle_outline_state(event: On<ValueChange<bool>>, mut next: ResMut<NextState<DebugOutlines>>) {
next.set(DebugOutlines(event.event().value));
}
fn draw_debug_outlines(
gizmos: Gizmos
fn draw_outline_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
/// TODO: Turn this into a hash-map
/// Systems on sartup insert `key:false` and update to `key;true`
#[derive(Default, Resource)]
struct SetupChecklist {
spawn_shape: bool,
spawn_grid: bool,
}
impl SetupChecklist {
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 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);
}

Loading…
Cancel
Save