pong/
pong.rs

1//! This is a more complex example of a more complete application.
2//! The Pong arcade game, but with sprites.
3//! This example shows off the usage of timer, audio, input and physics.
4//! The timer works as a respawn for the pong ball after it goes outbounds.
5//! The audio is used for the game music (streaming) and for the rackets hits (static).
6//! The input is used for mapping the users keyboard actions.
7
8use lotus_engine::*;
9use rand::{rngs::ThreadRng, Rng};
10use std::time::Duration;
11
12#[derive(Component)]
13struct Border();
14
15#[derive(Component)]
16struct Racket();
17
18#[derive(Component)]
19struct GrayRacket();
20
21#[derive(Component)]
22struct PinkRacket();
23
24#[derive(Component)]
25struct PongBall();
26
27#[derive(Clone, Resource)]
28pub struct PongBallRespawnTimer(pub Timer);
29
30impl Default for PongBallRespawnTimer {
31    fn default() -> Self {
32        return Self(Timer::new(TimerType::Repeat, Duration::new(2, 0)))
33    }
34}
35
36#[derive(Resource)]
37pub struct GameAudio(pub AudioSource);
38
39impl Default for GameAudio {
40    fn default() -> Self {
41        return Self(AudioSource::new().expect("Should create a audio source."));
42    }
43}
44
45your_game!(
46    WindowConfiguration {
47        icon_path: "textures/pong/pink_racket_256x256.png".to_string(),
48        title: "Pong Game :)".to_string(),
49        background_color: None,
50        background_image_path: Some("textures/pong/pong_background_960x600.png".to_string()),
51        width: 960.0,
52        height: 600.0,
53        position_x: 200.0,
54        position_y: 150.0,
55        resizable: false,
56        decorations: true,
57        transparent: false,
58        active: true,
59        enabled_buttons: WindowButtons::CLOSE | WindowButtons::MINIMIZE,
60        present_mode: PresentMode::AutoNoVsync
61    },
62    setup,
63    update
64);
65
66fn setup(context: &mut Context) {
67    let gray_racket_sprite: Sprite = Sprite::new("textures/pong/gray_racket_256x256.png".to_string());
68    let pink_racket_sprite: Sprite = Sprite::new("textures/pong/pink_racket_256x256.png".to_string());
69    let pong_ball_sprite: Sprite = Sprite::new("textures/pong/pong_ball_left_256x256.png".to_string());
70
71    let mut game_audio: GameAudio = GameAudio::default();
72    game_audio.0.load_streaming_sound(
73        "game_music",
74        "audio/pong/soundtrack/arcade_music.ogg",
75        AudioSettings::default().loop_region(..).volume(Value::Fixed(Decibels(-10.0)))
76    ).ok();
77    game_audio.0.play_streaming_sound("game_music".to_string()).ok();
78
79    game_audio.0.load_static_sound(
80        "racket_hit",
81        "audio/pong/effect/pong_hit.wav",
82        AudioSettings::default()
83    ).ok();
84
85    context.commands.add_resources(vec![
86        Box::new(PongBallRespawnTimer::default()),
87        Box::new(game_audio)
88    ]);
89
90    spawn_border(context, Vector2::new(0.0, -1.0));
91    spawn_border(context, Vector2::new(0.0, 1.0));
92
93    context.commands.spawn(
94        vec![
95            Box::new(gray_racket_sprite),
96            Box::new(Transform::new(
97                Position::new(Vector2::new(-1.0, 0.23), Strategy::Normalized),
98                0.0,
99                Vector2::new(0.55, 0.55)
100            )),
101            Box::new(Racket()),
102            Box::new(GrayRacket()),
103            Box::new(Velocity::new(Vector2::new(1.5, 1.5))),
104            Box::new(Collision::new(Collider::new_simple(GeometryType::Square)))
105        ]
106    );
107
108    context.commands.spawn(
109        vec![
110            Box::new(pink_racket_sprite),
111            Box::new(Transform::new(
112                Position::new(Vector2::new(1.0, 0.25), Strategy::Normalized),
113                0.0,
114                Vector2::new(0.55, 0.55)
115            )),
116            Box::new(Racket()),
117            Box::new(PinkRacket()),
118            Box::new(Velocity::new(Vector2::new(1.5, 1.5))),
119            Box::new(Collision::new(Collider::new_simple(GeometryType::Square)))
120        ]
121    );
122
123    context.commands.spawn(
124        vec![
125            Box::new(pong_ball_sprite),
126            Box::new(Transform::new(
127                Position::new(Vector2::new(0.0, 0.0), Strategy::Normalized),
128                0.0,
129                Vector2::new(0.55, 0.55)
130            )),
131            Box::new(PongBall()),
132            Box::new(Velocity::new(Vector2::new(1.0, 1.0))),
133            Box::new(Collision::new(Collider::new_simple(GeometryType::Square)))
134        ]
135    );
136}
137
138fn update(context: &mut Context) {
139    let input: Input = {
140        let input_ref: ResourceRefMut<'_, Input> = context.world.get_resource_mut::<Input>().unwrap();
141        input_ref.clone()
142    };
143
144    let mut pong_ball_query: Query = Query::new(&context.world).with::<PongBall>();
145    let pong_ball_entities: Vec<Entity> = pong_ball_query.entities_with_components().unwrap();
146    let pong_ball: &Entity = pong_ball_entities.first().unwrap();
147    let mut thread_rng: ThreadRng = rand::rng();
148    let random_factor: f32 = thread_rng.random_range(-0.5..0.5);
149
150    move_gray_racket(context, input.clone());
151    move_pink_racket(context, input.clone());
152    move_pong_ball(context, pong_ball);
153    check_rackets_ball_collision(context, pong_ball, random_factor);
154    check_borders_ball_collision(context, pong_ball, random_factor);
155    respawn_pong_ball_after_outbounds(context, pong_ball);
156}
157
158fn spawn_border(context: &mut Context, position: Vector2<f32>) {
159    let border: Shape = Shape::new(Orientation::Horizontal, GeometryType::Rectangle, Color::BLACK);
160
161    context.commands.spawn(
162        vec![
163            Box::new(border),
164            Box::new(Border()),
165            Box::new(Transform::new(
166                Position::new(position, Strategy::Normalized),
167                0.0,
168                Vector2::new(context.window_configuration.width as f32, 0.01)
169            )),
170            Box::new(Collision::new(Collider::new_simple(GeometryType::Rectangle)))
171        ]
172    );
173}
174
175fn move_gray_racket(context: &mut Context, input: Input) {
176    let mut query: Query = Query::new(&context.world).with::<GrayRacket>();
177    let entities: Vec<Entity> = query.entities_with_components().unwrap();
178    let gray_racket_entity: &Entity = entities.first().unwrap();
179
180    let mut transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(gray_racket_entity).unwrap();
181    let velocity: ComponentRef<'_, Velocity> = context.world.get_entity_component::<Velocity>(gray_racket_entity).unwrap();
182
183    if input.is_key_pressed(KeyCode::KeyW) {
184        transform.position.y += velocity.y * context.delta;
185        let new_position: Vector2<f32> = Vector2::new(transform.position.x, transform.position.y);
186        transform.set_position(&context.render_state, new_position);
187    } else if input.is_key_pressed(KeyCode::KeyS) {
188        transform.position.y -= velocity.y * context.delta;
189        let new_position: Vector2<f32> = Vector2::new(transform.position.x, transform.position.y);
190        transform.set_position(&context.render_state, new_position);
191    }
192}
193
194fn move_pink_racket(context: &mut Context, input: Input) {
195    let mut query: Query = Query::new(&context.world).with::<PinkRacket>();
196    let entities: Vec<Entity> = query.entities_with_components().unwrap();
197    let pink_racket_entity: &Entity = entities.first().unwrap();
198
199    let mut transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(pink_racket_entity).unwrap();
200    let velocity: ComponentRef<'_, Velocity> = context.world.get_entity_component::<Velocity>(pink_racket_entity).unwrap();
201    
202    if input.is_key_pressed(KeyCode::ArrowUp) {
203        transform.position.y += velocity.y * context.delta;
204        let new_position: Vector2<f32> = Vector2::new(transform.position.x, transform.position.y);
205        transform.set_position(&context.render_state, new_position);
206    } else if input.is_key_pressed(KeyCode::ArrowDown) {
207        transform.position.y -= velocity.y * context.delta;
208        let new_position: Vector2<f32> = Vector2::new(transform.position.x, transform.position.y);
209        transform.set_position(&context.render_state, new_position);
210    }
211}
212
213fn move_pong_ball(context: &mut Context, pong_ball: &Entity) {
214    let mut transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&pong_ball).unwrap();
215    let velocity: ComponentRef<'_, Velocity> = context.world.get_entity_component::<Velocity>(&pong_ball).unwrap();
216
217    let new_position: Vector2<f32> = transform.position.to_vec() + velocity.to_vec() * context.delta;
218    transform.set_position(&context.render_state, new_position);
219}
220
221fn check_rackets_ball_collision(context: &mut Context, pong_ball: &Entity, random_factor: f32) {
222    let mut racket_query: Query = Query::new(&context.world).with::<Racket>();
223    let rackets: Vec<Entity> = racket_query.entities_with_components().unwrap();
224    let mut game_audio: ResourceRefMut<'_, GameAudio> = context.world.get_resource_mut::<GameAudio>().unwrap();
225
226    for racket in &rackets {
227        let racket_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(racket).unwrap();
228        let racket_transform: ComponentRef<'_, Transform> = context.world.get_entity_component::<Transform>(racket).unwrap();
229
230        let pong_ball_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&pong_ball).unwrap();
231        let mut pong_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&pong_ball).unwrap();
232        let mut pong_ball_velocity: ComponentRefMut<'_, Velocity> = context.world.get_entity_component_mut::<Velocity>(&pong_ball).unwrap();
233
234        if Collision::check(CollisionAlgorithm::Aabb, &racket_collision, &pong_ball_collision) {
235            game_audio.0.play_static_sound("racket_hit".to_string()).ok();
236
237            let relative_collision_point: f32 = pong_ball_transform.position.y - racket_transform.position.y;
238            let rebound_angle: f32 = relative_collision_point * 1.0 + random_factor;
239
240            let pong_ball_new_velocity: Vector2<f32>;
241
242            if racket_transform.position.x > 0.0 {
243                pong_ball_new_velocity = Vector2::new(-1.0, rebound_angle).normalize() * pong_ball_velocity.to_vec().magnitude();
244                pong_ball_velocity.x = pong_ball_new_velocity.x; pong_ball_velocity.y = pong_ball_new_velocity.y;
245                pong_ball_transform.position.x -= 0.1;
246            } else if racket_transform.position.x < 0.0 {
247                pong_ball_new_velocity = Vector2::new(1.0, rebound_angle).normalize() * pong_ball_velocity.to_vec().magnitude();
248                pong_ball_velocity.x = pong_ball_new_velocity.x; pong_ball_velocity.y = pong_ball_new_velocity.y;
249                pong_ball_transform.position.x += 0.1;
250            }
251            let new_position: Vector2<f32> = Vector2::new(pong_ball_transform.position.x, pong_ball_transform.position.y);
252            pong_ball_transform.set_position(&context.render_state, new_position);
253        }
254    }
255}
256
257fn check_borders_ball_collision(context: &mut Context, pong_ball: &Entity, random_factor: f32) {
258    let mut border_query: Query = Query::new(&context.world).with::<Border>();
259    let borders: Vec<Entity> = border_query.entities_with_components().unwrap();
260
261    for border in &borders {
262        let border_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(border).unwrap();
263        let border_transform: ComponentRef<'_, Transform> = context.world.get_entity_component::<Transform>(border).unwrap();
264
265        let pong_ball_collision: ComponentRef<'_, Collision> = context.world.get_entity_component::<Collision>(&pong_ball).unwrap();
266        let mut pong_ball_velocity: ComponentRefMut<'_, Velocity> = context.world.get_entity_component_mut::<Velocity>(&pong_ball).unwrap();
267        let mut pong_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(&pong_ball).unwrap();
268
269        let pong_ball_new_velocity: Vector2<f32>;
270
271        if Collision::check(CollisionAlgorithm::Aabb, &border_collision, &pong_ball_collision) {
272            if border_transform.position.y > 0.0 {
273                pong_ball_new_velocity = Vector2::new(pong_ball_velocity.x.signum(), -1.0 + random_factor).normalize() * pong_ball_velocity.to_vec().magnitude();
274                pong_ball_velocity.x = pong_ball_new_velocity.x; pong_ball_velocity.y = pong_ball_new_velocity.y;
275                pong_ball_transform.position.y -= 0.1;
276            } else if border_transform.position.y < 0.0 {
277                pong_ball_new_velocity = Vector2::new(pong_ball_velocity.x.signum(), 1.0 + random_factor).normalize() * pong_ball_velocity.to_vec().magnitude();
278                pong_ball_velocity.x = pong_ball_new_velocity.x; pong_ball_velocity.y = pong_ball_new_velocity.y;
279                pong_ball_transform.position.y += 0.1;
280            }
281            let new_position: Vector2<f32> = Vector2::new(pong_ball_transform.position.x, pong_ball_transform.position.y);
282            pong_ball_transform.set_position(&context.render_state, new_position);
283        }
284    }
285}
286
287fn respawn_pong_ball_after_outbounds(context: &mut Context, pong_ball: &Entity) {
288    let mut pong_ball_transform: ComponentRefMut<'_, Transform> = context.world.get_entity_component_mut::<Transform>(pong_ball).unwrap();
289    let position_default: Vector2<f32> = Vector2::new(0.0, 0.0);
290
291    if pong_ball_transform.position.x > 2.0 || pong_ball_transform.position.x < -2.0 {
292        let mut pong_ball_respawn_timer: ResourceRefMut<'_, PongBallRespawnTimer> = context.world.get_resource_mut::<PongBallRespawnTimer>().unwrap();
293        pong_ball_respawn_timer.0.tick(context.delta);
294
295        if pong_ball_respawn_timer.0.is_finished() {
296            pong_ball_transform.set_position(&context.render_state, position_default);
297        }
298    }
299}