diff --git a/Cargo.lock b/Cargo.lock index f783d08..ed8add2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -255,6 +255,13 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" +[[package]] +name = "bitmob" +version = "0.1.0" +dependencies = [ + "comfy", +] + [[package]] name = "block" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index f6e48e2..0fc4527 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,7 @@ [workspace] resolver = "2" -members = [ - "comfy", - "comfy-core", - "comfy-wgpu", -] +members = ["comfy", "comfy-core", "comfy-wgpu", "games/bitmob"] [profile.dev] opt-level = 3 diff --git a/Makefile b/Makefile index b41acc5..a214967 100644 --- a/Makefile +++ b/Makefile @@ -1,21 +1,26 @@ # EXAMPLE=animated_shapes -EXAMPLE=sprite +# EXAMPLE=sprite # EXAMPLE=text # EXAMPLE=particles # EXAMPLE=post_processing # EXAMPLE=shapes # EXAMPLE=ecs_sprite -# EXAMPLE=ecs_topdown_game +EXAMPLE=ecs_topdown_game # default: build-examples # default: wasm-build -default: example # default: profile-startup +# default: bitmob +default: example FLAGS=--release +ENV_VARS=RUST_LOG=info,wgpu=warn,symphonia=warn + +bitmob: + $(ENV_VARS) cargo run --bin bitmob $(FLAGS) example: - RUST_LOG=info,wgpu=warn,symphonia=warn cargo run --example $(EXAMPLE) $(FLAGS) + $(ENV_VARS) cargo run --example $(EXAMPLE) $(FLAGS) tests: cargo test diff --git a/README.md b/README.md index e90dcde..2a31652 100644 --- a/README.md +++ b/README.md @@ -257,8 +257,8 @@ Only features that require maximum few weeks of work are listed here. does make games look better by default, but it is one of the first few things that will get fixed after v0.1 release. - Configurable post processing. -- Camera render targets - Custom shaders/materials. +- Render targets. - Gamepad & touchpad support. - Antialiasing. - 2D shadowcasters with soft shadows. @@ -284,6 +284,18 @@ Only features that require maximum few weeks of work are listed here. being said, almost everything you find in comfy should work to a reasonable extent. +While comfy is ready to use, the codebase is far from clean. The engine +evolves rapidly as we work on our games, and there are many parts that can +and will be improved. Comfy is being released before it is 100% perfect, +because even in its current state it can be very well used to make 2D games. + +There may be a few oddities you may run into, and some internals are +planned to be re-done, but anything covered by the examples should 100% +work. We have been using comfy internally for over 6 months, and a large +part of its codebase has been ported from our previous OpenGL based +engine. This doesn't mean the engine is mature, but we have had real +players play our games made with comfy. + # Contributing Comfy is still very early in its lifecycle. While it has been used to make diff --git a/comfy/examples/ecs_topdown_game.rs b/comfy/examples/ecs_topdown_game.rs index 2d375f3..90afa77 100644 --- a/comfy/examples/ecs_topdown_game.rs +++ b/comfy/examples/ecs_topdown_game.rs @@ -39,21 +39,40 @@ fn setup(c: &mut EngineContext) { } } + let sprite = AnimatedSpriteBuilder::new() + .z_index(10) + .add_animation("idle", 0.1, true, AnimationSource::Atlas { + name: "player".into(), + offset: ivec2(0, 0), + step: ivec2(16, 0), + size: isplat(16), + frames: 4, + }) + .add_animation("walk", 0.1, true, AnimationSource::Atlas { + name: "player".into(), + offset: ivec2(16 * 4, 0), + step: ivec2(16, 0), + size: isplat(16), + frames: 4, + }) + .build(); + // Spawn the player entity and make sure z-index is above the grass c.commands().spawn(( Transform::position(vec2(10.0, 10.0)), Player, - AnimatedSprite::spritesheet( - "player", - Spritesheet { rows: 1, columns: 28 }, - 0.1, - true, - 10, - splat(1.0), - WHITE, - splat(0.0), - Box::new(|_| {}), - ), + sprite, + // AnimatedSprite::spritesheet( + // "player", + // Spritesheet { rows: 1, columns: 28 }, + // 0.1, + // true, + // 10, + // splat(1.0), + // WHITE, + // splat(0.0), + // Box::new(|_| {}), + // ), )); } @@ -86,6 +105,12 @@ fn update(c: &mut EngineContext) { moved = true; } + if moved { + animated_sprite.play("walk"); + } else { + animated_sprite.play("idle"); + } + main_camera_mut().center = transform.position; } } diff --git a/comfy/examples/sprite.rs b/comfy/examples/sprite.rs index 031fd4d..e8c789f 100644 --- a/comfy/examples/sprite.rs +++ b/comfy/examples/sprite.rs @@ -14,7 +14,7 @@ fn setup(c: &mut EngineContext) { ); } -fn update(c: &mut EngineContext) { +fn update(_c: &mut EngineContext) { draw_sprite( // Drawing sprites/textures requires a TextureHandle which can be calculated from its // string name. This incurs a non-measurable overhead in hashing the string, but saves diff --git a/comfy/src/animated_sprite.rs b/comfy/src/animated_sprite.rs index 55a90cb..c5b18e9 100644 --- a/comfy/src/animated_sprite.rs +++ b/comfy/src/animated_sprite.rs @@ -1,5 +1,306 @@ use crate::*; +pub struct AnimatedSprite { + pub animations: HashMap, + pub state: AnimationState, + + pub z_index: i32, + pub size: Vec2, + pub color: Color, + + pub flip_x: bool, + pub flip_y: bool, + + pub blend_mode: BlendMode, + pub offset: Vec2, + + pub on_finished: ContextFn, +} + +impl AnimatedSprite { + pub fn play(&mut self, animation_name: &str) { + if let Some(animation) = self.animations.get(animation_name) { + if animation.name != self.state.animation_name { + self.state = animation.to_state(); + } + } + } + + // pub fn from_files( + // prefix: impl Into>, + // frames: i32, + // interval: f32, + // looping: bool, + // z_index: i32, + // size: Vec2, + // color: Color, + // offset: Vec2, + // on_finished: ContextFn, + // ) -> Self { + // Self { + // animations: HashMap::default(), + // state: AnimationState { + // source: AnimationSource::Files { + // prefix: prefix.into(), + // frames, + // }, + // interval, + // looping, + // timer: 0.0, + // current_frame: 0, + // }, + // + // z_index, + // size, + // color, + // + // flip_x: false, + // flip_y: false, + // + // blend_mode: BlendMode::None, + // + // offset, + // + // on_finished, + // } + // } + + // pub fn spritesheet( + // name: impl Into>, + // spritesheet: Spritesheet, + // interval: f32, + // looping: bool, + // z_index: i32, + // world_size: Vec2, + // color: Color, + // px_offset: Vec2, + // on_finished: ContextFn, + // ) -> Self { + // Self { + // animations: HashMap::default(), + // state: AnimationState { + // source: AnimationSource::Spritesheet { + // name: name.into(), + // spritesheet, + // }, + // interval, + // looping, + // timer: 0.0, + // current_frame: 0, + // }, + // + // z_index, + // size: world_size, + // color, + // + // flip_x: false, + // flip_y: false, + // + // blend_mode: BlendMode::None, + // + // offset: px_offset, + // + // on_finished, + // // on_finished, + // // on_finished_meta: Arc::new(Mutex::new(None as Option<()>)), + // } + // } + // + // pub fn atlas( + // name: impl Into>, + // offset: IVec2, + // step: IVec2, + // sprite_size: IVec2, + // frames: i32, + // interval: f32, + // looping: bool, + // z_index: i32, + // world_size: Vec2, + // color: Color, + // px_offset: Vec2, + // on_finished: ContextFn, + // ) -> Self { + // Self { + // animations: HashMap::default(), + // state: AnimationState { + // source: AnimationSource::Atlas { + // name: name.into(), + // offset, + // step, + // size: sprite_size, + // frames, + // }, + // interval, + // looping, + // timer: 0.0, + // current_frame: 0, + // }, + // + // z_index, + // size: world_size, + // color, + // + // flip_x: false, + // flip_y: false, + // + // blend_mode: BlendMode::None, + // + // offset: px_offset, + // + // on_finished, + // } + // } + + pub fn with_blend_mode(self, blend_mode: BlendMode) -> Self { + Self { blend_mode, ..self } + } + + pub fn to_quad_draw(&self, transform: &Transform) -> QuadDraw { + let (texture, source_rect) = self.state.current_rect(); + + QuadDraw { + transform: *transform, + texture: texture_id(&texture), + z_index: self.z_index, + color: self.color, + blend_mode: self.blend_mode, + dest_size: self.size * transform.scale, + source_rect, + flip_x: self.flip_x, + flip_y: self.flip_y, + } + } +} + +// impl Default for AnimatedSprite { +// fn default() -> Self { +// Self { +// animations: Default::default(), +// state: AnimationState { +// source: AnimationSource::Atlas { +// name: "1px".into(), +// offset: IVec2::ZERO, +// step: IVec2::ZERO, +// size: ivec2(16, 16), +// frames: 1, +// }, +// interval: 0.2, +// looping: true, +// timer: 0.0, +// current_frame: 0, +// }, +// +// z_index: 10, +// size: splat(1.0), +// color: WHITE, +// flip_x: false, +// flip_y: false, +// blend_mode: BlendMode::None, +// offset: Vec2::ZERO, +// on_finished: Box::new(|_| {}), +// } +// } +// } + +pub struct AnimatedSpriteBuilder { + pub animations: HashMap, + pub state: Option, + pub z_index: i32, + pub size: Vec2, + pub color: Color, + pub flip_x: bool, + pub flip_y: bool, + pub blend_mode: BlendMode, + pub offset: Vec2, + pub on_finished: Option, +} + +impl AnimatedSpriteBuilder { + pub fn new() -> Self { + Self { + animations: HashMap::new(), + state: None, + z_index: 0, + size: splat(1.0), + color: WHITE, + flip_x: false, + flip_y: false, + blend_mode: BlendMode::None, + offset: Vec2::ZERO, + on_finished: None, + } + } + + pub fn z_index(mut self, z_index: i32) -> Self { + self.z_index = z_index; + self + } + + pub fn add_animation( + mut self, + name: &str, + frame_time: f32, + looping: bool, + source: AnimationSource, + ) -> AnimatedSpriteBuilder { + let animation = Animation { + name: name.to_string(), + frame_time, + looping, + source: source.clone(), + }; + + if self.state.is_none() { + self.state = Some(animation.to_state()); + } + + + self.animations.insert(name.to_string(), animation); + + self + } + + pub fn build(self) -> AnimatedSprite { + AnimatedSprite { + animations: self.animations, + state: self + .state + .expect("AnimatedSpriteBuilder's `state` must be set."), + z_index: self.z_index, + size: self.size, + color: self.color, + flip_x: self.flip_x, + flip_y: self.flip_y, + blend_mode: self.blend_mode, + offset: self.offset, + on_finished: self.on_finished.unwrap_or_else(|| Box::new(|_| {})), + } + } +} + +#[derive(Clone, Debug)] +pub struct Animation { + // TODO: we need a better way of identifying animations when doing .play() + // to avoid excessive string allocations + pub name: String, + pub source: AnimationSource, + pub looping: bool, + pub frame_time: f32, +} + +impl Animation { + pub fn to_state(&self) -> AnimationState { + AnimationState { + animation_name: self.name.clone(), + source: self.source.clone(), + interval: self.frame_time, + looping: self.looping, + timer: 0.0, + current_frame: 0, + } + } +} + #[derive(Clone, Debug)] pub enum AnimationSource { Files { @@ -33,6 +334,7 @@ impl AnimationSource { #[derive(Clone, Debug)] pub struct AnimationState { + pub animation_name: String, pub source: AnimationSource, pub interval: f32, pub looping: bool, @@ -41,8 +343,14 @@ pub struct AnimationState { } impl AnimationState { - pub fn new(source: AnimationSource, time: f32, looping: bool) -> Self { + pub fn new( + animation_name: String, + source: AnimationSource, + time: f32, + looping: bool, + ) -> Self { Self { + animation_name, looping, interval: time / source.frames() as f32, timer: 0.0, @@ -118,202 +426,3 @@ impl AnimationState { } } } - -pub struct AnimatedSprite { - pub animations: HashMap, - - pub state: AnimationState, - pub z_index: i32, - pub size: Vec2, - pub color: Color, - - pub flip_x: bool, - pub flip_y: bool, - - pub blend_mode: BlendMode, - - pub offset: Vec2, - - pub on_finished: ContextFn, -} - -impl Default for AnimatedSprite { - fn default() -> Self { - Self { - animations: Default::default(), - state: AnimationState { - source: AnimationSource::Atlas { - name: "1px".into(), - offset: IVec2::ZERO, - step: IVec2::ZERO, - size: ivec2(16, 16), - frames: 1, - }, - interval: 0.2, - looping: true, - timer: 0.0, - current_frame: 0, - }, - - z_index: 10, - size: splat(1.0), - color: WHITE, - flip_x: false, - flip_y: false, - blend_mode: BlendMode::None, - offset: Vec2::ZERO, - on_finished: Box::new(|_| {}), - } - } -} - -impl AnimatedSprite { - pub fn from_files( - prefix: impl Into>, - frames: i32, - interval: f32, - looping: bool, - z_index: i32, - size: Vec2, - color: Color, - offset: Vec2, - on_finished: ContextFn, - ) -> Self { - Self { - animations: HashMap::default(), - state: AnimationState { - source: AnimationSource::Files { - prefix: prefix.into(), - frames, - }, - interval, - looping, - timer: 0.0, - current_frame: 0, - }, - - z_index, - size, - color, - - flip_x: false, - flip_y: false, - - blend_mode: BlendMode::None, - - offset, - - on_finished, - } - } - - pub fn spritesheet( - name: impl Into>, - spritesheet: Spritesheet, - interval: f32, - looping: bool, - z_index: i32, - world_size: Vec2, - color: Color, - px_offset: Vec2, - on_finished: ContextFn, - ) -> Self { - Self { - animations: HashMap::default(), - state: AnimationState { - source: AnimationSource::Spritesheet { - name: name.into(), - spritesheet, - }, - interval, - looping, - timer: 0.0, - current_frame: 0, - }, - - z_index, - size: world_size, - color, - - flip_x: false, - flip_y: false, - - blend_mode: BlendMode::None, - - offset: px_offset, - - on_finished, - // on_finished, - // on_finished_meta: Arc::new(Mutex::new(None as Option<()>)), - } - } - - pub fn atlas( - name: impl Into>, - offset: IVec2, - step: IVec2, - sprite_size: IVec2, - frames: i32, - interval: f32, - looping: bool, - z_index: i32, - world_size: Vec2, - color: Color, - px_offset: Vec2, - on_finished: ContextFn, - ) -> Self { - Self { - animations: HashMap::default(), - state: AnimationState { - source: AnimationSource::Atlas { - name: name.into(), - offset, - step, - size: sprite_size, - frames, - }, - interval, - looping, - timer: 0.0, - current_frame: 0, - }, - - z_index, - size: world_size, - color, - - flip_x: false, - flip_y: false, - - blend_mode: BlendMode::None, - - offset: px_offset, - - on_finished, - // on_finished, - // on_finished_meta: Arc::new(Mutex::new(None as Option<()>)), - } - } - - pub fn with_blend_mode(self, blend_mode: BlendMode) -> Self { - Self { blend_mode, ..self } - } - - pub fn to_quad_draw(&self, transform: &Transform) -> QuadDraw { - let (texture, source_rect) = self.state.current_rect(); - - QuadDraw { - transform: *transform, - texture: texture_id(&texture), - z_index: self.z_index, - color: self.color, - blend_mode: self.blend_mode, - dest_size: self.size * transform.scale, - source_rect, - flip_x: self.flip_x, - flip_y: self.flip_y, - } - } -} - -pub struct AnimatedSpriteBuilder {} diff --git a/comfy/src/animation.rs b/comfy/src/animation.rs index 5eb3fc7..fb36353 100644 --- a/comfy/src/animation.rs +++ b/comfy/src/animation.rs @@ -1,7 +1,7 @@ use crate::*; #[derive(Clone, Debug)] -pub struct Animation { +pub struct SimpleAnimation { pub name: Cow<'static, str>, pub sheet: Spritesheet, pub current_frame: usize, @@ -10,7 +10,7 @@ pub struct Animation { pub frame_range: Option<(usize, usize)>, } -impl Animation { +impl SimpleAnimation { // pub fn new(name: impl Into>, sheet: Spritesheet) -> Self { // let frames = sheet.rows * sheet.columns; // let animation_time = 10.0; diff --git a/games/bitmob/Cargo.toml b/games/bitmob/Cargo.toml new file mode 100644 index 0000000..6c29a96 --- /dev/null +++ b/games/bitmob/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "bitmob" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +comfy = { path = "../../comfy" } diff --git a/games/bitmob/src/main.rs b/games/bitmob/src/main.rs new file mode 100644 index 0000000..c6ac9e0 --- /dev/null +++ b/games/bitmob/src/main.rs @@ -0,0 +1,7 @@ +use comfy::*; + +simple_game!("BITMOB", update); + +fn update(c: &EngineContext) { + draw_circle(Vec2::ZERO, 2.0, PINK, 0); +}