Skip to content
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

Filter out multiple enemy messages moving to the same location #1

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

tmilker
Copy link

@tmilker tmilker commented Nov 6, 2021

This pull request is in regards to the code in Chapter 9: Victory and Defeat. At least, it's where I discovered it.

This fixes an issue with enemies being able to move into the same location at the same time. If you have two enemies like this:

.g.
g..
...

And manipulate them into moving into the center position, they'll both end up occupying the same space. There's no actual check at the time of processing the message that the space is free. I think Legion's parallelism is getting in the way but I'm not sure*.

Also would like to know if I wrote this correctly or if there is a better/more efficient way. Creating that Vec and throwing it away every frame for positions seen seems wasteful but I'm just getting started with rust so maybe not?

*I tried following your very recent article to disable Legion's parallelism and test without this fix, but it still occurs. I still see the program using multiple threads on my computer though so I don't trust that it's actually disabled.

@Laharah
Copy link

Laharah commented Jan 9, 2022

I found this same bug. The issue is that the movements are executed in batch rather than one at a time. Rather than implementing a filter system or some kind of reservation system, you can just change the system to execute the movements one at a time and preform a check that the space hasn't become occupied.

#[system(for_each)]
#[read_component(Player)]
#[write_component(Point)]
pub fn movement(
    message: &Entity,
    want_move: &WantsToMove,
    #[resource] map: &Map,
    #[resource] camera: &mut Camera,
    ecs: &mut SubWorld,
    commands: &mut CommandBuffer,
) {
    if map.can_enter_tile(want_move.destination) {
        let unoccupied = <&Point>::query()
            .iter(ecs)
            .all(|pos| *pos != want_move.destination);
        if unoccupied {
            *ecs.entry_mut(want_move.entity)
                .unwrap()
                .get_component_mut::<Point>()
                .unwrap() = want_move.destination;
        }

        if ecs
            .entry_ref(want_move.entity)
            .unwrap()
            .get_component::<Player>()
            .is_ok()
        {
            camera.on_player_move(want_move.destination);
        }
    }
    commands.remove(*message);
}

you duplicate some work from the player_input and the random_movement systems but it's the most minimal change I could think of.

I think to do it properly you should

  1. remove the for_each tag on the system so it only runs once not once for every message
  2. build a set of all the want_to_move.destinations, filtering out any conflicting messages
  3. batch the filtered movement commands
  4. delete all the message entities.

The only problem is that it would seriously change the system and might interrupt/overcomplicate what you're trying to teach the reader.

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.

2 participants