├── 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 |
--------------------------------------------------------------------------------
/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 | |  | ☑ | ☑ | ☑ | ☑ |
6 | |  | ☑ | ☑ | ☑ | ☑ |
7 | |  | ☑ | ☑ | ☑ |☑ |
8 | |  | ☑ | ☑ | ☑ | ☑ |
9 | |  | ☑ | ☑ | ☒ | ☒ |
10 | |  | ☑ | ☑ | ☒ | ☒ |
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 |
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 |
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 |
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