amethyst::StateMachine - independent states

(Rybek) #1

Problem

The engine currently doesn’t allow modeling of independent states.

Example:

    // Ran in both on_delete and on_stop of MenuState
    fn erase_menu(&mut self, world: &mut World) {
        match self.game_title {
            Some(entity) => {
                let _ = world.delete_entity(entity);
            }
            None => panic!("Menu doesn't exist"),
        };
        let _ = world.delete_entities(&self.menu_items);
        self.menu_items.clear();
    }
    // Ran in both on_start and on_resume of MenuState
    fn create_entities(&mut self, world: &mut World) {
        self.game_title = Some(initialise_title(world));
        self.menu_items.push(initialise_option(world, "START", 10.));
        self.menu_items.push(initialise_option(world, "EXIT", 10. + 110.));
    }

Partial solution

Like in the example above, manually make sure the data gets removed in State::on_stop, deactivated (no Systems operate on it) in State::on_pause, added in State::on_start and activated in State::on_resume.

The State transition overhead of the solutions grows tremendously if application is supposed to Trans::Push another state on top of our MainGameState, due to the sheer amount of Entities that we have to deactivate, all Systems also must follow strict guidelines to not operate on that stopped data.

I am aware that there are better partial solutions, but all involve making game developers try to add functionality to the engine rather than to the game.

Proposal

The proposal is to add option to create independent States, with their own data (Worlds) while preserving the separated data of States lower on the stack.

The proposal involves some assumptions that I have made, which shape the possible implementations and that also require prior discussion.

Assumptions

  • Users need the design freedom provided by independent states
  • Independent states are common in games
  • State transitions should be free of heavy logic
  • Game developers shouldn’t be forced to mix engine functionality with game code

Implementation ideas

Draft 1

Multiple Worlds implementation idea

  • 2 stacks, one containing state (StateStack), one containing world (WorldStack)
  • At the bottom of WorldStack - one unremovable world (GlobalWorld)
  • When pushing State::Push/State::Switch can decide to push with new world - adds another world on top of WorldStack
  • GlobalWorld can be accessed in any State
  • No Entities and Components in GlobalWorld
  • Every other World has it’s own ECS entity/component storage
  • Resource can be added to either GlobalWorld or local World
  • State::Pop on a State that added a World will also remove said World
  • Run only Systems for the ECS on top of the WorldStack

Problems

  • possible breaking changes in amethyst API
  • introduction of new concepts to the book due to more expressive design
  • Entity handles from different Worlds shouldn’t be moved between them

Some API changes can be minimalized, but torkleyy said current Application design is more complex than initially planned so it might be a better idea to focus not on keeping the current API, but simplyfing it where possible.

The book chapter changes wouldn’t be that big, but definitely a new chapter describing the changes should be added alongside regular updates.

Additionally the changes shouldn’t be commited directly onto master branch, but included as a part of bigger Application renovation on separate branch. (in case this gets a green light in any form)

Conclusion

I’m interested in hearing everyone’s opinions on the topic. Whether it’s positive or negative, I believe discussion is needed as the topic is bound to resurface from someone else in the future.

This is my first proposal for Amethyst engine, if anything is unclear I would be very grateful for DMs involving constructive critism. I have rewritten the whole post also, the first version is locally saved in case what I did is frowned upon.

4 Likes
(Michael Leandersson) #2

You can use the Removal component with some utility functions to create “managed” entities that is automatically removed when popping the state that created them.

See https://gist.github.com/tripokey/1cc937d4603bd7b38fc1229312dcb12b

1 Like
(Brian West) #3

Been letting this one ferment in my brain for a little bit. I recognize the concern presented here, but the solution is going down the wrong path. It mistakes the root cause of the issue. The root cause is not the need to separate entities into distinct worlds because they can’t know about each other. After all, that is the entire purpose of ECS - new components can be added to entities, or entities with different components can be added to the world without existing systems needing to know or care about them. They pull only the data they need and nothing more.

In one example we discussed, there was a mini-game that needed the same component as a the main game (Velocity). One option here is to simply add an empty pub struct MinigameObject that implements Component and attach an instance to each entity in the mini-game. Those systems can do a join over the combination of the two components rather than just the velocity.

Now, what if the velocity component needs to be treated identically between the two but you want some to be paused? A few options:

  1. you can attach a GameModeTag component and create a GameMode enum resource. A single velocity system can verify the game mode tag value matches the current game mode before processing an entity. This option should work fine.

  2. you can keep the original example with a MinigameObject component and then have separate systems, one with the join and one without. Both can call out to a separate function that handles the velocity for a particular entity. You can annotate the function with #[inline(always)] to force it to be inlined in the system if desired.

  3. when the main game state is shadowed, it can automatically add a Paused component to each entity that it manages (potentially maintaining a list of what has been paused). I find it generally effective for a state to keep track of the entities that it has created so that it can clean up after itself when it pushes something on top of it or pops itself off the state stack.

Are there cases where these don’t work? Sure. Without knowing the exact use case, I can only give examples of ways to approach the problem. But anyone can come up with a contrived niche case where an example won’t work. I’m not trying to over-engineer a solution that will handle every possible corner case here because I’m a firm believer in KISS. One thing that I think the above gets at is the temporal nature of components. It’s easy to think of components as static on an entity, but they are not. They can be added and removed at will to change the behavior of the entity throughout the game. In this sense, they’re not synonymous with interfaces in an OOP pattern as they are often compared to.

Ok, that said… I agree that managing the dispatcher with custom game data is a bit obtuse. I don’t mess with it myself because I find the idea of doing so daunting beyond what I wish to tackle. I typically use some combination of the SystemExt trait with .pausable and manual checks of particular resource states at the beginning of the system to do an early return from the system. It’s not the optimal solution, but the performance cost is low enough that it would be wasted effort to tackle. That may change some day, but I doubt it.

I don’t like the proposed solution of multiple isolated ECS environments for this because I think it skirts the advantages gained by ECS and the reason to use it as opposed to OOP or some other world management pattern. The principle advantage and the entire point of ECS is the flat hierarchy which makes it easier to be flexible and adapt to changing requirements or adding new features that were previously not considered. Creating isolated ECS environments with different systems and potentially different components cripples that benefit.

One option I feel would be nice is to have another function added to SystemExt - MySystem.should_run(some_func), where some_func would either be a function or closure that takes some S: SystemData and returns a boolean. It could be used to wrap the system in much the same way that .pausable already does but allow for greater control over whether a system executes. I tend to use several different enum resources to identify the game’s state, including an AnimationState, GameMode, LevelActivityState, etc. (the latter, for instance, has AwaitingInput, Pending, and Active. But some of the enum variants have data associated with them. LevelActivityState::Active(u64) for instance tracks the frame number when it became active. The way .pausable behaves is not conducive to enum variants with data because it does a simple comparison.

Finally, the problems described with the state stack seem to be an issue of mental framing rather than architecture. I would advise using more general purpose, smaller scale states rather than big monolithic ones. You could likely use a single GameState that takes a a prefab handle for the entities in the level/world to be instantiated. When it starts, it automatically adds the prefab to the world. When it is shadowed, it can automatically add the aforementioned Paused component to entities that it “owns.” When it is “unshadowed” it can remove the Paused component. when it is popped, it can remove the entities it made. Then you can push multiple GameState instances on top of each other and the same struct with different parameters can be used to represent the main game and the mini games. Likewise, a single MenuState(Handle<Prefab<MenuEntry>>) can be used for all menus in the game, where each menu entry implements PrefabData and has a OnSelect(MenuEvent) and a system can handle all of them. When it is shadowed, it likely removes its entities (and recreates them when unshadowed) so that other menus can be shown. In this way, it’s easier to build menu hierarchies by a MenuState(main_menu) pushing a MenuState(save_select_menu) which pushes a MenuState(level_select_menu), etc. Then that menu can push the first GameState(selected_level_prefab, ..) onto the stack. At that point, your state stack looks like:

GameState(selected_level_prefab, ..)
MenuState(level_select_menu)
MenuState(save_select_menu)
MenuState(main_menu)

My point is that many problems encountered by the state stack are resolved by reframing the problem to lean more heavily on it rather than leaning away from it. Use more lightweight states rather than fewer heavyweight ones. Another example is a DelayState(u64) that I created that simply sets GameMode to Paused on entry and pops itself off the stack after a specified number of frames, useful for pausing while some transition animation is played.

Reframing the state problem in this way helped me substantially, so hopefully you find it useful.

1 Like