Search Unity

  1. Unity 6 Preview is now available. To find out what's new, have a look at our Unity 6 Preview blog post.
    Dismiss Notice
  2. Unity is excited to announce that we will be collaborating with TheXPlace for a summer game jam from June 13 - June 19. Learn more.
    Dismiss Notice

Resolved Encoding Vector2 and Vector3 variables into single int or float and back

Discussion in 'Multiplayer' started by emotitron, Dec 28, 2016.

  1. emotitron

    emotitron

    Joined:
    Oct 9, 2016
    Posts:
    41
    I am starting to optimize a game for mobile pvp, and to that end I am looking to reduce my traffic as much as possible.

    The maps will rarely exceed a cube of (-50, -50, -50) to (50, 50, 50) units with a required resolution of 1 decimal point. Vector3 data for each projectile fire needs even less data. That means I could nicely fit my coordinates into 10bit chunks that give me a range of values from -51.2 to 51.2 for each axis.

    Integers are 32bits, and I need 30bits per vector.

    So my first easiest path seems to be to strip all V3 data down to a 3 digit integer (the decimal removed by multiplying times 10) and then crush all of that into a single integer for transmission, and then decode it back to a V3 on the receiving end.

    So before I go doing this, my question is - am I recreating someone else's work I can just download and can I save myself a couple hours of learning how to bitwise encode/decode this? If not, does anyone have some code snippets they would recommend for doing this efficiently? I am sure I will not be the last person looking to do this.
     
  2. Apparaten_

    Apparaten_

    Joined:
    Jul 9, 2013
    Posts:
    45
  3. emotitron

    emotitron

    Joined:
    Oct 9, 2016
    Posts:
    41
    It is intended to be people in a small space moving at higher than should work speeds for PvP over mobile, so if I can cut my V3 data in a third - it should be of value. It might not be noticeable on its own, but as I start piling up syncvars and command/rpcs every little bit is likely going to matter. I need every millisecond I can get. I also will be relying less on player authority than games of this time for hiding the latency. Player movement is likely going to be the only item under player authority - the rest is all server - so all latency will be felt and seen.

    I intend to squeeze every bit down as much as humanly possible. Thanks for the link, will add that to my reading when I attack this.
     
    Apparaten_ likes this.
  4. emotitron

    emotitron

    Joined:
    Oct 9, 2016
    Posts:
    41
    Code (CSharp):
    1.     public int encodeVector3ToInt(Vector3 v) {
    2.         //Vectors must stay within the -512 to 512 range per axis - no error handling coded here
    3.         //Add 512 to get numbers into the 0-1024 range rather than -512 to 512 range
    4.         //Multiply by 10 to save one decimal place from rounding
    5.         int xcomp = Mathf.RoundToInt((v.x * 10)) + 512;
    6.         int ycomp = Mathf.RoundToInt((v.y * 10)) + 512;
    7.         int zcomp = Mathf.RoundToInt((v.z * 10)) + 512;
    8.         return xcomp + ycomp * 1024 + zcomp * 1048576;
    9.     }
    10.     public Vector3 decodeVector3FromInt(int i) {
    11.         //Get the leftmost bits first. The fractional remains are the bits to the right.
    12.         // 1024 is 2 ^ 10 - 1048576 is 2 ^ 20 - just saving some calculation time doing that in advance
    13.         float z = Mathf.Floor(i / 1048576);
    14.         float y = Mathf.Floor ((i - z * 1048576) / 1024);
    15.         float x = (i - y * 1024 - z * 1048576);
    16.         // subtract 512 to move numbers back into the -512 to 512 range rather than 0 - 1024
    17.         return new Vector3 ((x - 512) / 10, (y - 512) / 10, (z - 512) / 10);
    18.     }
    This is my dirty working code to solve the problem. If you have a world that is smaller than 102.4x102.4x102.4 units and a resolution of .1 units is good enough - this will reduce your network traffic. If anyone has faster/cleaner code methods to do this same thing - PLEASE pass them along.

    There are also two unused bits on the left of the int. So they could be used to double the x & z limits to 204.8 units if your level is more wide than tall. Just replace the 1048576 and 1024 numbers with the appropriate 2^X result for the number of bits you want for each axis.
     
    Last edited: Dec 28, 2016
  5. emotitron

    emotitron

    Joined:
    Oct 9, 2016
    Posts:
    41
    The resolution was a bit low, the alternative is to use shorts as a cheap half-float. That knocks 12 bytes per tick down to 6 (plus whatever unet overhead there is in each serialized command).

    Code (CSharp):
    1.     [Command(channel = Channels.DefaultUnreliable)]
    2.     private void CmdSendPositionShort(short x, short y, short z)
    3.     {
    4.         _lastPosition.Set(
    5.             (float)x / 100f,
    6.             (float)y / 100f,
    7.             (float)z / 100f);
    8.     }
    9.  
    10.         CmdSendPositionShort (
    11.                 (short)(transform.position.x * 100),
    12.                 (short)(transform.position.y * 100),
    13.                 (short)(transform.position.z * 100));
     
  6. emotitron

    emotitron

    Joined:
    Oct 9, 2016
    Posts:
    41
    And my latest resting place for truncating vectors and encoding them into a single variable (and back). Note I am only using 3/4s of the ulongs 64bits. 16 bits per axis. The remaining unused flags I may use to pass other info later such things as animation states and other flags.

    Code (CSharp):
    1.     public ulong encodeVector3ToULong(Vector3 v) {
    2.         //Vectors must stay within the -320.00 to 320.00 range per axis - no error handling is coded here
    3.         //Adds 32768 to get numbers into the 0-65536 range rather than -32768 to 32768 range to allow unsigned
    4.         //Multiply by 100 to get two decimal place
    5.         ulong xcomp = (ulong)(Mathf.RoundToInt((v.x * 100f)) + 32768);
    6.         ulong ycomp = (ulong)(Mathf.RoundToInt((v.y * 100f)) + 32768);
    7.         ulong zcomp = (ulong)(Mathf.RoundToInt((v.z * 100f)) + 32768);
    8.         //Debug.Log ("comps " + xcomp + " " + ycomp + " " + zcomp);
    9.         return xcomp + ycomp * 65536 + zcomp * 4294967296;
    10.     }
    11.     public Vector3 decodeVector3FromULong(ulong i) {
    12.         //Debug.Log ("ulong " +i);
    13.         //Get the leftmost bits first. The fractional remains are the bits to the right.
    14.         // 1024 is 2 ^ 10 - 1048576 is 2 ^ 20 - just saving some calculation time doing that in advance
    15.         ulong z = (ulong)(i / 4294967296);
    16.         ulong y = (ulong)((i - z * 4294967296) / 65536);
    17.         ulong x = (ulong)(i - y * 65536 - z * 4294967296);
    18.         //Debug.Log (x + " " + y + " " + z);
    19.         // subtract 512 to move numbers back into the -512 to 512 range rather than 0 - 1024
    20.         return new Vector3 (((float)x - 32768f) / 100f, ((float)y - 32768f) / 100f, ((float)z - 32768f) / 100f);
    21.     }
    Hopefully this wasn't all for nothing - my understanding of what goes on with Syncvar behind the scenes is limited so I don't know if regular network compression would be condensing a V3 to a comparable size. Again, there must be a more efficient way of doing this than decimal math, but right now my concern is crushing network usage more than sparing the CPU.
     
    Stiffx likes this.
  7. abhayaagrawal

    abhayaagrawal

    Joined:
    Sep 30, 2016
    Posts:
    8
    @emotitron
    this works for me. but I am unable to understand why are you multiplying
    return xcomp + ycomp * 65536 + zcomp * 4294967296;
    in case of encoding.

    and in case of decoding how are you getting value of z and y

    1. ulong z = (ulong)(i / 4294967296);
    2. ulong y = (ulong)((i - z * 4294967296) / 65536);

      it will be great if you can explain the logic
     
  8. criyessei

    criyessei

    Joined:
    May 26, 2023
    Posts:
    1
    Thank you emotitron

    I have created a dynamic class by advancing your solution.


    Code (CSharp):
    1. abstract class Compressor
    2. {
    3.     public static UInt64 Encode(float[] values, int resolution)
    4.     {
    5.         sbyte parts = (sbyte)values.Length;
    6.         sbyte k = (sbyte)(64 / parts);
    7.         UInt32 halfKVal = 1U << (k - 1);
    8.  
    9.         float validAbsValue = (1 << (k - 1)) / resolution;
    10.         if (values.Any(val => Math.Abs(val) > validAbsValue))
    11.             throw new Exception(String.Format("Values({0}) must be between -{1} and {1} with resolution of {2}", string.Join(", ", values), validAbsValue, 1 / resolution));
    12.  
    13.         UInt64 sum = 0;
    14.         for (int i = 0; i < parts; i++)
    15.         {
    16.             UInt64 comp = (UInt64)(Mathf.RoundToInt(values[i] * resolution) + halfKVal);
    17.             sum += comp << (i * k);
    18.         }
    19.  
    20.         return sum;
    21.     }
    22.     public static float[] Decode(UInt64 sum, sbyte parts, int resolution)
    23.     {
    24.         sbyte k = (sbyte)(64 / parts);
    25.         UInt32 kVal = 1U << k;
    26.         UInt32 halfKVal = kVal >> 1;
    27.         UInt32 kValComplement = (kVal - 1);
    28.  
    29.         float[] values = new float[parts];
    30.  
    31.         for (int i = 0; i < parts; i++)
    32.         {
    33.             ulong comp = (sum >> (i * k)) & kValComplement;
    34.             values[i] = ((float)comp - halfKVal) / resolution;
    35.         }
    36.         return values;
    37.     }
    38. }

    Code (CSharp):
    1. sealed class Vector3Compressor1048_576 : Compressor // [-1048.576, 1048.576] (Resolution 0,001)
    2. {
    3.     public static readonly int resolution = 1000; // 3 digit
    4.     public static readonly sbyte parts = 3;
    5.     public static readonly float max =  1048.576f;
    6.     public static readonly float min = -1048.576f;
    7.  
    8.     public static UInt64 Encode(Vector3 vector)=> Encode(new float[] { vector.X, vector.Y, vector.Z }, resolution);
    9.     public static Vector3 Decode(UInt64 sum)
    10.     {
    11.         float[] values = Decode(sum, parts, resolution);
    12.         return new Vector3(values[0], values[1], values[2]);
    13.     }
    14. }
    15.  
    16. sealed class QuaternionCompressor1_6384 : Compressor // [-1.6384, 1.6384] (Resolution 0,0001)
    17. {
    18.     public static readonly int resolution = 10000; // 4 digit
    19.     public static readonly sbyte parts = 4;
    20.     public static readonly float max =  1.6384f;
    21.     public static readonly float min = -1.6384f;
    22.  
    23.     public static UInt64 Encode(Quaternion vector) => Encode(new float[] { vector.X, vector.Y, vector.Z, vector.W }, resolution);
    24.     public static Quaternion Decode(UInt64 sum)
    25.     {
    26.         float[] values = Decode(sum, parts, resolution);
    27.         return new Quaternion(values[0], values[1], values[2], values[3]);
    28.     }
    29. }
     
    Last edited: Dec 10, 2023