let-engine 0.10.0

game engine
Documentation
//#![windows_subsystem = "windows"]
#[cfg(feature = "client")]
use let_engine::prelude::*;

#[cfg(feature = "client")]
use std::{
    f64::consts::{FRAC_PI_2, FRAC_PI_4},
    sync::Arc,
    time::{Duration, SystemTime},
};

// A const that contains the constant window resolution.
#[cfg(feature = "client")]
const RESOLUTION: Vec2 = vec2(800.0, 600.0);

#[cfg(not(feature = "client"))]
fn main() {
    eprintln!("This example requires you to have the `client` feature enabled.");
}

#[cfg(feature = "client")]
fn main() {
    // Describing the window.
    let window_builder = WindowBuilder::default()
        .resizable(false)
        .inner_size(RESOLUTION)
        .title("Pong 2");
    // Initialize the engine.
    let engine = Engine::new(
        EngineSettingsBuilder::default()
            .window_settings(window_builder)
            // Do not update physics because there are no physics.
            .tick_settings(
                TickSettingsBuilder::default()
                    .update_physics(false)
                    .tick_wait(Duration::from_secs_f64(1.0 / 20.0)) // 20 ticks per second
                    .build()
                    .unwrap(),
            )
            .build()
            .unwrap(),
    )
    .unwrap();

    // Initialize the game struct after the engine was initialized.
    let game = Game::new();

    // Runs the game
    engine.start(game);
}

#[cfg(feature = "client")]
struct Game {
    /// Exits the program on true.
    exit: bool,

    left_paddle: Paddle,
    right_paddle: Paddle,
    ball: Ball,

    left_score: Label<Object>,
    right_score: Label<Object>,
}
#[cfg(feature = "client")]
impl Game {
    pub fn new() -> Self {
        let game_layer = SCENE.new_layer();
        let ui_layer = SCENE.new_layer();
        // limits the view to -1 to 1 max
        game_layer.set_camera_settings(CameraSettings::default().mode(CameraScaling::Limited));
        ui_layer.set_camera_settings(
            CameraSettings::default()
                .mode(CameraScaling::Expand)
                .zoom(0.8),
        );

        // Make left paddle controlled with W for up and S for down.
        let left_paddle = Paddle::new(&game_layer, (VirtualKeyCode::W, VirtualKeyCode::S), -0.95);
        // The right paddle controlled with J and K. Weird controls, but 60% keyboard friendly
        let right_paddle = Paddle::new(&game_layer, (VirtualKeyCode::K, VirtualKeyCode::J), 0.95);

        // Spawns a ball in the middle.
        let ball = Ball::new(&game_layer);

        // Loading the font for the score.
        let font = Font::from_slice(include_bytes!("Px437_CL_Stingray_8x16.ttf"))
            .expect("Font is invalid.");

        // Making a default label for the left side.
        let left_score = Label::new(
            &font,
            LabelCreateInfo {
                appearance: Appearance::new().transform(Transform::default().size(vec2(0.5, 0.7))),
                text: "0".to_string(),
                align: Direction::No,
                transform: Transform::default().position(vec2(-0.55, 0.0)),
                scale: vec2(50.0, 50.0),
            },
        );
        // initialize this one to the ui
        let left_score = left_score.init(&ui_layer).unwrap();

        // Making a default label for the right side.
        let right_score = Label::new(
            &font,
            LabelCreateInfo {
                appearance: Appearance::new().transform(Transform::default().size(vec2(0.5, 0.7))),
                text: "0".to_string(),
                align: Direction::Nw,
                transform: Transform::default().position(vec2(0.55, 0.0)),
                scale: vec2(50.0, 50.0),
            },
        );
        let right_score = right_score.init(&ui_layer).unwrap();

        // Just the line in the middle.
        let mut middle_line = NewObject::new();

        // Make a custom model that is just 2 lines.
        const MIDDLE_DATA: Data = Data::new_fixed(
            &[
                vert(0.0, 0.7),
                vert(0.0, 0.3),
                vert(0.0, -0.3),
                vert(0.0, -0.7),
            ],
            &[0, 1, 2, 3],
        );
        middle_line
            .appearance
            .set_model(Model::Custom(ModelData::new(MIDDLE_DATA).unwrap()));
        // A description of how the line should look like.
        let line_material = MaterialSettingsBuilder::default()
            .line_width(10.0)
            .topology(Topology::LineList)
            .build()
            .unwrap();
        let line_material = Material::new(line_material).unwrap();
        middle_line.appearance.set_material(Some(line_material));
        middle_line.init(&ui_layer).unwrap();

        Self {
            exit: false,
            left_paddle,
            right_paddle,
            ball,

            left_score,
            right_score,
        }
    }
}

#[cfg(feature = "client")]
impl let_engine::Game for Game {
    fn update(&mut self) {
        // run the update functions of the paddles.
        self.left_paddle.update();
        self.right_paddle.update();
        self.ball.update();
        self.left_score
            .update_text(format!("{}", self.ball.wins[0]));
        self.right_score
            .update_text(format!("{}", self.ball.wins[1]));
    }
    fn event(&mut self, event: events::Event) {
        match event {
            // Exit when the X button is pressed.
            Event::Window(WindowEvent::CloseRequested) => {
                self.exit = true;
            }
            Event::Input(InputEvent::KeyboardInput { input }) => {
                if input.state == ElementState::Pressed {
                    match input.keycode {
                        // Exit when the escape key is pressed.
                        Some(VirtualKeyCode::Escape) => self.exit = true,
                        // Troll the right paddle
                        Some(VirtualKeyCode::E) => {
                            self.right_paddle.shrink();
                        }
                        // Grow and show the right paddle whos boss.
                        Some(VirtualKeyCode::Q) => {
                            self.left_paddle.grow();
                        }
                        // Oh, so the left paddle thinks it's funny. I'll show it.
                        Some(VirtualKeyCode::Left) => {
                            self.left_paddle.shrink();
                        }
                        // I can grow too, noob.
                        Some(VirtualKeyCode::Right) => {
                            self.right_paddle.grow();
                        }
                        _ => (),
                    }
                }
            }
            _ => (),
        }
    }
    fn exit(&self) -> bool {
        self.exit
    }
}

#[cfg(feature = "client")]
struct Paddle {
    controls: (VirtualKeyCode, VirtualKeyCode), //up/down
    object: Object,
    height: f32,
}

#[cfg(feature = "client")]
impl Paddle {
    pub fn new(layer: &Arc<Layer>, controls: (VirtualKeyCode, VirtualKeyCode), x: f32) -> Self {
        let height = 0.05;
        let mut object = NewObject::new();
        object.transform = Transform {
            position: vec2(x, 0.0),
            size: vec2(0.015, height),
            ..Default::default()
        };

        // Make a collider that resembles the form of the paddle.
        object.set_collider(Some(ColliderBuilder::square(0.015, height).build()));

        // Initialize the object to the given layer.
        let object = object.init(layer).unwrap();
        Self {
            controls,
            object,
            height,
        }
    }
    pub fn update(&mut self) {
        // Turn the `True` and `False` of the input.key_down() into 1, 0 or -1.
        let shift = INPUT.key_down(self.controls.0) as i32 - INPUT.key_down(self.controls.1) as i32;

        // Shift Y and clamp it between 0.51 so it doesn't go out of bounds.
        let y = &mut self.object.transform.position.y;
        *y -= shift as f32 * TIME.delta_time() as f32 * 1.3;
        *y = y.clamp(-0.70, 0.70);

        // Updates the object in the game.
        self.object.sync();
    }
    /// To troll the opponent.
    pub fn shrink(&mut self) {
        self.resize(-0.001);
    }
    /// GROW BACK!
    pub fn grow(&mut self) {
        self.resize(0.001);
    }
    fn resize(&mut self, difference: f32) {
        self.height += difference;
        self.height = self.height.clamp(0.001, 0.7);
        self.object.transform.size.y = self.height;
        self.object
            .set_collider(Some(ColliderBuilder::square(0.015, self.height).build()));
        self.object.sync();
    }
}

#[cfg(feature = "client")]
struct Ball {
    object: Object,
    layer: Arc<Layer>,
    direction: Vec2,
    speed: f32,
    new_round: SystemTime,
    pub wins: [u32; 2],

    bounce_sound: Sound,
}

/// Ball logic.
#[cfg(feature = "client")]
impl Ball {
    pub fn new(layer: &Arc<Layer>) -> Self {
        let lifetime = SystemTime::now();
        let mut object = NewObject::new();
        object.transform.size = vec2(0.015, 0.015);
        let object = object.init(layer).unwrap();
        // make a sound to play when bouncing.
        let mut bounce_sound = Sound::new(
            SoundData::gen_square_wave(777.0, 0.03),
            SoundSettings::default().volume(0.05),
        );
        bounce_sound.bind_to_object(Some(&object));

        Self {
            object,
            layer: layer.clone(),
            direction: Self::random_direction(),
            speed: 1.1,
            new_round: lifetime,
            wins: [0; 2],
            bounce_sound,
        }
    }
    pub fn update(&mut self) {
        // Wait one second before starting the round.
        if self.new_round.elapsed().unwrap().as_secs() > 0 {
            let position = self.object.transform.position;

            // Check if the ball is touching a paddle.
            let touching_paddle = self
                .layer
                .intersection_with_shape(Shape::square(0.02, 0.02), (position, 0.0))
                .is_some();
            // Check if the top side or bottom side are touched by checking if the ball position is below or above the screen edges +- the ball size.
            let touching_floor =
                position.y < self.layer.side_to_world(vec2(0.0, 1.0), RESOLUTION).y + 0.015;
            let touching_roof =
                position.y > self.layer.side_to_world(vec2(0.0, -1.0), RESOLUTION).y - 0.015;
            let touching_wall = position.x.abs() > 1.0;

            if touching_paddle
                && (self.direction.x.is_sign_negative()
                    == self.object.transform.position.x.is_sign_negative())
            {
                self.rebound(position.x as f64);
                // It's getting faster with time.
                self.speed += 0.02;
            } else if touching_floor {
                self.direction.y = self.direction.y.abs();
            } else if touching_roof {
                self.direction.y = -self.direction.y.abs();
            } else if touching_wall {
                // Right wins increase by 1 in case the X is negative.
                if position.x.is_sign_negative() {
                    self.wins[1] += 1;
                } else {
                    self.wins[0] += 1;
                }
                self.reset();
                return;
            }

            self.object.transform.position +=
                self.direction * TIME.delta_time() as f32 * self.speed;
            self.object.sync();
            self.bounce_sound.update(Tween::default()).unwrap();
        }
    }
    fn reset(&mut self) {
        self.new_round = SystemTime::now();
        self.object.transform.position = vec2(0.0, 0.0);
        self.direction = Self::random_direction();
        self.speed = 1.1;
        self.object.sync();
    }
    fn rebound(&mut self, x: f64) {
        // Random 0.0 to 1.0 value. Some math that makes a random direction.
        let random = (TIME.time() * 135225.3).sin().copysign(-x);
        let direction = random.mul_add(FRAC_PI_2, FRAC_PI_4.copysign(-x)) - FRAC_PI_2;

        self.direction = Vec2::from_angle(direction as f32).normalize();

        // play the bounce sound.
        self.bounce_sound.play().unwrap();
    }
    fn random_direction() -> Vec2 {
        // Random -1.0 to 1.0 value. Some math that makes a random direction.
        let random = (TIME.time() * 135225.3).sin();
        let direction = random.mul_add(FRAC_PI_2, FRAC_PI_4.copysign(random)) - FRAC_PI_2;
        Vec2::from_angle(direction as f32)
    }
}