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_input = { path = "../comet_input" }
comet_structs = { path = "../comet_structs" }
comet_sound = { path = "../comet_sound" }
winit = { version = "0.29", features = ["rwh_05"] }
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_log::*;
use comet_renderer::renderer::Renderer;
use comet_sound::*;
use std::any::{type_name, Any, TypeId};
use std::sync::Arc;
use winit::dpi::LogicalSize;
@ -30,6 +31,7 @@ pub struct App {
delta_time: f32,
update_timer: f32,
game_state: Option<Box<dyn Any>>,
audio: Box<dyn Audio>,
scene: Scene,
fullscreen: bool,
should_quit: bool,
@ -47,6 +49,7 @@ impl App {
delta_time: 0.0,
update_timer: 0.0166667,
game_state: None,
audio: Box::new(KiraAudio::new()),
scene: Scene::new(),
fullscreen: false,
should_quit: false,
@ -136,6 +139,11 @@ impl App {
&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`.
pub fn input_manager(&self) -> &InputManager {
&self.input_manager
@ -246,6 +254,38 @@ impl App {
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`.
pub fn quit(&mut self) {
self.should_quit = true;
@ -341,6 +381,9 @@ impl App {
WindowEvent::Resized(physical_size) => {
renderer.resize(*physical_size);
}
WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
renderer.set_scale_factor(*scale_factor);
}
WindowEvent::RedrawRequested => {
window.request_redraw();
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
// 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)
// They are intended to work with the base suite of systems provided by the engine.
use crate::math::{v2, v3};
use crate::{Entity, Scene};
use comet_colors::Color as ColorTrait;
@ -45,6 +46,7 @@ pub struct Render2D {
is_visible: bool,
texture_name: &'static str,
scale: v2,
draw_index: u32,
}
#[derive(Component)]
@ -61,6 +63,7 @@ pub struct Text {
font_size: f32,
color: Color,
is_visible: bool,
bounds: v2,
}
#[derive(Component)]
@ -78,6 +81,15 @@ pub struct Timer {
done: bool,
}
#[derive(Component)]
pub struct AudioSource {
name: &'static str,
path: Option<&'static str>,
looped: bool,
volume: f32,
pitch: f32,
}
// ##################################################
// # BUNDLES #
// ##################################################
@ -264,13 +276,39 @@ impl Collider for Rectangle2D {
}
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 {
Self {
is_visible: true,
texture_name: texture,
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 {
@ -431,6 +469,7 @@ impl Text {
font_size,
color: Color::from_wgpu_color(color.to_wgpu()),
is_visible,
bounds: v2::ZERO,
}
}
@ -469,6 +508,14 @@ impl Text {
pub fn is_visible(&self) -> bool {
self.is_visible
}
pub fn bounds(&self) -> v2 {
self.bounds
}
pub fn set_bounds(&mut self, bounds: v2) {
self.bounds = bounds
}
}
impl Color {
@ -548,3 +595,47 @@ impl Timer {
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 winit::dpi::PhysicalSize;
use winit::window::Window;
use comet_colors::Color;
pub trait Renderer: Sized + Send + Sync {
fn new(window: Arc<Window>, clear_color: Option<impl Color>) -> Self;
fn size(&self) -> PhysicalSize<u32>;
fn resize(&mut self, new_size: winit::dpi::PhysicalSize<u32>);
fn scale_factor(&self) -> f64;
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,
config: wgpu::SurfaceConfiguration,
size: PhysicalSize<u32>,
scale_factor: f64,
universal_render_pipeline: wgpu::RenderPipeline,
texture_bind_group_layout: wgpu::BindGroupLayout,
texture_sampler: wgpu::Sampler,
@ -43,6 +44,7 @@ pub struct Renderer2D<'a> {
impl<'a> 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 scale_factor = window.scale_factor();
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::Backends::PRIMARY,
@ -258,6 +260,7 @@ impl<'a> Renderer2D<'a> {
queue,
config,
size,
scale_factor,
universal_render_pipeline,
texture_bind_group_layout,
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) {
let draw_info = DrawInfo::new(
draw_call,
@ -483,6 +494,7 @@ impl<'a> Renderer2D<'a> {
size: f32,
position: p2,
color: wgpu::Color,
bounds: &mut v2,
) -> (Vec<Vertex>, Vec<u16>) {
let vert_color = [
color.r as f32,
@ -495,24 +507,18 @@ impl<'a> Renderer2D<'a> {
position.x() / self.config.width 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
.fonts()
.iter()
.find(|f| f.name() == font)
.unwrap()
.line_height()
/ self.config.height as f32)
* scale_factor;
.unwrap();
let scale_factor = size / font_data.size();
let line_height = (font_data.line_height() / self.config.height as f32) * scale_factor;
let lines = text
.split("\n")
.map(|s| {
@ -525,9 +531,27 @@ impl<'a> Renderer2D<'a> {
})
.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 y_offset = 0.0;
let mut vertex_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.
/// 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()]);
if cameras.is_empty() {
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 =
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 (dim_x, dim_y) = region.dimensions();
let half_width = dim_x as f32 * 0.5;
let half_height = dim_y as f32 * 0.5;
let scale = renderer_component.scale();
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;
@ -780,26 +813,38 @@ impl<'a> Renderer2D<'a> {
}
for text in texts {
let component = scene.get_component::<Text>(text).unwrap();
if let Some(component) = scene.get_component_mut::<Text>(text) {
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();
let transform = scene.get_component::<Transform2D>(text).unwrap();
if component.is_visible() {
let mut bounds = v2::ZERO;
let (vertices, indices) = self.add_text_to_buffers(
component.content().to_string(),
component.font().to_string(),
component.font_size(),
content,
font.clone(),
size,
p2::from_vec(transform.position().as_vec()),
component.color().to_wgpu(),
color,
&mut bounds,
);
let component = scene.get_component_mut::<Text>(text).unwrap();
component.set_bounds(bounds);
let draw = self
.draw_info
.iter_mut()
.find(|d| d.name() == &format!("{}", component.font()))
.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);
}
}
}
self.set_buffers(vertex_buffer, index_buffer);
}
@ -885,6 +930,14 @@ impl<'a> Renderer for Renderer2D<'a> {
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 {
self.update()
}

View file

@ -4,4 +4,4 @@ version = "0.1.0"
edition = "2021"
[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());
let mut renderer2d = Render2D::new();
renderer2d.set_texture(r"res/textures/comet_icon.png");
renderer2d.set_visibility(true);
let mut renderer2d = Render2D::with_texture("res/textures/comet_icon.png");
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) {
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) {

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_y((size.height - 100) as f32);
renderer.render_scene_2d(app.scene());
renderer.render_scene_2d(app.scene_mut());
}
fn main() {

View file

@ -13,15 +13,13 @@ fn setup(app: &mut App, renderer: &mut Renderer2D) {
let e0 = app.new_entity();
app.add_component(e0, Transform2D::new());
let mut render = Render2D::new();
render.set_visibility(true);
render.set_texture("./res/textures/comet_icon.png");
let render = Render2D::with_texture("res/textures/comet_icon.png");
app.add_component(e0, render);
}
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() {

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