"Composite"/linked components?

I have this idea of “composite components”; bundling together existing, individual components to achieve something like “queued orders”.

For example:
I have a pathfinding component (managed by a pathfinding system) and a path movement component (managed by a path movement system). I would like to create a third composite component that first performs pathfinding and then, based on some boolean condition, pops/removes the current (pathfinding) component and pushes/adds the next (path movement) component until the queue is empty.

This is a simple two-step example, in reality these would probably be longer, but hopefully enough to get my point.

Ideally these could be constructed using any component, built and consumed using something simple like push/pop mechanics and maybe a closure to identify the condition when to perform the transition.

I’m not super familiar (yet) with all the modules of Amethyst and specs, does anyone have any ideas or have done something similar? Or am I overthinking this and missing some obvious way to achieve the same result?

Thanks!

I’m sure I didn’t understand what you mean by composite components, or what problems such a thing would solve, but I’ll try to think on your example and how I would go about it.

I would personally use a single component for it, that I believe would be equivalent to your Path Movement component. Its struct would be something like this:

    // To be used with other components that include relevant data
    //      like current position, speed, angle or whatever necessary
    pub struct Path {
        pub active: boolean,
        pub current_path: MyPathStruct,
        pub current_path_target: SomeWaypointStruct,
        pub interesting_data: f32,
        pub very_interesting_data: u64,
    }

I’d then create a resource for the PathFinder.

    //PathFinderResource.rs
    //This should hold the pathfinding data and logic
    pub struct PathFinderResource {
        navigation_data: BTreeMap<&'static u64, Handle<NavigationInfoStruct>>,
    }

And then create and insert the resource in the world

    //SOME_STATE_FILE.rs
    //Need to register the pathfinder resource
    let navigation_data = get_navigation_data(data.world);
    data.world.insert(PathFinderResource { navigation_data });

With both the path component and PathFinder resource in place, I’d then create 2 systems, PathfindSystem and MovementSystem.
The MovementSystem would join Path with Position, Speed, and whatever else necessary

    impl<'s> System<'s> for MovementSystem {
        type SystemData = (
            ReadStorage<'s, Path>,
            WriteStorage<'s, Position>,
            WriteStorage<'s, Speed>,
        );

        fn run(&mut self, (paths, mut positions, mut speeds): Self::SystemData) {
            for (path, position, speed) in (&paths, &mut positions, &mut speeds).join() {
                //If the path is inactive, ignore and continue the loop
                if (!path.active) continue;

                //Else, move stuff around, change their speeds, whatever applicable, 
                //  based on their current path data
            }
        }
    }

The second system would be responsible for updating the path components

    impl<'s> System<'s> for PathSystem {
        type SystemData = (
            WriteStorage<'s, Path>,
            ReadStorage<'s, Position>,
            //This would be a Resource with the navigation data
            Read<'s, PathFinder>,
        );

        fn run(&mut self, (mut paths, positions, path_finder_resource): Self::SystemData) {
            for (path, position, speed) in (&paths, &mut positions, &mut speeds).join() {
                //Test if should be active or deactivated
                path.active = path.test_if_arrived() || path.other_tests_to_keep_active();
                //If inactive, skip and continue the loop
                if (!path.active) continue;

                //Is active, update the path
                path_finder_resource.update_path(&mut path, &position);
                //      or like this
                path.interesting_data = path_finder_resource.update_interesting_data(&path, &position);
                path.very_interesting_data = path_finder_resource.update_very_interesting_data(&path);
                //      ... etc
            }
        }
    }

Now we have this, anything we need added, eg dynamic danger avoidance, entity stun / slow down, we just need to create a new system, and run it with those components.

My takeaways are,

  • I would favor updating component data over poping and pushing new components
  • I’d rather have more, simpler Systems that could be run in parallel instead of having a single complex System running on many mutable components.
  • I’d centralize the pathfinder in a resource, and give it all the navigation data, so it can be used by a system to simply update each path component. This would also make it easier to implement things like dynamic navigation (eg dynamic cost/walkability/stuff).
  • I clearly didn’t understand what you meant by composite components

Other considerations:

1 Like

Thanks for the in-depth reply, and yeah sorry for the bad naming I don’t know what to call it really.

What I was thinking about was instead of building a new component/system for each of the possible combinations for “orders” in my game, I thought maybe one can use some generic way.

So I have pathfinding and pathmovement, let’s pretend I also have components and systems in place for crafting weapons and for chopping wood.

Now for the case where I want to do:
pathfinding -> pathmovement -> chopping wood -> crafting

I could create a component to handle this specific scenario. But then I may want to do:
pathfinding -> pathmovement -> crafting

or

pathfinding -> pathmovement -> crushing rocks -> pathfinding -> pathmovement -> crafting -> pathfinding -> sleeping

Instead of creating a specific component/system that handles each unique case, I thought maybe one could use a generic component that always holds the “next step” (next component) and a simple bool on when to “change” into it.

something like

QueuedOrders::new(
    (Pathfinding::new(), |&w| // implement when this component is "done"),
    (PathMovement::new(), |&w| // implement when this component is "done"),
    (CraftItem::new(....), |&w| // implement when this component is "done"),
    (....),
    (....)

But after thinking about this some more I think I can achieve the same approaching this from a different angle. I’m probably overthinking things.

Yeah, I have one eye on legion but have not made the “jump” yet.

Thanks again!

1 Like

Oh, yeah, an AI example is a bit more interesting, and something I am having to solve myself, but taking it on to completely different direction. It is definitely possible to create an Actions or OrderQueue component, and create one or many systems on top of it. Still, I would definitely not create a component for each specific scenario/order combo.

Even if I could create a single generic Controllable component for that, I’d probably have components that flag what an entity can or not be ordered to do, WoodChopper, PathMovement, Crafter, and add whatever data I need for each order in their structs.

Now, to the crux of the problem, how to control and queue said orders. I would argue this should not be 100% contained in the ECS system, and should be treated the same way we would treat player input. Imagine player input and AI both creating commands (e.g. Jump, MoveTo(x,y), Craft(smthng) ). We could pass such commands to their controllable entities. These entities would then process those commands the same way they’d act on player input.

As the very word I’m using already suggests, I’m talking about the Command Pattern. As I did on the previous comment, removing the PathFinder component in favor of PathFinderResource, I’d also move all the AI logic to a resource. This resource would be the one responsible for controlling all queues, and all a system needs to do when run is check if their current command was cancelled or request the next command when the current is completed.

Using AI like this can have many positive side effects. It decouples much of the AI from the components. Simplifies the systems needed to make it. Good parallelization of these systems. Can leverage shred very well.

I am not there yet (probably for the coming weeks anyway), but that’s how I plan on doing my thing.

Just to reconnect on my main question,

EventChannels really was the missing piece of the puzzle in my case. I now have all my core systems (like pathfinding, movement) completely separated, split up to a single responsibility without mutating a bunch of stuff. Pretty much the only thing they mutate is the EventChannel they’re responsible for. This keeps the core systems running fast and parallell without them ever needing to know anything outside their specific domain.

With relevant events being emitted from the core systems it’s quite easy to build complex behaviour, like order management, using only lightweight “managenemt” systems.

2 Likes