0
\$\begingroup\$

I'm developing a MMO RPG using Rust and bevy both in the client and the server, which means that I'm using ECS on both sides. My game is tile based, so each tile is a point in my map in the server, like this:

enter image description here

I am trying to implement a movement system which works with a movement speed, but all the calculations are being done in the server and it works fine, my problem here is in the client. The problem is that, when the user clicks to move in the screen, I am sending a message to the server to set a path to that destination, and since during the path things can occur, like, players walking through or enemies, I'm sending one tile per time that the player should move after the amount of time based on the move speed. So let's say that the player can move 1 tile per second, if I set a path that has 10 tiles, it will take 10 seconds to walk.

The problem happens that sometimes in the client, the player ends up with a transform that is not aligned with the tile, like if it is losing some pixels during the walk animation. At first I thought the server is sending messages faster than the client expects and it starts moving from a point in the middle of the way to other tile, so every time the server sends a message to walk to a direction, I store that in a queue and only starts the next move once the first is done. But this didn't solve the issue and I've been trying to figure this out for days now and I just don't know how to do this.

Each frame I update the translation a little bit based on the move speed, but at the end the translation must be a multiple of 32 since my game is all 32x32 pixels. I will put some code here, but I think the problem is not the code but the way that I'm doing things, so I would like a new idea.

for (mut transform, entity, moving, mut sprite, movement, mut direction, mut moves) in
    query.iter_mut()
{
    let mut should_move = false;

    if let Some(moving) = moving {
        if (transform.translation.x < moving.final_destination.translation.x - 2.
            && moving.x_move > 0.)
            || (transform.translation.x > moving.final_destination.translation.x + 2.
                && moving.x_move < 0.)
            || (moving.y_move > 0.0
                && transform.translation.y > moving.final_destination.translation.y + 2.)
            || (moving.y_move < 0.0
                && transform.translation.y < moving.final_destination.translation.y - 2.)
        {
            let constant = 32. / ((1. / movement.0 as f32) * 200.);

            transform.translation = transform.translation.add(Vec3::new(
                moving.x_move * constant,
                -moving.y_move * constant,
                0.0,
            ));
        } else {
            if sprite.index % 3 == 0 {
                sprite.index += 1;
            } else if sprite.index % 3 == 2 {
                sprite.index -= 1;
            }

            transform.translation.y = moving.final_destination.translation.y;
            transform.translation.x = moving.final_destination.translation.x;

            for (entity, t) in destination_query.iter() {
                if t.translation.x == transform.translation.x
                    && t.translation.y + 10.0 == transform.translation.y
                {
                    commands.entity(entity).despawn_recursive();
                }
            }

            should_move = true;
        }
    } else {
        should_move = true;
    }

    if should_move {
        if moves.0.len() > 0 {
            let m = moves.0.remove(0);

            commands.entity(entity).insert(m.clone());

            if m.x_move > 0.0 {
                *direction = Direction::Right;
                sprite.index = 7;
            } else if m.x_move < 0.0 {
                *direction = Direction::Left;
                sprite.index = 4;
            } else if m.y_move < 0.0 {
                *direction = Direction::Up;
                sprite.index = 10;
            } else {
                *direction = Direction::Down;
                sprite.index = 1;
            }

            commands
                .entity(entity)
                .insert(AnimationTimer(Timer::from_seconds(
                    0.06 / movement.0 as f32,
                    TimerMode::Repeating,
                )));
        } else {
            commands.entity(entity).remove::<Moving>();
            commands.entity(entity).remove::<AnimationTimer>();
        }
    }
}

Basically when I receive the message from the server, I add to the Moves component the next move, so the Moves component is actually the queue. Then I check if it's moving already, if so, update the translation based on the move speed until reaches the final translation, which should be the next tile. This is the part that doesn't work. The bottom part of the code is just updating the sprite and getting the next move from the queue.

And to show what I mean that it loses some pixels during the animation, look the image below, the skeleton has the right alignment to the tile, see that the player is just a little bit off to the left?

enter image description here

This doesn't happen all the time, but happens very often, like 50% of the times, but I still can't figure why this is happening.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

Typically in a server authoritative game simulation the server would send or replicate the state of the object and the client would simulate to achieve that state.

So for example here might be some flow:

  1. client sends message to move to a position
  2. server receives message and begins moving the object
  3. server replicates the authoritative position of the object
  4. the client now simulates movement using the same speed that the server uses to move the client to that position
  5. the server continually sends updates about the next position and the client continually moves to achieve that state.

There are of course many types of variations to this, such as both client/server doing simulation at the same time, and then the client upon receiving a server update works to correct it's simulation to match.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .