AGuideToRustGameFrameworks2019
Another guide giving an overview of a segment of the Rust ecosystem. Goodness, is this becoming a habit?
This information is up to date as of: June 1st, 2019.
Introduction
Anyway, so here’s the story. ggez
is, naturally, The
Best Rust Game Framework, ’cause I maintain it. But there’s several
other game framework crates that have very similar goals to
ggez
: Make it easy to make a nice 2D game with minimum
friction. However, they have different approaches and make different
tradeoffs. Because ggez
was the first Rust project to try
to fill this niche, other projects often took a bit of inspiration from
ggez
’s API to a greater or lesser degree, at least to begin
with, but none of them are direct forks. tetra
was made
because the author wanted to make it. quicksilver
was made
because the author wanted to be able to target the web directly instead
of ggez
’s approach of “let dependencies do the heavy
lifting”. coffee
was made because the creator wanted to
play with the wgpu
graphics library. And so on. I’ve mostly
just left these other libraries to do their own thing and never dug into
any of them in much depth, but when I recently found myself looking at
quicksilver
for some other reason I realized that the only
time I’d looked at it was right after it was first announced, and it had
diverged quite from ggez
’s model in the mean time. So I’m
writing this both to attempt to see what’s different, and to catch up on
what’s new.
To compare all these various game libraries, we’re going to use a
case study: we will take the ggez
Astroblasto example game,
and port it to each game framework. Astroblasto is a very simple but
complete Asteroids-like game: there’s levels, a score, and a lose
condition. It uses asset loading, sprite drawing, audio, input, timing
and text rendering. These are the fundamental pieces of
ggez
. ggez
has other features like mesh
rendering, shaders, render targets and so on, but I basically want to
make SNES-style games most of the time, and so drawing, audio, input,
timing and text are the necessary features I cannot do without.
I’m going to look at these basically in order of how different they are (or seem to be) from ggez. I am going to include highlights of the source code focusing on the interesting bits, with links to the entire project on Sourcehut (partially so I can play with Sourcehut).
Because information on the internet rots quickly, here are the versions of everything we are going to use:
- ggez: 0.5.0-rc.2 (I’ll turn it into an actual release someday, I promise!)
- tetra: 0.2.18
- quicksilver: 0.3.12
- coffee: 0.2.0
- piston: 0.43.0
- amethyst: 0.10.0
All code produced in this tutorial is MIT licensed. Also the creators of most of these libraries seem to hang out on the unofficial Rust Discord server, including myself, so drop by and say hi if you have any questions. I wanted to have an unbiased and naive view of these libraries so I didn’t ask any of them for help, though. Thus any mistakes are mine alone, and any awesome things are entirely theirs.
Also I’m going to be writing all of this as I am porting the code, so it’s probably going to be long, rambly, and a little disjointed. This is also going to be an opinion piece, so recognize these are opinions, not objective truth. This is definitely not a Proper Technical Essay, though I’ll try to at least have some sort of introduction and conclusion to each part. And I will assume that you know at least a little bit about Rust and game development, or are willing to learn on the fly (LD42Postmortem and AGuideToRustGraphicsLibraries2019 might be interesting to you, if so).
So now that that’s clear, let’s fire up the stream-of-conciousness cannon and dive in!
ggez
First off, the difference between a lot of these libraries lies in
what other libraries they use, so for each one we’re going to see what
the major dependencies are. All of these are easy to find on http://crates.io/. For
ggez
0.5, that list looks like this:
- Graphics:
gfx-rs
(pre-HAL) - Windowing and input:
winit
andgilrs
- Audio:
rodio
- Math:
nalgebra
internally, though all public API’s usemint
- Other interesting stuff:
lyon
for mesh generation,log
for logging,image
for image loading,glyph-brush
for text rendering
I don’t need to find out how ggez
works, I already know.
So let’s just introduce Astroblasto and look over the basic structure of
it. Astroblasto is an Asteroids-like game I threw together to be an
example of a simple but complete example game for ggez
,
’cause I’d already written a Space Invaders clone and Pac-Man or Tetris
sounded like too much work. It’s actually even simpler than Asteroids
because the rocks don’t split when you shoot them. It’s about 600 lines
in a single file, and is written to be read top to bottom, with comments
explaining what is going on and why. So, Astroblasto is not supposed to
be the fanciest or most efficient setup, just a good one that’s easy to
understand and doesn’t need a bunch of external libraries. The full code
considered in this article is here,
and whatever version is most up to date for ggez is available in its examples
folder.
So instead of going through the whole thing in detail, let’s give the
highlights of how the non-ggez
parts of the code work, then
talk a little about the ggez
-specific parts before moving
on to porting it to other libraries.
We only have three types of things in the world: the player, shots,
and rocks. (Well, and the score and level text, but those don’t interact
with any other object, so they’re not really “in the world”.) These
things all follow the same physics and almost the same rules,
with a few special cases for each. They all move according to newtonian
rules (there’s no drag) and wrap around the edges of the screen. The
player is controlled with the arrow keys and spacebar, and can turn and
thrust and shoot things. Shots destroy rocks that they hit and die after
a certain time. Rocks destroy the player if they hit it. When all the
rocks are gone it adds more, and the number of rocks increases every
time this happens. That’s about it. So for this I made essentially make
a very ghetto entity-component system, where all entities have the same
fields and we only have a few different systems. I call our shared
entity object the Actor
, and it is just a tagged struct. It
may be more proper to make it an enum or whatever and organize things a
bit differently, but for a game this small there’s really no advantages
of one variation over another. So, it looks like this:
#[derive(Debug)]
enum ActorType {
Player,
Rock,
Shot,
}
#[derive(Debug)]
struct Actor {
tag: ActorType,
pos: Point2,
facing: f32,
velocity: Vector2,
ang_vel: f32,
bbox_size: f32,
life: f32,
}
To make different actors, we just have functions that give them different tags and initial properties, like so:
fn create_player() -> Actor {
Actor {
tag: ActorType::Player,
pos: Point2::origin(),
facing: 0.,
velocity: na::zero(),
ang_vel: 0.,
bbox_size: PLAYER_BBOX,
life: PLAYER_LIFE,
}
}
We have a function that does basic movement physics to any actor:
fn update_actor_position(actor: &mut Actor, dt: f32) {
let norm_sq = actor.velocity.norm_squared();
if norm_sq > MAX_PHYSICS_VEL.powi(2) {
actor.velocity = actor.velocity / norm_sq.sqrt() * MAX_PHYSICS_VEL;
}
let dv = actor.velocity * (dt);
actor.pos += dv;
actor.facing += actor.ang_vel;
}
And a few utility functions I won’t show to let the player move around, resolve collisions, spawn new rocks when they all are destroyed while making sure that none of them appear on top of/right next to the player, etc. None of these are particularly surprising. If you’re interested, check out the source code.
Next, asset handling! One of the important but tedious things about any game. For this, we just load everything we need into a struct when the game starts and have it sitting there until the game ends:
struct Assets {
player_image: graphics::Image,
shot_image: graphics::Image,
rock_image: graphics::Image,
font: graphics::Font,
shot_sound: audio::Source,
hit_sound: audio::Source,
}
On to the next tedious part of any game, events! Sometimes you want
input to be edge-triggered, that is, if you hold a button down you get a
single event saying “this button was pushed”. Sometimes you want it to
be level-triggered, so if you hold a button down you get events saying
“this button is down” for as long as the button is held down. These are
complementary models, and which is preferable depends entirely on what
you’re trying to do with it. You can write code to turn one model into
the other and back, which is never hard but always just more work than
it should be. ggez
tries to make life simple by just having
all events be edge-triggered, since I find it easier to be consistent if
you only have to deal with one sort of event, and I find edge-triggered
easier to turn into level-triggered events than going the other way
around. It’s not really hard and fast though, ggez
0.5 will
probably have the option to give you both. Anyway, for this game, we
want level-triggered input for the player – the game doesn’t care how
often you mash the thrust or fire button, it just checks whether it’s
currently pushed each frame. So we have a struct to store our current
input state:
So this gives us all we need for our game state! Hopefully it’s pretty self-explanatory.
struct MainState {
player: Actor,
shots: Vec<Actor>,
rocks: Vec<Actor>,
assets: Assets,
input: InputState,
level: i32,
score: i32,
screen_width: f32,
screen_height: f32,
player_shot_timeout: f32,
}
And then we can make our main loop! ggez
does let you
just write a loop and poll for events by hand and such, but it also
offers the EventHandler
trait which simplifies this a
little by letting you provide a trait with callbacks and a
run()
function that calls them for you. The callbacks are
update()
, draw()
and an optional input event
handler method for each event type, such as
key_down_event()
. Let’s look at each part of Astroblasto’s
EventHandler
implementation individually. Hopefully they’re
pretty easy to follow:
fn update(&mut self, ctx: &mut Context) -> GameResult {
const DESIRED_FPS: u32 = 60;
let seconds = 1.0 / (DESIRED_FPS as f32);
while timer::check_update_time(ctx, DESIRED_FPS) {
// Update the player state based on the user input.
player_handle_input(&mut self.player, &self.input, seconds);
self.player_shot_timeout -= seconds;
if self.input.fire && self.player_shot_timeout < 0.0 {
self.fire_player_shot();
}
// Update the physics for all actors.
// First the player...
update_actor_position(&mut self.player, seconds);
wrap_actor_position(
&mut self.player,
self.screen_width as f32,
self.screen_height as f32,
);
// Then the shots...
for act in &mut self.shots {
update_actor_position(act, seconds);
wrap_actor_position(act, self.screen_width as f32, self.screen_height as f32);
handle_timed_life(act, seconds);
}
// And finally the rocks.
for act in &mut self.rocks {
update_actor_position(act, seconds);
wrap_actor_position(act, self.screen_width as f32, self.screen_height as f32);
}
// Deal with collisions and any consequences.
self.handle_collisions();
self.clear_dead_stuff();
self.check_for_level_respawn();
if self.player.life <= 0.0 {
println!("Game over!");
ggez::quit(ctx);
}
}
Ok(())
}
So, that was simple. Long, but simple. Drawing is similarly
straightforward. We start with graphics::clear()
, we end
with graphics::present()
, and mostly just call
draw_actor()
in between.
fn draw(&mut self, ctx: &mut Context) -> GameResult {
graphics::clear(ctx, graphics::BLACK);
// Loop over all objects and draw them...
{
let assets = &mut self.assets;
let coords = (self.screen_width, self.screen_height);
let p = &self.player;
draw_actor(assets, ctx, p, coords)?;
for s in &self.shots {
draw_actor(assets, ctx, s, coords)?;
}
for r in &self.rocks {
draw_actor(assets, ctx, r, coords)?;
}
}
// And draw the GUI elements in the right places.
let level_dest = Point2::new(10.0, 10.0);
let score_dest = Point2::new(200.0, 10.0);
let level_str = format!("Level: {}", self.level);
let score_str = format!("Score: {}", self.score);
let level_display = graphics::Text::new((level_str, self.assets.font, 32.0));
let score_display = graphics::Text::new((score_str, self.assets.font, 32.0));
graphics::draw(ctx, &level_display, (level_dest, 0.0, graphics::WHITE))?;
graphics::draw(ctx, &score_display, (score_dest, 0.0, graphics::WHITE))?;
graphics::present(ctx)?;
timer::yield_now();
Ok(())
}
draw_actor()
looks like this:
fn draw_actor(
assets: &mut Assets,
ctx: &mut Context,
actor: &Actor,
world_coords: (f32, f32),
) -> GameResult {
let (screen_w, screen_h) = world_coords;
let pos = world_to_screen_coords(screen_w, screen_h, actor.pos);
let image = assets.actor_image(actor);
let drawparams = graphics::DrawParam::new()
.dest(pos)
.rotation(actor.facing as f32)
.offset(Point2::new(0.5, 0.5));
graphics::draw(ctx, image, drawparams)
}
Event handling just updates our InputState
struct based
on events. Like I said, these functions are not called every frame, they
are called whenever a button is pressed or released.
fn key_down_event(
&mut self,
ctx: &mut Context,
keycode: KeyCode,
_keymod: KeyMods,
_repeat: bool,
) {
match keycode {
KeyCode::Up => {
self.input.yaxis = 1.0;
}
KeyCode::Left => {
self.input.xaxis = -1.0;
}
KeyCode::Right => {
self.input.xaxis = 1.0;
}
KeyCode::Space => {
self.input.fire = true;
}
KeyCode::Escape => ggez::quit(ctx),
_ => (), // Do nothing
}
}
fn key_up_event(&mut self, _ctx: &mut Context, keycode: KeyCode, _keymod: KeyMods) {
match keycode {
KeyCode::Up => {
self.input.yaxis = 0.0;
}
KeyCode::Left | KeyCode::Right => {
self.input.xaxis = 0.0;
}
KeyCode::Space => {
self.input.fire = false;
}
_ => (), // Do nothing
}
}
}
And that’s basically it! There’s a little boilerplate for setup
that’s standard to basically all the ggez
examples, and a
variety of actual gameplay stuff like draw_actor()
and
clear_dead_stuff()
but really those do absolutely zero
stuff that’s surprising or ggez
-specific.
Observations on ggez’s style:
- The
EventHandler
trait is your usual entry point to the game. We’ve already discussed how that works. - Everything
ggez
offers is in the form of functions, not methods onContext
orImage
or such. That’s mainly a matter of taste. It’s also how LÖVE does it, whichggez
is based on, so I kept the same idiom. graphics::draw()
takesInto<DrawParam>
, andDrawParam
is a structure that defines all the options you have for how you can draw something: position, color, rotation, etc.DrawParam
then implements the builder pattern to set these values, and also hasFrom
impl’s for various tuple types such asFrom<(Point2, f32, Color)>
which is just a shortcut forDrawParam::default().dest(...).rotation(...).color(...)
. This is basically how you get named args, variable-length args and default args in Rust. Like many things in Rust, it’s a little bit verbose and a pain in the butt to write, and then once it’s written it works really well and basically never silently breaks.- ggez doesn’t try to handle any timing, FPS locking or anything for
you. It provides
timer::check_update_time()
andtimer::yield_now()
for you to control how you want to do it. USUALLY the way you want to do it is what’s demonstrated in Astroblasto’s code:update()
callstimer::check_update_time()
in a loop calculating fixed timesteps until it’s caught up with how much time has passed since the last frame, and if vsync is enabled thengraphics::present()
will block while waiting for it anyway. This is a compromise between making things easy for the user, and letting the user not have to hack their way through anything if they want to make their own code to, for example, do lock-step network updates or whatever. - I didn’t show the code for it but we’ll mention it later:
ggez
has a filesystem abstraction layer. Inggez
you choose a set of directories and/or zip files, and it will give you something that looks like an isolated filesystem that contains only the contents of those directories. This is partially for convenience, and partially so that you can, in theory, use the same filesystem hierarchy on any OS or platform andggez
will be able to put the files in the right places for you. - Pretty much everything requires a
ggez::Context
: drawing, events, etc. This contains allggez
state: OS window and graphics handles, textures for the font cache, timing info, etc. Really it’s basically “graphics stuff” and “everything else”. I grew up on functional programming languages, so passing a “world” object around as an explicit function argument feels much more reasonable than the SDL2-esque solution of “lol just make everything global”. Passing a&mut Context
everywhere you need it is also kinda irritating, but much like mayonnaise, it’s good for you. You end up trying to avoid passing aContext
everywhere, which results in a program with very explicit separation of “game logic” and “stuff the game needs to do to interact with the outside world”. This keeps the order things are done in explicit and makes it easier to debug what is happening where. To quote Rob Pike: “Data dominates. If you’ve chosen the right data structures and organized things well, the algorithms will almost always be self-evident. Data structures, not algorithms, are central to programming.” (Of course, as any Lisper will point out, programs are data. But limiting the extent to which you treat programs like data makes it easier to do what Rust does, namely make programs that execute quickly and are easy for both humans and computers to understand at compile-time. It’s a trade-off.)
Anyway, that more or less wraps up our intro to ggez
in
general and Astroblasto specifically. We have our baseline to compare
against.
tetra
Now, on to tetra
! tetra
was started in
December 2018 by 17cupsofcoffee because, as they said, “I want to see
what it’s like to make something like ggez”. The readme warns that it’s
early in development and unstable, but the current version is 0.2.18, so
it’s gone through a lot of incremental evolution without breaking API
compatibility very much. My impression is that it’s the closest
to ggez
of the libraries we’re describing, though it lists
XNA/Monogame as the main inspiration, which is a slightly higher-level
framework than ggez
’s original inspiration of LÖVE. Just
looking at it, the main departures from ggez
include
animations/spritesheets, the fact that the framework controls your
mainloop timing by default, and some other minor things here and there.
It also lacks ggez
’s filesystem abstraction layer. To me
the main technological difference appears to be that tetra
does draw batching by default, and so should get faster drawing of
quads/sprites with less work. In ggez
you have to use a
SpriteBatch
to get a similar speedup, while
tetra
basically automatically uses a
SpriteBatch
-like setup under the hood whenever possible.
I’ve tried to implement this feature in ggez
but it would
take significant rewriting of the graphics system. It’s on the books to
do when I rewrite the graphics system to use gfx-hal
anyway, but who knows when
that will happen.
Entertainingly though, looking at tetra
’s issue tracker,
it asks many of the same fundamental
design questions as ggez
has grappled with, or is still
grappling with. Also a lot of the slightly-higher-level features it
wants to add from XNA and such are things that I always intended to have
solid implementations of in the ggez-goodies
crate, but alas… writing about game framework code is more fun than
writing game framework code, it appears, so I’ve never really made that
library production-quality.
tetra
’s docs are also very good, with a nice readme, FAQ
and tutorial, and good examples that show off lots of things. The
tutorial doesn’t go very deep, but is well written. Frankly, if all
ggez
ever does is set the bar for the documentation quality
a Rust game framework should have, my life has meaning.
tetra
seems to be following well in its footsteps
there.
Anyway! Technology stack of tetra
:
- Graphics: OpenGL using the unsafe
gl
crate, with its own abstraction layer. - Windowing and input:
sdl2
- Audio:
rodio
- Math:
nalgebra-glm
- Other interesting stuff:
image
for image loading,glyph-brush
for text rendering
All right, on to the show! tetra
seems pretty similar to
ggez
, so I’m just going to copy-paste ggez
’s
Astroblasto code into a new project, change the dependencies, and let
the compiler tell me what to fix.
tetra
provides its own Vec2
type and such,
which are re-exported from nalgebra-glm
. So essentially, if
you use tetra
, you will probably be using
nalgebra-glm
for most of your math. This is exactly the
same as how ggez
0.4 works with nalgebra
, but
it makes porting code from one framework to another a bit of a pain,
especially since I don’t know nalgebra-glm
that well. ggez
0.5 uses mint
for all its public math types, which is basically just a portability
layer. If you wanted to convert nalgebra
vector and matrix
types to cgmath
or euclid
equivalents, instead
of nalgebra
’s Vector2
type implementing
From<cgmath::Vector2>
and
From<euclid::Vector2D>
and so on for every single one
of those libraries, each of those libraries just implements
Into
and From
for mint
’s vector
and matrix types. Then instead of
fn draw(destination: nalgebra::Vector2)
you have
fn draw(destination: impl Into<mint::Vector2>)
and
you can just shove nalgebra::Vector2
into it, or
cgmath::Vector2
, or whatever. All these libraries implement
mint
bindings, and they all do it behind a feature flag so
if you don’t use mint
then there’s no cost. It’s a sorta
weird idea that I’ve only ever seen work in Rust… but work it does, and
I’ve found using it to be very nice. Maybe tetra
0.3 will
use mint
? Dunno, there’s definitely a bit of a tradeoff to
make there in terms of complexity.
tetra
’s audio is arguably cleaner than
ggez
’s. It has a Sound
type which is loaded
audio data ready to play, and SoundInstance
which is
explicitly a handle to a playing sound that lets you control what’s
going on with that particular playback. These are roughly equivalent to
ggez
’s SoundData
and Source
,
kinda, but I find tetra
to have a much more explicit and
cleaner separation that makes it more clear what’s going on. I like it.
Both tetra
and ggez
use rodio
for
sound, and rodio
does a lot of stuff behind the scenes and
is kind of hard to figure out because it also doesn’t make this split
very clear. ggez
inherits that ’cause I don’t know enough
about what a good sound API should look like. Not to disparage,
but I sorta think that rodio
’s creator tomaka doesn’t
either, which is why rodio
looks the way it does… I keep
meaning to do more sound stuff and learn how to Make It Work Well but
it’s just not that interesting to me, I guess.
Exactly like ggez
, tetra
has a
Drawable
trait that takes
Into<DrawParams>
and things that are, well, drawable,
implement that trait, such as Texture
and
Canvas
. There’s an issue that
questions this design and I kinda agree with what it says; it’s hard
to say what a Drawable
trait should and should not have for
different object types, and maybe the trait itself is just unnecessary.
It would be interesting to see what ggez
or
tetra
would look like without it.
tetra
controls the game’s mainloop, including timing,
while ggez
is a little lower-level and forces the user to
do a little more work. Again, there’s an
issue discussing this, but it’s a tricky detail of API design
tradeoffs to work with. It’s also essentially something you write once
per game and never touch again, so I’m fine with tetra
’s
solution as long as it works well (I seldom need to do fancy timing in
my games), and I’m also fine with ggez
’s solution. The job
of a game framework is to make the design decisions that are obvious and
mostly irrelevant to your game, but also really important to
get right, and timing is high on the list. Though it would also
be nice if you could set the target FPS you want in tetra
’s
ContextBuilder
– oh wait, you can. ♥
That said, the hook that tetra
offers you to interact
with its mainloop, the State
trait, is pretty spartan:
pub trait State {
fn update(&mut self, ctx: &mut Context) -> Result { ... }
fn draw(&mut self, ctx: &mut Context, dt: f64) -> Result { ... }
}
Ah, here’s an actual architectural difference: tetra
doesn’t seem to have event callbacks, you just iterate over events
manually with tetra::input::get_keys_down()
or such. I
actually don’t like that as much; I talked about edge-based and
level-based input before, and it annoys me when the distinction and
interaction between the two is not clear. SDL2 is guilty of this too;
tetra
uses SDL2 for input handling, and so probably
inherits this tendancy from that. I dunno, it just feels like
tetra
’s model will make it easy to miss things or have
weird edge interactions. This isn’t something I have a great solution
for myself though. If you stop worrying about these design issues;
tetra
’s input handling is perfectly fine to actually use
correctly, and for Astroblasto looks like this:
fn update(&mut self, ctx: &mut Context) -> tetra::Result {
// ...snip...
let mut quit = false;
for key in tetra::input::get_keys_pressed(ctx) {
match key {
Key::Up => {
self.input.yaxis = 1.0;
}
Key::Left => {
self.input.xaxis = -1.0;
}
Key::Right => {
self.input.xaxis = 1.0;
}
Key::Space => {
self.input.fire = true;
}
Key::Escape => {
quit = true;
}
_ => (), // Do nothing
}
}
for key in tetra::input::get_keys_released(ctx) {
match key {
Key::Up => {
self.input.yaxis = 0.0;
}
Key::Left | Key::Right => {
self.input.xaxis = 0.0;
}
Key::Space => {
self.input.fire = false;
}
_ => (), // Do nothing
}
}
...
}
One semantic difference, too. In both ggez
and
tetra
, when you rotate an image/texture, it rotates from
its origin – the top-left corner. In both, the DrawParam
has a parameter that lets you move that origin so you can, for example,
rotate things around their center. In ggez
, you specify
this rotation point in the range [0-1.0]
, so to rotate an
image around its center you specify (0.5, 0.5)
. In
tetra
you specify it in pixels, so for a 16x16 image you
have to specify (8,8)
and for an 8x8 image you have to
specify (4,4)
. Astroblasto has sprites of different sizes,
so you have to check them and do the math in draw_actor()
instead of just telling it to rotate around (0.5, 0.5)
every time. This is precisely why ggez
uses a range of
[0-1.0]
in the first place, so you don’t have to do that.
It’s one of those tiny decisions that doesn’t actually MATTER, but
changes how an API feels.
Hmm, now that I look at that screenshot, the drawing is notably glitchy on rotated quads. I’m not sure what’s up with that; is there a filtering mode or something somewhere that isn’t getting set properly? Something to dig into later, perhaps.
All in all though, apart from event handling, porting Astroblasto to
tetra
is basically a straight copy and a bunch of very
minor fiddling of names, function parameters, and so on. There’s
probably some other edge cases somewhere, but I didn’t run into them.
tetra
has some more advanced features like animations and
spritesheet support, but Astroblasto doesn’t use those, so I have no
idea how it works. Overall, tetra
is easy to use and
well-documented. It’s a little more opinionated and has fewer moving
parts, omits some of ggez
’s more sophisticated lower-level
features, and adds some more sophisticated higher-level features. It
looks like the main thing that ggez
has that
tetra
doesn’t is mesh drawing and the filesystem layer.
quicksilver
Okay, this is exciting. Despite all my big
talk, I’ve never actually used Webassembly for anything real, and
ggez
cannot currently make games for the web.
quicksilver
can. That’s its claim to fame in my eyes, and
it seems to actually live up to it from what I’ve heard. So let’s see
how well it works!
quicksilver
0.1 was released January 2018 and it’s
currently on version 0.3. It is made by one ryanisaacg, and as well as
all the basic pieces it appears to offer all the more powerful things
that I don’t want to include with ggez
: collision
detection, GUI, animation, etc. Its main dependencies look like
this:
- Graphics: OpenGL with
gl
orwebgl_stdweb
- Windowing and input:
winit
andgilrs
- Audio:
rodio
- Math:
nalgebra
- Other interesting stuff:
lyon
for mesh generation,log
for logging,image
for image loading,rusttype
for text rendering (noglyph-brush
, so probably no text caching),ncollide2d
for collision,immi
for GUI,future
for… something, whew!
Documentation is okayish but not great. The readme is good and
there’s a good assortment of example code, but the examples are mostly
short and effectively uncommented. It seems to suffer from Programmer
Documentation Disease from what I can tell, where all the individual
bits have (kinda obvious) explanations but there’s no really good
explanation of how things fit together. Especially given that
there’s a module called quicksilver::lifecycle
that seems
to be necessary to make a game run, it has stuff in it for events and
main loops and async asset loading and who knows what else and has some
futures involved with it somehow. A bit of background on what the heck
is actually going on there would be pretty helpful.
So, let’s suck it and see.
Okay, first off, main loop handling appears to be much more like ggez
0.2 where you impl a trait on your game state object, that trait
includes a new()
method, and you just have some object or
function that magically creates your game state for you. This is fine
and semi-obvious to the library creator but is more complicated than it
needs to be for a user to understand, especially people who are new to
Rust. It’s also less flexible because it limits what you can actually do
in the process of creating your game state. quicksilver
seems to do stuff with futures and whatever, and running on web imposes
some weird constraints on how you program event loops, but when
ggez
did it this way I got lots of questions about it, so
I’d say this model isn’t really the best for people who are new to Rust.
The full trait you implement is called State
:
pub trait State: 'static {
fn new() -> Result<Self>
where
Self: Sized;
fn update(&mut self, _window: &mut Window) -> Result<()> { ... }
fn event(&mut self, _event: &Event, _window: &mut Window) -> Result<()> { ... }
fn draw(&mut self, window: &mut Window) -> Result<()> { ... }
fn handle_error(error: Error) { ... }
}
Hm, actually, I hadn’t noticed the handle_error()
method
before. That’s nice to have. As the docs explain, on the web doing
logging and stuff can be tricky, so errors pass through
handle_error()
where you can try to do something useful to
them before you panic or exit.
Oh wait, hang on, all the how-this-works docs are in the API docs for
the quicksilver::tutorials
module. This is actually mentioned in the readme, upon inspection, but
my eyes glazed right past it and I automatically went on to the example
code in Github, which is not as good. Hmm.
Hmm, drawing is… weird. You have
Window::draw(&mut self, draw: &impl Drawable, bkg: Background)
…
a Drawable
is a shape like a rectangle or line, while
Background
is a fill color, a texture, or a texture with a
blend color. This feels… off, and not composable. Then there’s
Window::draw_ex()
which adds arguments for a
Transform
for rotation and scaling and whatever, and a
z-value for layering. That at least makes sense, I suppose, but it feels
weird. Probably just ’cause it’s not ggez. Grr, rar, it’s different, how
dare! Where’s my DrawParam
!? Programmer smash!
The Drawable
/Background
division actually
annoys me, ’cause… well, it soooort of matches how a GPU thinks about
the world, you have your geometry and then you have a mesh bound to it
or something. But it feels like a somewhat arbitrary and imprecise
division. There’s nothing actually WRONG with it but I feel like there
should be a better way. And it appears there’s no actual
Mesh
drawable, but there is Circle
,
Line
, Rectangle
, and Triangle
.
What if I want to draw a Moebius strip? Oh, there is a
quicksilver::graphics::Mesh
type, but it doesn’t actually
implement Drawable
, so… how do you draw it? Mysteries
abound.
Input handling is state-based via function calls, similar to
tetra
, except you can’t get a list of key events to iterate
through, you have to call
window.keyboard()[Key::Right].is_down()
or whatever in your
update()
method. Again, I’m biased but indexing it feels
like unnecessary cuteness. You can’t iterate over the results of
window.keyboard()
so why pretend it’s a collection? Can
indexing it panic with an out-of-bounds error? Looks like it can’t,
unless there’s a bug in quicksilver, but the fact that I felt obligated
to check makes me uncomfortable.
Then you end up with input handling code in the tutorial that does this:
if window.keyboard()[Key::Right].is_down() ||
window.mouse()[MouseButton::Right].is_down() ||
window.gamepads().iter().any(|pad| pad[GamepadButton::TriggerRight].is_down())
{
self.position.x += 2.5;
}
Ick. Ugh. Why can’t it be something like this?
for event in window.events() {
match event {
| GamepadEvent(GamepadButtonState::Down(GamepadButton::TriggerRight), _gamepad)
| MouseEvent(MouseButtonState::Down(MouseButton::Right))
| KeyboardEvent(KeyState::Down(Key::Right) => {
self.position.x += 2.5;
}
_ => ()
}
}
Looks like you can do it more or less that way if
you use the State::event()
callback, and the tutorial even
talks about the distinction between edge-triggered and level-triggered
events. But the tutorials generally actually use the ugly way,
so people writing games are going to as well. Why is the ugly way bad?
Basically, because it’s global state. First, if you have nested states
(such as a menu screen with a game behind it) there’s no way for them to
dispatch events to each other, decide which of them get to handle events
at all, etc. It gets complicated. And second, you can have these state
checks scattered throughout a function or several functions or wherever
else you want, which is easy to figure out for small and simple programs
but can easily start getting annoying if you have larger programs
written over long times – you have to remember where events actually get
handled because it’s not instantly obvious, and it could happen in
several places. Event handling gets complicated when you have multiple
things going on at once, so you want it all to be kept in one place.
Also it appears that quicksilver’s Window
type is
basically what ggez
or tetra would call
Context
, or the closest thing to it, so we’re going to be
seeing it a lot.
Okay, enough racism against imperative programming! Back to business.
The tutorials DO explain a little about what’s going on with this
lifecycle
nonsense, and as expected, it’s basically the
web’s fault. Asset loading can’t block, and so has to be async, so other
things are forced to be async. I… really kinda hate that, but I seem to
be in a grumpy mood anyway. But also, a game is already essentially a
polling loop, so adding futures feels like it doesn’t get you much
unless you’re actually using them for timing or such, like Unity’s
coroutines. (Someone should make a fully futures-based game engine in
Rust someday just to see how it looks.) And since
quicksilver
doesn’t use tokio
or anything like
that, I would bet that it implements its own polling loop as part of its
run
function. But on web you are forced into this
callback-y async model by the platform, and there’s not really any way
to get around it, so maybe quicksilver
is just trying to
make that model work as nicely as it can.
Darn it, I don’t know why this irritates me so much. Is it actually irritating, or is it just that it’s different from what I’m used to dealing with? Human brains suck.
Oh, this tutorial does explain how you draw a Mesh
though: window.mesh().extend(&mesh);
OF COURSE! Okay,
THAT isn’t just things being different than what I expect, that’s leaky
and inconsistent abstraction. It has nothing to do with the rest of
quicksilver
’s drawing model, it’s just a hook into its
underlying implementation that happens to be useful for this one single
thing and doesn’t combine with anything else. If your drawing API is
immediate-mode, don’t have the retained-mode innards leak through,
especially when there’s no benefit to it (the window’s Mesh
gets cleared every frame so you have to keep adding to it anyway). It
doesn’t even follow Rust’s naming convention properly; extend()
should take an iterator, not a
single object.
Also there appears to be no way to attach a Transform
or
z-order to a Mesh
? How is that supposed to work? Okay, the
z
at least is part of the GpuTriangle
structure you build a Mesh
from. GpuTriangle
is otherwise just an index list, so z
is not actually a
vertex property. Ugh. Augh. How in the name of Eris do your shaders
work, quicksilver
? Time to read the source! It looks like
its shaders don’t know about z
at all, so the meshes must
get sorted back-to-front on the CPU sneakily behind the scenes
somewhere. Yep, that happens in Window::flush()
. I wonder
if it would be better to just keep the meshes all in a
BTreeSet
so they always stay sorted? Depends on how you
write your game’s drawing code, probably. Or you can just use a depth
buffer, since that’s literally what they’re there for. But the fact that
I have to ask these questions feels problematic.
Unfortunately though, shortly after basic asset loading the tutorials peter out a little. They’re there but in a more to-be-finished state. But I think we have enough to go on to start actually implementing things…
Error handling is a little squirrelly ’cause, for example,
Image::load
doesn’t return a Result
, it
returns
impl Future<Item = Image, Error = QuicksilverError>
.
Well, that’s just how that goes, I suppose; we won’t know whether an
image actually works or such until we try to use it. However, the way
it’s set up it currently looks like you always have to check
whether it’s loaded, every time you try to use it; not sure if
there’s a good way around that. More interesting is that there appears
to be no Context
type or equivalent for graphics loading.
ggez
needs a Context
even to just load an
image, since it has to access the GPU to turn that image into a texture.
I suppose like OpenGL itself, quicksilver
just says “okay
that’s just in some hidden global somewhere”.
So because of this future-y stuff, the basic sprite-drawing code looks like this:
fn draw_actor(
assets: &mut Assets,
window: &mut Window,
actor: &Actor,
world_coords: (f32, f32),
) -> quicksilver::Result<()> {
let (screen_w, screen_h) = world_coords;
let pos = world_to_screen_coords(screen_w, screen_h, actor.pos);
// `image` is type `Asset<Image>`, `Asset` is basically a future executor that
// provides combinators for `Future<Item = Image, Error = QuicksilverError>`
let image = assets.actor_image(actor);
image.execute(|i| {
let transform = geom::Transform::rotate(actor.facing);
let target_rect = i.area().with_center((pos.x, pos.y));
window.draw_ex(
&target_rect,
Background::Img(&i),
transform,
0,
);
Ok(())
})
}
The future handling is… actually not awful, I think.
Asset::execute()
returns a Result
, and so
either the image loading has succeeded and it executes the given
function returning Ok
, or it has failed and it returns
Err
, or the future is not done yet and
execute()
does nothing. There’s also an
Asset::execute_or()
combinator that takes another function
and executes that when asset loading is not done yet, so you could use
that to draw a placeholder, print an error, etc. I could definitely
imagine using this system to have assets pop in as needed, or just delay
with a loading screen until they’re all there, or whatever.
Hmmmm, quicksilver
’s State
trait requires
you to define a new()
method. But this method does not get
passed reference to quicksilver
’s Window
. So
it can’t do things like, say, set parameters based on the size of the
screen. Not actually a huge problem for this game, but irritating. That
sort of thing is one reason ggez
doesn’t define what a
new()
method looks like for you though.
Gotta say that, despite my bitching about global state, if you are
using the event()
callback then iterating over events is
quite nice and arguably more convenient than ggez
’s
key_down_event()
style of having different callbacks for
different event types. On the other hand there appears to be no way to
actually quit a game. Or if there is I can’t find it. Well, for now
let’s just call std::process:exit()
. I’m sure THAT will do
something sensible in a web browser!
There is no text cache, so rendering text happens in two steps: you
take a font and render it to an image (which is slow), then draw the
image (which is fast). A glyph cache renders characters or sets of
characters on the fly as needed and saves the results to a texture, so
then you can draw any text just by creating a mesh that draws from that
texture as if it were a spritesheet. This is a tiny bit slower if your
text never changes and is much, much faster if it does. The current
best-in-class text cache crate is probably glyph-brush
,
which is used by tetra
and by newer versions of
ggez
(anything after 0.4.2 I think?) Astroblasto used to
make sure it only re-rendered text when necessary because
ggez
had no glyph cache, but now just draws stuff on the
fly. I haven’t bothered re-doing the “save rendered text and only
re-draw it if it changes” bit, so our text drawing is slow, and looks
like this:
self.assets.font.execute(|f| {
let style = FontStyle::new(24.0, Color::WHITE);
let text = f.render(&level_str, &style)?;
window.draw(&text.area().with_center(level_dest), Background::Img(&text));
let text = f.render(&score_str, &style)?;
window.draw(&text.area().with_center(score_dest), Background::Img(&text));
Ok(())
})?;
(Note from the future: in practice, for Astroblasto this is Fast Enough.)
And, that’s about it! Despite my bitching about API details, pretty painless.
Now the real test… how well does it work online?
The first trial of getting stuff to work on a foreign platform is
always heckin’ building it at all. quicksilver
’s docs claim
that you can use cargo-web
and
it will just handle everything for you. Let’s try it… A quick
cargo install cargo-web
followed by
cargo web build
, and, about five minutes later out pops a
bouncing baby
target/wasm32-unknown-unknown/debug/astro_quicksilver.wasm
.
Huzzah! …now how to actually run it? Aha, cargo web deploy
will generate a HTML page and JS stub to actually load the thing. And we
need to put all our assets in a folder called static/
.
Right. Copy that all to a web server, load the webpage, and…
We get a blank screen.
Excellent! We can stop wondering when this house of cards will break,
and move on to fixing it. Open the JS console and it helpfully says
Error loading Rust wasm module 'astro_quicksilver': TypeError: "Response has unsupported MIME type"
.
Open Firefox’s page inspector, go to the networking tab and we can see
that astro_quicksilver.wasm
is indeed getting downloaded,
but the web server is giving it
content-type: application/octet-stream
, which is apparently
not what Firefox wants. cargo-web
doesn’t seem to have any
helpful advice for this, but I own the web server, so a quick edit to
/etc/nginx/mime.types
to add
application/wasm wasm;
, a quick config reload, and we get
an empty black screen instead of an empty white one! The console shows
some asset paths are messed up; that’s my bad, and easy to fix.
And it works!
…well. Sorta works. Looks like quicksilver
’s rotations
are in degrees rather than radians, so that needs some fixing. (Boo,
hiss, radians are sacred!) And text sizes need some fiddling to make
them look nice. Aaaaaand it looks like when we die,
std::process::exit()
on Webassembly panics with
unreachable
, which isn’t really ideal. (Actually, since
quicksilver
knows how to create our game state with a
new()
method, it should be able to provide a
quit()
method that just restarts the game for us on web.)
And the rotations are ugly as heck for some reason; something weird is
going on with filtering perhaps. That’s probably not quicksilver’s
fault, or at least, there’s probably an option somewhere to play with.
Those are all either easy fixes or minor inconveniences.
But it works! Woohoo! Pure Rust! No Emscripten necessary! Runs fast! Play it here! (~7 MB)
Actually, let’s see if we can make the binary smaller. Building in
debug vs. release mode doesn’t seem to change the size much. Enter the
Webassembly Binary Toolkit, wabt
, which is now actually
available as a package on Debian 10. wabt
is the reference
implementation of various tools for working with wasm, such as an
assembler, disassembler, etc, so we can take apart our wasm file and see
what’s taking up so much space. It also has wasm-strip
to
strip symbols, which shaves about 1.5 MB off of Astroblasto; less than I
expected. wasm-objdump -h astro_quicksilver.wasm
gives a
breakdown of the executable’s segments, with sizes, which reveals the
Code
segment is about 5.3 MB and defines 18,884 functions.
Rummaging into it a little reveals that, as is kinda typical for Rust
code, almost all of the functions are just endless generic
specializations of various standard library types. So, probably not much
to be gained there. HOWEVER, the original 7 MB does compress down to 1.7
MB on the wire, so it’s not actually as bad as it looks. …wait, isn’t
there any way to tell rustc
to optimize for size? There is,
it’s the opt-level = "z"
option! That gets us a mere 1.5
megabyte binary! MUCH better.
Oh, it also works on desktop:
And even on desktop it still has the same issue as on the web, where
rotated quads look weird. It looks different from tetra
’s
weird-rotated-quad-drawing, but occurs in a similar place, so I’m going
to assume they’re related, and both just have some setting somewhere
that needs to be poked at.
All in all? I feel like there’s still work to do in
quicksilver
’s ergonomics. But most of it is just work that
comes from using an API a bunch and obsessing over all the tiny details.
It builds games for both Webassembly and desktop, basically
transparently, and that’s damn impressive.
coffee
Okay, let’s look at coffee
next! coffee
ALSO is not in the least based on ggez
’s API, so let’s find
out if my frustration with quicksilver
is semi-justified or
whether I just hate anything different.
coffee
is made by hecrj, aka
lone_scientist
, and is BRAND SPANKIN’ NEW. First published
in April 2019. It is advertised as being massively incomplete, and it
has a really
pretty particle demo you should download and run.
coffee
exists mainly ’cause the author wanted to play with
wgpu
, which I’ve talked
about elsewhere. coffee
’s dependency list is:
- Graphics:
wgpu
, or “gfx
pre-ll for OpenGL support, based heavily on the ggez codebase”. Oh man, I’m sorry. XD - Windowing and input:
winit
- Audio: uh, nothing, it appears. Guess they weren’t kidding about it being incomplete! :D
- Math:
nalgebra
- Other interesting stuff: Nothing else! That’s it! …Oh, I take it
back, there’s
wgpu_glyph
, a separate crate wrappingglyph-brush
, also made by hecrj.
…hmm, beware though, the developer is doing the thing where they push
their in-progress work directly to the master
branch on
Github, and so what code you see in the examples and such drifts out of
sync with the published docs. I do wish that wasn’t the default way of
doing things…
Now, I’ve never actually looked at anything written in
coffee
before, but like everything else so far it seems to
have a trait defining what a gameloop looks like, so let’s take a
loo–
pub trait Game {
type View;
type Input;
const TICKS_PER_SECOND: u16;
const DEBUG_KEY: Option<KeyCode>;
fn new(window: &mut Window) -> Result<(Self, Self::View, Self::Input)> where Self: Sized;
fn update(&mut self, view: &Self::View, window: &Window);
fn draw(&self, view: &mut Self::View, window: &mut Window, timer: &Timer);
fn on_input(&self, _input: &mut Self::Input, _event: Event) { ... }
fn on_close_request(&self, _input: &mut Self::Input) -> bool { ... }
fn interact(
&mut self,
_input: &mut Self::Input,
_view: &mut Self::View,
_gpu: &mut Gpu
) { ... }
fn debug(
&self,
_input: &Self::Input,
_view: &Self::View,
window: &mut Window,
debug: &mut Debug
) { ... }
fn run(window_settings: WindowSettings) -> Result<()> where Self: Sized,
{ ... }
}
…huh. Now that’s interesting. Notable features:
- debug key! Defaults to F12, can be overridden or set to
None
… if you press it, it flips a toggle that makes the main loop call yourdebug()
method afterdraw()
, it appears. That method gets passed theDebug
struct which contains tons of metrics about your game, and can be drawn instead of/along with your own debug view, or you can ignore it and collect your own metrics. I like this. on_close_request()
which returns abool
, giving you a single place to handle logic and determine whether or not the game should quit. Not sure this is really necessary, but it’s a nice touch. You’re going to need SOMETHING like this no matter what, to handle “do you want to save your game before quitting?” situations. I’m just unconvinced this is the best place for it.- It gives you
on_input()
to let you handle edge-triggered events, and passes it anInput
type you define so that you can turn those into the level-triggered events you care about as you wish – or map key bindings, or whatever. - Then there’s a
View
type, which it appears should be where you put all your graphics-related stuff. As the docs for theGame
trait say: “Coffee forces you to decouple your game state from your view and input state. While this might seem limiting at first, it helps you to keep mutability at bay and forces you to think about the architecture of your game.” - What’s this
interact()
method? It looks like that’s where input actually gets handled; notice thatupdate()
anddraw()
don’t get passed anInput
object. So the main loop goesframe start -> on_input(&self, &mut Input, &Events) -> interact(&mut self, &mut Input, &mut View) -> update(&mut self, &View) -> draw(&self, &mut View) -> debug(&self, &Input, &View, &Debug) -> frame end
. Note thatupdate()
’s access toView
is read-only, anddraw()
’s access toself
is read-only!
Veeeeeery interesting!
The interesting-ness carries on to the drawing stuff. So far
everything we’ve looked at has presented a fairly implicit
immediate-mode style of API. You call a function that says “draw this
thing” or “clear the screen” or “switch this graphics setting” and it
does it “immediately”. You can imagine sitting down at a REPL and typing
these commands in one by one and having a screen update after each one
to reflect the change. In ALL of these cases though, it’s a lie. A
total, complete and bald-faced lie! Graphics cards do NOT work this way!
GPU’s achieve absurd throughput by having absurd latency, and so to make
a graphics card run fast at ALL you need to send it big queues of
drawing commands in batches and then let them get handled
asynchronously. As discussed
previously (maybe not terribly clearly), this immediate mode
vs. batched drawing distinction is the main philosophical difference
between OpenGL and Vulkan. ggez
, tetra
and
quicksilver
all make an immediate mode API by recording
each “immediate” command you give it in a buffer behind the scenes, and
one way or another sending that buffer to the GPU once per frame
(ggez
when you call flush()
,
tetra
and quicksilver
implicitly at the end of
your draw()
callback).
coffee
, it appears, does not do immediate mode drawing,
and instead makes this batching process and the data it depends on more
explicit. Everything specifies a target that it is drawn on and all
properties it is drawn with. You start with a Frame
, which
is the final render target that your game will be shown on. This turns
into a Target
, which is a slightly more abstract render
target that could come from an off-screen Canvas
as well.
There’s only a few things you can actually draw to a
Target
: Image
, Canvas
, or
Batch
(sprite batch). (I can’t find any way to create
arbitrary meshes, and text drawing looks… weird and different, so for
now I’m going to just consider those unfinished.) The
draw()
method of these types usually takes an
IntoQuad
value that specifies where to draw it, and there’s
a Sprite
type which is just a Quad
-like thing
with a different coordinate system which implements
IntoQuad
. (Not Into<Quad>
? Odd.)
Earlier I wondered what ggez
or tetra
would
look like without a Drawable
trait? I suppose it could look
kinda like this: mostly the same, but with less guidance about what
actually is drawable and how the bits fit together. In that
case the Drawable
trait isn’t there for the computer, it’s
for the human.
Ooooh, the Target
also incorporates a
Transformation
; you can create a new Target
from an old one plus a transformation. So to draw something with a
transform you do something like this:
let target1 = frame.as_target();
thing.draw(some_quad, target1);
{
// Draw the thing again, scaled down
let scaled = Transformation::scale(0.5);
let target2 = target1.transform(scaled);
thing.draw(some_quad, target2);
}
// Draw again with original transform in a different target location
thing.draw(some_other_quad, target1);
Very clever! It’s not perfect, there’s some work to be done, but it looks like it could be an extremely nice approach.
Oh, documentation! coffee'
s documentation is good! Good
enough I didn’t notice it, because it explained everything I needed.
Examples could use some work, but that takes time.
Okay, so can we make Astroblasto fit into this sort of model? Let’s see how that goes…
First off, as is common, coffee
exports vector and point
types from nalgebra
. (Nitpick: It should re-export
nalgebra
so you can always easily find the precise version
it uses.) Second off, it sorta has an async style of asset loading as
well, with a Task
abstraction that has nothing to do with
Rust’s future
types but sort of seems to be approaching the
same thing from a different direction. The goal of it seems to be to
make progress bars work nicely for loading assets, and it does that just
fine, but when you start making combinators to combine them they start
looking more and more like futures in any other async framework. Third,
unlike ggez
which intended to have multiple different
graphics backends but never actually got around to it,
coffee
seems to live up to the goal; there are different
feature flags for running it on Vulkan, DirectX, etc. I… uh, forgot to
actually try out anything besides Vulkan though, so I don’t know how
well they work in practice. Probably about as well as wgpu
itself does, which is to say, very well, but not always flawlessly.
Image::load()
takes a AsRef<Path>
but
Font::load()
takes a &[u8]
. Tsk tsk,
inconsistent API’s, for shame! You’ve had almost two months to make
everything perfect!
Looking at Astroblasto, we already have the
appropriate types for Game::View
and
Game::Input
: our Assets
and
InputState
structs. These fit those roles
perfectly. Zero modifications necessary, this is exactly what
coffee
wants. Really, this is one of those coincidences
that is, actually, not very coincidental at all… But it still leaves me
flabbergasted, in a happy way.
Okay, so, loading assets. The easy way to run a
Task<Foo>
to completion and get
Result<Foo, whatever>
out of it is to make a loading
screen with loading_screen::ProgressBar::new(window.gpu())
,
then run it with loading_screen.run(some_task, window)
. The
run()
method basically takes over display and and input and
draws the loading screen until everything’s loaded, then returns. Hm,
does that not cause a double borrow between &window
and
&window.gpu()
? Apparently not, huh.
So implementing the Game
trait for our main state object
starts something like this:
impl Game for MainState {
type View = Assets;
type Input = InputState;
const TICKS_PER_SECOND: u16 = 60;
fn new(window: &mut Window) -> Result<(Self, Self::View, Self::Input)> {
print_instructions();
let player = create_player();
let rocks = create_rocks(5, player.pos, 100.0, 250.0);
let asset_loading = Task::stage("Loading assets...", Assets::new());
let mut loading_screen = loading_screen::ProgressBar::new(window.gpu());
let view = loading_screen.run(asset_loading, window)?;
let s = MainState {
player,
shots: Vec::new(),
rocks,
level: 0,
score: 0,
screen_width: window.frame().width(),
screen_height: window.frame().height(),
player_shot_timeout: 0.0,
};
Ok((s, view, InputState::default()))
}
...
}
on_input()
and interact()
are also quite
simple to sort out:
fn on_input(&self, input: &mut Self::Input, event: Event) {
match event {
Event::KeyboardInput {
state: ButtonState::Pressed,
key_code: KeyCode::Up,
} => { input.yaxis = 1.0;}
Event::KeyboardInput {
state: ButtonState::Pressed,
key_code: KeyCode::Left,
} => { input.xaxis = -1.0;}
// ...and so on...
_ => (),
}
}
fn interact(&mut self, input: &mut Self::Input, _view: &mut Self::View, _gpu: &mut Gpu) {
const SECONDS: f32 = 1.0 / (MainState::TICKS_PER_SECOND as f32);
player_handle_input(&mut self.player, input, SECONDS);
self.player_shot_timeout -= SECONDS;
if input.fire && self.player_shot_timeout < 0.0 {
self.fire_player_shot();
}
// This is a sort of odd place to put end-of-game checking, but
// we need access to input for it, so here is where it goes.
if self.player.life <= 0.0 || input.quit_requested {
println!("Game over!");
// Like quicksilver, we still seem to have no easy way to signal
// that the game is over!
std::process::exit(0);
}
}
It’s occasionally asked why ggez
uses a single big
honkin’ Context
object instead of breaking it into
pieces and only having functions take the sub-context types they
actually need. The answer is basically “it would be harder than it looks
and more trouble than it’s worth”. ggez
is quite small so
passing a single Context
around doesn’t get in your way
much, and it’s already fairly well modularized under the hood so making
it more broken apart wouldn’t make maintenance much easier. However,
with it’s very-explicit drawing API it feels like coffee
takes precisely that approach. It’s interesting to see how it works out.
It DOES get fiddly and confusing to figure out: drawing something in
coffee
starts with a Window
, from that you get
a Frame
, from that you get a Target
and NOW
you can actually draw your stuff. Though there’s also a Gpu
object floating around doing stuff for some reason I still haven’t
figured out. (Looks like it’s mainly used for asset loading?) But once
you learn what goes where, I bet it becomes much clearer how things
interact because each object only does one part of the drawing
process.
Actually drawing a full image is a little fiddly ’cause you have to
get the width and height of it, then specify those as the image’s
source. This is similar to tetra
’s rotation origin point
issue. Again, ggez
uses float coordinates in the range
[0-1.0]
for its source rectangle, so you don’t have to
treat different images differently. You can use the exact same drawing
code for a 16x16 pixel image as for a 91x14 image. I mean… I guess you
can do that in coffee
too, but you need an extra step to
check the size of the image, if a small one. A nice ergonomic
improvement would be an Image::get_rect()
function or such
that just returns a Rectangle
representing the full image.
On the other hand, now that I think of it ggez
basically
has to call the equivalent of Image::get_rect()
in its
Image::draw()
implementation and scale the drawing to fit
it, and it gets hairy anyway; ggez
just hides the hair from
you. Anyway, once you work it out it is pretty simple:
fn draw_actor(assets: &mut Assets, target: &mut graphics::Target, actor: &Actor, world_coords: (f32, f32)) {
let (screen_w, screen_h) = world_coords;
let pos = world_to_screen_coords(screen_w, screen_h, actor.pos);
let image = assets.actor_image(actor);
let dest = graphics::Sprite {
source: Rectangle {
x: 0,
y: 0,
width: image.width(),
height: image.height(),
},
position: pos,
};
image.draw(dest, target);
}
Though, ahahahaha, I just noticed. I want to make a
Transformation
that encodes a rotation and apply that to
the Target
here, to draw rotated images. However,
Transformation
does not seem to have a constructor to make
a rotation. If there is any way to rotate an image, I can’t find it in
the docs! Whoops! Buddy. Buddy, this is a slightly fundamental piece of
functionality you’ve overlooked! XD Well, you warned me the library was
unfinished!
Okay, Transformation
does implement
From<[[f32;4];4]>
, so you can create a rotation
matrix by hand or from nalgebra
or whatever if you want.
I’m not going to bother in this case though, since this write-up is
already way huger than I intended it to be, we’ll just assume it works
and press onwards without rotating anything.
Text drawing is somewhat crude, but again, it’s a young project. Its
API makes perfect sense if you know the innards of
glyph-brush
, but isn’t the most convenient.
glyph-brush
actually makes it kinda hard to make a nice
drawing API atop it, for a variety of very good and rather annoying
technical reasons. (Basically, because it does rendering to a texture in
a separate step from the rest of your system, it needs its own graphics
pipeline, and it’s hard to make that look and act like whatever graphics
pipeline you’re already trying to build.) A notable roadbump in
coffee
is that you have to specify some sort of
bounds
vector for the Text
object you are
creating… but how do you know what the bounds SHOULD be to render text
without distortion, until you’ve drawn it already? You don’t, as far as
I can tell. …Actually, I’m not quite sure what the
bounds
field is for, I just filled in some values that
looked semi-sane and it seemed to work. Also, Font::draw()
taking Into<Text>
instead of just Text
would be nice, ’cause constructing a full Text
object every
time is tedious. And Font::draw()
takes a
Frame
instead of a Target
, so you can’t apply
a transformation to it. No swirly demoscene effects here!
It works fine though once you actually get it all done:
fn draw(&self, view: &mut Self::View, window: &mut Window, _timer: &Timer) {
let frame = &mut window.frame();
let target = &mut frame.as_target();
target.clear(Color::BLACK);
// ...Bunches of calls to draw_actor() omitted, they're basically unchanged...
let level_dest = Point2::new(10.0, 10.0);
let score_dest = Point2::new(200.0, 10.0);
let level_str = format!("Level: {}", self.level);
let score_str = format!("Score: {}", self.score);
let level_display = graphics::Text {
content: level_str,
position: level_dest,
bounds: (100.0, 50.0),
size: 24.0,
color: Color::WHITE,
};
let score_display = graphics::Text {
content: score_str,
position: score_dest,
bounds: (100.0, 50.0),
size: 24.0,
color: Color::WHITE,
};
view.font.add(level_display);
view.font.add(score_display);
view.font.draw(frame);
}
And our update()
method needs basically no changing.
Again, there’s no audio support in coffee
yet, so I’ve
omitted the sounds. I could easily just pull in rodio
and
use it directly, but as I said, this is already taking a long time so
I’m going to skip that.
But after that, it builds! Woohoo, how does it run?
Nice. I like the color scheme. Needs one of those little
loading-screen mini-games built in though. :D Maybe one where you have
to mash buttons to drink enough coffee
for your next coding
binge?
Heyyyyy, looks good, apart from the whole not having rotations thing! What does the debug screen look like?
Nice. Though its text blends into the white level display text. Perhaps add a semi-transparent background behind it?
Conclusion: coffee
is not finished, but is finished
enough to be real cool. The fact that I took an
existing game from an entirely unrelated engine and it trivially slotted
into this very opinionated structure is mind-croggling, and says to me
that coffee
’s opinions are extremely good ones (or at
least, a good fit with my opinions, which is the same thing, right?).
This makes me super happy, and I am very excited to see where
it goes next.
Piston
So, Piston is the first Rust game engine project that went anywhere,
as far as I know. It started in the pre-1.0 days, it has lots of
contributors and an order of magnitude more downloads than its closest
competition. I have Opinions about Piston, mainly that it’s poorly
documented and poorly designed with no direction. This is what lead me
to make ggez
in the first place, I wanted something simpler
and better! But these Opinions are now going on three years out of date,
so it’s long past time I came back and gave it a fair go again. So I
will try not to rant too much. I honestly want to learn how it works and
try to make an unbiased assessment here.
Technology stack! Okay, first off, Piston is a giant buttload of modules, so you have to choose the bits you want. Going off its basic spinning-square example, we will use:
- Graphics:
piston2d-graphics
andpiston2d-opengl_graphics
. Or maybepiston2d-gfx_graphics
. I dunno. Which is better? Well the tutorial seems to usepiston2d-opengl_graphics
so let’s use that. - Windowing and input:
pistoncore-glutin_window
, which usesglutin
, which is basically justwinit
with some OpenGL stuff. - Audio:
piston-music
, which appears to use SDL2 for audio. Odd choice there, but there doesn’t seem to be any other sound-related crate. - Math: Nothing, it appears? I’ll just use
nalgebra
; I likeeuclid
better these days butnalgebra
is what the Astroblasto code already uses. - Other interesting stuff: Not sure where to find what it would use
for text or such. It’s a billion different modules in unrelated repos,
so I can’t just open up a single
Cargo.toml
and see what depends on what.
Oy, where to start… there’s just so much STUFF going on and still no clear high-level overview. Though it’s better than it was, I suppose. There doesn’t appear to be an event-loop trait of any kind, it seems, so we’ll have to make our own. No big deal. Let’s start with loading assets, that’s usually a decent entry point.
Okay, to draw an image, we need two things: an Image
and
a Texture
. This is not an uncommon division it seems, it’s
basically the same concept represented by
Drawable
/Background
in
quicksilver
and Image
/Sprite
in
coffee
. It divides geometry and texture into separate
objects, because they are separate objects on the GPU. I can complain
about Image
being an unhelpful name when contrasted with
Texture
, but whatever. We have to know how big a
Texture
is so we can make our Image
the size
we want, and Texture
is a generic associated type on the
graphics backend trait – ah, but it must implement the
graphics::ImageSize
trait, which gives us the methods we
want. So to create and draw an image it looks like this:
let texture = opengl_graphics::Texture::from_path(Path::new("resources/player.png")).unwrap();
let (width, _height) = texture.get_size();
let rectangle = graphics::rectangle::square(0.0, 0.0, width as f32);
let image = graphics::Image::new().rect(rectangle);
image.draw(&texture, default_draw_state(), transform, gl);
Wait, hang on, look at the rectangle
definition again.
What if our width
and height
are not equal? In
Astroblasto all the assets are square, but if we have both width and
height from our texture we might as well use them – and I’m going to
need to draw non-square stuff like text eventually. Okay, looking at the
graphics::rectangle::Rectangle
struct I see fields for
corner shape, border, color, constructors for various variants of these,
some draw methods… wait, where is the size of the rectangle at all?
Rectangle::new()
just takes a Color
. What?
There is no graphics::rectangle::rectangle()
function, just
square()
and some other stuff. There’s
graphics::rectangle::centered()
which does a coordinate
change but that takes a Rectangle
as input already. How the
heck do I make a rectangle that’s actually, you know, a rectangle? Fine,
RTFS, how does the square()
function make a rectangle?
wat
wait
there’s two Rectangle
types?
There’s graphics::types::Rectangle
, which is
type Rectangle<T = Scalar> = [T; 4];
, and there’s
graphics::rectangle::Rectangle
, which is
pub struct Rectangle { pub color: Color, pub shape: Shape, pub border: Option<Border> }
.
And just to ice the cake there’s also a Rectangled
trait,
which does nothing useful for this and is only implemented for
types::Rectangle
.
Okay. Okay, deep breaths.
I don’t want to have to store an Image
in our
Assets
structure alongside our Texture
, so
we’ll just make it on the fly as we need it. We need a
DrawState
struct too, but that’s just blend, stencil and
scissor settings, so we’re fine with the defaults. Oh, we also need a
Transform
. Piston defines its own transform type – oh wait,
no, dig deep enough and it uses the vecmath
crate, whatever
that is. So how do we create a transform? It looks like one is provided
to us from the GlGraphics
object, indirectly, and we have
to pass it around and use it as a base for further transformations, I
think? FINE. This results in our drawing looking something like
this:
fn draw_actor(
assets: &mut Assets,
gl: &mut GlGraphics,
transform: graphics::math::Matrix2d,
actor: &Actor,
world_coords: (f32, f32),
) {
let (screen_w, screen_h) = world_coords;
let pos = world_to_screen_coords(screen_w, screen_h, actor.pos);
let texture = assets.actor_image(actor);
use graphics::ImageSize;
let (width, height) = texture.get_size();
let rect = [0.0f64, 0.0f64, width as f64, height as f64];
let image = graphics::Image::new().rect(rect);
use graphics::Transformed;
// heckin' nalgebra makes it hard to turn a Point into a [f32;2]
let pos_x = actor.pos.x as f64;
let pos_y = actor.pos.y as f64;
let transform = transform.rot_deg(actor.facing as f64 * 180.0f64 * std::f64::consts::FRAC_1_PI)
.trans_pos([pos_x, pos_y]);
image.draw(texture, &graphics::default_draw_state(), transform, gl);
}
HOPEFULLY the transform math works out. Also,
graphics::default_draw_state()
? Really? There’s no
DrawState::default()
? Really? Wait a second,
graphics::default_draw_state()
doesn’t exist.
DrawState
DOES implement Default
, though
presumably at one point it didn’t. Your tutorials are out of date,
buddy. …actually, I sympathize. Keeping code examples in sync with what
actually exists is hard. skeptic
helps a
lot but it isn’t a panacea and still takes a fair amount of work to use
well. The example of how to load a texture has also drifted out of date,
but I think I got it worked out with a little digging.
Also, apparently all the transforms and such default to the scalar
type being f64
for some reason instead of f32
.
This is an easy trap to fall into these days, and I’ve certainly done
it, but really for graphics stuff you almost never need to use
f64
. I only noticed ’cause Astroblasto uses
f32
everywhere anyway. You might want f64
for
physics in extreme situations, but using f32
for all your
drawing code is just fine. Your screen is still only 4000 pixels across
or whatever, max.
Hmm, upon inspection, Piston having separate backends for different
drawing libraries is useless here. Lots of types leak through the
abstraction layer from your underlying graphics implementation:
Texture
being the main one so far. And
gl_graphics::Texture
is not interchangable with
gfx_graphics::Texture
; they have different methods,
different signatures for things like new()
, and don’t
implement a useful set of shared traits to provide a common API. So
while a small portion of your code doesn’t care whether your
Texture
comes from gl_graphics
or
gfx_graphics
and can be generic over both… large parts of
your program can NOT do this, at least not without a whole lot of extra
work. If you want to actually be able to pull one drawing
backend out of your code and drop another one in, then you have to really
commit to it and make EVERYTHING go through an abstract API.
Otherwise you’re just adding a lot of complexity for no real gain.
Right, sprite drawing should work, what about text? There’s
graphics::text::Text
which looks pretty straightforward,
but… where do I specify a font? I can specify a font SIZE, but not a
font? There’s a glyph cache trait in
piston::graphics::character
, but I don’t see a way to
actually create something that implements this trait. But you need to
pass a glyph cache to Text::draw()
. What the heck
implements this trait? Nothing in piston2d_graphics
. Is
there a piston_text
crate somewhere or something?
Ah, after rummaging around in the tutorials it appears that you use
piston_window::Window::load_font()
.
You load a font through the window handle?
I… I don’t even. That’s even beyond an M. Bison clip. I honestly have no words.
Right, so we’re not going to be drawing any text, on to audio!
piston-music
appears to be your only choice here, at least
that I can find, so let’s dig into it. Docs are nigh-nonexistent, but
there’s only like six functions, so we should be able to figure it out.
piston_music::start()
appears to be the place to start, it
just does some setup and then… takes a callback thunk to actually play
your music in? I think that’s what’s happening? Yeah, looking at the
source code that’s what you’re supposed to do,
piston_music::start(whatever, || stuff_that_plays_music() );
,
’cause it calls your thunk and then drops all the audio context objects
after it returns. I wonder what happens if you try to call
play_sound()
outside of that thunk, hm? And just how ARE
you supposed to make this work well together with your event and drawing
loops, which use the same model of “pass a thunk in to be called”? You
can nest the closures I suppose but that starts getting… eeeeugh.
Actually, hang on, play_sound()
is fishy too. There’s no
docs so all we have to go on is the function signature, which is this:
pub fn play_sound<T>(val: &T, repeat: Repeat, volume: f64) where T: 'static + Eq + Hash + Any
.
What in the world is this T
type for? There’s no particular
trait attached to it, and no documentation explaining it. Is it… some
sort of context? Or handle? Or arbitrary user-data or something? Wait,
there’s also this function:
pub fn bind_sound_file<T, P>(val: T, file: P) where T: 'static + Eq + Hash + Any, P: AsRef<Path>
.
So you bind a sound file to some sort of entirely arbitrary type to use
as a handle, and then feed that handle into play_sound()
to
index which sound to play? Let’s look at the source…
pub fn bind_sound_file<T, P>(val: T, file: P)
where T: 'static + Eq + Hash + Any,
P: AsRef<Path>
{
let track = mixer::Chunk::from_file(file.as_ref()).unwrap();
unsafe { current_sound_tracks() }.insert(val, track);
}
YEP that’s EXACTLY what’s going on! And what the hell is
unsafe { current_sound_tracks() }
? That leads me to the current
crate, which describes itself as “A library for setting current values
for stack scope, such as application structure”. Digging into the source
a bit, it looks like some sort of weird, ghetto, unsafe-as-hell
stack-ish-something thread-local version of lazy_static
that just lets you shove global mutable state into an
AnyMap
from wherever you want.
Man, you know what ELSE is good for setting current values for stack
scope such as application structure? LOCAL VARIABLES. Is
piston-music
some kind of ancient experiment that just got
made and then forgotten about and nobody was ever intended to use it?
’Cause that’s what it looks like. …no, it’s actively maintained. The
latest version was published in April 2019.
So we’re not going to have any audio! Are we even going to get this game to work at all? I really don’t know if I can handle it. I… I’ll try, I suppose. This stopped being fun with text rendering, but all we have left is event handling and main loop.
Dum de dum, more out of date tutorial docs, more useless backend
abstraction with piston_window
wrapping
glutin_window
plus one or two other options that aren’t at
all the same and so aren’t interchangable. Actually, now that I try to
look at it more, piston_window
for some reason seems to
offer a lot of graphics drawing stuff itself, and doesn’t seem to use
piston_graphics
or anything. It also uses the
gfx
drawing backend instead of OpenGL– okay. Shit. It turns
out that the Piston-Tutorials
repo and piston-examples
repo are mutually exclusive things that both use different subsections
of the ecosystem that don’t seem to overlap. piston_window
looks like it’s an entirely different thing that doesn’t interact with
piston_graphics2d
or piston-glutin_window
at all. My bad, I have no idea how I could have thought
that piston_window
and piston-glutin_window
were related! So let’s kick piston_window
in the head and
go back to what it turns out we’ve been trying to use from the FIRST
tutorial.
So I’m TRYING to make a main loop that looks like this tutorial code:
pub fn main() {
let mut state = MainState::new();
let opengl = OpenGL::V3_2;
// Create an Glutin window.
let mut window: glutin_window::GlutinWindow = piston::window::WindowSettings::new(
"spinning-square",
[200, 200]
)
.opengl(opengl)
.exit_on_esc(true)
.build()
.unwrap();
let mut events = Events::new(EventSettings::new());
while let Some(e) = events.next(&mut window) {
if let Some(r) = e.render_args() {
state.draw(&r);
}
if let Some(u) = e.update_args() {
state.update();
}
}
}
Except I can’t find docs for Events
or the types that
render_args()
and update_args()
gives me.
Okay, it looks like those are from piston::event_loop
,
which is a re-export of pistoncore-event_loop
and for
whatever reason rustdoc on docs.rs isn’t following the link to it, and
so I search for the function render_args
and find nothing
in the piston
crate. FINE, that’s not Piston’s fault,
docs.rs has had that bug forever. So I find the docs for the
pistoncore-event_loop
crate and it looks like
events.next()
returns Option<Event>
…
except Event
has no link on docs.rs either, so it comes
from some OTHER crate! It looks like it comes from
pistoncore-input
… I HOPE. Well there’s an
Event::render_args()
there, so I assume that’s what I’m
actually using! So render_args()
gets me a
pistoncore-input::RenderArgs
… which is absolutely useless
and has nothing on it that I need. How the heck do I get my OpenGL
context handle?!
Oh, I missed it, the
tutorial I’m trying to follow demonstrates that you have to call
GlGraphics::new()
by hand and store the
GlGraphics
object yourself. Though creating the window also
takes the opengl
object that says what version you want to
use, so it presumably ALSO creates an OpenGL context. Yeah this seems
okay.
Right, so pistoncore-input::Event
implements a
fuckzillion traits that all are different event types and all do
basically the same thing and it’s the ONLY thing that implements any of
those traits and there’s no generics involved so all those traits are
useless. But eventually I find that Event::button_events()
is probably what I want for input. …well, it’s both keyboard AND mouse
buttons for some reason, but forget that. So our input handling looks
like this:
fn input(&mut self, event: ButtonArgs) {
match event {
ButtonArgs {
state: ButtonState::Press,
button: Button::Keyboard(Key::Up),
..
} => {
self.input.yaxis = 1.0;
}
ButtonArgs {
state: ButtonState::Press,
button: Button::Keyboard(Key::Left),
..
} => {
self.input.xaxis = -1.0;
}
...
}
}
Okay so now that we’ve gotten this window-creation fuckup sorted out,
we have to go back to drawing and try to smooth it over. …um. Hmmm.
Wait, I need a base transform to feed into my draw functions, right.
Where does that come from again? Right, you call
self.gl.draw(draw_args.viewport(), |ctx, gl| ...)
and then
ctx.transform
has the projection or whatever that I’m
supposed to start from. Then you call
graphics::clear(color, gl)
and such in that thunk. …But you
could call graphics::clear(color, self.gl)
anyway,
outside of the draw()
thunk. What the hell
happens THERE I wonder? Let’s… let’s just not find out. And I also CAN’T
do what the tutorial does and store the GlGraphics
object
in my MainState
’cause that causes a double-borrow
elsewhere in draw_actor()
. Whatever, I’ll just store it in
a local in main()
and pass it in to
MainState::draw()
. So in the end, our drawing looks like
this:
fn draw(&mut self, gl: &mut GlGraphics, render_args: &RenderArgs) {
gl.draw(render_args.viewport(), |ctx, gl| {
graphics::clear([0.0, 0.0, 0.0, 1.0], gl);
{
let assets = &mut self.assets;
let coords = (self.screen_width, self.screen_height);
let p = &self.player;
draw_actor(assets, gl, ctx.transform, p, coords);
for s in &self.shots {
draw_actor(assets, gl, ctx.transform, s, coords);
}
for r in &self.rocks {
draw_actor(assets, gl, ctx.transform, r, coords);
}
}
})
}
And our setup and main loop looks like this:
pub fn main() {
let gl_version = OpenGL::V3_2;
let gl = &mut GlGraphics::new(gl_version);
let mut state = MainState::new();
let mut window: glutin_window::GlutinWindow =
piston::window::WindowSettings::new("Astroblasto Piston", [800, 600])
.opengl(gl_version)
.exit_on_esc(true)
.build()
.unwrap();
let mut events = Events::new(EventSettings::new());
while let Some(e) = events.next(&mut window) {
if let Some(b) = e.button_args() {
state.input(b)
}
if let Some(_u) = e.update_args() {
state.update();
}
if let Some(r) = e.render_args() {
state.draw(gl, &r);
}
}
}
Right! Finally. I THINK that’s it. It all builds! Let’s see what it looks like!
icefox@localhost ~/RustGameFrameworkGuide/astro_piston $ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.11s
Running `target/debug/astro_piston`
thread 'main' panicked at '
OpenGL function pointers must be loaded before creating the `Gl` backend!
For more info, see the following issue on GitHub:
https://github.com/PistonDevelopers/opengl_graphics/issues/103
', ~/.cargo/registry/src/github.com-1ecc6299db9ec823/piston2d-opengl_graphics-0.62.0/src/back_end.rs:317:9
note: Run with `RUST_BACKTRACE=1` environment variable to display a backtrace.
Great. Hm, maybe I just have to call GlGraphics::new()
after creating the window instead of before?
thread 'main' panicked at 'gl function was not loaded', ~/RustGameFrameworkGuide/astro_piston/target/debug/build/gl-833fcc759a3b905e/out/bindings.rs:20624:13
Okay. I think that’s all the work I’m going to put into that. How depressing. What a waste of time.
Amethyst
Fortunately, we’re on the finishing stretch!
So this is it. We’re off the edge of the map. The last familiar pixel-y mountain has drifted below the horizon with some very nice layering and parallax effects, we’ve glitched through the skybox, and we are now sailing into the shadowy, geometry-shader-tesselated waters of 3D game engines. Not that we’re going to make Astroblasto 3D of course; a Real Programmer can write a SNES-style game in any engine. And Astroblasto is barely even Atari 2600 quality!
Similar to Piston, Amethyst is a giant pile of sub-crates. I usually dislike this sort of structure because it makes it harder to understand how things fit together. You can arrange things so the sub-crates form nice layers and it’s more clear what depends on what, but this takes work and is not easy to do well. The tradeoff is that, for large programs, it’s far easier to understand them one sub-crate at a time, the sub-crates can be versioned independently, and you can pick and choose which pieces you actually need… but few projects are actually large enough that this matters. Is Amethyst in this category? Dunno, let’s find out!
(Oh– Dissimilar to Piston, these crates are all in one git repo so
you can actually see what exists. However they are still versioned
separately, which to my mind only increases the complexity without much
gain. How do I see the source code on github for the latest published
crate-set? Well… uh, I don’t think I can. There’s a branch marked v0.10,
but there’s no v0.9 so I don’t trust it. – Oh, though actually the
top-level amethyst
crate does
the right thing and re-exports known versions of all its
subcrates. Woohoo!)
So with that in mind, let’s take a look at what other dependencies Amethyst uses:
- Graphics:
gfx
, word on the street is that the next release will usegfx-hal
andrendy
and it will be super sexy - Windowing and input:
winit
- Audio:
rodio
- Math:
nalgebra
- Other interesting stuff:
specs
for the entity-component system,fluent
for localization(!),gfx-glyph
for text rendering (basically an earlier version ofglyph-brush
; I assume they’ll switch toglyph-brush
as part of the renderer upgrade)
Well, Amethyst has a full book-style Getting Started guide, so let’s
dive into that! …it also has a whole CLI tool of its own to set up a new
project. Hoo boy. Run amethyst new astro_amethyst
and it
looks like it creates… a Cargo.toml
, a single short config
file, and a main.rs
with a non-trivial-but-still-simple
Hello World application. …Is that really worth having a whole separate
tool for?
No, no, I’m being positive, honest! Well, trying to. I don’t hate everything! Really!
The docs are quite good; the book has sections on both abstract concepts and on practical implementations. It explains Amethyst’s state-stack system, which is a useful structure for just about any game. It explains how the entity-component-system model works, though I would probably try to organize the explaination a bit differently. It explains how Amethyst’s event channels work, which is something I haven’t seen before. It also explains all the deps you need to set up to run it, though it misses a minor thing or two. Like I said, keeping docs in sync is hard.
Okay, so the “make a window and clear it” hello-world program of Amethyst looks something like this:
extern crate amethyst;
use amethyst::{
prelude::*,
renderer::{DisplayConfig, DrawFlat, Pipeline, PosNormTex, RenderBundle, Stage},
utils::application_root_dir,
};
struct MainState;
impl SimpleState for MainState {
fn on_start(&mut self, data: StateData<'_, GameData<'_, '_>>) {
}
}
fn main() -> amethyst::Result<()> {
amethyst::start_logger(Default::default());
let path = format!(
"{}/resources/display_config.ron",
application_root_dir()
);
let config = DisplayConfig::load(&path);
let pipe = Pipeline::build().with_stage(
Stage::with_backbuffer()
.clear_target([0.0, 0.0, 0.0, 1.0], 1.0)
.with_pass(DrawFlat::<PosNormTex>::new()),
);
let game_data =
GameDataBuilder::default().with_bundle(RenderBundle::new(pipe, Some(config)))?;
let mut game = Application::new("./", MainState, game_data)?;
game.run();
Ok(())
}
Application
is our entry point.
GameDataBuilder
appears to create our assets and graphics
setup…? No, it has something to do with initializing the
entity-component system. Pipeline
defines the steps in
which we draw things. Amethyst uses the specs
ECS for
everything, so we’re going to have to rewrite quite a bit of our game
structure to re-arrange our stuff into specs
’s data
structures. For example creating a camera:
fn create_camera(world: &mut World) {
let mut transform = Transform::default();
transform.set_z(1.0);
world
.create_entity()
.with(Camera::from(Projection::orthographic(
0.0,
SCREEN_WIDTH,
0.0,
SCREEN_HEIGHT,
)))
.with(transform)
.build();
}
I’ve used specs
a fair bit before, so this feels pretty
familiar to me. We’re just going to shove what we have into this
structure and make it fit without worrying too much about making it
super nice, so let’s just make our Actor
a component.
Ideally we’d break it apart into pieces like Life
and
Motion
and whatever else, which I’d do if this were a
bigger game. However, we don’t NEED to, and this will take a fair bit of
re-architecting of Astroblasto’s guts to work though already, so I’m
going to do it the quick-and-dirty way for now. Let’s get to it!
Hm, does Amethyst not expose the
tragically-underdocumented-and-underused specs-derive
?
Nope, doesn’t look like it, but it uses the most recent version of
specs
so we can just pull in the matching version of
specs_derive
, slap #[derive(Component)]
on our
Actor
type and that’s about done. Update our creation
functions to take a World
and create a new entity in it,
and that should work fine.
I couldn’t find any math library associated with Amethyst, but it
turns out it uses nalgebra
, it just has it tucked away in
amethyst::core
. Having the whole thing be a pile of
different crates really makes searching for things on docs.rs a pain. I
think that’s the aforementioned bug with docs.rs not handling
re-exported crates properly; I can’t test it just now but I vaguely
recall that using rustdoc to build docs locally makes it work properly.
Ah, wait, Amethyst provides working docs at https://docs.amethyst.rs/stable/amethyst, much better.
Not perfect still, ’cause it doesn’t include major and complex
dependencies like specs
and nalgebra
, but
better.
So we now have a component, and turning our functions like
update_actor_position()
and
handle_timed_life()
and such into systems should be
straightforward, but first there’s a bit of boilerplate to handle. You
have to register components with specs
, basically so it can
do some bookkeeping and allocating of structures and such ahead of time
and not have to check it every time you iterate through a system. Looks
like Amethyst has some shortcuts for this in its
GameDataBuilder
, which it calls “bundles”, which let you
automatically register a bunch of related components such as
Transform
and Camera
.
I’m not going to explain how specs
works in depth, I’ve
done so
elsewhere and Amethyst doesn’t seem to add anything special to it.
Suffice to say that our “move actor” system looks like this:
pub struct MotionSystem;
impl<'s> System<'s> for MotionSystem {
type SystemData = (
WriteStorage<'s, Actor>,
);
fn run(&mut self, (mut actors,): Self::SystemData) {
for (actor,) in (&mut actors,).join() {
// Clamp the velocity to the max efficiently
let norm_sq = actor.velocity.norm_squared();
if norm_sq > MAX_PHYSICS_VEL.powi(2) {
actor.velocity = actor.velocity / norm_sq.sqrt() * MAX_PHYSICS_VEL;
}
let dv = actor.velocity * SECONDS_PER_FRAME;
actor.pos += dv;
actor.facing += actor.ang_vel;
}
}
}
The rest of the systems are also pretty simple to rewrite into
specs
, though the ones that add and remove entities are
kind of a pain in the butt. It’s much easier to explicitly know “this
runs at time T, this runs at time T+1, and they will not conflict
because I wrote them in the right order” than it is to handle the
general case of expressing “we must lazily sum up all the changes to
this collection, then apply them all at once after we’re done mutating
the things in it.”
But once all these are defined we add the systems to our world in our
GameDataBuilder
. However, since specs
will run
multiple systems in parallel threads if possible, we have to provide
explicit sequencing of how these systems may be executed. We must update
actor positions, then do the bit that makes them wrap around the screen
if they’re outside of it, then check for collisions and decrement timed
life, then remove actors that are dead. specs
lets you give
a system a name as a string, and specify by name which systems it
depends on, which feels a little crude ’cause you can misspel a name or
something and it won’t notice until runtime. But nobody’s ever been able
to come up with something better, including me. So, when we add our
systems to the GameDataBuilder
, it looks like this:
let game_data = GameDataBuilder::default()
// ...snip bundle stuff ...
.with(InputSystem, "input_system", &[])
.with(MotionSystem, "motion_system", &["input_system"])
.with(WrapSystem, "wrap_system", &["motion_system"])
.with(CollisionSystem, "collision_system", &["wrap_system"])
.with(TimedLifeSystem, "timed_life_system", &[])
.with(
CheckDeadSystem,
"check_dead_system",
&["timed_life_system", "collision_system"],
)
.with(
RespawnLevelSystem,
"respawn_level_system",
&["check_dead_system"],
);
Sure, we could just have one system that does our entire
update()
function, but I wanna do it this way. Interesting
to see how complicated the sequencing dependencies actually are for
something this simple, isn’t it?
Ah, but we never made an InputSystem
! I was putting that
off, but let’s attack it now. It appears that Amethyst provides an
InputHandler
resource and system that can load input
mappings from a config file. So we throw something like the following
into resources/bindings_config.ron
:
(
axes: {
"turn": Emulated(pos: Key(Right), neg: Key(Left)),
"thrust": Emulated(pos: Key(Up), neg: Key(Down)),
},
actions: {
"fire": Key(Space),
},
)
I’m guessing a little on how the actions
bit works, I
can’t find any documentation explaining it. Well, we’ll see how it
works. We just load the config file up and register the
InputHandler
as shown in the tutorial.
Then our InputSystem
becomes:
pub struct InputSystem;
impl<'s> System<'s> for InputSystem {
type SystemData = (
WriteStorage<'s, Actor>,
Read<'s, InputHandler<String, String>>,
Entities<'s>,
Read<'s, LazyUpdate>,
);
fn run(&mut self, (mut actors, input, entities, lazy): Self::SystemData) {
for (actor,) in (&mut actors,).join() {
// Matching on the actor tag is a hack but it works.
// I don't feel like breaking Actor apart into proper
// components.
if let ActorType::Player = actor.tag {
let xaxis = input.axis_value("turn").unwrap_or(0.0) as f32;
let yaxis = input.axis_value("thrust").unwrap_or(0.0) as f32;
actor.facing += SECONDS_PER_FRAME * PLAYER_TURN_RATE * xaxis;
if yaxis > 0.0 {
let direction_vector = vec_from_angle(actor.facing);
let thrust_vector = direction_vector * PLAYER_THRUST;
actor.velocity += thrust_vector * SECONDS_PER_FRAME;
}
if input.action_is_down("fire").unwrap_or(false) {
//self.player_shot_timeout = PLAYER_SHOT_TIME;
let direction = vec_from_angle(actor.facing);
let velocity =
na::Vector2::new(SHOT_SPEED * direction.x, SHOT_SPEED * direction.y);
lazy.create_entity(&entities)
.with(Actor {
tag: ActorType::Shot,
pos: actor.pos,
facing: actor.facing,
velocity: velocity,
ang_vel: SHOT_ANG_VEL,
bbox_size: SHOT_BBOX,
life: SHOT_LIFE,
})
.build();
}
}
}
}
}
To me this supports my assertion that input always kinda sucks, and
adding/removing entities is always a pain. Amethyst does make it about
as nice as is possible, though. Part of the pain is just that I’m not
taking the time to REALLY reorganize this to fit nicely into a
specs
-y style. I’m tired, and I think I’m going to leave
collision and level respawning as exercises to the reader. They’re a lot
of reorganization that isn’t terribly interesting, and I want to get on
to asset loading and drawing.
Ah, Amethyst actually has a full asset loading system! As usually
happens whenever you try to make an asset loading system in Rust, it
looks like an asset is basically an Arc<Whatever>
. It
separates asset loaders and asset storage, and makes them explicit
objects that are stored as resources in the specs
World
. Unfortunately, it seems to always expect a sprite
sheet with a specification file telling it where things are, instead of
just letting us handle each image as its own thing. So, let’s create a
spritesheet config file for each of our three sprites, such as:
(
spritesheet_width: 16,
spritesheet_height: 16,
sprites: [
(
x: 0,
y: 0,
width: 16,
height: 16,
),
]
)
Amethyst’s config files all appear to be RON, which is a bit too “cute” IMO – Yay, yet another config file format to figure out! On the other hand, it IS very nice for representing Rust’s enums and options, which JSON never does well and TOML doesn’t really handle either. So as Yet Another Config File Format’s go, it serves a useful purpose. And at least it’s not YAML!
So as part of our on_start()
method we load the images,
load the spritesheet specifications, slap them together, and create
Amethyst’s SpriteRender
components out of them and register
them. Then we update our actor creation functions to add those
components to our Actor
entities when we create them.
…except it won’t work ’cause we don’t use Amethyst’s
Transform
component so it doesn’t know where to draw our
stuff. Augh, doing things the quick and dirty way has bitten me in the
ass! Plus, I’m not sure if it’s possible to get an
AssetHandle
back out of the AssetStorage
after
the asset has been created; I don’t see a way to feed it a name or label
or such and pull an Asset
out. So we’d have to rework our
Assets
type and shove it into the World
as a
resource.
…
…Okay, you know what, I think I’m done. Not because Amethyst is bad, ’cause it isn’t, but just because I’ve ported this game to four and a half different systems already and I’m tired, and I’ve gone far enough to get a feel for Amethyst works. Amethyst offers a lot of functionality, but much like with other Real Game Engines, you have to organize your program to its expectations. For Astroblasto that would basically be a complete rewrite, and I don’t have the energy to do that. This is an overview, not a tutorial on Amethyst, so when I mostly end up copy-pasta’ing code from its existing tutorial I’m not making anything new.
Don’t get the feeling that I am being a downer about Amethyst’s
boilerplate, though. If you’re making a FULL game, with all the things a
full game needs like a menu screen, screen config options, and so on,
then you will end up with this sort of stuff even if you use something
like ggez
or coffee
. You can write all this
yourself, it’s not too hard… but it’s boring and you end up with a lot of
boilerplate that way as well. Past a certain complexity you
need an asset system, you need an input abstraction
layer, you need a scene system, you need some abstraction over your
lower-level drawing engine, and so on and so forth. However, this is all
stuff that mostly gets written once, and then increases in size far more
slowly than your game itself. If your game logic is 500 lines, then
adding 500 lines of boilerplate is pretty painful. But when your game
logic becomes 10,000 lines, then your boilerplate is probably about 1500
lines or so, and the amount of power you get from it is incredibly
useful. Creating a new object type becomes a matter of throwing some
components together and maybe changing a system or two, saving and
loading games just dumps existing state with built-in serialization
using serde
, going from one screen to another is a function
call or two, and life is great.
So, Amethyst looks like it would do just fine for a 2D game: it has
sprite sheets, it has animations that let you interpolate things around,
and it gives you a ridiculous amount of control over the rendering
pipeline. It doesn’t look like quite a full skeletal animation system,
and it doesn’t seem to let you do flipbook-style sprite animations
easily, but the renderer submodule is big enough I could easily have
missed those features, and if someone wanted to write them I’m sure the
Amethyst devs would love contributions. Its input system is exactly what
you want. It exposes specs
for you to use exactly how you
would expect to use it, and then adds a few ergonomic shortcuts. I
haven’t touched its audio system, but at a glance it looks like a thin
layer over rodio
, and so is fairly similar to everything
else that’s a thin layer over rodio
, nothing special but
perfectly usable.
And, more to the point, Amethyst’s library structure is fairly sensible, its documentation is good, and there’s tons of example code. It’s a large, old open-source project being worked on by many people (at least by the standards of the Rust gamedev community), so I expected it to be very fuzzy and poorly organized, the epitome of design-by-committee. While it may or may not have been that way once, and there may still be places that’s the case, the current structure is sensible and everything seems to fit together as far as I could explore in an afternoon. It’s not flawless, but there’s piles of small conveniences that give the feeling that, yes, this is a framework that people are actively using to make real games. These people know what they are doing, and they are not so caught up with their grand design that sharp edges and impedance mismatches get ignored because their design doesn’t account for them. This feeling of polish is a big deal for a user. Amethyst also seems to have a good half-dozen serious contributors, so hopefully they’re getting to the point where the size of the project can snowball a bit more and they can start doing things that one-maintainer projects really can’t.
So I said in my graphics library guide that Amethyst feels like vaporware? Welp, I was wrong. Hopefully it will be the next Godot instead.
Conclusions
Whew! So, that’s what Astroblasto looks like in each of the major Rust game frameworks. Took a bit more work than I hoped, but I also learned a hell of a lot about how my stuff fits into the larger picture. Time for random observations!
First, wow, I didn’t realize ggez
was so low level! I
guess I kinda like writing lower-level code. ggez
also goes
more for completeness than most of these libraries, though part of that
is just ’cause it’s been around the longest and so has had the most
people saying “hey,
can you make it so it’s possible to do X?”. But it has also been a
deliberate design decision all along to expose ggez
’s guts
to the user more than is typical: you can use your own event loop, you
can pull the gfx-rs
renderer out of it and make your own
drawing pipeline with it, there’s provisions for saving and loading
config files full of graphics options, etc. It offers lots of hooks, and
tries to be moooooostly unopinionated about which ones you care to use
or not. This leads to a more powerful API that is very broadly usable,
but also rather larger and hairier than a nice minimalistic thing.
ggez
also has deliberately avoided including animation,
physics and so on, because those things really don’t need to be
built in to it. I once had hopes that multiple people, including me,
would write libraries for those features atop it. Alas, that doesn’t
seem to have happened; instead everyone just writes their own whole
framework. XD
Also, I just realized, none of the other 2D libraries offer general purpose mesh rendering as a first-class API. I admit its a niche thing for 2D that’s not often used for games, and optimizing your drawing code to only handle quads makes your life a bit easier, but its not THAT much work.
Turns out that one way or another, event handling always sucks! No
matter what you do you end up with big long ugly match statements or
stuff, fiddling random state variables that then get checked by update
functions. As I said, IMO it’s easier to go from edge-triggered to
level-triggered input than the other way around, so that’s what I
usually do. Having something like Astroblasto’s InputState
struct that only has the abstract input that your specific game cares
about also makes it less hairy… or at least isolates all the hair to one
place. Life gets much cleaner when you have a pure function that’s
basically fn(Events, KeyMap) -> InputState
, and then
your event handling just becomes another function,
fn(&mut GameWorld, InputState)
. Making
InputState
actually represent everything you want is still
a pain in the butt though, especially if you add or remove capabilities
or have a GUI or a complex interface with text-box input or
roguelike-style hotkeys… Like I said, it always sucks. Doesn’t look like
anyone’s come up with a significantly better way! Though
coffee
tries.
It would appear that thinking about graphics and main loop structure
is more fun than implementing “solved” problems like audio and asset
loading, so people put more work into those. It’s weird, ’cause I think
the filesystem abstraction layer was the first thing I actually wrote
for ggez
.
Async asset loading actually works pretty well… Most of the time you don’t need to get fancy about things though, so all you actually need to do with async stuff is “update loading screen and otherwise basically block until stuff is ready”. Limiting use cases make complicated problems simpler!
Measuring graphics coordinates in our 2D games in units of pixels
with (0,0)
at the top-left corner with Y increasing
downwards is generally a stupid way to do things… but it’s also what
everyone actually wants for 2D rendering, so that’s just gonna
stay that way for a while. And it actually has its advantages ’cause it
makes life simpler for rendering left-to-right text. But apart from that
there’s no reason to do it anymore… besides the fact that everyone
expects it to be that way. So, everyone does it.
I’ve noticed this is really one of the sorts of things that is a fundamental different mindset between 2D and 3D graphics, actually. 2D graphics people want everything to be in pixels, because their art editors use pixels, their images are pixels, the screens are pixels, and it’s very easy to just want to slap one onto the other. But all our graphics goes through a 3D rendering pipeline these days, even 2D stuff, and 3D fundamentally does not care about pixels. Everything is in floating point coordinates. Textures don’t have pixels, they have texels; these are not the same, because the graphics card can and will do filtering and resampling and whatever else it feels like before some part of your texel gets mapped to some little blinky lights and actually put on the screen. Even if you just have a 16x16 texture and slap it on the screen at a 1-to-1 scaling, there are MANY opportunities for a graphics card to do various transformations to it under the hood. Beyond filtering and projection you have mipmaps, shaders, render targets, and Eris only knows what else. Pixels do not exist until the very, very last moment in the rendering process.
The GPU wants to ignore pixels as much as possible, and does everything possible to cover them up. But if you’re doing retro 2D graphics, you want to think about pixels as much as possible, because your art style relies on Getting It Right. With 3D graphics you rely on anti-aliasing and filtering to smooth out rough edges, but with pixel-art graphics those rough edges are part of the art. Smoothing and anti-aliasing of pixel-art graphics tends to look like crap, so you want your graphics pipeline to avoid it, and if you’re the type of person to make pixel-art graphics in the first place you’re probably the sort of person to obsess over these sorts of details. This can lead to some contention. So it’s a hard choice for a game library to make: Do you represent things in the way humans want, and lie to them about what the world looks like to the GPU? Or do you represent things the way that the GPU wants, and make life harder for the human?
ggez
sort of takes the middle road, leaning a little
towards the GPU side. Coordinates on the screen default to units of
pixels, and images are scaled to their size in pixels. But coordinates
within an image, for origin points of rotation and source
rectangles, are in normalized floating point units. On the other hand,
all of the other 2D libraries seem to take the first option: lie to the
human about the GPU’s world, pixel coordinates everywhere. And I
actually find it less convenient, not more. But it’s interesting that
ggez
alone is the odd one out in this. Is there a better
way to have it both ways? Dunno.
Error handling is also something ggez
pays a lot of
attention to, everything that can concievably fail returns
Result
. Some other libraries do as well. Others don’t. It’s
surprising how unimportant it is sometimes though. I mean, really, if
you try to draw an image, and can’t ’cause, say, a graphics driver has
crashed and your OpenGL context is Just Gone… what ARE you going to do
besides panic anyway? Plus that sort of thing really doesn’t happen
often, so it’s not something that we the developers exercise a
lot in day-to-day programming. So perhaps instead of returning
Result
wherever it’s remotely possible it would make life
marginally smoother to hide it and instead offer a centralized hook that
errors get dispatched to, perhaps in the vein of
quicksilver
’s handle_error()
or
coffee
’s on_close_request()
? Dunno, would be
neat to play with.
I hadn’t realized nalgebra
had taken over the world so
thoroughly, but I couldn’t find anything that didn’t use it.
It’s wayyyyy more complicated than it needs to be for video game math…
but it’s also very high quality, and it’s nice to have a de-facto
standard even if the standard is not optimal. It would be cool to see
more things using mint
though!
Also, on the meta level, Sourcehut works pretty well. Using it with Mercurial is sometimes slowish but works fine. There was a problem once where I couldn’t pull from HTTP due to some server-side error… but then I tried again a few hours later and it had already been fixed. Try it out, or run your own instance if gitlab is too heavy for you. It doesn’t really provide a full project-management suite, but for smaller projects it’s probably perfect.
Dang, I need to make a non-trivial game with Amethyst someday. Coffee, too. Maybe next Ludum Dare?
…You know, I kinda want to make a new 2D game framework too, now. I have some new ideas to try out.
Appendix: Those New Ideas To Try Out
I’m putting some thoughts here ’cause I don’t have anywhere else to put them. sylvite, maybe? I have no particular plans on actually acting on these ideas yet, I just want to brainstorm.
- coffee’s
View
andInput
types are very interesting, but as demonstrated, not actually essential. Helpful though… Keep thinking about that sort of model and see how it evolves. - coffee’s debug screen is excellent and I want it.
- Making better keybinding options and axes and such built in a la Amethyst would be very, very nice. Hard to do well in the case of roguelike-ish games or text box input or such though… Maybe two different API’s for raw or cooked events? dunno.
- A built-in scene stack might be very nice.
- tetra’s audio API is probably the best way to go. Skip
rodio
and build something like that atopcpal
perhaps? - ggez’s VFS layer is best in class and deserves to be its own thing
- ggez’s mesh API is best in class but could still be better. Might have some things to learn from Amethyst.
- being moar opinionated about drawing and coordinate systems might make life simpler.
- A clearer separation between geometry and textures, as well as
different methods for addressing textures by pixels or by normalized
float coordinates (coffee’s
Sprite
vsQuad
) might also make life simpler. - Life would get simpler if we go for a higher level API in general. For example, entirely controlling the event loop.
- the world really needs a good, general-purpose animation crate. FFI binding to Spine, whatever.
- You must treat wasm as a first-class citizen from the start instead of trying to tack it on later
- You must not treat Mac as a first-class citizen from the start because they are a huge maintenance burden and it’s not worth it. :|
- Avoid success at all costs.
- Async might be worth experimenting with, and probably will be necessary to handle somehow on web.
- Keep using
mint
.