Optimizing Chunks [Distance]

Started by
11 comments, last by SpikeViper 8 years ago

Currently in my game I have gotten chunks to load and unload well - except for one part. In order to find how close a player is to chunks, the planet class that controls the chunks checks every chunks distance to figure out which to load first. I know there is probably a much better way to do this, because when planets over 1,000 chunks load, there is a VERY noticeable freeze when the distances are calculated.

Current code:


   public void FixedUpdate()
    {

        StartCoroutine(ChunkQueue());


        timer = timer + 1;

        if (timer > 20)
        {

            foreach (GameObject player in players)
            {

                int playerx = (int)player.transform.position.x;
                int playery = (int)player.transform.position.y;
                int playerz = (int)player.transform.position.z;

                    //Each chunk is 16x16 but since planets can have different sizes planetSize / 16 always returns the correct amount
?	            //Also for technical reasons planets start in the 1 position in the array, not 0
                    for (int x = 1; x <= planetSize / 16; x++)
                    {
                        for (int y = 1; y <= planetSize / 16; y++)
                        {
                            for (int z = 1; z <= planetSize / 16; z++)
                            {

                                float distance = Vector3.Distance(player.transform.position, new Vector3(this.transform.position.x + (x * 16), this.transform.position.y + (y * 16), this.transform.position.z + (z * 16)));

                                if (ischunkloaded[x, y, z] == true)
                                {
                                    if (distance > 200)
                                    {

                                        ischunkloaded[x, y, z] = false;
                                        UnloadChunk(planetchunks[x, y, z]);

                                    }

                                }
                                if (ischunkloaded[x, y, z] == false)
                                {
                                    if (distance < 200)
                                    {
                                        if (LoadDone == true)
                                        {

                                            toLoad.Add(new Vector4(x, y, z, Mathf.RoundToInt(distance)));
                                            toLoad.Sort(delegate (Vector4 first, Vector4 second) { return first.w.CompareTo(second.w); });

                                        }

                                    }
                                }

                            }
                        }
                    }


                }

                timer = 0;

            }

        }

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce

Advertisement

So, when you say "planets over 1,000 chunks"... do you means planets made up of a 10x10x10 cube of chunks? Or do you mean planets of a diameter or radius of 1,000 chunks (1000 x 1000 x 1000)? Because there's six orders of magnitude difference between how many chunks that totals up to.

Totally aside from that, there's two whole categories of things you're doing suboptimally here.

* You're testing every chunk of every planet for being in or out of range of every player... when in fact, you should at worst be iterating over the chunks currently loaded for that player to find which ones to unload, and then searching for chunks that need to become loaded.

* You're iterating over all players, and presumably, all planets (assuming this FixedUpdate() function is a member of your planet object) outside this loop.

* You're re-sorting the list every time you add an item to it, instead of only sorting it once after all adds.

You can exclude a lot of tests by adding layers of guards here:

* You can exclude an entire player/planet test based on the player being too far from that planet to even consider testing chunks of that planet.

* You can also only iterate over only the chunks which are inside the bounding cube of that player's range, instead of testing every cube of the entire planet.

* You can test against range-squared, instead of range, to save you a square root calculation per chunk/player pair. Here's some pseudocode fragments which may help...


float loadRange = 200;                      // Extract this magic constant from the inner loop to somewhere more maintainable.
float loadRangeSq = loadRange * loadRange;  // Calculate its square.
int currentLoad = toLoad.Size();            // Memorize the current length of the load queue.

int playerx = (int)(player.transform.position.x - this.transform.position.x);  // Express player's position relative to planet.
[... and for the other two axes ...]

int minX = max(1, (playerx - loadRange) / 16);               // Calculate extends of bounding rectangle of view, in chunk units.
int maxX = min(planetSize, (playerx + loadRange + 15) / 16); // Ditto. Note the +15 to cause rounding up.
if( minX > planetSize || maxX < 1 ) continue;                // Stop looking at this player if the planet is entirely out of range.

for(int x = minx; x <= maxx; x++)    // Use the calculated bounds, not the whole planet.
{
float dX = x * 16 - playerx;  // Only need to calculate this once per X term. Could even make this cheaper; I leave it as exercise for the reader.
[... Y and Z loops too ...]

// And now; the entire inner loop body.
float distanceSq = dX*dX + dY*dY + dZ*dZ;  // This, too, can be made cheaper. How is an exercise to the reader.

if( distanceSq < loadRangeSq && !isChunkLoaded[x,y,z] && LoadDone ) {
    toLoad.Add( [...] );
    // Presumably, after the chunk is loaded it is added to that player's chunks-displayed list.
  }
}

// And then, outside of the X loop again...
if( toLoad.Size() > currentLoad ) {
    // Only re-sort if we did add anything.
    toLoad.Sort( [...] );
}

That's the load-queue populator. You should iterate over the player's current chunk list and un-load any that are now out of range, because the player's current view list is probably *always* going to be smaller than the number of chunks that *might* be in the player's view.

Also, if this load/unload is the server-side view of chunks, remember to load chunks when ANY player enters range, but don't unload a chunk until ALL players are out of range.

RIP GameDev.net: launched 2 unusably-broken forum engines in as many years, and now has ceased operating as a forum at all, happy to remain naught but an advertising platform with an attached social media presense, headed by a staff who by their own admission have no idea what their userbase wants or expects.Here's to the good times; shame they exist in the past.

So, when you say "planets over 1,000 chunks"... do you means planets made up of a 10x10x10 cube of chunks? Or do you mean planets of a diameter or radius of 1,000 chunks (1000 x 1000 x 1000)? Because there's six orders of magnitude difference between how many chunks that totals up to.

Totally aside from that, there's two whole categories of things you're doing suboptimally here.

* You're testing every chunk of every planet for being in or out of range of every player... when in fact, you should at worst be iterating over the chunks currently loaded for that player to find which ones to unload, and then searching for chunks that need to become loaded.

* You're iterating over all players, and presumably, all planets (assuming this FixedUpdate() function is a member of your planet object) outside this loop.

* You're re-sorting the list every time you add an item to it, instead of only sorting it once after all adds.

You can exclude a lot of tests by adding layers of guards here:

* You can exclude an entire player/planet test based on the player being too far from that planet to even consider testing chunks of that planet.

* You can also only iterate over only the chunks which are inside the bounding cube of that player's range, instead of testing every cube of the entire planet.

* You can test against range-squared, instead of range, to save you a square root calculation per chunk/player pair. Here's some pseudocode fragments which may help...


float loadRange = 200;                      // Extract this magic constant from the inner loop to somewhere more maintainable.
float loadRangeSq = loadRange * loadRange;  // Calculate its square.
int currentLoad = toLoad.Size();            // Memorize the current length of the load queue.

int playerx = (int)(player.transform.position.x - this.transform.position.x);  // Express player's position relative to planet.
[... and for the other two axes ...]

int minX = max(1, (playerx - loadRange) / 16);               // Calculate extends of bounding rectangle of view, in chunk units.
int maxX = min(planetSize, (playerx + loadRange + 15) / 16); // Ditto. Note the +15 to cause rounding up.
if( minX > planetSize || maxX < 1 ) continue;                // Stop looking at this player if the planet is entirely out of range.

for(int x = minx; x <= maxx; x++)    // Use the calculated bounds, not the whole planet.
{
float dX = x * 16 - playerx;  // Only need to calculate this once per X term. Could even make this cheaper; I leave it as exercise for the reader.
[... Y and Z loops too ...]

// And now; the entire inner loop body.
float distanceSq = dX*dX + dY*dY + dZ*dZ;  // This, too, can be made cheaper. How is an exercise to the reader.

if( distanceSq < loadRangeSq && !isChunkLoaded[x,y,z] && LoadDone ) {
    toLoad.Add( [...] );
    // Presumably, after the chunk is loaded it is added to that player's chunks-displayed list.
  }
}

// And then, outside of the X loop again...
if( toLoad.Size() > currentLoad ) {
    // Only re-sort if we did add anything.
    toLoad.Sort( [...] );
}

That's the load-queue populator. You should iterate over the player's current chunk list and un-load any that are now out of range, because the player's current view list is probably *always* going to be smaller than the number of chunks that *might* be in the player's view.

Also, if this load/unload is the server-side view of chunks, remember to load chunks when ANY player enters range, but don't unload a chunk until ALL players are out of range.

Thank you for the in-depth explanation! To answer a few questions, I'm thinking right now I'm testing with 10 x 10 x 10 chunks, but I plan to go much bigger. I'm also going to have a "SolarSystem" script that handles unloading the planets (main class). I'll make these changes and post back when I see the performance difference!

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce


int minX = max(1, (playerx - loadRange) / 16); // Calculate extends of bounding rectangle of view, in chunk units.
int maxX = min(planetSize, (playerx + loadRange + 15) / 16); // Ditto. Note the +15 to cause rounding up.
if( minX > planetSize || maxX < 1 ) continue; // Stop looking at this player if the planet is entirely out of range.

Quick question - why is max using the min method and vice versa?

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce


* You can also only iterate over only the chunks which are inside the bounding cube of that player's range, instead of testing every cube of the entire planet.

in my experience, this seems to be the way to go.

from player position, you can calculate which chunks are in visual range and may need to be drawn - and thus should be in the chunk cache. you don't have to check the range of each chunk to the player to determine what should be in the cache.

cache chunks. if a required chunk is not in the cache, discard the least recently used chunk, and load the required chunk. this can be done in foreground or background, on demand, or ahead of time.

Norm Barrows

Rockland Software Productions

"Building PC games since 1989"

rocklandsoftware.net

PLAY CAVEMAN NOW!

http://rocklandsoftware.net/beta.php

Here's what I've got:


    public void FixedUpdate()
    {

        int currentLoad = toLoad.Count;

        StartCoroutine(ChunkQueue());


        timer = timer + 1;

        if (timer > 20)
        {

            foreach (GameObject player in players)
            {


                int playerx = (int)(player.transform.position.x - this.transform.position.x);
                int playery = (int)(player.transform.position.y - this.transform.position.y);
                int playerz = (int)(player.transform.position.z - this.transform.position.z);

                int minX = (int)Mathf.Max(1, (playerx - loadRange) / 16);               // Calculate extends of bounding rectangle of view, in chunk units.
                int maxX = (int)Mathf.Min(planetSize / 16, (playerx + loadRange + 15) / 16); // Ditto. Note the +15 to cause rounding up.
                if (minX > planetSize / 16 || maxX < 1) continue;                  // Test whether planet is even in bounding rectangle on this axis.

                int minY = (int)Mathf.Max(1, (playery - loadRange) / 16);
                int maxY = (int)Mathf.Min(planetSize / 16, (playery + loadRange + 15) / 16);
                if (minY > planetSize / 16 || maxY < 1) continue;

                int minZ = (int)Mathf.Max(1, (playerz - loadRange) / 16);
                int maxZ = (int)Mathf.Min(planetSize / 16, (playerz + loadRange + 15) / 16);
                if (minZ > planetSize / 16 || maxZ < 1) continue;


                for (int x = minX; x <= maxX; x++)    // Use the calculated bounds, not the whole planet.
                {
                    float dX = x * 16 - playerx;

                    for (int y = minY; x <= maxY; y++)
                    {
                        float dY = y * 16 - playery;

                        for (int z = minZ; x <= maxZ; z++)
                        {
                            float dZ = z * 16 - playerz;

                            float distanceSq = dX * dX + dY * dY + dZ * dZ;


                                if (distanceSq < loadRangeSq && !ischunkloaded[x, y, z] && LoadDone)
                                {

                                    toLoad.Add(new Vector4(x, y, z, distanceSq));

                                }




                        }
                    }
                }


            }

            if (toLoad.Count > currentLoad)
            {
                // Only re-sort if we did add anything.
                toLoad.Sort(delegate (Vector4 first, Vector4 second) { return first.w.CompareTo(second.w); });
            }

            timer = 0;


        }

        
    }

This loads (for an 8 x 8 x 8 planet) from 1, 1, 1 to 1, 1, 8 before throwing out of bounds exceptions (On !ischunkloaded[x, y, z]). When I try to catch the exception and figure out what values are breaking it, unity crashes. Another problem is that 1, 1, 1 through 1, 1, 8 aren't actually the closest to the player (my previous code was able to sort it so closest chunks loaded first). I made a few small changes, like planetSize to planetSize / 16 (planetSize is in blocks, not chunks).

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce

Is is because you access the ischunkloaded ranging from 1..8 and not 0..7?

You won't need to test against loadRangeSq since all chunks are already inside the bounds (you're not checking against the whole planet anymore right?) :)

.:vinterberg:.


This loads (for an 8 x 8 x 8 planet) from 1, 1, 1 to 1, 1, 8 before throwing out of bounds exceptions (On !ischunkloaded[x, y, z]). When I try to catch the exception and figure out what values are breaking it, unity crashes.
Try just printing to console each x,y,z and distance, or comment out that entire if construct and debugging it. Possibly, Unity is trying to evaluate ischunkloaded[x,y,z] and show you the value, but not trapping the same exception that's being thrown at runtime.

Alternatively, if it is a bounds issue, print out all of minX/Y/Z and maxX/Y/Z before entering the loop. Find out what axis we're off on.

Also, I noticed a problem in the bounds logic I gave you. This may be part of it.


int minX = (int)Mathf.Max(0, (playerx - loadRange) / 16) + 1;
int maxX = (int)Mathf.Min(planetSize, playerx + loadRange + 15) / 16 + 1;
if (minX > planetSize / 16 || maxX < 1) continue;

Note two things. First, division doesn't flatten at 1, it flattens at 0, so we have to add one after the division to get the right indexes. Second, I've moved the division by 16 out of the upper bound calculation, since both terms were being divided, and there was a possible rounding error going on there as it was.

Is is because you access the ischunkloaded ranging from 1..8 and not 0..7?
OP has said that his array is 1-indexed, though not why. I don't know if that's a Unity thing, or part of how he designed his ischunkloaded array.
RIP GameDev.net: launched 2 unusably-broken forum engines in as many years, and now has ceased operating as a forum at all, happy to remain naught but an advertising platform with an attached social media presense, headed by a staff who by their own admission have no idea what their userbase wants or expects.Here's to the good times; shame they exist in the past.


Try just printing to console each x,y,z and distance, or comment out that entire if construct and debugging it. Possibly, Unity is trying to evaluate ischunkloaded[x,y,z] and show you the value, but not trapping the same exception that's being thrown at runtime.

It's trying to do 1, 1, 9 - I can't figure out why. When I subtract one on each of the planetchunks[] array, it tries 1, 1, 10 - its not stopping at what should be the max.

I'm making an open source voxel space game called V0xel_Sp4ce! Help me out with the repository here: https://github.com/SpikeViper/V0xel_Sp4ce

Okay. What is playerz, planetsize, minZ, and maxZ at that time? Sounds like some part of the math is going afoul, or the ischunkloaded[] array is not correctly sized for the indexes you're expecting to be valid. Show us where ischunkloaded[] is created, so we can double-check that last assumption while we're at it.

RIP GameDev.net: launched 2 unusably-broken forum engines in as many years, and now has ceased operating as a forum at all, happy to remain naught but an advertising platform with an attached social media presense, headed by a staff who by their own admission have no idea what their userbase wants or expects.Here's to the good times; shame they exist in the past.

This topic is closed to new replies.

Advertisement