Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

My ChunkTracker class.

Discussion in 'Scripting' started by BenZed, Dec 22, 2014.

  1. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    I whipped this up the other night after reading a few threads about chunking.
    In a game where, say, I have voxel based terrain and I need to check each terrain cube for changes as often as possible.

    In this game, if we have a million terrain cubes, we obviously can't check every cube every frame.

    So I've developed this as my chunkTracker, to be used in conjunction with coroutines to ensure that as many checks happen without bogging down the frameRate:

    Updated as per suggestions by @lordofduct
    Code (CSharp):
    1. using System.Diagnostics;
    2.  
    3. /*
    4. *  This class is going to be used as a helper for tracking task execution times and providing information
    5. *  relevent to distributing chunking tasks across frames to prevent lag resulting from task overhead.
    6. *
    7. *  For example, so far as scripting CPU is concerned, the creation of Structure mesh and colliders is a
    8. *  computationally dense process, and must be done every time a structure gains or loses a tile.
    9. *
    10. *  This overhead adds up significantly if there are many ships and many battles occuring. A single, 40 tile
    11. *  basic ship can take up to a full frame (30 ms) to complete it's update process. If the design goal of this
    12. *  game is to have a hundred ships with a hundred tiles each, we need to chunk the task of creating and
    13. *  updating ships. This class provides a tool to assist.
    14. *
    15. *  This class is very simple, but it's usage is very specific:
    16. *  It will check the last time since ApplyChunk has been called. If it's passed the threshold, it will return true and reset the timer.
    17. *  Therefore, if ApplyChunk returns true, optimization depends on a yield being called afterwords.
    18. *
    19. */
    20.  
    21. namespace UnityEngine.Extensions {
    22.  
    23.     public static class ChunkTracker {
    24.  
    25.         public const int MinimumFramesPerSecond = 35;
    26.  
    27.         //If we want at least 35 frames per second, we'll allow a frame calculation to take 28 milliseconds.
    28.         //Longer than that, and we advocate a chunk.
    29.         const float allowableMillisecondsPerFrame = (1f / (float) MinimumFramesPerSecond) * 1000f;
    30.  
    31.         static Stopwatch intraFrameStopWatch;
    32.  
    33.         static float lastConsideredTime = 0f;
    34.  
    35.         static public bool AdvocateChunk(){
    36.  
    37.             //if computational demand is low, Time.time will be higher than lastConsideredTime, because
    38.             //the frame will have been updated without having to advocate a chunk.
    39.             if (lastConsideredTime < Time.time) {
    40.                 lastConsideredTime = Time.time;
    41.                 return false;
    42.             }
    43.  
    44.             //If we're doing something computationally intense, and it's taken too long, we'll reset
    45.             //the tracker and suggest a chunk. It's imporant to note that the first time per frame
    46.             //we get here, we'll always suggest a chunk, because the stopwatch will have been running
    47.             //since last reset. Which is fine, because we'll only get here if an entire frame has elapsed,
    48.             //so a chunk will be necessary anyway.
    49.             if (intraFrameStopWatch.ElapsedMilliseconds >= allowableMillisecondsPerFrame) {
    50.                 intraFrameStopWatch.Reset();
    51.                 intraFrameStopWatch.Start();
    52.                 lastConsideredTime = Time.time;
    53.  
    54.                 return true;
    55.             }
    56.  
    57.             //If we've gotten here, it means we're doing something computationally intense and we've already
    58.             //advocated at least one chunk, but we haven't blown our frame calculation budget for the next one.
    59.             return false;
    60.         }
    61.  
    62.         static ChunkTracker() {
    63.             intraFrameStopWatch = new Stopwatch();
    64.             intraFrameStopWatch.Start();
    65.         }
    66.     }
    67. }
    68.  
    And here's a test usage:

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEngine.Extensions;
    3. using System.Collections;
    4.  
    5. //We'll say the game controller will manage ALL CPU Intensive checks, in the form of coroutines.
    6. //Just place it on the Main Camera of a new scene.
    7. public class GameController : MonoBehaviour {
    8.  
    9.     int chunked_frames = 0;
    10.  
    11.     IEnumerator Start () {
    12.  
    13.         //Infinite loops are pretty CPU Intensive
    14.         while(true) {
    15.  
    16.             //Since we do a chunk whenever the frame takes too long, this
    17.             //Infinite loop wont crash the game.
    18.             if (ChunkTracker.AdvocateChunk()) {
    19.  
    20.                 chunked_frames++;
    21.  
    22.                 Debug.Log (chunked_frames);
    23.  
    24.                 yield return new WaitForEndOfFrame();
    25.            
    26.             }
    27.         }
    28.  
    29.     }
    30.  
    31. }
    32.  
    What do you guys think? Is there a better/simpler solution? How do you handle chunking?

    Anything about this that might break? (I don't know much about the System.Diagnostics namespace. Maybe StopWatches shouldn't be used this way.)

    Let me know!
     
    Last edited: Dec 25, 2014
  2. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
  3. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    Why not put the initialize code in the static constructor for the class.

    This way you don't have to call Initialize. Which is helpful in case ChunkTracker needs to be referenced in different locations (otherwise... who calls 'Initialize', so that Initialize isn't called multiple times).

    If it's only supposed to be used in one place, then why is it static?

    Also, this is just personal taste, I'd have a separate method to 'push' a chunk processed. Basically I could call AdvocateChunk to see if the threshold has been passed since the last time 'Push' was called. I might have code that calls AdvocateChunk, but doesn't actually do any intense processing, for various reasons. Maybe I'm just polling it for debug display purposes... or maybe I call to advocate, but my pool that I'm processing is empty right now. Or just the mere fact that it might be used in several places at the same time (it is static after all)... whichever code gets permission to process, signals back that it used its advocated chunk, so another bit of code can get permission in its turn with out being blocked just because earlier asked with out really needing it.
     
    Last edited: Dec 23, 2014
  4. BenZed

    BenZed

    Joined:
    May 29, 2014
    Posts:
    524
    I did not know static constructors were a thing! Excellent suggestion, I will do that.

    As opposed to a singleton? I guess that would work too, but this doesn't necessarily only have to be used in one place. I was imagining that any game objects simultaneously running coroutines could all use it once, that way they would all get chunks without blowing a frame.

    As for your last point, I think you (or I) might be a bit confused. It works as how I think you are describing it. Right now, you call advocate chunk to see weather or not a chunk should take place. If a chunk is not advocated, the polling code would continue without interruption.

    Code (CSharp):
    1. IENumerator SomeTask (){
    2.  
    3.          foreach(Transform child in gameObjectWithManyChildren) {
    4.         //do something crazy with child. Rebuild it’s mesh or whatever.
    5.  
    6.         //Check to see if we need to chunk.
    7.         bool shouldWeChunk = ChunkTracker.AdvocateChunk();
    8.  
    9.         //If so, we pull a yield
    10.         if (shouldWeChunk) {
    11.             yield return new WaitForEndOfFrame();
    12.         }
    13.  
    14.         //otherwise we move on to the next child.
    15.       }
    16. }
    If this was run on a game object WITHOUT many children, for example, you might never actually have to do a chunk, you'd just be testing to do so safety.
     
    Last edited: Dec 25, 2014
  5. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    No, I meant just making it a class you create an instance of when you need it.

    This is why I suggested having the 'Push' method that uses the chunk. So that way one place can check if it's available, but not use it.

    As for the rest... I have to go right now. Just got a text message to head to the mamasita's house for x-mas.