sge 1.0.2

Simple game engine
Documentation
use sge::prelude::*;

const BOUNDS_SIZE: Vec2 = Vec2::new(1000.0, 1000.0);
const BOUNDS_THICKNESS: f32 = 50.0;
const FORCE_RADIUS: f32 = 250.0;
const FORCE_STRENGTH: f32 = 100.0;

#[derive(Clone, Copy, PartialEq)]
enum ShapeType {
    Circle,
    Square,
}

impl ShapeType {
    fn from_index(i: usize) -> Self {
        match i % 2 {
            0 => Self::Circle,
            _ => Self::Square,
        }
    }

    fn bounds(&self) -> Bounds {
        match self {
            Self::Circle => Bounds::Circle(15.0),
            Self::Square => Bounds::Rect(Vec2::splat(30.0)),
        }
    }

    fn draw(&self, pos: Vec2, color: Color, rotation: f32) {
        match self {
            Self::Circle => draw_circle(pos, 15.0, color),
            Self::Square => draw_square_rotation(pos - Vec2::splat(15.0), 30.0, color, rotation),
        }
    }
}

fn speed_color(speed: f32) -> Color {
    Color::from_oklch(
        0.8,
        0.1 + (speed / 100.0).clamp(0.0, 0.1),
        142.94 - (speed / 5.0).clamp(0.0, 142.94 - 26.17),
    )
}

fn main() -> anyhow::Result<()> {
    init("Physics Showcase")?;

    let mut world = World::new();

    let wall_rects = [
        (
            Vec2::new(BOUNDS_THICKNESS * 0.5, BOUNDS_SIZE.y * 0.5),
            Vec2::new(BOUNDS_THICKNESS, BOUNDS_SIZE.y),
        ),
        (
            Vec2::new(BOUNDS_SIZE.x * 0.5, BOUNDS_THICKNESS * 0.5),
            Vec2::new(BOUNDS_SIZE.x, BOUNDS_THICKNESS),
        ),
        (
            Vec2::new(BOUNDS_SIZE.x * 0.5, BOUNDS_SIZE.y - BOUNDS_THICKNESS * 0.5),
            Vec2::new(BOUNDS_SIZE.x, BOUNDS_THICKNESS),
        ),
        (
            Vec2::new(BOUNDS_SIZE.x - BOUNDS_THICKNESS * 0.5, BOUNDS_SIZE.y * 0.5),
            Vec2::new(BOUNDS_THICKNESS, BOUNDS_SIZE.y),
        ),
    ];
    for (pos, size) in wall_rects {
        world.create_fixed(Bounds::Rect(size)).with_position(pos);
    }

    let ramp_pos = Vec2::new(BOUNDS_SIZE.x * 0.5, BOUNDS_SIZE.y * 0.5 + 200.0);
    world
        .create_fixed(Bounds::Triangle(
            Vec2::new(-120.0, 40.0),
            Vec2::new(120.0, 40.0),
            Vec2::new(-120.0, -40.0),
        ))
        .with_position(ramp_pos);

    let sensor_pos = Vec2::new(BOUNDS_SIZE.x * 0.75, BOUNDS_SIZE.y * 0.25);
    let sensor = world
        .create_fixed_with(Bounds::Circle(80.0), ColliderConfig::default().sensor(true))
        .with_position(sensor_pos);

    let mut objects: Vec<(ObjectRef, ShapeType)> = Vec::new();

    for i in 0..50 {
        let pos = Vec2::new(
            rand::<f32>() * (BOUNDS_SIZE.x - BOUNDS_THICKNESS * 2.0) + BOUNDS_THICKNESS,
            rand::<f32>() * (BOUNDS_SIZE.y * 0.6) + BOUNDS_THICKNESS,
        );
        let velocity = Vec2::new(rand::<f32>() * 500.0 - 250.0, rand::<f32>() * 500.0 - 250.0);
        let shape_type = ShapeType::from_index(i);

        let collider = world
            .create_dynamic(shape_type.bounds())
            .with_ccd()
            .with_position(pos);

        let mut collider = collider;
        collider.set_velocity(velocity);
        objects.push((collider, shape_type));
    }

    let mut show_colliders = false;

    set_cursor_visible(false);

    loop {
        world.update();
        clear_screen(Color::NEUTRAL_900);

        draw_rect(
            Vec2::ZERO,
            Vec2::new(BOUNDS_THICKNESS, BOUNDS_SIZE.y),
            Color::NEUTRAL_800,
        );
        draw_rect(
            Vec2::ZERO,
            Vec2::new(BOUNDS_SIZE.x, BOUNDS_THICKNESS),
            Color::NEUTRAL_800,
        );
        draw_rect(
            Vec2::new(0.0, BOUNDS_SIZE.y - BOUNDS_THICKNESS),
            Vec2::new(BOUNDS_SIZE.x, BOUNDS_THICKNESS),
            Color::NEUTRAL_800,
        );
        draw_rect(
            Vec2::new(BOUNDS_SIZE.x - BOUNDS_THICKNESS, 0.0),
            Vec2::new(BOUNDS_THICKNESS, BOUNDS_SIZE.y),
            Color::NEUTRAL_800,
        );

        {
            use ui::prelude::*;
            let ui = Fit::new(Fill::new(
                Color::NEUTRAL_800,
                Padding::all(
                    50.0,
                    Col::new([
                        Text::title_nowrap("Physics Showcase"),
                        Text::mono(format!("Objects: {}", objects.len())),
                        Text::mono(format!("FPS: {:.2}", avg_fps())),
                        Text::h2("Controls"),
                        Text::body("• Left Click: Spawn object"),
                        Text::body("• Right Click (hold): Apply force"),
                        Text::body("• D: Toggle collider debug"),
                        Text::h2("Shapes"),
                        Text::body("Circle, Square, Rect, Capsule (Y/X),"),
                        Text::body("Triangle, Hexagon, Star (compound)"),
                    ]),
                ),
            ));
            ui::draw_ui(ui, vec2(0.0, BOUNDS_SIZE.y - BOUNDS_THICKNESS));
        }

        draw_tri(
            ramp_pos + Vec2::new(-120.0, 40.0),
            ramp_pos + Vec2::new(120.0, 40.0),
            ramp_pos + Vec2::new(-120.0, -40.0),
            Color::NEUTRAL_700,
        );

        let sensor_active = sensor.is_colliding();
        let sensor_fill = if sensor_active {
            Color::CYAN_500.with_alpha(0.25)
        } else {
            Color::CYAN_900.with_alpha(0.15)
        };
        let sensor_outline = if sensor_active {
            Color::CYAN_400
        } else {
            Color::CYAN_700
        };

        draw_circle_with_outline(sensor_pos, 80.0, sensor_fill, sensor_outline, 2.5);
        if sensor_active {
            draw_circle_outline(sensor_pos, 90.0, Color::CYAN_300.with_alpha(0.4), 1.0);
        }

        for (collider, shape_type) in &objects {
            let pos = collider.get_position();
            let speed = collider.get_velocity().length();
            let rot = collider.get_rotation();
            shape_type.draw(pos, speed_color(speed), rot);
        }

        if show_colliders {
            world.draw_colliders();
        }

        if let Some(cursor_pos) = cursor() {
            draw_circle_with_outline(cursor_pos, 10.0, Color::CYAN_400, Color::WHITE, 3.0);

            if mouse_held(MouseButton::Right) {
                for (collider, _) in &mut objects {
                    let pos = collider.get_position();
                    let to_cursor = cursor_pos - pos;
                    let distance = to_cursor.length();
                    if distance < FORCE_RADIUS && distance > 0.0 {
                        let strength =
                            (1.0 - distance.powi(2) / FORCE_RADIUS.powi(2)) * FORCE_STRENGTH;
                        collider.add_velocity(to_cursor.normalize() * strength);
                    }
                }

                draw_radial_gradient_circle_with_outline(
                    cursor_pos,
                    FORCE_RADIUS,
                    Color::CYAN_500.with_alpha(0.2),
                    Color::CYAN_500.with_alpha(0.15),
                    3.0,
                    Color::WHITE,
                );
            }

            if mouse_pressed(MouseButton::Left) {
                let velocity =
                    Vec2::new(rand::<f32>() * 200.0 - 100.0, rand::<f32>() * 200.0 - 100.0);
                let shape_type = ShapeType::from_index(objects.len());
                let mut collider = world.create_dynamic(shape_type.bounds()).with_ccd();
                collider.set_position(cursor_pos);
                collider.set_velocity(velocity);
                objects.push((collider, shape_type));
            }
        }

        if key_pressed(KeyCode::KeyD) {
            show_colliders = !show_colliders;
        }

        if should_quit() {
            break;
        }
        next_frame();
    }

    Ok(())
}