-
Notifications
You must be signed in to change notification settings - Fork 66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Dev tools abstraction #77
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good! Really liked this RFC, just a couple comments with things that I noticed here and there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Noticed a error in my previous comment, sending again
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an interesting problem to try and solve. I'm not fully sold on the current distinction between commands and modals, and feel like it needs a bit more refinement. To me, the biggest difference between the two is one has state, while the other is stateless. Taking the no-clip
camera as an example, that is definitely stateful, but you still want commands to interact with it (e.g., /noclip speed 10
, /noclip on
, etc.).
I think it would be useful to think of the ModalDevTool
trait as describing a tool state which can be toggled (omitting the parse_from_str
entirely). This lends itself well to adding extra traits for visualising these tool states (e.g., in a log, in a toolbox, etc.). Rich interactions with the dev tool states would then be the job of dev commands.
So for a no-clip camera dev tool, you'd define the ModalDevTool
as the state of the tool, and a DevCommand
for adjusting parameters. It would make sense for a default DevCommand
for toggling an arbitrary ModalDevTool
to be provided (e.g. /toggle noclip true
).
This is really cool! And since I am maintaining bevy-console I am very excited about the possibilities here! A few things though: First of all why have Also I don't think we necessarily should depend on clap directly, but we could have some more abstractions here, I am thinking something along the lines of a smaller let matches = parse(input);
let val: usize = matches.get<usize>("my_arg"); // 123
// or
let val: String = matches.get("my_arg"); //123 EDIT: |
Co-authored-by: Pablo Reinhardt <[email protected]>
I've expanded the toolkit example to try and clarify this: you need it to be able to allow for free-form runtime config of these resources. Obviously you could make dev commands for each way you might want to configure the resource, but I was thinking it would be more user-friendly to allow them to just set the struct's values directly.
Yeah, I'm not sure of the best way to handle this. I'd like to interoperate with
Yeah, I actually really want to support parsing these objects from the new |
The RFC looks really good! I really like the good analysis done on usage and what should be classified as dev tools and will be used and what will not. From this analysis comes for me a reasonable and good trait structure. However, I think the RFC did not address one important issue in my opinion. Part of the dev tools may want to display debugging information on the screen or get some input from the developer. For example, the fps overlay displays the number of fps in the bottom right corner, and the entity inspector may want to show some editable fields and buttons near the right edge of the screen. Or the realtime statistics render graph will want to display rendering times information somewhere in the static area. And if multiple dev_tools claim the same area of the screen to display, it will be a mess. And obviously dev_tools can and will conflict over displaying information. Accordingly, the question not solved and not asked in my opinion in RFC is "how is it planned to organise the location of the displayed information of dev modules?". (Or how do we start building editor UI from dev tools xDDDD, sorry). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The DevToolsRegistry was a bit confusing, but I tried my best to organize my thoughts here
rfcs/77-dev-tools-abstraction.md
Outdated
/// Parses the given str `s` into a modal dev tool corresponding to its name if possible. | ||
/// | ||
/// For a typed equivalent, simply use the `FromStr` trait that this method relies on directly. | ||
fn parse_and_insert_tool(&self, world: &mut World, s: &str) -> Result<(), DevToolParseError>{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First: What are we parsing here? I was having a look and ModalDevTool trait doesn't have a metadata at all. If we expect to not only parse commands here, but also tools themselves, we can store in the Metadata some kind of identifier to it: Maybe a enum of some kind?
I Think we need to clarify how are we going to register commands so this can be an easy way forward.
For example, lets say we store in the metadata an enum
enum DevToolType {
ModalDevTool,
DevCommand
}
Now, something that maybe can be strange: To do everything the way we currently do with strings, we must ensure that all the tools have different names.
And so, we get the tool metadata, with the enum stored, and we can make a command or insert the tool, as simple as possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First: What are we parsing here? I was having a look and ModalDevTool trait doesn't have a metadata at all
This is an omission! Lemme fix that...
And yeah, I've been relying on unique names for each dev tool.
rfcs/77-dev-tools-abstraction.md
Outdated
modal_dev_tools: HashMap<String, ToolMetaData>, | ||
/// The metadata for all registered dev commands. | ||
/// | ||
/// The key is the `name()` provided by the `DevCommand` trait. | ||
dev_commands: HashMap<String, DevCommandMetaData> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For starters, can we unify this two metadatas into a single metadata?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For now, maybe. Down the line, I think their implementations will diverge, so I'd like to keep them separate and eat the code duplication.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From my perspective, I like them separated. In my mind this goes well with the comment I made about they being +- equivalent to Resources and Events, so the separation makes them pretty clear
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The approach will change depending on what we do, if we have a function parsing both the commands and modals at the same block, I think we might need to unify the metadatas. At the same time, if we choose to have two functions: A command parsing method and a tool parsing method, this can be simplified and we can have two distinct metadatas.
But that will have significant differences in parsing, as we have to know beforehand if we are parsing a tool or a command.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of having a common pattern for development tools. I'll read more on weekend but for now my first point is this RFC should decouple the parsing and keep Reflect
as a generic requirement, since requiring FromStr
will only be useful for "toolboxes" like bevy_console
, for all others like having UI, remote debugging or even WASI integration, you won't use &str
at all, you just want to serialize the command and process it or use Reflect
meta info.
If ModalDevTools: Reflect
this means any "toolbox" can parse a String
without forcing the dev tools devs to do that.
my_awesome_command 10 15.5 enabled=true
Args without name would be used as positional (Reflect::field_at(index)
) and args with name would be set with (Reflect::field(name)
).
This way we shift the command parsing from MoldaDevTools
itself and move to "toolbox" impls, making this even more flexible.
Extending DevCommand metadata for allowing untyped pushing of DevCommand to Commands
Co-authored-by: Afonso Lage <[email protected]>
@alice-i-cecile / @rewin123 future considerations RE: layout is input redirection / modality. Might be kinda bike-sheddy. The free-cam example seems obviously modal, and I would expect it to capture input (to avoid binding collisions with gameplay systems). (I do wonder whether we want to let the developers determine at which point input is captured, but I think this is a wider issue in bevy as it stands.) For things like overlays (which is more where layout comes into play), I wouldn't expect them to be 'modal', since that usually refers to whole-window + input capture. We could just have an orthogonal system for allocating dev overlay space, and have these kind of things be If you toggle multiple dev tools, which should get the input? (I'm expecting a lot of these tools will have overlapping bindings with gameplay, and each other.) |
Yeah, the focus management thing is generally a big open question in Bevy as a whole: see bevyengine/bevy#3570 for more context there. I think it can and should be tackled independently though, and done ad-hoc to start. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some comments about object safety.
rfcs/77-dev-tools-abstraction.md
Outdated
fn get_tool_by_id(world: &World, component_id: ComponentId) -> Option<&dyn ModalDevTool> { | ||
let resource = world.get_resource_by_id(component_id)?; | ||
resource.downcast_ref() | ||
} | ||
|
||
/// Gets a mutable reference to the specified modal dev tool by `ComponentId`. | ||
fn get_tool_mut_by_id(mut world: &mut World, component_id: ComponentId) -> Option<&mut dyn ModalDevTool> { | ||
let resource = world.get_resource_mut_by_id(component_id)?; | ||
resource.downcast_mut() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
pub fn get_tool_by_component_id<'w>(&self, cmp: ComponentId, world: &'w World) -> Option<&'w dyn Reflect> {
let type_id = world.components().get_info(cmp)?.type_id()?;
let type_registry = world.get_resource::<AppTypeRegistry>()?.read();
type_registry.get(type_id)?.data::<ReflectResource>()?.reflect(world)
}
pub fn get_tool_by_component_id_mut<'w>(&self, cmp: ComponentId, world: &'w mut World) -> Option<Mut<'w, dyn Reflect>> {
let type_id = world.components().get_info(cmp)?.type_id()?;
let reflect_resource = {
let type_registry = world.get_resource::<AppTypeRegistry>()?.read();
type_registry.get(type_id)?.data::<ReflectResource>()?.clone()
};
reflect_resource.reflect_mut(world)
}
With this approach, we can return a dyn Reflect instead of dyn ModalDevTool while keeping component_id
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know for sure the implications of using ComponentId instead of TypeId, this can simplified if we don't need to get the type_id from components first.
rfcs/77-dev-tools-abstraction.md
Outdated
fn get_tool_by_name(&self, world: &World, name: &str) -> Option<&dyn ModalDevTool> { | ||
let component_id = self.lookup_tool_component_id(name)?; | ||
DevToolsRegistry::get_by_id(world, component_id) | ||
} | ||
|
||
/// Gets a mutable reference to the specified modal dev tool by name. | ||
fn get_tool_mut_by_name(&self, mut world: &mut World, component_id: ComponentId) -> Option<&mut dyn ModalDevTool> { | ||
let component_id = self.lookup_tool_component_id(name)?; | ||
DevToolsRegistry::get_mut_by_id(world, component_id) | ||
} | ||
|
||
/// Iterates over the list of registered modal dev tools. | ||
fn iter_tools(&self, world: &World) -> impl Iterator<Item = (ComponentId, &dyn ModalDevTool)> { | ||
self.modal_dev_tools.iter().map(|&id| (id, self.get(world, id))) | ||
} | ||
|
||
/// Mutably iterates over the list of registered modal dev tools. | ||
fn iter_tools_mut(&self, mut world: &mut World) -> impl Iterator<Item = (ComponentId, &mut dyn ModalDevTool)> { | ||
self.modal_dev_tools.iter_mut().map(|&id| (id, self.get(world, id))) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A cleanup is probably needed here.
We should probably have the methods match the impl above (see my comment above), only the name and return will change.
} | ||
``` | ||
|
||
Once the user has a `dyn ModalDevTool`, they can perform whatever operations they'd like: enabling and disabling it, reading metadata and so on. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here we're talking about dyn ModalDevTool, but that's not possible, so this section should probably be changed.
@@ -0,0 +1,826 @@ | |||
# Feature Name: `dev-tools-abstraction` | |||
|
|||
## Summary |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for putting this together. I now understand where you all are coming from (for context: my original comment pushing back against the "dev tool" concept).
I see the appeal of a common way to represent commands:
- Single searchable database of commands
- Common data model for command input
That being said, I think this is trying to be too opinionated, and focusing on "dev tool" as a specific thing might be getting in our way:
- Every "toolbox" will have a different way of interacting with these commands. Command serialization/deserialization is something the "toolbox" should be responsible for. We cannot just throw FromStr on the impl and let each dev tool trait impl choose what format to expect. We should rely on Reflect for this (with optional reflect-driven Serde support), similar to how we handle scene serialization. Leave things like the "command line format" to the "command line toolbox".
- There is no reasonable way to solve the "Dev tool UI overlap" problem in a generic system. Each "toolbox" will define a different context for displaying and rendering "dev tools". The rendering/UX of a "dev tool" is fully the responsibility of the toolbox, and most "interactive" functionality will be toolbox specific. A "dev-tool" that renders content inside of the Bevy Editor should define itself relative to the Bevy Editor's constraints (which are notably ... currently undefined). I do not think we should make solving this problem generically across toolboxes "future work". Instead it should be a non-goal.
I believe we should be fully focused on (1) a command-style abstraction and (2) queryable/categorized settings configured by commands. We should try very hard to build on what already exists when possible, and build reusable pieces when it isn't.
- Reflect (with optional reflect-driven Serde) for all serialization
- Use
Command
for "applying a command to a World" (I acknowledge that this is already part of the proposal) - Rely on existing registry tooling instead of building a new one. Some options:
a. TypeRegistry enumeration of types registered with a specific reflected trait (ex:DevCommand
)
b. Make this Component / Query driven instead of Resource driven (ex: use a common DevCommand component) - Use Commands for configuring settings (including enabling / disabling features)
I think by shifting focus to these things, we sidestep the "dev tool is a nebulous concept" problem and focus on building the specific functionality we need. This also leaves the door open to other categories of commands / configuration.
For example (not a final design, just illustrating where my head is at):
// Reflect registry-driven commands
#[derive(Reflect, DevCommand, Debug)]
#[reflect(DevCommand)]
struct SetGold {
amount: u64,
}
impl Command for SetGold {
fn apply(self, world: &mut World){
let mut current_gold = world.resource_mut::<Gold>();
current_gold.0 = self.amount;
}
}
app.register_type::<SetGold>()
// later, use the type registry to invoke / enumerate / serialize this command
type_registry.iter_with_data::<ReflectDevCommand>()
// Commands for enabling / disabling the settings of a dev tool
#[derive(Resource, Reflect, DevToolSettings, Debug)]
#[reflect(DevToolSettings)]
struct DevFlyCamera {
enabled: bool,
/// How fast the camera travels forwards, backwards, left, right, up and down, in world units.
movement_speed: Option<f32>,
/// How fast the camera turns, in radians per second.
turn_speed: Option<f32>,
}
impl Toggleable for DevFlyCamera {
fn is_enabled(&self) -> bool {
self.enabled
}
fn set_enabled(&mut self, enabled: bool) {
self.enabled = enabled;
}
}
app.register_type::<DevFlyCamera>()
// Makes these Dev commands available (these could be put behind a helper function):
app.register_type::<Enable<DevFlyCamera>>()
app.register_type::<Disable<DevFlyCamera>>()
app.register_type::<Toggle<DevFlyCamera>>()
// this DevCommand writes a new value of DevFlyCamera on top of the existing value
app.register_type::<Configure<DevFlyCamera>>()
// later (ex: if you want to enumerate DevToolSettings types in a tool)
type_registry.iter_with_data::<DevToolSettings>()
/// Reusable Enable dev command for toggleable things
#[derive(DevCommand)]
pub struct Enable<T: Toggleable>();
impl<T: Toggleable> Command for Enable<T> {
// TODO
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can absolutely work with that! I'll do a clean up pass next week, reframing and simplifying to capture these ideas.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome!
Also note that type_registry.iter_with_data
does not currently exist. You can do something similar with .iter()
and filtering the results, but we could consider providing that ourselves (and maybe building an index to accelerate, although building that index might be too detrimental to startup times and I'm not sure we need it).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Me and @alice-i-cecile noticed during the re-designing of the abstraction to use TypeRegistry
a few issues:
TypeRegistry
was not built for this. TheTypeRegistry
was designed to store the registration of types, not tools or metadata of them. If we would use this, we are potentially allowing usage ofTypeRegistry
for storing anything.- Using a dedicated resource (e.g. DevToolsRegistry) is a standard design pattern in the engine. For example, let's take a look at
DiagnosticStore
resource that stores diagnostic information. Similarly, aDevToolsRegistry
could be used to store and manage tools and commands, while hiding a lot of magic that could be intimidating to users. - In the
DevToolsRegistry
we would store metadata about a tool or a command, which is accessible by providing its name. While this still could be possible withTypeRegistry
it would be a misuse of the registry, and would create additional overhead. TypeRegistry
limits our flexibility and extensibility of the abstraction in the future.
The use of Reflect
instead of FromStr
is great though!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TypeRegistry was not built for this. The TypeRegistry was designed to store the registration of types, not tools or metadata of them. If we would use this, we are potentially allowing usage of TypeRegistry for storing anything.
Tools and metadata are represented as types, and whether a type is a tool (or command) should be encoded as a Rust Trait. From my perspective these line up perfectly with the TypeRegistry's functionality and purpose.
In the DevToolsRegistry we would store metadata about a tool or a command, which is accessible by providing its name. While this still could be possible with TypeRegistry it would be a misuse of the registry, and would create additional overhead.
From my perspective the "dev tool name" should be the type name (short name by default with disambiguation possible via the full type path). Again this feels like a great fit (and the right one). I consider the overhead of querying the reflected trait registration for a type to be acceptable for this use case (and just barely more expensive than the HashMap<String, Data>
that a custom registry would use).
I think the biggest missing piece is how DevCommands correlate to dev tools / DevToolSettings (which we could just shorten to DevTool if we want). For example: the DevTool trait could have a function that provides this correlation.
TypeRegistry limits our flexibility and extensibility of the abstraction in the future.
This was intentional. My goal here was to put bounds on this abstraction in a way that prevents it from becoming a new unbounded "framework". I'd like to reconcile the requirements of the "dev tool" space with the rest of Bevy.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree here: I don't really see a use for Metadata anymore if we can use the type registry for many things
For example, let's say we have the Toggleable
trait, and the ReflectToggleable
type and the 3 new commands: Toggle<T>
, Enable<T>
and Disable<T>
, and the CLI tool as an example of functionality here.
We can have the CLI tool be just a HashMap<ToolName, TypeId>
(tool name would probably be of type String
here). We can just have the CLI tool use the name here and lookup in type registry for the ReflectToggleable
data. For inserting new tools with CLI, we would use the same approach, but with ReflectDeserialize
instead.
And so, this would lessen the uses for metadata. Note that with this approach, the CLI tool for example would have to use serde with deserialize, as we don't really have a way to store the given function to deserialize the tool without some metadata. But with the main commands this RFC proposes: Enable, disable and toggle, this will go alright without metadata.
At the same time, I don't see a way to go without a ToolBox
resource that will store at least the TypeId
and the Tool / command name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
At the same time, I don't see a way to go without a ToolBox resource that will store at least the TypeIdand the Tool / command name.
That's why I believe we should have a registry. If we would ask every toolbox creator to "make your own storage for tools", we would end up with multiple resources that have the same purpose.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's why I believe we should have a registry
Yeah, I believe we at can have a global resource to store what tools the app currently have, and rely more on the app registry for parse and creation of commands
Co-authored-by: Alice Cecile <[email protected]>
Dev tools type registry
I've been experimenting with abstraction for the dev tools for some time now and:
From my understanding Even if we made // New method on `TypeRegistry`
pub fn iter_with_data<T: TypeData>(&self) -> impl Iterator<Item = (&TypeRegistration, &T)> {
self.registrations.values().filter_map(|item| {
let type_data = self.get_type_data::<T>(item.type_id());
type_data.map(|data| (item, data))
})
}
// Possible implementation of printing name of every tool
for dev_tool in type_registry.iter_with_data::<ReflectDevTool>() {
// This is the problem - we need to specify a tool here if we want to get a `&dyn DevTool`
let reflected = Box::new(DevFlyCamera::default());
if let Some(tool) = dev_tool.1.get(&*reflected) {
println!("{:?}", tool.name());
}
}
We would need to drop
You don't want dev tools to become a new unbounded framework - but actually, our abstraction can restrict what fits in it. With a type registry, you can always register a type, but whether you would be able to register a tool in our registry is going to be decided by the API of the registry. Would it be opinionated? - yes, but I think it should. |
I'm not seeing any problems here. Your first two hangups have solutions: trait DevTool: Resource {
fn name(&self) -> &'static str;
}
#[derive(Clone)]
struct ReflectDevTool {
get_reflect: fn(&World) -> Option<&dyn Reflect>,
get: fn(&World) -> Option<&dyn DevTool>,
from_reflect: fn(&dyn Reflect) -> Option<Box<dyn DevTool>>,
}
impl ReflectDevTool {
fn get_reflect<'a>(&self, world: &'a World) -> Option<&'a dyn Reflect> {
(self.get_reflect)(world)
}
fn get<'a>(&self, world: &'a World) -> Option<&'a dyn DevTool> {
(self.get)(world)
}
fn from_reflect(&self, reflect: &dyn Reflect) -> Option<Box<dyn DevTool>> {
(self.from_reflect)(reflect)
}
}
impl<D: DevTool + Reflect + FromReflect> FromType<D> for ReflectDevTool {
fn from_type() -> Self {
Self {
get_reflect: |world| world.get_resource::<D>().map(|d| d as &dyn Reflect),
get: |world| world.get_resource::<D>().map(|d| d as &dyn DevTool),
from_reflect: |reflect| {
D::from_reflect(reflect).map(|d| {
let d: Box<dyn DevTool> = Box::new(d);
d
})
},
}
}
}
#[derive(Resource)]
pub struct DevFlyCamera {}
fn iter_dev_tools(world: &mut World, type_registry: &TypeRegistry) {
// Possible implementation of printing name of every tool
for (registration, dev_tool) in type_registry.iter_with_data::<ReflectDevTool>() {
if let Some(tool) = dev_tool.get(world) {
println!("{:?}", tool.name());
}
let reflect = DynamicStruct::default();
let d = dev_tool.from_reflect(&reflect);
}
} |
As I'm reading this spec, it sounds exactly what an editor would be used for? Like a temporary solution that alleviates the lack of editor conventions, e.g. command panel.
Also hi everyone, I normally just lurk around while gathering courage to contribute 😅 |
Co-authored with @matiqo15 and @rewin123.
Read it RENDERED here.