Issues with network usage and cheating using client side input prediction?

Started by
5 comments, last by hplus0603 12 years, 8 months ago
I've implemented a simple version of client side input prediction, allowing my player character to instantly move and then have the server "confirm" the move and re-send the 100% correct position to the client. I've read the information at these two resources:

And so far it seems to be working pretty nicely, the general scheme is this:

On every frame on the client, calculate desired movement vector from keys down, store this in a move_cmd struct, also store the exact time and the frame delta time, struct looks like this:


struct move_cmd {
float time;
float delta;
vector3 moveVector;
}



I then send of this to the server, and then store it in a list of yet to be verified commands. When I receive a response from the server, it contains the current position and the time of the move_cmd that caused the current position to be where it is. I go through the list of move_cmd objects and remove all of the ones that have time that is less then the server response. When it's time to move the model on the client screen i take the last verified server position and add all the moveVectors * delta to it, giving me a predicted position.

It seems to be working pretty well, I still need to interpolate when I predict wrong, but it seems to be working. Now to my two questions:


1) This causes the client and server to basically exchange movement information on every frame, I can compress it a bit and it'll end up to about 800bytes/s, maybe this isn't that much but still feels like a lot?
2) Since the server bases the calculation on the frame time delta given by the client in the message, it feels as if the client could modify the time delta it at will and cheat? Assume that the client puts some type of "man-in-the-middle" attack and changes the part of the message that represents the delta from 0.015 to 1.0 which would make the client run really fast. Is there some other timing/delta I should base the movement of instead?


Edit, addition to question #2: Should I use some kind of heuristic on the server to verify that the client has to move within certain "bounds" or otherwise label it as cheating and boot him, or should I base the movement speeds on some other delta/timing then the frame delta from the client?


And last, is this a good way to go about it or are there other ways of doing input prediction? I've read several articles on the subject (the source engine wiki for example), but none go into detail on how the actual sync:ing of the client and server is done but are mostly high level overviews. The only detailed one I found was the PDF linked earlier (http://web.cs.wpi.ed...mes/bernier.pdf) and that explains a model that is pretty much identical to the one I just explained and have implemented.
Advertisement
Also, a follow up question: The movement instructions sent from the client has to be sent over some reliable protocol (built on top of UDP), I assume? since packet loss would make the server miss a movement? Or should I (if the movement is missed) just force the client into the server position?

Also, a follow up question: The movement instructions sent from the client has to be sent over some reliable protocol (built on top of UDP), I assume? since packet loss would make the server miss a movement? Or should I (if the movement is missed) just force the client into the server position?



First, you should fix your physics time step. 30 Hz is a common value. Each input from the client should be quantized to this rate. That way, you can't adjust the time slot size to cheat.

Second, you don't necessarily need to send full state each time; you can send client input only, and the server can figure out what the movement should be.

Third, you can send the input for several previous ticks along with each packet, to let the server catch up if you miss some particular step. If you miss too many steps in a row, just drop the user. If the internet kills a second a connectivity, well, get better internet :-)
enum Bool { True, False, FileNotFound };

[quote name='fholm' timestamp='1313270445' post='4848747']
Also, a follow up question: The movement instructions sent from the client has to be sent over some reliable protocol (built on top of UDP), I assume? since packet loss would make the server miss a movement? Or should I (if the movement is missed) just force the client into the server position?



First, you should fix your physics time step. 30 Hz is a common value. Each input from the client should be quantized to this rate. That way, you can't adjust the time slot size to cheat.

Second, you don't necessarily need to send full state each time; you can send client input only, and the server can figure out what the movement should be.

Third, you can send the input for several previous ticks along with each packet, to let the server catch up if you miss some particular step. If you miss too many steps in a row, just drop the user. If the internet kills a second a connectivity, well, get better internet :-)
[/quote]

Ok, so I fixed my physics time step to 30Hz (or well, 0.03 sec, so 33.3333Hz). And while I'm getting the correct movement on both the client and server, the movement on the client was kind of "choppy" (as I was only reading the input and moving the character once every two frames (on average, since I render at 66fps most of the time) ). Maybe this is the wrong way to go about it?

The solution I came up with (since I can't depend on the delta time between frames to do the rendering was this):

I read the asked move vector in the physics step, like this (C# code):



Vector3 getMoveVector()
{
var moveVector = Vector3.zero;

if (Input.GetKey(KeyCode.U)) moveVector += Vector3.forward;
if (Input.GetKey(KeyCode.J)) moveVector += Vector3.back;
if (Input.GetKey(KeyCode.H)) moveVector += Vector3.left;
if (Input.GetKey(KeyCode.K)) moveVector += Vector3.right;

return moveVector.normalized * 0.2f;
}



Vector3 realPosition;
Vector3 wishedPosition;
public override void PhysicsStep ()
{

realPosition = transform.position = wishedPosition;
wishedPosition = transform.position + getMoveVector();
}


Basically I read the current wished move by the user, calculate a movement vector from it and then create a "wished position" by taking "current position" + "movement vector".
I then, in my Update function (that gets called every frame) Lerp between the current position and the wished position, like this:


public override void Update() {
// These three variables are calculated/set elsewhere
// just showing them here for clarity
var timeOfLastPhysicsStep = ...;
var currenTime = ...;
var physicsStepFrequency = 0.03f; (every 33.33333 ms)

// Calculate how much [0.0 ... 1.0] of the last physic step that has run "in frames"
// and render the position accordingly, giving us a smooth movement

var timeSincePhysicsStep = (currenTime - timeOfLastPhysicsStep) / physicsStepFrequency;
transform.position = Vector3.Lerp(realPosition, wishedPosition, timeSincePhysicsStep);

}


This gives me the following (or at least I think so)
  1. A fixed physics speed for the client, allowing me to calculate the correct movement on both the client and the server
  2. A smooth animation on the client, since I Lerp over the next frame(s) (usually 2, since I'm running at max 66 fps) to create a smooth movement of the character

So my only question is really: Is this the correct way to go about it? To both get movement that can be predicted on the client and then verified/changed by the server, and also have a smooth movement that doesn't "skip" because the update frequency is only 30Hz ?

Edit: Updated the Lerp from real to wished position over the rendered frames.
I wouldn't send a movement vector to the server as the only thing the client should have authority over are its input controls(which buttons are pressed).

Both the client scene and server scene should know how to update the position based on the input controls, so your outbound from the client should only be a bunch of boolean values which you could pack into a byte or so. (ofcourse it is common to allow the client to have authority over a direction facing vector as a result of the mouse input)

Then at each tick the server AND client update checks the state of the input boolean values and derives a movement vector for themselves and adds it on to position.

The inbound to the client will be a position and time, to which you will do your corrections

To smooth things out to your render framerate, just check how long since your last phys update occurred and interpolate the positions of objects according to how far they should have moved in that time.
Obviously to do that you will need a velocity (movement vector) so would be quickest to store the calculated movement vector on the client between phys ticks.

Regarding your second question,

You don't need a reliability layer on input because if for example:

You are holding forward on the client

client sending 30hz updates to server

1. forwardkey = true
2. forwardkey = true(packet lost)
3. forwardkey = false (packet lost)
4. forwardkey = false
5. forwardkey = true
6. forwardkey = true
...
30. forwardkey = true

the server will move you forward for ticks 1,2 and 3, stop moving you on 4, then start again on 5 etc.
the resultant positional difference from that 1 tick of incorrect buttons being pressed will be how far your player can travel in 1/30th of a second so smoothing that out once the server corrects you is no drama

Hope that helps

I wouldn't send a movement vector to the server as the only thing the client should have authority over are its input controls(which buttons are pressed).

Both the client scene and server scene should know how to update the position based on the input controls, so your outbound from the client should only be a bunch of boolean values which you could pack into a byte or so. (ofcourse it is common to allow the client to have authority over a direction facing vector as a result of the mouse input)

Then at each tick the server AND client update checks the state of the input boolean values and derives a movement vector for themselves and adds it on to position.

The inbound to the client will be a position and time, to which you will do your corrections

To smooth things out to your render framerate, just check how long since your last phys update occurred and interpolate the positions of objects according to how far they should have moved in that time.
Obviously to do that you will need a velocity (movement vector) so would be quickest to store the calculated movement vector on the client between phys ticks.

Regarding your second question,

You don't need a reliability layer on input because if for example:

You are holding forward on the client

client sending 30hz updates to server

1. forwardkey = true
2. forwardkey = true(packet lost)
3. forwardkey = false (packet lost)
4. forwardkey = false
5. forwardkey = true
6. forwardkey = true
...
30. forwardkey = true

the server will move you forward for ticks 1,2 and 3, stop moving you on 4, then start again on 5 etc.
the resultant positional difference from that 1 tick of incorrect buttons being pressed will be how far your player can travel in 1/30th of a second so smoothing that out once the server corrects you is no drama

Hope that helps


Thanks for an awesome response! Yeah I should probably have clarified that by movement vector i meant a vector representing the keys pressed down, the server does verify everything and do proper movement calculations (which the client also does, but the server obviously have precedence, and overrides the client if it detects an error).

Working on the final implementation now, using the two answers I've gotten, I will post again when I have a working solution.

So my only question is really: Is this the correct way to go about it? To both get movement that can be predicted on the client and then verified/changed by the server, and also have a smooth movement that doesn't "skip" because the update frequency is only 30Hz ?

Edit: Updated the Lerp from real to wished position over the rendered frames.


Yes, that's a pretty common way to do it. Check for example this reference on the canonical game loop.
enum Bool { True, False, FileNotFound };

This topic is closed to new replies.

Advertisement