Bug? Player::unpackUpdate reading into mHead
by Daniel Buckmaster · in Torque Game Engine · 09/03/2009 (6:30 am) · 8 replies
I've just done a merger of Player and AIPlayer, and fixed some odd jittering (I like to call it... the jitterbug). In Player::un/packUpdate is the following code:
That seems to be quite a key issue... solving the problem by writing normal floats seems to preclude this from it being a case of reading the wrong values. Is there an underlying problem in the read/writeSignedFloat method?
stream->writeSignedFloat(mHead.x / mDataBlock->maxLookAngle, 6); stream->writeSignedFloat(mHead.z / mDataBlock->maxLookAngle, 6);
mHead.x = stream->readSignedFloat(6) * mDataBlock->maxLookAngle; mHead.z = stream->readSignedFloat(6) * mDataBlock->maxLookAngle;I had a situation where mHead.x and mHead.z were both 0 on the server, but the client was reading them as small float values (around the order of 0.02). Changing these calls to stream->write(mHead.x/z) and readstream->(&mHead.x/z) solved the issue.
That seems to be quite a key issue... solving the problem by writing normal floats seems to preclude this from it being a case of reading the wrong values. Is there an underlying problem in the read/writeSignedFloat method?
About the author
Studying mechatronic engineering and computer science at the University of Sydney. Game development is probably my most time-consuming hobby!
#2
Yes. They cannot properly represent 0. Here's why:
To send a signed float (range -1 to 1), what writeSignedFloat does is to add one to that value and divide by 2. So [-1 to 1] becomes [0 to 2] becomes [0 to 1]. Then we send that [0 to 1] the same way we send unsigned floats: Multiply by the maximum int value representable in bitCount bits and send that int. So with a bitCount of 6, [0 to 1] becomes [0 to 63].
Then just reverse the process on the other side: [0 to 63] divided by 63 becomes [0 to 1], multiply by 2 for [0 to 2] and subtract 1 so you have [-1 to 1] again. (actually the real code uses a slightly different order, but the result is the same)
That sounds like a perfectly good solution. There is of course a loss of precision, increasing as we decrease the bitCount; but that is expected. -1 is -1, 1 is 1, and we have a variable number of steps in between. Indeed, this works just fine for unsigned floats. The problem is when sending signed floats, we have one additional expectation. A loss of precision is ok, but we still want -1 to be -1, 1 to be 1, and 0 to be 0. Except we're sending 0 as 0.5, and the number of steps is even. Whoops. 0.5 will never be represented with an even number of steps, ergo 0 will never come out as 0 when sending a signed float.
Example: float value 0 with 6 bits (6 bits gives us 0 to 63, which is 64 steps):
Write:
0 + 1 is 1, divided by 2 is 0.5, multiplied by 63 is 31.5
Floor that as we convert to an int and we send the value 31
Read:
31 / 63 * 2 - 1 is -0.0158730159
There's your 0.02
Fortunately there are two easy solutions. We can either subtract one from the maximum int value so that we have an odd number of steps, or we can send the absolute value of the float along with a bit to indicate sign.
I'm personally going with the second approach because reducing the step count by 1 is slightly wasteful.
So here's my fix:
09/03/2009 (12:31 pm)
Quote:Is there an underlying problem in the read/writeSignedFloat method?
Yes. They cannot properly represent 0. Here's why:
To send a signed float (range -1 to 1), what writeSignedFloat does is to add one to that value and divide by 2. So [-1 to 1] becomes [0 to 2] becomes [0 to 1]. Then we send that [0 to 1] the same way we send unsigned floats: Multiply by the maximum int value representable in bitCount bits and send that int. So with a bitCount of 6, [0 to 1] becomes [0 to 63].
Then just reverse the process on the other side: [0 to 63] divided by 63 becomes [0 to 1], multiply by 2 for [0 to 2] and subtract 1 so you have [-1 to 1] again. (actually the real code uses a slightly different order, but the result is the same)
That sounds like a perfectly good solution. There is of course a loss of precision, increasing as we decrease the bitCount; but that is expected. -1 is -1, 1 is 1, and we have a variable number of steps in between. Indeed, this works just fine for unsigned floats. The problem is when sending signed floats, we have one additional expectation. A loss of precision is ok, but we still want -1 to be -1, 1 to be 1, and 0 to be 0. Except we're sending 0 as 0.5, and the number of steps is even. Whoops. 0.5 will never be represented with an even number of steps, ergo 0 will never come out as 0 when sending a signed float.
Example: float value 0 with 6 bits (6 bits gives us 0 to 63, which is 64 steps):
Write:
0 + 1 is 1, divided by 2 is 0.5, multiplied by 63 is 31.5
Floor that as we convert to an int and we send the value 31
Read:
31 / 63 * 2 - 1 is -0.0158730159
There's your 0.02
Fortunately there are two easy solutions. We can either subtract one from the maximum int value so that we have an odd number of steps, or we can send the absolute value of the float along with a bit to indicate sign.
I'm personally going with the second approach because reducing the step count by 1 is slightly wasteful.
So here's my fix:
void BitStream::writeSignedFloat(F32 f, S32 bitCount)
{
// writeInt((S32)(((f + 1) * .5) * ((1 << bitCount) - 1)), bitCount);
writeFloat(mFabs(f), bitCount - 1);
writeFlag(f < 0.f);
}
F32 BitStream::readSignedFloat(S32 bitCount)
{
// return readInt(bitCount) * 2 / F32((1 << bitCount) - 1) - 1.0f;
return readFloat(bitCount - 1) * (readFlag() ? -1.f : 1.f);
}
#3
Thanks for the detailed explanation, Scott! I'll try that implementation. Another idea that springs to mind is to write a flag if the value is not zero - then write the entire value. This would save bits in the case that the value is zero, and get rid of the error I'm seeing.
Thanks again!
09/03/2009 (12:40 pm)
Stefan - the problem has come about after I started doing getAIMove on both server and client, instead of just doing it on the server and expecting the client to get the results. The serverside and clientside getAIMoves were fighting for control fo the character, since the client thought their head rotations were 0.02.Thanks for the detailed explanation, Scott! I'll try that implementation. Another idea that springs to mind is to write a flag if the value is not zero - then write the entire value. This would save bits in the case that the value is zero, and get rid of the error I'm seeing.
Thanks again!
#4
09/03/2009 (1:24 pm)
That would be an effective optimization if the value is frequently zero. Of course if the value is rarely zero then you're just sending extra bits. Personally I wouldn't do that in BitStream::read/writeSignedFloat as they are general purpose functions, but if you had a class like Item or Player where you found it was often sending 0s, that would be a good optimization to implement within that class.
#5
09/03/2009 (1:35 pm)
Any optimization in this case is best done later. Like Scott said, it could very well be non-zero most of the time and in that case you're actually wasting bandwidth.
#6
09/03/2009 (9:52 pm)
Quote:Personally I wouldn't do that in BitStream::read/writeSignedFloat as they are general purpose functions, but if you had a class like Item or Player where you found it was often sending 0s, that would be a good optimization to implement within that class.No, you're right - I would implement that in the specific classes where I was having trouble.
#7
09/04/2009 (1:44 pm)
fwiw, i encountered this same issue with sending floats-near-zero over the wire with small bitcounts, and ended up using the exact same approach.
#8
07/08/2012 (11:14 pm)
Thanks for the fix. This resolved a precision issue I've been chasing down for a couple days now.
Torque Owner Stefan Lundmark
If this problem isn't happening with the regular Player class, you need to take a look at your merge. AIPlayers receive lower priority and send less packets about, but you should get the same values just at a different rate.
Did you try increasing the bit precision from 6 bits to something higher? 0.02 seems a bit large to get lost with 6 bits of precision though so that shouldn't be the problem.