What is happening in that function?

I am still following this ECS tutorial and cannot figure out what is happening in that function
There is a lot of syntax that looks unfamiliar but I will get there with the book hopefully. What I am wondering right now is the logic of moving boxes around:

Here is the whole function (with the author comments):

pub struct InputSystem {}

impl<'a> System<'a> for InputSystem {
   type SystemData = (
       Write<'a, InputQueue>,
       Entities<'a>,
       WriteStorage<'a, Position>,
       ReadStorage<'a, Player>,
       ReadStorage<'a, Movable>,
       ReadStorage<'a, Immovable>,
   );

   fn run(&mut self, data: Self::SystemData) {
       let (mut input_queue, entities, mut positions, players, movables, immovables) = data;

       let mut to_move = Vec::new();

       for (position, _player) in (&positions, &players).join() {
           if let Some(key) = input_queue.keys_pressed.pop() {
               //get all movables and immovables
               let mov: HashMap<(u8, u8), Index> = (&entities, &movables, &positions)
                   .join()
                   .map(|t| ((t.2.x, t.2.y), t.0.id()))
                   .collect::<HashMap<_, _>>();
               let immov: HashMap<(u8, u8), Index> = (&entities, &immovables, &positions)
                   .join()
                   .map(|t| ((t.2.x, t.2.y), t.0.id()))
                   .collect::<HashMap<_, _>>();

               // now iterate through current position to the end of the map on
               // correct axis
               let (start, end, is_x) = match key {
                   KeyCode::Up => (position.y, 0, false),
                   KeyCode::Down => (position.y, MAP_HEIGHT, false),
                   KeyCode::Left => (position.x, 0, true),
                   KeyCode::Right => (position.x, MAP_WIDTH, true),
                   _ => continue,
               };

               let range = if start < end {
                   (start..=end).collect::<Vec<_>>()
               } else {
                   (end..=start).rev().collect::<Vec<_>>()
               };

               for x_or_y in range {
                   let pos = if is_x {
                       (x_or_y, position.y)
                   } else {
                       (position.x, x_or_y)
                   };

                   //find a movable
                   //if it exist, we try to move and continue
                   // if it doesn't we continue to try to find an immovable instead

                   match mov.get(&pos) {
                       Some(id) => to_move.push((key, id.clone())),
                       None => {
                           //find an immovable
                           // if it exist we need to stop and not move anything
                           //if it doesn't exist we stop because we found a gap
                           match immov.get(&pos) {
                               Some(id) => to_move.clear(),
                               None => break,
                           }
                       }
                   }
               }
           }
       }

       //now let's move what need to be moved
       for (key, id) in to_move {
           let position = positions.get_mut(entities.entity(id));
           if let Some(position) = position {
               match key {
                   KeyCode::Up => position.y -= 1,
                   KeyCode::Down => position.y += 1,
                   KeyCode::Left => position.x -= 1,
                   KeyCode::Right => position.x += 1,
                   _ => (),
               }
           }
       }
   }
}
  1. What is meant by "iterating on the correct axis"?
 let (start, end, is_x) = match key {
                    KeyCode::Up => (position.y, 0, false),
                    KeyCode::Down => (position.y, MAP_HEIGHT, false),
                    KeyCode::Left => (position.x, 0, true),
                    KeyCode::Right => (position.x, MAP_WIDTH, true),
                    _ => continue,
                };
  1. what are we doing with those vectors?
let range = if start < end {
                    (start..=end).collect::<Vec<_>>()
                } else {
                    (end..=start).rev().collect::<Vec<_>>()
                };
  1. what are we doing with those tuples?
for x_or_y in range {
                    let pos = if is_x {
                        (x_or_y, position.y)
                    } else {
                        (position.x, x_or_y)
                    };
  1. how does she proceeds with movables and immovables here?
 match mov.get(&pos) {
                        Some(id) => to_move.push((key, id.clone())),
                        None => {
                            //find an immovable
                            // if it exist we need to stop and not move anything
                            //if it doesn't exist we stop because we found a gap
                            match immov.get(&pos) {
                                Some(id) => to_move.clear(),
                                None => break,
                            }
                        }
                    }

I am aware I should probably sleep on this and reread with a fresh head tomorrow morning but I understood a lot from discussing with you, so I would be very grateful if someone could share their insights :smiley:

Well just a quick one - this comment refers to this iteration as far as I get the code:

Within this iteration the first if expressions fills the tupel pos. This tupel is then used with the function move.get. The result of this function is pattern matched, indicating whether at the position a moveable could be detected. If this is the case it will be pushed to a hashmap.

Once this iteration is over the hashmap to_move is processed and based on key input the position is updated for the respective entity.

Hope this still high level explanations sheds a bit of a light...

1 Like

I'll take a stab :slight_smile:

range will be a Vec. (start..=end) is a Range, which is basically an iterator from start to end (inclusive because of the =).
It would be an error if start > end (Range forbids it), hence the if statement. But for whatever reason the author wanted the Vec to count down if start > end. They get that by using rev(), an iterator method.

So, this range Vec has the numbers from start to end, e.g. vec![1,2,3] or vec![3,2,1]

Then the loop with x_or_y happens... Is it starting to make more sense?

1 Like

Let me have a try as well :slight_smile:

First of all, is_x is a bool value designating whether we are moving along the x-axis (left/right) or the y-axis (up/down):

let (start, end, is_x) = match key {
    KeyCode::Up => (position.y, 0, false),
    KeyCode::Down => (position.y, MAP_HEIGHT, false),
    KeyCode::Left => (position.x, 0, true),
    KeyCode::Right => (position.x, MAP_WIDTH, true),
    _ => continue,
};

As explained by @drmason13, in a movement along a specific axis, one of the coordinates is stationary, and the other (x_or_y) is updated by the loop for x_or_y in range. For example, to move left from the point (5, 4), the path would be (4, 4), (3, 4), etc.; the y-coordinate is stationary. Therefore, is_x is true, and x_or_y loops over [5, 4, 3, 2, 1, 0].

Now the meaning of the tuple should be clearer — it's just the coordinate corresponding to the current iteration of the loop; (5, 4), (4, 4), etc. in the example above.

1 Like

This one is related to the mechanism of Sokoban. Here's a partial explanation. In each move,

  • If the target position (the position the player is facing) is empty, then the player moves to that position.

  • Otherwise, if the target position is an immovable object (wall), the move fails.

  • Otherwise, the target position is a movable object. Consider the longest chain of adjacent movable objects in the direction the player is facing.

    • If the chain is ended by an empty space, then every object in the chain is moved.

    • Otherwise, if the chain is ended by an immovable object, then the move fails.

The first two rules above are actually special cases of the last rule where the chain is empty. In the code, the vector to_move collects the aforementioned chain of movable objects (with the player included). In each step of iteration, match the object at the current position:

  • movable => add it to the chain, and continue iterating;

  • immovable => the move fails, so the chain is cleared (since nothing can be moved) and the loop terminated; *

  • empty => the move succeeds, so the chain is kept (later handled by the loop that performs the actual moving) and the loop terminated.

* Note: the code contains a bug here — it does not break out of the loop in the immovable case, causing the iteration to erroneously continue. For example, attempting to move right in the situation P W B . will result in P W . B. This bug has been reported.

2 Likes

Thank you very much everyone :pray:t5:! I can obviously cannot choose an answer, just wanted to say that it was very helpful to read all your posts.
Thanks for also explaining basic syntax, and informing about the bug.

(so there's a bit of light but I am still digesting!)

hmmm ... why cloning the id?

id is a reference to specs::world::Index, which is an alias for u32. Simply dereferencing id works as well. I guess the reason for this explicit .clone() is that indexes are expected to be cheap to clone but not necessarily Copy; the fact that it is Copy is an implementation detail perhaps.

Personally, I would directly use *id.

2 Likes

It does look more natural. Will also dereference.

Can I bother you(s) again?
Why do we have an empty struct but a complete implementation? I can't fathom having an implementation for an empty object (probably incorrect terminology).

Empty structs are permitted in Rust, and {} is a complete definition of an empty struct. An empty struct is mostly treated the same way as non-empty structs (barring special treatment of zero-sized types), and they are often used for types that aren't associated with data, like systems.

1 Like

Make sense. That's why we were having type System = {} later inside of the implementation.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.