I have a game: www.ball3d.com
It has an input lag, because I don’t use any client side prediction at the moment.
I am going to implement this now.
Questions:
Is there any chance that we will have in the future something like Rigidbody.Simulate (it could work similar to this: Unity - Scripting API: ParticleSystem.Simulate) ? Anyone from Unity Team can say anything about this? There are some other topics about this and people would love to see something like that. It would be useful in many ways.
technically there isn’t a global button for turning on and off the simulations but you can make them sleep and wake them up if you want them to react and even turn on and off collisions and some people simply just make the rigidbodies kinematic. just look at the different functions in the scripting manual.
rigidbody.Wake(); and yes it’s not a joke you can make rigidbodies wake up and go to sleep.
Rigidbody has Sleep() and WakeUp() methods which are used to optimize physics performance.
If your Rigidbody sleeps it will not be processed (and will therefore not move).
Invoke Sleep() on a Rigidbody manually and it will fall asleep (stop processing) for at least one frame.
If you call WakeUp() on it, it will be forced to simulate again.
However, in case it is sleeping it will automatically wake up if there’s a collision.
In short: You can use Sleep() to make it stop simulations for a bit. You can then also use obj.WakeUp() as substitute for your proposed Simulate() method.
If you wrap that up in some controller script it might even be a nice solution.
About the client-side prediction, have you tried the Networking examples over at the asset store?
There’s a thing called NetworkRigidbody along the files, which is used to do prediction on rigidbody objects.
That might help you out.
Collisions are the main problem. Fortunately I was able to make my own version of PhysicsSimulation (I use Rigidbody.SweepTest() to detect collisions).
I have to make further tests (I will post this function here later). It doesn’t work exactly like the built-in physics, but it should be enough for prediction.
I was able to make the client side physics prediction. Here is how:
The client works on higher Time.timeScale all the time (or at least for one frame after a new packet arrive from the server). If you want to simulate 500ms (0.5sec) into the future, you should use this equation to calculate what the timeScale should be: (timeScale = 0.5 / Time.fixedDeltaTime).
When the timeScale is higher, the FixedUpdate function is called many times each frame. I set the initial position in the special script that is first in the script order. Then I remember position, rotation, velocity and angular velocity from each FixedUpdate, I also remember timeStamp. First frame has timeStamp = 0, the second one has timeStamp equal to the previous one plus Time.fixedDeltaTime. Thanks to this I have informations about rigidbody 500ms into the future.
Then in the Update function (which is called still once per frame, even though the timeScale is higher), I use the positions from above and set the final position of the object before rendering (I can for example set it 200ms into the future). I am doing interpolation to make it as smooth as possible.
Because the client works at a higher speed, you may need to make your own delta, to use it in the places, when the time should pass normally. You can do this using Time.realTimeSinceStartup.
This is basically how it works. It’s easier than it sounds.
Actually the best way of doing it is to do the calculations on server and hold clients back 200ms or 300ms instead of doing simulation on client for prediction.
You know, The floating point simulations can become quite different many times and then snapping the values back will produce not great results.
The whole point of prediction is to not hold the client back. I already have what you described, the game is waiting for two states from the server and then is interpolating between them. It causes the input lag, that’s why I am doing the prediction. Input prediction has to be done on the client.
I am not sure how your method would work, but the way I am doing this now is quite accurate.
But, on the topic of the thread: I’ve found it incredibly hard to use the built in Unity PhysX solution, as it does not offer enough control for you to be able to rewind/correct positions of the local player. I make my player character a kinematic rigidbody, and then control him manually using raycasts, etc, giving me full control and allowing proper client prediction + smoothed correction and by keeping your own, manual fixed time step you can get rid of all silly temporal artifacts also :
@tholm
We are talking on physics based gameplay which u are controlling a ball in or …
As u said the best way is to use a deterministic physX system or at least just have a physics engine that works with reversed time steps.
in good network with ping of 30ms and added 100 ms you’ll have 130ms delay and it’s not a great thing but might be acceptable in many online games and also is better that constantly predicting and correcting/snapping in a huge amount.
This is the closest thing I could find for proper network prediction for Unity physics that don’t involve re-writing the physics engine, but due to lack of experience I’ll probably have a hard time implementing it without any working example to look at, and I haven’t found this concept mentioned anywhere else.
I was working on this for more than two weeks, but in the end I decided not to use it in the game for now. It’s because when someone kicked the ball, it changed the direction rapidly and since it’s not predictable, players (testers) have noticed that there was something wrong. I was trying lerps and other stuff, but when I solved one problem, the other one came out. I will probably add this in the future as an option.
I will try to give you the code with the simulation part (the network part would not help you, since I was doing it just to test the concept, it’s one big mess, but it’s working pretty well).
So as far as I remember (it was some time ago :)) the idea is to keep the client running at a higher timeScale. It means that all the Time.deltaTime will not work so you have to make your own delta:
Thanks to the increased timeScale, every FixedUpdate function will be called many times, but the Update function will be called only once after all the FixedUpdates. The execution order is the key here.
My example works only for one object. If you want to make it work for more, then you need to add another ballRigidbodyStates variable.
You need to create special script, called FirstScript:
#pragma strict
function Update()
{
GameManager.instance.RigidbodySimulationUpdate();
}
function FixedUpdate()
{
GameManager.instance.RigidbodySimulationFixedUpdate();
}
After creating it, go to Edit->Project->Script Execution Order. In the inspector add that FirstScript and make it running before the Default Time (so put it above, with a negative number like -100). Then attach that FirstScript to some GameObject
Here is the code (I am copying and pasting from different files, I hope it will work):
(You can past this to the FirstScript if you want.)
@System.NonSerialized rigidbodyStatesCount : int = 50;
@System.NonSerialized var ballRigidbodyStates : CRigidbodyState[];
@System.NonSerialized var rigidbodyTimestamp : double = 0.0;
@System.NonSerialized var rigidbodyTimestampTmp : double = 0.0;
class CRigidbodyState
{
var position : Vector3;
var rotation : Quaternion;
var velocity : Vector3;
var angularVelocity : Vector3;
var timestamp : double;
}
//call this once in Awake()
function InitRigidbodySimulation()
{
ballRigidbodyStates = new CRigidbodyState[rigidbodyStatesCount];
for (var i = 0; i < ballRigidbodyStates.length; i++)
{
ballRigidbodyStates[i] = new CRigidbodyState();
}
}
//You will call this function to prepare your rigidbody object, so give this function the initial position, rotation, velocity and angular velocity.
private var prepareRigidbodyBallPosition : Vector3;
private var prepareRigidbodyBallRotation : Quaternion;
private var prepareRigidbodyBallVelocity : Vector3;
private var prepareRigidbodyBallAngularVelocity : Vector3;
function PrepareRigidbodyBall(position : Vector3, rotation : Quaternion, velocity : Vector3, angularVelocity : Vector3)
{
prepareRigidbodyBallPosition = position;
prepareRigidbodyBallRotation = rotation;
prepareRigidbodyBallVelocity = velocity;
prepareRigidbodyBallAngularVelocity = angularVelocity;
}
function SetRigidbodyTimestamp(timestamp : double)
{
rigidbodyTimestampTmp = timestamp;
}
function RigidbodySimulationUpdate()
{
currentRigidbodyState = 0;
}
function RigidbodySimulationFixedUpdate()
{
if (Network.isClient !ReplayManager.instance.isPlaying (!GameManager.instance.localPlayer || GameManager.instance.localPlayer.team != Team.Spec))
{
if (currentRigidbodyState == 0)
{
validRigidbodyStates = 0;
rigidbodyTimestamp = rigidbodyTimestampTmp;
if (!GameManager.instance.ballObject.rigidbody.isKinematic) //przez chwile po zmianie ze specta zle bedzie, wiec trzeba sprawdzac
{
GameManager.instance.ballObject.transform.position = prepareRigidbodyBallPosition;
GameManager.instance.ballObject.transform.rotation = prepareRigidbodyBallRotation;
GameManager.instance.ballObject.rigidbody.velocity = prepareRigidbodyBallVelocity;
GameManager.instance.ballObject.rigidbody.angularVelocity = prepareRigidbodyBallAngularVelocity;
}
}
if (currentRigidbodyState < rigidbodyStatesCount)
{
if (GameManager.instance.ballObject)
{
ballRigidbodyStates[currentRigidbodyState].position = GameManager.instance.ballObject.transform.position;
ballRigidbodyStates[currentRigidbodyState].rotation = GameManager.instance.ballObject.transform.rotation;
ballRigidbodyStates[currentRigidbodyState].velocity = GameManager.instance.ballObject.rigidbody.velocity;
ballRigidbodyStates[currentRigidbodyState].angularVelocity = GameManager.instance.ballObject.rigidbody.angularVelocity;
if (currentRigidbodyState == 0)
ballRigidbodyStates[currentRigidbodyState].timestamp = 0;
else
ballRigidbodyStates[currentRigidbodyState].timestamp = ballRigidbodyStates[currentRigidbodyState - 1].timestamp + Time.fixedDeltaTime;
}
currentRigidbodyState++;
validRigidbodyStates = currentRigidbodyState;
}
}
}
OK now as you can see I am using GameManager.instance.ballObject, you should change this to your own rigidbodyObject, the one you want to simulate. It should be non-Kinematic.
If this code will work, then you will have the predicted states of your rigidbody in the array ballRigidbodyStates.
Now in the Update function you can interpolate between these states and use it to change the position of your rigidbody. You can even use the same rigidbody you have used in the simulation, it doesn’t matter.
Here is an example:
#pragma strict
private var testTimer : float = 0.1;
private var currentIndex : int = 0;
function Update()
{
if (Input.GetKey(KeyCode.C))
{
GameManager.instance.ballObject.transform.position = Vector3(0, 0.4, 0);
GameManager.instance.ballObject.rigidbody.velocity = Vector3(0.1, 0.1, 0);
}
if (Input.GetKey(KeyCode.Z))
{
testTimer -= 0.03 * StaticVariables.delta;
if (testTimer < 0.1)
testTimer = 0.1;
}
if (Input.GetKey(KeyCode.X))
{
testTimer += 0.03 * StaticVariables.delta;
}
//Debug.Log("testTimer: " + testTimer);
if (Input.GetKey(KeyCode.V))
{
currentIndex--;
if (currentIndex < 0)
currentIndex = 0;
}
if (Input.GetKey(KeyCode.B))
{
currentIndex++;
}
if (currentIndex >= GameManager.instance.validRigidbodyStates)
currentIndex = GameManager.instance.validRigidbodyStates - 1;
if (GameManager.instance.validRigidbodyStates > 0)
{
var extrapolationLength : float = testTimer;
for (var i : int = 0; i < GameManager.instance.validRigidbodyStates; i++)
{
//Debug.Log(GameManager.instance.ballRigidbodyStates[i].timestamp);
if (GameManager.instance.ballRigidbodyStates[i].timestamp >= extrapolationLength || i == GameManager.instance.validRigidbodyStates - 1)
{
var lhsBall : CRigidbodyState = GameManager.instance.ballRigidbodyStates[Mathf.Max(i - 1, 0)];
var rhsBall : CRigidbodyState = GameManager.instance.ballRigidbodyStates[i];
//Use the time between the two slots to determine if interpolation is necessary
var length : double = rhsBall.timestamp - lhsBall.timestamp;
var t : float = 0.0F;
if (length > 0.0001)
t = (extrapolationLength - lhsBall.timestamp) / length;
GameManager.instance.ballObject.transform.position = Vector3.Lerp(lhsBall.position, rhsBall.position, t);
GameManager.instance.ballObject.transform.rotation = Quaternion.Slerp(lhsBall.rotation, rhsBall.rotation, t);
break;
}
}
}
GameManager.instance.PrepareRigidbodyBall(Vector3(6, 0.4, 0), Quaternion.identity, Vector3(10, 10, 0), Vector3(4, 6, 1));
}
I hope it will help you. It may look weird, but this is all quite simple
The “kick” happens once in a few second, so it’s not that often I will probably make it as an option in the menu. The game works good enough if your ping is smaller than 100 for good players even 120 is ok. But it’s not possible to play on the ping above 200. And here is when someone could turn this code on.
I have to rethink all this. I don’t like this timeScale to be that high at all. It may have negative impact on performance, I would have to optimize all this.
Maybe I will not do anything for now and wait. The game is fun without it. The only problem is that it’s hard to play intercontinental games
I would like to inform you that I have improved the method described by me in this topic and was able to implement a fully working client side prediction using Unity builtin physics engine. If you want to see it in action, go to www.ball3d.com
It took me more than 3 months of hard work to implement it and a lot of time to even figure out how it all should work. If you don’t need a realistic physics in your game, the best option is not to use this method. But if you are desperate enough like I was, you can read a few clues how to do this:
Client is working at Time.timeScale = 100 all the time
Setting Time.fixedDeltaTime to 99999 breaks the physics loop, but you have to set it to a normal fixed delta time before the next frame.
You should not use OnCollisionStay and other functions like that, because they allocate memory and when your game will do the physics for example 15 times per frame, then it will add up, so you have to overcome this somehow, I did this by creating my own optimized collision detection, using simple ifs and distance function.
Particles will no longer be working as you want, you have to use Simulate() function to go around this.
Animation will no longer work as you want, so again you have to use some tricks to overcome this.
Other things you need to know are described in my previous posts.
It’s working perfectly and it’s even possible to optimize it to a point when it’s working as it should without any fps drops.
So if you have no other choice this is probably your only chance to do this. But I don’t recommend it, it’s only for desperate people
I wrote this post just to let you know that it’s possible.