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

Topics #65

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

Topics #65

wants to merge 7 commits into from

Conversation

mxgrey
Copy link

@mxgrey mxgrey commented Nov 1, 2022

RENDERED

This RFC propose a reactive pub-sub framework for Bevy, organized by topics.

I anticipate this RFC will need a considerable amount of feedback and iteration. I'm opening this to help facilitate technical discussion and not to declare that I have a firm solution for this feature.

If this concept is of significant interest, should we go ahead and reserve the bevy_topics crate?

Signed-off-by: Michael X. Grey <grey@openrobotics.org>
Signed-off-by: Michael X. Grey <grey@openrobotics.org>
Signed-off-by: Michael X. Grey <grey@openrobotics.org>
Signed-off-by: Michael X. Grey <grey@openrobotics.org>
Signed-off-by: Michael X. Grey <grey@openrobotics.org>
Signed-off-by: Michael X. Grey <grey@openrobotics.org>
Signed-off-by: Michael X. Grey <grey@openrobotics.org>
@mxgrey mxgrey marked this pull request as draft November 1, 2022 16:53
@mxgrey mxgrey mentioned this pull request Nov 1, 2022
@james7132
Copy link
Member

Haven't had the time to fully read through the RFC, but a quick skim through raises the following questions:

  • Why can't this be an ecosystem crate? Why must this be core to the engine itself?
  • What does this offer that Events do not? Besides the fact that events are global resources, events are already MPMC by design. What benefits and tradeoffs are we making by architecting it this way over existing events?
  • The finer implementation details here are important and are largely hand-waved away. If we're looking at topics as distributed message queues in the traditional sense, they must be guarded from aliasing access. This will require at the very minimum guarding atomic accesses which can be detrimental to performance considering bevy's ECS is currently requires zero atomic accesses on a per-entity basis.
  • When in the schedule do subscriber's handlers run? Immediately on publish? In a different stage? During a pre-created system? If so, how do we schedule other systems relative to it?
  • Why take this approach to networking over established game networking flows? Historically, games have been bottlenecked more by latency than distributed consistency. Simply using a distributed message broker (i.e. MQTT) may counter these goals.

Copy link
Member

@alice-i-cecile alice-i-cecile left a comment

Choose a reason for hiding this comment

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

Seconding James's comments. Seems interesting, but definitely feels like something that can and should be explored extensively in a 3rd party crate.

@mxgrey
Copy link
Author

mxgrey commented Nov 2, 2022

Thanks for the incredibly quick feedback!

Why can't this be an ecosystem crate? Why must this be core to the engine itself?

Totally agree that this can/should be an ecosystem crate. I opened the PR here to seek inputs from anyone else who may be interested in this idea and to avoid duplicating effort in case someone else is already working on this concept. If that's an inappropriate use of RFCs then feel free to close this PR and I'll seek out a new home for the document.

All the same applies to #66 .

What does this offer that Events do not?

I think there are several key benefits, although the value of these benefits is situational, so I expect topics to complement Events and not replace them:

  • Messages are scoped to publishers and topics, so subscribers can be set up to react to messages differently based on their source, without needing to embed extra information in the message data
  • Subscription callback systems will only be run when one or more new subscribed messages are available instead of being run on every cycle the way an Event reaction system would be
    • Admittedly it's likely possible to implement a subscription+callback system for Events to achieve this same benefit

The finer implementation details here are important and are largely hand-waved away.

Very much acknowledged at the top of Implementation Strategy 😅

If we're looking at topics as distributed message queues in the traditional sense, they must be guarded from aliasing access.

Acknowledged in the parallel publishing section.

When in the schedule do subscriber's handlers run?

Acknowledged in the Callback driven and Callback timing sections.

Why take this approach to networking over established game networking flows?

Truthfully I'm interested in using bevy in robotics, simulation, and IoT contexts rather than for conventional video game development, so I won't have a good answer for this since I'm not familiar with how client-server communication is done for conventional video games. But for the context that I'm coming from, distributed pub-sub architecture is a must-have, so making that accessible in bevy would open bevy up to whole new industries outside of video game development. Rising tide lifts all boats and whatnot.

But really, why not Events??

As I think more about it, I wonder if it would be possible to piggyback directly on the existing Events implementation to make scoped pub-sub work. That may also help to solve the concurrency concerns.

Maybe instead of Publisher<T> being a component it could be a SystemParam that wraps around EventWriter<Message<T>> where:

pub struct Message<T> {
  for_topic: Topic,
  data: T,
}

Then each time you use Publisher<T> you just specify the entity or topic that's being published for. Entity could be convertible into a valid Topic object.

Each message type T would need to be registered so that an Events<Message<T>> can be registered for it so that Publisher<T> can be queried. Registering the message type T would make it much easier to generate systems that can query for Subscription<T> components that contain callbacks which need to be triggered. Then since Bevy can already recognize when an event was not registered correctly, it will transitively recognize when a message type was not registered correctly.

Does that seem like a more feasible approach?

Scheduling callbacks still presents some challenges, especially since this feature would need to support stageless systems and I'm not familiar with those yet. I'll need to get up to speed on stageless before I can reason about that.

@alice-i-cecile
Copy link
Member

Maybe instead of Publisher being a component it could be a SystemParam that wraps around EventWriter<Message>

Unsure if this will be easier than making it a custom SystemParam :)

Scheduling callbacks still presents some challenges, especially since this feature would need to support stageless systems and I'm not familiar with those yet. I'll need to get up to speed on stageless before I can reason about that.

Scheduling callbacks should be pretty straightforward post-Stageless: they'd just be emitted as commands, which can be evaluated whenever the user inserts a system that processes them ( a "command flush point"). You may end up wanting some performance improvements here; commands could be faster and the callbacks will be evaluated completely sequentially, but eh, for tiny callbacks that may actually be faster.

@mxgrey
Copy link
Author

mxgrey commented Nov 2, 2022

making it a custom SystemParam :)

That's what I was thinking! 😁

Something like:

#[derive(SystemParam)]
pub struct Publisher<'w, 's, T> {
    writer: EventWriter<'w, 's, Message<T>>,
}

impl<'w, 's, T> Publisher<'w, 's, T> {
    pub fn publish(&mut self, to_topic: Topic, msg: T) {
        self.writer.send(Message::new(to_topic, msg));
    }
}

Then there'd be a Subscriber custom system param that can generate Subscription<T>:

#[derive(SystemParam)]
pub struct Subscriber<'w, 's> {
    commands: Commands<'w, 's>,
}

impl<'w, 's> Subscriber<'w, 's> {
    pub fn subscribe<T, F>(&mut self, to_topic: Topic, callback: F) -> Result<Entity> {
        // TODO: Do something to make sure that T is registered as a message type
        // and return Err if it is not.
        Ok(self.commands.spawn().insert(Subscription::new(to_topic, callback)).id())
    }
}

Then there'd be a system inserted for each T message type to read Message<T> events and query for Subscription<T> components that are storing callbacks. When a message is published to a topic that matches the topic a subscription is listening to, the callback gets triggered, ideally with a way for the subscription to specify exactly when to trigger it.

I'm still hand waiving a lot when it comes to callback scheduling, but I think the approach to connecting publishers and subscriptions is feeling a lot more concrete. It seems like the Domain concept from the proposal might be thrown out, or perhaps just incorporated into the Topic concept.

@alice-i-cecile
Copy link
Member

You may also be interested in #17 and #32, which tackle related problem spaces :)

@huang12zheng
Copy link

huang12zheng commented May 22, 2023

I'm new to rust and I'm just saying what I think.

1. ancestor can create a Container::;
2. What can posterity do?
let mut publisher = Publisher::< Mission> ::new();
let mut subscriber = Subscriber::< Mission> ::new();

1. If ancestor is no Container::, posterity can be shared with everyone or subscribed to by everyone
2. If ancestor has a Container::, all the posterity would only see it, they would ignore higher Container::
> root can have a Container, and None won't happen

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants