Skip to content

different system for state transition #105

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

Open
wants to merge 28 commits into
base: development
Choose a base branch
from

Conversation

tomara-x
Copy link
Contributor

@tomara-x tomara-x commented Mar 26, 2025

check the example to see how it works, and let me know what you think and if there's any limitations/issues with this

still need to edit the docs to reflect the changes

@tomara-x
Copy link
Contributor Author

tomara-x commented Mar 28, 2025

the last commit

videos

before:

2025-03-28_05-04-37.mp4

after:

2025-03-28_05-06-13.mp4

added looping
cleaned up the constructor functions
and added a continue segment (for moving weight from current value)
and updated the example
@tomara-x
Copy link
Contributor Author

tomara-x commented Mar 29, 2025

we can merge the base balancer into the transition system (i'm thinking "we're already looping the states here, and less loops = faster") but i'm not sure how correct that is. and it becomes an even bigger mess than it already is! xD

horror (code)

pub fn system_animate_transition(
    mut query: Query<(Entity, &mut UiState, &mut UiStateAnimation)>,
    time: Res<Time<Virtual>>,
    mut commands: Commands,
) {
    let mut should_recompute = false;
    for (entity, mut manager, mut animations) in &mut query {
        let mut total_nonbase_weight = 0.0;
        for (state, anim) in animations.iter_mut() {
            // we want to check if we reached a trig separately so we can trigger it
            // and resume animation without skipping a frame
            if let Some(Seg::Trig) = anim.segments.get(anim.current_seg) {
                commands.trigger_targets(AnimTrig(state), entity);
                anim.step();
            }
            if let Some(seg) = anim.segments.get(anim.current_seg) {
                match seg {
                    Seg::To(target, duration) => {
                        if !manager.states.contains_key(state) {
                            manager.states.insert(*state, 0.);
                        }
                        let weight = manager.states.get_mut(state).unwrap();
                        if anim.value == *target {
                            anim.step();
                        } else if anim.value > *target {
                            anim.value = (anim.value - time.delta_secs() / *duration).max(*target);
                            *weight = anim.value;
                        } else {
                            anim.value = (anim.value + time.delta_secs() / *duration).min(*target);
                            *weight = anim.value;
                        }
                        should_recompute = true;
                    }
                    Seg::Curved(target, duration, f) => {
                        if !manager.states.contains_key(state) {
                            manager.states.insert(*state, 0.);
                        }
                        let weight = manager.states.get_mut(state).unwrap();
                        if anim.value == *target {
                            anim.step();
                        } else if anim.value > *target {
                            anim.value = (anim.value - time.delta_secs() / *duration).max(*target);
                            *weight = f(anim.value);
                        } else {
                            anim.value = (anim.value + time.delta_secs() / *duration).min(*target);
                            *weight = f(anim.value);
                        }
                        should_recompute = true;
                    }
                    Seg::Continue(target, duration) => {
                        if !manager.states.contains_key(state) {
                            manager.states.insert(*state, 0.);
                        }
                        let weight = manager.states.get_mut(state).unwrap();
                        if *weight == *target {
                            anim.step();
                        } else if *weight > *target {
                            *weight = (*weight - time.delta_secs() / *duration).max(*target);
                        } else {
                            *weight = (*weight + time.delta_secs() / *duration).min(*target);
                        }
                        should_recompute = true;
                    }
                    Seg::Hold(duration) => {
                        if *duration <= anim.elapsed_duration {
                            anim.elapsed_duration = 0.;
                            anim.step();
                        } else {
                            anim.elapsed_duration += time.delta_secs();
                        }
                    }
                    Seg::Trig => {}
                }
            }
            // Normalize the active nobase state weights
            if *state != "base" {
                if let Some(weight) = manager.states.get(state) {
                    total_nonbase_weight += weight;
                }
            }
        }
        // Decrease base transition based on other states
        if should_recompute {
            if let Some(value) = manager.states.get_mut("base") {
                *value = (1.0 - total_nonbase_weight).clamp(0.0, 1.0);
            }
        }
    }
    if should_recompute {
        commands.trigger(RecomputeUiLayout);
    }
}

@tomara-x
Copy link
Contributor Author

...is that some states will need to have different logic. For example the intro state, which I want to play when the component is spawned. This could be either instant, delayed, or triggered on event, where it waits for other UiIntros in the scene to finish...

#99 (comment)

for this i'm thinking about adding a Trig variant to the Seg enum. whenever the transitioning system finds this segment, it triggers an event (targeting this entity, and maybe with info of the state being animated) and it jumps to the next segment. this way you can do things at any stage of an animation on any state for any specific entity

idk why i thought that was right
we always check and insert, so this is just unnecessary indentation
@tomara-x
Copy link
Contributor Author

tomara-x commented Mar 30, 2025

the workaround (previous commit) avoids this issue

videos

before: (spawning with the observer)

2025-03-30_19-16-17.mp4

after: (spawning with an event)

2025-03-30_19-18-22.mp4

and use Time<Real> (we're the ui, we stop for nothing)
@tomara-x tomara-x marked this pull request as ready for review March 31, 2025 02:29
@tomara-x
Copy link
Contributor Author

tomara-x commented Apr 1, 2025

thinking about this since yesterday #99 (reply in thread)
i want to do something close to this, i want to have this freedom to animate properties separately like you're describing, but i'm thinking of using the same Anim type, so it's just one closure that gets called every frame and update whatever value in the layout based on a given animation (still just an unfinished thought)

@tomara-x tomara-x marked this pull request as draft April 2, 2025 21:31
@tomara-x
Copy link
Contributor Author

i've done something on top of this that's a little more... conceptually adventurous xD
https://github.com/tomara-x/bevy_lunex/pull/1/files

you can animate different fields separately

2025-04-11_06-15-17.mp4

@tomara-x tomara-x marked this pull request as ready for review April 11, 2025 04:32
@tomara-x

This comment was marked as resolved.

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.

1 participant