Bevy Game Engine, Embedded Graphics Updates, and Enforcing API Spec with Traits
Topics:
- Wheel of Names with Bevy via @DanielPBank
- Embedded Graphics Updates via @jacobrosenthal
- Making a Tiny Game with Raylib via @mysteriouspants
- Enforcing Api Specs with Traits @JesusGuzmanJr
Wheel of Names with Bevy via @DanielPBank
Repo: https://github.com/danielbank/ferris-wheel
I'm attempting to build a Rust port of the Wheel of Names JS App using Bevy Game Engine. Currently, Bevy does not support drawing custom shapes in an easy way. Fortunately, there is a Bevy plugin called, bevy_prototype_lyon which provides this functionality using lyon, which implements 2D Graphics rendering on the GPU using tessellation.
Make a Wheel
I started with the bevy_prototype_lyon example code and made a circle. The code for this is straightforward
let wheel = shapes::Circle { radius: 200.0, ..shapes::Circle::default() }; commands.spawn_bundle(GeometryBuilder::build_as( &wheel, ShapeColors::outlined(Color::TOMATO, Color::BLACK), DrawMode::Outlined { fill_options: FillOptions::default(), outline_options: StrokeOptions::default().with_line_width(10.0), }, Transform::default(), ));
Make the Wheel Spin
To figure out how to do motion, I copied from the Bevy Breakout Game example, here I learned some things that surprised me.
Query Components Not Entities
Bevy follows the Entity Component System paradigm so we need to understand a little bit about that in order to move things. Specifically to move an entity, we need to first query for a component of that specific entity before applying transformations:
if let Ok((ball, mut transform)) = ball_query.single_mut() {
transform.translation += ball.velocity * delta_seconds;
}
Naively, we may want to try to do something with the Shapes (e.g. wheel = shapes::Circle { ... }
) or the ShapeBundles (e.g. GeometryBuilder::build_as( ... )
) to get them to move. However, Bevy wants us to interact with Components which are structs attached to Entities. We attach these components to entities by calling .insert()
on the ShapeBundle we get back from GeometryBuilder::build_as( ... )
.
commands
.spawn_bundle(GeometryBuilder::build_as( ... )
.insert(Component {});
The components could even be an empty structs, we simply need something to get a handle on when we query the system for things to transform.
Since I wanted to apply my rotation movement to all the Entities in my app, I didn't use single_mut()
, which matches a single entity, and instead iterated over:
particles_query
.iter_mut()
.for_each(|(particle, mut transform)| {
transform.rotate(Quat::from_rotation_z(FRAC_PI_2));
});
Caveat about Getting Past an Initial wgpu Validation Error
When I first tried running the bevy_prototype_lyon example code, I was getting the following error:
wgpu error: Validation Error
Caused by:
In a RenderPass
note: encoder = `<CommandBuffer-(1, 2, Metal)>`
In a pass parameter
note: command buffer = `<CommandBuffer-(1, 2, Metal)>`
attachment's sample count 8 is invalid
I fixed this issue by changing the Msaa { samples: 8 }
to Msaa { samples: 4 }
. Some light reading on Multisample anti-aliasing revealed that most modern GPUs support 2×, 4×, and 8× MSAA samples. It looks like my 2015 MacBook Pro is starting to show its age.
Embedded Graphics Updates via @jacobrosenthal
The embedded-graphics crate is getting close to a 0.7.0 release and lots of cool stuff is now possible.
-
Embedded Graphics Simulator: You don't have to run on a device to test your graphics, you can run in the simulator which opens an SDL2 window for previewing. Here is a tweet demo'ing compound animations (made using the simulator).
-
Embedded Graphics GUI: Building upon the low-level code of the embedded-graphics crate, there is work going into building a composable UI framework which allows building more complex UI. Here is a tweet demo'ing of the new UI framework in action.
-
Embedded Text: Provides TextBox functionality (how do you flow text into a box, align the text, prevent it from overflowing, etc.) Here is a demo of a typing animation.
-
Here are Even More Embedded Graphics Examples
The Pygamer Board Support Crate already uses (an older version of) embedded-graphics, so a lot of these recent advancements can hopefully be made available in it soon.
Enforcing Api Specs with Traits via @JesusGuzmanJr
Jesus demonstrates how to use traits to enforce restful apis.
Motivation
I've been experimenting with Rust to gauge its suitability as a fullstack programming language for web dev shops. How can Rust help a team of developers write code that is easy to maintain and less buggy?
One way to do this is by leveraging Rust's type system.
This tutorial assumes that you have familiarity with an http web framework like flask
, Django
, Spring
, Vertx
, gin
etc.
Aside: please excuse the mal-formamted code. I attempted condensed the line width to make it more legible on narrower screens.
Say I have a fullstack webapp composed of 3 crates:
common
- common code that's shared by other crateswebapp
- webapp code that runs in browserserver
- server code that runs on cloud
First I defined a Fetch
trait. When a type implements Fetch
, that means it can be fetched from my server. In common/src/fetch.rs
:
pub trait Fetch { // implement for a Response type type Request; // the associated Request type const RATE_LIMIT_SEC: usize; const ENABLE_ANTICSRF_PROTECTION: bool; fn path() -> Path; }
Now I can define the types for a particular endpoint of my api. In common/src/api/user.rs
:
pub struct Request { // (1) pub username: Username, pub email: Email, pub password: Password, } pub enum Response { Created(User), UsernameUnavailable, EmailUnavailable, } impl Fetch for Response { // the associated Request type is Request (1) type Request = Request; const ENABLE_ANTICSRF_PROTECTION: bool = true; const RATE_LIMIT_SEC: usize = 3; fn path() -> Path { Path::CreateUser } }
Now that we have a Fetch
type, we need to define a fetch function to use Fetch
types. Let's jump to our webapp. In webapp/src/request
:
pub async fn fetch<Req, Res>(request: &Req) -> Result<Res, Error> where Req: Serialize, // (1) // (2) (3) Res: Fetch<Request = Req> + DeserializeOwned, { use seed::browser::fetch::Request as HttpRequest; let mut request = HttpRequest::new(Res::path()) // I use POST for all apis :P .method(Method::Post) .bytes(rmp_serde::to_vec(request)?) // (1) required here .header(build_message_pack_header()); // Since `Res` is `Fetch` // we can grab any of the associated items like // associated types or // associated constants // from the `Fetch` trait // In this case we grab the associated boolean constant if Res::ENABLE_ANTICSRF_PROTECTION { // add the anti-cross-site-request-forgery header } // the request function comes from Seed-rs let bytes = request .fetch() .await? // I use 200 OK for apis :P .check_status()? .bytes() .await? .as_slice(); let response = rmp_serde::from_read_ref(bytes)?; Ok(response) }
- Notice how
fetch<Req, Res>(_: &Req) -> Result<Res, Error> where ...
is generic over both a request and respond type. The only constraint on the request parameter is that it must be serializable (1) - The constraint of the response type is that is must be
Fetch
and its associated Request type (3) must be the same type as the input parameter (4)
Let's learn how to call this fetch function to make http requests from our webapp to our server.
In webapp/srs/pages/signup.rs
:
// create our request object let request = api::user::Request { // ~snip~ }; // and just fetch it! let response = request::fetch(&request).await; // make some matcha while we're at it! match response { Err(error) => { seed::log(error); // log to browser console :( } Ok(Response::Created(_newborn_user)) => () Ok(Response::UsernameUnavailable) => () Ok(Response::EmailUnavailable) => () }
Now let's take a look at our server. In server/src/responder.rs
:
#[async_trait::async_trait] pub trait FetchResponder: Fetch + Sized { // (1) async fn respond(request: Self::Request) -> Result<Self, Error>; }
We define a new trait called FetchResponder
. Any type that is FetchResponder
can be responded with as the payload of an http response.
- Also notice that
FetchResponder
can only be implemented on types that areFetch
(1). That means that a type that isFetchResponder
is alsoFetch
.
Recall that we would normally create an http request handler for each of our api items. While this process is not difficult, it is tedious and requires manual work. (E.g. read the api doc and construct the proper http response to send back)
To improve upon this situation, we'll define a generic request handler that implements the http, non-variable parts or our api while leaving generic the variable, api types.
// (1) pub async fn respond<F>(request: HttpRequest, bytes: Bytes) -> Result<HttpResponse, Error> where F: FetchResponder + Serialize, // (1) <F as Fetch>::Request: DeserializeOwned + Validate, // (2) <<F as Fetch>::Request as Validate>::Invalidity: Serialize, // (3) { if <F as Fetch>::ENABLE_ANTICSRF_PROTECTION { // perform anti-cross-site-request forgery checks // ~snip~ } // (4) let rate_limit_sec = <F as Fetch>::RATE_LIMIT_SEC; // perform rate limiting // ~snip~ // my server can handle payloads of various content types :P let format = Format::from(&request); // (5) match deserialize::deserialize::<<F as Fetch>::Request>(&bytes, format) { Err(error) => Ok( response::Builder::error( StatusCode::BAD_REQUEST, error ) ), Ok(request) => { // `Validate` required here // | // v let response = if let Err(invalidities) = request.validate() { response::Builder::error( StatusCode::BAD_REQUEST, &invalidities ) } else { // `Invalidity: Serialize` required here // | // v response::Builder::ok().serialize( &F::respond(request).await?, format // (5) )? }; Ok(response) } } }
-
Notice how
respond<F>(_: HttpRequest, _: Bytes) -> Result<HttpResponse, Error> where ...
is generic overF
yetF
is neither found as a parameter nor return type! When I first saw this, I thought it was black-magic! But it's not. In thewhere
clause we state the trait bound:F
must beFetchResponder
andSerialize
. -
This is where it gets interesting. The type
F
isFetchResponder
, but recall the definition ofFetchResponder
. Any type that isFetchResponder
is alsoFetch
! The generic typeF
that we use in this function call must implement both traits. Because of this, we can castF
into the supertraitFetch
and grab an associated item. In this case we are constraining the associated typeRequest
of the traitFetch
of the subtraitFetchResponder
of the generic typeF
to be bothDeserializeOwned
andValidate
.Note that
Validate
is a trait I created that is outside the scope of this discussion. The trait bound is required so I can validate the request payload. -
Also outside the scope of this discussion. Here I'm bounding the
Invalidity
type of theRequest
type to beSerialize
. -
We get the associated constant
RATE_LIMIT_SEC
from theFetch
trait.
Now that we have defined FetchResponder
and shown how it is used, lets implement it on our Response
type. Recall that Response
as defined in common/src/api/user.rs
looks like:
pub enum Response { Created(User), UsernameUnavailable, EmailUnavailable, }
Now back to our server in server/src/route/user.rs
:
#[async_trait::async_trait] impl FetchResponder for Response { async fn respond( Request { name, username, email, password, language, }: Request, ) -> Result<Response, Error> { type PgError = persistance::postgres::Error; let hashed_password = security::hash::hash_password(&password)?; match persistance::user::create( &name, &username, &email, &hashed_password, language ).await { Err( persistance::Error::PostgresError( PgError::UniqueViolation(violation) ) ) if violation.constraint() == Some( persistance::user::USERNAME_CONTRAINT ) => { Ok(Response::UsernameUnavailable) } Err( persistance::Error::PostgresError( PgError::UniqueViolation(violation) ) ) if violation.constraint() == Some( persistance::user::EMAIL_CONTRAINT ) => { Ok(Response::EmailUnavailable) } Err(error) => Err(error.into()), Ok((user, _credentials)) => Ok(Response::Created(user)), } } }
Notice the lack of http types. E.g. We don't have to worry abut http status codes. No json, nada of the sort. Instead we focus on calling our database and doing some matching to figure out what the db is saying.
The last part is to register our http handler with actix_web, my web framework of choice. In server/src/route.rs
:
macro_rules! fetch_responder { ($config:ident, $fetch:ty) => { let path = <$fetch as Fetch>::path(); // (2) let resource = Resource::new(path); let responder = responder::respond::<$fetch>; // (3) // I use POST methods for all apis :P let route = Route::new().method(Method::POST).to(responder); $config.service(resource.route(route)); }; } pub fn routes(config: &mut use actix_web::web::ServiceConfig) { // ~snip~ fetch_responder!(config, api::user::Response); // (1) // ~snip~ }
- Recall that
Response
isFetchResponder
andFetch
. We pass this api type into our macro. - We cast the
Response
type asFetch
and call the trait'spath()
function to get the url path. Because this method was implemented in our common crate where our api lives, any change to our api, will automatically propagate here like magic! - Here is where our
respond<F>(...)
function gets specialized. The genericF
now becomesResponse
. Actix will call theresponder
whenever an http request hits our path!
That's it folks!
Summary
We used traits to create required items (functions, types and constants) on elements of our api that vary per api endpoint. We then proceeded to abstract away the non-variable api aspects (e.g. the response code, http method, payload content type) to let our request handlers focus singularly on the aspect of the api they care about (in our case it was writing to the db).
By leveraging Rust's type system, we can build a full-stack app that is amenable to changing business requirements and easily maintainable by newbies like me or Rusty gurus like you.
Cheers!
Jesus
Making a Tiny Game with Raylib via @mysteriouspants
Chris is participating in the 4MB Game Jam this month. As per the name, game submissions need to be smaller than 4mb. Chris is making his game with Rust using the raylib crate, which provides Rust bindings for raylib, a C library for making video games. Part of the fun is optimizations, including trimming down raylib to not include modules that he isn't using. The full raylib library is around 1MB in size, but by trimming out what he isn't using, Chris can get the final dependency to only 40KB!
Crates You Should Know
- lyon: 2D Graphics rendering on the GPU using tessellation
- bevy_prototype_lyon: Draw 2D shapes and paths in the Bevy game engine
- raylib: Safe Rust bindings for Raylib