rusty_engine 5.2.1

Learn Rust with a simple, cross-platform, 2D game engine.
Documentation
use bevy::{
    app::AppExit,
    prelude::{
        debug, App, AssetServer, Camera2dBundle, Color, Commands, Component, DefaultPlugins,
        Entity, EventReader, EventWriter, HorizontalAlign, ParallelSystemDescriptorCoercion,
        ParamSet, Query, Res, ResMut, SpriteBundle, Text as BevyText, Text2dBundle, TextAlignment,
        TextStyle, Transform, Vec2, VerticalAlign, Windows,
    },
    render::texture::ImageSettings,
    time::Time,
    utils::HashMap,
    window::close_on_esc,
};
use bevy_prototype_lyon::prelude::*;
use std::{
    ops::{Deref, DerefMut},
    path::PathBuf,
    time::Duration,
};

use crate::{
    audio::AudioManager,
    mouse::{CursorMoved, MouseButtonInput, MouseMotion, MousePlugin, MouseWheel},
    prelude::{
        AudioManagerPlugin, CollisionEvent, KeyboardInput, KeyboardPlugin, KeyboardState,
        MouseState, PhysicsPlugin,
    },
    sprite::Sprite,
    text::Text,
};

// Public re-export
pub use bevy::window::{WindowDescriptor, WindowMode, WindowResizeConstraints};

/// Engine is the primary way that you will interact with Rusty Engine. Each frame this struct
/// is provided to the "logic" functions (or closures) that you provided to [`Game::add_logic`]. The
/// fields in this struct are divided into two groups:
///
/// 1. `SYNCED` fields.
///
/// These fields are marked with `SYNCED`. These fields are shared between you and the engine. Each
/// frame Rusty Engine will populate these fields, then provide them to the user's game logic
/// function, and then examine any changes the user made and sync those changes back to the engine.
///
/// 2. `INFO` fields
///
/// INFO fields are provided as fresh, readable information to you each frame. Since information in
/// these fields are overwritten every frame, any changes to them are ignored. Thus, you can feel
/// free to, e.g. consume all the events out of the `collision_events` vector.
#[derive(Default, Debug)]
pub struct Engine {
    /// SYNCED - The state of all sprites this frame. To add a sprite, use the
    /// [`add_sprite`](Engine::add_sprite) method. Modify & remove sprites as you like.
    pub sprites: HashMap<String, Sprite>,
    /// SYNCED - The state of all texts this frame. For convenience adding a text, use the
    /// [`add_text`](Engine::add_text) method. Modify & remove text as you like.
    pub texts: HashMap<String, Text>,
    /// SYNCED - If set to `true`, the game exits. Note: the current frame will run to completion first.
    pub should_exit: bool,
    /// SYNCED - If set to `true`, then debug lines are shown depicting sprite colliders
    pub show_colliders: bool,
    // so we can tell if the value changed this frame
    last_show_colliders: bool,
    /// INFO - All the collision events that occurred this frame. For collisions to be generated
    /// between sprites, both sprites must have [`Sprite.collision`] set to `true` and both sprites
    /// must have colliders (use the collider example to create a collider for your own images).
    /// Collision events are generated when two sprites' colliders begin or end overlapping in 2D
    /// space.
    pub collision_events: Vec<CollisionEvent>,
    /// INFO - The current state of mouse location and buttons. Useful for input handling that only
    /// cares about the final state of the mouse each frame, and not the intermediate states.
    pub mouse_state: MouseState,
    /// INFO - All the mouse button events that occurred this frame.
    pub mouse_button_events: Vec<MouseButtonInput>,
    /// INFO - All the mouse location events that occurred this frame. The events are Bevy
    /// [`CursorMoved`] structs, but despite the name they represent the _location_ of the mouse
    /// during this frame.
    pub mouse_location_events: Vec<CursorMoved>,
    /// INFO - All the mouse motion events that occurred this frame. These represent the relative
    /// movements of the mouse, not the location of the mouse.
    pub mouse_motion_events: Vec<MouseMotion>,
    /// INFO - All the mouse wheel events that occurred this frame.
    pub mouse_wheel_events: Vec<MouseWheel>,
    /// INFO - The current state of all the keys on the keyboard. Use this to control movement in
    /// your games!  A [`KeyboardState`] has helper methods you should use to query the state of
    /// specific [`KeyCode`](crate::prelude::KeyCode)s.
    pub keyboard_state: KeyboardState,
    /// INFO - All the keyboard input events. These are text-processor-like events. If you are
    /// looking for keyboard events to control movement in a game character, you should use
    /// [`Engine::keyboard_state`] instead. For example, one pressed event will fire when you
    /// start holding down a key, and then after a short delay additional pressed events will occur
    /// at the same rate that additional letters would show up in a word processor. When the key is
    /// finally released, a single released event is emitted.
    pub keyboard_events: Vec<KeyboardInput>,
    /// INFO - The delta time (time between frames) for the current frame as a [`Duration`], perfect
    /// for use with [`Timer`](crate::prelude::Timer)s
    pub delta: Duration,
    /// INFO - The delta time (time between frames) for the current frame as an [`f32`], perfect for
    /// use in math with other `f32`'s. A cheap and quick way to approximate smooth movement
    /// (velocity, accelleration, etc.) is to multiply it by `delta_f32`.
    pub delta_f32: f32,
    /// INFO - The amount of time the game has been running since startup as a [`Duration`]
    pub time_since_startup: Duration,
    /// INFO - The amount of time the game has been running as an [`f64`]. This needs to be an f64,
    /// since it gets to be large enough that an f32 would lose precision. For best results, do your
    /// math on the `f64` and get it to a smaller value _before_ casting it to an `f32`.
    pub time_since_startup_f64: f64,
    /// A struct with methods to play sound effects and music
    pub audio_manager: AudioManager,
    /// INFO - Window dimensions in logical pixels. On high DPI screens, there will often be four
    /// physical pixels per logical pixel. On low DPI screens, one logical pixel is one physical
    /// pixel.
    pub window_dimensions: Vec2,
}

impl Engine {
    #[must_use]
    /// Create and add a [`Sprite`] to the game. Use the `&mut Sprite` that is returned to adjust
    /// the translation, rotation, etc. Use a *unique* label for each sprite. Attempting to add two
    /// sprites with the same label will cause a crash.
    pub fn add_sprite<T: Into<String>, P: Into<PathBuf>>(
        &mut self,
        label: T,
        file_or_preset: P,
    ) -> &mut Sprite {
        let label = label.into();
        self.sprites
            .insert(label.clone(), Sprite::new(label.clone(), file_or_preset));
        // Unwrap: Can't crash because we just inserted the sprite
        self.sprites.get_mut(&label).unwrap()
    }

    #[must_use]
    /// Create and add a [`Text`] to the game. Use the `&mut Text` that is returned to adjust the
    /// translation, rotation, etc. Use a *unique* label for each text. Attempting to add two texts
    /// with the same label will cause a crash.
    pub fn add_text<T, S>(&mut self, label: T, text: S) -> &mut Text
    where
        T: Into<String>,
        S: Into<String>,
    {
        let label = label.into();
        let text = text.into();
        let curr_text = Text {
            label: label.clone(),
            value: text,
            ..Default::default()
        };
        self.texts.insert(label.clone(), curr_text);
        // Unwrap: Can't crash because we just inserted the text
        self.texts.get_mut(&label).unwrap()
    }
}

/// startup system - grab window settings, initialize all the starting sprites
#[doc(hidden)]
pub fn setup(mut commands: Commands, asset_server: Res<AssetServer>, mut engine: ResMut<Engine>) {
    add_sprites(&mut commands, &asset_server, &mut engine);
    add_texts(&mut commands, &asset_server, &mut engine);
}

/// Add visible lines representing a collider
fn add_collider_lines(commands: &mut Commands, sprite: &mut Sprite) {
    // Add the collider lines, a visual representation of the sprite's collider
    let points = sprite.collider.points(); // will be empty vector if NoCollider
    if points.len() >= 2 {
        let mut path_builder = PathBuilder::new();
        path_builder.move_to(points[0]);
        for point in &points[1..] {
            path_builder.line_to(*point);
        }
        path_builder.close(); // draws the line from the last point to the first point
        let line = path_builder.build();
        let transform = sprite.bevy_transform();
        commands
            .spawn_bundle(GeometryBuilder::build_as(
                &line,
                DrawMode::Stroke(StrokeMode::new(Color::WHITE, 1.0 / transform.scale.x)),
                transform,
            ))
            .insert(ColliderLines {
                sprite_label: sprite.label.clone(),
            });
    }
    sprite.collider_dirty = false;
}

/// helper function: Add Bevy components for all the sprites in engine.sprites
#[doc(hidden)]
pub fn add_sprites(commands: &mut Commands, asset_server: &Res<AssetServer>, engine: &mut Engine) {
    for (_, sprite) in engine.sprites.drain() {
        // Create the sprite
        let transform = sprite.bevy_transform();
        let texture_path = sprite.filepath.clone();
        commands.spawn().insert(sprite).insert_bundle(SpriteBundle {
            texture: asset_server.load(texture_path),
            transform,
            ..Default::default()
        });
    }
}

/// Bevy system which adds any needed Bevy components to correspond to the texts in
/// `engine.texts`
#[doc(hidden)]
pub fn add_texts(commands: &mut Commands, asset_server: &Res<AssetServer>, engine: &mut Engine) {
    for (_, text) in engine.texts.drain() {
        let transform = text.bevy_transform();
        let font_size = text.font_size;
        let text_string = text.value.clone();
        let font_path = text.font.clone();
        commands.spawn().insert(text).insert_bundle(Text2dBundle {
            text: BevyText::from_section(
                text_string,
                TextStyle {
                    font: asset_server.load(font_path.as_str()),
                    font_size,
                    color: Color::WHITE,
                },
            )
            .with_alignment(TextAlignment {
                vertical: VerticalAlign::Center,
                horizontal: HorizontalAlign::Center,
            }),
            transform,
            ..Default::default()
        });
    }
}

/// system - update current window dimensions in the engine, because people resize windows
#[doc(hidden)]
pub fn update_window_dimensions(windows: Res<Windows>, mut engine: ResMut<Engine>) {
    // It's possible to not have window dimensions for the first frame or two
    if let Some(window) = windows.get_primary() {
        let screen_dimensions = Vec2::new(window.width(), window.height());
        if screen_dimensions != engine.window_dimensions {
            engine.window_dimensions = screen_dimensions;
            debug!("Set window dimensions: {}", engine.window_dimensions);
        }
    }
}

/// Component to add to the collider lines visualizations to link them to the sprite they represent
#[derive(Component)]
#[doc(hidden)]
pub struct ColliderLines {
    sprite_label: String,
}

/// A [`Game`] represents the entire game and its data.
/// By default the game will spawn an empty window, and exit upon Esc or closing of the window.
/// Under the hood, Rusty Engine syncs the game data to [Bevy](https://bevyengine.org/) to power
/// most of the underlying functionality.
///
/// [`Game`] forwards method calls to [`Engine`] when it can, so you should be able to use all
/// of the methods from [`Engine`] on [`Game`] during your game setup in your `main()` function.
pub struct Game<S: Send + Sync + 'static> {
    app: App,
    engine: Engine,
    logic_functions: Vec<fn(&mut Engine, &mut S)>,
    window_descriptor: WindowDescriptor,
}

impl<S: Send + Sync + 'static> Default for Game<S> {
    fn default() -> Self {
        Self {
            app: App::new(),
            engine: Engine::default(),
            logic_functions: vec![],
            window_descriptor: WindowDescriptor {
                title: "Rusty Engine".into(),
                ..Default::default()
            },
        }
    }
}

impl<S: Send + Sync + 'static> Game<S> {
    /// Create an new, empty [`Game`] with an empty [`Engine`]
    pub fn new() -> Self {
        if std::fs::read_dir("assets").is_err() {
            println!("FATAL: Could not find assets directory. Have you downloaded the assets?\nhttps://github.com/CleanCut/rusty_engine#you-must-download-the-assets-separately");
            std::process::exit(1);
        }
        Default::default()
    }

    /// Use this to set properties of the native OS window before running the game. See the
    /// [window](https://github.com/CleanCut/rusty_engine/blob/main/examples/window.rs) example for
    /// more information.
    pub fn window_settings(&mut self, window_descriptor: WindowDescriptor) -> &mut Self {
        self.window_descriptor = window_descriptor;
        self
    }

    /// Start the game.
    pub fn run(&mut self, initial_game_state: S) {
        self.app
            .insert_resource::<WindowDescriptor>(self.window_descriptor.clone())
            .insert_resource(ImageSettings::default_nearest())
            .insert_resource::<S>(initial_game_state);
        self.app
            // Built-ins
            .add_plugins(DefaultPlugins)
            .add_system(close_on_esc)
            // External Plugins
            .add_plugin(ShapePlugin) // bevy_prototype_lyon, for displaying sprite colliders
            // Rusty Engine Plugins
            .add_plugin(AudioManagerPlugin)
            .add_plugin(KeyboardPlugin)
            .add_plugin(MousePlugin)
            .add_plugin(PhysicsPlugin)
            //.insert_resource(ReportExecutionOrderAmbiguities) // for debugging
            .add_system(
                update_window_dimensions
                    .label("update_window_dimensions")
                    .before("game_logic_sync"),
            )
            .add_system(game_logic_sync::<S>.label("game_logic_sync"))
            .add_startup_system(setup);
        self.app
            .world
            .spawn()
            .insert_bundle(Camera2dBundle::default());
        let engine = std::mem::take(&mut self.engine);
        self.app.insert_resource(engine);
        let logic_functions = std::mem::take(&mut self.logic_functions);
        self.app.insert_resource(logic_functions);
        self.app.run();
    }

    /// `logic_function` is a function or closure that takes two parameters and returns nothing:
    ///
    /// - `engine: &mut Engine`
    /// - `game_state`, which is a mutable reference (`&mut`) to the game state struct you defined,
    ///    or `&mut ()` if you didn't define one.
    pub fn add_logic(&mut self, logic_function: fn(&mut Engine, &mut S)) {
        self.logic_functions.push(logic_function);
    }
}

/// system - the magic that connects Rusty Engine to Bevy, frame by frame
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
fn game_logic_sync<S: Send + Sync + 'static>(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut engine: ResMut<Engine>,
    mut game_state: ResMut<S>,
    logic_functions: Res<Vec<fn(&mut Engine, &mut S)>>,
    keyboard_state: Res<KeyboardState>,
    mouse_state: Res<MouseState>,
    time: Res<Time>,
    mut app_exit_events: EventWriter<AppExit>,
    mut collision_events: EventReader<CollisionEvent>,
    mut query_set: ParamSet<(
        Query<(Entity, &mut Sprite, &mut Transform)>,
        Query<(Entity, &mut Text, &mut Transform, &mut BevyText)>,
        Query<(Entity, &mut DrawMode, &mut Transform, &ColliderLines)>,
    )>,
) {
    // Update this frame's timing info
    engine.delta = time.delta();
    engine.delta_f32 = time.delta_seconds();
    engine.time_since_startup = time.time_since_startup();
    engine.time_since_startup_f64 = time.seconds_since_startup();

    // Copy keyboard state over to engine to give to users
    engine.keyboard_state = keyboard_state.clone();

    // Copy mouse state over to engine to give to users
    engine.mouse_state = mouse_state.clone();

    // Copy all collision events over to the engine to give to users
    engine.collision_events.clear();
    for collision_event in collision_events.iter() {
        engine.collision_events.push(collision_event.clone());
    }

    // Copy all sprites over to the engine to give to users
    engine.sprites.clear();
    for (_, sprite, _) in query_set.p0().iter() {
        let _ = engine
            .sprites
            .insert(sprite.label.clone(), (*sprite).clone());
    }

    // Copy all texts over to the engine to give to users
    engine.texts.clear();
    for (_, text, _, _) in query_set.p1().iter() {
        let _ = engine.texts.insert(text.label.clone(), (*text).clone());
    }

    // Perform all the user's game logic for this frame
    for func in logic_functions.iter() {
        func(&mut engine, &mut game_state);
    }

    if !engine.last_show_colliders && engine.show_colliders {
        // Just turned on show_colliders -- create collider lines for all sprites
        for sprite in engine.sprites.values_mut() {
            add_collider_lines(&mut commands, sprite);
        }
    } else if engine.last_show_colliders && !engine.show_colliders {
        // Just turned off show_colliders -- delete collider lines for all sprites
        for (entity, _, _, _) in query_set.p2().iter_mut() {
            commands.entity(entity).despawn();
        }
    }
    // Update transform & line width of all collider lines
    if engine.show_colliders {
        // Delete collider lines for sprites which are missing, or whose colliders are dirty
        for (entity, _, _, collider_lines) in query_set.p2().iter_mut() {
            if let Some(sprite) = engine.sprites.get(&collider_lines.sprite_label) {
                if sprite.collider_dirty {
                    commands.entity(entity).despawn();
                }
            } else {
                commands.entity(entity).despawn();
            }
        }
        // Add collider lines for sprites whose colliders are dirty
        for sprite in engine.sprites.values_mut() {
            if sprite.collider_dirty {
                add_collider_lines(&mut commands, sprite);
            }
        }
        // Update transform & line width
        for (_, mut draw_mode, mut transform, collider_lines) in query_set.p2().iter_mut() {
            if let Some(sprite) = engine.sprites.get(&collider_lines.sprite_label) {
                *transform = sprite.bevy_transform();
                // We want collider lines to appear on top of the sprite they are for, so they need a
                // slightly higher z value. We tell users to only use up to 999.0.
                transform.translation.z = (transform.translation.z + 0.1).clamp(0.0, 999.1);
            }
            // Stroke line width gets scaled with the transform, but we want it to appear to be the same
            // regardless of scale, so we have to counter the scale.
            if let DrawMode::Stroke(ref mut stroke_mode) = *draw_mode {
                let line_width = 1.0 / transform.scale.x;
                *stroke_mode = StrokeMode::new(Color::WHITE, line_width);
            }
        }
    }
    engine.last_show_colliders = engine.show_colliders;

    // Transfer any changes in the user's Sprite copies to the Bevy Sprite and Transform components
    for (entity, mut sprite, mut transform) in query_set.p0().iter_mut() {
        if let Some(sprite_copy) = engine.sprites.remove(&sprite.label) {
            *sprite = sprite_copy;
            *transform = sprite.bevy_transform();
        } else {
            commands.entity(entity).despawn();
        }
    }

    // Add Bevy components for any new sprites remaining in engine.sprites
    add_sprites(&mut commands, &asset_server, &mut engine);

    // Transfer any changes in the user's Texts to the Bevy Text and Transform components
    for (entity, mut text, mut transform, mut bevy_text_component) in query_set.p1().iter_mut() {
        if let Some(text_copy) = engine.texts.remove(&text.label) {
            *text = text_copy;
            *transform = text.bevy_transform();
            if text.value != bevy_text_component.sections[0].value {
                bevy_text_component.sections[0].value = text.value.clone();
            }
            #[allow(clippy::float_cmp)]
            if text.font_size != bevy_text_component.sections[0].style.font_size {
                bevy_text_component.sections[0].style.font_size = text.font_size;
            }
            let font = asset_server.load(text.font.as_str());
            if bevy_text_component.sections[0].style.font != font {
                bevy_text_component.sections[0].style.font = font;
            }
        } else {
            commands.entity(entity).despawn();
        }
    }

    // Add Bevy components for any new texts remaining in engine.texts
    add_texts(&mut commands, &asset_server, &mut engine);

    if engine.should_exit {
        app_exit_events.send(AppExit);
    }
}

// The Deref and DerefMut implementations make it so that you can call all the `Engine` methods
// on a `Game`, which is much more straightforward for game setup in `main()`
impl<S: Send + Sync + 'static> Deref for Game<S> {
    type Target = Engine;

    fn deref(&self) -> &Self::Target {
        &self.engine
    }
}

impl<S: Send + Sync + 'static> DerefMut for Game<S> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.engine
    }
}