package org.jrenner.fps.entity; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.math.Vector3; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.TimeUtils; import org.jrenner.fps.Main; import org.jrenner.fps.Tools; import org.jrenner.fps.net.client.EntityFrame; import org.jrenner.fps.net.packages.EntityUpdate; public class EntityInterpolator { private Entity entity; private EntityFrame oldFrame = new EntityFrame(); private EntityFrame currentFrame = new EntityFrame(); private int lastInputTickFromServer; private EntityUpdate queuedUpdate; private long lastUpdateTime; public Vector3 posError = new Vector3(); public EntityInterpolator(Entity ent) { this.entity = ent; } public void handleUpdateFromServer(EntityUpdate upd, int serverInputTick) { synchronized (this) { queuedUpdate = upd; lastInputTickFromServer = serverInputTick; } } public void update() { if (queuedUpdate != null) { oldFrame.set(currentFrame); currentFrame.inputTick = lastInputTickFromServer; currentFrame.position.set(queuedUpdate.getPosition()); currentFrame.rotation.set(queuedUpdate.getRotation()); queuedUpdate = null; lastUpdateTime = TimeUtils.millis(); if (entity.getPlayer() != null) { posError.set(entity.getPosition()).sub(currentFrame.position); } } // use client prediction for self, interpolate all other entities if (Main.isClient() && Main.inst.client.player.entity == entity) { clientPrediction(); } else { interpolate(oldFrame, currentFrame); } } public float getInterpolationAlpha() { float alpha = (TimeUtils.millis() - lastUpdateTime) / Main.getNetClient().tickInterval; return MathUtils.clamp(alpha, 0f, 1.0f); } private static Vector3 tmp = new Vector3(); /** Interpolate position and rotation based on updates received from server */ private void interpolate(EntityFrame prev, EntityFrame next) { entity.getPosition().set(prev.position).lerp(next.position, getInterpolationAlpha()); tmp.set(prev.rotation).slerp(next.rotation, getInterpolationAlpha()); entity.setYawPitchRoll(tmp); } /** we store all old state to be replayed on top of server updates */ private Array<EntityFrame> clientFrames = new Array<>(); private Vector3 lastPosition = new Vector3(); private Vector3 nextPosition = new Vector3(); private void clientPrediction() { lastPosition.set(entity.getPosition()); EntityFrame frame = new EntityFrame(); frame.inputTick = Main.getNetClient().inputTick; Vector3 currentVel = tmp.set(entity.getVelocity()); frame.velocity.set(currentVel); clientFrames.add(frame); if (clientFrames.size > 200) { clientFrames.removeIndex(0); } // get rid of input frames older than authoritative server update for (int i = clientFrames.size - 1; i >= 0; i--) { EntityFrame cf = clientFrames.get(i); if (cf.inputTick <= currentFrame.inputTick) { clientFrames.removeIndex(i); } } // update to last authoritative server position entity.setPosition(currentFrame.position); // apply all client input that is newer than last server update for (EntityFrame cf : clientFrames) { entity.getVelocity().set(cf.velocity.x, cf.velocity.y, cf.velocity.z); float timeStep = 1f; entity.movement.applyVelocity(timeStep, false); } // finished applying client input, the result is our predicted position nextPosition.set(entity.getPosition()); // now restore pre-prediction velocity entity.setVelocity(currentVel); // now we interpolate from our last position to our new predicted position, to smooth out errors boolean interpEnabled = true; if (interpEnabled) { float alpha = 0.1f; entity.getPosition().set(lastPosition).lerp(nextPosition, alpha); } else { entity.setPosition(nextPosition); } } }