use bevy::{
app::AppExit,
platform::collections::HashMap,
prelude::{Sprite as BevySprite, *},
time::Time,
window::{PrimaryWindow, WindowPlugin},
};
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,
};
pub use bevy::window::{CursorOptions, Window, WindowMode, WindowResolution};
#[derive(Default, Debug, Resource)]
pub struct Engine {
pub sprites: HashMap<String, Sprite>,
pub texts: HashMap<String, Text>,
pub should_exit: bool,
pub show_colliders: bool,
last_show_colliders: bool,
pub collision_events: Vec<CollisionEvent>,
pub mouse_state: MouseState,
pub mouse_button_events: Vec<MouseButtonInput>,
pub mouse_location_events: Vec<CursorMoved>,
pub mouse_motion_events: Vec<MouseMotion>,
pub mouse_wheel_events: Vec<MouseWheel>,
pub keyboard_state: KeyboardState,
pub keyboard_events: Vec<KeyboardInput>,
pub delta: Duration,
pub delta_f32: f32,
pub time_since_startup: Duration,
pub time_since_startup_f64: f64,
pub audio_manager: AudioManager,
pub window_dimensions: Vec2,
}
impl Engine {
#[must_use]
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));
self.sprites.get_mut(&label).unwrap()
}
#[must_use]
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);
self.texts.get_mut(&label).unwrap()
}
}
#[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);
}
fn add_collider_lines(commands: &mut Commands, sprite: &mut Sprite) {
let points = sprite.collider.points(); if points.len() >= 2 {
let mut shape_path = ShapePath::new().move_to(points[0]);
for point in &points[1..] {
shape_path = shape_path.line_to(*point);
}
shape_path = shape_path.close();
let transform = sprite.bevy_transform();
let line_width = 1.0 / transform.scale.x;
commands
.spawn((
ShapeBuilder::with(&shape_path)
.stroke(Stroke::new(Color::WHITE, line_width))
.build(),
transform,
))
.insert(ColliderLines {
sprite_label: sprite.label.clone(),
});
}
sprite.collider_dirty = false;
}
#[doc(hidden)]
pub fn add_sprites(commands: &mut Commands, asset_server: &Res<AssetServer>, engine: &mut Engine) {
for (_, sprite) in engine.sprites.drain() {
let transform = sprite.bevy_transform();
let texture_path = sprite.filepath.clone();
commands.spawn((
sprite,
BevySprite {
image: asset_server.load(texture_path),
..Default::default()
},
transform,
));
}
}
#[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((
text,
Text2d(text_string),
TextFont {
font: asset_server.load(font_path),
font_size,
..Default::default()
},
TextColor(Color::WHITE),
TextLayout::new_with_justify(Justify::Center).with_no_wrap(),
transform,
));
}
}
#[doc(hidden)]
pub fn update_window_dimensions(
window_query: Query<&Window, With<PrimaryWindow>>,
mut engine: ResMut<Engine>,
) {
let Ok(window) = window_query.single() else {
return;
};
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);
}
}
#[derive(Component)]
#[doc(hidden)]
pub struct ColliderLines {
sprite_label: String,
}
pub struct Game<S: Resource + Send + Sync + 'static> {
app: App,
engine: Engine,
logic_functions: LogicFuncVec<S>,
window: Window,
}
impl<S: Resource + Send + Sync + 'static> Default for Game<S> {
fn default() -> Self {
Self {
app: App::new(),
engine: Engine::default(),
logic_functions: LogicFuncVec(vec![]),
window: Window {
title: "Rusty Engine".into(),
..Default::default()
},
}
}
}
impl<S: Resource + Send + Sync + 'static> Game<S> {
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()
}
pub fn window_settings(&mut self, window: Window) -> &mut Self {
self.window = window;
self
}
pub fn run(&mut self, initial_game_state: S) {
self.app.insert_resource::<S>(initial_game_state);
self.app
.insert_resource(ClearColor(Color::srgb(0.4, 0.4, 0.4)))
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(self.window.clone()),
..Default::default()
})
.set(ImagePlugin::default_nearest()),
)
.add_systems(
Update,
(
(close_on_esc),
(update_window_dimensions, game_logic_sync::<S>),
),
)
.add_plugins(ShapePlugin) .add_plugins((
AudioManagerPlugin,
KeyboardPlugin,
MousePlugin,
PhysicsPlugin,
))
.add_systems(Startup, setup);
self.app.world_mut().spawn(Camera2d);
let engine = std::mem::take(&mut self.engine);
self.app.insert_resource(engine);
let mut logic_functions = LogicFuncVec(vec![]);
std::mem::swap(&mut self.logic_functions, &mut logic_functions);
self.app.insert_resource(logic_functions);
self.app.run();
}
pub fn add_logic(&mut self, logic_function: fn(&mut Engine, &mut S)) {
self.logic_functions.0.push(logic_function);
}
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
fn game_logic_sync<S: Resource + Send + Sync + 'static>(
mut commands: Commands,
asset_server: Res<AssetServer>,
mut engine: ResMut<Engine>,
mut game_state: ResMut<S>,
logic_functions: Res<LogicFuncVec<S>>,
keyboard_state: Res<KeyboardState>,
mouse_state: Res<MouseState>,
time: Res<Time>,
mut app_exit_events: MessageWriter<AppExit>,
mut collision_events: MessageReader<CollisionEvent>,
mut query_set: ParamSet<(
Query<(Entity, &mut Sprite, &mut Transform)>,
Query<(
Entity,
&mut Text,
&mut Transform,
&mut Text2d,
&mut TextFont,
)>,
Query<(Entity, &mut Shape, &mut Transform, &ColliderLines)>,
)>,
) {
engine.delta = time.delta();
engine.delta_f32 = time.delta_secs();
engine.time_since_startup = time.elapsed();
engine.time_since_startup_f64 = time.elapsed_secs_f64();
engine.keyboard_state = keyboard_state.clone();
engine.mouse_state = mouse_state.clone();
engine.collision_events.clear();
for collision_event in collision_events.read() {
engine.collision_events.push(collision_event.clone());
}
engine.sprites.clear();
for (_, sprite, _) in query_set.p0().iter() {
let _ = engine
.sprites
.insert(sprite.label.clone(), (*sprite).clone());
}
engine.texts.clear();
for (_, text, _, _, _) in query_set.p1().iter() {
let _ = engine.texts.insert(text.label.clone(), (*text).clone());
}
for func in logic_functions.0.iter() {
func(&mut engine, &mut game_state);
}
if !engine.last_show_colliders && engine.show_colliders {
for sprite in engine.sprites.values_mut() {
add_collider_lines(&mut commands, sprite);
}
} else if engine.last_show_colliders && !engine.show_colliders {
for (entity, _, _, _) in query_set.p2().iter_mut() {
commands.entity(entity).despawn();
}
}
if engine.show_colliders {
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();
}
}
for sprite in engine.sprites.values_mut() {
if sprite.collider_dirty {
add_collider_lines(&mut commands, sprite);
}
}
for (_, mut shape, 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();
transform.translation.z = (transform.translation.z + 0.1).clamp(0.0, 999.1);
}
if let Some(ref mut stroke) = shape.stroke {
stroke.options.line_width = 1.0 / transform.scale.x;
}
}
}
engine.last_show_colliders = engine.show_colliders;
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_sprites(&mut commands, &asset_server, &mut engine);
for (entity, mut text, mut transform, mut bevy_text_component, mut text_font) 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.0 {
bevy_text_component.0 = text.value.clone();
}
#[allow(clippy::float_cmp)]
if text.font_size != text_font.font_size {
text_font.font_size = text.font_size;
}
let font = asset_server.load(text.font.clone());
if text_font.font != font {
text_font.font = font;
}
} else {
commands.entity(entity).despawn();
}
}
add_texts(&mut commands, &asset_server, &mut engine);
if engine.should_exit {
app_exit_events.write(AppExit::Success);
}
}
impl<S: Resource + Send + Sync + 'static> Deref for Game<S> {
type Target = Engine;
fn deref(&self) -> &Self::Target {
&self.engine
}
}
impl<S: Resource + Send + Sync + 'static> DerefMut for Game<S> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.engine
}
}
#[derive(Resource)]
struct LogicFuncVec<S: Resource + Send + Sync + 'static>(Vec<fn(&mut Engine, &mut S)>);
pub fn close_on_esc(
mut commands: Commands,
focused_windows: Query<(Entity, &Window)>,
input: Res<ButtonInput<KeyCode>>,
) {
for (window, focus) in focused_windows.iter() {
if !focus.focused {
continue;
}
if input.just_pressed(KeyCode::Escape) {
commands.entity(window).despawn();
}
}
}