Compare commits

..

6 commits

14 changed files with 374 additions and 55 deletions

View file

@ -11,6 +11,7 @@ comet_colors = { path = "../comet_colors" }
comet_log = { path = "../comet_log" } comet_log = { path = "../comet_log" }
comet_input = { path = "../comet_input" } comet_input = { path = "../comet_input" }
comet_structs = { path = "../comet_structs" } comet_structs = { path = "../comet_structs" }
comet_sound = { path = "../comet_sound" }
winit = { version = "0.29", features = ["rwh_05"] } winit = { version = "0.29", features = ["rwh_05"] }
pollster = "0.3" pollster = "0.3"

View file

@ -3,6 +3,7 @@ use comet_ecs::{Camera2D, Component, Entity, Render2D, Scene, Text, Transform2D,
use comet_input::keyboard::Key; use comet_input::keyboard::Key;
use comet_log::*; use comet_log::*;
use comet_renderer::renderer::Renderer; use comet_renderer::renderer::Renderer;
use comet_sound::*;
use std::any::{type_name, Any, TypeId}; use std::any::{type_name, Any, TypeId};
use std::sync::Arc; use std::sync::Arc;
use winit::dpi::LogicalSize; use winit::dpi::LogicalSize;
@ -30,6 +31,7 @@ pub struct App {
delta_time: f32, delta_time: f32,
update_timer: f32, update_timer: f32,
game_state: Option<Box<dyn Any>>, game_state: Option<Box<dyn Any>>,
audio: Box<dyn Audio>,
scene: Scene, scene: Scene,
fullscreen: bool, fullscreen: bool,
should_quit: bool, should_quit: bool,
@ -47,6 +49,7 @@ impl App {
delta_time: 0.0, delta_time: 0.0,
update_timer: 0.0166667, update_timer: 0.0166667,
game_state: None, game_state: None,
audio: Box::new(KiraAudio::new()),
scene: Scene::new(), scene: Scene::new(),
fullscreen: false, fullscreen: false,
should_quit: false, should_quit: false,
@ -136,6 +139,11 @@ impl App {
&self.scene &self.scene
} }
/// Retrieves a mutable reference to the current `Scene` in the `App`
pub fn scene_mut(&mut self) -> &mut Scene {
&mut self.scene
}
/// Retrieves a reference to the `InputManager`. /// Retrieves a reference to the `InputManager`.
pub fn input_manager(&self) -> &InputManager { pub fn input_manager(&self) -> &InputManager {
&self.input_manager &self.input_manager
@ -246,6 +254,38 @@ impl App {
self.scene.has_prefab(name) self.scene.has_prefab(name)
} }
pub fn load_audio(&mut self, name: &str, path: &str) {
self.audio.load(name, path);
}
pub fn play_audio(&mut self, name: &str, looped: bool) {
self.audio.play(name, looped);
}
pub fn pause_audio(&mut self, name: &str) {
self.audio.pause(name);
}
pub fn stop_audio(&mut self, name: &str) {
self.audio.stop(name);
}
pub fn stop_all_audio(&mut self) {
self.audio.stop_all();
}
pub fn update_audio(&mut self, dt: f32) {
self.audio.update(dt);
}
pub fn is_playing(&self, name: &str) -> bool {
self.audio.is_playing(name)
}
pub fn set_volume(&mut self, name: &str, volume: f32) {
self.audio.set_volume(name, volume);
}
/// Stops the event loop and with that quits the `App`. /// Stops the event loop and with that quits the `App`.
pub fn quit(&mut self) { pub fn quit(&mut self) {
self.should_quit = true; self.should_quit = true;
@ -341,6 +381,9 @@ impl App {
WindowEvent::Resized(physical_size) => { WindowEvent::Resized(physical_size) => {
renderer.resize(*physical_size); renderer.resize(*physical_size);
} }
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
renderer.set_scale_factor(*scale_factor);
}
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {
window.request_redraw(); window.request_redraw();
match renderer.render() { match renderer.render() {

View file

@ -2,6 +2,7 @@
// You can use these components as is or as a reference to create your own components // You can use these components as is or as a reference to create your own components
// Also just as a nomenclature: bundles are a component made up of multiple components, // Also just as a nomenclature: bundles are a component made up of multiple components,
// so it's a collection of components bundled together (like Transform2D) // so it's a collection of components bundled together (like Transform2D)
// They are intended to work with the base suite of systems provided by the engine.
use crate::math::{v2, v3}; use crate::math::{v2, v3};
use crate::{Entity, Scene}; use crate::{Entity, Scene};
use comet_colors::Color as ColorTrait; use comet_colors::Color as ColorTrait;
@ -45,6 +46,7 @@ pub struct Render2D {
is_visible: bool, is_visible: bool,
texture_name: &'static str, texture_name: &'static str,
scale: v2, scale: v2,
draw_index: u32,
} }
#[derive(Component)] #[derive(Component)]
@ -61,6 +63,7 @@ pub struct Text {
font_size: f32, font_size: f32,
color: Color, color: Color,
is_visible: bool, is_visible: bool,
bounds: v2,
} }
#[derive(Component)] #[derive(Component)]
@ -78,6 +81,15 @@ pub struct Timer {
done: bool, done: bool,
} }
#[derive(Component)]
pub struct AudioSource {
name: &'static str,
path: Option<&'static str>,
looped: bool,
volume: f32,
pitch: f32,
}
// ################################################## // ##################################################
// # BUNDLES # // # BUNDLES #
// ################################################## // ##################################################
@ -264,13 +276,39 @@ impl Collider for Rectangle2D {
} }
impl Render2D { impl Render2D {
pub fn new(texture: &'static str, is_visible: bool, scale: v2, draw_index: u32) -> Self {
Self {
is_visible,
texture_name: texture,
scale,
draw_index,
}
}
pub fn with_texture(texture: &'static str) -> Self { pub fn with_texture(texture: &'static str) -> Self {
Self { Self {
is_visible: true, is_visible: true,
texture_name: texture, texture_name: texture,
scale: v2::new(1.0, 1.0), scale: v2::new(1.0, 1.0),
draw_index: 0,
} }
} }
pub fn scale(&self) -> v2 {
self.scale
}
pub fn set_scale(&mut self, scale: v2) {
self.scale = scale;
}
pub fn draw_index(&self) -> u32 {
self.draw_index
}
pub fn set_draw_index(&mut self, index: u32) {
self.draw_index = index
}
} }
impl Render for Render2D { impl Render for Render2D {
@ -431,6 +469,7 @@ impl Text {
font_size, font_size,
color: Color::from_wgpu_color(color.to_wgpu()), color: Color::from_wgpu_color(color.to_wgpu()),
is_visible, is_visible,
bounds: v2::ZERO,
} }
} }
@ -469,6 +508,14 @@ impl Text {
pub fn is_visible(&self) -> bool { pub fn is_visible(&self) -> bool {
self.is_visible self.is_visible
} }
pub fn bounds(&self) -> v2 {
self.bounds
}
pub fn set_bounds(&mut self, bounds: v2) {
self.bounds = bounds
}
} }
impl Color { impl Color {
@ -548,3 +595,47 @@ impl Timer {
self.done = false; self.done = false;
} }
} }
impl AudioSource {
pub fn new(name: &'static str, path: Option<&'static str>) -> Self {
Self {
name,
path,
looped: false,
volume: 1.0,
pitch: 1.0,
}
}
pub fn name(&self) -> &str {
self.name
}
pub fn path(&self) -> Option<&str> {
self.path
}
pub fn looped(&self) -> bool {
self.looped
}
pub fn volume(&self) -> f32 {
self.volume
}
pub fn pitch(&self) -> f32 {
self.pitch
}
pub fn set_looped(&mut self, looped: bool) {
self.looped = looped;
}
pub fn set_volume(&mut self, volume: f32) {
self.volume = volume.clamp(0.0, 1.0);
}
pub fn set_pitch(&mut self, pitch: f32) {
self.pitch = pitch;
}
}

View file

@ -1,12 +1,15 @@
use comet_colors::Color;
use std::sync::Arc; use std::sync::Arc;
use winit::dpi::PhysicalSize; use winit::dpi::PhysicalSize;
use winit::window::Window; use winit::window::Window;
use comet_colors::Color;
pub trait Renderer: Sized + Send + Sync { pub trait Renderer: Sized + Send + Sync {
fn new(window: Arc<Window>, clear_color: Option<impl Color>) -> Self; fn new(window: Arc<Window>, clear_color: Option<impl Color>) -> Self;
fn size(&self) -> PhysicalSize<u32>; fn size(&self) -> PhysicalSize<u32>;
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>); fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>);
fn update(&mut self) -> f32; fn scale_factor(&self) -> f64;
fn render(&mut self) -> Result<(), wgpu::SurfaceError>; fn set_scale_factor(&mut self, scale_factor: f64);
} fn update(&mut self) -> f32;
fn render(&mut self) -> Result<(), wgpu::SurfaceError>;
}

View file

@ -26,6 +26,7 @@ pub struct Renderer2D<'a> {
queue: wgpu::Queue, queue: wgpu::Queue,
config: wgpu::SurfaceConfiguration, config: wgpu::SurfaceConfiguration,
size: PhysicalSize<u32>, size: PhysicalSize<u32>,
scale_factor: f64,
universal_render_pipeline: wgpu::RenderPipeline, universal_render_pipeline: wgpu::RenderPipeline,
texture_bind_group_layout: wgpu::BindGroupLayout, texture_bind_group_layout: wgpu::BindGroupLayout,
texture_sampler: wgpu::Sampler, texture_sampler: wgpu::Sampler,
@ -43,6 +44,7 @@ pub struct Renderer2D<'a> {
impl<'a> Renderer2D<'a> { impl<'a> Renderer2D<'a> {
pub fn new(window: Arc<Window>, clear_color: Option<impl Color>) -> Renderer2D<'a> { pub fn new(window: Arc<Window>, clear_color: Option<impl Color>) -> Renderer2D<'a> {
let size = window.inner_size(); //PhysicalSize::<u32>::new(1920, 1080); let size = window.inner_size(); //PhysicalSize::<u32>::new(1920, 1080);
let scale_factor = window.scale_factor();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY, backends: wgpu::Backends::PRIMARY,
@ -258,6 +260,7 @@ impl<'a> Renderer2D<'a> {
queue, queue,
config, config,
size, size,
scale_factor,
universal_render_pipeline, universal_render_pipeline,
texture_bind_group_layout, texture_bind_group_layout,
texture_sampler, texture_sampler,
@ -294,6 +297,14 @@ impl<'a> Renderer2D<'a> {
} }
} }
pub fn scale_factor(&self) -> f64 {
self.scale_factor
}
pub fn set_scale_factor(&mut self, scale_factor: f64) {
self.scale_factor = scale_factor
}
pub fn add_draw_call(&mut self, draw_call: String, texture: Texture) { pub fn add_draw_call(&mut self, draw_call: String, texture: Texture) {
let draw_info = DrawInfo::new( let draw_info = DrawInfo::new(
draw_call, draw_call,
@ -483,6 +494,7 @@ impl<'a> Renderer2D<'a> {
size: f32, size: f32,
position: p2, position: p2,
color: wgpu::Color, color: wgpu::Color,
bounds: &mut v2,
) -> (Vec<Vertex>, Vec<u16>) { ) -> (Vec<Vertex>, Vec<u16>) {
let vert_color = [ let vert_color = [
color.r as f32, color.r as f32,
@ -495,24 +507,18 @@ impl<'a> Renderer2D<'a> {
position.x() / self.config.width as f32, position.x() / self.config.width as f32,
position.y() / self.config.height as f32, position.y() / self.config.height as f32,
); );
let scale_factor = size
/ self
.graphic_resource_manager
.fonts()
.iter()
.find(|f| f.name() == font)
.unwrap()
.size();
let line_height = (self let font_data = self
.graphic_resource_manager .graphic_resource_manager
.fonts() .fonts()
.iter() .iter()
.find(|f| f.name() == font) .find(|f| f.name() == font)
.unwrap() .unwrap();
.line_height()
/ self.config.height as f32) let scale_factor = size / font_data.size();
* scale_factor;
let line_height = (font_data.line_height() / self.config.height as f32) * scale_factor;
let lines = text let lines = text
.split("\n") .split("\n")
.map(|s| { .map(|s| {
@ -525,9 +531,27 @@ impl<'a> Renderer2D<'a> {
}) })
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let mut max_line_width_px = 0.0;
let mut total_height_px = 0.0;
for line in &lines {
let mut line_width_px = 0.0;
for c in line.chars() {
if let Some(region) = font_data.get_glyph(c) {
line_width_px += region.advance();
}
}
if line_width_px > max_line_width_px {
max_line_width_px = line_width_px;
}
total_height_px += font_data.line_height();
}
bounds.set_x((max_line_width_px / self.config.width as f32) * scale_factor);
bounds.set_y((total_height_px / self.config.height as f32) * scale_factor);
let mut x_offset = 0.0; let mut x_offset = 0.0;
let mut y_offset = 0.0; let mut y_offset = 0.0;
let mut vertex_data = Vec::new(); let mut vertex_data = Vec::new();
let mut index_data = Vec::new(); let mut index_data = Vec::new();
@ -679,14 +703,22 @@ impl<'a> Renderer2D<'a> {
/// A function to automatically render all the entities of the `Scene` struct. /// A function to automatically render all the entities of the `Scene` struct.
/// The entities must have the `Render2D` and `Transform2D` components to be rendered as well as set visible. /// The entities must have the `Render2D` and `Transform2D` components to be rendered as well as set visible.
pub fn render_scene_2d(&mut self, scene: &Scene) { pub fn render_scene_2d(&mut self, scene: &mut Scene) {
let cameras = scene.get_entities_with(vec![Transform2D::type_id(), Camera2D::type_id()]); let cameras = scene.get_entities_with(vec![Transform2D::type_id(), Camera2D::type_id()]);
if cameras.is_empty() { if cameras.is_empty() {
return; return;
} }
let entities = scene.get_entities_with(vec![Transform2D::type_id(), Render2D::type_id()]); let mut entities =
scene.get_entities_with(vec![Transform2D::type_id(), Render2D::type_id()]);
entities.sort_by(|&a, &b| {
let ra = scene.get_component::<Render2D>(a).unwrap();
let rb = scene.get_component::<Render2D>(b).unwrap();
ra.draw_index().cmp(&rb.draw_index())
});
let texts = let texts =
scene.get_entities_with(vec![Transform2D::type_id(), comet_ecs::Text::type_id()]); scene.get_entities_with(vec![Transform2D::type_id(), comet_ecs::Text::type_id()]);
@ -713,8 +745,9 @@ impl<'a> Renderer2D<'a> {
let region = t_region.unwrap(); let region = t_region.unwrap();
let (dim_x, dim_y) = region.dimensions(); let (dim_x, dim_y) = region.dimensions();
let half_width = dim_x as f32 * 0.5; let scale = renderer_component.scale();
let half_height = dim_y as f32 * 0.5; let half_width = dim_x as f32 * 0.5 * scale.x();
let half_height = dim_y as f32 * 0.5 * scale.y();
let buffer_size = vertex_buffer.len() as u16; let buffer_size = vertex_buffer.len() as u16;
@ -780,24 +813,36 @@ impl<'a> Renderer2D<'a> {
} }
for text in texts { for text in texts {
let component = scene.get_component::<Text>(text).unwrap(); if let Some(component) = scene.get_component_mut::<Text>(text) {
let transform = scene.get_component::<Transform2D>(text).unwrap(); if component.is_visible() {
let font = component.font().to_string();
let size = component.font_size();
let color = component.color().to_wgpu();
let content = component.content().to_string();
if component.is_visible() { let transform = scene.get_component::<Transform2D>(text).unwrap();
let (vertices, indices) = self.add_text_to_buffers(
component.content().to_string(), let mut bounds = v2::ZERO;
component.font().to_string(), let (vertices, indices) = self.add_text_to_buffers(
component.font_size(), content,
p2::from_vec(transform.position().as_vec()), font.clone(),
component.color().to_wgpu(), size,
); p2::from_vec(transform.position().as_vec()),
let draw = self color,
.draw_info &mut bounds,
.iter_mut() );
.find(|d| d.name() == &format!("{}", component.font()))
.unwrap(); let component = scene.get_component_mut::<Text>(text).unwrap();
draw.update_vertex_buffer(&self.device, &self.queue, vertices); component.set_bounds(bounds);
draw.update_index_buffer(&self.device, &self.queue, indices);
let draw = self
.draw_info
.iter_mut()
.find(|d| d.name() == &format!("{}", font))
.unwrap();
draw.update_vertex_buffer(&self.device, &self.queue, vertices);
draw.update_index_buffer(&self.device, &self.queue, indices);
}
} }
} }
@ -885,6 +930,14 @@ impl<'a> Renderer for Renderer2D<'a> {
self.resize(new_size) self.resize(new_size)
} }
fn scale_factor(&self) -> f64 {
self.scale_factor()
}
fn set_scale_factor(&mut self, scale_factor: f64) {
self.set_scale_factor(scale_factor);
}
fn update(&mut self) -> f32 { fn update(&mut self) -> f32 {
self.update() self.update()
} }

View file

@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
rodio = "0.12.0" kira = "0.10.8"

View file

@ -0,0 +1,13 @@
pub trait Audio {
fn new() -> Self
where
Self: Sized;
fn load(&mut self, name: &str, path: &str);
fn play(&mut self, name: &str, looped: bool);
fn pause(&mut self, name: &str);
fn stop(&mut self, name: &str);
fn stop_all(&mut self);
fn update(&mut self, dt: f32);
fn is_playing(&self, name: &str) -> bool;
fn set_volume(&mut self, name: &str, volume: f32);
}

View file

@ -0,0 +1,86 @@
use crate::audio::Audio;
use kira::{
sound::static_sound::{StaticSoundData, StaticSoundHandle, StaticSoundSettings},
AudioManager, AudioManagerSettings, Decibels, Tween,
};
use std::{collections::HashMap, path::Path};
pub struct KiraAudio {
manager: AudioManager,
sounds: HashMap<String, StaticSoundData>,
handles: HashMap<String, StaticSoundHandle>,
}
impl KiraAudio {
fn load_sound(path: &Path) -> Option<StaticSoundData> {
StaticSoundData::from_file(path).ok()
}
}
impl Audio for KiraAudio {
fn new() -> Self {
Self {
manager: AudioManager::new(AudioManagerSettings::default()).unwrap(),
sounds: HashMap::new(),
handles: HashMap::new(),
}
}
fn load(&mut self, name: &str, path: &str) {
if let Some(sound) = Self::load_sound(Path::new(path)) {
self.sounds.insert(name.to_string(), sound);
}
}
fn play(&mut self, name: &str, looped: bool) {
if let Some(sound) = self.sounds.get(name) {
let mut settings = StaticSoundSettings::default();
if looped {
settings = settings.loop_region(..);
}
if let Ok(handle) = self.manager.play(sound.clone().with_settings(settings)) {
self.handles.insert(name.to_string(), handle);
}
}
}
fn pause(&mut self, name: &str) {
if let Some(handle) = self.handles.get_mut(name) {
handle.pause(Tween::default());
}
}
fn stop(&mut self, name: &str) {
if let Some(handle) = self.handles.get_mut(name) {
handle.stop(Tween::default());
}
}
fn stop_all(&mut self) {
for handle in self.handles.values_mut() {
handle.stop(Tween::default());
}
}
// KiraAudio needs no updating function, it just exists to make the trait happy
fn update(&mut self, _dt: f32) {}
fn is_playing(&self, name: &str) -> bool {
self.handles.contains_key(name)
}
fn set_volume(&mut self, name: &str, volume: f32) {
let vol = volume.clamp(0.0, 1.0);
let db = if vol == 0.0 {
Decibels::from(-80.0) // effectively silent
} else {
Decibels::from(20.0 * vol.log10())
};
if let Some(handle) = self.handles.get_mut(name) {
handle.set_volume(db, Tween::default());
}
}
}

View file

@ -0,0 +1,5 @@
mod audio;
mod kira;
pub use audio::Audio;
pub use kira::KiraAudio;

View file

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}

View file

@ -14,9 +14,7 @@ fn setup(app: &mut App, renderer: &mut Renderer2D) {
app.add_component(e1, Transform2D::new()); app.add_component(e1, Transform2D::new());
let mut renderer2d = Render2D::new(); let mut renderer2d = Render2D::with_texture("res/textures/comet_icon.png");
renderer2d.set_texture(r"res/textures/comet_icon.png");
renderer2d.set_visibility(true);
app.add_component(e1, renderer2d); app.add_component(e1, renderer2d);
} }
@ -24,7 +22,7 @@ fn setup(app: &mut App, renderer: &mut Renderer2D) {
fn update(app: &mut App, renderer: &mut Renderer2D, dt: f32) { fn update(app: &mut App, renderer: &mut Renderer2D, dt: f32) {
handle_input(app, dt); handle_input(app, dt);
renderer.render_scene_2d(app.scene()); renderer.render_scene_2d(app.scene_mut());
} }
fn handle_input(app: &mut App, dt: f32) { fn handle_input(app: &mut App, dt: f32) {

View file

@ -34,7 +34,7 @@ fn update(app: &mut App, renderer: &mut Renderer2D, dt: f32) {
transform.position_mut().set_x(-((size.width - 50) as f32)); transform.position_mut().set_x(-((size.width - 50) as f32));
transform.position_mut().set_y((size.height - 100) as f32); transform.position_mut().set_y((size.height - 100) as f32);
renderer.render_scene_2d(app.scene()); renderer.render_scene_2d(app.scene_mut());
} }
fn main() { fn main() {

View file

@ -13,15 +13,13 @@ fn setup(app: &mut App, renderer: &mut Renderer2D) {
let e0 = app.new_entity(); let e0 = app.new_entity();
app.add_component(e0, Transform2D::new()); app.add_component(e0, Transform2D::new());
let mut render = Render2D::new(); let render = Render2D::with_texture("res/textures/comet_icon.png");
render.set_visibility(true);
render.set_texture("./res/textures/comet_icon.png");
app.add_component(e0, render); app.add_component(e0, render);
} }
fn update(app: &mut App, renderer: &mut Renderer2D, dt: f32) { fn update(app: &mut App, renderer: &mut Renderer2D, dt: f32) {
renderer.render_scene_2d(app.scene()) renderer.render_scene_2d(app.scene_mut())
} }
fn main() { fn main() {

31
goals.md Normal file
View file

@ -0,0 +1,31 @@
# Goals of the Comet Game Engine
Comet should be an unopinionated game engine built in Rust that tries to
combine the simplicity of Raylib and modularity of Bevy without the user
needing to become a follower of a cult.
The engine itself should be expandable and swappable in its components.
Don't like the standard 2D renderer? Just make your own, implement the
`Renderer` trait and use it instead of the provided one.
If you really don't want to work with the ECS, just ignore it and add
your own custom `GameState` (or whatever you want to call it) struct
and work on it using the tools provided to you by the engine.
These things should be provided for an official 1.0 version of Comet:
- [x] 2D rendering
- [ ] 3D rendering
- [ ] UI system
- [ ] particle system
- [x] ECS
- [x] sound system
- [ ] simple physics engine
- [ ] multiple scenes (aka serialization and deserialization)
- [ ] extensive documentation
Future endeavors might include:
- [ ] project creation tool
- [ ] editor
- [ ] scripting using Rhai