#bevy_gearbox

1113 messages · Page 2 of 2 (latest)

crystal marten
#
bsn! {
    #AppState
    AppStateRoot
    StateMachineId("app_state".into())
    InitialState(#MainMenu)
    Transitions [
        Target(#MainMenu)
        EventEdge::<AppEvent>::from("ReturnToMain")
        ---
        Target(#Settings)
        EventEdge::<AppEvent>::from("SelectSettings")
        ---
        Target(#Quit)
        EventEdge::<AppEvent>::from("SelectQuit")
    ]
    Substates [
        #MainMenu
        AppState::MainMenu
        Transitions [
            Target(#New)
            EventEdge::<AppEvent>::from("SelectNew")
            ---
            Target(#Load)
            EventEdge::<AppEvent>::from("SelectLoad")
        ]
        ---
        #New
        AppState::New
        InitialState(#Appearance)
        Transitions [
            Target(#InGame)
            EventEdge::<TryLoadCharacter>::from("TryLoadCharacter")
        ]
        Substates [
            #Appearance
            NewState::Appearance
            Transitions [
                Target(#Parameters)
                EventEdge::<AppEvent>::from("SelectNext")
            ]
            ---
            #Parameters
            NewState::Parameters
            Transitions [
                Target(#Name)
                EventEdge::<AppEvent>::from("SelectNext")
                ---
                Target(#Parameters)
                EventEdge::<AppEvent>::from("SelectBack")
            ]
            ---
            #Name
            NewState::Name
            Transitions [
                Target(#Parameters)
                EventEdge::<AppEvent>::from("SelectBack")
            ]
        ]
        ---
        #Load
        AppState::Load
        Transitions [
            Target(#InGame)
            EventEdge::<TryLoadCharacter>::from("TryLoadCharacter")
        ]
        ---
        #Settings
        AppState::Settings
        ---
        #Quit
        AppState::Quit
        ---
        #InGame
        AppState::InGame
        Transitions [
            Target(#Talents)
            EventEdge::<AppEvent>::from("ToggleTalents")
            ---
            Target(#FtSinMap)
            EventEdge::<AppEvent>::from("ToggleFtSinMap")
            ---
            Target(#FtSinMap)
            EventEdge::<AppEvent>::from("EnterFtSinMap")
        ]
        Substates [
            #Panels
            InGameState::Panels
            Substates [
                #LeftPanel
                SubStates [
                    #Closed
                    LeftPanelState::Closed
                ]
                ---
                #RightPanel
                SubStates [
                    #Closed
                    RightPanelState::Closed
                    Transitions [
                        Target(#Inventory)
                        EventEdge::<AppEvent>::from("ToggleInventory")
                    ]
                    ---
                    #Inventory
                    RightPanelState::Inventory
                    Transitions [
                        Target(#Closed)
                        EventEdge::<AppEvent>::from("ToggleInventory")
                    ]
                ]
            ]
            ---
            #Talents
            InGameState::Talents
            Transitions [
                Target(#Panels)
                EventEdge::<AppEvent>::from("ToggleTalents")
            ]
            ---
            #FtSinMap
            InGameState::FtSinMap
            Transitions [
                Target(#Panels)
                EventEdge::<AppEvent>::from("ToggleFtSinMap")
                ---
                Target(#InGame)
                EventEdge::<AppEvent>::from("ExitFtSinMap")
            ]
        ]
    ]
}

This is what it would look like in the most recently proposed iteration of bsn! for reference

#

but yeah it's not great

vague pumice
#

kk

crystal marten
# vague pumice kk

btw there's a huge update coming that will make the editor much more plug and play

vague pumice
#

nice

crystal marten
#

Technically you could get on that branch now but it's not quite at parity yet

vague pumice
#

the window plugin change didnt fix it

crystal marten
#

can you make a minimal reproduction of it? or link me the repo?

vague pumice
#

sure

#

bevy_gearbox = "0.4.0"
bevy_gearbox_editor = "0.4.2"
bevy_egui = "0.38.0"
use bevy::prelude::*;
use bevy_egui::EguiPlugin;
use bevy_gearbox::GearboxPlugin;
use bevy_gearbox_editor::GearboxEditorPlugin;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins((EguiPlugin::default(), GearboxPlugin, GearboxEditorPlugin))
        .run();
}
crystal marten
#

I think I mostly agree, but I like the idea that the builder exposes a lot of the API via auto complete. A builder is also better (in theory) at enforcing correctness and producing domain specific warnings, which could make the package easier to use

crystal marten
#

I think that at some point in the future, it may make sense to think about a machine! macro that's similar to bsn but enforces correctness on some domain specific things specific to state machines

#

But I think you're right that the builder isn't the right play

vague pumice
#

thanks it worked

#

how can you make the editor sync with the actual state of the state machine

crystal marten
#

Not sure why that is... but it's fixed in the next version.

vague pumice
crystal marten
#

add

[toolchain]
channel = "nightly" 

to your cargo.toml.. I'll update the readme

#

@vague pumice

vague pumice
#

kk thanks

crystal marten
#

Wait no

#

You need to add a rust-toolchain.toml to your project and put

[toolchain]
channel = "nightly" 

in it

#

@vague pumice

vague pumice
#

i already set the override with rustup dw

vague pumice
#

does this work well with bevy_asset_loader

crystal marten
#

I assume you mean the loading state stuff? I'm not familiar with bevy_asset_loader. If it uses regular bevy states it should work fine

#

But I'd need more info about what you're actually trying to do

vague pumice
#

it uses regular bevy states

#
fn main() {
App::new()
        .add_plugins(DefaultPlugins)
        .init_state::<GameState>()
        .add_loading_state(
            LoadingState::new(GameState::Loading)
                .continue_to_state(GameState::Next)
                .load_collection::<AudioAssets>()
                .load_collection::<ImageAssets>()
        )
        .add_systems(Update, use_asset_handles.run_if(in_state(GameState::Next)))
        .run();
}

#[derive(AssetCollection, Resource)]
struct AudioAssets {
    #[asset(path = "audio/background.ogg")]
    background: Handle<AudioSource>,
    #[asset(path = "audio/plop.ogg")]
    plop: Handle<AudioSource>
}

#[derive(AssetCollection, Resource)]
pub struct ImageAssets {
    #[asset(path = "images/player.png")]
    pub player: Handle<Image>,
    #[asset(path = "images/tree.png")]
    pub tree: Handle<Image>,
}

// since this function runs in MyState::Next, we know our assets are loaded.
// We can get their handles from the AudioAssets resource.
fn use_asset_handles(mut commands: Commands, audio_assets: Res<AudioAssets>) {
    commands.spawn(AudioPlayer(audio_assets.background.clone()));
}

#[derive(Clone, Eq, PartialEq, Debug, Hash, Default, States)]
enum GameState {
    #[default]
    Loading,
    Next
}

this is the example of using it idk how it would work with the gearbox states

crystal marten
#

In the gearbox editor examples there's an example for using gearbox to drive your app state

#

It's called "app_state"

vague pumice
#

thanks

crystal marten
#

Running into some issues with state machine serialization

crystal marten
#

The specific case (though I'm sure this is a more general problem) is state machines for characters. Basically the problem is 2 fold:

  1. Sometimes the entity hierarchy live in bevy (where editing currently happens) has components and state you might not want to serialize. For instance, character have meshes which can't be serialized and will cause saving to fail.
  2. Grafting state machines onto entities is not straightforward. Lets say I have my character_sm.scn.ron which represents my characters state machine, and character.scn.ron which represents the character model. How do I get the character_sm to play nice with character

I've thrown around a lot of "bsn will fix this" and it's true that bsn will allow us to patch scenes onto other scenes, (solves 2 but not 1) BUT bsn! macro is months away and .bsn file format is probably going to come in 0.19, which puts it even further away. Here's my current "solve" I'm thinking of:

Introduce a way to associate state machines to an entity without that entity having to be the state machine, so you could have a "character" entity and "character_sm" state machine entity and just associate the state machine to the character via some component. This also opens up other patterns, like having multiple state machines associated with an entity. A similar pattern is possible today but requires a little custom wiring. This would basically make that first party

chrome helm
chrome helm
#

so I'm not sure I can help you much further than saying that effectively the scenesv2 patch will help but you already noticed it

chrome helm
#

@crystal marten something's either not working or I'm not understanding it

#

I have this ```rust
let aiming = world
.commands()
.spawn((
Name::new("Aiming"),
StateChildOf(entity),
StateComponent(Aiming),
))
.id();

#

and this: ```rust
fn while_aiming(
time: Res<Time>,
mut time_physics: ResMut<Time<Physics>>,
mut aiming_projectiles: Query<(&mut AngularVelocity, &mut LinearVelocity), With<Aiming>>,
) {}

#

the "aiming_projectiles" query is always empty, even when the Projectile is indeed in the Aiming state

crystal marten
#

You're not gonna be happy about this, but state components have to be registered

#

Make sure you're doing that.

#

This is another thing I'm fixing

#

@chrome helm

chrome helm
#

Fk 😅

#

how do you register them?

#

okay, its just register_type

#

okay it's still not working :/

#

@crystal marten

#

I'm trying to find it, but I think it isn't in the docs nor the example 😭

crystal marten
#

.add_state_component()

#

I think

#

@chrome helm

chrome helm
#

although it's better if there's no need like you said ❤️

crystal marten
#

Yeah it'll end up being a derive

vague pumice
#

is it possible to use this without nightly rust

crystal marten
#

Not currently but I could sneak that into the next version

vague pumice
#

thanks

crystal marten
# vague pumice thanks

I'm curious what the use case is? Like I genuinely don't know why someone would choose to not use nightly

vague pumice
#

just kinda weird to force to use the nightly toolchain especially since making it stable compatible wouldnt be that hard

#

also it would be nice to mention in the readme

unreal arch
#

Is it a pure fsm or we can go from AnyState to another like in seldom_state ?
I'm making a state machine for my animation layer but some stuff can be interrupted and defining every transition will be a nightmare without Any

crystal marten
#

This is my app state machine, which shows how you can do "AnyState" style behavior

#

In gearbox, instead of a specific Any state, you are able to define transitions from parents to children

#

However you may have better options. Give me a sec

crystal marten
#

Published a version for 0.18... better late than never

crystal marten
#

Considering an experiment. Right now a pretty big weakness of gearbox is that it's very observer heavy. Like, extremely. A dozen observers could go off in a single transition.

The reason it's so observer heavy is that I decided to user observers to maintain invariants instead of direct world access. Those are really the two ways I knew about to make sure that, when a transition fired, I could guarantee that all relevant effects happened in order that frame. It's important to me that I am able to give my user those guarantees.

#

However, I thought of a third option: I could have a gearbox schedule. I could loop the schedule until all states resolve for that frame. Most the time this would be one or two loops, but when it needed to it could loop as many times as the user likes, up to some limit they set.

#

This wouldn't change anything about how you interface with or use gearbox, it would just change the performance characteristics. I'm going to play around with some benchmarking

crystal marten
#

Actually the observers are shockingly fast so never mind lol

crystal marten
#

Actually actually it's an insane performance improvement at scale. Since I plan on using state machines for my ability system it's probably a good idea to make it fast.

crystal marten
#

Seeing some serious wins

crystal marten
#

Migration guide for the upcoming changes

crystal marten
#

Discussing making Active a relationship (and possibly replacing corresponding fields on the StateMachine component):

That's right, StateMachine uses hashsets internally now, not vecs, so this wouldn't be a total replacement. More just making Active a little more idiomatic bevy. I am concerned that introducing multiple different ways to do stuff like this is duplicating information and splitting the API though. I'm going tho think on it a little more.

maiden ember
#

I think the approach you mentioned in the other chat of having Active be a relationship but keeping the existing field with a #[deprecated] attribute is a good one. You can remove the field later. I also like the idea of making StateMachine just a marker, it would make parallelising things a bit easier in general.

crystal marten
#

I'm running into issues in resolve_transitions that I can't think of how to get around without allocating more than I currently do. Right now I rely on checking if the active and active_leaves contain certain states. Since these are regular fields on a regular component and not relationships, I can mutate those fields and read the updated state in the same system for loop. In the relationship version the commands.entity(s).insert(Active {...}) is deferred.

That becomes a problem when you have two transition messages in the same frame for states A, B, and C like so:

A->B
A->C

You go through the resolve_transitions for loop and do the A->B. Now we're in state B so A->C is invalid. The current system handles this gracefully. I think relationships would mean I'd need to allocate per-machine tracking hashsets. Something like a mutable HashMap<Entity, HashSet<Entity>> that would end up accumulating all of the state changes in the system so they can be applied at the end. If you can figure this out I'm open to hearing you out but there's a couple more problems that I'll lay out.

#
  1. I haven't checked, but I assume
    sm.active.contains(e), which might have a dozen entities in it is going to be faster than
    active_query.contains(e) which works on every entity that has an Active component

(Note: I could be totally wrong on this point. Maybe the query lookup is faster for all I know. This isn't really a blocker, just a concern.)

#
  1. To be quite honest I don't really like the Active component. It was the best way I could think of to solve the problem of allowing users to react to states being entered or exited, but it has a lot of problems. Since the previous version was observer based, I fired EnterState events that users could listen to in an observer. This was great because the events scoped to the machine. When I tried to port this to messages I ran into a problem. An EnterState message reader has EnterState messages for EVERY state. Any MessageReader<EnterState> is iterating over all state changes.

A system with a query like Query<&MyStateStuff, Changed<Active>> can detect any time the state is entered and it discriminates so the user only gets state changes relevant to their system. If users only want when a state is initially entered (not necessarily re-entering already active states, which gearbox allows) they can change to Added<Active>. The issues is detecting when a state is left... you have to do RemovedComponents<Active> and don't get access to the machine. You have to manually traverse the SubstateOf relationship component query to get the machine.

#

Actually typing it out like that I suppose I could have an Inactive component as well so users don't have to do that. I could even use component hooks to auto add this if Active is removed... that could work.

#

Another thing to consider in all this is that, whatever variant of this we go with (StateMachine component with fields vs separate relationship components) is that this should be a completely internal API detail. Users should be reasoning about the surface of their state machines, not the internals. StateMachines fields are pub because that was convenient while I was developing this crate but they should probably be pub(crate) for correctness.

We want users to be able to react to state changes, but consumers of state machines should be reasoning about the entire state machine, not whether Entity43583v1 is currently in the active set. This is why Active has the machine field. Sure, the user is querying on states, but they are only doing that to do things like fire other messages, alter their state machine via StateComponents, etc.

#

This is my current thoughts. Not saying it's not possible I'm just not sure how you get around resolve_transitions. Everything else seems plausible without introducing a bunch of additional weirdness. If you can figure it out I'll gladly accept a PR

crystal marten
#

Gonna drop this in crates soon:

bevy_gearbox 0.6

What is it?

  • State machines are entity hierarchies: States and transitions are plain entities you can query, spawn from scenes, or edit at runtime. Primed for bsn!
  • Experimental visual editor: Build, edit, and monitor state machines while your game runs.

What's new?

Message-based internals. The resolution engine has been completely rewritten. Transitions are now triggered via Bevy messages instead of entity events/observers, and resolution runs through a parallelized schedule instead of observers. The result is roughly 100x better scaling - thousands of state machines per frame for the price of a couple ms.

For most users the migration is straightforward. See MIGRATION.md for details.

Branching transitions. A single edge can now branch to different targets based on guards. BranchTransition evaluates arms in order. The first with passing guards wins, with an otherwise fallback.

Terminal states. Mark a state with TerminalState and when it's entered, a Done message is automatically emitted to the parent state. The parent can then transition out via MessageEdge<Done>. This is a standard statechart "final state" pattern, now in bevy_gearbox!

Gauge integration. With the gauge feature enabled, Gearbox embeds gauge attribute sync directly into its resolution schedule. Transition delays can read from attributes (e.g. a "Cooldown" stat), and AttributeRequirements can act as transition guards.

crates.io serves as a central registry for sharing crates, which are packages or libraries written in Rust that you can use to enhance your projects

maiden ember
# crystal marten I'm running into issues in `resolve_transitions` that I can't think of how to ge...

A mutable hashmap is usually fine in a system because you can use Local<EntityHashMap<EntityHashSet>> or Local<Parallel<EntityHashMap<EntityHashSet>>> (the latter for if you want to use par_iter().for_each_init(..)), which amortises the cost of allocation across frames. You still have to pay some cost compared to storing it directly in the ECS but in my experience if you only care about mutating state within a single system and then applying it to the ECS at the end then the performance is fine.

maiden ember
crystal marten
#

So with events we kinda had to do blockers how I did them which is a bit awkward. With messages, we can insert MessageMutator systems between when a transition is called and when it actually goes off and discard bad transitions based on blockers.

#

In other words no need for a Blockers component at all, just user defined systems that in the schedule that discard bad transition messages!

#

@maiden ember

maiden ember
#

And fits bevy_gearbox's goal of being very bevy-native and idiomatic

crystal marten
#

Fixed a ton of bugs on core. I'll be putting up a new release soon. New release will include auto layout for the editor

maiden ember
#

I replied to the wrong message, I meant to talk about the video

crystal marten
#

Thanks!

dusk lance
#

Hello! Love this crate, just what I needed. I just wanted to ask about exposing a way to run the loop on a different schedule, I need to run it on FixedPreUpdate. I could submit a PR if you want

crystal marten
#

Yeah please do

#

@dusk lance it would be appreciated 🙂

#

You'll want to hurry as the next major version drops with bevy 0.19

dusk lance
#

I took a liberty and gitignored Cargo.lock. I assumed that was a mistake, let me know if you want it back

crystal marten
#

Looks good. Merged @dusk lance