[legion_v2] How to perform secondary Entity reads while iterating over query results?

I’m putting together a trivial little test project to experiment with Amethyst. I was advised to try out the legion_v2 branch, which apparently will become the “official” Amethyst “real soon now”, so that’s what I’ve been doing.

I’ve hit a wall with my very first System, and it’s trivial enough that I’m sure someone knows the answer, but I’ve now spent 2-3 hours banging my head against it.

Background: Basically, what’s going on is that I’m trying to build the world’s stupidest Tetris implementation ever. Because I want to experiment with Amethyst, I have totally over-engineered it. Specifically, the problem I have right now is:

  • There is a Board entity, which represents the play area. It contains a sprite holding the outline of the play area, and a component with game state (such as which squares are filled). This is used when an overall picture of the board is needed (for instance, to find filled rows, or to check collisions with a falling piece).
  • There is a Block entity, which represents a filled square. It contains a sprite (so I can draw the square) and an Entity reference to the Board. The Block’s component stores its location in “board coordinates” (where (0, 0) is the lowest-left square on the board grid). This is the source of truth for the block’s location; its Transform and Sprite are derived data that are used to place it on the screen.

I’m sure there are better ways to do Tetris. I chose this approach because it has some parallels with other projects I’m interested in trying out.

My very first System is called UpdateBlockTransform. It is responsible for … updating the block transform. Specifically, the logic should be (pseudocode):

  • For each Block b:
    • Set b.Transform to b.Board.Transform + (BlockDimensions.width * b.x, BlockDimensions.height * b.y).

That’s all well and good, but my efforts to implement this in code have been unsuccessful, because I can’t figure out how to get access to the Transform component of b.Board. Here is my first attempt:

            SystemBuilder::new("UpdateBlockTransformSystem")
                .with_query(<(&components::Block, &mut Transform)>::query()
                            .filter(maybe_changed::<components::Block>()))
                .read_component::<Transform>()
                .build(move |_commands, world, (), query| {
                    query.for_each_mut(world, |(block, local_transform)| {
                        let board_ref = world.entry_ref(block.board).unwrap();
                        let board_transform: &Transform = board_ref.get_component().unwrap();

                        *local_transform = board_transform.clone();
                        local_transform.prepend_translation_x(block.x as f32 * components::BLOCK_WIDTH);
                        local_transform.prepend_translation_y(block.y as f32 * components::BLOCK_HEIGHT);
                    })
                })

This fails because world is borrowed mutably by for_each_mut, but immutably by entry_ref.

I also tried using a “get” API on queries, which is used by the unit tests of the new “system” API.

            SystemBuilder::new("UpdateBlockTransformSystem")
                .with_query(<(&components::Block, &mut Transform)>::query()
                            .filter(maybe_changed::<components::Block>()))
                .read_component::<Transform>()
                .build(move |_commands, world, (), query| {
                    query.for_each_mut(world, |(block, local_transform)| {
                        let mut transform_query = <&Transform>::query();

                        let board_transform : &Transform = transform_query.get(world, block.board).unwrap();

                        *local_transform = board_transform.clone();
                        local_transform.prepend_translation_x(block.x as f32 * components::BLOCK_WIDTH);
                        local_transform.prepend_translation_y(block.y as f32 * components::BLOCK_HEIGHT);
                    })
                })

This ran into the same problem: world is borrowed as both mutable and immutable.

I tried a few weirder options out of desperation, and spent a lot of time poring over the docs and code trying to figure out what was going on. None of them worked.

Combining data of two entities seems like a pretty basic functionality – without it, there are a bunch of things I just can’t do (without bypassing the ECS library entirely, of course). As far as I understand, it was available in specs by just invoking get or get_mut on an appropriate *Storage implementation. Has this been removed in legion? Or am I misunderstanding how one of the libraries works?

Thanks!

Replying to my own comment to maybe clarify it.

I think I really have two problems here:

  1. I’m trying to access World both mutably and immutably (the syntactic problem).
  2. I’m trying to read and write the Transform component.

(1) seems like something the library absolutely should support. Maybe it does with “sub worlds”? But from past experience, I’d expect something I can pass to SystemBuilder to declare "oh, and I want random write access to component T", and then expect to receive it as part of my closure’s input.

(2) is a more fundamental issue, and I can see why it might be rejected on principle. But I’m having trouble squaring that with practicality. Cyclic data dependencies can be a real pain, but they’re also needed for some pretty important kinds of algorithms, generally algorithms that work in some kind of iterative way. For instance, the code I wrote above is basically a poor man’s version of a scene graph. (EDIT) And there are other categories of algorithm besides graphs that require iteration that could be interesting for game purposes, like cellular automata or economic “simulations” (using the word quite loosely to mean something like Civ).

My attempt to finesse this by moving the upstream Transform into the Board component didn’t work:

SystemBuilder::new("UpdateBlockTransformSystem")
    .with_query(<(&components::Block, &mut Transform, &components::Board)>::query()
                .filter(maybe_changed::<components::Block>()))
    .build(move |_commands, world, (), query| {
        let (board_world, mut block_world) = world.split::<&components::Board>();
        query.for_each_mut(&mut block_world, |(block, local_transform, _board)| {
            let board_ref = board_world.entry_ref(block.board).unwrap();
            let board: &components::Board = board_ref.get_component().unwrap();

            *local_transform = board.origin.clone();
            local_transform.prepend_translation_x(block.x as f32 * components::BLOCK_WIDTH);
            local_transform.prepend_translation_y(block.y as f32 * components::BLOCK_HEIGHT);
        })
    }))

This compiles, but query.for_each_mut fails at runtime with AccessDenied and no further explanation of what went wrong.

I’m guessing this is because I queried components::Board, but some entities don’t have that component? But if I don’t query it, I get an error that my world doesn’t have Board. (EDIT) I changed the query to retrieve Option<&Board>, which I believe should handle missing components, and I still get AccessDenied.

Ah, the AccessDenied is happening when the query tries to retrieve components. That … kinda makes sense, since the SubWorld I provided doesn’t support the “board” component. I’m a bit surprised it wasn’t a type error … oh, I see, SubWorld doesn’t have type parameters. Interesting.

So:

  • If I don’t query for Board, I can’t read it because it’s not in the SubWorld my System gets.
  • If I query for Board but use a single World, I can’t read it because I would be accessing the World twice.
  • If I split the World as above, I get an error because the split World no longer has components required by the query.
  • If I split the World but use the un-split World for my query, I get a compile error because I’m borrowing world twice (fair).

AHA!

The earlier versions of this code had cargo-culted a call to read_component from the Pong tutorial. I read the docs and they said all it does is take a lock on a whole component, which seems pointless, so I deleted it somewhere along the way. But it appears to also have a side effect of adding the component to the SubWorld the system receives. This works:

SystemBuilder::new("UpdateBlockTransformSystem")
    .with_query(<(&components::Block, &mut Transform)>::query()
    .filter(maybe_changed::<components::Block>()))
    .read_component::<components::Board>()
    .build(move |_commands, world, (), query| {
        let (board_world, mut block_world) = world.split::<&components::Board>();
        query.for_each_mut(&mut block_world, |(block, local_transform)| {
            let board_ref = board_world.entry_ref(block.board).unwrap();
            let board: &components::Board = board_ref.get_component().unwrap();

            *local_transform = board.origin.clone();
            local_transform.prepend_translation_x(block.x as f32 * components::BLOCK_WIDTH);
            local_transform.prepend_translation_y(block.y as f32 * components::BLOCK_HEIGHT);
        })
    }))

This unblocks my immediate Tetris related exercise. But I’m still worried about the larger question of how to handle iterative algorithms with Legion. e.g, how would you implement Conway’s Life on top of Amethyst?

1 Like

I found an implementation of Life on Amethyst. Looks like it calculates a list of changes to make, then applies them in a second pass. I guess that probably works pretty well as long as most cells don’t change (which is typically true for Life).

If your automaton’s cells change more often, and the overhead of the temporary storage becomes significant … I guess you could have a “scratch” component that you write to in the first pass, then read back from in a second pass that just “promotes” the scratch component to real data. Takes O(N) time and space, but if you have this problem you’re kind of stuck with that part of it.