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
using System.Diagnostics;
/*
* This class is going to be used as a helper for tracking task execution times and providing information
* relevent to distributing chunking tasks across frames to prevent lag resulting from task overhead.
*
* For example, so far as scripting CPU is concerned, the creation of Structure mesh and colliders is a
* computationally dense process, and must be done every time a structure gains or loses a tile.
*
* This overhead adds up significantly if there are many ships and many battles occuring. A single, 40 tile
* basic ship can take up to a full frame (30 ms) to complete it's update process. If the design goal of this
* game is to have a hundred ships with a hundred tiles each, we need to chunk the task of creating and
* updating ships. This class provides a tool to assist.
*
* This class is very simple, but it's usage is very specific:
* 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.
* Therefore, if ApplyChunk returns true, optimization depends on a yield being called afterwords.
*
*/
namespace UnityEngine.Extensions {
public static class ChunkTracker {
public const int MinimumFramesPerSecond = 35;
//If we want at least 35 frames per second, we'll allow a frame calculation to take 28 milliseconds.
//Longer than that, and we advocate a chunk.
const float allowableMillisecondsPerFrame = (1f / (float) MinimumFramesPerSecond) * 1000f;
static Stopwatch intraFrameStopWatch;
static float lastConsideredTime = 0f;
static public bool AdvocateChunk(){
//if computational demand is low, Time.time will be higher than lastConsideredTime, because
//the frame will have been updated without having to advocate a chunk.
if (lastConsideredTime < Time.time) {
lastConsideredTime = Time.time;
return false;
}
//If we're doing something computationally intense, and it's taken too long, we'll reset
//the tracker and suggest a chunk. It's imporant to note that the first time per frame
//we get here, we'll always suggest a chunk, because the stopwatch will have been running
//since last reset. Which is fine, because we'll only get here if an entire frame has elapsed,
//so a chunk will be necessary anyway.
if (intraFrameStopWatch.ElapsedMilliseconds >= allowableMillisecondsPerFrame) {
intraFrameStopWatch.Reset();
intraFrameStopWatch.Start();
lastConsideredTime = Time.time;
return true;
}
//If we've gotten here, it means we're doing something computationally intense and we've already
//advocated at least one chunk, but we haven't blown our frame calculation budget for the next one.
return false;
}
static ChunkTracker() {
intraFrameStopWatch = new Stopwatch();
intraFrameStopWatch.Start();
}
}
}
And here’s a test usage:
using UnityEngine;
using UnityEngine.Extensions;
using System.Collections;
//We'll say the game controller will manage ALL CPU Intensive checks, in the form of coroutines.
//Just place it on the Main Camera of a new scene.
public class GameController : MonoBehaviour {
int chunked_frames = 0;
IEnumerator Start () {
//Infinite loops are pretty CPU Intensive
while(true) {
//Since we do a chunk whenever the frame takes too long, this
//Infinite loop wont crash the game.
if (ChunkTracker.AdvocateChunk()) {
chunked_frames++;
Debug.Log (chunked_frames);
yield return new WaitForEndOfFrame();
}
}
}
}
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!