breakout/
breakout.rs

1//! This example aims to recreate the Breakout arcade game.
2//! Is a show off of timer, multiple entities rendering, game state workflow, text rendering and physics.
3//! The targets are spawned as a matrix of 8 rows and 10 columns.
4//! Each target is a specific entity with its own physics.
5
6use lotus_engine::*;
7use std::time::Duration;
8use rand::{rngs::ThreadRng, Rng};
9
10#[derive(Clone, Component)]
11pub struct Player();
12
13#[derive(Clone, Component)]
14pub struct LittleBall();
15
16#[derive(Clone, Component)]
17pub struct Target();
18
19#[derive(Clone, Component)]
20pub struct Border();
21
22#[derive(Clone, Resource)]
23pub struct LittleBallRespawnTimer(Timer);
24
25impl LittleBallRespawnTimer {
26    pub fn new() -> Self {
27        return Self(Timer::new(TimerType::Repeat, Duration::new(2, 0)));
28    }
29}
30
31#[derive(Clone, Resource)]
32pub struct NextState(pub GameState);
33
34impl Default for NextState {
35    fn default() -> Self {
36        return Self(GameState::Stopped);
37    }
38}
39
40#[derive(Clone, PartialEq, Debug)]
41pub enum GameState {
42    Running,
43    Stopped
44}
45
46your_game!(
47    WindowConfiguration {
48        icon_path: "textures/lotus_pink_256x256.png".to_string(),
49        title: "Breakout Game :)".to_string(),
50        background_color: Some(Color::CYAN),
51        background_image_path: None,
52        width: 725.0,
53        height: 695.0,
54        position_x: 200.0,
55        position_y: 150.0,
56        resizable: false,
57        decorations: true,
58        transparent: false,
59        active: true,
60        enabled_buttons: WindowButtons::CLOSE | WindowButtons::MINIMIZE
61    },
62    setup,
63    update
64);
65
66fn setup(context: &mut Context) {
67    let player: Shape = Shape::new(Orientation::Horizontal, GeometryType::Rectangle, Color::PURPLE);
68    let little_ball: Shape = Shape::new(Orientation::Horizontal, GeometryType::Circle(Circle::new(64, 0.2)), Color::BLACK);
69    let start_text: Text = Text::new(
70        Font::new(Fonts::RobotoMonoItalic.get_path(), 40.0),
71        Position::new(Vector2::new(0.10, 0.50), Strategy::Normalized),
72        Color::PURPLE,
73        "Press 'Enter' to start the game!".to_string()
74    );
75
76    let mut thread_rng: ThreadRng = rand::rng();
77    let random_x_direction: f32 = thread_rng.random_range(-0.8..0.8);
78
79    context.commands.add_resources(vec![
80        Box::new(LittleBallRespawnTimer::new()),
81        Box::new(NextState::default())
82    ]);
83    context.commands.spawn(vec![Box::new(start_text)]);
84
85    context.commands.spawn(
86        vec![
87            Box::new(player),
88            Box::new(Player()),
89            Box::new(Transform::new(
90                Position::new(Vector2::new(0.0, -0.85), Strategy::Normalized),
91                0.0,
92                Vector2::new(0.15, 0.10)
93            )),
94            Box::new(Velocity::new(Vector2::new(2.0, 2.0))),
95            Box::new(Collision::new(Collider::new_simple(GeometryType::Rectangle)))
96        ]
97    );
98
99    context.commands.spawn(
100        vec![
101            Box::new(little_ball),
102            Box::new(LittleBall()),
103            Box::new(Transform::new(
104                Position::new(Vector2::new(0.0, -0.5), Strategy::Normalized),
105                0.0,
106                Vector2::new(0.10, 0.10)
107            )),
108            Box::new(Velocity::new(Vector2::new(random_x_direction, -0.5))),
109            Box::new(Collision::new(Collider::new_simple(GeometryType::Square)))
110        ]
111    );
112
113    spawn_border(context, Vector2::new(1.1, 0.));
114    spawn_border(context, Vector2::new(-1.1, 0.));
115    spawn_targets(context);
116}
117
118fn update(context: &mut Context) {
119    let input: Input = {
120        let input_ref: ResourceRefMut<'_, Input> = context.world.get_resource_mut::<Input>().unwrap();
121        input_ref.clone()
122    };
123
124    if input.is_key_released(PhysicalKey::Code(KeyCode::Enter)) {
125        let mut next_state: ResourceRefMut<'_, NextState> = context.world.get_resource_mut::<NextState>().unwrap();
126        next_state.0 = GameState::Running;
127        
128        let mut query: Query = Query::new(&context.world).with::<Text>();
129        if let Some(entity) = query.entities_with_components().unwrap().first() {
130            context.commands.despawn(entity.clone());
131        }
132    }
133
134    if context.world.get_resource::<NextState>().unwrap().0 == GameState::Running {
135        let mut player_query: Query = Query::new(&context.world).with::<Player>();
136        let player_entity: Entity = player_query.entities_with_components().unwrap().first().unwrap().clone();
137
138        let mut little_ball_query: Query = Query::new(&context.world).with::<LittleBall>();
139        let little_ball_entity: Entity = little_ball_query.entities_with_components().unwrap().first().unwrap().clone();
140
141        let mut thread_rng: ThreadRng = rand::rng();
142        let random_factor: f32 = thread_rng.random_range(-0.5..0.5);
143
144        move_player(context, input, player_entity);
145        move_little_ball(context, little_ball_entity);
146        check_player_little_ball_collision(context, player_entity, little_ball_entity, random_factor);
147        check_little_ball_borders_collision(context, little_ball_entity, random_factor);
148        check_litte_ball_targets_collision(context, little_ball_entity, random_factor);
149        respawn_little_ball_after_outbounds(context, little_ball_entity);
150    }
151}
152
153fn spawn_border(context: &mut Context, position: Vector2<f32>) {
154    let border: Shape = Shape::new(Orientation::Vertical, GeometryType::Rectangle, Color::CYAN);
155
156    context.commands.spawn(
157        vec![
158            Box::new(border),
159            Box::new(Border()),
160            Box::new(Transform::new(
161                Position::new(position, Strategy::Normalized),
162                0.0,
163                Vector2::new(0.01, context.window_configuration.height as f32)
164            )),
165            Box::new(Collision::new(Collider::new_simple(GeometryType::Rectangle)))
166        ]
167    );
168}
169
170fn spawn_targets(context: &mut Context) {
171    let width: f32 = 0.15;
172    let height: f32 = 0.10;
173
174    let rows: i32 = 8;
175    let columns: i32 = 10;
176    let spacing_x: f32 = 0.09;
177    let spacing_y: f32 = 0.02;
178
179    let start_x: f32 = -(columns as f32 * (width + spacing_x)) / 2.0;
180    let start_y: f32 = 1.0 - 0.1;
181
182    for row in 0..rows {
183        for column in 0..columns {
184            let x: f32 = start_x + column as f32 * (width + spacing_x);
185            let y: f32 = start_y - row as f32 * (height + spacing_y);
186
187            let mut color: Color = Color::RED;
188
189            if row == 2 || row == 3 {
190                color = Color::ORANGE;
191            } else if row == 4 || row == 5 {
192                color = Color::GREEN;
193            } else if row == 6 || row == 7 {
194                color = Color::YELLOW;
195            }
196
197            context.commands.spawn(
198                vec![
199                    Box::new(Shape::new(Orientation::Horizontal, GeometryType::Rectangle, color)), 
200                    Box::new(Target()),
201                    Box::new(Transform::new(
202                        Position::new(Vector2::new(x, y), Strategy::Normalized),
203                        0.0,
204                        Vector2::new(width, height)
205                    )),
206                    Box::new(Collision::new(Collider::new_simple(GeometryType::Rectangle))),
207                ]
208            );
209        }
210    }
211}
212
213fn move_player(context: &mut Context, input: Input, player_entity: Entity) {
214    let mut player_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut(&player_entity).unwrap();
215    let player_velocity: ComponentRef<'_, Velocity> = context.world.get_entity_component(&player_entity).unwrap();
216
217    if input.is_key_pressed(PhysicalKey::Code(KeyCode::ArrowRight)) {
218        let x: f32 = player_transform.position.x + player_velocity.x * context.delta;
219        player_transform.set_position_x(&context.render_state, x);
220    } else if input.is_key_pressed(PhysicalKey::Code(KeyCode::ArrowLeft)) {
221        let x: f32 = player_transform.position.x - player_velocity.x * context.delta;
222        player_transform.set_position_x(&context.render_state, x);
223    }
224}
225
226fn move_little_ball(context: &mut Context, little_ball_entity: Entity) {
227    let mut little_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
228    let little_ball_velocity: ComponentRef<'_, Velocity> = context.world.get_entity_component::<Velocity>(&little_ball_entity).unwrap();
229
230    let new_position: Vector2<f32> = little_ball_transform.position.to_vec() + little_ball_velocity.to_vec() * context.delta;
231    little_ball_transform.set_position(&context.render_state, new_position);
232}
233
234fn check_player_little_ball_collision(context: &mut Context, player_entity: Entity, little_ball_entity: Entity, random_factor: f32) {
235    let mut little_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
236    let mut little_ball_velocity: ComponentRefMut<'_, Velocity> = context.world.get_entity_component_mut::<Velocity>(&little_ball_entity).unwrap();
237    let little_ball_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&little_ball_entity).unwrap();
238
239    let player_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&player_entity).unwrap();
240
241    if Collision::check(CollisionAlgorithm::Aabb, &player_collision, &little_ball_collision) {
242        let relative_collision_position: Vector2<f32> = little_ball_collision.collider.position - player_collision.collider.position;
243        
244        let rebound_direction: Vector2<f32> = if relative_collision_position.y > 0.0 {
245            Vector2::new(relative_collision_position.x, 1.0)
246        } else {
247            Vector2::new(relative_collision_position.x, -1.0)
248        };
249        let rebound_vector: Vector2<f32> = (rebound_direction + Vector2::new(random_factor, 0.0)).normalize();
250        let little_ball_new_velocity: Vector2<f32> = rebound_vector * little_ball_velocity.to_vec().magnitude();
251
252        little_ball_velocity.x = little_ball_new_velocity.x; little_ball_velocity.y = little_ball_new_velocity.y;
253        little_ball_transform.position.y += rebound_direction.y * 0.02;
254    }
255}
256
257fn check_little_ball_borders_collision(context: &mut Context, little_ball_entity: Entity, random_factor: f32) {
258    let mut border_query: Query = Query::new(&context.world).with::<Border>();
259    let borders_entities: Vec<Entity> = border_query.entities_with_components().unwrap();
260
261    let mut little_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
262    let mut little_ball_velocity: ComponentRefMut<'_, Velocity> = context.world.get_entity_component_mut::<Velocity>(&little_ball_entity).unwrap();
263    let little_ball_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&little_ball_entity).unwrap();
264
265    for border in &borders_entities {
266        let border_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(border).unwrap();
267
268        if Collision::check(CollisionAlgorithm::Aabb, &little_ball_collision, &border_collision) {
269            if border_collision.collider.position.x > 0.0 {
270                let little_ball_new_velocity: Vector2<f32> =
271                    Vector2::new(-1.0 + random_factor, little_ball_velocity.y.signum()).normalize() * little_ball_velocity.to_vec().magnitude();
272
273                little_ball_velocity.x = little_ball_new_velocity.x; little_ball_velocity.y = little_ball_new_velocity.y;
274                little_ball_transform.position.x -= 0.1;
275            } else if border_collision.collider.position.x < 0.0 {
276                let little_ball_new_velocity: Vector2<f32> =
277                    Vector2::new(1.0 + random_factor, little_ball_velocity.y.signum()).normalize() * little_ball_velocity.to_vec().magnitude();
278
279                little_ball_velocity.x = little_ball_new_velocity.x; little_ball_velocity.y = little_ball_new_velocity.y;
280                little_ball_transform.position.x += 0.1;
281            }
282        }
283    }
284}
285
286fn check_litte_ball_targets_collision(context: &mut Context, little_ball_entity: Entity, random_factor: f32) {
287    let mut targets_query: Query = Query::new(&context.world).with::<Target>();
288    let targets_entities: Vec<Entity> = targets_query.entities_with_components().unwrap();
289
290    let mut little_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
291    let mut little_ball_velocity: ComponentRefMut<'_, Velocity> = context.world.get_entity_component_mut::<Velocity>(&little_ball_entity).unwrap();
292    let little_ball_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&little_ball_entity).unwrap();
293
294    for target in &targets_entities {
295        let target_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(target).unwrap();
296
297        if Collision::check(CollisionAlgorithm::Aabb, &little_ball_collision, &target_collision) {
298            let little_ball_new_velocity: Vector2<f32> =
299                Vector2::new(little_ball_velocity.x.signum(), -1.0 + random_factor).normalize() * little_ball_velocity.to_vec().magnitude();
300
301            little_ball_velocity.x = little_ball_new_velocity.x; little_ball_velocity.y = little_ball_new_velocity.y;
302            little_ball_transform.position.y -= 0.1;
303            context.commands.despawn(target.clone());
304        }
305    }
306}
307
308fn respawn_little_ball_after_outbounds(context: &mut Context, little_ball_entity: Entity) {
309    let mut litte_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
310    let position_default: Vector2<f32> = Vector2::new(0.0, -0.25);
311
312    if litte_ball_transform.position.y < -1.0 {
313        let mut little_ball_respawn_timer: ResourceRefMut<'_, LittleBallRespawnTimer> = context.world.get_resource_mut::<LittleBallRespawnTimer>().unwrap();
314        little_ball_respawn_timer.0.tick(context.delta);
315
316        if little_ball_respawn_timer.0.is_finished() {
317            litte_ball_transform.set_position(&context.render_state, position_default);
318        }
319    }
320}