Jump to content
  • Advertisement
Sign in to follow this  
DrDeath3191

Client Side Prediction and Server Reconciliation When Sending Player Speeds

Recommended Posts

Hello everyone,

So I have finally gotten around to implementing client side prediction. It seemed rather simple at first; set the state on the client to what it was at the tick the server sends back, then re-simulate your inputs. I've read the likes of Gabriel Gambetta, and I'm really close to something that works.However, I think my math is off or I am misunderstanding something.

You see, the entity the client controls moves essentially at a constant velocity based on input. Most examples of client side prediction, as far as I can tell, implement movement instructions as a series of 'nudges', i.e. "Move 10 to the left", followed by another message saying "Move 10 to the left". I didn't want to do this because A) I theoretically don't need to send that much information if I know when this linear movement started, and B) Using this strategy may allow multiple instructions to execute in the same frame, resulting in a small but noticeable hitch. So my solution was just to send a packet to the server saying "Move this portion of your top speed starting now". On the server side, this appears to work without a hitch.

Then's there's the client. Here's my strategy regarding reconciliation and prediction; as usual, I eliminate inputs from my prediction list that have already been executed by the server. However, as I do that I keep track of the last velocity at which the player was confirmed to be moving. When I reconcile, I start from the last acknowledged input frame, and move as far as I can at the saved velocity until I reach another input in my prediction list or the current frame number of the client. If I find another input, I change the velocity and the start frame and continue onward.

This, at least to me, seems like a sound means to handle both reconciliation and prediction. In fact it almost works. However, at surprising regularity the entity being predicted jitters around a bit. It looks as though it happens when I actually receive new state from the server. Specifically, it doesn't look like my prediction method is aware that now that I've received a new frame from the server, my predicted change in position needs to be smaller. But I have no clue what the math is to do this correctly. I'm sure its something quite obvious, but it currently eludes me. I've been struggling with this issue all week, and I could really use some help.

I apologize if I'm not clear at the moment; I'm a bit tired. If needed, I can provide clarifications tomorrow.

Thank you very much for your help.

Share this post


Link to post
Share on other sites
Advertisement

do you keep 2 positions for predicted entities? one true position, and one visual with an offset which is slowly reduced towards the true position?

on received update you snap the true position and calculate a new offset towards the position the entity is currently seen at on the client. that should reduce erratic position changes upon updates coming in.

Share this post


Link to post
Share on other sites
Posted (edited)
4 hours ago, ninnghazad said:

do you keep 2 positions for predicted entities? one true position, and one visual with an offset which is slowly reduced towards the true position?

on received update you snap the true position and calculate a new offset towards the position the entity is currently seen at on the client. that should reduce erratic position changes upon updates coming in.

No I do not. I didn't think it was necessary, and I still don't. Because the information I'm receiving from the server appears to be accurate and smooth; my prediction is what's jittering around. Ergo, I must be making some sort of mathematical error somewhere, or an errant assumption as to how this works.

I should have included the relevant source earlier, but here's the code I'm using for player movement, both on the server and on the client:

//Server Movement Code
//...

for (auto currentInput = inputsToHandle.begin(); currentInput != inputsToHandle.end(); currentInput++) {

  uint16_t inputCode = currentInput->inputCode;
  uint16_t playerIndex = inputCode & 0xff;
  uint16_t command = inputCode >> 8;

  //Player movement commands
  if (command == PLAYER_VELOCITY) {

    uint32_t endFrame = frameNumber;

    auto endMovement = std::find_if(currentInput + 1, inputsToHandle.end(), [inputCode](PlayerInputStruct other) {
      return other.inputCode == inputCode;
    });

    if (endMovement != inputsToHandle.end()) {
      endFrame = endMovement->frameNumber;
    }

    int duration = (int)(endFrame - currentInput->frameNumber);

    size_t transformIndex = transformTable.getEntityIndex(playerControllerTable.entities[playerIndex]);
    glm::vec3& position = transformTable.positions[transformIndex];
    position.x -= playerControllerTable.speeds[playerIndex] * currentInput->value * duration;


    playerTargetVelocities[playerIndex] = currentInput->value;

  }

  //Other, non-relvant inputs handled here...
  
}

for (size_t i = 0; i < playerControllerTable.getNumberOfComponents(); i++) {

  size_t transformIndex = transformTable.getEntityIndex(playerControllerTable.entities[i]);
  transformTable.positions[transformIndex].x -= playerControllerTable.speeds[i] * playerTargetVelocities[i];

}
  
  
//Client Code
//...
//selectionStruct contains information needed for prediction, including player entity ID numbers and speeds for players on this client

int snapDiff = currentSnapshot.snapshotNumber - previousSnapshot.snapshotNumber;

float alpha = 1.0f;

//The only reason snapDiff would be more than 1 is if we dropped a packet somewhere,
//in that case, we need to interpolate between the states
if (snapDiff > 1) {
  alpha = (std::min)((float)(frameNumber - previousSnapshot.snapshotNumber) / (snapDiff), 1.0f);
}

//reset player positions to where they were at the current snapshot
for (size_t i = 0; i < selectionStruct.numberOfPlayersOnClient; i++) {

  Entity currentPlayerEntity = selectionStruct.playerControllerEntityIDs[i];

  size_t transformIndex = transformTable.getEntityIndex(currentPlayerEntity);

  if (transformIndex == transformTable.getNumberOfComponents()) {
    continue;
  }

  auto prevTransform = std::find_if(previousSnapshot.getEntityTransformData().begin(), previousSnapshot.getEntityTransformData().end(),
[currentPlayerEntity](EntityTransformData data) {
  return data.entity == currentPlayerEntity;
});

  auto currTransform = std::find_if(currentSnapshot.getEntityTransformData().begin(), currentSnapshot.getEntityTransformData().end(),
[currentPlayerEntity](EntityTransformData data) {
  return data.entity == currentPlayerEntity;
});

  transformTable.positions[transformIndex] = ((1 - alpha) * prevTransform->position) + (alpha * currTransform->position);

}

//reconcile inputs not yet applied in this snapshot
std::vector<float> currentVelocities = lastConfirmedVelocities;
std::vector<uint32_t> velStartFrames = std::vector<uint32_t>(2, currentSnapshot.lastAcknowledgedInputNumber);

for (auto inputIt = predictionQueue.begin(); inputIt != predictionQueue.end(); inputIt++) {

  uint16_t inputCode = inputIt->inputCode;
  uint16_t playerIndex = inputCode & 0xff;
  uint16_t command = inputCode >> 8;
  float value = inputIt->value;

  if (command == PLAYER_VELOCITY) {

    int duration = (inputIt->frameNumber) - velStartFrames[playerIndex];

    Entity playerEntity = selectionStruct.playerControllerEntityIDs[playerIndex];

    size_t transformIndex = transformTable.getEntityIndex(playerEntity);

    if (transformIndex == transformTable.getNumberOfComponents()) {
      continue;
    }

    glm::vec3& position = transformTable.positions[transformIndex];
    position.x -= selectionStruct.playerSpeeds[playerIndex] * currentVelocities[playerIndex] * duration;

    currentVelocities[playerIndex] = inputIt->value;
    velStartFrames[playerIndex] = inputIt->frameNumber;

  }

}

for (size_t playerIndex = 0; playerIndex < selectionStruct.numberOfPlayersOnClient; playerIndex++) {

  int duration = frameNumber - velStartFrames[playerIndex];

  Entity playerEntity = selectionStruct.playerControllerEntityIDs[playerIndex];

  size_t transformIndex = transformTable.getEntityIndex(playerEntity);

  glm::vec3& position = transformTable.positions[transformIndex];

  position.x -= selectionStruct.playerSpeeds[playerIndex] * currentVelocities[playerIndex] * duration;

}

I think I boiled this down to the bare essentials to understand my problem; if you need more information, please let me know.

Edited by DrDeath3191

Share this post


Link to post
Share on other sites

Unfortunately, I have not made much in the way of progress. I keep looking for where I am specifically going wrong, but I'm just not seeing it. I have been printing out some debug information; maybe you guys can see something? A slight change made for this input; instead of velStartFrames being initialized with the last acknowledged input, I start it off with the last processed input, and I add an artificial "null" input every frame to make sure that number increments correctly. For the record, the position starts at 250, and moves with a velocity of 10 for 10 frames, so it should end up at position 350.


Player x position set to 250.000000 before prediction
Player x position set to 250.000000 in prediction queue
Input start: 57
Input end: 60
Duration: 3
Velocity Value: 0.000000
Player x position set to 260.000000 with prediction remainder
Next Frame Number: 61
Input Start: 60
Duration: 1
Velocty Value: -1.000000
Current Ack: 57
Previous Ack: 56
Current Snapshot: 60
Previous Snapshot: 59
Current Performed Input: 57
Previous Performed Input: 56
Previous X Position: 250.000000
-----------------------------------------------------
Player x position set to 250.000000 before prediction
Player x position set to 250.000000 in prediction queue
Input start: 58
Input end: 60
Duration: 2
Velocity Value: 0.000000
Player x position set to 270.000000 with prediction remainder
Next Frame Number: 62
Input Start: 60
Duration: 2
Velocty Value: -1.000000
Current Ack: 59
Previous Ack: 57
Current Snapshot: 61
Previous Snapshot: 60
Current Performed Input: 58
Previous Performed Input: 57
Previous X Position: 260.000000
-----------------------------------------------------
Player x position set to 250.000000 before prediction
Player x position set to 250.000000 in prediction queue
Input start: 59
Input end: 60
Duration: 1
Velocity Value: 0.000000
Player x position set to 280.000000 with prediction remainder
Next Frame Number: 63
Input Start: 60
Duration: 3
Velocty Value: -1.000000
Current Ack: 59
Previous Ack: 59
Current Snapshot: 62
Previous Snapshot: 61
Current Performed Input: 59
Previous Performed Input: 58
Previous X Position: 270.000000
-----------------------------------------------------
Player x position set to 260.000000 before prediction
Player x position set to 300.000000 with prediction remainder
Next Frame Number: 64
Input Start: 60
Duration: 4
Velocty Value: -1.000000
Current Ack: 61
Previous Ack: 59
Current Snapshot: 63
Previous Snapshot: 62
Current Performed Input: 60
Previous Performed Input: 59
Previous X Position: 280.000000
-----------------------------------------------------
Player x position set to 270.000000 before prediction
Player x position set to 310.000000 with prediction remainder
Next Frame Number: 65
Input Start: 61
Duration: 4
Velocty Value: -1.000000
Current Ack: 61
Previous Ack: 61
Current Snapshot: 64
Previous Snapshot: 63
Current Performed Input: 61
Previous Performed Input: 60
Previous X Position: 300.000000
-----------------------------------------------------
Player x position set to 280.000000 before prediction
Player x position set to 320.000000 with prediction remainder
Next Frame Number: 66
Input Start: 62
Duration: 4
Velocty Value: -1.000000
Current Ack: 63
Previous Ack: 61
Current Snapshot: 65
Previous Snapshot: 64
Current Performed Input: 62
Previous Performed Input: 61
Previous X Position: 310.000000
-----------------------------------------------------
Player x position set to 290.000000 before prediction
Player x position set to 330.000000 with prediction remainder
Next Frame Number: 67
Input Start: 63
Duration: 4
Velocty Value: -1.000000
Current Ack: 63
Previous Ack: 63
Current Snapshot: 66
Previous Snapshot: 65
Current Performed Input: 63
Previous Performed Input: 62
Previous X Position: 320.000000
-----------------------------------------------------
Player x position set to 300.000000 before prediction
Player x position set to 340.000000 with prediction remainder
Next Frame Number: 68
Input Start: 64
Duration: 4
Velocty Value: -1.000000
Current Ack: 64
Previous Ack: 63
Current Snapshot: 67
Previous Snapshot: 66
Current Performed Input: 64
Previous Performed Input: 63
Previous X Position: 330.000000
-----------------------------------------------------
Player x position set to 310.000000 before prediction
Player x position set to 350.000000 with prediction remainder
Next Frame Number: 69
Input Start: 65
Duration: 4
Velocty Value: -1.000000
Current Ack: 65
Previous Ack: 64
Current Snapshot: 68
Previous Snapshot: 67
Current Performed Input: 65
Previous Performed Input: 64
Previous X Position: 340.000000
-----------------------------------------------------
Player x position set to 320.000000 before prediction
Player x position set to 360.000000 with prediction remainder
Next Frame Number: 70
Input Start: 66
Duration: 4
Velocty Value: -1.000000
Current Ack: 66
Previous Ack: 65
Current Snapshot: 69
Previous Snapshot: 68
Current Performed Input: 66
Previous Performed Input: 65
Previous X Position: 350.000000
-----------------------------------------------------
Player x position set to 330.000000 before prediction
Player x position set to 360.000000 in prediction queue
Input start: 67
Input end: 70
Duration: 3
Velocity Value: -1.000000
Player x position set to 360.000000 with prediction remainder
Next Frame Number: 71
Input Start: 70
Duration: 1
Velocty Value: 0.000000
Current Ack: 67
Previous Ack: 66
Current Snapshot: 70
Previous Snapshot: 69
Current Performed Input: 67
Previous Performed Input: 66
Previous X Position: 360.000000
-----------------------------------------------------
Player x position set to 340.000000 before prediction
Player x position set to 360.000000 in prediction queue
Input start: 68
Input end: 70
Duration: 2
Velocity Value: -1.000000
Player x position set to 360.000000 with prediction remainder
Next Frame Number: 72
Input Start: 70
Duration: 2
Velocty Value: 0.000000
Current Ack: 68
Previous Ack: 67
Current Snapshot: 71
Previous Snapshot: 70
Current Performed Input: 68
Previous Performed Input: 67
Previous X Position: 360.000000
-----------------------------------------------------

 

Share this post


Link to post
Share on other sites

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

  • Advertisement
×

Important Information

By using GameDev.net, you agree to our community Guidelines, Terms of Use, and Privacy Policy.

GameDev.net is your game development community. Create an account for your GameDev Portfolio and participate in the largest developer community in the games industry.

Sign me up!