-
-
Notifications
You must be signed in to change notification settings - Fork 3.6k
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
Plugins should be more customizable and work with states #2160
Comments
A bit of context about AppBuilder and App
The Bevy plugins (first or third party), implementing the The problemThe property of a plugin to be added with no configurability (at least by default), can have its pros and cons. The major concern that comes to my mind is relevance of builder method order. This comes from the fact that some methods overlap and end up overwriting previous data or other kinds of stuff. For example, you can have two plugins, With
With
A possible solutionSolution 5 seems the best decision when compared against the others. It will require the most work, but it will give us the most benefits in the long run. About system reordering, it's ok to give the user the chance of moving them around, but there should be constraints. I'll explain. Once, I had to fix an obscure bug on Therefore I think that it's ok to give the user the ability to reorder system sets within a stage, but they should not be able to move them to other stages, since stages are kind of hard boundaries. Also, stage reordering should be limited to only across plugins. If we take the previous example, you shouldn't be able to have a stage configuration like the following:
since that would break the The solution that I see is one where a Plugin is made of smaller building blocks that the client can (not must) move around (with some well thought limitations) to their needs. |
For others who may encounter this before we fix it properly: I hand-rolled a solution for allowing the end user to control which state if any a plugin's system runs. You can examine it here: it's reasonably nice, but must be added to each plugin ahead of time and adds significant boilerplate. |
Just want to add from the other issue (#4362) that I think you should also analyze in what circumstances people what to change the plugin behaviour. In my case I just wanted to disable the plugin under some conditions and something like: app.add_plugin_with_criterium(PluginA, some_criterium_system) or inheriting System Set app.add_plugin(PluginA.with_run_criteria(some_criterium_system)) Would solve the problem. I'm a new Bevy user and I found it a little bit confusing that plugins and System Sets have different functionalities. In my opinion when someone writes a plugin that is just a single System Set then it shouldn't be in fact a Plugin but a System Set. This makes it possible to use every functionality of a System Set in this case. So a solution in this case would be not to allow creating objects which behave in the same way but are different type. I think it makes me vote for 4th solution. Maybe creating |
I really like that I can add a plugin and mostly don't have to care how it is implemented inside. Just imagine a plugin which is one system set today, but suddenly needs more complex logic after an update - which means that I have to switch out the system set and make it a plugin again. I really love the idea of focusing things around APIs. Plugin handling should have a rich, enjoyable API from Bevy's side, so that common things can be done with "standard" Bevy. Maybe adding run criteria for a plugin is one of those things. However, I would also love to have plugins which expose configuration themselves. It is hard to cover every plugin use case from Bevy's side after all. So, plugins should expose a way to be configured. I am not talking about code, necessarily. Plugins may be forced to implement traits to get some sort of uniformity. What I would expect, though, is a best practice guide on how to create a plugin for Bevy, which covers how to expose a configuration object, how to access custom plugin APIs, etc. Imagine someone wrote a 2D RPG plugin, which already implements a lot of common logic for a 2D game - and works like a sandbox if just slapped into Bevy with a single line of code (think RPG Maker sandbox). Game Devs could use that plugin as a base to build their own game, but would need to configure a ton of things and call APIs to interact with the sandbox, which Bevy would never be able to foresee. No game dev wants to care about how the plugin works under the hood, and even after many version updates and internal changes, it should ideally have the same API and usage as before. To sum it up, I'd propose another solution to the above problem, which would mainly be: Add common QoL API around plugin handling and write a good guide on how to create plugins, which covers passing labels into the plugin, calling functionality of plugins inside systems, exposing components as API, which can be queried by users, etc. |
I really like this idea and bevy providing a trait like ConfigurablePlugin might be best way to allow some uniformity. At the same time if a plugin really wants something different, that's possible even now. Or just the Plugin trait could expose some extra optional methods that take some configuration primitives and applies it during build. |
I'd like to split fn build_minimal(&self, app: &mut App);
fn build_default(&self, app: &mut App);
This encourages plugin authors (including Bevy itself!) to expose both a "works out of the box" and a "minimal set of constraints required" version of their plugin, greatly improving end-user customizability without breaking our "constraints cannot be removed once added" privacy guarantee. |
Recording my thoughts from Discord here: I think that this is probably a clearer and more powerful approach, if a bit heavier. I want to keep an eye on user stories here, to try and more exactly figure out the needs of both plugin authors or users. |
For some simple plugins, the extent of the configuration needed might just be passing a State into it. Example code: // 3rd party plugin
struct TestPlugin(State<dyn States>)
impl Plugin for TestPlugin{
fn build(&self, app: &mut App) {
app
.add_systems(OnEnter(self.0), start_asset_load)
.add_systems(Update, check_asset_load.run_if(in_state(self.0)));
}
}
// Consumer might then do this:
#[derive(States, PartialEq, Eq, Debug, Default, Hash, Copy, Clone)]
pub enum GameState {
Startup,
MainMenu,
Loading,
InGame,
// etc...
}
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
TestPlugin(GameState::Startup),
))
.run();
} I've been trying to get the above working, but I'm getting this error: error[E0038]: the trait `bevy_ecs::schedule::States` cannot be made into an object
--> src/lib.rs:22:34
|
22 | struct TestPlugin(pub State<dyn States>);
| ^^^^^^^^^^ `bevy_ecs::schedule::States` cannot be made into an object
|
39 | pub trait States: 'static + Send + Sync + Clone + PartialEq + Eq + Hash + Debug + Default {
| ^^^^^^^^^ the trait cannot be made into an object because it uses `Self` as a type parameter Is there a way to work around this, or is this approach incompatible with the current State design? |
Tried a few more permutations, this seems to work: pub struct TestPlugin<S: States + Copy>(pub S);
impl<S: States + Copy> Plugin for TestPlugin<S> {
fn build(&self, app: &mut App) {
app
.add_systems(OnEnter(self.0), start_asset_load)
.add_systems(Update, check_asset_load.run_if(in_state(self.0)));
}
} Very little boilerplate needed! |
Yep, this is a great approach for plugin authors to provide these days. It would be nice if there was a standard though, so then users know what to expect. This is particularly painful for update vs fixed-update for physics and gameplay plugins. |
Personally, I'd be happy if there is a way to define plugins under a state so that the systems added there are only run when the app is in that particular state. My intuition, overall, is that it makes sense to merge the concept of state and plugins. Somehow get rid of the concept of plugins and have everything be a state. |
I want to catch errors from Plugin, becouse Plugin can broke my game (I want to implement mods to my game as Plugins. And disable if mod doesn't work) |
I also thought about this idea and I really liked it. So the only real purpose of I think the And could we maybe rename the issue to something like "Missing customisability of plugins", so that it is more clear, what the core problem is about? So I thought about some code examples and came up with the following (I must admit, that I've used fn main() {
let test_plugin = (system_a, system_b.after(system_a).run_if(run_criteria))
App::new()
.add_systems(Update, test_plugin)
.run();
}
fn system_a() {
// ...
}
fn system_b() {
// ...
} So basically you have the systems predefined, as in the #[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)]
struct Gameplay;
fn main() {
let network_plugin = (ping_server, send_packets.after(ping_server).run_if(run_if_internet_connection_available))
app.configure_sets(Update,
Gameplay
.run_if(run_if_player_alive)
);
App::new()
.add_systems(Update, network_plugin.in_set(Gameplay))
.run();
}
fn ping_server() {
// ...
}
fn send_packets() {
// ...
} The problem here is, that it is not always clear how the // plugin.rs
plugin!(test_plugin, (system_a, system_b.after(system_a).run_if(run_criteria)))
fn system_a() {
// ...
}
fn system_b() {
// ...
}
// main.rs
fn main() {
App::new()
.add_systems(Update, test_plugin.run_if(run_if_test_condition))
.run();
} I don't know how easy it is, to implement this, but I think it would be a breaking change, especially for the |
Just copy/paste from #14412 for better visibility here:
To expand on this a bit, I recently settled on a pattern where each module has a |
Bevy version
0.5
Problem description
The State problem here is side effect of the fact that we cannot add run criteria to systems added in plugins.
Potential solutions
Solution 1: Everything is
pub
Rather than trying to solve this directly, the Bevy community forces plugin authors to make all of their systems (and supporting types)
pub
.Users then reconstitute the plugins from scratch by adding the systems etc. on their own terms in their own code to get the interoperability they need.
This is bad because it creates cluttered, confusing APIs and exposes details that should be internal carelessly. This then causes every non-trivial plugin crate version to have breaking changes.
It is also very heavy on boilerplate.
Solution 2: Just fork it
As Solution 1, but users fork all plugins (and the engine) and forcibly make the things they need
pub
/ make the changes they need.Like Solution 1, but shifts the maintenance burden and headaches to the end users.
Very bad for user experience and ecosystem fragmentation.
Solution 3: Post-hoc configurability
Allow users to set the stages / states / run criteria of all systems in a plugin at once.
This doesn't involve any changes to our logic, and patches the most pressing issues.
However, it starts to break down pretty badly if you need to have plugins with systems that cannot coexist in the same stage or state: startup systems and those that must be separated by commands are particularly common and painful here.
Solution 4: Plugins contain only one system set
As solution 3, but each plugin can only have a single system set.
Solves the problems above, but makes their common use more onerous.
Solution 5: App as a structured object
Completely rework how
App
works by replacing the "mutate the entire AppBuilder" approach with one where theAppBuilder
(andApp
) have carefully structured fields that you can mutate regardless of the visibility of the field.To solve this particular issue:
AppBuilder
.App
is not constructed until.run
is called, at which point it is constituted in a deterministic order that is independent of insertion order.Solves #1255, which would be great for clarity and ease of refactoring. This would be a serious endeavor though, and might limit users ability to throw new ad-hoc behavior onto the
AppBuilder
(not that I've ever seen anyone try?).This is a sensible compromise on the level of granularity and visibility that plugins expose, grouping their functionality into configurable units.
Conclusions
Solution 1 is the de-facto standard right now for the Bevy engine, as virtually all of our systems are pub. Solution 2 is what will happen if we do nothing (or advanced users instead develop severe NIH syndrome). Both of these are pretty terrible.
Thus, we need a thoughtful approach to configuration. Solutions 3 and 4 are somewhat better than what we have now as they allow more than 0 configurability, but are almost certain to fall short in real use.
I am in favor of Solution 5, but this needs some serious thought. If you want to make an RFC, go for it! Otherwise, I'll probably get around to it after 0.6 launches due to other priorities.
Of course, all of these problems get dramatically worse once we have large games and an editor, and want to examine and manipulate systems visually.
The text was updated successfully, but these errors were encountered: