Breakout
This commit is contained in:
parent
f83c7c0781
commit
9910fd5799
4 changed files with 572 additions and 20 deletions
|
|
@ -4,7 +4,7 @@ version = "0.1.0"
|
|||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
bevy = "0.19.0"
|
||||
bevy = { version = "0.19.0", features = ["debug_glam_assert"] }
|
||||
chacha20 = { version = "0.10.1", default-features = false, features = ["rng"] }
|
||||
getrandom = "0.4.3"
|
||||
rand = "0.10.2"
|
||||
|
|
|
|||
|
|
@ -8,16 +8,33 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background-color: #aaaaaa;
|
||||
background-color: #222222;
|
||||
color: white;
|
||||
}
|
||||
canvas {
|
||||
background-color: black;
|
||||
}
|
||||
.mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
<title>Pong</title>
|
||||
<title>Breakout</title>
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<canvas id="breakout"></canvas>
|
||||
<p style="text-align: center;">
|
||||
Use
|
||||
<span class="mono">⬅</span>
|
||||
<span class="mono">➡</span>
|
||||
to move and
|
||||
<span class="mono">Space</span>
|
||||
to serve.
|
||||
</p>
|
||||
</div>
|
||||
<script type="module">
|
||||
import init from './breakout.js';
|
||||
init();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
49
src/debug.rs
Normal file
49
src/debug.rs
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
use bevy::prelude::*;
|
||||
|
||||
use crate::{Brick, GameState, WORLD_SIZE};
|
||||
|
||||
pub(crate) struct DebugPlugin;
|
||||
|
||||
impl Plugin for DebugPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
if !cfg!(feature = "debug") {
|
||||
return;
|
||||
}
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
draw_world_bounds,
|
||||
log_state_transitions.run_if(state_changed::<GameState>),
|
||||
cheat_codes,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_world_bounds(mut gizmos: Gizmos) {
|
||||
gizmos.rect_2d(
|
||||
Isometry2d::from_translation(WORLD_SIZE / 2.),
|
||||
WORLD_SIZE,
|
||||
LinearRgba::RED,
|
||||
);
|
||||
}
|
||||
|
||||
fn log_state_transitions(state: Res<State<GameState>>) {
|
||||
info!({ ?state }, "State changed");
|
||||
}
|
||||
|
||||
fn cheat_codes(
|
||||
mut commands: Commands,
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
bricks: Query<Entity, With<Brick>>,
|
||||
) {
|
||||
if input.pressed(KeyCode::Digit1) {
|
||||
let mut iter = bricks.iter();
|
||||
_ = iter.next();
|
||||
|
||||
for entity in iter {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
}
|
||||
512
src/main.rs
512
src/main.rs
|
|
@ -1,11 +1,44 @@
|
|||
use bevy::camera::ScalingMode;
|
||||
use bevy::math::bounding::{Aabb2d, Bounded2d, BoundingCircle, BoundingVolume, IntersectsVolume};
|
||||
use bevy::prelude::*;
|
||||
use bevy::sprite::Anchor;
|
||||
use core::f32::consts::PI;
|
||||
|
||||
pub const WORLD_SIZE: Vec2 = Vec2::new(480., 640.);
|
||||
use crate::debug::DebugPlugin;
|
||||
|
||||
mod debug;
|
||||
|
||||
const WORLD_SIZE: Vec2 = Vec2::new(480., 640.);
|
||||
|
||||
#[derive(States, Debug, Copy, Clone, PartialEq, Eq, Hash, Default)]
|
||||
enum GameState {
|
||||
#[default]
|
||||
Build,
|
||||
Serve,
|
||||
Play,
|
||||
Over,
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, Copy, Clone, PartialEq, Eq, Default)]
|
||||
struct Level(u8);
|
||||
|
||||
#[derive(Resource, Debug, Copy, Clone, PartialEq, Eq, Deref)]
|
||||
struct Lives(u8);
|
||||
|
||||
impl Default for Lives {
|
||||
fn default() -> Self {
|
||||
Self(3)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, Copy, Clone, PartialEq, Eq, Default, Deref)]
|
||||
struct Score(u64);
|
||||
|
||||
fn main() {
|
||||
App::new()
|
||||
.insert_resource(ClearColor(Color::BLACK))
|
||||
.init_resource::<Level>()
|
||||
.init_resource::<Score>()
|
||||
.init_resource::<Lives>()
|
||||
.add_plugins(DefaultPlugins.set(WindowPlugin {
|
||||
// Until there's a reliable way to hint that a game window should
|
||||
// float on tiling window managers, the most reliable way to achieve
|
||||
|
|
@ -13,6 +46,7 @@ fn main() {
|
|||
//
|
||||
// https://github.com/rust-windowing/winit/issues/862#issuecomment-2047791401
|
||||
primary_window: Some(Window {
|
||||
canvas: Some("#breakout".into()),
|
||||
resize_constraints: WindowResizeConstraints {
|
||||
min_width: WORLD_SIZE.x,
|
||||
min_height: WORLD_SIZE.y,
|
||||
|
|
@ -23,23 +57,475 @@ fn main() {
|
|||
}),
|
||||
..default()
|
||||
}))
|
||||
.add_systems(Startup, (setup, greet).chain())
|
||||
.add_plugins(DebugPlugin)
|
||||
.init_state::<GameState>()
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(
|
||||
FixedUpdate,
|
||||
(
|
||||
(collisions, cleared_level, void_out, move_ball)
|
||||
.chain()
|
||||
.run_if(in_state(GameState::Play)),
|
||||
move_paddle,
|
||||
show_score,
|
||||
show_lives.run_if(resource_changed::<Lives>),
|
||||
serve_ball.run_if(in_state(GameState::Serve)),
|
||||
)
|
||||
.chain(),
|
||||
)
|
||||
.add_systems(OnEnter(GameState::Build), build_level)
|
||||
.add_systems(OnEnter(GameState::Over), reset_game)
|
||||
.run();
|
||||
}
|
||||
|
||||
fn setup(mut commands: Commands) {
|
||||
fn setup(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
) {
|
||||
commands.spawn((
|
||||
Camera2d,
|
||||
Projection::Orthographic(OrthographicProjection {
|
||||
scaling_mode: ScalingMode::Fixed {
|
||||
width: WORLD_SIZE.x,
|
||||
height: WORLD_SIZE.y,
|
||||
},
|
||||
..OrthographicProjection::default_2d()
|
||||
}),
|
||||
Transform::from_translation((WORLD_SIZE / 2.).extend(0.)),
|
||||
));
|
||||
|
||||
commands.spawn(Ball::build(&mut meshes, &mut materials));
|
||||
|
||||
commands.spawn(Wall::top());
|
||||
commands.spawn(Wall::left());
|
||||
commands.spawn(Wall::right());
|
||||
// There's no bottom wall because that's a void plane instead.
|
||||
commands.spawn((
|
||||
VoidPlane(Plane2d::new(Vec2::Y)),
|
||||
Transform::from_translation(Vec3::ZERO),
|
||||
));
|
||||
commands.add_observer(on_void_out);
|
||||
|
||||
commands.spawn((
|
||||
Paddle,
|
||||
Sprite::from_color(Paddle::COLOR, Vec2::ONE),
|
||||
Transform {
|
||||
translation: Vec3::new(WORLD_SIZE.x / 2., Paddle::SIZE.y / 2. + Paddle::GAP, 0.),
|
||||
scale: Paddle::SIZE.extend(1.),
|
||||
..default()
|
||||
},
|
||||
Collider::Aabb(Aabb2d::new(Vec2::ZERO, Paddle::SIZE / 2.)),
|
||||
));
|
||||
|
||||
commands.spawn((
|
||||
ScoreText,
|
||||
Text2d::default(),
|
||||
TextLayout::justify(Justify::Right),
|
||||
Anchor::BOTTOM_RIGHT,
|
||||
Transform::from_translation(Vec3::new(WORLD_SIZE.x - 8., WORLD_SIZE.y - HUD_HEIGHT, 1.)),
|
||||
));
|
||||
|
||||
commands.add_observer(start_game);
|
||||
}
|
||||
|
||||
fn greet() {
|
||||
println!("Hello, world!");
|
||||
#[derive(Component, Debug)]
|
||||
struct Ball;
|
||||
|
||||
impl Ball {
|
||||
const COLOR: Color = Color::LinearRgba(LinearRgba::GREEN);
|
||||
const RADIUS: f32 = 8.;
|
||||
const SPAWN_POSITION: Vec2 = Vec2::new(WORLD_SIZE.x / 2., 40.);
|
||||
const SERVE_SPEED: f32 = 200.;
|
||||
const MAX_SPEED: f32 = 900.;
|
||||
|
||||
fn build(
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
) -> impl Bundle {
|
||||
(
|
||||
Ball,
|
||||
Mesh2d(meshes.add(Circle::new(Ball::RADIUS))),
|
||||
MeshMaterial2d(materials.add(Ball::COLOR)),
|
||||
Transform::from_translation(Ball::SPAWN_POSITION.extend(0.)),
|
||||
Velocity(Vec2::ZERO),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug, Copy, Clone, PartialEq, Deref, DerefMut)]
|
||||
struct Velocity(Vec2);
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
struct Wall;
|
||||
|
||||
impl Wall {
|
||||
const COLOR: Color = Color::LinearRgba(LinearRgba::rgb(0.5, 0.5, 0.5));
|
||||
const THICKNESS: f32 = 4.;
|
||||
|
||||
const LENGTH_VERTICAL: f32 = WORLD_SIZE.y - HUD_HEIGHT;
|
||||
|
||||
fn top() -> impl Bundle {
|
||||
(
|
||||
Wall,
|
||||
Sprite::from_color(Wall::COLOR, Vec2::ONE),
|
||||
Transform {
|
||||
translation: Vec3::new(
|
||||
WORLD_SIZE.x / 2.,
|
||||
WORLD_SIZE.y - Wall::THICKNESS / 2. - HUD_HEIGHT,
|
||||
0.,
|
||||
),
|
||||
scale: Vec3::new(WORLD_SIZE.x, Wall::THICKNESS, 1.),
|
||||
..default()
|
||||
},
|
||||
Collider::Plane(Plane2d::new(Vec2::NEG_Y)),
|
||||
)
|
||||
}
|
||||
|
||||
fn left() -> impl Bundle {
|
||||
let x = Wall::THICKNESS / 2.;
|
||||
(
|
||||
Wall,
|
||||
Sprite::from_color(Wall::COLOR, Vec2::ONE),
|
||||
Transform {
|
||||
translation: Vec3::new(x, Self::LENGTH_VERTICAL / 2., 0.),
|
||||
scale: Vec3::new(Wall::THICKNESS, Self::LENGTH_VERTICAL, 1.),
|
||||
..default()
|
||||
},
|
||||
Collider::Plane(Plane2d::new(Vec2::X)),
|
||||
)
|
||||
}
|
||||
|
||||
fn right() -> impl Bundle {
|
||||
let x = WORLD_SIZE.x - Wall::THICKNESS / 2.;
|
||||
(
|
||||
Wall,
|
||||
Sprite::from_color(Wall::COLOR, Vec2::ONE),
|
||||
Transform {
|
||||
translation: Vec3::new(x, Self::LENGTH_VERTICAL / 2., 0.),
|
||||
scale: Vec3::new(Wall::THICKNESS, Self::LENGTH_VERTICAL, 1.),
|
||||
..default()
|
||||
},
|
||||
Collider::Plane(Plane2d::new(Vec2::NEG_X)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
struct Paddle;
|
||||
|
||||
impl Paddle {
|
||||
const COLOR: Color = Color::LinearRgba(LinearRgba::rgb(0., 1., 1.));
|
||||
const SIZE: Vec2 = Vec2::new(60., 10.);
|
||||
const GAP: f32 = 4.;
|
||||
const SPEED: f32 = WORLD_SIZE.x / 2.;
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
enum Collider {
|
||||
Aabb(Aabb2d),
|
||||
Plane(Plane2d),
|
||||
}
|
||||
|
||||
fn collisions(
|
||||
mut commands: Commands,
|
||||
ball: Single<(&Transform, &mut Velocity), With<Ball>>,
|
||||
colliders: Query<
|
||||
(
|
||||
Entity,
|
||||
&Transform,
|
||||
&Collider,
|
||||
Option<&Brick>,
|
||||
Option<&Paddle>,
|
||||
),
|
||||
Without<Ball>,
|
||||
>,
|
||||
mut score: ResMut<Score>,
|
||||
) {
|
||||
let (transform, mut velocity) = ball.into_inner();
|
||||
let circle = BoundingCircle::new(transform.translation.truncate(), Ball::RADIUS);
|
||||
|
||||
for (entity, transform, collider, brick, paddle) in colliders {
|
||||
let tx = transform.translation.truncate();
|
||||
|
||||
let target = match collider {
|
||||
Collider::Aabb(aabb) => aabb.translated_by(tx),
|
||||
Collider::Plane(plane) => plane.aabb_2d(tx),
|
||||
};
|
||||
if !target.intersects(&circle) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if brick.is_some() {
|
||||
commands.entity(entity).despawn();
|
||||
score.0 += Brick::SCORE;
|
||||
}
|
||||
|
||||
let point = target.closest_point(circle.center);
|
||||
let normal = match collider {
|
||||
Collider::Plane(plane) => plane.normal.as_vec2(),
|
||||
Collider::Aabb(_) => {
|
||||
let dist = circle.center - point;
|
||||
|
||||
if dist.x.abs() > dist.y.abs() {
|
||||
if dist.x < 0. { Vec2::NEG_X } else { Vec2::X }
|
||||
} else if dist.y < 0. {
|
||||
Vec2::NEG_Y
|
||||
} else {
|
||||
Vec2::Y
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Only reflect if the current velocity has a component moving *against*
|
||||
// the box face's normal vector. This allows the ball to exit the box
|
||||
// without getting stuck "re-colliding" with it.
|
||||
//
|
||||
// Put another way, if the ball is already moving in the same direction
|
||||
// that the plane would bounce it, that's not a real collision!
|
||||
if velocity.dot(normal) >= 0. {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut v = velocity.reflect(normal);
|
||||
|
||||
if paddle.is_some() && normal == Vec2::Y {
|
||||
let offset = (point.x - tx.x) / Paddle::SIZE.x;
|
||||
let tilt = (0.).lerp(-PI / 4., offset);
|
||||
|
||||
let angle = (v.to_angle() + tilt).clamp(PI / 6., 5. * PI / 6.);
|
||||
let mag = v.length() * (1. + offset.abs() / 2.);
|
||||
|
||||
v = Vec2::from_angle(angle).rotate(Vec2::X * mag);
|
||||
}
|
||||
|
||||
velocity.0 = v.clamp_length_max(Ball::MAX_SPEED);
|
||||
}
|
||||
}
|
||||
|
||||
fn move_ball(time: Res<Time>, ball: Single<(&mut Transform, &Velocity), With<Ball>>) {
|
||||
let (mut transform, velocity) = ball.into_inner();
|
||||
transform.translation += velocity.extend(0.) * time.delta_secs();
|
||||
}
|
||||
|
||||
fn move_paddle(
|
||||
time: Res<Time>,
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut paddle: Single<&mut Transform, With<Paddle>>,
|
||||
) {
|
||||
let mut v = Vec3::ZERO;
|
||||
if input.pressed(KeyCode::ArrowLeft) {
|
||||
v.x -= 1.;
|
||||
}
|
||||
if input.pressed(KeyCode::ArrowRight) {
|
||||
v.x += 1.;
|
||||
}
|
||||
|
||||
if v == Vec3::ZERO {
|
||||
return;
|
||||
}
|
||||
|
||||
paddle.translation += v.normalize() * Paddle::SPEED * time.delta_secs();
|
||||
|
||||
let w = Paddle::SIZE.x / 2.;
|
||||
paddle.translation.x = paddle.translation.x.clamp(w, WORLD_SIZE.x - w);
|
||||
}
|
||||
|
||||
fn serve_ball(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
paddle: Single<&Transform, (With<Paddle>, Without<Ball>)>,
|
||||
ball: Single<(&mut Transform, &mut Velocity), With<Ball>>,
|
||||
mut game_state: ResMut<NextState<GameState>>,
|
||||
) {
|
||||
let (mut transform, mut velocity) = ball.into_inner();
|
||||
if input.pressed(KeyCode::Space) {
|
||||
velocity.0 = Vec2::from_angle(-PI / 6.).rotate(Ball::SERVE_SPEED * Vec2::Y);
|
||||
game_state.set(GameState::Play)
|
||||
} else {
|
||||
transform.translation =
|
||||
paddle.translation + Vec3::ZERO.with_y(Paddle::SIZE.y + Ball::RADIUS);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Event, Debug)]
|
||||
struct VoidOut;
|
||||
|
||||
#[derive(Component, Debug, Deref, DerefMut)]
|
||||
struct VoidPlane(Plane2d);
|
||||
|
||||
fn void_out(
|
||||
mut commands: Commands,
|
||||
ball: Single<&Transform, With<Ball>>,
|
||||
planes: Query<(&VoidPlane, &Transform), Without<Ball>>,
|
||||
) {
|
||||
let circle = BoundingCircle::new(ball.translation.truncate(), Ball::RADIUS);
|
||||
|
||||
for (plane, transform) in planes {
|
||||
let plane = plane.aabb_2d(transform.translation.truncate());
|
||||
|
||||
if plane.intersects(&circle) {
|
||||
commands.trigger(VoidOut);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_void_out(
|
||||
_: On<VoidOut>,
|
||||
mut ball: Single<&mut Velocity, With<Ball>>,
|
||||
mut game_state: ResMut<NextState<GameState>>,
|
||||
mut lives: ResMut<Lives>,
|
||||
) {
|
||||
ball.0 = Vec2::ZERO;
|
||||
lives.0 = lives.0.saturating_sub(1);
|
||||
if lives.0 == 0 {
|
||||
game_state.set(GameState::Over);
|
||||
} else {
|
||||
game_state.set(GameState::Serve)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
struct Brick;
|
||||
|
||||
impl Brick {
|
||||
const SCORE: u64 = 100;
|
||||
const SIZE: Vec2 = Vec2::new(50., 20.);
|
||||
const GAP: f32 = 10.;
|
||||
const COLORS: [Color; 8] = [
|
||||
Color::LinearRgba(LinearRgba::rgb(1., 0., 0.)), // red
|
||||
Color::LinearRgba(LinearRgba::rgb(1., 0.5, 0.)), // orange
|
||||
Color::LinearRgba(LinearRgba::rgb(1., 1., 0.)), // yellow
|
||||
Color::LinearRgba(LinearRgba::rgb(0., 1., 0.)), // green
|
||||
Color::LinearRgba(LinearRgba::rgb(0., 1., 1.)), // cyan
|
||||
Color::LinearRgba(LinearRgba::rgb(0., 0., 1.)), // blue
|
||||
Color::LinearRgba(LinearRgba::rgb(0.5, 0., 1.)), // purple
|
||||
Color::LinearRgba(LinearRgba::rgb(1., 0., 1.)), // magenta
|
||||
];
|
||||
|
||||
fn build(pos: Vec2, color: Color) -> impl Bundle {
|
||||
(
|
||||
Brick,
|
||||
Sprite::from_color(color, Vec2::ONE),
|
||||
Transform {
|
||||
translation: pos.extend(0.),
|
||||
scale: Brick::SIZE.extend(1.),
|
||||
..default()
|
||||
},
|
||||
Collider::Aabb(Aabb2d::new(Vec2::ZERO, Brick::SIZE / 2.)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn cleared_level(
|
||||
mut level: ResMut<Level>,
|
||||
mut ball: Single<&mut Velocity, With<Ball>>,
|
||||
mut game_state: ResMut<NextState<GameState>>,
|
||||
bricks: Query<(), With<Brick>>,
|
||||
) {
|
||||
if bricks.is_empty() {
|
||||
ball.0 = Vec2::ZERO;
|
||||
level.0 += 1;
|
||||
game_state.set(GameState::Build);
|
||||
}
|
||||
}
|
||||
|
||||
fn build_level(
|
||||
mut commands: Commands,
|
||||
mut game_state: ResMut<NextState<GameState>>,
|
||||
level: Res<Level>,
|
||||
) {
|
||||
let rows = 3 + level.0;
|
||||
|
||||
let half_cols = ((WORLD_SIZE.x / 2. / (Brick::SIZE.x + Brick::GAP)) - 1.).floor() as i32;
|
||||
|
||||
let start = Vec2::new(
|
||||
WORLD_SIZE.x / 2.,
|
||||
WORLD_SIZE.y - HUD_HEIGHT - Wall::THICKNESS - 3. * Brick::GAP,
|
||||
);
|
||||
|
||||
for r in 0..rows {
|
||||
let color = Brick::COLORS[(r as usize) % Brick::COLORS.len()];
|
||||
for c in -half_cols..=half_cols {
|
||||
let x = start.x + (c as f32) * (Brick::SIZE.x + Brick::GAP);
|
||||
let y = start.y - (r as f32) * (Brick::SIZE.y + Brick::GAP);
|
||||
commands.spawn(Brick::build(Vec2::new(x, y), color));
|
||||
}
|
||||
}
|
||||
|
||||
game_state.set(GameState::Serve);
|
||||
}
|
||||
|
||||
const HUD_HEIGHT: f32 = 24.;
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
struct ScoreText;
|
||||
|
||||
fn show_score(score: Res<Score>, mut text: Single<&mut Text2d, With<ScoreText>>) {
|
||||
text.0 = score.to_string();
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
struct LivesDot(u8);
|
||||
|
||||
impl LivesDot {
|
||||
const RADIUS: f32 = 5.;
|
||||
|
||||
fn build(
|
||||
meshes: &mut ResMut<Assets<Mesh>>,
|
||||
materials: &mut ResMut<Assets<ColorMaterial>>,
|
||||
n: u8,
|
||||
) -> impl Bundle {
|
||||
let x = Self::RADIUS * (2. + 3. * n as f32);
|
||||
let y = WORLD_SIZE.y - HUD_HEIGHT / 2.;
|
||||
(
|
||||
LivesDot(n),
|
||||
Mesh2d(meshes.add(Circle::new(Self::RADIUS))),
|
||||
MeshMaterial2d(materials.add(Ball::COLOR)),
|
||||
Transform::from_translation(Vec3::new(x, y, 1.)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn show_lives(
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
lives: Res<Lives>,
|
||||
dots: Query<(Entity, &LivesDot)>,
|
||||
) {
|
||||
let actual: u8 = dots.iter().len().try_into().expect("small number of lives");
|
||||
let expected = **lives;
|
||||
|
||||
for (entity, dot) in dots {
|
||||
let n = dot.0;
|
||||
if n >= expected {
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
for n in actual..expected {
|
||||
commands.spawn(LivesDot::build(&mut meshes, &mut materials, n));
|
||||
}
|
||||
}
|
||||
|
||||
fn reset_game(mut commands: Commands) {
|
||||
let mut delayed = commands.delayed();
|
||||
delayed.secs(3.).trigger(StartGame);
|
||||
}
|
||||
|
||||
#[derive(Event, Debug)]
|
||||
struct StartGame;
|
||||
|
||||
fn start_game(
|
||||
_: On<StartGame>,
|
||||
mut commands: Commands,
|
||||
mut score: ResMut<Score>,
|
||||
mut lives: ResMut<Lives>,
|
||||
mut level: ResMut<Level>,
|
||||
mut game_state: ResMut<NextState<GameState>>,
|
||||
bricks: Query<Entity, With<Brick>>,
|
||||
) {
|
||||
*score = Score::default();
|
||||
*lives = Lives::default();
|
||||
*level = Level::default();
|
||||
|
||||
for brick in bricks {
|
||||
commands.entity(brick).despawn();
|
||||
}
|
||||
|
||||
game_state.set(GameState::default());
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue