hecrj / coffee Goto Github PK
View Code? Open in Web Editor NEWAn opinionated 2D game engine for Rust
Home Page: https://docs.rs/coffee
License: MIT License
An opinionated 2D game engine for Rust
Home Page: https://docs.rs/coffee
License: MIT License
I think we could extend the Debug
type with some methods to allow games to easily customize the debug view.
Here is how it currently looks:
Here are a couple of features that would be nice to have:
The API should be declarative (i.e. no side-effects until Debug::draw
). Feel free to share any more ideas here!
Some ideas possibly worth implementing:
Color::hex(0xFFFFFF)
This is probably a winit
bug ๐ฐ
Maybe a Window::toggle_fullscreen
method?
Task::new
doesn't expect a Result
, but Task::from_gpu
does. I'm loading a non-image data file using serde_json
; which I figured made sense to put inside a Task
.
I'd initially started with Task::new
because i don't need a Gpu
but it doesn't expect a Result
, and neither does Game.load
, so I found myself needing to unwrap the Result<(), MyError>
during the .map
portion of task loading.
I eventually ended up using Task::using_gpu
like so
Task::using_gpu(|_gpu| load_data().map_err(|e| coffee::Error::from(e)))
with a custom Error
enum providing the From
s for i.e. io::Error
, serde_json::Error
, and coffee::Error
(converting everything into a coffee::Error::IO
variant because that seemed like the only potentially relevant category of error.
On a somewhat related note, I have a config file that I'm currently loading twice because it specifies the WindowSettings
size, which it's not clear to me how to only load this file during the load
Task process. Maybe this doesn't make sense, given that you need an initial display size to display e.g. the progress bar. but at the same time I didn't see a way to programmatically resize the display after load either.
The current wgpu
graphics backend implementation in Coffee is quite naive and inefficient when performing multiple draw calls (benchmarks needed, though!). It scales quite well when using Batch
, though. I get better performance in the particles
example using wgpu
.
In any case, I am a total beginner in computer graphics. I basically learned about Vulkan 3 weeks ago, and I am pretty sure the current implementation can be improved a lot.
I was able to get quite a speedup by only submitting the work once every frame here.
Currently, here is basically where most of the work happens. I suspect that creating mapped buffers every time is not a good idea. Should we implement something like a buffer pool?
Also, because a SwapChainOutput
is bound to the SwapChain
by a lifetime, we are currently rendering on a texture and then copying it here. We could maybe tie the SwapChainOutput
to a Frame
, keep the frame alive in the game loop and pass that into Game::draw
instead of a Window
, consuming it at the end of the iteration.
I would greatly appreciate any insights from more experienced people on how to proceed here.
In order to avoid directly depending on the SPIR-V compiler when building the crate, we are currently providing the compiled shaders for the wgpu
graphics backend here.
It would be great to have CI validate that these binaries are up-to-date with the shader code. I guess we could just simply recompile the code in CI (using glslangValidator -V
) and check for differences with Git.
As of now, Coffee converts logical coordinates into physical coordinates (examples here and here) for everything.
However, the winit::dpi
documentation seems to hint that we should instead expose logical coordinates everywhere and apply a scaling transformation when rendering on screen. This can be easily implemented by making Frame
apply a scaling transformation using Target::with_transform
in the as_target
method here.
This would have the benefit that games will look similar in size in different devices. However, targeting specific pixels when rendering becomes harder. Can this cause graphical glitches? Maybe Window
could expose the dpi
value so developers can undo the dpi
scaling if they deem necessary? What about a different type, like Frame
but that does not apply a DPI transformation when seen as a Target
?
We can simply wrap nalgebra::Matrix4::new_rotation
.
The wgpu
backend does not currently render any text.
The gfx
backend is using gfx_glyph
, which was pretty simple to integrate. For the wgpu
backend, we could use glyph_brush
directly. It won't be as easy to integrate, but I think it should be doable.
We only have a quite simple ProgressBar
loading screen built into Coffee currently. It would be great to have more diversity!
This can be a great way to get familiar with the graphics
module of the engine and contribute at the same time. If you want to give it a shot, start by taking a look at the LoadingScreen
trait!
I think we could introduce a Viewport
type combining a Canvas
and a Transformation
once we deal with #6 and #9.
This idea came up in the #game-dev channel in the Rust Community Discord:
We could use gilrs
. I have never dealt with gamepad integration in a game, so any insights here will be greatly appreciated.
I imagine we could simply extend input::Event
to add gamepad events.
We need a widget to ask users for text input. This seems to be a highly interactive widget that may pose a real challenge. We could start simple and forget about text selection, copy, cut, paste, etc.
Here are a couple of challenges that I see:
glyph-brush
to get this working properly.Canvas
to emulate it, but it may be tricky with the current Widget
API. We may need to change it in order to make rendering more composable.This is the kind of API I would personally like:
pub enum Message {
TextChanged(String),
}
let state = &mut text_input::State::new();
let value = String::from("Some text!");
TextInput::new(state, &value).on_change(Message::TextChanged);
I found this issue when working on #35.
Right now, the UI renderer applies a correction when measuring Text
widgets. This is a dirtyfix and it probably doesn't work well with different fonts.
Here is what happens if we do not apply the correction with Inconsolata Regular:
I think GlyphBrush::pixel_bounds
is not returning a correct value. We should probably investigate why and file an issue/PR in the glyph-brush
repo.
Scrolling is quite a basic feature for a UI toolkit. We need it!
I see two different approaches to tackle this:
Canvas
. The issue is that we would need to recreate it once the scrollable changes dimensions. We could circumvent this initially by forcing users to set a fixed width
and height
for scrollables. However, this destroys the purpose of a responsive UI. Another approach could be simply avoid resizing the Canvas
every frame while resizes happen, and only do it at a specific rate. Although this would cause the UI to not resize smoothly, it could be good enough for now.Target
. This sounds way more elegant, but it entails more work as we will have to add scissor support for all the current pipelines (quads and font rendering). It should be doable, but it needs to be done carefully.In both scenarios, Widget::draw
definition will probably need to change to improve composability and recursive draws (scrollables inside scrollables). Although I think a tree-like data structure stored in Renderer
could work too.
Any other ideas?
We could split KeyboardAndMouse
into two structs: Keyboard
and Mouse
, and use composition. Then it could be used like this (particles example):
fn interact(&mut self, input: &mut KeyboardAndMouse, window: &mut Window) {
let mouse = input.mouse();
self.gravity_centers[0] = mouse.cursor_position();
self.gravity_centers.extend(mouse.button_clicks(mouse::Button::Left));
let keyboard = input.keyboard();
if keyboard.was_key_released(keyboard::KeyCode::I) {
self.interpolate = !self.interpolate;
}
if keyboard.was_key_released(keyboard::KeyCode::F) {
window.toggle_fullscreen();
}
}
Both Keyboard
and Mouse
could implement Input
too.
This is probably outside of the scope of the game engine, but is it possible to use the current target or frame as an image type from the Image crate? My goal is essentially to save the frame as an image file. Thanks
A Font
currently represents a bunch of text with a single font that can be drawn. However, glyph_brush
supports rendering text with multiple fonts with a single draw call.
We should rethink the API so we can take advantage of this. Maybe the Text
type should hold a collection of Section
s, which have a Font
attached to them. Then, we store the GlyphBrush
type on the Gpu
type instead of Font
and we make Font
contain just a FontId
.
Something I have enjoyed in other game frameworks is the ability to debug ideas by first using shape primitives such as circles, rectangles, lines, etc, using some sort of shape renderer. Off the top of my head LibGDX, as well as Love2D support this feature.
I propose a similar API for prototyping with Coffee.
A ShapeBatch
type and possibly rename Batch
to SpriteBatch
to avoid confusion.
Some possible methods:
impl ShapeBatch {
color(&mut self, color: Color) -> &mut self;
circle(&mut self, x: f32, y: f32, radius: f32) -> &mut self;
line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32) -> &mut self;
rectangle(&mut self, x: f32, y: f32, width: f32, height: f32) -> &mut self;
}
Where each method returns self
, or ShapeBatch
for method chaining if desired.
The implementation would likely require a separate render pass, shader set, and a different per-vertex definition with just position and color. Fill vs Line drawing per shape would need to change the primitive type from lines to triangles and generate slightly different geometry.
Of course, this is just a rough idea and can use much improvement and could be improved by debate. Let me know what you think of the idea.
Hello! I'm having some trouble with borrowing when integrating Coffee and specs. The Coffee interact function borrows &mut State
which is where I'm storing my specs World
. When I try to run a specs System on the World
it blows up with a alloc::boxed::Box already borrowed mutably error. Is there a way I can make this work? Is there any prior art for coffee + specs?
I'm re-posting here so I don't lose it in the Discord again!
Your response:
You shouldn't need to do anything special to get specs to work with Coffee. Simply store it in your type implementing Game, and you should be able to access it normally from interact and update.
โ @hecrj
I'm storing the World
on my type implementing Game
as suggested which is where I believe the borrow error is coming from.
a few days later
After you said it should just work I took another crack and worked it out. Here's the code with the fix in case it helps other folks. The problematic code and fix is in the interact
function.
use coffee::{
graphics::{Color, Frame, Window, WindowSettings},
load::Task,
Game, Result, Timer,
};
use specs::{prelude::*, Builder, Component, VecStorage, World, Write, WriteStorage};
#[macro_use]
extern crate specs_derive;
#[derive(Component, Debug)]
#[storage(VecStorage)]
pub struct Foo;
#[derive(Component, Debug, Default)]
#[storage(VecStorage)]
pub struct Bar {
baz: f32,
}
pub struct System;
impl<'a> specs::System<'a> for System {
type SystemData = (WriteStorage<'a, Foo>, Write<'a, Bar>);
fn run(&mut self, (_, _): Self::SystemData) {
//
}
}
struct State {
world: World,
}
impl Game for State {
type Input = ();
type LoadingScreen = ();
fn load(_window: &Window) -> Task<Self> {
Task::new(move || {
let mut world = World::new();
world.register::<Foo>();
world.insert(Bar { baz: 42.0 });
world.create_entity().with(Foo {}).build();
State { world }
})
}
fn interact(&mut self, _input: &mut (), _window: &mut Window) {
// Breaks:
let mut bar = self.world.write_resource::<Bar>();
*bar = Bar { baz: 43.0 };
// Fixed:
// {
// let mut bar = self.world.write_resource::<Bar>();
// *bar = Bar { baz: 43.0 };
// }
let mut system = System;
system.run_now(&self.world);
}
fn draw(&mut self, frame: &mut Frame, _timer: &Timer) {
frame.clear(Color::BLACK);
}
}
fn main() -> Result<()> {
State::run(WindowSettings {
title: String::from("Foo"),
size: (1280, 1024),
resizable: true,
fullscreen: false,
})
}
Thanks!
We should probably follow the Keep a Changelog format.
First of all thanks for all the hard work and coffee
is rad, but i would like to report something i found.
When i am trying to run the examples from https://github.com/hecrj/coffee/tree/master/examples
But instead of cloning the repository and then running them, i am using coffee
as a dependency.
I am adding coffee
to Cargo.toml
like this.
[dependencies]
coffee = { version = "0.2", features = ["vulkan"] }
When i now copy the examples and try to run them, i get trait mismatch errors in all examples,
for example such.
--> src/main.rs:36:5
|
36 | fn draw(&self, view: &mut Self::View, frame: &mut Frame, _timer: &Timer) {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected struct `coffee::graphics::window::Window`, found struct `coffee::graphics::window::frame::Frame`
|
= note: expected type `fn(&Colors, &mut View, &mut coffee::graphics::window::Window, &coffee::timer::Timer)`
found type `fn(&Colors, &mut View, &mut coffee::graphics::window::frame::Frame<'_>, &coffee::timer::Timer)`
Using a debug build, opt-level=2
on win10 I observe low framerate for the first few ~5 seconds, only when the debug timing view is enabled.
If we stop the Text
widget from filling the width of its parent by default and its parent aligns items using Align::Start
(the default), Text
just keeps growing horizontally with no limit:
I don't think this issue can be produced with the current API, so this is an improvement instead of a bug for now.
It seems that stretch
chooses the wrong bounding box for the Node
. Text
should shrink and have its own width, while filling horizontal space of its parent when possible. Maybe I am wrong, but I think that is the whole point of measure functions.
Additionally, when I implemented the Text
widget, I noticed that stretch
was calling the measure function provided in Node::with_measure
way too many times. I was able to optimize this by using a RefCell
to cache the first measurement, which seems to be the only one that matters. This last assumption could be wrong, but toggling this optimization seems to have no effect on the issue described above.
In any case, I think both things could be related. We should try to create a simple test in the stretch
codebase reproducing the problem and open an issue/PR.
Right now, Coffee only exposes three input events (keyboard input, mouse input, and mouse movement).
We should probably expose additional useful events like CursorLeft
, CursorEntered
, MouseWheel
, etc.
When rendering over 1k PolyLines on a single mesh, as the number of lines increased, I saw other lines disappear and a gradient streak appear, stretching from the top-left corner towards the middle where some line was rendered. See the below image:
Given that my code works for a lower number of lines perfectly, I tried to allocate more than 1 mesh for drawing all the lines. Distributing the total number of lines among several meshes seems to have fixed the bug. I assume that this bug relates to some max bound for how many shapes a mesh can hold, but have not been able to find evidence of that in the documentation. If this bound does exist, it should be documented.
the link in the docstring for pub fn next_tick_proximity(&self) -> f32
is dead i.e. https://gafferongames.com/post/fix_your_timestep/
The current OpenGL graphics backend is implemented using the deprecated gfx
crate. While it works, it seems to be performing a lot of unnecessary state changes that may be affecting performance.
I think we could try to rewrite the backend using a well-maintained crate. Maybe glow
?
We should have a widget to simply render a graphics::Image
:
let image = graphics::Image::new(gpu, "my_image.png");
widget::Image::new(&image).clip(Sprite { /* ... */ });
It should try to fill its parent and keep the correct aspect ratio at all times.
This should be similar in difficulty to #45.
A ProgressBar
widget should be easy to implement. It must fill the parent width by default. The implementation of the current widgets could be used as a guide.
ProgressBar::new(percentage);
For consistency, we should probably choose sprites from Kenney UI packs.
This is just an idea. I do not know how practical it is, but I think it is worth a shot.
I am quite a fan of integration tests.
We could implement a suite of image-based integration tests for the graphics
module. We would render to an image and compare it with an expected image pixel by pixel. If a test fails, we could keep the wrong image and mark the differences somehow (maybe another transparent image pointing the pixels that differ). We could have simple tests and very complicated ones, with off-screen rendering, text, batched draws and texture arrays, etc.
This will give us a lot of confidence when making changes/optimizations to the different graphics backends, or when implementing a new one. Also, I think it would be really cool.
I would like to do this myself, but any help or insights will be greatly appreciated. Share your thoughts here first!
I apparently forgot to update this method.
As of now, the only way to gracefully quit a Game
after Game::run
is to implement Game::on_close_request
and wait for a close request.
However, it is very common in games to have a UI option to "Quit the game". While we can use std::process::exit
, it's not ideal as it doesn't gracefully stop the execution.
Therefore, I think we need to rethink the Game
API a bit to allow graceful quits on demand.
I guess we could use rodio
.
The tricky part will be exposing an interesting API for it, and how to extend the current Game
trait to make it work. Maybe Track
or Sound
types as resources, and something like a Speaker
with a play
method?
I don't think we should allow playing audio from update
(the same way we do not allow rendering), so we will probably need a new provided method in the Game
trait to play audio. And also a new associated type Audio
to store the audio data.
We could implement a Gamepad
input handler, similar to the KeyboardAndMouse
type. This type would easily allow to check the current gamepad state: buttons, axes, etc.
We could use gilrs::ev::state::GamepadState
as a source of inspiration.
As of now, there is no way to set a rotation/color per quad/sprite. This is a really basic feature.
We could add support for it easily here and here. However, some games do not really need to use this feature and sending additional data to the GPU has a cost.
Before implementing this, I think we should write some basic benchmarks so we can discuss how to proceed (see #13). Maybe the overhead is negligible (I doubt it)? Maybe we can implement a different pipeline with a different shader and smartly select it at runtime? Maybe instanced drawing is not a good idea once we add more data? Maybe we can add it as a feature?
We should implement a Game::on_close_request
and use its return value here to decide whether we should end the game loop or not.
There is no way to draw a triangle right now!
It would be cool to have something like ggez's MeshBuilder
. I think it uses lyon
under the hood.
I will need this for my game so I will work on this pretty soon, unless someone else wants to tackle it!
This could be used to create a Game
differently based on command line arguments or any other external configuration already loaded before calling Game::run
.
Currently, Coffee does only nearest neighbor interpolation.
We should probably allow to change this somehow. Is texture filtering normally a parameter that stays constant for a particular game? Could it be a simple configuration field on initialization?
Possible improvements:
Custom shaders aren't supported atm, right? Are there any plans for it?
Right now, text can only be rendered on a Frame
. However, it should be theoretically possible to render text on any Target
.
In order to implement this properly, some changes in glyph_brush
will be necessary:
Transformation
directly, and it makes target dimensions unnecessary, simplifying the Gpu::draw_font
method.gfx_glyph
.After this, it should be a matter of simply fitting everything together:
The current KeyboardAndMouse
implementation is quite bare-bones. We should be able to track a lot more! For instance:
OS: Win10
Version: crates.io
Just minimized the example project (opengl + debug feat.) and it crashed with the following error:
thread 'main' panicked at 'The left corner must not be equal to the right corner.', C:\Users\USER\.cargo\registry\src\github.com-1ecc6299db9ec823\nalgebra-0.18.0\src\geometry\orthographic.rs:622:9
stack backtrace:
0: backtrace::backtrace::trace_unsynchronized
at C:\Users\VssAdministrator\.cargo\registry\src\github.com-1ecc6299db9ec823\backtrace-0.3.29\src\backtrace\mod.rs:66
1: std::sys_common::backtrace::_print
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\sys_common\backtrace.rs:47
2: std::sys_common::backtrace::print
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\sys_common\backtrace.rs:36
3: std::panicking::default_hook::{{closure}}
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\panicking.rs:200
4: std::panicking::default_hook
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\panicking.rs:214
5: std::panicking::rust_panic_with_hook
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\panicking.rs:477
6: std::panicking::begin_panic<str*>
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\src\libstd\panicking.rs:411
7: nalgebra::geometry::orthographic::Orthographic3<f32>::new
at C:\Users\USER\.cargo\registry\src\github.com-1ecc6299db9ec823\nalgebra-0.18.0\src\geometry\orthographic.rs:0
8: nalgebra::base::matrix::Matrix<f32, nalgebra::base::dimension::U4, nalgebra::base::dimension::U4, nalgebra::base::array_storage::ArrayStorage<f32, nalgebra::base::dimension::U4, nalgebra::base::dimension::U4>>::new_orthographic
at C:\Users\USER\.cargo\registry\src\github.com-1ecc6299db9ec823\nalgebra-0.18.0\src\base\cg.rs:118
9: coffee::graphics::transformation::Transformation::orthographic
at C:\Users\USER\.cargo\registry\src\github.com-1ecc6299db9ec823\coffee-0.3.1\src\graphics\transformation.rs:30
10: coffee::graphics::target::Target::new
at C:\Users\USER\.cargo\registry\src\github.com-1ecc6299db9ec823\coffee-0.3.1\src\graphics\target.rs:32
11: coffee::graphics::window::frame::Frame::as_target
at C:\Users\USER\.cargo\registry\src\github.com-1ecc6299db9ec823\coffee-0.3.1\src\graphics\window\frame.rs:55
12: coffee::graphics::window::frame::Frame::clear
at C:\Users\USER\.cargo\registry\src\github.com-1ecc6299db9ec823\coffee-0.3.1\src\graphics\window\frame.rs:62
13: example::gui::{{impl}}::draw
at .\client\src\gui.rs:29
14: coffee::game::Game::run<example::gui::MyGame>
at C:\Users\USER\.cargo\registry\src\github.com-1ecc6299db9ec823\coffee-0.3.1\src\game.rs:217
15: example::gui::start
at .\client\src\gui.rs:6
16: example::main
at .\client\src\main.rs:4
17: std::rt::lang_start::{{closure}}<()>
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\src\libstd\rt.rs:64
18: std::rt::lang_start_internal::{{closure}}
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\rt.rs:49
19: std::panicking::try::do_call<closure,i32>
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\panicking.rs:296
20: panic_unwind::__rust_maybe_catch_panic
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libpanic_unwind\lib.rs:82
21: std::panicking::try
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\panicking.rs:275
22: std::panic::catch_unwind
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\panic.rs:394
23: std::rt::lang_start_internal
at /rustc/eae3437dfe991621e8afdc82734f4a172d7ddf9b\/src\libstd\rt.rs:48
24: main
25: invoke_main
at d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:78
26: __scrt_common_main_seh
at d:\agent\_work\3\s\src\vctools\crt\vcstartup\src\startup\exe_common.inl:288
27: BaseThreadInitThunk
28: RtlUserThreadStart
error: process didn't exit successfully: `target\debug\example.exe` (exit code: 101)
I guess this can be solved in here.
It would be great to create some actual game examples to showcase the engine as a whole. It should also help us understand the shortcomings of the engine and serve us to choose what to improve next!
The examples should be simple, so we shouldn't go too crazy with the game complexity. I think simple games like tetris, breakout, pong, etc., are the way to go. We can use OpenGameArt.org for any assets we need and give proper credits in the examples README
!
This is the perfect chance if you want to get familiar with the engine while contributing at the same time!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.