Skip to content
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 #13582

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft

Conversation

rewin123
Copy link
Contributor

@rewin123 rewin123 commented May 30, 2024

Objective

Implementation of bevyengine/rfcs#77 rfc

This rfc introduces the traits and concepts of DevTool and DevCommand as tools and commands that speed up the development process and are not normally built into the final build of a game. RFC target is create unified input for DevCommand creation and unified controls to enable/disable/configure DevTools. I have tried to implement this RFC in order to improve it. The implementation does not match the RFC text completely, as my solution takes into account both the RFC itself and comments to it that have not yet been corrected in the text.

PR and RFC aim to achieve three primary objectives and one additional goal:

  • Unified Method for Spawning DevCommands: Establish a consistent approach for spawning DevCommands and retrieving information about them.
  • Standardized Management of DevTools: Create a unified method for enabling, disabling, listing, and obtaining information about DevTools.
  • Maximize Reuse of Existing Functionality: Leverage existing functionality wherever possible, such as using the robust TypeRegistry instead of creating a separate DevCommandRegistry.

Additional Goal:

  • Lay the groundwork for a Quake-like console for Bevy or a Command Palette similar to VSCode.

Solution

  • DevCommand trait turned out to be as simple as possible. However, reflection gives us an incredible number of possibilities, which will be described below
pub trait DevCommand : Command + FromReflect + Reflect + Typed {
    /// The metadata of the dev command
    fn metadata() -> DevCommandMetadata {
        DevCommandMetadata {
            self_to_commands: Arc::new(|reflected_self, commands| {
                let Some(typed_self) = <Self as FromReflect>::from_reflect(reflected_self) else {
                    error!("Can not construct self from reflect");
                    return;
                };
                commands.add(typed_self);
            })
        }
    }
}

/// DevCommand implementation example 
#[derive(Reflect, Default, DevCommand)]
#[reflect(DevCommand, Default)]
pub struct SetGold {
    pub gold: usize,
}
impl Command for SetGold {
    fn apply(self, world: &mut World) {
        world.insert_resource(Gold(self.gold));
    }
}
  • CLI Toolbox: Implemented TypedCliDeserializer and CliDeserializer for deserializing CLI inputs into Bevy commands, enabling a flexible and typed deserialization process. And I created CLIToolbox, which allow spawn commands from console. Look at dev_tools/dev_cli.rs example!
  • DevTool trait is simplest! It firstly focused to auto register default commands for it (like enable, disable etc)
pub trait DevTool : Reflect + FromReflect + GetTypeRegistration  {}
  • Added app.register_toggable_dev_tool<T: DevTool>() which will register in type registry DevTool and default commands for it
  • Added Toggable trait
  • Added default commands for DevTools Enable, Disable, SetField (allow to set one field in resource dev tool), SetTool (allow to set whole resource dev tool)
  • Added dev_cli example to demonstrate CLIToolbox and DevCommand, DevTool abstractions
    For starting this example use cargo run --example dev_cli --features="bevy_dev_tools"
    To try this demo you must print into your console while app is running
    Try this:
  1. disable fpsoverlay -- will hide fps overlay
  2. enable fpsoverlay -- will show fps overlay
  3. setfield fpsoverlay text_config.font_size 16 -- will change font size in fps overlay
  4. printcommands -- will list all dev commands
  5. setgold 100 -- will set gold amount
  6. printgold -- will print gold amount
  7. disable showgold -- will hide gold overlay (right top corner on screen)

Testing

I created 21 tests for CLIDeserialization. Since this is a draft designed to improve RFC and not merge, that's enough for now. If this PR reaches a finished state, everything possible will be covered in tests.


Migration Guide

No migration needed

Copy link
Contributor

Welcome, new contributor!

Please make sure you've read our contributing guide and we look forward to reviewing your pull request shortly ✨

Copy link
Contributor

The generated examples/README.md is out of sync with the example metadata in Cargo.toml or the example readme template. Please run cargo run -p build-templated-pages -- update examples to update it, and commit the file change.

@alice-i-cecile alice-i-cecile added S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged X-Controversial There is active debate or serious implications around merging this PR labels May 31, 2024
@alice-i-cecile alice-i-cecile added A-Dev-Tools Tools used to debug Bevy applications. C-Usability A targeted quality-of-life change that makes Bevy easier to use C-Feature A new feature, making something new possible and removed C-Usability A targeted quality-of-life change that makes Bevy easier to use labels May 31, 2024
@alice-i-cecile alice-i-cecile added the D-Complex Quite challenging from either a design or technical perspective. Ask for help! label May 31, 2024
Copy link
Contributor

@MiniaczQ MiniaczQ left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very interesting topic, took a very quick look at the PR!

I'm not super sold on the reflection-driver approach, but this seems to be the decision from RFC discussion. If you were to sum it up, why is Reflection preferred here?

Comment on lines +1 to +52
use crate::dev_command::*;
use bevy_ecs::world::{Command, World};
use bevy_reflect::std_traits::ReflectDefault;
use bevy_reflect::FromReflect;
use bevy_reflect::Reflect;
use bevy_reflect::TypePath;

/// Trait that represents a toggable dev tool
pub trait Toggable {
/// Enables the dev tool in the given `world`.
fn enable(world: &mut World);
/// Disables the dev tool in the given `world`.
fn disable(world: &mut World);
/// Checks if the dev tool is enabled in the given `world`.
fn is_enabled(world: &World) -> bool;
}

/// Command to enable a `Toggable` component or system.
#[derive(Reflect, Default)]
#[reflect(DevCommand, Default)]
pub struct Enable<T: Toggable + FromReflect + Send + Sync + 'static + Default> {
/// PhantomData to hold the type `T`.
#[reflect(ignore)]
_phantom: std::marker::PhantomData<T>,
}

impl<T: Toggable + Send + Sync + 'static + TypePath + FromReflect + Default> DevCommand for Enable<T> {}

impl<T: Toggable + FromReflect + Send + Sync + 'static + Default> Command for Enable<T> {
/// Applies the enable command, enabling the `Toggable` dev tool in the `world`.
fn apply(self, world: &mut World) {
T::enable(world);
}
}

/// Command to disable a `Toggable` dev tool.
#[derive(Reflect, Default)]
#[reflect(DevCommand, Default)]
pub struct Disable<T: Toggable + FromReflect + Send + Sync + 'static + Default> {
/// PhantomData to hold the type `T`.
#[reflect(ignore)]
_phantom: std::marker::PhantomData<T>,
}

impl<T: Toggable + Send + Sync + 'static + TypePath + FromReflect + Default> DevCommand for Disable<T> {}

impl<T: Toggable + FromReflect + Send + Sync + 'static + Default> Command for Disable<T> {
/// Applies the disable command, disabling the `Toggable` dev tool in the `world`.
fn apply(self, world: &mut World) {
T::disable(world);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is my favorite part, intermediate traits can be used to define commands 🎉

@rewin123
Copy link
Contributor Author

rewin123 commented Jun 3, 2024

Very interesting topic, took a very quick look at the PR!

I'm not super sold on the reflection-driver approach, but this seems to be the decision from RFC discussion. If you were to sum it up, why is Reflection preferred here?

Thank you for reviewing the PR!

The PR and RFC aim to achieve three primary objectives and one additional goal:

  1. Unified Method for Spawning DevCommands: Establish a consistent approach for spawning DevCommands and retrieving information about them.
  2. Standardized Management of DevTools: Create a unified method for enabling, disabling, listing, and obtaining information about DevTools.
  3. Maximize Reuse of Existing Functionality: Leverage existing functionality wherever possible, such as using the robust TypeRegistry instead of creating a separate DevCommandRegistry.

Additional Goal:

  1. Lay the groundwork for a Quake-like console for Bevy or a Command Palette similar to VSCode.

Reflection was preferred for several reasons:

  1. Type Information: TypeInfo allows us to access detailed information about the fields of a DevCommand or DevTool structure, including field types and trait implementations like Default. This is crucial for dynamically building and managing these commands and tools.
  2. Runtime Widget Construction: Reflection enables the construction of runtime widgets for spawning DevCommands or modifying DevTool fields, similar to what bevy-inspector-egui does. This dynamic capability is essential for a flexible development environment.
  3. Documentation Access: With the .docs() method (when the documentation feature is enabled), we can retrieve comprehensive documentation for structures and their fields. This supports the creation of tooltips in the UI and detailed --help instructions for each DevCommand in the console.
  4. Unified Deserialization: ReflectDeserialize provides a unified mechanism for constructing Reflect instances from any deserializer in an untyped manner. This is particularly advantageous at runtime, as it allows modification or construction of structures without requiring knowledge of their types (and skip ussing typed code and register typed deserialization functions). This flexibility is often not possible with other deserialization approaches.

For me these benefits make Reflection an best choice for achieving the goals. And in this PR I implemented dev console mode for Bevy to test that selected goals are achived.

@MiniaczQ
Copy link
Contributor

@rewin123 What's the status? Is this still meant to be a draft?

@rewin123
Copy link
Contributor Author

rewin123 commented Sep 2, 2024

@rewin123 What's the status? Is this still meant to be a draft?

I apologize for the long delay in replying, been on vacation.
PR in draft state, as the underlying RFC is still in draft state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Dev-Tools Tools used to debug Bevy applications. C-Feature A new feature, making something new possible D-Complex Quite challenging from either a design or technical perspective. Ask for help! S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged X-Controversial There is active debate or serious implications around merging this PR
Projects
Status: Needs Working Group
Development

Successfully merging this pull request may close these issues.

4 participants