1 hour ago, hplus0603 said:
"interpolation durations are sometimes one frame short" is the kind of bug that's usually almost impossible to debug without having the actual code, knowing how it works, and stepping through it with known input data.
Do your server messages all have their own ticks in the message, and you use those ticks, not just assume they're exactly matching up with your client ticks?
The messages do indeed have their own tick numbers. I believe I am using the numbers correctly, but I'll put all the relevant code below to be sure.
Some things that might bare mentioning;
When I receive a snapshot from the server, I add the number of frames I intend to buffer it for to its snapshot number. Thus, I would ideally use that snapshot in that many frames, and interpolate in the event that I dropped any snapshots before it.
I am currently testing this on a local server, so actual latency should be near zero.
The error seems to occur in the first part of the reconciliation; sometimes it is only for a single frame, sometimes the entire duration of that movement. Either way, it's annoying and I wish it would stop.
Without further ado, the code;
void GameplayState::handleInput(std::vector<InputEvent> input) {
for (size_t i = 0; i < inputMappers.size(); i++) {
PlayerInputMapper currentMapper = inputMappers[i];
currentMapper.mapInputs(input);
float resultantVelocity = 0.0f;
for (PlayerInputChange change : currentMapper.getInputChanges()) {
uint16_t command = 255;
switch (change.input) {
case SUMMON_1:
command = SUMMON_1_START + (1 - (change.newValue >= .5f));
break;
case SUMMON_2:
command = SUMMON_2_START + (1 - (change.newValue >= .5f));
break;
case SUMMON_3:
command = SUMMON_3_START + (1 - (change.newValue >= .5f));
break;
case SUMMON_4:
command = SUMMON_4_START + (1 - (change.newValue >= .5f));
break;
case ITEM:
command = ITEM_START + (1 - (change.newValue >= .5f));
break;
case PAUSE:
if (change.newValue >= .5f) {
command = PAUSE_REQUEST;
}
break;
case MOVE_DOWN:
downVelocities[i] = change.newValue;
break;
case MOVE_UP:
upVelocities[i] = change.newValue;
break;
case SET_DOWN_VELOCITY:
resultantVelocity -= change.newValue;
break;
case SET_UP_VELOCITY:
resultantVelocity += change.newValue;
break;
}
//we set the command code, we should send it along
if (command != 255) {
uint16_t commandCode = (command << 8) | (uint8_t) i;
client.queuePlayerInput(commandCode, frameNumber);
predictionQueue.emplace_back(PlayerInputStruct(0, commandCode, frameNumber)); // packet number is not relevant to prediction, just set it to zero
}
}
resultantVelocity += upVelocities[i] - downVelocities[i];
resultantVelocity = (std::min)(1.0f, (std::max)(-1.0f, resultantVelocity));
if (resultantVelocity != resultantVelocities[i]) {
uint16_t commandCode = (PLAYER_VELOCITY << 8) | (uint8_t) i;
client.queuePlayerInput(commandCode, frameNumber, resultantVelocity);
resultantVelocities[i] = resultantVelocity;
predictionQueue.emplace_back(PlayerInputStruct(frameNumber, commandCode, frameNumber, resultantVelocity));
}
}
}
void GameplayState::update(GameTime time) {
client.sendPlayerInput();
client.update();
if (!client.isConnected()) {
game.getStateManager().popState();
return;
}
while (frameNumber > currentSnapshot.snapshotNumber && client.hasNextSnapshot()) {
uint32_t previousInputFrame = previousSnapshot.lastPerformedInputFrameNumber;
previousSnapshot = currentSnapshot;
currentSnapshot = client.getNextSnapshot();
currentDelta = SnapshotDelta(previousSnapshot, currentSnapshot);
applyNonInterpolatedDelta();
uint32_t lastInputFrame = previousSnapshot.lastPerformedInputFrameNumber;
auto lastProcessed = std::find_if(predictionQueue.begin(), predictionQueue.end(), [lastInputFrame](PlayerInputStruct& s) {
return s.frameNumber > lastInputFrame;
});
for (auto currentInput = predictionQueue.begin(); currentInput != lastProcessed; currentInput++) {
uint16_t playerIndex = currentInput->inputCode & 0xff;
uint16_t command = currentInput->inputCode >> 8;
if (command == PLAYER_VELOCITY) {
lastConfirmedVelocities[playerIndex] = currentInput->value;
}
}
predictionQueue.erase(predictionQueue.begin(), lastProcessed);
if (lastInputFrame != previousInputFrame) {
inputProcessedConfirmationFrame = lastInputFrame;
inputProcessedConfirmationSnapshot = previousSnapshot.snapshotNumber;
}
}
interpolateState();
doClientSidePrediction();
frameNumber++;
}
void GameplayState::interpolateState() {
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);
}
const std::vector<EntityPositionData>& positionUpdates = currentDelta.getUpdatedEntityPositionData();
std::vector<glm::vec3>& positions = world.transformTable.positions;
for (auto it = positionUpdates.begin(); it != positionUpdates.end(); it++) {
Entity currentEntity = it->entity;
size_t currentIndex = world.transformTable.getEntityIndex(currentEntity);
positions[currentIndex] = (it->previousPosition * (1.0f - alpha)) + (alpha * it->position);
}
const std::vector<EntityRotationData>& rotationUpdates = currentDelta.getUpdatedEntityRotationData();
std::vector<glm::quat>& rotations = world.transformTable.rotations;
for (auto it = rotationUpdates.begin(); it != rotationUpdates.end(); it++) {
Entity currentEntity = it->entity;
size_t currentIndex = world.transformTable.getEntityIndex(currentEntity);
rotations[currentIndex] = (it->previousRotation * (1.0f - alpha)) + (alpha * it->rotation);
}
const std::vector<EntityScaleData>& scaleUpdates = currentDelta.getUpdatedEntityScaleData();
std::vector<glm::vec3>& scales = world.transformTable.scales;
for (auto it = scaleUpdates.begin(); it != scaleUpdates.end(); it++) {
Entity currentEntity = it->entity;
size_t currentIndex = world.transformTable.getEntityIndex(currentEntity);
scales[currentIndex] = (it->previousScale * (1.0f - alpha)) + (alpha * it->scale);
}
}
void GameplayState::applyNonInterpolatedDelta() {
const std::vector<Entity>& removedTransforms = currentDelta.getRemovedTransformEntities();
for (auto it = removedTransforms.begin(); it != removedTransforms.end(); it++) {
world.transformTable.unregisterEntity(*it);
}
const std::vector<Entity>& removedTextures = currentDelta.getRemovedTextureEntities();
for (auto it = removedTextures.begin(); it != removedTextures.end(); it++) {
world.spriteTable.unregisterEntity(*it);
}
const std::vector<EntityTransformData>& fullTransforms = currentDelta.getFullTransformData();
for (auto it = fullTransforms.begin(); it != fullTransforms.end(); it++) {
world.transformTable.registerEntity(it->entity, it->parentEntity, it->position, it->rotation,
it->scale);
}
const std::vector<EntityTextureData>& updatedTextures = currentDelta.getUpdatedEntityTextureData();
for (auto it = updatedTextures.begin(); it != updatedTextures.end(); it++) {
world.spriteTable.registerEntity(it->entity, game.getTextureAssetLibrary().acquire(it->textureAssetID),
it->dimensions, it->origin);
}
}
void GameplayState::doClientSidePrediction() {
TransformComponentTable& transformTable = world.transformTable;
ShieldControllerComponentTable& playerTable = world.playerControllerTable;
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);
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);
}
for (size_t playerIndex = 0; playerIndex < selectionStruct.numberOfPlayersOnClient; playerIndex++) {
uint32_t endFrame = frameNumber;
auto search = std::find_if(predictionQueue.begin(), predictionQueue.end(), [playerIndex](PlayerInputStruct input) {
return input.inputCode == ((PLAYER_VELOCITY << 8) | playerIndex);
});
if (search != predictionQueue.end()) {
endFrame = search->frameNumber + BUFFER_FRAMES;
}
Entity currentEntity = selectionStruct.playerControllerEntityIDs[playerIndex];
size_t transformIndex = transformTable.getEntityIndex(currentEntity);
if (transformIndex >= transformTable.getNumberOfComponents()) {
continue;
}
uint32_t startFrame = frameNumber - 1;
int duration = endFrame - startFrame;
if (duration < 0) {
duration = 0;
}
float previousX = transformTable.positions[transformIndex].x;
transformTable.positions[transformIndex].x -= selectionStruct.playerSpeeds[playerIndex] * lastConfirmedVelocities[playerIndex] * duration;
}
for (auto it = predictionQueue.begin(); it != predictionQueue.end(); it++) {
uint16_t inputCode = it->inputCode;
uint16_t playerIndex = inputCode & 0xff;
uint16_t command = inputCode >> 8;
float value = it->value;
if (command == PLAYER_VELOCITY) {
uint32_t endFrame = frameNumber;
auto search = std::find_if(it + 1, predictionQueue.end(), [inputCode](PlayerInputStruct input) {
return input.inputCode == inputCode;
});
if (search != predictionQueue.end()) {
endFrame = search->frameNumber;
}
int duration = endFrame - it->frameNumber;
//TODO: this is stupid, find a better solution for this issue
if (it->frameNumber == currentSnapshot.lastPerformedInputFrameNumber) {
duration -= 1;
}
Entity currentEntity = selectionStruct.playerControllerEntityIDs[playerIndex];
size_t transformIndex = transformTable.getEntityIndex(currentEntity);
transformTable.positions[transformIndex].x -= selectionStruct.playerSpeeds[playerIndex] * value * duration;
}
}
for (size_t i = 0; i < selectionStruct.numberOfPlayersOnClient; i++) {
Entity currentEntity = selectionStruct.playerControllerEntityIDs[i];
size_t transformIndex = transformTable.getEntityIndex(currentEntity);
transformTable.positions[transformIndex].x -= selectionStruct.playerSpeeds[i] * resultantVelocities[i];
}
}
I'm pretty sure the error is here in the client; before I started to implement prediction stuff looked nice and smooth.
Thank you for your assistance! I will of course continue to investigate on my own.