Necking devlog 2
Second weekly devildahu devlog, the first one is available here.
Necking
This week, I only dedicated 3 days to Necking, and three very short days at that.
I wanted to make Necking more editable to enable less technical members of the team to play with parameters of the game, and especially load arbitrary models for the giraffes.
Scene reload
bevy ostensibly supports hot reloading. However, hot reloading in bevy is broken for scenes. This is a problem, since giraffes are loaded as scenes, this bug would prevent loading live new giraffe models.
Solving this involved two things:
- Have some sort of management of scenes to keep track of spawned entities related to a scene.
- Writing hot-reloading code.
For (1) I was already using bevy-scene-hook
. However, as implemented, it
didn’t allow for reloading. So I created a reload
module, mostly a copy of
the original code, but with handling for respawning and despawing.
For (2), I started by shamelessly copying the bevy code for hot-reloading,
it’s about 30 lines of code. It’s nothing more than creating a
RecommendedWatcher
from the notify crate, establishing a communication
channel between it and a bevy system and reacting to events it sends.
I kept this code, and when receiving an event, I set the ReloadSate
component
from the newly created reload
module of bevy-scene-hook
.
Parametrization
I’ve been a great fan of Sakurai’s series on video games. He explains in one of those videos that he uses an Excel Add-In to manage all the parameters (damage, time, cancel frames etc.) of the Smash Bross characters.
Necking had a lot of parameters hardcoded. But as a good programmer, I avoid
just sprinkling the code with arbitrary values. Most of the parameters, even if
hardcoded, were named and centralized as a series of const
definitions.
Hot programming tip
If in your code, you see a literal value within a function such as 0.132
(something extremely specific), it is probably worthwhile to replace it with a
constant. Naming things help build a semantic narrative around what your code
is doing.
Furthermore if in the future, you need to re-use the same value, you can simply use the variable name rather than copy/paste the value:
// What is 12.344???
fn do_the_foobar(position: &mut Vec3) {
position.x += 12.344;
}
// much better,
// I know that I'm adding RETICULAR_OFFSET to `x`!
const RETICULAR_OFFSET: f32 = 12.344;
fn do_the_foobar(position: &mut Vec3) {
position.x += RETICULAR_OFFSET;
}
My goal was to get rid of all those const
s and replace them with a bevy
resource that I would load at startup from an external file.
I also wanted to be able to write a template file, ideally with default
values taken from the const
s pre-existing values.
It was almost straightforward. I decided to create a struct Parameters
per
bevy plugin/rust module that used to rely on hardcoded const
values.
#[derive(Clone, Debug, Resource, Default)]
#[cfg_attr(feature = "debug", derive(Reflect, FromReflect))]
pub struct Parameters {
pub controls: scheme::Parameters,
pub neck: neck::Parameters,
pub camera: cam::Parameters,
pub input_mapping: input::Mapping,
pub models: Models,
}
mod scheme {
#[derive(Clone, Debug, Resource, Default)]
#[cfg_attr(feature = "debug", derive(Reflect, FromReflect))]
pub(super) struct Parameters {
neck: neck::Parameters,
giraffe: giraffe::Parameters,
tongue: tongue::Parameters,
}
mod neck {
pub(super) struct Parameters {
idle_stiffness_multiplier: f32,
idle_damping_multiplier: f32,
moving_stiffness_multiplier: f32,
// ...
}
}
mod giraffe {
// ...
}
// ...
}
mod cam {
// ...
}
//...
Then, in systems defined in those plugins, I would add a Res<Parameters>
to
access the parameters, instead of directly using a const
.
This worked fairly well, the rust module privacy system also aligns very
nicely with this. In a module, I needed to access the root Parameters
fields,
so the root’s field needed to be public, but each individual module
Parameters
could keep their field private, hence limiting what can be
influenced by which field, resulting in much more robust code.
A snag was the hook for spawning giraffes. The physics parameters for the neck
are used in the scheme::neck
module, but also in the neck
module, where the
giraffe is spawned.
I simply made those parameters public across the whole game, so that I could
use them in both places. The cleaner solution would have been to get rid of
that neck
module, but I didn’t have time for that.
Serializing parameters
Only thing left to do was reading and writing to a file the parameters, so that they could be changed by anyone with a text editor.
I hit a snag here. I relied on bevy’s bevy_reflect
for serialization, this is
because bevy_reflect
is readily available. The standard rust crate for
serialization is serde
, but it has the reputation of drastically slowing
down compile time.
I then used the toml
crate for the parameters file.
When I first launched the game to write the
default list of parameters, I hit a very generic error. It said something along
the line of “enums are not supported” without further details. I recalled that once
upon a time, bevy_reflect
didn’t support serialization of enum
s. But having myself
reviewed the code that added support for enum
s in bevy_reflect
, I was very
surprised to encounter this error now!
Turned out the error emanated from the toml
crate, indeed, the
doc string for the toml::ser::Serializer
struct has a line explaining that
toml
doesn’t support enums (It would have been judicious to advertise that
limitation in the README or the crate-level doc string, or having a better
error message).
In my Parameters
, I only had a single enum
, for custom input:
enum KmKey {
Mouse(MouseButton),
Key(KeyCode),
}
I needed to replace this. I immediately thought of two options:
- Use a
struct
with twoOption
fields, one for mouse buttons, one for keyboard keys. MouseButton
andKeyCode
being two enums, merge them in a giga-enum. Precisely, whattoml
doesn’t support are enums with data,KeyCode
andMouseButton
are what rust calls “C-like enums”, they do not have associated data, so making a large enum would work.
I opted for (2), KeyCode
is a very large enum, but whatever eh! If at any
point KeyCode
or MouseButton
in bevy changes, I’ll have a compilation error
allowing me to update the enum. This is thanks to rust’ exhaustiveness check.
The February game
I solicited game ideas in the devildahu discord chat for the 3 weeks project. Brinzer posted his detailed design doc of PointFusion, a 2d Ik-driven game.
I’m not sure it’s possible to complete it in 3 weeks, so you might not hear much more of it until the distant future.
Cuicui
In the meantime, between two games projects, I thought it would be productive
to focus on my UI framework, bevy_mod_cuicui
.
A very naive thought.
I spent the past 4 days writing design documentation, just to know what code to write for it! I always feel “so close” to getting somewhere, yet at each point in time, I am seemingly as far from a solution as I was at any given moment in the past.
I think this stems from me not having an exact idea of what I want to do with
bevy_mod_cuicui
. How much flexibility should it have? What should it do?
After 4 days though, I’m convinced (as I was 2 days ago though) that I’ve a vague – if not clear – idea of what I want.
Bevy and ECS’s strength is in modularity. I don’t really want to write a UI framework. UI frameworks have a tendency to be totalitarian, they deal poorly with things that exist outside of them. This is the antithesis of what I want to do. I want to empower people, give more possibilities.
My current plan is to simply write a few systems, that, if used together, enable a pleasant UI experience in bevy:
- The layout algorithm I mentioned last week.
- A
Prefab
trait to spawn things into the ECS, despawn them and query their current state. - A set of
Prefab
s to build UI elements that are a composition of entities. - A QT-inspired signaling system, where functions associated with certain entities can be “activated” and read the state of children entities to do things.
I’m still not sure for (4). The goal is to avoid footguns and enable developers to reason locally (avoid having to think holistically, accidentally mixing several instances of the UI elements)
I’ll probably need an ultimate layer of coating on this. Raw, it will still be painful to use, so I expect to add something like a macro to thread together multiple UI elements.
Turns out making a UI framework is as difficult as people say.
(you may be interested in this blog’s rss feed)