Task Scheduler (C# / Unity)
Sometimes certain tasks don’t need to be updated every frame. For instance, if AI decision making is a time-consuming process, we could do it every 10 frames instead of every frame. Not only would this help alleviate the per-frame cost, but it would also provide an inherent reaction time for the AI, arguably making it behave more realistically. The TaskScheduler is a system for scheduling given objects for updates in this fashion, allowing you to define the update frequency explicitly. It also allows for defining how to space tasks in relation to each other, preventing tasks with similar frequencies from stacking up on the same frames.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
using System; using System.Collections.Generic; using UnityEngine.Assertions; namespace Gemini { public class TaskScheduler : IScheduledTask { #region public public struct TaskRecord : IEquatable { public IScheduledTask m_task; public uint m_frequency; public int m_phase; // implement IEquatable to avoid allocations for calls to List.Contains public bool Equals( TaskRecord record ) { return m_task == record.m_task && m_frequency == record.m_frequency && m_phase == record.m_phase; } public override bool Equals( object obj ) { return Equals( ( TaskRecord )obj ); } public override int GetHashCode() { return HashCode.Combine( m_task.GetHashCode(), m_frequency.GetHashCode(), m_phase.GetHashCode() ); } } /// /// Constructor /// ///Number of frame iterations to look at when determining the optimum phase for a task public TaskScheduler( int phaseIterationCount ) { m_tasks = new List(); m_frame = 0; m_phaseIterationCount = phaseIterationCount; } public int TaskCount { get { return m_tasks.Count; } } public int Frame { get { return m_frame; } } /// /// Registers the given task for scheduled updates /// ///The task to schedule ///The desired update frequency, in frames ///An offset used to scatter tasks with similar frequencies public void AddTask( IScheduledTask task, uint frequency, int phase ) { Assert.IsFalse( frequency == 0, string.Format( "TaskScheduler.AddTask: Frequency for task '{0}' must be greater than 0", task ) ); m_tasks.Add( new TaskRecord { m_task = task, m_frequency = frequency, m_phase = phase } ); } /// /// Registers the given task for scheduled updates, with an automatic phase assignment /// ///The task to schedule ///The desired update frequency, in frames public void AddTask( IScheduledTask task, uint frequency ) { Assert.IsFalse( frequency == 0, string.Format( "TaskScheduler.AddTask: Frequency for task '{0}' must be greater than 0", task ) ); AddTask( task, frequency, FindOptimumPhase() ); } public void RemoveTask( IScheduledTask task ) { int index = m_tasks.FindIndex( x => x.m_task == task ); Assert.IsTrue( index > 0, string.Format( "TaskScheduler.RemoveTask: Task '{0}' isn't registered with the scheduler", task ) ); if ( index > 0 ) { m_tasks.RemoveAt( index ); } } public void ChangeFrequency( IScheduledTask task, uint frequency ) { Assert.IsFalse( frequency == 0, string.Format( "TaskScheduler.ChangeFrequency: Frequency for task '{0}' must be greater than 0", task ) ); // remove and re-add task to recalculate phase RemoveTask( task ); AddTask( task, frequency ); } public void ScheduledTick() { for ( int i = 0; i < m_tasks.Count; i++ ) { TaskRecord record = m_tasks[i]; if ( IsTaskScheduled( record, m_frame ) ) { record.m_task.ScheduledTick(); } } ++m_frame; } #endregion // public #region private private List m_tasks; private int m_frame; private int m_phaseIterationCount; private bool IsTaskScheduled( TaskRecord record, int frame ) { int offsetFrame = frame - record.m_phase; return offsetFrame >= 0 && ( offsetFrame % record.m_frequency ) == 0; } private int FindOptimumPhase() { int phase = 0; int minTaskCount = int.MaxValue; // look ahead to find frame with fewest running tasks for ( int frame = m_frame; frame < m_phaseIterationCount; ++frame ) { int frameTaskCount = 0; // count number of tasks scheduled to run this frame for ( int i = 0; i < m_tasks.Count; i++ ) { if ( IsTaskScheduled( m_tasks[i], frame ) ) { ++frameTaskCount; } } if ( frameTaskCount < minTaskCount ) { minTaskCount = frameTaskCount; phase = frame - m_frame; } } return phase; } #endregion // private } } |