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::LIGHTGRAY),
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        ..Default::default()
62    },
63    setup,
64    update
65);
66
67fn setup(context: &mut Context) {
68    let player: Shape = Shape::new(Orientation::Horizontal, GeometryType::Rectangle, Color::PURPLE);
69    let little_ball: Shape = Shape::new(Orientation::Horizontal, GeometryType::Circle(Circle::new(64, 0.2)), Color::BLACK);
70    let start_text: Text = Text::new(
71        &mut context.render_state,
72        Font::new(Fonts::RobotoMonoItalic.get_path(), 40.0),
73        Position::new(Vector2::new(298.0, 380.0), Strategy::Pixelated),
74        Color::BLACK,
75        "> enter <".to_string()
76    );
77
78    let mut thread_rng: ThreadRng = rand::rng();
79    let random_direction: bool = thread_rng.random_bool(1.0 / 3.0);
80 
81    let velocity_x: f32 = if random_direction {
82        1.2
83    } else {
84        -1.2
85    };
86
87    context.commands.add_resources(vec![
88        Box::new(LittleBallRespawnTimer::new()),
89        Box::new(NextState::default())
90    ]);
91    context.commands.spawn(vec![Box::new(start_text)]);
92
93    context.commands.spawn(
94        vec![
95            Box::new(player),
96            Box::new(Player()),
97            Box::new(Transform::new(
98                Position::new(Vector2::new(0.0, -0.85), Strategy::Normalized),
99                0.0,
100                Vector2::new(0.15, 0.10)
101            )),
102            Box::new(Velocity::new(Vector2::new(2.0, 2.0))),
103            Box::new(Collision::new(Collider::new_simple(GeometryType::Rectangle)))
104        ]
105    );
106
107    context.commands.spawn(
108        vec![
109            Box::new(little_ball),
110            Box::new(LittleBall()),
111            Box::new(Transform::new(
112                Position::new(Vector2::new(0.0, -0.5), Strategy::Normalized),
113                0.0,
114                Vector2::new(0.10, 0.10)
115            )),
116            Box::new(Velocity::new(Vector2::new(velocity_x, -0.5))),
117            Box::new(Collision::new(Collider::new_simple(GeometryType::Square)))
118        ]
119    );
120
121    spawn_border(context, Vector2::new(1.05, 0.0));
122    spawn_border(context, Vector2::new(-1.05, 0.0));
123    spawn_targets(context);
124}
125
126fn update(context: &mut Context) {
127    let input: Input = {
128        let input_ref: ResourceRefMut<'_, Input> = context.world.get_resource_mut::<Input>().unwrap();
129        input_ref.clone()
130    };
131    let is_hover: bool = input.mouse_position.x >= 298.0 && (input.mouse_position.y > 380.0 && input.mouse_position.y < 416.0);
132
133    if
134        input.is_key_released(KeyCode::Enter) ||
135        (input.is_mouse_button_released(MouseButton::Left) && is_hover)
136    {
137        let mut next_state: ResourceRefMut<'_, NextState> = context.world.get_resource_mut::<NextState>().unwrap();
138        next_state.0 = GameState::Running;
139
140        let mut query: Query = Query::new(&context.world).with::<Text>();
141        if let Some(entity) = query.entities_with_components().unwrap().first() {
142            context.commands.despawn(entity.clone());
143        }
144    }
145
146    if context.world.get_resource::<NextState>().unwrap().0 == GameState::Running {
147        let mut player_query: Query = Query::new(&context.world).with::<Player>();
148        let player_entity: Entity = player_query.entities_with_components().unwrap().first().unwrap().clone();
149
150        let mut little_ball_query: Query = Query::new(&context.world).with::<LittleBall>();
151        let little_ball_entity: Entity = little_ball_query.entities_with_components().unwrap().first().unwrap().clone();
152
153        let mut thread_rng: ThreadRng = rand::rng();
154        let random_factor: f32 = thread_rng.random_range(-0.5..0.5);
155
156        move_player(context, input, player_entity);
157        move_little_ball(context, little_ball_entity);
158        check_player_little_ball_collision(context, player_entity, little_ball_entity, random_factor);
159        check_little_ball_borders_collision(context, little_ball_entity, random_factor);
160        check_litte_ball_targets_collision(context, little_ball_entity, random_factor);
161        respawn_little_ball_after_outbounds(context, little_ball_entity);
162    }
163}
164
165fn spawn_border(context: &mut Context, position: Vector2<f32>) {
166    let border: Shape = Shape::new(Orientation::Vertical, GeometryType::Rectangle, Color::CYAN);
167
168    context.commands.spawn(
169        vec![
170            Box::new(border),
171            Box::new(Border()),
172            Box::new(Transform::new(
173                Position::new(position, Strategy::Normalized),
174                0.0,
175                Vector2::new(0.01, context.window_configuration.height as f32)
176            )),
177            Box::new(Collision::new(Collider::new_simple(GeometryType::Rectangle)))
178        ]
179    );
180}
181
182fn spawn_targets(context: &mut Context) {
183    let width: f32 = 0.15;
184    let height: f32 = 0.10;
185
186    let rows: i32 = 8;
187    let columns: i32 = 10;
188    let spacing_x: f32 = 0.09;
189    let spacing_y: f32 = 0.02;
190
191    let start_x: f32 = -(columns as f32 * (width + spacing_x)) / 2.0;
192    let start_y: f32 = 1.0 - 0.1;
193
194    for row in 0..rows {
195        for column in 0..columns {
196            let x: f32 = start_x + column as f32 * (width + spacing_x);
197            let y: f32 = start_y - row as f32 * (height + spacing_y);
198
199            let mut color: Color = Color::RED;
200
201            if row == 2 || row == 3 {
202                color = Color::ORANGE;
203            } else if row == 4 || row == 5 {
204                color = Color::GREEN;
205            } else if row == 6 || row == 7 {
206                color = Color::YELLOW;
207            }
208
209            context.commands.spawn(
210                vec![
211                    Box::new(Shape::new(Orientation::Horizontal, GeometryType::Rectangle, color)), 
212                    Box::new(Target()),
213                    Box::new(Transform::new(
214                        Position::new(Vector2::new(x, y), Strategy::Normalized),
215                        0.0,
216                        Vector2::new(width, height)
217                    )),
218                    Box::new(Collision::new(Collider::new_simple(GeometryType::Rectangle))),
219                ]
220            );
221        }
222    }
223}
224
225fn move_player(context: &mut Context, input: Input, player_entity: Entity) {
226    let mut player_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut(&player_entity).unwrap();
227    let player_velocity: ComponentRef<'_, Velocity> = context.world.get_entity_component(&player_entity).unwrap();
228
229    if input.is_key_pressed(KeyCode::ArrowRight) {
230        let x: f32 = player_transform.position.x + player_velocity.x * context.delta;
231        player_transform.set_position_x(&context.render_state, x);
232    } else if input.is_key_pressed(KeyCode::ArrowLeft) {
233        let x: f32 = player_transform.position.x - player_velocity.x * context.delta;
234        player_transform.set_position_x(&context.render_state, x);
235    }
236}
237
238fn move_little_ball(context: &mut Context, little_ball_entity: Entity) {
239    let mut little_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
240    let little_ball_velocity: ComponentRef<'_, Velocity> = context.world.get_entity_component::<Velocity>(&little_ball_entity).unwrap();
241
242    let new_position: Vector2<f32> = little_ball_transform.position.to_vec() + little_ball_velocity.to_vec() * context.delta;
243    little_ball_transform.set_position(&context.render_state, new_position);
244}
245
246fn check_player_little_ball_collision(context: &mut Context, player_entity: Entity, little_ball_entity: Entity, random_factor: f32) {
247    let mut little_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
248    let mut little_ball_velocity: ComponentRefMut<'_, Velocity> = context.world.get_entity_component_mut::<Velocity>(&little_ball_entity).unwrap();
249    let little_ball_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&little_ball_entity).unwrap();
250
251    let player_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&player_entity).unwrap();
252
253    if Collision::check(CollisionAlgorithm::Aabb, &player_collision, &little_ball_collision) {
254        let velocity_magnitude: f32 = little_ball_velocity.to_vec().magnitude();
255        let collision_point: f32 = (
256            (little_ball_collision.collider.position.x - player_collision.collider.position.x) / 
257            (player_collision.collider.scale.x * 0.5)
258        ).clamp(-1.0, 1.0);
259        
260        let mut new_direction: Vector2<f32> = Vector2::new(
261            collision_point * 1.5,
262            1.0 - collision_point.abs() * 0.3
263        ).normalize();
264
265        new_direction.x += random_factor * 0.15;
266        new_direction = new_direction.normalize();
267
268        little_ball_velocity.x = new_direction.x * velocity_magnitude;
269        little_ball_velocity.y = new_direction.y * velocity_magnitude;
270        little_ball_transform.position.y += 0.03;
271    }
272}
273
274fn check_little_ball_borders_collision(context: &mut Context, little_ball_entity: Entity, random_factor: f32) {
275    let mut border_query: Query = Query::new(&context.world).with::<Border>();
276    let borders_entities: Vec<Entity> = border_query.entities_with_components().unwrap();
277
278    let mut little_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
279    let mut little_ball_velocity: ComponentRefMut<'_, Velocity> = context.world.get_entity_component_mut::<Velocity>(&little_ball_entity).unwrap();
280    let little_ball_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&little_ball_entity).unwrap();
281
282    for border in &borders_entities {
283        let border_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(border).unwrap();
284
285        if Collision::check(CollisionAlgorithm::Aabb, &little_ball_collision, &border_collision) {
286            let velocity_magnitude: f32 = little_ball_velocity.to_vec().magnitude();
287            let collision_normal: Vector2<f32> = if border_collision.collider.position.x > 0.0 {
288                Vector2::new(-1.0, 0.0)
289            } else {
290                Vector2::new(1.0, 0.0)
291            };
292
293            let new_direction: Vector2<f32> = (
294                little_ball_velocity.to_vec().normalize() - 2.0 *
295                little_ball_velocity.to_vec().normalize().dot(collision_normal) * collision_normal
296            ).normalize();
297            
298            let randomized_direction: Vector2<f32> = Vector2::new(
299                new_direction.x + random_factor * 0.3,
300                new_direction.y
301            ).normalize();
302
303            little_ball_velocity.x = randomized_direction.x * velocity_magnitude;
304            little_ball_velocity.y = randomized_direction.y * velocity_magnitude;
305
306            let collision_offset: Vector2<f32> = collision_normal * 0.02;
307            little_ball_transform.position.x += collision_offset.x;
308        }
309    }
310}
311
312fn check_litte_ball_targets_collision(context: &mut Context, little_ball_entity: Entity, random_factor: f32) {
313    let mut targets_query: Query = Query::new(&context.world).with::<Target>();
314    let targets_entities: Vec<Entity> = targets_query.entities_with_components().unwrap();
315
316    let mut little_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
317    let mut little_ball_velocity: ComponentRefMut<'_, Velocity> = context.world.get_entity_component_mut::<Velocity>(&little_ball_entity).unwrap();
318    let little_ball_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&little_ball_entity).unwrap();
319
320    for target in &targets_entities {
321        let target_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(target).unwrap();
322
323        if Collision::check(CollisionAlgorithm::Aabb, &little_ball_collision, &target_collision) {
324            let velocity_magnitude: f32 = little_ball_velocity.to_vec().magnitude();
325            let impact_vector: Vector2<f32> = (target_collision.collider.position - little_ball_collision.collider.position).normalize();
326
327            let mut new_direction: Vector2<f32> = Vector2::new(
328                -impact_vector.x * 0.8 + random_factor * 0.2,
329                -impact_vector.y * 0.8 + random_factor * 0.2
330            ).normalize();
331
332            new_direction.y = new_direction.y.signum() * new_direction.y.abs().max(0.3);
333
334            little_ball_velocity.x = new_direction.x * velocity_magnitude;
335            little_ball_velocity.y = new_direction.y * velocity_magnitude;
336
337            little_ball_transform.position.x -= impact_vector.x * 0.05;
338            little_ball_transform.position.y -= impact_vector.y * 0.05;
339            context.commands.despawn(target.clone());
340        }
341    }
342}
343
344fn respawn_little_ball_after_outbounds(context: &mut Context, little_ball_entity: Entity) {
345    let mut litte_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&little_ball_entity).unwrap();
346    let position_default: Vector2<f32> = Vector2::new(0.0, -0.25);
347
348    if litte_ball_transform.position.y < -1.0 {
349        let mut little_ball_respawn_timer: ResourceRefMut<'_, LittleBallRespawnTimer> = context.world.get_resource_mut::<LittleBallRespawnTimer>().unwrap();
350        little_ball_respawn_timer.0.tick(context.delta);
351
352        if little_ball_respawn_timer.0.is_finished() {
353            litte_ball_transform.set_position(&context.render_state, position_default);
354        }
355    }
356}