├── emerald ├── src │ ├── world │ │ ├── physics │ │ │ ├── components.rs │ │ │ ├── types.rs │ │ │ └── physics_handler.rs │ │ ├── physics.rs │ │ ├── world_physics_loader.rs │ │ ├── ent │ │ │ ├── ent_color_rect_loader.rs │ │ │ ├── ent_sound_player_loader.rs │ │ │ ├── ent_transform_loader.rs │ │ │ ├── ent_sprite_loader.rs │ │ │ ├── ent_aseprite_loader.rs │ │ │ ├── ent_rigid_body_loader.rs │ │ │ └── ent_label_loader.rs │ │ └── ent.rs │ ├── audio │ │ ├── components.rs │ │ ├── components │ │ │ └── sound_player.rs │ │ ├── handler.rs │ │ ├── engine.rs │ │ ├── mixer │ │ │ └── dummy.rs │ │ ├── sound.rs │ │ └── mixer.rs │ ├── logging.rs │ ├── rendering │ │ ├── shaders.rs │ │ ├── components.rs │ │ ├── components │ │ │ ├── camera.rs │ │ │ ├── sprite.rs │ │ │ ├── color_rect.rs │ │ │ ├── label.rs │ │ │ └── color_tri.rs │ │ ├── render_settings.rs │ │ └── shaders │ │ │ ├── textured_quad.wgsl │ │ │ └── textured_quad.rs │ ├── resources.rs │ ├── input │ │ ├── systems.rs │ │ ├── components.rs │ │ ├── button_state.rs │ │ ├── mouse_state.rs │ │ ├── touch_state.rs │ │ ├── components │ │ │ └── ui_button.rs │ │ └── systems │ │ │ └── ui_button.rs │ ├── core │ │ ├── components.rs │ │ ├── game.rs │ │ ├── game_settings.rs │ │ ├── components │ │ │ └── transform.rs │ │ └── error.rs │ ├── profiling.rs │ ├── audio.rs │ ├── rendering.rs │ ├── assets.rs │ ├── profiling │ │ ├── profile_settings.rs │ │ ├── profiler.rs │ │ └── profile_cache.rs │ ├── assets │ │ ├── writer.rs │ │ └── asset_key.rs │ ├── colors.rs │ ├── logging │ │ └── engine.rs │ ├── input.rs │ └── types.rs ├── .gitignore ├── clippy.toml ├── examples │ ├── assets │ │ ├── bunny.png │ │ ├── smiley.png │ │ ├── tileset.png │ │ ├── test_music.wav │ │ ├── test_sound.wav │ │ ├── Roboto-Light.ttf │ │ ├── cursor-sheet.png │ │ ├── smiley.aseprite │ │ ├── button_pressed.png │ │ ├── hotreload_bunny.png │ │ ├── button_unpressed.png │ │ ├── sample_prefab.json │ │ ├── bunny.ent │ │ ├── example.wrld │ │ └── smiley.json │ ├── logging.rs │ ├── user_data.rs │ ├── hotreload.rs │ ├── load_world_file.rs │ ├── window_manipulation.rs │ ├── profiler.rs │ ├── tilemap.rs │ ├── buttons.rs │ ├── input_actions.rs │ ├── aseprite.rs │ ├── touch.rs │ ├── labels.rs │ ├── gamepads.rs │ ├── shapes.rs │ ├── world_merging.rs │ ├── camera.rs │ ├── mouse.rs │ ├── sprites.rs │ ├── render_to_texture.rs │ ├── audio.rs │ ├── ent.rs │ ├── physics_groups.rs │ ├── raycast.rs │ ├── bunnymark.rs │ └── physics_joints.rs ├── Cargo.toml └── README.md ├── .gitignore ├── editor ├── src │ ├── main.rs │ └── app.rs └── Cargo.toml ├── assets ├── banner_large.png ├── aseprite_settings.png ├── html5.svg ├── windows.svg ├── webassembly.svg ├── android.svg └── apple.svg ├── Cargo.toml ├── docs ├── src │ ├── README.md │ ├── aseprite.md │ ├── SUMMARY.md │ ├── audio.md │ ├── worlds.md │ ├── rendering.md │ ├── worlds │ │ ├── queries.md │ │ └── spawning.md │ ├── portability.md │ └── input.md └── book.toml ├── LICENSE ├── .github └── workflows │ └── rust.yml └── README.md /emerald/src/world/physics/components.rs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /emerald/src/audio/components.rs: -------------------------------------------------------------------------------- 1 | pub mod sound_player; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | /target 3 | Cargo.lock 4 | emerald.log 5 | .idea -------------------------------------------------------------------------------- /emerald/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | emerald.log 4 | .idea -------------------------------------------------------------------------------- /emerald/src/logging.rs: -------------------------------------------------------------------------------- 1 | mod engine; 2 | 3 | pub use engine::*; 4 | -------------------------------------------------------------------------------- /emerald/src/rendering/shaders.rs: -------------------------------------------------------------------------------- 1 | pub(crate) mod textured_quad; 2 | -------------------------------------------------------------------------------- /emerald/src/resources.rs: -------------------------------------------------------------------------------- 1 | pub type Resources = anymap::AnyMap; 2 | -------------------------------------------------------------------------------- /emerald/src/input/systems.rs: -------------------------------------------------------------------------------- 1 | mod ui_button; 2 | pub use ui_button::*; 3 | -------------------------------------------------------------------------------- /editor/src/main.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("Hello, world!"); 3 | } 4 | -------------------------------------------------------------------------------- /emerald/src/input/components.rs: -------------------------------------------------------------------------------- 1 | mod ui_button; 2 | pub use ui_button::*; 3 | -------------------------------------------------------------------------------- /emerald/clippy.toml: -------------------------------------------------------------------------------- 1 | too-many-arguments-threshold = 12 2 | type-complexity-threshold = 350 3 | -------------------------------------------------------------------------------- /assets/banner_large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/assets/banner_large.png -------------------------------------------------------------------------------- /emerald/src/core/components.rs: -------------------------------------------------------------------------------- 1 | pub mod autotilemap; 2 | pub mod tilemap; 3 | pub mod transform; 4 | -------------------------------------------------------------------------------- /emerald/src/profiling.rs: -------------------------------------------------------------------------------- 1 | pub mod profile_cache; 2 | pub mod profile_settings; 3 | pub mod profiler; 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "emerald", 4 | "editor", 5 | ] 6 | resolver = "2" 7 | -------------------------------------------------------------------------------- /assets/aseprite_settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/assets/aseprite_settings.png -------------------------------------------------------------------------------- /emerald/examples/assets/bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/bunny.png -------------------------------------------------------------------------------- /emerald/examples/assets/smiley.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/smiley.png -------------------------------------------------------------------------------- /emerald/examples/assets/tileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/tileset.png -------------------------------------------------------------------------------- /emerald/examples/assets/test_music.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/test_music.wav -------------------------------------------------------------------------------- /emerald/examples/assets/test_sound.wav: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/test_sound.wav -------------------------------------------------------------------------------- /emerald/examples/assets/Roboto-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/Roboto-Light.ttf -------------------------------------------------------------------------------- /emerald/examples/assets/cursor-sheet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/cursor-sheet.png -------------------------------------------------------------------------------- /emerald/examples/assets/smiley.aseprite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/smiley.aseprite -------------------------------------------------------------------------------- /emerald/examples/assets/button_pressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/button_pressed.png -------------------------------------------------------------------------------- /emerald/examples/assets/hotreload_bunny.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/hotreload_bunny.png -------------------------------------------------------------------------------- /emerald/examples/assets/button_unpressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Bombfuse/emerald/HEAD/emerald/examples/assets/button_unpressed.png -------------------------------------------------------------------------------- /editor/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "editor" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | emerald = { path = "../emerald" } -------------------------------------------------------------------------------- /docs/src/README.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | Welcome to the Emerald Engine! Emerald is a free, open-source, 2D game engine that focuses on portability and usability. 4 | 5 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["Clay"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Emerald" 7 | 8 | [output.html.playground] 9 | runnable = false 10 | -------------------------------------------------------------------------------- /editor/src/app.rs: -------------------------------------------------------------------------------- 1 | use emerald::Game; 2 | 3 | pub struct EmeraldEditor {} 4 | impl EmeraldEditor { 5 | pub fn new() -> Self { 6 | Self {} 7 | } 8 | } 9 | impl Game for EmeraldEditor {} 10 | -------------------------------------------------------------------------------- /emerald/src/audio.rs: -------------------------------------------------------------------------------- 1 | pub mod components; 2 | mod engine; 3 | mod handler; 4 | mod mixer; 5 | mod sound; 6 | 7 | pub(crate) use engine::*; 8 | pub use handler::*; 9 | pub use mixer::*; 10 | pub use sound::*; 11 | -------------------------------------------------------------------------------- /emerald/src/rendering.rs: -------------------------------------------------------------------------------- 1 | pub mod components; 2 | pub mod font; 3 | pub mod render_settings; 4 | pub(crate) mod rendering_engine; 5 | pub(crate) mod rendering_handler; 6 | pub(crate) mod shaders; 7 | pub mod texture; 8 | -------------------------------------------------------------------------------- /emerald/src/assets.rs: -------------------------------------------------------------------------------- 1 | mod asset_engine; 2 | mod asset_loader; 3 | mod writer; 4 | 5 | pub mod asset_key; 6 | pub mod asset_storage; 7 | 8 | pub(crate) use asset_engine::*; 9 | pub use asset_loader::*; 10 | pub use writer::*; 11 | -------------------------------------------------------------------------------- /docs/src/aseprite.md: -------------------------------------------------------------------------------- 1 | 2 | ## Aseprite 3 | [Aseprite](https://www.aseprite.org/) is an open-source pixel art animation tool. 4 | Many people use Aseprite and providing easy access to 5 | rendering aseprites in the engine increases usability. 6 | -------------------------------------------------------------------------------- /emerald/examples/assets/sample_prefab.json: -------------------------------------------------------------------------------- 1 | { 2 | "entities": [ 3 | [ 4 | { "Position": { "x": 80, "y": 80 } }, 5 | { "Sprite": { "path": "./examples/assets/bunny.png", "offset": { "x": 0, "y": 0 } } } 6 | ] 7 | ] 8 | } -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | [Introduction](README.md) 3 | - [Worlds](worlds.md) 4 | - [Spawning](worlds/spawning.md) 5 | - [Queries](worlds/queries.md) 6 | - [Input](input.md) 7 | - [Audio](audio.md) 8 | - [Rendering](rendering.md) 9 | - [Portability](portability.md) -------------------------------------------------------------------------------- /docs/src/audio.md: -------------------------------------------------------------------------------- 1 | # Audio 2 | Emerald supports .ogg and .wav audio files. 3 | 4 | 5 | ## Mixers 6 | Mixers are basically sound containers, we'll play sounds using these. 7 | 8 | ```rust 9 | let sound = emd.loader().sound("my_sound.ogg").unwrap(); 10 | emd.audio().mixer("sfx").unwrap().play(sound).unwrap(); 11 | ``` -------------------------------------------------------------------------------- /emerald/src/core/game.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | pub trait Game { 4 | fn initialize(&mut self, mut _emd: Emerald) {} 5 | fn update(&mut self, _emd: Emerald) {} 6 | fn draw(&mut self, mut emd: Emerald) { 7 | emd.graphics().begin().unwrap(); 8 | emd.graphics().render().unwrap(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /assets/html5.svg: -------------------------------------------------------------------------------- 1 | HTML5 icon -------------------------------------------------------------------------------- /emerald/src/world/physics.rs: -------------------------------------------------------------------------------- 1 | mod components; 2 | mod physics_engine; 3 | mod physics_handler; 4 | mod types; 5 | 6 | pub use components::*; 7 | pub use physics_engine::*; 8 | pub use physics_handler::*; 9 | pub use types::*; 10 | 11 | pub use rapier2d::prelude::{ActiveCollisionTypes, Group, InteractionGroups, QueryFilterFlags}; 12 | -------------------------------------------------------------------------------- /emerald/examples/logging.rs: -------------------------------------------------------------------------------- 1 | use emerald::*; 2 | 3 | pub fn main() { 4 | emerald::start(Box::new(LoggingExample {}), GameSettings::default()) 5 | } 6 | 7 | pub struct LoggingExample; 8 | impl Game for LoggingExample { 9 | fn update(&mut self, mut emd: Emerald) { 10 | emd.logger().info("hey we're logging in emerald").unwrap(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /emerald/src/rendering/components.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "aseprite")] 2 | pub mod aseprite; 3 | 4 | mod camera; 5 | mod color_rect; 6 | mod color_tri; 7 | mod label; 8 | mod sprite; 9 | 10 | #[cfg(feature = "aseprite")] 11 | pub use aseprite::*; 12 | 13 | pub use camera::*; 14 | pub use color_rect::*; 15 | pub use color_tri::*; 16 | pub use label::*; 17 | pub use sprite::*; 18 | -------------------------------------------------------------------------------- /docs/src/worlds.md: -------------------------------------------------------------------------------- 1 | # Worlds 2 | 3 | ### What is a World? 4 | A world is Emerald's equivalent to a scene tree. 5 | It's a space in which game entities live and act. 6 | 7 | A world does not contain a scene tree or hierarchy, 8 | as a flat world architecture lends 9 | itself very well to Rust's borrow checker. 10 | 11 | A world (optionally) has physics that the games entities can interact with. -------------------------------------------------------------------------------- /emerald/src/profiling/profile_settings.rs: -------------------------------------------------------------------------------- 1 | pub struct ProfileSettings { 2 | /// Number of frames to keep in memory for each profile, before dropping 3 | /// the oldest frame. 4 | pub frame_limit: usize, 5 | } 6 | impl Default for ProfileSettings { 7 | fn default() -> Self { 8 | Self { 9 | // Approximately 10 seconds worth of frame data. 10 | frame_limit: 600, 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /emerald/src/rendering/components/camera.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Clone, Copy, Debug)] 4 | pub struct Camera { 5 | pub offset: Vector2, 6 | pub zoom: f32, 7 | pub(crate) is_active: bool, 8 | } 9 | impl Default for Camera { 10 | fn default() -> Camera { 11 | Camera { 12 | offset: Vector2::new(0.0, 0.0), 13 | zoom: 1.0, 14 | is_active: false, 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /emerald/src/core/game_settings.rs: -------------------------------------------------------------------------------- 1 | use crate::render_settings::RenderSettings; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct GameSettings { 5 | pub title: String, 6 | pub render_settings: RenderSettings, 7 | } 8 | impl Default for GameSettings { 9 | fn default() -> GameSettings { 10 | GameSettings { 11 | title: String::from("Emerald"), 12 | render_settings: RenderSettings::default(), 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /docs/src/rendering.md: -------------------------------------------------------------------------------- 1 | 2 | ## Rendering 3 | Simple world rendering, with the option of more custom 4 | rendering via the graphics api. 5 | ```rust 6 | emd.graphics().begin(); 7 | emd.graphics().draw_world(&mut self.world); 8 | emd.graphics().render(); 9 | ``` 10 | 11 | It's also possible to draw visual components directly in relation to the 12 | screen. 13 | 14 | ```rust 15 | let sprite = emd.loader().sprite("my_sprite.png").unwrap(); 16 | emd.graphics().draw_sprite(&sprite, &Transform::default()).unwrap(); 17 | ``` -------------------------------------------------------------------------------- /emerald/src/input/button_state.rs: -------------------------------------------------------------------------------- 1 | #[derive(Copy, Clone, Debug, Default)] 2 | pub struct ButtonState { 3 | pub is_pressed: bool, 4 | pub was_pressed: bool, 5 | } 6 | impl ButtonState { 7 | pub fn new() -> Self { 8 | ButtonState::default() 9 | } 10 | 11 | #[inline] 12 | pub(crate) fn rollover(&mut self) { 13 | self.was_pressed = self.is_pressed; 14 | } 15 | 16 | #[inline] 17 | pub fn is_just_pressed(&self) -> bool { 18 | !self.was_pressed && self.is_pressed 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /emerald/src/input/mouse_state.rs: -------------------------------------------------------------------------------- 1 | use crate::{transform::Translation, ButtonState}; 2 | 3 | /// State of mouse over last few frames 4 | #[derive(Clone, Copy, Debug, Default)] 5 | pub struct MouseState { 6 | pub translation: Translation, 7 | pub left: ButtonState, 8 | pub middle: ButtonState, 9 | pub right: ButtonState, 10 | } 11 | 12 | impl MouseState { 13 | pub fn new() -> Self { 14 | MouseState::default() 15 | } 16 | 17 | pub(crate) fn rollover(&mut self) { 18 | self.left.rollover(); 19 | self.middle.rollover(); 20 | self.right.rollover(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /emerald/src/assets/writer.rs: -------------------------------------------------------------------------------- 1 | use crate::EmeraldError; 2 | use std::fs::File; 3 | use std::io::prelude::Write; 4 | 5 | #[derive(Clone)] 6 | pub struct Writer { 7 | user_directory: String, 8 | } 9 | impl Writer { 10 | pub(crate) fn new(user_directory: String) -> Self { 11 | Writer { user_directory } 12 | } 13 | 14 | pub fn write_to_user_file>( 15 | &mut self, 16 | bytes: &[u8], 17 | relative_path: T, 18 | ) -> Result<(), EmeraldError> { 19 | let path = self.user_directory.clone() + &relative_path.into(); 20 | let mut file = File::create(path)?; 21 | file.write_all(bytes)?; 22 | Ok(()) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/worlds/queries.md: -------------------------------------------------------------------------------- 1 | # Queries 2 | 3 | ### What is a query? 4 | A query is something that operates on the entities of the World. 5 | Entites are just bundles of data, and we use queries to access that data. 6 | 7 | 8 | Let's write a query that moves the player! 9 | 10 | ```rust 11 | let is_jog_pressed = emd.input().is_key_pressed(KeyCode::D); 12 | 13 | // A query that moves our player to the right if they're in jogging state 14 | for (entity_id, (transform, aseprite, player_data)) in world.query::<(&mut Transform, &mut Aseprite, &mut MyPlayerData)>().iter() { 15 | player.is_jogging = is_jog_pressed; 16 | 17 | if player.is_jogging { 18 | transform.translation.x += 10.0; 19 | } 20 | } 21 | ``` 22 | 23 | -------------------------------------------------------------------------------- /docs/src/worlds/spawning.md: -------------------------------------------------------------------------------- 1 | # Spawning 2 | 3 | If worlds are a space for entities to act, then lets spawn some entities! 4 | 5 | Let's start by spawning our player. 6 | 7 | Our player will need a Transform (its location/stretch/rotation data). 8 | Let's give it a visual component (ex. ColorRect) and a custom player 9 | component as well. 10 | 11 | ```rust 12 | use emerald::{World, Transform, ColorRect}; 13 | 14 | // Lets spawn our player, visualized by a rectangle! 15 | struct MyPlayerData { pub is_jogging: bool } 16 | 17 | let mut world = World::new(); 18 | world.spawn(( 19 | Transform::default(), 20 | ColorRect::default(), 21 | MyPlayerData { is_jogging: false } 22 | )); 23 | ``` 24 | 25 | Later on, we'll query this player and mutate its data! -------------------------------------------------------------------------------- /emerald/examples/assets/bunny.ent: -------------------------------------------------------------------------------- 1 | [sprite] 2 | texture = "bunny.png" 3 | offset = { x = 16.0, y = 200.0 } 4 | scale = { x = 2.0, y = 2.0 } 5 | 6 | [rigid_body] 7 | body_type = "dynamic" 8 | colliders = [{ shape = "cuboid", half_width = 16.0, half_height = 16.0, translation = { x = 16.0, y = 200.0 } }] 9 | 10 | [my_custom_player_component] 11 | name = "Bunny" 12 | max_hp = 50 13 | 14 | [aseprite] 15 | aseprite = "smiley.aseprite" 16 | offset = { x = 16.0, y = 160.0 } 17 | default_animation = { name = "smile", looping = true } 18 | 19 | [label] 20 | font = "Roboto-Light.ttf" 21 | text = "Blah Blah Emerald Engine" 22 | font_size = 48 23 | size = 32 24 | offset = { x = -48.0, y = 0.0 } 25 | horizontal_align = "left" 26 | vertical_align = "bottom" 27 | 28 | [color_rect] 29 | color = { r = 0, g = 0, b = 0, a = 255 } 30 | width = 12 31 | height = 12 -------------------------------------------------------------------------------- /emerald/examples/user_data.rs: -------------------------------------------------------------------------------- 1 | use emerald::*; 2 | 3 | pub fn main() { 4 | emerald::start(Box::new(UserDataExample {}), GameSettings::default()) 5 | } 6 | 7 | pub struct UserDataExample {} 8 | impl Game for UserDataExample { 9 | fn initialize(&mut self, mut emd: Emerald) { 10 | let my_json = r#" 11 | { 12 | "some_data": "this is some save file data for a game" 13 | } 14 | "#; 15 | 16 | // This will write the contents of `my_json` to a new file in the root user data directory. 17 | // This will overwrite any contents that previously existed at that location. 18 | emd.writer() 19 | .write_to_user_file(my_json.as_bytes(), "user_data_example.sav") 20 | .unwrap(); 21 | } 22 | 23 | fn update(&mut self, _emd: Emerald) {} 24 | } 25 | -------------------------------------------------------------------------------- /emerald/src/world/world_physics_loader.rs: -------------------------------------------------------------------------------- 1 | use rapier2d::na::Vector2; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{ent::Vec2f32Schema, AssetLoader, EmeraldError, World}; 5 | 6 | #[derive(Deserialize, Serialize)] 7 | struct WorldPhysicsSchema { 8 | pub gravity: Option, 9 | } 10 | 11 | pub(crate) fn load_world_physics<'a>( 12 | _loader: &mut AssetLoader<'a>, 13 | world: &mut World, 14 | toml: &toml::Value, 15 | ) -> Result<(), EmeraldError> { 16 | if !toml.is_table() { 17 | return Err(EmeraldError::new( 18 | "Cannot load world physics from a non-table toml value.", 19 | )); 20 | } 21 | 22 | let schema: WorldPhysicsSchema = toml::from_str(&toml.to_string())?; 23 | 24 | if let Some(gravity) = schema.gravity { 25 | world 26 | .physics() 27 | .set_gravity(Vector2::new(gravity.x, gravity.y)); 28 | } 29 | 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /docs/src/portability.md: -------------------------------------------------------------------------------- 1 | 2 | ## Portability Table 3 | | Platform | Graphics | Audio | Gamepad Input | User Data Writing (save files) | 4 | | :--- | :----: | :----: | ---: | ---: | 5 | | ![Windows](../../assets/windows.svg) | ☑ | ☑ | ☑ | ☑ | 6 | | ![Linux](../../assets/linux.svg) | ☑ | ☑ | ☑ | ☑ | 7 | | ![MacOS](../../assets/apple.svg) | ☑ | ☑ | ☑ |☑ | 8 | | ![Raspberry Pi (Debian)](../../assets/raspberrypi.svg) | ☑ | ☑ | ☑ | ☑ | 9 | | ![Web Browser](../../assets/webassembly.svg) | ☑ | ☑ | ☒ | ☒ | 10 | | ![Android](../../assets/android.svg) | ☑ | ☑ | ☒ | ☒ | 11 | | iOS | ☑ | ☑ | ☒ | ☒ | 12 | 13 | -------------------------------------------------------------------------------- /emerald/src/rendering/components/sprite.rs: -------------------------------------------------------------------------------- 1 | use crate::{texture::TextureKey, *}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct Sprite { 5 | // TODO: Make this an Option, where None means draw the entire texture 6 | pub target: Rectangle, 7 | pub rotation: f32, 8 | pub scale: Vector2, 9 | pub offset: Vector2, 10 | pub visible: bool, 11 | pub color: Color, 12 | pub centered: bool, 13 | pub(crate) texture_key: TextureKey, 14 | pub z_index: f32, 15 | } 16 | impl Sprite { 17 | pub fn from_texture(texture_key: TextureKey) -> Self { 18 | Sprite { 19 | texture_key, 20 | target: Rectangle::new(0.0, 0.0, 0.0, 0.0), 21 | rotation: 0.0, 22 | scale: Vector2::new(1.0, 1.0), 23 | offset: Vector2::new(0.0, 0.0), 24 | color: WHITE, 25 | centered: true, 26 | z_index: 0.0, 27 | visible: true, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /emerald/src/rendering/render_settings.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct RenderSettings { 5 | pub background_color: Color, 6 | pub fullscreen: bool, 7 | pub resolution: (u32, u32), 8 | pub high_dpi: bool, 9 | pub resizable_window: bool, 10 | 11 | pub present_mode: PresentMode, 12 | 13 | // Whether or not the game engine should automatically cull sprites that are not in camera view 14 | pub frustrum_culling: bool, 15 | pub pixel_snap: bool, 16 | } 17 | impl Default for RenderSettings { 18 | fn default() -> RenderSettings { 19 | RenderSettings { 20 | background_color: CORNFLOWER_BLUE, 21 | fullscreen: false, 22 | resolution: (800, 600), 23 | high_dpi: false, 24 | present_mode: wgpu::PresentMode::AutoVsync, 25 | resizable_window: true, 26 | frustrum_culling: true, 27 | pixel_snap: true, 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /emerald/src/rendering/components/color_rect.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, Copy, Clone)] 4 | pub struct ColorRect { 5 | pub color: Color, 6 | pub offset: Vector2, 7 | pub visible: bool, 8 | pub width: u32, 9 | pub height: u32, 10 | pub centered: bool, 11 | pub rotation: f32, 12 | pub z_index: f32, 13 | } 14 | impl ColorRect { 15 | pub fn new(color: Color, width: u32, height: u32) -> Self { 16 | ColorRect { 17 | color, 18 | width, 19 | height, 20 | ..Default::default() 21 | } 22 | } 23 | } 24 | impl Default for ColorRect { 25 | fn default() -> ColorRect { 26 | ColorRect { 27 | color: WHITE, 28 | offset: Vector2::new(0.0, 0.0), 29 | visible: true, 30 | width: 32, 31 | height: 32, 32 | centered: true, 33 | rotation: 0.0, 34 | z_index: 0.0, 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /docs/src/input.md: -------------------------------------------------------------------------------- 1 | # Input 2 | 3 | There are a few different ways to get input in Emerald. 4 | 5 | ## Keyboard 6 | Access key states directly 7 | ```rust 8 | if emd.input().is_key_just_pressed(KeyCode::Space) { 9 | // player jump 10 | } 11 | ``` 12 | 13 | ## Buttons 14 | Access the buttons on a gamepad directly. 15 | ```rust 16 | if emd.input().is_button_just_pressed(Button::South) { 17 | // player jump 18 | } 19 | ``` 20 | 21 | ## Joysticks 22 | Access the joysticks of a gamepad directly. 23 | ```rust 24 | let joystick_value = emd.input().joystick(Joystick::Left); 25 | // Now we can use this value to move the player in the joysticks direction 26 | ``` 27 | 28 | ## Actions 29 | Access the states of action bindings. 30 | ```rust 31 | emd.input().add_action_binding_button("jump".to_string(), Button::South); 32 | emd.input().add_action_binding_key("jump".to_string(), KeyCode::Space); 33 | if emd.input().is_action_just_pressed("jump".to_string()) { 34 | // player jump 35 | } 36 | ``` 37 | -------------------------------------------------------------------------------- /emerald/src/rendering/shaders/textured_quad.wgsl: -------------------------------------------------------------------------------- 1 | struct VertexInput { 2 | @location(0) position: vec2, 3 | @location(1) tex_coords: vec2, 4 | @location(2) color: vec4, 5 | } 6 | 7 | struct VertexOutput { 8 | @builtin(position) clip_position: vec4, 9 | @location(0) tex_coords: vec2, 10 | @location(1) color_tint: vec4, 11 | } 12 | 13 | @vertex 14 | fn vs_main( 15 | in: VertexInput, 16 | ) -> VertexOutput { 17 | var out: VertexOutput; 18 | out.clip_position = vec4(in.position, 0.0, 1.0); 19 | out.tex_coords = in.tex_coords; 20 | out.color_tint = in.color; 21 | return out; 22 | } 23 | 24 | // Fragment shader 25 | @group(0) @binding(0) 26 | var t_diffuse: texture_2d; 27 | @group(0) @binding(1) 28 | var s_diffuse: sampler; 29 | 30 | @fragment 31 | fn fs_main(in: VertexOutput) -> @location(0) vec4 { 32 | var color = textureSample(t_diffuse, s_diffuse, in.tex_coords); 33 | return color * in.color_tint; 34 | } 35 | -------------------------------------------------------------------------------- /emerald/examples/hotreload.rs: -------------------------------------------------------------------------------- 1 | use emerald::*; 2 | 3 | pub fn main() { 4 | emerald::start( 5 | Box::new(HotreloadExample { 6 | world: World::new(), 7 | }), 8 | GameSettings::default(), 9 | ) 10 | } 11 | 12 | pub struct HotreloadExample { 13 | world: World, 14 | } 15 | impl Game for HotreloadExample { 16 | fn initialize(&mut self, mut emd: Emerald) { 17 | emd.set_asset_folder_root(String::from("./examples/assets/")); 18 | let mut sprite = emd.loader().sprite("hotreload_bunny.png").unwrap(); 19 | sprite.scale.x = 5.0; 20 | sprite.scale.y = 5.0; 21 | self.world.spawn((sprite, Transform::default())); 22 | } 23 | 24 | fn update(&mut self, mut emd: Emerald) { 25 | emd.loader().hotreload(); 26 | } 27 | 28 | fn draw(&mut self, mut emd: Emerald) { 29 | emd.graphics().begin().unwrap(); 30 | emd.graphics().draw_world(&mut self.world).unwrap(); 31 | emd.graphics().render().unwrap(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /emerald/examples/load_world_file.rs: -------------------------------------------------------------------------------- 1 | use emerald::{rendering::components::aseprite_update_system, *}; 2 | 3 | pub fn main() { 4 | emerald::start( 5 | Box::new(WorldLoadingExample { 6 | world: World::new(), 7 | }), 8 | GameSettings::default(), 9 | ) 10 | } 11 | 12 | pub struct WorldLoadingExample { 13 | world: World, 14 | } 15 | impl Game for WorldLoadingExample { 16 | fn initialize(&mut self, mut emd: Emerald) { 17 | emd.set_asset_folder_root("./examples/assets/".to_string()); 18 | self.world = emd.loader().world("example.wrld").unwrap(); 19 | } 20 | 21 | fn update(&mut self, emd: Emerald) { 22 | let delta = emd.delta(); 23 | aseprite_update_system(&mut self.world, delta); 24 | self.world.physics().step(delta); 25 | } 26 | 27 | fn draw(&mut self, mut emd: Emerald) { 28 | emd.graphics().begin().unwrap(); 29 | emd.graphics().draw_world(&mut self.world).unwrap(); 30 | emd.graphics().render().unwrap(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /emerald/src/world/ent/ent_color_rect_loader.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{rendering::components::ColorRect, AssetLoader, Color, EmeraldError, World}; 5 | 6 | #[derive(Deserialize, Serialize)] 7 | pub(crate) struct EntColorRectSchema { 8 | pub color: Color, 9 | pub width: u32, 10 | pub height: u32, 11 | 12 | #[serde(default)] 13 | pub z_index: f32, 14 | } 15 | 16 | pub(crate) fn load_ent_color_rect<'a>( 17 | _loader: &mut AssetLoader<'a>, 18 | entity: Entity, 19 | world: &mut World, 20 | toml: &toml::Value, 21 | ) -> Result<(), EmeraldError> { 22 | if !toml.is_table() { 23 | return Err(EmeraldError::new( 24 | "Cannot load color_rect from a non-table toml value.", 25 | )); 26 | } 27 | 28 | let schema: EntColorRectSchema = toml::from_str(&toml.to_string())?; 29 | let mut color_rect = ColorRect::new(schema.color, schema.width, schema.height); 30 | color_rect.z_index = schema.z_index; 31 | world.insert_one(entity, color_rect)?; 32 | 33 | Ok(()) 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Bombfuse 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /emerald/src/audio/components/sound_player.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{Emerald, EmeraldError, SoundInstanceId, SoundKey}; 4 | 5 | /// Holds sound keys and can play them given Emerald. 6 | /// Mostly useful for preloading the sounds needed by Entities, or the World. 7 | pub struct SoundPlayer { 8 | keys: HashMap, 9 | mixer: String, 10 | } 11 | impl SoundPlayer { 12 | pub fn new>(mixer: T) -> Self { 13 | Self { 14 | mixer: mixer.into(), 15 | keys: HashMap::new(), 16 | } 17 | } 18 | 19 | pub fn add_sound(&mut self, label: &str, key: SoundKey) { 20 | self.keys.insert(label.to_string(), key); 21 | } 22 | 23 | pub fn play(&self, emd: &mut Emerald, label: &str) -> Result { 24 | if let Some(key) = self.keys.get(label) { 25 | let id = emd.audio().mixer(&self.mixer)?.play(key)?; 26 | return Ok(id); 27 | } 28 | 29 | Err(EmeraldError::new(format!( 30 | "{:?} is not loaded into the Sound Player", 31 | label 32 | ))) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /emerald/src/audio/handler.rs: -------------------------------------------------------------------------------- 1 | use crate::EmeraldError; 2 | use crate::{audio::*, AssetEngine}; 3 | 4 | pub struct AudioHandler<'a> { 5 | audio_engine: &'a mut AudioEngine, 6 | asset_store: &'a mut AssetEngine, 7 | } 8 | impl<'a> AudioHandler<'a> { 9 | pub(crate) fn new(audio_engine: &'a mut AudioEngine, asset_store: &'a mut AssetEngine) -> Self { 10 | AudioHandler { 11 | audio_engine, 12 | asset_store, 13 | } 14 | } 15 | 16 | pub fn mixer>( 17 | &mut self, 18 | mixer_name: T, 19 | ) -> Result, EmeraldError> { 20 | let mixer_name: String = mixer_name.into(); 21 | 22 | if let Ok(mixer) = self.audio_engine.mixer(mixer_name.clone()) { 23 | return Ok(MixerHandler::new(mixer, &mut self.asset_store)); 24 | } 25 | 26 | Err(EmeraldError::new(format!( 27 | "Unable to create and/or retrieve the mixer named: {}", 28 | mixer_name 29 | ))) 30 | } 31 | 32 | /// Deletes and clears all mixers 33 | pub fn clear(&mut self) -> Result<(), EmeraldError> { 34 | self.audio_engine.clear()?; 35 | 36 | Ok(()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /emerald/src/profiling/profiler.rs: -------------------------------------------------------------------------------- 1 | use crate::EmeraldError; 2 | 3 | use super::profile_cache::ProfileCache; 4 | 5 | pub type ProfileFloat = f64; 6 | 7 | pub type ProfileName = String; 8 | 9 | /// Captures the current time at instantiation, profiler should only be used inline. 10 | /// Ex. 11 | /// emd.profiler().start_frame("game_loop").unwrap(); 12 | /// do_my_game_logic(); 13 | /// emd.profiler().finish_frame("game_loop").unwrap(); 14 | pub struct Profiler<'c> { 15 | profile_cache: &'c mut ProfileCache, 16 | profile_name: ProfileName, 17 | now: ProfileFloat, 18 | } 19 | impl<'c> Profiler<'c> { 20 | pub(crate) fn new>( 21 | profile_cache: &'c mut ProfileCache, 22 | profile_name: T, 23 | now: ProfileFloat, 24 | ) -> Self { 25 | Self { 26 | profile_cache, 27 | profile_name: profile_name.into(), 28 | now, 29 | } 30 | } 31 | 32 | pub fn start_frame(&mut self) -> Result<(), EmeraldError> { 33 | self.profile_cache 34 | .start_frame(self.profile_name.clone(), self.now) 35 | } 36 | 37 | pub fn finish_frame(&mut self) -> Result { 38 | self.profile_cache 39 | .finish_frame(self.profile_name.clone(), self.now) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /emerald/examples/window_manipulation.rs: -------------------------------------------------------------------------------- 1 | use emerald::*; 2 | 3 | pub fn main() { 4 | emerald::start( 5 | Box::new(WindowManipulationExample {}), 6 | GameSettings::default(), 7 | ) 8 | } 9 | 10 | pub struct WindowManipulationExample {} 11 | impl Game for WindowManipulationExample { 12 | fn initialize(&mut self, mut emd: Emerald) { 13 | emd.graphics().set_fullscreen(true).unwrap(); 14 | } 15 | 16 | fn update(&mut self, mut emd: Emerald) { 17 | if emd.input().is_key_just_pressed(KeyCode::A) { 18 | emd.graphics().set_window_size(600, 600).unwrap(); 19 | } 20 | if emd.input().is_key_just_pressed(KeyCode::D) { 21 | emd.graphics().set_window_size(1200, 1200).unwrap(); 22 | } 23 | if emd.input().is_key_just_pressed(KeyCode::Escape) { 24 | emd.graphics().set_fullscreen(false).unwrap(); 25 | } 26 | if emd.input().is_key_just_pressed(KeyCode::Enter) { 27 | emd.graphics().set_fullscreen(true).unwrap(); 28 | } 29 | 30 | if emd.input().mouse().left.is_just_pressed() { 31 | emd.set_cursor(CursorIcon::Progress); 32 | } else if emd.input().mouse().right.is_just_pressed() { 33 | emd.set_cursor(CursorIcon::Default); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /emerald/src/input/touch_state.rs: -------------------------------------------------------------------------------- 1 | use crate::{transform::Translation, TouchPhase}; 2 | 3 | #[derive(Debug, Clone, Copy)] 4 | pub struct TouchState { 5 | pub translation: Translation, 6 | pub previous: TouchPhase, 7 | pub phase: TouchPhase, 8 | } 9 | 10 | impl Default for TouchState { 11 | fn default() -> Self { 12 | TouchState { 13 | translation: Translation::default(), 14 | previous: TouchPhase::Cancelled, 15 | phase: TouchPhase::Cancelled, 16 | } 17 | } 18 | } 19 | 20 | impl TouchState { 21 | #[inline] 22 | pub fn was_pressed(&self) -> bool { 23 | self.previous == TouchPhase::Started || self.previous == TouchPhase::Moved 24 | } 25 | 26 | #[inline] 27 | pub fn is_pressed(&self) -> bool { 28 | self.phase == TouchPhase::Started || self.phase == TouchPhase::Moved 29 | } 30 | 31 | #[inline] 32 | pub fn is_just_pressed(&self) -> bool { 33 | !self.was_pressed() && self.is_pressed() 34 | } 35 | 36 | #[inline] 37 | pub fn is_just_released(&self) -> bool { 38 | self.was_pressed() && !self.is_pressed() 39 | } 40 | 41 | pub(crate) fn is_outdated(&self) -> bool { 42 | !self.was_pressed() && !self.is_pressed() 43 | } 44 | 45 | pub(crate) fn rollover(&mut self) { 46 | self.previous = self.phase; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /emerald/src/rendering/components/label.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | use crate::{font::FontKey, rendering::*}; 3 | 4 | pub use fontdue::layout::{HorizontalAlign, VerticalAlign, WrapStyle}; 5 | 6 | #[derive(Clone)] 7 | pub struct Label { 8 | pub text: String, 9 | pub offset: Vector2, 10 | pub scale: f32, 11 | pub font_key: FontKey, 12 | pub font_size: u16, 13 | pub z_index: f32, 14 | pub color: Color, 15 | pub centered: bool, 16 | pub visible: bool, 17 | pub visible_characters: i64, 18 | 19 | pub horizontal_align: HorizontalAlign, 20 | pub vertical_align: VerticalAlign, 21 | pub wrap_style: WrapStyle, 22 | pub max_height: Option, 23 | pub max_width: Option, 24 | } 25 | impl Label { 26 | pub fn new>(text: T, font_key: FontKey, font_size: u16) -> Self { 27 | Label { 28 | font_key, 29 | text: text.into(), 30 | font_size, 31 | offset: Vector2::new(0.0, 0.0), 32 | scale: 1.0, 33 | z_index: 0.0, 34 | centered: true, 35 | color: WHITE, 36 | visible: true, 37 | visible_characters: -1, 38 | horizontal_align: HorizontalAlign::Center, 39 | vertical_align: VerticalAlign::Middle, 40 | wrap_style: WrapStyle::Word, 41 | max_height: None, 42 | max_width: Some(300.0), 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /emerald/src/world/ent/ent_sound_player_loader.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{audio::components::sound_player::SoundPlayer, AssetLoader, EmeraldError, World}; 5 | 6 | use super::Vec2f32Schema; 7 | 8 | #[derive(Deserialize, Serialize)] 9 | pub(crate) struct EntSoundSchema { 10 | /// Label for the sound key 11 | pub label: String, 12 | 13 | /// Path to the sound file 14 | pub sound: String, 15 | } 16 | 17 | #[derive(Deserialize, Serialize)] 18 | pub(crate) struct EntSoundPlayerSchema { 19 | sounds: Vec, 20 | mixer: String, 21 | } 22 | 23 | pub const SOUND_PLAYER_SCHEMA_KEY: &str = "sound_player"; 24 | 25 | pub(crate) fn load_ent_sound_player<'a>( 26 | loader: &mut AssetLoader<'a>, 27 | entity: Entity, 28 | world: &mut World, 29 | toml: &toml::Value, 30 | ) -> Result<(), EmeraldError> { 31 | if !toml.is_table() { 32 | return Err(EmeraldError::new( 33 | "Cannot load sprite from a non-table toml value.", 34 | )); 35 | } 36 | 37 | let schema: EntSoundPlayerSchema = toml::from_str(&toml.to_string())?; 38 | let mut sound_player = SoundPlayer::new(schema.mixer); 39 | 40 | for sound_schema in schema.sounds { 41 | let key = loader.sound(sound_schema.sound)?; 42 | sound_player.add_sound(&sound_schema.label, key); 43 | } 44 | 45 | world.insert_one(entity, sound_player)?; 46 | 47 | Ok(()) 48 | } 49 | -------------------------------------------------------------------------------- /emerald/examples/assets/example.wrld: -------------------------------------------------------------------------------- 1 | [physics] 2 | gravity = { x = 0.0, y = -9.8 } 3 | 4 | [my_custom_resource] 5 | some_custom_data = 10 6 | 7 | [[entities]] 8 | path = "bunny.ent" 9 | transform = { translation = { x = 0.0, y = 0.0 } } 10 | 11 | [[entities]] 12 | transform = { translation = { x = -100.0, y = 0.0 } } 13 | aseprite = { aseprite = "smiley.aseprite", default_animation = { name = "smile", looping = true } } 14 | physics_body = { body_type = "dynamic", colliders = [{ shape = "cuboid", half_width = 4.0, half_height = 4.0 }] } 15 | 16 | [[entities]] 17 | transform = { translation = { x = 0.0, y = 0.0 } } 18 | [entities.tilemap] 19 | width = 10 20 | height = 10 21 | tile_height_px = 16 22 | tile_width_px = 16 23 | tileset_width = 2 24 | tileset_height = 2 25 | tileset = "tileset.png" 26 | [[entities.tilemap.tiles]] 27 | x = 0 28 | y = 0 29 | id = 0 30 | [[entities.tilemap.tiles]] 31 | x = 1 32 | y = 1 33 | id = 1 34 | [[entities.tilemap.tiles]] 35 | x = 1 36 | y = 0 37 | id = 2 38 | [[entities.tilemap.tiles]] 39 | x = 0 40 | y = 1 41 | id = 3 42 | 43 | [[entities]] 44 | transform = { translation = { x = 0.0, y = -50.0 } } 45 | [entities.autotilemap] 46 | width = 10 47 | height = 10 48 | tile_height_px = 16 49 | tile_width_px = 16 50 | tileset_width = 2 51 | tileset_height = 2 52 | tileset = "tileset.png" 53 | [[entities.autotilemap.rulesets]] 54 | x = 1 55 | y = 1 56 | [[entities.autotilemap.tiles]] 57 | x = 5 58 | y = 5 59 | [[entities.autotilemap.tiles]] 60 | x = 1 61 | y = 1 62 | -------------------------------------------------------------------------------- /emerald/src/audio/engine.rs: -------------------------------------------------------------------------------- 1 | use crate::{audio::*, EmeraldError}; 2 | use std::collections::HashMap; 3 | 4 | pub(crate) struct AudioEngine { 5 | mixers: HashMap, 6 | } 7 | impl AudioEngine { 8 | pub(crate) fn new() -> Self { 9 | AudioEngine { 10 | mixers: HashMap::new(), 11 | } 12 | } 13 | 14 | pub(crate) fn mixer>( 15 | &mut self, 16 | mixer_name: T, 17 | ) -> Result<&mut ThreadSafeMixer, EmeraldError> { 18 | let mixer_name: String = mixer_name.into(); 19 | 20 | if !self.mixers.contains_key(&mixer_name) { 21 | self.mixers 22 | .insert(mixer_name.clone(), crate::audio::mixer::new_mixer()?); 23 | } 24 | 25 | if let Some(mixer) = self.mixers.get_mut(&mixer_name) { 26 | return Ok(mixer); 27 | } 28 | 29 | Err(EmeraldError::new(format!( 30 | "Error creating and/or retrieving the mixer: {:?}", 31 | mixer_name 32 | ))) 33 | } 34 | 35 | pub(crate) fn post_update(&mut self) -> Result<(), EmeraldError> { 36 | for mixer in self.mixers.values_mut() { 37 | mixer.post_update()?; 38 | } 39 | 40 | Ok(()) 41 | } 42 | 43 | pub(crate) fn clear(&mut self) -> Result<(), EmeraldError> { 44 | for mixer in self.mixers.values_mut() { 45 | mixer.clear()?; 46 | } 47 | 48 | self.mixers = HashMap::new(); 49 | Ok(()) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /emerald/examples/assets/smiley.json: -------------------------------------------------------------------------------- 1 | { "frames": [ 2 | { 3 | "filename": "Sprite-0001_0.", 4 | "frame": { "x": 0, "y": 0, "w": 32, "h": 32 }, 5 | "rotated": false, 6 | "trimmed": false, 7 | "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 }, 8 | "sourceSize": { "w": 32, "h": 32 }, 9 | "duration": 100 10 | }, 11 | { 12 | "filename": "Sprite-0001_1.", 13 | "frame": { "x": 32, "y": 0, "w": 32, "h": 32 }, 14 | "rotated": false, 15 | "trimmed": false, 16 | "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 }, 17 | "sourceSize": { "w": 32, "h": 32 }, 18 | "duration": 100 19 | }, 20 | { 21 | "filename": "Sprite-0001_2.", 22 | "frame": { "x": 0, "y": 32, "w": 32, "h": 32 }, 23 | "rotated": false, 24 | "trimmed": false, 25 | "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 }, 26 | "sourceSize": { "w": 32, "h": 32 }, 27 | "duration": 100 28 | }, 29 | { 30 | "filename": "Sprite-0001_3.", 31 | "frame": { "x": 32, "y": 32, "w": 32, "h": 32 }, 32 | "rotated": false, 33 | "trimmed": false, 34 | "spriteSourceSize": { "x": 0, "y": 0, "w": 32, "h": 32 }, 35 | "sourceSize": { "w": 32, "h": 32 }, 36 | "duration": 100 37 | } 38 | ], 39 | "meta": { 40 | "app": "http://www.aseprite.org/", 41 | "version": "1.2.21-x64", 42 | "image": "smiley.png", 43 | "format": "RGBA8888", 44 | "size": { "w": 64, "h": 64 }, 45 | "scale": "1", 46 | "frameTags": [ 47 | { "name": "smile", "from": 0, "to": 3, "direction": "forward" } 48 | ] 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /emerald/examples/profiler.rs: -------------------------------------------------------------------------------- 1 | use emerald::*; 2 | 3 | pub fn main() { 4 | emerald::start( 5 | Box::new(ProfilingExample { 6 | world: World::new(), 7 | }), 8 | GameSettings::default(), 9 | ) 10 | } 11 | 12 | pub struct ProfilingExample { 13 | world: World, 14 | } 15 | impl Game for ProfilingExample { 16 | fn initialize(&mut self, mut emd: Emerald) { 17 | let profile_initialize = "initialize"; 18 | emd.profiler(profile_initialize).start_frame().unwrap(); 19 | 20 | emd.set_asset_folder_root(String::from("./examples/assets/")); 21 | let sprite = emd.loader().sprite("bunny.png").unwrap(); 22 | self.world.spawn((sprite, Transform::default())); 23 | 24 | let initialize_time = emd.profiler(profile_initialize).finish_frame().unwrap(); 25 | println!("initialize time: {}", initialize_time); 26 | } 27 | 28 | fn update(&mut self, mut emd: Emerald) { 29 | emd.profiler("update").start_frame().unwrap(); 30 | 31 | let update_time = emd.profiler("update").finish_frame().unwrap(); 32 | println!("update time: {}", update_time); 33 | } 34 | 35 | fn draw(&mut self, mut emd: Emerald) { 36 | emd.profiler("draw").start_frame().unwrap(); 37 | 38 | emd.graphics().begin().unwrap(); 39 | emd.graphics().draw_world(&mut self.world).unwrap(); 40 | emd.graphics().render().unwrap(); 41 | 42 | let draw_time = emd.profiler("draw").finish_frame().unwrap(); 43 | println!("draw time: {}", draw_time); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /emerald/src/world/ent/ent_transform_loader.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{AssetLoader, EmeraldError, Scale, Transform, Translation, World}; 5 | 6 | use super::Vec2f32Schema; 7 | 8 | #[derive(Deserialize, Serialize)] 9 | pub(crate) struct EntTransformSchema { 10 | pub translation: Option, 11 | pub rotation: Option, 12 | pub scale: Option, 13 | } 14 | 15 | pub(crate) fn load_ent_transform<'a>( 16 | _loader: &mut AssetLoader<'a>, 17 | entity: Entity, 18 | world: &mut World, 19 | toml: &toml::Value, 20 | ) -> Result<(), EmeraldError> { 21 | if !toml.is_table() { 22 | return Err(EmeraldError::new( 23 | "Cannot load transform from a non-table toml value.", 24 | )); 25 | } 26 | 27 | let transform = load_transform_from_toml(toml)?; 28 | 29 | world.insert_one(entity, transform)?; 30 | 31 | Ok(()) 32 | } 33 | 34 | pub(crate) fn load_transform_from_toml(toml: &toml::Value) -> Result { 35 | let schema: EntTransformSchema = toml::from_str(&toml.to_string())?; 36 | let mut transform = Transform::default(); 37 | 38 | if let Some(translation) = schema.translation { 39 | transform.translation = Translation::new(translation.x, translation.y); 40 | } 41 | 42 | if let Some(rotation) = schema.rotation { 43 | transform.rotation = rotation; 44 | } 45 | 46 | if let Some(scale) = schema.scale { 47 | transform.scale = Scale::new(scale.x, scale.y); 48 | } 49 | 50 | Ok(transform) 51 | } 52 | -------------------------------------------------------------------------------- /emerald/examples/tilemap.rs: -------------------------------------------------------------------------------- 1 | use emerald::{tilemap::Tilemap, tilemap::TilemapSchema, *}; 2 | 3 | pub fn main() { 4 | emerald::start( 5 | Box::new(TilemapExample { 6 | world: World::new(), 7 | }), 8 | GameSettings::default(), 9 | ) 10 | } 11 | 12 | pub struct TilemapExample { 13 | world: World, 14 | } 15 | impl Game for TilemapExample { 16 | fn initialize(&mut self, mut emd: Emerald) { 17 | emd.set_asset_folder_root(String::from("./examples/assets/")); 18 | let tileset_key = emd.loader().texture("tileset.png").unwrap(); 19 | let mut tilemap = Tilemap::new(tileset_key, Vector2::new(16, 16), 2, 2, 4, 4); 20 | 21 | tilemap.set_tile(0, 0, Some(0)).unwrap(); 22 | tilemap.set_tile(1, 0, Some(1)).unwrap(); 23 | tilemap.set_tile(0, 1, Some(2)).unwrap(); 24 | tilemap.set_tile(1, 1, Some(3)).unwrap(); 25 | 26 | //This is how you serialize/deserialize a tilemap: 27 | let serialized_schema = toml::to_string(&tilemap).unwrap(); 28 | let deserialized_schema: TilemapSchema = toml::from_str(&serialized_schema).unwrap(); 29 | let deserialized_tilemap: Tilemap = 30 | deserialized_schema.to_tilemap(&mut emd.loader()).unwrap(); 31 | 32 | self.world 33 | .spawn((deserialized_tilemap, Transform::default())); 34 | } 35 | 36 | fn update(&mut self, _emd: Emerald) {} 37 | 38 | fn draw(&mut self, mut emd: Emerald) { 39 | emd.graphics().begin().unwrap(); 40 | emd.graphics().draw_world(&mut self.world).unwrap(); 41 | emd.graphics().render().unwrap(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /emerald/src/world/ent/ent_sprite_loader.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{AssetLoader, EmeraldError, Rectangle, World}; 5 | 6 | use super::Vec2f32Schema; 7 | 8 | #[derive(Deserialize, Serialize)] 9 | pub(crate) struct EntSpriteSchema { 10 | pub texture: String, 11 | 12 | #[serde(default)] 13 | pub offset: Option, 14 | 15 | #[serde(default)] 16 | pub visible: Option, 17 | 18 | #[serde(default)] 19 | pub scale: Option, 20 | 21 | #[serde(default)] 22 | pub z_index: Option, 23 | 24 | #[serde(default)] 25 | pub target: Option, 26 | } 27 | 28 | pub(crate) fn load_ent_sprite<'a>( 29 | loader: &mut AssetLoader<'a>, 30 | entity: Entity, 31 | world: &mut World, 32 | toml: &toml::Value, 33 | ) -> Result<(), EmeraldError> { 34 | if !toml.is_table() { 35 | return Err(EmeraldError::new( 36 | "Cannot load sprite from a non-table toml value.", 37 | )); 38 | } 39 | 40 | let schema: EntSpriteSchema = toml::from_str(&toml.to_string())?; 41 | let mut sprite = loader.sprite(schema.texture)?; 42 | sprite.z_index = schema.z_index.unwrap_or(0.0); 43 | sprite.visible = schema.visible.unwrap_or(true); 44 | 45 | if let Some(offset) = schema.offset { 46 | sprite.offset.x = offset.x; 47 | sprite.offset.y = offset.y; 48 | } 49 | if let Some(scale) = schema.scale { 50 | sprite.scale.x = scale.x; 51 | sprite.scale.y = scale.y; 52 | } 53 | 54 | schema.target.map(|t| sprite.target = t); 55 | 56 | world.insert_one(entity, sprite)?; 57 | 58 | Ok(()) 59 | } 60 | -------------------------------------------------------------------------------- /emerald/src/colors.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Copy, Clone, Debug, Deserialize, Serialize)] 4 | pub struct Color { 5 | pub r: u8, 6 | pub g: u8, 7 | pub b: u8, 8 | pub a: u8, 9 | } 10 | impl Color { 11 | pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { 12 | Color { r, g, b, a } 13 | } 14 | 15 | pub fn to_percentage(&self) -> (f32, f32, f32, f32) { 16 | let r = self.r as f32 / 255.0; 17 | let g = self.g as f32 / 255.0; 18 | let b = self.b as f32 / 255.0; 19 | let a = self.a as f32 / 255.0; 20 | 21 | (r, g, b, a) 22 | } 23 | 24 | pub fn to_percentage_linear(&self) -> (f64, f64, f64, f64) { 25 | let r = to_linear(self.r as f64 / 255.0); 26 | let g = to_linear(self.g as f64 / 255.0); 27 | let b = to_linear(self.b as f64 / 255.0); 28 | let a = self.a as f64 / 255.0; 29 | 30 | (r, g, b, a) 31 | } 32 | 33 | pub fn to_percentage_slice(&self) -> [f32; 4] { 34 | let (r, g, b, a) = self.to_percentage(); 35 | [r, g, b, a] 36 | } 37 | 38 | pub fn with_alpha(&self, a: u8) -> Color { 39 | Color::new(self.r, self.g, self.b, a) 40 | } 41 | } 42 | 43 | fn to_linear(x: f64) -> f64 { 44 | if x <= 0.04045 { 45 | x / 12.92 46 | } else { 47 | ((x + 0.055) / 1.055).powf(2.4) 48 | } 49 | } 50 | 51 | pub const BLACK: Color = Color { 52 | r: 0, 53 | g: 0, 54 | b: 0, 55 | a: 255, 56 | }; 57 | pub const WHITE: Color = Color { 58 | r: 255, 59 | g: 255, 60 | b: 255, 61 | a: 255, 62 | }; 63 | pub const CORNFLOWER_BLUE: Color = Color { 64 | r: 100, 65 | g: 149, 66 | b: 237, 67 | a: 255, 68 | }; 69 | -------------------------------------------------------------------------------- /emerald/examples/buttons.rs: -------------------------------------------------------------------------------- 1 | use emerald::{render_settings::RenderSettings, *}; 2 | 3 | pub fn main() { 4 | let mut settings = GameSettings::default(); 5 | let render_settings = RenderSettings { 6 | resolution: (320 * 3, 180 * 3), 7 | ..Default::default() 8 | }; 9 | settings.render_settings = render_settings; 10 | emerald::start( 11 | Box::new(MyGame { 12 | world: World::new(), 13 | }), 14 | settings, 15 | ) 16 | } 17 | 18 | pub struct MyGame { 19 | world: World, 20 | } 21 | impl Game for MyGame { 22 | fn initialize(&mut self, mut emd: Emerald) { 23 | emd.set_asset_folder_root(String::from("./examples/assets/")); 24 | let unpressed = emd.loader().texture("button_unpressed.png").unwrap(); 25 | let pressed = emd.loader().texture("button_pressed.png").unwrap(); 26 | emd.touches_to_mouse(true); 27 | 28 | self.world.spawn(( 29 | Transform::default(), 30 | UIButton::new(pressed.clone(), unpressed.clone()), 31 | )); 32 | self.world.spawn(( 33 | Transform::from_translation((320.0, 180.0)), 34 | UIButton::new(pressed.clone(), unpressed.clone()), 35 | )); 36 | self.world.spawn(( 37 | Transform::from_translation((-320.0, -180.0)), 38 | UIButton::new(pressed.clone(), unpressed.clone()), 39 | )); 40 | } 41 | 42 | fn update(&mut self, mut emd: Emerald) { 43 | ui_button_system(&mut emd, &mut self.world); 44 | } 45 | 46 | fn draw(&mut self, mut emd: Emerald) { 47 | emd.graphics().begin().ok(); 48 | emd.graphics().draw_world(&mut self.world).ok(); 49 | emd.graphics().render().ok(); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /emerald/src/audio/mixer/dummy.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | audio::{Mixer, SoundInstanceId, SoundKey}, 3 | AssetEngine, EmeraldError, 4 | }; 5 | 6 | pub struct DummyMixer {} 7 | impl DummyMixer { 8 | pub fn new() -> Result, EmeraldError> { 9 | Ok(Box::new(DummyMixer {})) 10 | } 11 | } 12 | 13 | impl Mixer for DummyMixer { 14 | fn play( 15 | &mut self, 16 | _key: &SoundKey, 17 | _asset_store: &mut AssetEngine, 18 | ) -> Result { 19 | Ok(SoundInstanceId::new(0)) 20 | } 21 | 22 | fn play_and_loop( 23 | &mut self, 24 | _key: &SoundKey, 25 | _asset_store: &mut AssetEngine, 26 | ) -> Result { 27 | Ok(SoundInstanceId::new(0)) 28 | } 29 | fn get_volume(&self) -> Result { 30 | Ok(0.0) 31 | } 32 | fn set_volume(&mut self, _volume: f32) -> Result<(), EmeraldError> { 33 | Ok(()) 34 | } 35 | fn set_instance_volume( 36 | &mut self, 37 | _snd_instance_id: SoundInstanceId, 38 | _volume: f32, 39 | ) -> Result<(), EmeraldError> { 40 | Ok(()) 41 | } 42 | fn get_instances(&self) -> Result, EmeraldError> { 43 | Ok(Vec::new()) 44 | } 45 | fn stop(&mut self, _snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError> { 46 | Ok(()) 47 | } 48 | fn pause(&mut self, _snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError> { 49 | Ok(()) 50 | } 51 | fn resume(&mut self, _snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError> { 52 | Ok(()) 53 | } 54 | fn clear(&mut self) -> Result<(), EmeraldError> { 55 | Ok(()) 56 | } 57 | fn post_update(&mut self) -> Result<(), EmeraldError> { 58 | Ok(()) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /emerald/src/audio/sound.rs: -------------------------------------------------------------------------------- 1 | use crate::{asset_key::AssetKey, EmeraldError}; 2 | 3 | #[derive(Clone, Debug, Copy, Hash, Eq, PartialEq)] 4 | pub enum SoundFormat { 5 | Ogg, 6 | Wav, 7 | } 8 | 9 | /// An instance of a sound playing. Use this id to pause, play, and resume this sound instance. 10 | #[derive(Clone, Debug, Copy, Hash, Eq, PartialEq)] 11 | pub struct SoundInstanceId(usize); 12 | impl SoundInstanceId { 13 | pub(crate) fn new(id: usize) -> Self { 14 | SoundInstanceId(id) 15 | } 16 | } 17 | 18 | /// A key to sound data in the engine. 19 | #[derive(Clone, Debug)] 20 | pub struct SoundKey { 21 | pub(crate) asset_key: AssetKey, 22 | format: SoundFormat, 23 | } 24 | impl SoundKey { 25 | pub fn new(asset_key: AssetKey, format: SoundFormat) -> Self { 26 | SoundKey { asset_key, format } 27 | } 28 | } 29 | 30 | pub use sound_backend::*; 31 | 32 | // Kira sound backend 33 | #[cfg(feature = "audio")] 34 | mod sound_backend { 35 | use crate::audio::sound::*; 36 | 37 | #[derive(Clone)] 38 | pub struct Sound { 39 | pub(crate) inner: kira::sound::Sound, 40 | } 41 | impl Sound { 42 | pub(crate) fn new(bytes: Vec, format: SoundFormat) -> Result { 43 | let reader = std::io::Cursor::new(bytes); 44 | let settings = kira::sound::SoundSettings::new(); 45 | 46 | let inner = match format { 47 | SoundFormat::Ogg => kira::sound::Sound::from_ogg_reader(reader, settings), 48 | SoundFormat::Wav => kira::sound::Sound::from_wav_reader(reader, settings), 49 | }?; 50 | 51 | Ok(Sound { inner }) 52 | } 53 | } 54 | } 55 | 56 | // Dummy sound backend 57 | #[cfg(not(feature = "audio"))] 58 | mod sound_backend { 59 | use crate::audio::sound::*; 60 | 61 | #[derive(Clone)] 62 | pub struct Sound {} 63 | impl Sound { 64 | pub(crate) fn new(_bytes: Vec, _format: SoundFormat) -> Result { 65 | Ok(Sound {}) 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /emerald/src/assets/asset_key.rs: -------------------------------------------------------------------------------- 1 | use std::any::{Any, TypeId}; 2 | 3 | use rapier2d::crossbeam::channel::{Receiver, Sender}; 4 | 5 | pub(crate) type AssetId = usize; 6 | 7 | pub type Asset = Box; 8 | 9 | #[derive(Debug)] 10 | pub struct AssetKey { 11 | pub(crate) type_id: TypeId, 12 | pub(crate) asset_id: AssetId, 13 | pub(crate) ref_sender: Sender, 14 | } 15 | impl AssetKey { 16 | pub(crate) fn new(asset_id: AssetId, type_id: TypeId, ref_sender: Sender) -> Self { 17 | ref_sender.send(RefChange::Increment(asset_id)).unwrap(); 18 | 19 | Self { 20 | type_id, 21 | asset_id, 22 | ref_sender, 23 | } 24 | } 25 | } 26 | impl PartialEq for AssetKey { 27 | fn eq(&self, other: &Self) -> bool { 28 | self.type_id == other.type_id && self.asset_id == other.asset_id 29 | } 30 | } 31 | impl Clone for AssetKey { 32 | fn clone(&self) -> Self { 33 | self.ref_sender 34 | .send(RefChange::Increment(self.asset_id)) 35 | .unwrap(); 36 | 37 | Self { 38 | type_id: self.type_id.clone(), 39 | asset_id: self.asset_id.clone(), 40 | ref_sender: self.ref_sender.clone(), 41 | } 42 | } 43 | } 44 | impl Drop for AssetKey { 45 | fn drop(&mut self) { 46 | self.ref_sender 47 | .send(RefChange::Decrement(self.asset_id)) 48 | .expect(&format!( 49 | "Fatal Error: Failed to drop asset {:?}", 50 | (self.type_id, self.asset_id) 51 | )); 52 | } 53 | } 54 | 55 | pub(crate) enum RefChange { 56 | Increment(AssetId), 57 | Decrement(AssetId), 58 | } 59 | 60 | #[derive(Clone)] 61 | pub(crate) struct RefChangeChannel { 62 | pub sender: Sender, 63 | pub receiver: Receiver, 64 | } 65 | impl Default for RefChangeChannel { 66 | fn default() -> Self { 67 | let (sender, receiver) = crate::crossbeam::channel::unbounded(); 68 | RefChangeChannel { sender, receiver } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /emerald/examples/input_actions.rs: -------------------------------------------------------------------------------- 1 | use emerald::{render_settings::RenderSettings, *}; 2 | 3 | pub fn main() { 4 | let mut settings = GameSettings::default(); 5 | let render_settings = RenderSettings { 6 | resolution: (320 * 3, 180 * 3), 7 | ..Default::default() 8 | }; 9 | settings.render_settings = render_settings; 10 | emerald::start( 11 | Box::new(InputActionsExample { 12 | world: World::new(), 13 | }), 14 | settings, 15 | ) 16 | } 17 | 18 | const ACTION_TEST: &str = "test_action"; 19 | 20 | const ACTION_ATTACK_P1: &str = "p1_attack"; 21 | const ACTION_ATTACK_P2: &str = "p2_attack"; 22 | 23 | pub struct InputActionsExample { 24 | world: World, 25 | } 26 | impl Game for InputActionsExample { 27 | fn initialize(&mut self, mut emd: Emerald) { 28 | emd.set_asset_folder_root(String::from("./examples/assets/")); 29 | emd.input() 30 | .add_action_binding_key(&ACTION_TEST.to_string(), KeyCode::A); 31 | emd.input() 32 | .add_action_binding_key(&ACTION_TEST.to_string(), KeyCode::P); 33 | emd.input() 34 | .add_action_binding_key(&ACTION_TEST.to_string(), KeyCode::G); 35 | 36 | emd.input() 37 | .add_action_binding_button(&ACTION_ATTACK_P1.to_string(), Button::West, 0); 38 | 39 | emd.input() 40 | .add_action_binding_button(&ACTION_ATTACK_P2.to_string(), Button::West, 1); 41 | } 42 | 43 | fn update(&mut self, mut emd: Emerald) { 44 | if emd.input().is_action_just_pressed(&ACTION_TEST.to_string()) { 45 | println!("Test Action Pressed"); 46 | } 47 | if emd 48 | .input() 49 | .is_action_just_pressed(&ACTION_ATTACK_P1.to_string()) 50 | { 51 | println!("p1 attack"); 52 | } 53 | if emd 54 | .input() 55 | .is_action_just_pressed(&ACTION_ATTACK_P2.to_string()) 56 | { 57 | println!("p2 attack"); 58 | } 59 | } 60 | 61 | fn draw(&mut self, mut emd: Emerald) { 62 | emd.graphics().begin().ok(); 63 | emd.graphics().draw_world(&mut self.world).ok(); 64 | emd.graphics().render().ok(); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /emerald/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "emerald" 3 | version = "0.3.214" 4 | authors = ["Bombfuse "] 5 | edition = "2018" 6 | description = "A lite, fully featured 2D game engine." 7 | repository = "https://github.com/Bombfuse/emerald" 8 | license = "MIT OR Apache-2.0" 9 | resolver = "2" 10 | 11 | [features] 12 | default = ["gamepads", "aseprite", "audio"] 13 | audio = ["kira"] 14 | gamepads = ["gamepad"] 15 | aseprite = ["asefile"] 16 | hotreload = [] 17 | physics-deterministic = ["rapier2d/enhanced-determinism"] 18 | 19 | [dependencies] 20 | fontdue = "0.7.2" 21 | toml = "0.5.9" 22 | serde_json = "1.0" 23 | wgpu = "0.14.0" 24 | env_logger = "0.9.1" 25 | pollster = "0.2.5" 26 | winit = "0.27.4" 27 | rapier2d = "0.15.0" 28 | instant = "0.1.12" 29 | emd_earcutr = "0.1.0" 30 | hecs = { version = "0.9", default-features = false } 31 | image = { version = "0.23.14", default-features = false, features = [ "png" ] } 32 | serde = { version = "1.0.145", features=["derive"] } 33 | bytemuck = { version = "1.12.1", features = ["derive"] } 34 | gamepad = { version = "0.1.6", optional = true } 35 | asefile = { version = "0.3.4", optional = true } 36 | anymap = { version = "0.12.1", default-features = false } 37 | 38 | [target.'cfg(target_arch = "wasm32")'.dependencies] 39 | kira = { version= "0.5.3", optional = true, default-features = false, features = ["ogg", "flac", "wav"] } 40 | console_error_panic_hook = "0.1.6" 41 | console_log = "0.2.0" 42 | log = "0.4.17" 43 | wgpu = { version = "0.14.0", features = ["webgl"]} 44 | wasm-bindgen = "0.2" 45 | wasm-bindgen-futures = "0.4.30" 46 | web-sys = { version = "0.3", features = [ 47 | "Document", 48 | "Window", 49 | "Element", 50 | ]} 51 | 52 | [target.'cfg(not(target_arch="wasm32"))'.dependencies] 53 | kira = { version= "0.5.3", optional = true, default-features = false, features = ["ogg", "flac", "wav"] } 54 | 55 | [target.'cfg(target_os = "windows")'.dependencies] 56 | winapi = { version = "0.3", features = ["winerror", "knownfolders", "shtypes", "shlobj", "combaseapi"] } 57 | 58 | [target.'cfg(target_os = "android")'.dependencies] 59 | sapp-android = "0.1.9" 60 | ndk-glue = "0.7.0" 61 | 62 | [lib] 63 | name = "emerald" 64 | path = "src/lib.rs" 65 | crate-type = ["lib", "cdylib"] 66 | -------------------------------------------------------------------------------- /emerald/src/input/components/ui_button.rs: -------------------------------------------------------------------------------- 1 | use crate::{texture::TextureKey, Rectangle}; 2 | 3 | pub struct UIButton { 4 | pub pressed_texture: TextureKey, 5 | pub unpressed_texture: TextureKey, 6 | 7 | /// Custom bounding box for the pressed area of the button, overwrites the usage of the texture for the box. 8 | pub custom_pressed_bounding_box: Option, 9 | 10 | /// Custom bounding box for the unpressed area of the button, overwrites the usage of the texture for the box. 11 | pub custom_unpressed_bounding_box: Option, 12 | 13 | pub(crate) is_pressed: bool, 14 | pub(crate) was_pressed: bool, 15 | 16 | pub z_index: f32, 17 | pub visible: bool, 18 | } 19 | impl UIButton { 20 | pub fn new(pressed_texture: TextureKey, unpressed_texture: TextureKey) -> Self { 21 | UIButton { 22 | unpressed_texture, 23 | pressed_texture, 24 | custom_pressed_bounding_box: None, 25 | custom_unpressed_bounding_box: None, 26 | is_pressed: false, 27 | was_pressed: false, 28 | z_index: 0.0, 29 | visible: true, 30 | } 31 | } 32 | 33 | pub fn is_pressed(&self) -> bool { 34 | self.is_pressed 35 | } 36 | pub fn is_just_pressed(&self) -> bool { 37 | self.is_pressed && !self.was_pressed 38 | } 39 | pub fn is_just_released(&self) -> bool { 40 | !self.is_pressed && self.was_pressed 41 | } 42 | 43 | /// Presses the button 44 | pub fn press(&mut self) { 45 | self.rollover(); 46 | self.is_pressed = true; 47 | } 48 | 49 | /// Releases the button 50 | pub fn release(&mut self) { 51 | self.rollover(); 52 | self.is_pressed = false; 53 | } 54 | 55 | fn rollover(&mut self) { 56 | self.was_pressed = self.is_pressed; 57 | self.is_pressed = false; 58 | } 59 | 60 | pub fn reset(&mut self) { 61 | self.is_pressed = false; 62 | self.was_pressed = false; 63 | } 64 | 65 | pub(crate) fn current_texture(&self) -> &TextureKey { 66 | if self.is_pressed() { 67 | &self.pressed_texture 68 | } else { 69 | &self.unpressed_texture 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /emerald/examples/aseprite.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ 2 | render_settings::RenderSettings, rendering::components::aseprite_update_system, 3 | transform::Transform, Emerald, Game, GameSettings, World, 4 | }; 5 | 6 | pub fn main() { 7 | let mut settings = GameSettings::default(); 8 | let render_settings = RenderSettings { 9 | resolution: (320, 180), 10 | ..Default::default() 11 | }; 12 | settings.render_settings = render_settings; 13 | emerald::start( 14 | Box::new(MyGame { 15 | world: World::new(), 16 | }), 17 | settings, 18 | ) 19 | } 20 | 21 | pub struct MyGame { 22 | world: World, 23 | } 24 | impl Game for MyGame { 25 | fn initialize(&mut self, mut emd: Emerald) { 26 | // Set up the root folder for all asset loading 27 | emd.set_asset_folder_root(String::from("./examples/assets/")); 28 | 29 | // Load an aseprite file from the asset folder 30 | let mut aseprite = emd.loader().aseprite("smiley.aseprite").unwrap(); 31 | aseprite.play_and_loop("smile").unwrap(); 32 | 33 | let mut a2 = aseprite.clone(); 34 | // Play the animation just once without looping 35 | a2.play("smile").unwrap(); 36 | 37 | // Load an exported sprite sheet (in "Array" mode). (The file loaded 38 | // here is the same animation as the aseprite file above, except it's 39 | // exported.) 40 | let mut a3 = emd 41 | .loader() 42 | .aseprite_with_animations("smiley.png", "smiley.json") 43 | .unwrap(); 44 | a3.play_and_loop("smile").unwrap(); 45 | 46 | self.world 47 | .spawn((aseprite, Transform::from_translation((64.0, 64.0)))); 48 | self.world 49 | .spawn((a2, Transform::from_translation((-64.0, 64.0)))); 50 | self.world 51 | .spawn((a3, Transform::from_translation((0.0, -64.0)))); 52 | } 53 | 54 | fn update(&mut self, emd: Emerald) { 55 | let delta = emd.delta(); 56 | 57 | aseprite_update_system(&mut self.world, delta); 58 | } 59 | 60 | fn draw(&mut self, mut emd: Emerald) { 61 | emd.graphics().begin().unwrap(); 62 | emd.graphics().draw_world(&mut self.world).unwrap(); 63 | emd.graphics().render().unwrap(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /emerald/examples/touch.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use emerald::{rendering::components::Sprite, *}; 4 | 5 | pub fn main() { 6 | let game = TouchExample { 7 | bunnies: HashMap::new(), 8 | sprite: None, 9 | world: World::new(), 10 | }; 11 | emerald::start(Box::new(game), GameSettings::default()) 12 | } 13 | 14 | pub struct TouchExample { 15 | bunnies: HashMap, 16 | sprite: Option, 17 | world: World, 18 | } 19 | 20 | impl Game for TouchExample { 21 | fn initialize(&mut self, mut emd: Emerald) { 22 | emd.set_asset_folder_root(String::from("./examples/assets/")); 23 | self.sprite = emd.loader().sprite("bunny.png").ok(); 24 | emd.mouse_to_touch(true); 25 | } 26 | 27 | fn update(&mut self, mut emd: Emerald) { 28 | let input = emd.input(); 29 | let touches = input.touches().clone(); 30 | 31 | let screen = emd.screen_size(); 32 | let screen_center = 33 | Transform::from_translation((screen.0 as f32 / 2.0, screen.1 as f32 / 2.0)); 34 | 35 | for (id, touch) in &touches { 36 | let bunny_position = touch.translation - screen_center.translation; 37 | if touch.is_just_pressed() { 38 | let components: (Sprite, Transform) = ( 39 | self.sprite.clone().unwrap(), 40 | Transform::from_translation(bunny_position), 41 | ); 42 | self.bunnies.insert(*id, self.world.spawn(components)); 43 | } else if touch.is_just_released() { 44 | if let Some(x) = self.bunnies.remove(&id) { 45 | self.world.despawn(x).unwrap(); 46 | } 47 | } else { 48 | let bunny = self 49 | .bunnies 50 | .get(&id) 51 | .copied() 52 | .and_then(|ent| self.world.get::<&mut Transform>(ent).ok()); 53 | if let Some(mut bunny) = bunny { 54 | bunny.translation = bunny_position; 55 | } 56 | } 57 | } 58 | } 59 | 60 | fn draw(&mut self, mut emd: Emerald) { 61 | emd.graphics().begin().unwrap(); 62 | emd.graphics().draw_world(&mut self.world).unwrap(); 63 | emd.graphics().render().unwrap(); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /emerald/src/world/physics/types.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use rapier2d::na::{Point2, Vector2}; 3 | use rapier2d::{parry::query::Ray, prelude::ColliderHandle}; 4 | 5 | use crate::transform::Translation; 6 | 7 | /// # Parameters 8 | /// - `ray`: the ray to cast. 9 | /// - `max_toi`: the maximum time-of-impact that can be reported by this cast. This effectively 10 | /// limits the length of the ray to `ray.dir.norm() * max_toi`. Use `Real::MAX` for an unbounded ray. 11 | /// - `solid`: if this is `true` an impact at time 0.0 (i.e. at the ray origin) is returned if 12 | /// it starts inside of a shape. If this `false` then the ray will hit the shape's boundary 13 | /// even if its starts inside of it. 14 | /// - `query_groups`: the interaction groups which will be tested against the collider's `contact_group` 15 | /// to determine if it should be taken into account by this query. 16 | /// - `filter`: a more fine-grained filter. A collider is taken into account by this query if 17 | /// its `contact_group` is compatible with the `query_groups`, and if this `filter` 18 | /// is either `None` or returns `true`. 19 | #[derive(Clone)] 20 | pub struct RayCastQuery<'a> { 21 | pub ray: Ray, 22 | pub max_toi: f32, 23 | pub filter: QueryFilter<'a>, 24 | pub solid: bool, 25 | } 26 | impl<'a> Default for RayCastQuery<'a> { 27 | fn default() -> RayCastQuery<'a> { 28 | RayCastQuery { 29 | ray: Ray::new(Point2::new(0.0, 0.0), Vector2::new(0.0, 0.0)), 30 | max_toi: 4.0, 31 | filter: Default::default(), 32 | solid: true, 33 | } 34 | } 35 | } 36 | 37 | pub use rapier2d::prelude::QueryFilter; 38 | 39 | pub struct ShapeCastQuery<'a> { 40 | /// The origin position that the shape will be cast from. 41 | pub origin_translation: Translation, 42 | /// The directional velocity the shape will move at during the cast. 43 | pub velocity: Vector2, 44 | pub max_toi: f32, 45 | pub filter: QueryFilter<'a>, 46 | pub stop_at_penetration: bool, 47 | } 48 | impl<'a> Default for ShapeCastQuery<'a> { 49 | fn default() -> ShapeCastQuery<'a> { 50 | ShapeCastQuery { 51 | origin_translation: Translation::new(0.0, 0.0), 52 | velocity: Vector2::new(0.0, 0.0), 53 | max_toi: 4.0, 54 | filter: Default::default(), 55 | stop_at_penetration: false, 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /emerald/src/rendering/components/color_tri.rs: -------------------------------------------------------------------------------- 1 | use crate::*; 2 | 3 | #[derive(Debug, Copy, Clone)] 4 | pub struct ColorTri { 5 | pub color: Color, 6 | pub offset: Vector2, 7 | pub visible: bool, 8 | pub centered: bool, 9 | pub z_index: f32, 10 | pub points: [Vector2; 3], 11 | } 12 | impl ColorTri { 13 | pub fn new(color: Color, points: [Vector2; 3]) -> Self { 14 | ColorTri { 15 | color, 16 | points, 17 | ..Default::default() 18 | } 19 | } 20 | 21 | pub fn get_bounding_rect(&self) -> Rectangle { 22 | get_bounding_box_of_triangle(&self.points) 23 | } 24 | 25 | pub fn get_max_x(&self) -> f32 { 26 | get_max_x(&self.points) 27 | } 28 | 29 | pub fn get_min_x(&self) -> f32 { 30 | get_min_x(&self.points) 31 | } 32 | 33 | pub fn get_max_y(&self) -> f32 { 34 | get_max_y(&self.points) 35 | } 36 | 37 | pub fn get_min_y(&self) -> f32 { 38 | get_min_y(&self.points) 39 | } 40 | } 41 | impl Default for ColorTri { 42 | fn default() -> ColorTri { 43 | ColorTri { 44 | color: WHITE, 45 | offset: Vector2::new(0.0, 0.0), 46 | visible: true, 47 | centered: true, 48 | z_index: 0.0, 49 | // default of 1 pixel wide triangle 50 | points: [ 51 | Vector2::new(-0.5, -0.5), 52 | Vector2::new(0.0, 0.5), 53 | Vector2::new(0.5, -0.5), 54 | ], 55 | } 56 | } 57 | } 58 | 59 | pub fn get_bounding_box_of_triangle(points: &[Vector2; 3]) -> Rectangle { 60 | let max_x = get_max_x(points); 61 | let max_y = get_max_y(points); 62 | let min_x = get_min_x(points); 63 | let min_y = get_min_y(points); 64 | 65 | Rectangle::new(min_x, min_y, max_x - min_x, max_y - min_y) 66 | } 67 | 68 | fn get_max_x(points: &[Vector2; 3]) -> f32 { 69 | let (x1, x2, x3) = (points[0].x, points[1].x, points[2].x); 70 | x1.max(x2).max(x3) 71 | } 72 | 73 | fn get_min_x(points: &[Vector2; 3]) -> f32 { 74 | let (x1, x2, x3) = (points[0].x, points[1].x, points[2].x); 75 | x1.min(x2).min(x3) 76 | } 77 | 78 | fn get_max_y(points: &[Vector2; 3]) -> f32 { 79 | let (y1, y2, y3) = (points[0].y, points[1].y, points[2].y); 80 | y1.max(y2).max(y3) 81 | } 82 | 83 | fn get_min_y(points: &[Vector2; 3]) -> f32 { 84 | let (y1, y2, y3) = (points[0].y, points[1].y, points[2].y); 85 | y1.min(y2).min(y3) 86 | } 87 | -------------------------------------------------------------------------------- /emerald/examples/labels.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ 2 | rendering::components::Label, Emerald, Game, GameSettings, KeyCode, Transform, World, 3 | }; 4 | use fontdue::layout::HorizontalAlign; 5 | 6 | pub fn main() { 7 | emerald::start( 8 | Box::new(GamepadExample { 9 | world: World::new(), 10 | }), 11 | GameSettings::default(), 12 | ) 13 | } 14 | 15 | pub struct ElapsedTime(f32); 16 | 17 | pub struct GamepadExample { 18 | world: World, 19 | } 20 | impl Game for GamepadExample { 21 | fn initialize(&mut self, mut emd: Emerald) { 22 | emd.set_asset_folder_root(String::from("./examples/assets/")); 23 | let font = emd.loader().font("Roboto-Light.ttf", 40).unwrap(); 24 | 25 | let mut left_aligned_label = Label::new("Emerald Engine", font, 80); 26 | left_aligned_label.max_width = Some(400.0); 27 | 28 | let mut centered_label = left_aligned_label.clone(); 29 | centered_label.horizontal_align = HorizontalAlign::Center; 30 | 31 | let mut right_label = left_aligned_label.clone(); 32 | right_label.horizontal_align = HorizontalAlign::Right; 33 | 34 | self.world 35 | .spawn((ElapsedTime(0.0), Transform::default(), left_aligned_label)); 36 | self.world.spawn(( 37 | ElapsedTime(0.0), 38 | Transform::from_translation((0.0, 300.0)), 39 | centered_label, 40 | )); 41 | self.world.spawn(( 42 | ElapsedTime(0.0), 43 | Transform::from_translation((0.0, -300.0)), 44 | right_label, 45 | )); 46 | } 47 | 48 | fn update(&mut self, mut emd: Emerald) { 49 | let mut input = emd.input(); 50 | 51 | for (_, (label, _elapsed_time)) in 52 | self.world.query::<(&mut Label, &mut ElapsedTime)>().iter() 53 | { 54 | if input.is_key_just_pressed(KeyCode::A) { 55 | label.scale *= 0.5; 56 | } else if input.is_key_just_pressed(KeyCode::D) { 57 | label.scale *= 2.0; 58 | } else if input.is_key_just_pressed(KeyCode::E) { 59 | label.max_width = Some(800.0); 60 | } else if input.is_key_just_pressed(KeyCode::R) { 61 | label.max_width = Some(400.0); 62 | } 63 | } 64 | } 65 | 66 | fn draw(&mut self, mut emd: Emerald) { 67 | emd.graphics().begin().unwrap(); 68 | emd.graphics().draw_world(&mut self.world).unwrap(); 69 | emd.graphics().render().unwrap(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /emerald/src/logging/engine.rs: -------------------------------------------------------------------------------- 1 | use crate::EmeraldError; 2 | 3 | use std::{ 4 | fs::{File, OpenOptions}, 5 | io::{prelude::*, LineWriter}, 6 | }; 7 | 8 | pub(crate) enum Log { 9 | Info(String), 10 | Warning(String), 11 | Error(String), 12 | } 13 | 14 | const DEFAULT_LOG_FILE_PATH: &str = "./emerald.log"; 15 | 16 | pub struct LoggingEngine { 17 | logs: Vec, 18 | line_writer: Option>, 19 | } 20 | impl LoggingEngine { 21 | pub(crate) fn new() -> Self { 22 | let mut line_writer = None; 23 | 24 | #[cfg(not(target_arch = "wasm32"))] 25 | { 26 | let file = OpenOptions::new() 27 | .write(true) 28 | .append(true) 29 | .create(true) 30 | .open(String::from(DEFAULT_LOG_FILE_PATH)) 31 | .unwrap(); 32 | line_writer = Some(LineWriter::new(file)); 33 | } 34 | 35 | LoggingEngine { 36 | logs: Vec::new(), 37 | line_writer, 38 | } 39 | } 40 | 41 | pub(crate) fn update(&mut self) -> Result<(), EmeraldError> { 42 | if let Some(line_writer) = &mut self.line_writer { 43 | for log in &self.logs { 44 | match log { 45 | Log::Info(msg) => line_writer.write_all(msg.as_bytes())?, 46 | Log::Warning(msg) => line_writer.write_all(msg.as_bytes())?, 47 | Log::Error(msg) => line_writer.write_all(msg.as_bytes())?, 48 | }; 49 | 50 | line_writer.write_all("\n".as_bytes())?; 51 | } 52 | line_writer.flush()?; 53 | } 54 | 55 | self.logs = Vec::with_capacity(self.logs.len()); 56 | Ok(()) 57 | } 58 | 59 | fn log(&mut self, log: Log) { 60 | self.logs.push(log); 61 | } 62 | 63 | pub fn info>(&mut self, msg: T) -> Result<(), EmeraldError> { 64 | let log = Log::Info(msg.into()); 65 | 66 | self.log(log); 67 | self.update()?; 68 | 69 | Ok(()) 70 | } 71 | 72 | pub fn warning>(&mut self, msg: T) -> Result<(), EmeraldError> { 73 | let log = Log::Warning(msg.into()); 74 | 75 | self.log(log); 76 | self.update()?; 77 | 78 | Ok(()) 79 | } 80 | 81 | pub fn error>(&mut self, msg: T) -> Result<(), EmeraldError> { 82 | let log = Log::Error(msg.into()); 83 | 84 | self.log(log); 85 | self.update()?; 86 | 87 | Ok(()) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /emerald/examples/gamepads.rs: -------------------------------------------------------------------------------- 1 | use emerald::{render_settings::RenderSettings, rendering::components::Sprite, *}; 2 | 3 | pub fn main() { 4 | let mut settings = GameSettings::default(); 5 | let render_settings = RenderSettings { 6 | resolution: (480, 320), 7 | ..Default::default() 8 | }; 9 | settings.render_settings = render_settings; 10 | emerald::start( 11 | Box::new(GamepadExample { 12 | world: World::new(), 13 | }), 14 | settings, 15 | ) 16 | } 17 | 18 | pub struct GamepadExample { 19 | world: World, 20 | } 21 | impl Game for GamepadExample { 22 | fn initialize(&mut self, mut emd: Emerald) { 23 | emd.set_asset_folder_root(String::from("./examples/assets/")); 24 | 25 | match emd.loader().sprite("bunny.png") { 26 | Ok(sprite) => { 27 | self.world 28 | .spawn((sprite, Transform::from_translation((16.0, 16.0)))); 29 | } 30 | Err(_) => {} 31 | }; 32 | } 33 | 34 | fn update(&mut self, mut emd: Emerald) { 35 | let delta = emd.delta(); 36 | let mut input = emd.input(); 37 | let mut velocity = Vector2::new(0.0, 0.0); 38 | let speed = 500.0; 39 | let mut direction = input.joystick(Joystick::Left); 40 | 41 | if input.is_button_pressed(Button::DPadNorth) { 42 | direction.1 = 1.0; 43 | } else if input.is_button_pressed(Button::DPadSouth) { 44 | direction.1 = -1.0; 45 | } 46 | 47 | if input.is_button_pressed(Button::DPadWest) { 48 | direction.0 = -1.0; 49 | } else if input.is_button_pressed(Button::DPadEast) { 50 | direction.0 = 1.0; 51 | } 52 | 53 | velocity.x = direction.0 * speed; 54 | velocity.y = direction.1 * speed; 55 | 56 | for (_, (transform, sprite)) in self.world.query::<(&mut Transform, &mut Sprite)>().iter() { 57 | if input.is_button_just_pressed(Button::North) { 58 | sprite.scale *= 2.0; 59 | } else if input.is_button_just_pressed(Button::South) { 60 | sprite.scale *= 0.5; 61 | } 62 | 63 | if input.is_button_just_pressed(Button::West) { 64 | sprite.visible = !sprite.visible; 65 | } 66 | 67 | transform.translation.x += delta * velocity.x; 68 | transform.translation.y += delta * velocity.y; 69 | } 70 | } 71 | 72 | fn draw(&mut self, mut emd: Emerald) { 73 | emd.graphics().begin().unwrap(); 74 | emd.graphics().draw_world(&mut self.world).unwrap(); 75 | emd.graphics().render().unwrap(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /emerald/src/world/ent/ent_aseprite_loader.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | use crate::{AssetLoader, EmeraldError, World}; 5 | 6 | use super::Vec2f32Schema; 7 | 8 | #[derive(Deserialize, Serialize)] 9 | pub(crate) struct EntAsepriteSchema { 10 | pub aseprite: Option, 11 | pub texture: Option, 12 | pub animations: Option, 13 | pub offset: Option, 14 | pub visible: Option, 15 | pub centered: Option, 16 | pub scale: Option, 17 | pub default_animation: Option, 18 | pub z_index: Option, 19 | } 20 | 21 | #[derive(Deserialize, Serialize)] 22 | pub(crate) struct AsepriteDefaultAnimationSchema { 23 | pub name: String, 24 | pub looping: Option, 25 | } 26 | 27 | pub(crate) fn load_ent_aseprite<'a>( 28 | loader: &mut AssetLoader<'a>, 29 | entity: Entity, 30 | world: &mut World, 31 | toml: &toml::Value, 32 | ) -> Result<(), EmeraldError> { 33 | if !toml.is_table() { 34 | return Err(EmeraldError::new( 35 | "Cannot load aseprite from a non-table toml value.", 36 | )); 37 | } 38 | let schema: EntAsepriteSchema = toml::from_str(&toml.to_string())?; 39 | 40 | if (schema.animations.is_none() || schema.texture.is_none()) && schema.aseprite.is_none() { 41 | return Err(EmeraldError::new(format!("Failed to load Aseprite for entity {:?}. Either (animations AND texture) OR aseprite must be provided.", entity))); 42 | } 43 | 44 | let mut aseprite = None; 45 | 46 | if let Some(aseprite_path) = schema.aseprite { 47 | aseprite = Some(loader.aseprite(aseprite_path)?); 48 | } 49 | 50 | if let (Some(texture), Some(animations)) = (schema.texture, schema.animations) { 51 | aseprite = Some(loader.aseprite_with_animations(texture, animations)?); 52 | } 53 | 54 | let mut aseprite = aseprite.unwrap(); 55 | aseprite.z_index = schema.z_index.unwrap_or(0.0); 56 | aseprite.visible = schema.visible.unwrap_or(true); 57 | aseprite.centered = schema.centered.unwrap_or(true); 58 | 59 | if let Some(offset) = schema.offset { 60 | aseprite.offset.x = offset.x; 61 | aseprite.offset.y = offset.y; 62 | } 63 | 64 | if let Some(scale) = schema.scale { 65 | aseprite.scale.x = scale.x; 66 | aseprite.scale.y = scale.y; 67 | } 68 | 69 | if let Some(default_animation_schema) = schema.default_animation { 70 | let looping = default_animation_schema.looping.unwrap_or(false); 71 | if looping { 72 | aseprite.play_and_loop(default_animation_schema.name)?; 73 | } else { 74 | aseprite.play(default_animation_schema.name)?; 75 | } 76 | } 77 | 78 | world.insert_one(entity, aseprite)?; 79 | 80 | Ok(()) 81 | } 82 | -------------------------------------------------------------------------------- /emerald/examples/shapes.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ 2 | rendering::components::ColorTri, Emerald, Game, GameSettings, KeyCode, Transform, Vector2, 3 | World, BLACK, WHITE, 4 | }; 5 | use rapier2d::prelude::{ConvexPolygon, Point}; 6 | 7 | pub fn main() { 8 | emerald::start(Box::new(ShapesExample {}), GameSettings::default()) 9 | } 10 | 11 | pub struct ShapesExample {} 12 | impl Game for ShapesExample { 13 | fn initialize(&mut self, mut emd: Emerald) { 14 | emd.set_asset_folder_root("./examples/assets/".to_string()); 15 | } 16 | 17 | fn update(&mut self, emd: Emerald) {} 18 | 19 | fn draw(&mut self, mut emd: Emerald<'_>) { 20 | let color_tri = ColorTri::new( 21 | WHITE, 22 | [ 23 | Vector2::new(-10.0, -10.0), 24 | Vector2::new(10.0, -10.0), 25 | Vector2::new(0.0, 10.0), 26 | ], 27 | ); 28 | emd.graphics().begin().unwrap(); 29 | emd.graphics() 30 | .draw_color_tri(&color_tri, &Default::default()) 31 | .unwrap(); 32 | emd.graphics() 33 | .draw_color_tri(&color_tri, &Transform::from_translation((30.0, 30.0))) 34 | .unwrap(); 35 | emd.graphics() 36 | .draw_color_tri( 37 | &ColorTri::new( 38 | BLACK, 39 | [ 40 | Vector2::new(-20.0, -10.0), 41 | Vector2::new(10.0, -20.0), 42 | Vector2::new(0.0, 20.0), 43 | ], 44 | ), 45 | &Transform::from_translation((-50.0, -50.0)), 46 | ) 47 | .unwrap(); 48 | emd.graphics() 49 | .draw_convex_polygon( 50 | &BLACK, 51 | &ConvexPolygon::from_convex_polyline(vec![ 52 | Point::new(0.0, 0.0), 53 | Point::new(10.0, 0.0), 54 | Point::new(20.0, 10.0), 55 | Point::new(20.0, 20.0), 56 | Point::new(10.0, 30.0), 57 | Point::new(0.0, 30.0), 58 | Point::new(-10.0, 20.0), 59 | Point::new(-10.0, 10.0), 60 | ]) 61 | .unwrap(), 62 | &Transform::from_translation((-50.0, 100.0)), 63 | ) 64 | .unwrap(); 65 | emd.graphics() 66 | .draw_convex_polygon( 67 | &BLACK, 68 | &ConvexPolygon::from_convex_polyline(vec![ 69 | Point::new(-10.0, 0.0), 70 | Point::new(10.0, 10.0), 71 | Point::new(10.0, 20.0), 72 | Point::new(15.0, 40.0), 73 | ]) 74 | .unwrap(), 75 | &Transform::from_translation((100.0, 100.0)), 76 | ) 77 | .unwrap(); 78 | emd.graphics().render().unwrap(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /emerald/examples/world_merging.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use emerald::*; 4 | 5 | pub fn main() { 6 | emerald::start( 7 | Box::new(WorldMergingExample { 8 | world: World::new(), 9 | }), 10 | GameSettings::default(), 11 | ) 12 | } 13 | 14 | pub struct WorldMergingExample { 15 | world: World, 16 | } 17 | impl Game for WorldMergingExample { 18 | fn initialize(&mut self, mut emd: Emerald) { 19 | emd.set_asset_folder_root(String::from("./examples/assets/")); 20 | let sprite = emd.loader().sprite("bunny.png").unwrap(); 21 | self.world.spawn((sprite, Transform::default())); 22 | self.world.physics().set_gravity(Vector2::new(0.0, -19.8)); 23 | } 24 | 25 | fn update(&mut self, mut emd: Emerald) { 26 | let mut input = emd.input(); 27 | 28 | if input.is_key_just_pressed(KeyCode::A) { 29 | let amount = 100; 30 | let world = build_other_world( 31 | &mut emd, 32 | Transform::from_translation((100.0, 100.0)), 33 | amount, 34 | ) 35 | .unwrap(); 36 | let now = emd.now(); 37 | self.world 38 | .merge(world, Transform::from_translation((100.0, 100.0))) 39 | .unwrap(); 40 | let after = emd.now(); 41 | println!( 42 | "merged {} bunnies in {:?}us", 43 | amount, 44 | Duration::from_secs_f64(after - now).as_micros() 45 | ); 46 | } 47 | 48 | self.world.physics().step(emd.delta()); 49 | } 50 | 51 | fn draw(&mut self, mut emd: Emerald) { 52 | emd.graphics().begin().unwrap(); 53 | emd.graphics().draw_world(&mut self.world).unwrap(); 54 | emd.graphics().render().unwrap(); 55 | } 56 | } 57 | 58 | fn build_other_world( 59 | emd: &mut Emerald, 60 | offset: Transform, 61 | amount: i32, 62 | ) -> Result { 63 | let mut world = World::new(); 64 | let sprite = emd.loader().sprite("bunny.png")?; 65 | let amount = (amount as f32).sqrt() as usize; 66 | 67 | for x in 0..amount { 68 | for y in 0..amount { 69 | let (_entity, rbh) = world 70 | .spawn_with_body( 71 | ( 72 | Transform::from_translation(( 73 | x as f32 * 30.0 - 300.0, 74 | y as f32 * 30.0 - 300.0, 75 | )) + offset, 76 | sprite.clone(), 77 | ), 78 | RigidBodyBuilder::dynamic() 79 | .can_sleep(false) 80 | .linvel(Vector2::new(10.0, 10.0)), 81 | ) 82 | .unwrap(); 83 | 84 | world 85 | .physics() 86 | .build_collider(rbh, ColliderBuilder::cuboid(1.0, 1.0)); 87 | } 88 | } 89 | 90 | Ok(world) 91 | } 92 | -------------------------------------------------------------------------------- /emerald/examples/camera.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ 2 | rendering::components::{aseprite_update_system, Camera, Sprite}, 3 | *, 4 | }; 5 | 6 | pub fn main() { 7 | let mut settings = GameSettings::default(); 8 | settings.render_settings.resolution = (320, 180); 9 | emerald::start( 10 | Box::new(CameraExample { 11 | world: World::new(), 12 | }), 13 | settings, 14 | ) 15 | } 16 | 17 | pub struct CameraExample { 18 | world: World, 19 | } 20 | impl Game for CameraExample { 21 | fn initialize(&mut self, mut emd: Emerald) { 22 | emd.set_asset_folder_root(String::from("./examples/assets/")); 23 | let mut aseprite = emd.loader().aseprite("smiley.aseprite").unwrap(); 24 | aseprite.play_and_loop("smile").unwrap(); 25 | 26 | let sprite = emd.loader().sprite("bunny.png").unwrap(); 27 | let e = self 28 | .world 29 | .spawn((sprite.clone(), Camera::default(), Transform::default())); 30 | self.world.make_active_camera(e).unwrap(); 31 | 32 | self.world 33 | .spawn((aseprite.clone(), Transform::from_translation((32.0, 0.0)))); 34 | 35 | self.world 36 | .spawn((sprite.clone(), Transform::from_translation((-180.0, 0.0)))); 37 | self.world 38 | .spawn((aseprite.clone(), Transform::from_translation((-200.0, 0.0)))); 39 | self.world 40 | .spawn((sprite.clone(), Transform::from_translation((180.0, 0.0)))); 41 | self.world 42 | .spawn((aseprite.clone(), Transform::from_translation((200.0, 0.0)))); 43 | self.world 44 | .spawn((sprite.clone(), Transform::from_translation((0.0, 180.0)))); 45 | self.world 46 | .spawn((aseprite.clone(), Transform::from_translation((0.0, 200.0)))); 47 | self.world 48 | .spawn((sprite.clone(), Transform::from_translation((0.0, -180.0)))); 49 | } 50 | 51 | fn update(&mut self, mut emd: Emerald) { 52 | let mut velocity = Vector2::new(0.0, 0.0); 53 | let speed = 100.0; 54 | let delta = emd.delta(); 55 | if emd.input().is_key_pressed(KeyCode::A) { 56 | velocity.x = -speed * delta; 57 | } else if emd.input().is_key_pressed(KeyCode::D) { 58 | velocity.x = speed * delta; 59 | } 60 | if emd.input().is_key_pressed(KeyCode::W) { 61 | velocity.y = speed * delta; 62 | } else if emd.input().is_key_pressed(KeyCode::S) { 63 | velocity.y = -speed * delta; 64 | } 65 | 66 | for (_, transform) in self 67 | .world 68 | .query::<&mut Transform>() 69 | .with::<&Camera>() 70 | .iter() 71 | { 72 | transform.translation.x += velocity.x; 73 | transform.translation.y += velocity.y; 74 | } 75 | 76 | aseprite_update_system(&mut self.world, delta); 77 | } 78 | 79 | fn draw(&mut self, mut emd: Emerald) { 80 | emd.graphics().begin().unwrap(); 81 | emd.graphics().draw_world(&mut self.world).unwrap(); 82 | emd.graphics().render().unwrap(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /emerald/examples/mouse.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ 2 | rendering::components::{ColorRect, Sprite}, 3 | *, 4 | }; 5 | 6 | pub fn main() { 7 | let game = MouseExample { 8 | rect: ColorRect::new(BLACK, 0, 0), 9 | transform: Transform::default(), 10 | background: ColorRect::new(BLACK, 0, 0), 11 | screen_center: Translation::default(), 12 | world: World::new(), 13 | }; 14 | emerald::start(Box::new(game), GameSettings::default()) 15 | } 16 | 17 | pub struct MouseExample { 18 | rect: ColorRect, 19 | transform: Transform, 20 | background: ColorRect, 21 | screen_center: Translation, 22 | world: World, 23 | } 24 | 25 | impl Game for MouseExample { 26 | fn initialize(&mut self, mut emd: Emerald) { 27 | emd.set_asset_folder_root(String::from("./examples/assets/")); 28 | 29 | if let Ok(sprite) = emd.loader().sprite("bunny.png") { 30 | self.world 31 | .spawn((sprite, Transform::from_translation((16.0, 16.0)))); 32 | } 33 | 34 | emd.touches_to_mouse(true); 35 | } 36 | 37 | fn update(&mut self, mut emd: Emerald) { 38 | let mouse = emd.input().mouse(); 39 | let (width, height) = emd.screen_size(); 40 | let translation = screen_translation_to_world_translation( 41 | (width, height), 42 | &mouse.translation, 43 | &self.world, 44 | ); 45 | self.transform.translation.x = translation.x; 46 | self.transform.translation.y = translation.y; 47 | let mut color = Color::new(0, 0, 0, 255); 48 | let mut flash = Color::new(128, 128, 128, 128); 49 | 50 | if mouse.left.is_pressed { 51 | color.r = 255; 52 | } 53 | if mouse.left.is_just_pressed() { 54 | flash.r = 192; 55 | } 56 | 57 | if mouse.middle.is_pressed { 58 | color.g = 255; 59 | } 60 | if mouse.middle.is_just_pressed() { 61 | flash.g = 192; 62 | } 63 | 64 | if mouse.right.is_pressed { 65 | color.b = 255; 66 | } 67 | if mouse.right.is_just_pressed() { 68 | flash.b = 192; 69 | } 70 | 71 | println!("Color should be {:?}", color); 72 | 73 | self.rect = ColorRect::new(color, 40, 40); 74 | 75 | self.screen_center = Translation::new(width as f32 / 2.0, height as f32 / 2.0); 76 | self.background = ColorRect::new(flash, width as u32, height as u32); 77 | 78 | for (_, (transform, _)) in self.world.query::<(&mut Transform, &mut Sprite)>().iter() { 79 | // It's important to convert coordinates to the physical world space. 80 | *transform = self.transform; 81 | } 82 | } 83 | 84 | fn draw(&mut self, mut emd: Emerald) { 85 | emd.graphics().begin().unwrap(); 86 | 87 | emd.graphics() 88 | .draw_color_rect(&self.background, &Transform::default()) 89 | .ok(); 90 | emd.graphics() 91 | .draw_color_rect(&self.rect, &self.transform) 92 | .ok(); 93 | emd.graphics().draw_world(&mut self.world).unwrap(); 94 | emd.graphics().render().unwrap(); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /emerald/examples/sprites.rs: -------------------------------------------------------------------------------- 1 | use emerald::{rendering::components::Sprite, *}; 2 | 3 | pub fn main() { 4 | let mut settings = GameSettings::default(); 5 | settings.render_settings.resolution = (320, 180); 6 | emerald::start( 7 | Box::new(SpritesExample { 8 | world: World::new(), 9 | }), 10 | settings, 11 | ) 12 | } 13 | 14 | pub struct SpritesExample { 15 | world: World, 16 | } 17 | impl Game for SpritesExample { 18 | fn initialize(&mut self, mut emd: Emerald) { 19 | emd.set_asset_folder_root(String::from("./examples/assets/")); 20 | let sprite = emd.loader().sprite("bunny.png").unwrap(); 21 | self.world.spawn((sprite, Transform::default())); 22 | let sprite = emd.loader().sprite("smiley.png").unwrap(); 23 | self.world 24 | .spawn((sprite, Transform::from_translation((-32.0, 0.0)))); 25 | } 26 | 27 | fn update(&mut self, mut emd: Emerald) { 28 | let mouse = emd.input().mouse(); 29 | let screen_size = emd.screen_size(); 30 | let mouse_position = screen_translation_to_world_translation( 31 | (screen_size.0 as u32, screen_size.1 as u32), 32 | &mouse.translation, 33 | &mut self.world, 34 | ); 35 | 36 | // Spawn with left mouse 37 | if mouse.left.is_just_pressed() { 38 | let mut sprite = emd.loader().sprite("bunny.png").unwrap(); 39 | sprite.offset = Vector2::new(-10.0, 0.0); 40 | 41 | let mut transform = Transform::default(); 42 | transform.translation = mouse_position; 43 | self.world.spawn((sprite, transform)); 44 | } 45 | 46 | if emd.input().is_key_just_pressed(KeyCode::A) { 47 | for (_, sprite) in self.world.query::<&mut Sprite>().iter() { 48 | sprite.scale *= 0.5; 49 | } 50 | } 51 | if emd.input().is_key_just_pressed(KeyCode::S) { 52 | for (_, sprite) in self.world.query::<&mut Sprite>().iter() { 53 | sprite.scale *= 2.0; 54 | } 55 | } 56 | if emd.input().is_key_pressed(KeyCode::W) { 57 | for (_, sprite) in self.world.query::<&mut Sprite>().iter() { 58 | sprite.rotation += 0.1; 59 | } 60 | } 61 | 62 | if emd.input().is_key_pressed(KeyCode::Q) { 63 | for (_, sprite) in self.world.query::<&mut Sprite>().iter() { 64 | sprite.rotation -= 0.1; 65 | } 66 | } 67 | 68 | // move to mouse position 69 | if mouse.right.is_pressed { 70 | let speed = 0.1 * emd.delta(); 71 | 72 | for (_, (transform, _sprite)) in 73 | self.world.query::<(&mut Transform, &mut Sprite)>().iter() 74 | { 75 | transform.translation.x += (mouse_position.x - transform.translation.x) * speed; 76 | transform.translation.y += (mouse_position.y - transform.translation.y) * speed; 77 | } 78 | } 79 | } 80 | 81 | fn draw(&mut self, mut emd: Emerald) { 82 | emd.graphics().begin().unwrap(); 83 | emd.graphics().draw_world(&mut self.world).unwrap(); 84 | emd.graphics().render().unwrap(); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /emerald/examples/render_to_texture.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ 2 | render_settings::RenderSettings, 3 | rendering::components::{ColorRect, Sprite}, 4 | texture::TextureKey, 5 | *, 6 | }; 7 | 8 | const RES_WIDTH: usize = 320; 9 | const RES_HEIGHT: usize = 160; 10 | 11 | pub fn main() { 12 | let mut settings = GameSettings::default(); 13 | let render_settings = RenderSettings { 14 | resolution: (320 * 2, 160 * 2), 15 | ..Default::default() 16 | }; 17 | settings.render_settings = render_settings; 18 | 19 | emerald::start( 20 | Box::new(MyGame { 21 | pos: Transform::default(), 22 | scale: 1.0, 23 | render_texture: None, 24 | }), 25 | settings, 26 | ) 27 | } 28 | 29 | pub struct MyGame { 30 | pos: Transform, 31 | scale: f32, 32 | render_texture: Option, 33 | } 34 | impl Game for MyGame { 35 | fn initialize(&mut self, mut emd: Emerald) { 36 | emd.set_asset_folder_root(String::from("./examples/assets/")); 37 | 38 | self.render_texture = Some( 39 | emd.loader() 40 | .render_texture(RES_WIDTH as usize, RES_HEIGHT as usize) 41 | .unwrap(), 42 | ); 43 | } 44 | 45 | fn update(&mut self, mut emd: Emerald) { 46 | let delta = emd.delta(); 47 | let speed = 150.0; 48 | let mut input = emd.input(); 49 | 50 | if input.is_key_pressed(KeyCode::Left) { 51 | self.pos.translation.x -= speed * delta; 52 | } 53 | 54 | if input.is_key_pressed(KeyCode::Right) { 55 | self.pos.translation.x += speed * delta; 56 | } 57 | 58 | if input.is_key_pressed(KeyCode::Up) { 59 | self.pos.translation.y += speed * delta; 60 | } 61 | 62 | if input.is_key_pressed(KeyCode::Down) { 63 | self.pos.translation.y -= speed * delta; 64 | } 65 | 66 | if input.is_key_just_pressed(KeyCode::A) { 67 | self.scale *= 0.5; 68 | } 69 | 70 | if input.is_key_just_pressed(KeyCode::S) { 71 | self.scale *= 2.0; 72 | } 73 | } 74 | 75 | fn draw(&mut self, mut emd: Emerald) { 76 | emd.graphics() 77 | .begin_texture(&self.render_texture.as_ref().unwrap()) 78 | .unwrap(); 79 | 80 | let rabbit = emd.loader().sprite("bunny.png").unwrap(); 81 | emd.graphics() 82 | .draw_color_rect( 83 | &ColorRect::new(WHITE, 500 * 500, 500 * 500), 84 | &Transform::default(), 85 | ) 86 | .unwrap(); 87 | emd.graphics() 88 | .draw_sprite(&rabbit, &Transform::default()) 89 | .unwrap(); 90 | 91 | emd.graphics().render_texture().unwrap(); 92 | 93 | let mut screen_sprite = Sprite::from_texture(self.render_texture.as_ref().unwrap().clone()); 94 | screen_sprite.centered = true; 95 | screen_sprite.scale.x = self.scale; 96 | screen_sprite.scale.y = self.scale; 97 | 98 | emd.graphics().begin().unwrap(); 99 | emd.graphics() 100 | .draw_sprite(&screen_sprite, &self.pos) 101 | .unwrap(); 102 | emd.graphics().render().unwrap(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /emerald/examples/audio.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ 2 | audio::components::sound_player::SoundPlayer, render_settings::RenderSettings, 3 | rendering::components::Label, *, 4 | }; 5 | 6 | /// Music found from https://opengameart.org/content/5-chiptunes-action 7 | pub fn main() { 8 | let mut settings = GameSettings::default(); 9 | let render_settings = RenderSettings { 10 | ..Default::default() 11 | }; 12 | settings.render_settings = render_settings; 13 | emerald::start( 14 | Box::new(Example { 15 | world: World::new(), 16 | }), 17 | settings, 18 | ) 19 | } 20 | 21 | pub struct Example { 22 | world: World, 23 | } 24 | impl Game for Example { 25 | fn initialize(&mut self, mut emd: Emerald) { 26 | emd.set_asset_folder_root(String::from("./examples/assets/")); 27 | 28 | let mut sound_player = SoundPlayer::new("sfx"); 29 | sound_player.add_sound("test", emd.loader().sound("test_sound.wav").unwrap()); 30 | self.world.spawn((sound_player,)); 31 | } 32 | 33 | fn update(&mut self, mut emd: Emerald) { 34 | let volume = emd.audio().mixer("test").unwrap().get_volume().unwrap(); 35 | if emd.input().is_key_just_pressed(KeyCode::A) { 36 | emd.audio() 37 | .mixer("test") 38 | .unwrap() 39 | .set_volume(volume - 0.1) 40 | .unwrap(); 41 | emd.audio() 42 | .mixer("test2") 43 | .unwrap() 44 | .set_volume(volume - 0.1) 45 | .unwrap(); 46 | } else if emd.input().is_key_just_pressed(KeyCode::D) { 47 | emd.audio() 48 | .mixer("test") 49 | .unwrap() 50 | .set_volume(volume + 0.1) 51 | .unwrap(); 52 | emd.audio() 53 | .mixer("test2") 54 | .unwrap() 55 | .set_volume(volume + 0.1) 56 | .unwrap(); 57 | } 58 | 59 | if emd.input().is_key_just_pressed(KeyCode::C) { 60 | for (_, player) in self.world.query::<&SoundPlayer>().iter() { 61 | player.play(&mut emd, "test").unwrap(); 62 | } 63 | } 64 | 65 | if emd.input().is_key_just_pressed(KeyCode::Space) { 66 | let snd = emd.loader().sound("test_music.wav").unwrap(); 67 | emd.audio() 68 | .mixer("test") 69 | .unwrap() 70 | .play_and_loop(&snd) 71 | .unwrap(); 72 | } 73 | 74 | if emd.input().is_key_just_pressed(KeyCode::Z) { 75 | for _ in 0..10 { 76 | let snd = emd.loader().sound("test_sound.wav").unwrap(); 77 | emd.audio().mixer("test2").unwrap().play(&snd).unwrap(); 78 | } 79 | } 80 | } 81 | 82 | fn draw(&mut self, mut emd: Emerald) { 83 | emd.graphics().begin().unwrap(); 84 | let font = emd.loader().font("Roboto-Light.ttf", 48).unwrap(); 85 | let volume = emd.audio().mixer("test").unwrap().get_volume().unwrap(); 86 | 87 | let volume_label = Label::new(format!("Volume: {:05.2}", volume), font, 48); 88 | emd.graphics() 89 | .draw_label(&volume_label, &Transform::from_translation((240.0, 180.0))) 90 | .unwrap(); 91 | 92 | emd.graphics().render().unwrap(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /assets/windows.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 36 | 37 | 60 | 62 | 63 | 65 | image/svg+xml 66 | 68 | 69 | 70 | 71 | 72 | 77 | 82 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | on: 2 | pull_request: 3 | branches: 4 | - master 5 | - development 6 | 7 | name: Build 8 | 9 | jobs: 10 | build: 11 | name: X Compile 12 | runs-on: ${{ matrix.config.os }} 13 | strategy: 14 | fail-fast: false 15 | matrix: 16 | config: 17 | - { os: ubuntu-latest, target: 'x86_64-pc-windows-gnu' } 18 | - { os: ubuntu-latest, target: 'wasm32-unknown-unknown' } 19 | - { os: macos-latest, target: 'aarch64-apple-ios' } 20 | - { os: macos-latest, target: 'x86_64-apple-ios' } 21 | include: 22 | - os: ubuntu-latest 23 | packages: libx11-dev libxi-dev libgl1-mesa-dev gcc-mingw-w64 libsdl2-dev 24 | 25 | steps: 26 | - uses: actions/checkout@v2 27 | - name: Install packages (Linux) 28 | if: runner.os == 'Linux' 29 | run: | 30 | sudo apt-get update 31 | sudo apt-get -yq --no-install-suggests --no-install-recommends install ${{ matrix.packages }} 32 | - uses: actions-rs/toolchain@v1 33 | with: 34 | toolchain: stable 35 | target: ${{ matrix.config.target }} 36 | override: true 37 | - name: Workaround MinGW issue # https://github.com/rust-lang/rust/issues/47048 38 | if: runner.os == 'Linux' && matrix.config.target == 'x86_64-pc-windows-gnu' 39 | run: | 40 | sudo cp /usr/x86_64-w64-mingw32/lib/dllcrt2.o ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-pc-windows-gnu/lib/dllcrt2.o 41 | sudo cp /usr/x86_64-w64-mingw32/lib/crt2.o ~/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/x86_64-pc-windows-gnu/lib/crt2.o 42 | echo "[target.x86_64-pc-windows-gnu]" >> ~/.cargo/config 43 | echo "linker = \"/usr/bin/x86_64-w64-mingw32-gcc\"" >> ~/.cargo/config 44 | - uses: actions-rs/cargo@v1 45 | with: 46 | command: build 47 | args: --all-targets --target=${{ matrix.config.target }} 48 | 49 | 50 | test: 51 | name: Test 52 | runs-on: ${{ matrix.config.os }} 53 | strategy: 54 | fail-fast: false 55 | matrix: 56 | config: 57 | - { os: ubuntu-latest, target: 'x86_64-unknown-linux-gnu' } 58 | - { os: macos-latest, target: 'x86_64-apple-darwin' } 59 | - { os: windows-latest, target: 'x86_64-pc-windows-msvc' } 60 | include: 61 | - os: ubuntu-latest 62 | packages: libx11-dev libxi-dev libgl1-mesa-dev gcc-mingw-w64 libsdl2-dev 63 | 64 | steps: 65 | - uses: actions/checkout@v2 66 | - name: Install packages (Linux) 67 | if: runner.os == 'Linux' 68 | run: | 69 | sudo apt-get update 70 | sudo apt-get -yq --no-install-suggests --no-install-recommends install ${{ matrix.packages }} 71 | - uses: actions-rs/toolchain@v1 72 | with: 73 | toolchain: stable 74 | target: ${{ matrix.config.target }} 75 | override: true 76 | - uses: actions-rs/cargo@v1 77 | with: 78 | command: test 79 | args: --lib --all-targets --target=${{ matrix.config.target }} 80 | 81 | 82 | android: 83 | name: Android 84 | runs-on: ubuntu-latest 85 | steps: 86 | - uses: actions/checkout@v2 87 | - name: cargo apk 88 | run: | 89 | rustup target add aarch64-linux-android 90 | cargo update 91 | cargo install cargo-apk 92 | cd emerald 93 | cargo apk build --lib -p emerald -------------------------------------------------------------------------------- /emerald/examples/ent.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ent::EntLoadConfig, rendering::components::aseprite_update_system, *}; 2 | use serde::Deserialize; 3 | 4 | pub fn main() { 5 | emerald::start( 6 | Box::new(EntLoadingExample { 7 | world: World::new(), 8 | }), 9 | GameSettings::default(), 10 | ) 11 | } 12 | 13 | struct PlayerData { 14 | pub name: String, 15 | pub max_hp: i64, 16 | } 17 | 18 | #[derive(Deserialize)] 19 | #[serde(crate = "emerald::serde")] // must be below the derive attribute 20 | struct CustomWorldResource { 21 | pub some_custom_data: usize, 22 | } 23 | 24 | fn world_resource_loader( 25 | loader: &mut AssetLoader, 26 | world: &mut World, 27 | toml_value: toml::Value, 28 | toml_key: String, 29 | ) -> Result<(), EmeraldError> { 30 | match toml_key.as_str() { 31 | "my_custom_resource" => { 32 | let resource: CustomWorldResource = emerald::toml::from_str(&toml_value.to_string())?; 33 | world.resources().insert(resource); 34 | } 35 | _ => {} 36 | } 37 | 38 | Ok(()) 39 | } 40 | 41 | fn custom_component_loader( 42 | loader: &mut AssetLoader, 43 | entity: Entity, 44 | world: &mut World, 45 | toml_value: toml::Value, 46 | toml_key: String, 47 | ) -> Result<(), EmeraldError> { 48 | // We want to match here because in a real game we'll probably have many custom components 49 | match toml_key.as_str() { 50 | "my_custom_player_component" => { 51 | let name = toml_value 52 | .get("name") 53 | .unwrap() 54 | .as_str() 55 | .unwrap() 56 | .to_string(); 57 | let max_hp = toml_value.get("max_hp").unwrap().as_integer().unwrap(); 58 | 59 | world 60 | .insert_one(entity, PlayerData { max_hp, name }) 61 | .unwrap(); 62 | } 63 | _ => {} 64 | } 65 | 66 | Ok(()) 67 | } 68 | 69 | pub struct EntLoadingExample { 70 | world: World, 71 | } 72 | impl Game for EntLoadingExample { 73 | fn initialize(&mut self, mut emd: Emerald) { 74 | emd.set_asset_folder_root("./examples/assets/".to_string()); 75 | emd.loader() 76 | .set_custom_component_loader(custom_component_loader); 77 | emd.loader() 78 | .set_world_resource_loader(world_resource_loader); 79 | self.world = emd.loader().world("example.wrld").unwrap(); 80 | 81 | let entity = emd 82 | .loader() 83 | .ent(&mut self.world, "bunny.ent", Transform::default()) 84 | .unwrap(); 85 | 86 | // assert that we've successfully loaded a user defined component 87 | assert!(self.world.get::<&PlayerData>(entity).is_ok()); 88 | assert_eq!( 89 | self.world 90 | .resources() 91 | .get::() 92 | .unwrap() 93 | .some_custom_data, 94 | 10 95 | ); 96 | } 97 | 98 | fn update(&mut self, emd: Emerald) { 99 | aseprite_update_system(&mut self.world, emd.delta()); 100 | } 101 | 102 | fn draw(&mut self, mut emd: Emerald<'_>) { 103 | emd.graphics().begin().unwrap(); 104 | emd.graphics().draw_world(&mut self.world).unwrap(); 105 | emd.graphics() 106 | .draw_colliders(&mut self.world, Color::new(255, 0, 0, 100)) 107 | .unwrap(); 108 | emd.graphics().render().unwrap(); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /emerald/src/profiling/profile_cache.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{HashMap, VecDeque}; 2 | 3 | use crate::EmeraldError; 4 | 5 | use super::{ 6 | profile_settings::ProfileSettings, 7 | profiler::{ProfileFloat, ProfileName}, 8 | }; 9 | 10 | pub(crate) struct ProfileCache { 11 | settings: ProfileSettings, 12 | 13 | profiles: HashMap, 14 | 15 | /// Map of ProfileName to (start, has_ended). 16 | in_progress_profiles: HashMap, 17 | } 18 | impl ProfileCache { 19 | pub(crate) fn new(settings: ProfileSettings) -> Self { 20 | Self { 21 | settings, 22 | profiles: HashMap::new(), 23 | in_progress_profiles: HashMap::new(), 24 | } 25 | } 26 | 27 | pub fn start_frame>( 28 | &mut self, 29 | profile_name: T, 30 | now: ProfileFloat, 31 | ) -> Result<(), EmeraldError> { 32 | let profile_name: ProfileName = profile_name.into(); 33 | if self.in_progress_profiles.contains_key(&profile_name) { 34 | return Err(EmeraldError::new(format!( 35 | "Profile Error: {} has already been started.", 36 | profile_name 37 | ))); 38 | } 39 | 40 | if !self.profiles.contains_key(&profile_name) { 41 | self.profiles 42 | .insert(profile_name.clone(), Profile::new(&self.settings)); 43 | } 44 | 45 | self.in_progress_profiles.insert(profile_name, (now, false)); 46 | 47 | Ok(()) 48 | } 49 | 50 | pub fn finish_frame>( 51 | &mut self, 52 | profile_name: T, 53 | now: ProfileFloat, 54 | ) -> Result { 55 | let profile_name: ProfileName = profile_name.into(); 56 | if let Some(in_progress_profile) = self.in_progress_profiles.get_mut(&profile_name) { 57 | if in_progress_profile.1 { 58 | return Err(EmeraldError::new(format!( 59 | "Profile Error: {} has already ended.", 60 | profile_name 61 | ))); 62 | } 63 | 64 | in_progress_profile.1 = true; 65 | let new_frame = (in_progress_profile.0, now); 66 | 67 | if let Some(profile) = self.profiles.get_mut(&profile_name) { 68 | profile.add_frame(new_frame, &self.settings); 69 | self.in_progress_profiles.remove(&profile_name); 70 | return Ok(new_frame.1 - new_frame.0); 71 | } else { 72 | return Err(EmeraldError::new(format!( 73 | "Profile Error: Profile {} does not exist.", 74 | profile_name 75 | ))); 76 | } 77 | } 78 | 79 | Err(EmeraldError::new(format!( 80 | "Profile Error: {} was never started.", 81 | profile_name 82 | ))) 83 | } 84 | } 85 | 86 | #[derive(Debug, Clone)] 87 | pub(crate) struct Profile { 88 | /// (start, finish) 89 | pub frames: VecDeque<(ProfileFloat, ProfileFloat)>, 90 | } 91 | impl Profile { 92 | pub fn new(settings: &ProfileSettings) -> Self { 93 | Self { 94 | frames: VecDeque::with_capacity(settings.frame_limit), 95 | } 96 | } 97 | 98 | pub fn add_frame( 99 | &mut self, 100 | new_frame: (ProfileFloat, ProfileFloat), 101 | settings: &ProfileSettings, 102 | ) { 103 | self.frames.push_front(new_frame); 104 | 105 | if self.frames.len() >= settings.frame_limit { 106 | self.frames.pop_back(); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /emerald/src/input.rs: -------------------------------------------------------------------------------- 1 | mod button_state; 2 | mod components; 3 | mod input_engine; 4 | mod input_handler; 5 | mod mouse_state; 6 | mod systems; 7 | mod touch_state; 8 | 9 | pub use button_state::*; 10 | pub use components::*; 11 | pub(crate) use input_engine::*; 12 | pub use input_handler::*; 13 | pub use mouse_state::*; 14 | pub use systems::*; 15 | pub use touch_state::*; 16 | 17 | use crate::{transform::Translation, World}; 18 | 19 | /// Returns a world translation equivalent to the given point on a given screen. 20 | pub fn screen_translation_to_world_translation( 21 | screen_size: (u32, u32), 22 | screen_translation: &Translation, 23 | world: &World, 24 | ) -> Translation { 25 | let camera_pos = world 26 | .get_active_camera() 27 | .and_then(|id| world.get::<&Translation>(id.clone()).ok()) 28 | .map(|pos| *pos) 29 | .unwrap_or_default(); 30 | 31 | // TODO(bombfuse): take the camera zoom level into account when translating 32 | let screen_size = Translation::new(screen_size.0 as f32, screen_size.1 as f32); 33 | let normalized_screen_translation = Translation::new( 34 | screen_translation.x - screen_size.x / 2.0, 35 | screen_size.y - screen_translation.y - screen_size.y / 2.0, 36 | ); 37 | 38 | camera_pos + normalized_screen_translation 39 | } 40 | 41 | /// Describes touch-screen input state. 42 | #[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)] 43 | #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] 44 | pub enum TouchPhase { 45 | Started, 46 | Moved, 47 | Ended, 48 | Cancelled, 49 | } 50 | 51 | #[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)] 52 | pub enum MouseButton { 53 | Right, 54 | Left, 55 | Middle, 56 | Unknown, 57 | } 58 | 59 | #[derive(Debug, Copy, Clone)] 60 | pub struct Touch { 61 | pub id: u32, 62 | pub x: f32, 63 | pub y: f32, 64 | } 65 | 66 | #[derive(Debug, Copy, Clone, PartialEq, Hash, Eq)] 67 | pub enum KeyCode { 68 | Space, 69 | Apostrophe, 70 | Comma, 71 | Minus, 72 | Period, 73 | Slash, 74 | Key0, 75 | Key1, 76 | Key2, 77 | Key3, 78 | Key4, 79 | Key5, 80 | Key6, 81 | Key7, 82 | Key8, 83 | Key9, 84 | Semicolon, 85 | Equal, 86 | A, 87 | B, 88 | C, 89 | D, 90 | E, 91 | F, 92 | G, 93 | H, 94 | I, 95 | J, 96 | K, 97 | L, 98 | M, 99 | N, 100 | O, 101 | P, 102 | Q, 103 | R, 104 | S, 105 | T, 106 | U, 107 | V, 108 | W, 109 | X, 110 | Y, 111 | Z, 112 | LeftBracket, 113 | Backslash, 114 | RightBracket, 115 | GraveAccent, 116 | Escape, 117 | Enter, 118 | Tab, 119 | Backspace, 120 | Insert, 121 | Delete, 122 | Right, 123 | Left, 124 | Down, 125 | Up, 126 | PageUp, 127 | PageDown, 128 | Home, 129 | End, 130 | CapsLock, 131 | ScrollLock, 132 | NumLock, 133 | Pause, 134 | F1, 135 | F2, 136 | F3, 137 | F4, 138 | F5, 139 | F6, 140 | F7, 141 | F8, 142 | F9, 143 | F10, 144 | F11, 145 | F12, 146 | F13, 147 | F14, 148 | F15, 149 | F16, 150 | F17, 151 | F18, 152 | F19, 153 | F20, 154 | F21, 155 | F22, 156 | F23, 157 | F24, 158 | Kp0, 159 | Kp1, 160 | Kp2, 161 | Kp3, 162 | Kp4, 163 | Kp5, 164 | Kp6, 165 | Kp7, 166 | Kp8, 167 | Kp9, 168 | KpDecimal, 169 | KpDivide, 170 | KpMultiply, 171 | KpSubtract, 172 | KpAdd, 173 | KpEnter, 174 | KpEqual, 175 | LeftShift, 176 | LeftControl, 177 | LeftAlt, 178 | RightShift, 179 | RightControl, 180 | RightAlt, 181 | Unknown, 182 | } 183 | -------------------------------------------------------------------------------- /assets/webassembly.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 36 | 37 | 60 | 62 | 63 | 65 | image/svg+xml 66 | 68 | 69 | 70 | 71 | 72 | 77 | 82 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /emerald/src/input/systems/ui_button.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::{ 4 | screen_translation_to_world_translation, 5 | texture::Texture, 6 | transform::{Transform, Translation}, 7 | Emerald, TouchState, UIButton, World, 8 | }; 9 | 10 | /// Updates the status of UI Buttons. 11 | /// Presses the button if the user has pressed it, etc... 12 | pub fn ui_button_system(emd: &mut Emerald<'_>, world: &mut World) { 13 | let mouse = emd.input().mouse(); 14 | let touches = emd.input().touches().clone(); 15 | let screen_size = emd.screen_size(); 16 | let mouse_position = screen_translation_to_world_translation( 17 | (screen_size.0 as u32, screen_size.1 as u32), 18 | &mouse.translation, 19 | world, 20 | ); 21 | 22 | let touch_world_positions: HashMap = touches 23 | .iter() 24 | .map(|(id, touch_state)| { 25 | let sc = (screen_size.0 as u32, screen_size.1 as u32); 26 | ( 27 | *id, 28 | screen_translation_to_world_translation(sc, &touch_state.translation, world), 29 | ) 30 | }) 31 | .collect(); 32 | 33 | for (_, (ui_button, transform)) in world.query::<(&mut UIButton, &Transform)>().iter() { 34 | let button_check = 35 | is_translation_inside_button(emd, &ui_button, &transform, &mouse_position) 36 | || check_touches_overlap_button( 37 | emd, 38 | &touches, 39 | &touch_world_positions, 40 | &ui_button, 41 | &transform, 42 | ); 43 | 44 | if button_check { 45 | let press = mouse.left.is_pressed 46 | || touches 47 | .iter() 48 | .any(|(_key, touch_state)| touch_state.is_pressed()); 49 | 50 | if press { 51 | ui_button.press(); 52 | } else { 53 | ui_button.release(); 54 | } 55 | } else { 56 | // Wipe button state if mouse is not in button 57 | ui_button.reset(); 58 | } 59 | } 60 | } 61 | 62 | fn check_touches_overlap_button( 63 | emd: &mut Emerald<'_>, 64 | touches: &HashMap, 65 | touch_world_positions: &HashMap, 66 | ui_button: &UIButton, 67 | ui_button_transform: &Transform, 68 | ) -> bool { 69 | touches.iter().any(|(id, _touch)| { 70 | let mut is_inside = false; 71 | 72 | if let Some(position) = touch_world_positions.get(id) { 73 | is_inside = 74 | is_translation_inside_button(emd, &ui_button, &ui_button_transform, position); 75 | } 76 | 77 | is_inside 78 | }) 79 | } 80 | 81 | // TODO: take into account the scale and rotation of the button. 82 | fn is_translation_inside_button( 83 | emd: &mut Emerald<'_>, 84 | ui_button: &UIButton, 85 | ui_button_transform: &Transform, 86 | translation: &Translation, 87 | ) -> bool { 88 | let mut is_inside = false; 89 | 90 | let texture_key = ui_button.current_texture(); 91 | 92 | if let Some(texture) = emd 93 | .asset_engine 94 | .get_asset::(&texture_key.asset_key.asset_id) 95 | { 96 | if (translation.x >= ui_button_transform.translation.x - texture.size.width as f32 / 2.0) 97 | && (translation.x 98 | <= ui_button_transform.translation.x + texture.size.width as f32 / 2.0) 99 | { 100 | if (translation.y 101 | >= ui_button_transform.translation.y - texture.size.height as f32 / 2.0) 102 | && (translation.y 103 | <= ui_button_transform.translation.y + texture.size.height as f32 / 2.0) 104 | { 105 | is_inside = true; 106 | } 107 | } 108 | } 109 | 110 | is_inside 111 | } 112 | -------------------------------------------------------------------------------- /emerald/examples/physics_groups.rs: -------------------------------------------------------------------------------- 1 | use emerald::world::physics::InteractionGroups; 2 | use emerald::{render_settings::RenderSettings, rendering::components::ColorRect, *}; 3 | const RES_WIDTH: f32 = 640.0; 4 | const RES_HEIGHT: f32 = 480.0; 5 | 6 | pub fn main() { 7 | let mut settings = GameSettings::default(); 8 | let render_settings = RenderSettings { 9 | resolution: (RES_WIDTH as u32, RES_HEIGHT as u32), 10 | ..Default::default() 11 | }; 12 | settings.render_settings = render_settings; 13 | emerald::start( 14 | Box::new(PhysicsGroupsExample { 15 | e1: None, 16 | e2: None, 17 | e3: None, 18 | world: World::new(), 19 | }), 20 | settings, 21 | ) 22 | } 23 | 24 | const GROUP_ONE: InteractionGroups = InteractionGroups::new(Group::GROUP_1, Group::GROUP_1); 25 | const GROUP_TWO: InteractionGroups = InteractionGroups::new(Group::GROUP_2, Group::GROUP_2); 26 | 27 | #[derive(Clone, Debug)] 28 | pub struct Velocity { 29 | pub dx: f32, 30 | pub dy: f32, 31 | } 32 | 33 | #[derive(Clone, Debug)] 34 | pub struct Controller {} 35 | 36 | pub struct PhysicsGroupsExample { 37 | e1: Option, 38 | e2: Option, 39 | e3: Option, 40 | world: World, 41 | } 42 | impl Game for PhysicsGroupsExample { 43 | fn initialize(&mut self, mut emd: Emerald) { 44 | emd.set_asset_folder_root(String::from("./examples/assets/")); 45 | 46 | let (entity1, body1) = self 47 | .world 48 | .spawn_with_body( 49 | ( 50 | Transform::from_translation((0.0, 40.0)), 51 | ColorRect::new(Color::new(0, 0, 255, 255), 32, 16), 52 | ), 53 | RigidBodyBuilder::dynamic(), 54 | ) 55 | .unwrap(); 56 | 57 | self.world.physics().build_collider( 58 | body1, 59 | ColliderBuilder::cuboid(16.0, 8.0).collision_groups(GROUP_ONE), 60 | ); 61 | 62 | self.world.physics().build_collider( 63 | body1, 64 | ColliderBuilder::cuboid(16.0, 8.0) 65 | .collision_groups(GROUP_ONE) 66 | .sensor(true), 67 | ); 68 | 69 | let (entity2, body2) = self 70 | .world 71 | .spawn_with_body( 72 | ( 73 | Transform::from_translation((0.0, 0.0)), 74 | ColorRect::new(Color::new(0, 255, 0, 255), 32, 16), 75 | ), 76 | RigidBodyBuilder::new_kinematic_position_based() 77 | .translation(Vector2::new(0.0, 0.0)), 78 | ) 79 | .unwrap(); 80 | 81 | self.world.physics().build_collider( 82 | body2, 83 | ColliderBuilder::cuboid(16.0, 8.0).collision_groups(GROUP_TWO), 84 | ); 85 | 86 | let (entity3, body3) = self 87 | .world 88 | .spawn_with_body( 89 | ( 90 | Transform::from_translation((0.0, 80.0)), 91 | ColorRect::new(Color::new(0, 255, 0, 255), 32, 16), 92 | ), 93 | RigidBodyBuilder::dynamic(), 94 | ) 95 | .unwrap(); 96 | 97 | self.world.physics().build_collider( 98 | body3, 99 | ColliderBuilder::cuboid(16.0, 8.0).collision_groups(GROUP_TWO), 100 | ); 101 | 102 | self.e1 = Some(entity1); 103 | self.e2 = Some(entity2); 104 | self.e3 = Some(entity3); 105 | 106 | self.world.physics().set_gravity(Vector2::new(0.0, -18.8)); 107 | } 108 | 109 | fn update(&mut self, emd: Emerald) { 110 | let delta = emd.delta(); 111 | self.world.physics().step(delta); 112 | } 113 | 114 | fn draw(&mut self, mut emd: Emerald) { 115 | emd.graphics().begin().unwrap(); 116 | emd.graphics().draw_world(&mut self.world).unwrap(); 117 | emd.graphics().render().unwrap(); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /emerald/src/rendering/shaders/textured_quad.rs: -------------------------------------------------------------------------------- 1 | use rapier2d::na::Matrix4; 2 | 3 | // lib.rs 4 | #[repr(C)] 5 | #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] 6 | pub(crate) struct Vertex { 7 | pub position: [f32; 2], 8 | pub tex_coords: [f32; 2], 9 | pub color: [f32; 4], 10 | } 11 | impl Vertex { 12 | pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { 13 | wgpu::VertexBufferLayout { 14 | array_stride: std::mem::size_of::() as wgpu::BufferAddress, 15 | step_mode: wgpu::VertexStepMode::Vertex, 16 | attributes: &[ 17 | wgpu::VertexAttribute { 18 | offset: 0, 19 | shader_location: 0, 20 | format: wgpu::VertexFormat::Float32x2, 21 | }, 22 | wgpu::VertexAttribute { 23 | offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress, 24 | shader_location: 1, 25 | format: wgpu::VertexFormat::Float32x2, 26 | }, 27 | wgpu::VertexAttribute { 28 | offset: (std::mem::size_of::<[f32; 2]>() * 2) as wgpu::BufferAddress, 29 | shader_location: 2, 30 | format: wgpu::VertexFormat::Float32x4, 31 | }, 32 | ], 33 | } 34 | } 35 | } 36 | 37 | pub(crate) const VERTICES: &[Vertex] = &[ 38 | // Changed 39 | Vertex { 40 | position: [-1.0, 1.0], 41 | tex_coords: [0.0, 0.0], 42 | color: [0.0, 0.0, 0.0, 1.0], 43 | }, // A 44 | Vertex { 45 | position: [-1.0, -1.0], 46 | tex_coords: [0.0, 1.0], 47 | color: [0.0, 0.0, 0.0, 1.0], 48 | }, // B 49 | Vertex { 50 | position: [1.0, -1.0], 51 | tex_coords: [1.0, 1.0], 52 | color: [0.0, 0.0, 0.0, 1.0], 53 | }, // C 54 | Vertex { 55 | position: [1.0, 1.0], 56 | tex_coords: [1.0, 0.0], 57 | color: [0.0, 0.0, 0.0, 1.0], 58 | }, 59 | ]; 60 | 61 | pub(crate) const INDICES: &[u16] = &[0, 1, 2, 0, 2, 3]; 62 | 63 | pub(crate) struct Camera2D { 64 | pub view_height: u32, 65 | pub view_width: u32, 66 | } 67 | 68 | impl Camera2D { 69 | pub fn new(view_width: u32, view_height: u32) -> Self { 70 | Self { 71 | view_height, 72 | view_width, 73 | } 74 | } 75 | 76 | fn build_view_projection_matrix(&self) -> Matrix4 { 77 | // // 1. 78 | // let view = cgmath::Matrix4::look_at_rh(self.eye, self.target, self.up); 79 | // // 2. 80 | let proj = Matrix4::new_orthographic(-1.0, 1.0, -1.0, 1.0, 0.0, 100.0); 81 | 82 | // 3. 83 | return proj; 84 | } 85 | } 86 | 87 | // We need this for Rust to store our data correctly for the shaders 88 | #[repr(C)] 89 | // This is so we can store this in a buffer 90 | #[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)] 91 | pub(crate) struct CameraUniform { 92 | // We can't use cgmath with bytemuck directly so we'll have 93 | // to convert the Matrix4 into a 4x4 f32 array 94 | view_width: f32, 95 | view_height: f32, 96 | } 97 | 98 | impl CameraUniform { 99 | pub fn new(view_width: f32, view_height: f32) -> Self { 100 | Self { 101 | view_height, 102 | view_width, 103 | } 104 | } 105 | } 106 | 107 | // Credit(https://github.com/not-fl3/good-web-game/blob/master/src/graphics/image.rs#L129) 108 | // pub(crate) fn param_to_instance_transform( 109 | // rotation: f32, 110 | // scale: Vec2, 111 | // offset: Vec2, 112 | // dest: Vec2, 113 | // ) -> Mat4 { 114 | // let cosr = rotation.cos(); 115 | // let sinr = rotation.sin(); 116 | // let m00 = cosr * scale.x; 117 | // let m01 = -sinr * scale.y; 118 | // let m10 = sinr * scale.x; 119 | // let m11 = cosr * scale.y; 120 | // let m03 = offset.x * (1.0 - m00) - offset.y * m01 + dest.x; 121 | // let m13 = offset.y * (1.0 - m11) - offset.x * m10 + dest.y; 122 | 123 | // Mat4::from_cols( 124 | // Vec4::new(m00, m10, 0.0, 0.0), 125 | // Vec4::new(m01, m11, 0.0, 0.0), 126 | // Vec4::new(0.0, 0.0, 1.0, 0.0), 127 | // Vec4::new(m03, m13, 0.0, 1.0), 128 | // ) 129 | // } 130 | -------------------------------------------------------------------------------- /emerald/src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | pub use hecs::Entity; 4 | pub use rapier2d::na::Vector2; 5 | pub use rapier2d::na::Vector3; 6 | 7 | #[derive(Clone, Copy, Debug, Deserialize, Serialize)] 8 | pub struct Rectangle { 9 | pub x: f32, 10 | pub y: f32, 11 | pub width: f32, 12 | pub height: f32, 13 | } 14 | impl Rectangle { 15 | pub fn new(x: f32, y: f32, width: f32, height: f32) -> Self { 16 | Self { 17 | x, 18 | y, 19 | width, 20 | height, 21 | } 22 | } 23 | 24 | pub fn contains_point(&self, x: f32, y: f32) -> bool { 25 | x >= self.x && y >= self.y && x <= self.x + self.width && y <= self.y + self.height 26 | } 27 | 28 | /// Creates a `Rectangle` from its bottom-left point and its size. 29 | pub fn from_point_and_size( 30 | point: impl Into>, 31 | size: impl Into>, 32 | ) -> Self { 33 | let point = point.into(); 34 | let size = size.into(); 35 | 36 | Self { 37 | x: point.x, 38 | y: point.y, 39 | width: size.x, 40 | height: size.y, 41 | } 42 | } 43 | 44 | // Zeroed out rectangle. When a sprite uses a zeroed out rect, it draws the whole sprite. 45 | pub fn zeroed() -> Self { 46 | Rectangle::new(0.0, 0.0, 0.0, 0.0) 47 | } 48 | 49 | pub fn is_zero_sized(self) -> bool { 50 | self.width == 0.0 && self.height == 0.0 51 | } 52 | 53 | /// An alias for self.x 54 | #[inline] 55 | pub fn left(&self) -> f32 { 56 | self.x 57 | } 58 | 59 | #[inline] 60 | pub fn right(&self) -> f32 { 61 | self.x + self.width 62 | } 63 | 64 | /// An alias for self.y 65 | #[inline] 66 | pub fn bottom(&self) -> f32 { 67 | self.y 68 | } 69 | 70 | #[inline] 71 | pub fn top(&self) -> f32 { 72 | self.y + self.height 73 | } 74 | 75 | #[inline] 76 | pub fn bottom_left(&self) -> Vector2 { 77 | Vector2::new(self.left(), self.bottom()) 78 | } 79 | 80 | #[inline] 81 | pub fn size(&self) -> crate::Vector2 { 82 | Vector2::new(self.width, self.height) 83 | } 84 | 85 | #[inline] 86 | pub fn center(&self) -> crate::Vector2 { 87 | self.bottom_left() + self.size() / 2.0 88 | } 89 | 90 | /// Whether or not the given rectangle and this rectangle intersect 91 | pub fn intersects_with(&self, other: &Rectangle) -> bool { 92 | self.x < other.x + other.width 93 | && self.x + self.width > other.x 94 | && self.y < other.y + other.height 95 | && self.y + self.height > other.y 96 | } 97 | } 98 | 99 | #[cfg(test)] 100 | pub mod tests { 101 | use crate::Rectangle; 102 | 103 | #[test] 104 | fn rect_contains_point() { 105 | let rect = Rectangle::new(10.0, 10.0, 5.0, 5.0); 106 | 107 | assert!(rect.contains_point(10.0, 10.0)); 108 | assert!(rect.contains_point(15.0, 15.0)); 109 | assert!(rect.contains_point(12.0, 12.0)); 110 | 111 | assert!(!rect.contains_point(16.0, 12.0)); 112 | assert!(!rect.contains_point(0.0, 12.0)); 113 | } 114 | 115 | #[test] 116 | fn exact_overlap_intersects() { 117 | let rect_a = Rectangle::new(0.0, 0.0, 10.0, 10.0); 118 | let rect_b = rect_a.clone(); 119 | 120 | assert!(rect_a.intersects_with(&rect_b)); 121 | assert!(rect_b.intersects_with(&rect_a)); 122 | } 123 | 124 | #[test] 125 | fn rect_intersects_halfway() { 126 | let rect_a = Rectangle::new(0.0, 0.0, 10.0, 10.0); 127 | let mut rect_b = rect_a.clone(); 128 | rect_b.x = 5.0; 129 | rect_b.y = 5.0; 130 | 131 | assert!(rect_a.intersects_with(&rect_b)); 132 | assert!(rect_b.intersects_with(&rect_a)); 133 | } 134 | 135 | #[test] 136 | fn rect_intersects_halfway_negative() { 137 | let rect_a = Rectangle::new(0.0, 0.0, 10.0, 10.0); 138 | let mut rect_b = rect_a.clone(); 139 | rect_b.x = -5.0; 140 | rect_b.y = -5.0; 141 | 142 | assert!(rect_a.intersects_with(&rect_b)); 143 | assert!(rect_b.intersects_with(&rect_a)); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /emerald/src/world/ent/ent_rigid_body_loader.rs: -------------------------------------------------------------------------------- 1 | use hecs::Entity; 2 | use rapier2d::{ 3 | na::Vector2, 4 | parry::shape::Cuboid, 5 | prelude::{ColliderBuilder, ColliderHandle, RigidBodyBuilder, RigidBodyHandle, RigidBodyType}, 6 | }; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | use crate::{AssetLoader, EmeraldError, World}; 10 | 11 | use super::Vec2f32Schema; 12 | 13 | #[derive(Deserialize, Serialize)] 14 | pub(crate) struct EntColliderSchema { 15 | pub shape: String, 16 | pub translation: Option, 17 | pub half_width: Option, 18 | pub half_height: Option, 19 | pub radius: Option, 20 | pub sensor: Option, 21 | } 22 | 23 | #[derive(Deserialize, Serialize)] 24 | pub(crate) struct EntRigidBodySchema { 25 | pub body_type: String, 26 | pub colliders: Option>, 27 | pub lock_rotations: Option, 28 | pub lock_translations: Option, 29 | } 30 | 31 | fn load_ent_collider( 32 | rbh: RigidBodyHandle, 33 | world: &mut World, 34 | collider_schema: EntColliderSchema, 35 | ) -> Result { 36 | // Load collider attributes 37 | let mut builder = match collider_schema.shape.as_str() { 38 | "cuboid" => { 39 | if let (Some(half_width), Some(half_height)) = 40 | (collider_schema.half_width, collider_schema.half_height) 41 | { 42 | ColliderBuilder::cuboid(half_width, half_height) 43 | } else { 44 | return Err(EmeraldError::new( 45 | "Cuboid colliders expect both a half_width and half_height.", 46 | )); 47 | } 48 | } 49 | "ball" => { 50 | if let Some(radius) = collider_schema.radius { 51 | ColliderBuilder::ball(radius) 52 | } else { 53 | return Err(EmeraldError::new("Ball colliders require a radius")); 54 | } 55 | } 56 | _ => { 57 | return Err(EmeraldError::new( 58 | "Collider shape does not match an expected shape.", 59 | )) 60 | } 61 | }; 62 | 63 | if let Some(translation_value) = collider_schema.translation { 64 | builder = builder.translation(Vector2::new(translation_value.x, translation_value.y)); 65 | } 66 | 67 | if let Some(sensor) = collider_schema.sensor { 68 | builder = builder.sensor(sensor); 69 | } 70 | 71 | Ok(world.physics().build_collider(rbh, builder)) 72 | } 73 | 74 | pub(crate) fn load_ent_rigid_body<'a>( 75 | loader: &mut AssetLoader<'a>, 76 | entity: Entity, 77 | world: &mut World, 78 | toml: &toml::Value, 79 | ) -> Result { 80 | if !toml.is_table() { 81 | return Err(EmeraldError::new( 82 | "Cannot load rigid_body from a non-table toml value.", 83 | )); 84 | } 85 | let schema: EntRigidBodySchema = toml::from_str(&toml.to_string())?; 86 | 87 | let mut body_type = RigidBodyType::Dynamic; 88 | match schema.body_type.as_str() { 89 | "dynamic" => {} 90 | "fixed" => body_type = RigidBodyType::Fixed, 91 | "kinematic_velocity_based" => body_type = RigidBodyType::KinematicVelocityBased, 92 | "kinematic_position_based" => body_type = RigidBodyType::KinematicPositionBased, 93 | _ => { 94 | return Err(EmeraldError::new(format!( 95 | "{:?} does not match a valid body type.", 96 | schema.body_type.as_str() 97 | ))); 98 | } 99 | } 100 | 101 | let mut rigid_body_builder = RigidBodyBuilder::new(body_type); 102 | 103 | if schema.lock_rotations.filter(|l| *l).is_some() { 104 | rigid_body_builder = rigid_body_builder.lock_rotations(); 105 | } 106 | 107 | if schema.lock_translations.filter(|l| *l).is_some() { 108 | rigid_body_builder = rigid_body_builder.lock_translations(); 109 | } 110 | 111 | let rbh = world.physics().build_body(entity, rigid_body_builder)?; 112 | if let Some(collider_schemas) = schema.colliders { 113 | for collider_schema in collider_schemas { 114 | load_ent_collider(rbh, world, collider_schema)?; 115 | } 116 | } 117 | 118 | Ok(rbh) 119 | } 120 | -------------------------------------------------------------------------------- /assets/android.svg: -------------------------------------------------------------------------------- 1 | 2 | 17 | 19 | 36 | 37 | 60 | 62 | 63 | 65 | image/svg+xml 66 | 68 | 69 | 70 | 71 | 72 | 77 | 82 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /emerald/src/world/ent/ent_label_loader.rs: -------------------------------------------------------------------------------- 1 | use fontdue::layout::{HorizontalAlign, VerticalAlign}; 2 | use hecs::Entity; 3 | use rapier2d::na::Vector2; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::{rendering::components::Label, AssetLoader, EmeraldError, World}; 7 | 8 | use super::Vec2f32Schema; 9 | 10 | #[derive(Deserialize, Serialize)] 11 | pub(crate) struct EntLabelSchema { 12 | pub font: Option, 13 | pub font_size: Option, 14 | 15 | /// A path to the font resource file 16 | pub resource: Option, 17 | 18 | #[serde(default)] 19 | pub z_index: f32, 20 | pub size: u16, 21 | pub text: Option, 22 | pub offset: Option, 23 | 24 | pub visible: Option, 25 | /// options: "bottom", "middle", "top" 26 | pub vertical_align: Option, 27 | 28 | /// options: "center", "left", "right" 29 | pub horizontal_align: Option, 30 | } 31 | 32 | #[derive(Deserialize, Serialize)] 33 | pub struct FontResource { 34 | /// The path to the font 35 | pub font: String, 36 | 37 | /// The size of the font 38 | pub size: u32, 39 | } 40 | 41 | pub(crate) fn load_ent_label<'a>( 42 | loader: &mut AssetLoader<'a>, 43 | entity: Entity, 44 | world: &mut World, 45 | toml: &toml::Value, 46 | ) -> Result<(), EmeraldError> { 47 | if !toml.is_table() { 48 | return Err(EmeraldError::new( 49 | "Cannot load label from a non-table toml value.", 50 | )); 51 | } 52 | 53 | let schema: EntLabelSchema = toml::from_str(&toml.to_string())?; 54 | 55 | if (schema.font.is_none() || schema.font_size.is_none()) && schema.resource.is_none() { 56 | return Err(EmeraldError::new(format!("Failure loading entity {:?}: Labels require either a resource OR a (font AND font_size).", entity))); 57 | } 58 | 59 | let mut font = None; 60 | 61 | if let (Some(font_path), Some(font_size)) = (schema.font, schema.font_size) { 62 | font = Some(loader.font(font_path, font_size)?); 63 | } else if let Some(resource_file) = schema.resource { 64 | let resource_data = loader.string(resource_file)?; 65 | let resource: FontResource = toml::from_str(&resource_data)?; 66 | font = Some(loader.font(resource.font, resource.size)?); 67 | } 68 | 69 | if font.is_none() { 70 | return Err(EmeraldError::new(format!("Failure loading entity {:?}: Labels require either a resource OR a (font AND font_size).", entity))); 71 | } 72 | 73 | let font = font.unwrap(); 74 | let text = schema.text.unwrap_or("".into()); 75 | let mut label = Label::new(text, font, schema.size); 76 | label.z_index = schema.z_index; 77 | 78 | schema.visible.map(|v| label.visible = v); 79 | 80 | if let Some(offset) = schema.offset { 81 | label.offset = Vector2::new(offset.x, offset.y); 82 | } 83 | 84 | if let Some(vertical_align) = schema.vertical_align { 85 | match vertical_align.as_str() { 86 | "bottom" => { 87 | label.vertical_align = VerticalAlign::Bottom; 88 | } 89 | "middle" => { 90 | label.vertical_align = VerticalAlign::Middle; 91 | } 92 | "top" => { 93 | label.vertical_align = VerticalAlign::Top; 94 | } 95 | _ => { 96 | return Err(EmeraldError::new(format!( 97 | "{:?} is not a valid vertical align value", 98 | vertical_align 99 | ))) 100 | } 101 | } 102 | } 103 | 104 | if let Some(horizontal_align) = schema.horizontal_align { 105 | match horizontal_align.as_str() { 106 | "center" => { 107 | label.horizontal_align = HorizontalAlign::Center; 108 | } 109 | "left" => { 110 | label.horizontal_align = HorizontalAlign::Left; 111 | } 112 | "right" => { 113 | label.horizontal_align = HorizontalAlign::Right; 114 | } 115 | _ => { 116 | return Err(EmeraldError::new(format!( 117 | "{:?} is not a valid horizontal align value", 118 | horizontal_align 119 | ))) 120 | } 121 | } 122 | } 123 | 124 | world.insert_one(entity, label)?; 125 | 126 | Ok(()) 127 | } 128 | 129 | #[cfg(test)] 130 | mod tests { 131 | use super::FontResource; 132 | 133 | #[test] 134 | fn validate_font_resource() { 135 | let example_resource = r#" 136 | font = "Roboto-Light.ttf" 137 | size = 48 138 | "#; 139 | 140 | toml::from_str::(example_resource).unwrap(); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /emerald/examples/raycast.rs: -------------------------------------------------------------------------------- 1 | use emerald::{parry::shape::Cuboid, *}; 2 | use nalgebra::Point2; 3 | 4 | pub fn main() { 5 | emerald::start( 6 | Box::new(RaycastExample { 7 | world: World::new(), 8 | }), 9 | GameSettings::default(), 10 | ) 11 | } 12 | 13 | pub struct RaycastExample { 14 | world: World, 15 | } 16 | impl Game for RaycastExample { 17 | fn initialize(&mut self, mut emd: Emerald) { 18 | emd.set_asset_folder_root(String::from("./examples/assets/")); 19 | let sprite = emd.loader().sprite("bunny.png").unwrap(); 20 | 21 | let (_, rbh1) = self 22 | .world 23 | .spawn_with_body( 24 | ( 25 | sprite.clone(), 26 | Transform::from_translation((200.0, 0.0)), 27 | String::from("entity on the right"), 28 | ), 29 | RigidBodyBuilder::new_static(), 30 | ) 31 | .unwrap(); 32 | let (_, rbh2) = self 33 | .world 34 | .spawn_with_body( 35 | ( 36 | sprite.clone(), 37 | Transform::from_translation((-200.0, 0.0)), 38 | String::from("entity on the left"), 39 | ), 40 | RigidBodyBuilder::fixed(), 41 | ) 42 | .unwrap(); 43 | let (_, rbh3) = self 44 | .world 45 | .spawn_with_body( 46 | ( 47 | sprite.clone(), 48 | Transform::from_translation((90.0, 200.0)), 49 | String::from("entity on the top"), 50 | ), 51 | RigidBodyBuilder::fixed(), 52 | ) 53 | .unwrap(); 54 | let (_, rbh4) = self 55 | .world 56 | .spawn_with_body( 57 | ( 58 | sprite.clone(), 59 | Transform::from_translation((-40.0, -200.0)), 60 | String::from("entity on the bottom"), 61 | ), 62 | RigidBodyBuilder::fixed(), 63 | ) 64 | .unwrap(); 65 | 66 | self.world 67 | .physics() 68 | .build_collider(rbh1, ColliderBuilder::cuboid(20.0, 20.0)); 69 | self.world 70 | .physics() 71 | .build_collider(rbh2, ColliderBuilder::cuboid(20.0, 20.0)); 72 | 73 | self.world 74 | .physics() 75 | .build_collider(rbh3, ColliderBuilder::cuboid(20.0, 20.0)); 76 | self.world 77 | .physics() 78 | .build_collider(rbh4, ColliderBuilder::cuboid(20.0, 20.0)); 79 | } 80 | 81 | fn update(&mut self, mut emd: Emerald) { 82 | let delta = emd.delta(); 83 | let mut ray = None; 84 | 85 | if emd.input().is_key_just_pressed(KeyCode::Left) { 86 | ray = Some(Ray::new(Point2::new(0.0, 0.0), Vector2::new(-500.0, 0.0))); 87 | } else if emd.input().is_key_just_pressed(KeyCode::Right) { 88 | ray = Some(Ray::new(Point2::new(0.0, 0.0), Vector2::new(500.0, 0.0))); 89 | } 90 | 91 | if let Some(ray) = ray { 92 | let entity = self.world.physics().cast_ray(RayCastQuery { 93 | ray, 94 | ..RayCastQuery::default() 95 | }); 96 | 97 | if let Some(e) = entity { 98 | if let Ok(s) = self.world.get::<&String>(e) { 99 | println!("Found {:?}", *s); 100 | } 101 | } 102 | } 103 | 104 | let mut vel = None; 105 | 106 | if emd.input().is_key_just_pressed(KeyCode::Up) { 107 | vel = Some(Vector2::new(0.0, 50.0)); 108 | } else if emd.input().is_key_just_pressed(KeyCode::Down) { 109 | vel = Some(Vector2::new(0.0, -50.0)); 110 | } 111 | 112 | if let Some(vel) = vel { 113 | let shape = Cuboid::new(Vector2::new(40.0, 10.0)); 114 | 115 | let entity = self.world.physics().cast_shape( 116 | &shape, 117 | ShapeCastQuery { 118 | velocity: vel, 119 | origin_translation: Translation::default(), 120 | max_toi: 30.0, 121 | ..ShapeCastQuery::default() 122 | }, 123 | ); 124 | 125 | if let Some(e) = entity { 126 | if let Ok(s) = self.world.get::<&String>(e) { 127 | println!("Found {:?}", *s); 128 | } 129 | } 130 | } 131 | 132 | self.world.physics().step(delta); 133 | } 134 | 135 | fn draw(&mut self, mut emd: Emerald) { 136 | emd.graphics().begin().unwrap(); 137 | emd.graphics().draw_world(&mut self.world).unwrap(); 138 | emd.graphics().render().unwrap(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /emerald/src/audio/mixer.rs: -------------------------------------------------------------------------------- 1 | use crate::{audio::sound::SoundInstanceId, AssetEngine, EmeraldError, SoundKey}; 2 | 3 | #[cfg(feature = "audio")] 4 | mod kira_backend; 5 | 6 | #[cfg(feature = "audio")] 7 | use kira::manager::AudioManager; 8 | #[cfg(feature = "audio")] 9 | use kira_backend::KiraMixer as BackendMixer; 10 | 11 | #[cfg(not(feature = "audio"))] 12 | mod dummy; 13 | #[cfg(not(feature = "audio"))] 14 | use dummy::DummyMixer as BackendMixer; 15 | 16 | #[cfg(target_arch = "wasm32")] 17 | pub(crate) type ThreadSafeMixer = Box; 18 | 19 | #[cfg(not(target_arch = "wasm32"))] 20 | pub(crate) type ThreadSafeMixer = Box; 21 | 22 | pub(crate) trait Mixer { 23 | fn play( 24 | &mut self, 25 | sound: &SoundKey, 26 | asset_store: &mut AssetEngine, 27 | ) -> Result; 28 | fn play_and_loop( 29 | &mut self, 30 | sound: &SoundKey, 31 | asset_store: &mut AssetEngine, 32 | ) -> Result; 33 | fn get_volume(&self) -> Result; 34 | fn set_volume(&mut self, volume: f32) -> Result<(), EmeraldError>; 35 | fn set_instance_volume( 36 | &mut self, 37 | snd_instance_id: SoundInstanceId, 38 | volume: f32, 39 | ) -> Result<(), EmeraldError>; 40 | fn get_instances(&self) -> Result, EmeraldError>; 41 | fn stop(&mut self, snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError>; 42 | fn pause(&mut self, snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError>; 43 | fn resume(&mut self, snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError>; 44 | fn clear(&mut self) -> Result<(), EmeraldError>; 45 | fn post_update(&mut self) -> Result<(), EmeraldError>; 46 | } 47 | 48 | #[cfg(not(feature = "audio"))] 49 | pub(crate) fn new_mixer() -> Result { 50 | let mixer = BackendMixer::new()?; 51 | 52 | Ok(mixer) 53 | } 54 | 55 | #[cfg(feature = "audio")] 56 | use std::sync::{Arc, Mutex}; 57 | 58 | #[cfg(feature = "audio")] 59 | static mut KIRA_AUDIO_MANAGER: Option>> = None; 60 | 61 | #[cfg(feature = "audio")] 62 | pub(crate) fn new_mixer() -> Result { 63 | use kira::manager::AudioManagerSettings; 64 | 65 | unsafe { 66 | if KIRA_AUDIO_MANAGER.is_none() { 67 | let audio_manager = AudioManager::new(AudioManagerSettings { 68 | num_sounds: 1000, 69 | num_instances: 1000, 70 | ..Default::default() 71 | })?; 72 | KIRA_AUDIO_MANAGER = Some(Arc::new(Mutex::new(audio_manager))); 73 | } 74 | 75 | if let Some(audio_manager) = &mut KIRA_AUDIO_MANAGER { 76 | let mixer = BackendMixer::new(audio_manager.clone())?; 77 | 78 | return Ok(mixer); 79 | } 80 | } 81 | 82 | Err(EmeraldError::new( 83 | "Unable to find or creat the kira audio manager", 84 | )) 85 | } 86 | 87 | pub struct MixerHandler<'a> { 88 | inner: &'a mut ThreadSafeMixer, 89 | asset_store: &'a mut AssetEngine, 90 | } 91 | impl<'a> MixerHandler<'a> { 92 | pub(crate) fn new(inner: &'a mut ThreadSafeMixer, asset_store: &'a mut AssetEngine) -> Self { 93 | MixerHandler { inner, asset_store } 94 | } 95 | 96 | pub fn play(&mut self, key: &SoundKey) -> Result { 97 | self.inner.play(key, &mut self.asset_store) 98 | } 99 | pub fn play_and_loop(&mut self, key: &SoundKey) -> Result { 100 | self.inner.play_and_loop(key, &mut self.asset_store) 101 | } 102 | pub fn get_volume(&self) -> Result { 103 | self.inner.get_volume() 104 | } 105 | pub fn set_instance_volume( 106 | &mut self, 107 | snd_instance_id: SoundInstanceId, 108 | volume: f32, 109 | ) -> Result<(), EmeraldError> { 110 | self.inner.set_instance_volume(snd_instance_id, volume) 111 | } 112 | pub fn set_volume(&mut self, volume: f32) -> Result<(), EmeraldError> { 113 | self.inner.set_volume(volume) 114 | } 115 | pub fn get_instances(&self) -> Result, EmeraldError> { 116 | self.inner.get_instances() 117 | } 118 | pub fn stop(&mut self, snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError> { 119 | self.inner.stop(snd_instance_id) 120 | } 121 | pub fn pause(&mut self, snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError> { 122 | self.inner.pause(snd_instance_id) 123 | } 124 | pub fn resume(&mut self, snd_instance_id: SoundInstanceId) -> Result<(), EmeraldError> { 125 | self.inner.resume(snd_instance_id) 126 | } 127 | pub fn clear(&mut self) -> Result<(), EmeraldError> { 128 | self.inner.clear() 129 | } 130 | pub fn clear_sounds(&mut self) -> Result<(), EmeraldError> { 131 | for instance_id in self.get_instances()? { 132 | self.stop(instance_id)?; 133 | } 134 | Ok(()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /emerald/examples/bunnymark.rs: -------------------------------------------------------------------------------- 1 | use emerald::{ 2 | font::FontKey, 3 | rendering::components::{Label, Sprite}, 4 | *, 5 | }; 6 | 7 | // Bunnymark is super disappointing right now, need to fix 8 | // https://github.com/Bombfuse/emerald/issues/10 9 | 10 | #[derive(Clone, Debug)] 11 | struct Velocity { 12 | pub x: f32, 13 | pub y: f32, 14 | } 15 | impl Velocity { 16 | pub fn new(x: f32, y: f32) -> Self { 17 | Velocity { x, y } 18 | } 19 | } 20 | 21 | #[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] 22 | pub fn main() { 23 | let mut settings = GameSettings::default(); 24 | settings.render_settings.resolution = (320 * 5, 180 * 5); 25 | 26 | emerald::start( 27 | Box::new(BunnymarkGame { 28 | count: 0, 29 | world: World::new(), 30 | fps_label: None, 31 | bunnymark_label: None, 32 | }), 33 | settings, 34 | ) 35 | } 36 | 37 | pub struct BunnymarkGame { 38 | count: u64, 39 | world: World, 40 | fps_label: Option