Unity 计时器
2015-10-12 14:39
597 查看
Unity 计时器
ISimTimeObserver.csusing System; namespace MyScheduling { /// <summary> /// Implement and register a frame time observer with the sim time engine to receive frame /// time deltas per frame. /// </summary> public interface ISimTimeObserver { // Called once per "frame", simulating a constant frame rate.(每一帧都会被调用,模拟一个恒定的帧速率。) // Number of milliseconds since the last frame is given. void OnSimTime(UInt32 dt); } }
MutableIterator.cs
using System; using System.Collections; namespace MyScheduling { //迭代器类 public class MutableIterator { int index; int count; public MutableIterator() { Reset(); } public void Reset() { index = 0; count = 0; } public void Init(int count) { index = 0; this.count = count; } public void Init(ICollection list) { index = 0; this.count = list.Count; } public bool Active() { return index < count; } public void Next() { index++; } public int Index { get { return index; } set { // Only allow manual index setting if we haven't yet started iterating. if (index == 0) { index = value; } } } public int Count { get { return count; } } public void OnRemove(int i) { if (count > 0) { count--; if (i <= index) { index--; } } } } }
Service.cs
using System; using System.Collections.Generic; namespace MyScheduling { //可以通过 Set Get 的方式 获取很多单例的引用 public static class Service { #if SERVER [ThreadStatic] #endif //所有设置过的的类的引用列表 private static List<IServiceWrapper> serviceWrapperList; //设置某些类的引用 public static void Set<T>(T instance) { if (ServiceWrapper<T>.instance != null) { throw new Exception("An instance of this service class has already been set!"); } ServiceWrapper<T>.instance = instance; if (serviceWrapperList == null) { serviceWrapperList = new List<IServiceWrapper>(); } serviceWrapperList.Add(new ServiceWrapper<T>()); } //获取某些类的引用 public static T Get<T>() { return ServiceWrapper<T>.instance; } //是否设置某些类的引用 public static bool IsSet<T>() { return ServiceWrapper<T>.instance != null; } // Resets references to all services back to null so that they can go out of scope and // be subjected to garbage collection. // * Services that reference each other will be garbage collected. // * AssetBundles should be manually unloaded by an asset manager. // * GameObjects will be destroyed by the next level load done by the caller. // * Any application statics should be reset by the caller as well. // * If there are any unmanaged objects, those need to be released by the caller, too. public static void ResetAll() { if (serviceWrapperList == null) { return; } // Unset in the reverse order in which services were set. Probably doesn't matter. for (int i = serviceWrapperList.Count - 1; i >= 0; i--) { serviceWrapperList[i].Unset(); } serviceWrapperList = null; } } internal class ServiceWrapper<T> : IServiceWrapper { #if SERVER [ThreadStatic] #endif public static T instance = default(T); public void Unset() { ServiceWrapper<T>.instance = default(T); } } internal interface IServiceWrapper { void Unset(); } }
SimTimeEngine.cs
using System; using System.Collections.Generic; using UnityEngine; namespace MyScheduling { /// <summary> /// A millisecond-based sim time manager. Observers can register to be given sim time /// events. Simulation time is constant. To handle device lag spikes, observers will see /// multiple events come through in quick succession on a single frame as seen on screen. /// This "variable fixed" approach is useful for physics observers, so they don't suffer /// from lag spikes. Being millisecond-based helps with portability w/r/t determinism. /// This class gets Update()s from the main Engine MonoBehaviour in the game. /// We want to avoid having multiple MonoBehaviours with their own Update() methods. /// Instead, you register as an observer and you are given frame time deltas frequently. /// </summary> public class SimTimeEngine { // This is the maximum level of lag in sim frames we can tolerate before we give // up catching up and resetting the error accumulator // Assuming timePerFrame is 33ms the following constant allows 1 second of lag private const int MAX_SIM_FRAMES_LAG_BEFORE_RESET = 30; // We do forced rollovers frequently, for robustness. Not too frequently, though. // It needs to be larger than the time between the largest of lag spikes since our // rollover handling code can only detect when a single rollover happens. We can't // have more than one rollover happen on a single frame. // One rollover per minute seems frequent enough that it gets a lot of exercise, yet // if you have a 1-minute lag spike and a bug is caused, your game experience isn't // that great to begin with, so that's a bug in itself that needs fixing elsewhere. private const UInt32 ROLLOVER_MILLISECONDS = 60000; private const float ROLLOVER_SECONDS = (float)ROLLOVER_MILLISECONDS / 1000f; private const float LOW_SIM_TIME_PER_FRAME = 33 / 6000f; // 1/6 of a frame. // Number of milliseconds that observers get to see between simulated frames. // 16 milliseconds is just over 60fps. The target is 60fps, but we want to stay // above 30fps at all times. Observers may see multiple frame time deltas per "real" // frame as seen on screen. But we will simulate it as though we're getting close to // 60fps at all times. We simulate "just over 60fps" rather than "just under" so that // we can take full advantage of when the game can handle/render/update at full 60fps. // If we used 17 here, for example, then we wouldn't ever send out all frame time // deltas to physics and other systems when running full speed. For 30fps, use 33. private UInt32 timePerFrame; private UInt32 maxLag; // Number of milliseconds that have passed since the last frame. // 32 bits gets us 23 days of continuous gameplay until rollover. // We deal with that internally and observers never have to worry about it. private UInt32 timeLast; // So we don't lose left-over milliseconds each frame. private UInt32 timeAccumulator; // How far we roll back on each rollover. private float rolloverNext; // Active observers receiving simulation time deltas. private List<ISimTimeObserver> observers; private MutableIterator miter; // For pausing or fast-forwarding simulation. private UInt32 scale; // For keeping track of total number of frames since start of the game // Since we are running on 30FPS, it will work upto 1657 days private UInt32 frameCount; public SimTimeEngine(UInt32 timePerFrame) { Service.Set<SimTimeEngine>(this); this.timePerFrame = timePerFrame; ScaleTime(1); // Default to real-time simulation speed. timeLast = this.Now(); timeAccumulator = 0; rolloverNext = 0; observers = new List<ISimTimeObserver>(); miter = new MutableIterator(); frameCount = 0; } // Registers an observer to start receiving sim time deltas. public void RegisterSimTimeObserver(ISimTimeObserver observer) { if (observer == null) { return; } if (observers.IndexOf(observer) < 0) { observers.Add(observer); } } // Unregisters an observer from receiving sim time deltas. public void UnregisterSimTimeObserver(ISimTimeObserver observer) { int i = observers.IndexOf(observer); if (i >= 0) { observers.RemoveAt(i); miter.OnRemove(i); } } // Used by the engine when it's about to shutdown or reload the entire app. public void UnregisterAll() { observers.Clear(); miter.Reset(); } // Use a scale of 0 for paused, 1 for normal speed, 2+ for fast-forward time scale. public void ScaleTime(UInt32 scale) { this.scale = scale; maxLag = timePerFrame * MAX_SIM_FRAMES_LAG_BEFORE_RESET * scale; } public bool IsPaused() { return this.scale == 0; } public UInt32 GetFrameCount() { return frameCount; } // Call this from the main Engine's Update() method. We let Unity update this class, // then this class updates (typically) every other class in the game. public void OnUpdate() { #if !SERVER UInt32 timeThis = this.Now(); UInt32 dt = timeThis < timeLast ? timeThis + ROLLOVER_MILLISECONDS - timeLast : timeThis - timeLast; timeLast = timeThis; dt *= scale; timeAccumulator += dt; float t = Time.realtimeSinceStartup; int simFrameCount = 0; while (timeAccumulator >= timePerFrame) { #endif for (miter.Init(observers); miter.Active(); miter.Next()) { ISimTimeObserver observer = observers[miter.Index]; observer.OnSimTime(timePerFrame); } miter.Reset(); #if !SERVER frameCount++; timeAccumulator -= timePerFrame; // Allow extra frame to be simulated than the scale factor to // catch up after a lag. For example, when scale is 1 we can // catch up 1 frame for every frame simulated. When scale is 4 we can // catch up 1 frame for every 4 frames simulated. if (++simFrameCount == scale) { if (Time.realtimeSinceStartup - t > LOW_SIM_TIME_PER_FRAME) { break; } } else if (simFrameCount == scale + 1) { break; } } if (timeAccumulator > maxLag) { // There are a few reasons to reset the accumulator here: // 1) So that it doesn't rollover, for sanity. (N/A in view time engine.) // 2) So that on devices with poor performance, we give them a chance to // catch up to real time after large lag spikes. If we didn't reset // the accumulator, then it has the potential to grow a lot during large // lag spikes, causing multiple successive view frames to get the max // sim frames per view frame, for multiple view frames in a row. This // could continue to cause the accumulator to grow, always playing // catch-up. So we throw out the accumulator if we ever hit the limit, // which currently equates to a poor 6 frames per second performance. // The drawback here is that we'll never catch up to real time. So, // for a 1-minute battle timer, it might take over 1 minute to complete. // 3) An alert (or other device-dependent thing) might cause a single long // lag spike. In that case, we won't play catch-up if we reset the // accumulator. Instead it'll behave as if the app were paused. timeAccumulator = 0; } #endif } // Returns the number of milliseconds since the start of the game or the last rollover. // This is the only place that deals with time as a float. All callers assume that // time is on millisecond granularity. private UInt32 Now() { float time = Time.time; // NOTE: This is going to be a deterministic port issue. // Unity's API returns a float. // We want something deterministic, so we convert to 32-bit unsigned int. float t = time - rolloverNext; // We can't cast from float to UInt32 after 23 days worth of milliseconds, // So we impose our own rollover at an interval that we decide. if (t >= ROLLOVER_SECONDS) { t -= ROLLOVER_SECONDS; rolloverNext += ROLLOVER_SECONDS; } // Round in a way that is likely portable to other platforms. UInt32 ms = (UInt32)Mathf.Floor(t * 1000.0f); return ms; } } }
SimTimerManager.cs
using System; using System.Collections.Generic; namespace MyScheduling { /// <summary> /// Manager for creating and killing sim timers. /// Sim timers are deterministic and affected by simulation pause and fast-forward. /// </summary> public class SimTimerManager : TimerManager, ISimTimeObserver { public SimTimerManager() { Service.Set<SimTimerManager>(this); Service.Get<SimTimeEngine>().RegisterSimTimeObserver(this); } /// Create an optionally-repeating timer with a custom object associated with it. public uint CreateSimTimer(UInt32 delay, bool repeat, TimerDelegate callback, object cookie) { return CreateTimer(delay, repeat, callback, cookie); } /// Kill the timer with the given id. It'll stop firing after this. public void KillSimTimer(uint id) { KillTimer(id); } /// Trigger and Kill the timer with the given id. public void TriggerKillSimTimer(uint id) { TriggerKillTimer(id); } public void OnSimTime(UInt32 dt) { OnDeltaTime(dt); } } }
Timer.cs
using System; namespace MyScheduling { // Prototype for timer callbacks. // The timer id is given along with an optional user-specified object. public delegate void TimerDelegate(uint id, object cookie); /// <summary> /// The timer manager keeps a list of these. One instance per active timer in the system. /// </summary> public class Timer { // Public read-only access for speed. //每个计时器ID public uint Id; //延迟多少毫秒执行 public UInt32 Delay; //是否重复 public bool Repeat; //回调函数 public TimerDelegate Callback; //传递的参数(可以为空) public object Cookie; private UInt32 timeFire; public Timer(uint id, UInt32 delay, bool repeat, TimerDelegate callback, object cookie) { Id = id; Delay = delay; Repeat = repeat; Callback = callback; Cookie = cookie; this.timeFire = 0; // The timer manager will modify this. } public UInt32 TimeFire { get { return timeFire; } set { timeFire = value; } } // Wrapper for this.TimeFire = this.TimeFire - delta. public void DecTimeFire(UInt32 delta) { timeFire -= delta; } // Wrapper for this.TimeFire = this.TimeFire + this.Delay. public UInt32 IncTimeFireByDelay() { return timeFire += Delay; } } }
TimerId.cs
using System; namespace MyScheduling { /// <summary> /// Common id-related functionality between our timer managers. /// </summary> public class TimerId { // This is exposed so that timer ID member variables can have a default "unset" value. // The timer manager's factory method will always return a valid timer ID however. public const uint INVALID = 0; public static uint GetNext(ref uint idLast) { // Increment and check for rollover. if (++idLast == INVALID) { // This is just here for completeness. // It'd take a multi-month play session to trigger it. throw new Exception("Timer id rollover has occurred"); } return idLast; } } }
TimerList.cs
using System; using System.Collections.Generic; namespace MyScheduling { /// <summary> /// Priority list specifically for ordering which timer will fire next. /// </summary> public class TimerList { // Raw public access for speed. public List<Timer> Timers; public TimerList() { Timers = new List<Timer>(); } // Inserts the timer by increasing fire time order. Caller is responsible // for ensuring that timer is not null, and is not already in the list. public virtual int Add(Timer timer) { UInt32 timeFire = timer.TimeFire; for (int i = 0, count = Timers.Count; i < count; i++) { if (timeFire < Timers[i].TimeFire) { Timers.Insert(i, timer); return i; } } Timers.Add(timer); return Timers.Count - 1; } // Reinsert the first timer in the list back into the list given its current fire time. // Returns its new position. Caller ensures the list is non-empty to start with. public int ReprioritizeFirst() { Timer timer = Timers[0]; UInt32 timeFire = timer.TimeFire; // Bubble insert the timer into its new position. for (int i = 1, count = Timers.Count; i < count; i++) { Timer otherTimer = Timers[i]; if (timeFire < otherTimer.TimeFire) { return i - 1; } Timers[i] = timer; Timers[i - 1] = otherTimer; } return Timers.Count - 1; } // Decrement all timer fire times in the list by the given amount. public void Rebase(UInt32 amount) { for (int i = 0, count = Timers.Count; i < count; i++) { Timers[i].DecTimeFire(amount); } } } }
TimerManager.cs
using System; using System.Collections.Generic; namespace MyScheduling { /// <summary> /// Base class for managing timers. /// Timer delay is in milliseconds (number of ms between timer callbacks). /// A timer id is returned from the factory method. /// Creators of repeating timers are responsible for killing them. /// One-shot timers are killed automatically after one firing. /// When you create timers, they won't start firing until the next frame. /// So, a 0ms-delay one-shot timer is useful to "do something soon but not right now." /// </summary> public class TimerManager { // So that we can handle rollover internally, we have a max delay. We use this to // safely re-base times on Timer instances periodically. private const UInt32 ONE_DAY = 1000 * 3600 * 24; private const UInt32 MAX_DELAY = 5 * ONE_DAY; // This is how often we rebase. Once a minute is good because it's frequently enough // that it gets exercised during a normal run, and any bugs would show up quickly. // Important: MAX_DELAY + REBASE_TIME must be strictly less than 0xffffffff. private const UInt32 REBASE_TIME = 60000; // Used for when we have no timers we're currently managing. protected const UInt32 INFINITY = 0xffffffff; // Used for assigning a unique timer id for every timer created. // A uint is plenty of room for the number of timers we support. We can't support 4B // unique timers for memory reasons, and even if they are created and destroyed on // every frame, it'd take months of continuous gameplay before rollover occurred. private uint idLast; // Using a priority list so we can lookup which timers to fire quickly on each frame. // NOTE: We could possibly use a rebase-friendly form of a PriorityQueue if this list // is ever proven to be slow in practice. private TimerList timers; // Our internal global time. To avoid rollover we reset this to zero periodically. private UInt32 timeNow; // For optimization, we only need to know when the next timer is going to fire. That // way we don't have to iterate our timer list every frame. private UInt32 timeNext; // We copy the timer references we want to fire, in case the callbacks reenter. private List<Timer> timersToFire; public TimerManager() { idLast = TimerId.INVALID; timers = new TimerList(); timeNow = 0; timeNext = INFINITY; timersToFire = new List<Timer>(); } // Create an optionally-repeating timer with a custom object associated with it. protected uint CreateTimer(UInt32 delay, bool repeat, TimerDelegate callback, object cookie) { if (delay > MAX_DELAY) { throw new Exception(string.Format("Timer delay {0} exceeds maximum {1}", delay, MAX_DELAY)); } if (delay == 0) { // We don't support this because it would complicate our priority calculation. // Typically a zero-delay timer comes from code that is creating one-shot // timers with dynamic delays, and a zero-delay just means "call me next frame". // So, using 1ms will also produce that result and it works in the optimization. delay = 1; } if (callback == null) { throw new Exception("Null timer callback not supported nor useful"); } uint id = TimerId.GetNext(ref idLast); Timer timer = new Timer(id, delay, repeat, callback, cookie); UInt32 timeFire = timeNow + delay; timer.TimeFire = timeFire; if (timers.Add(timer) == 0) { // If we're the new highest priority timer, we're the next to fire. timeNext = timeFire; } return id; } // Kill the timer with the given id. It'll stop firing after this. protected void KillTimer(uint id) { List<Timer> list = timers.Timers; for (int i = 0, count = list.Count; i < count; i++) { if (list[i].Id == id) { RemoveTimerAt(i); break; } } } // Trigger and Kill the timer with the given id. protected void TriggerKillTimer(uint id) { List<Timer> list = timers.Timers; for (int i = 0, count = list.Count; i < count; i++) { Timer timer = list[i]; if (timer.Id == id) { // Trigger the timer. timer.Callback(timer.Id, timer.Cookie); // Kill the timer. RemoveTimerAt(i); break; } } } // Helper function for killing timers. This is how we kill the i'th timer internally. private void RemoveTimerAt(int i) { // The Timer will go out of scope and subject itself to garbage collection. timers.Timers.RemoveAt(i); // If we just removed the highest priority timer, refigure the next one. if (i == 0) { SetTimeNext(); } } // Convenience function for killing a timer and resetting its id to invalid. public void EnsureTimerKilled(ref uint id) { if (id != TimerId.INVALID) { // Assumes that subclasses timer killers just call KillTimer(). KillTimer(id); id = TimerId.INVALID; } } // Sets timeNext based on the current highest priority timer in the priority list. private void SetTimeNext() { timeNext = timers.Timers.Count == 0 ? INFINITY : timers.Timers[0].TimeFire; } // Called by subclasses to process timers after the given millisecond delta. protected void OnDeltaTime(UInt32 dt) { timeNow += dt; // Detect when to rebase. if (timeNow >= REBASE_TIME) { // timeNext will be at infinity precisely when there are no timers. if (timeNext == INFINITY) { timeNow -= REBASE_TIME; } else if (timeNext >= REBASE_TIME) // Don't go below zero when rebasing timers. { timeNow -= REBASE_TIME; timeNext -= REBASE_TIME; timers.Rebase(REBASE_TIME); } else { // If we didn't rebase this time, then once the next timer fire time gets // above the rebase time, we'll rebase then. Likely to be on the next frame // since this condition means those timers will fire this frame and if they // are repeating, they'll be > timeNow >= REBASE_TIME on the next frame. } } // Common case, early out when the next timer isn't firing this time. if (timeNext > timeNow) { return; } // We maintain a list of them to fire independently of our master list. That // way we don't have to worry if the master list gets modified by the callback. int fired = 0; int availableSlots = timersToFire.Count; // Iterate until we see the highest priority timer being set to fire in the future. List<Timer> list = timers.Timers; int count = list.Count; // Guaranteed non-zero on the first iteration. for (;;) { // The first one in the list is always the next one to be fired. Timer timer = list[0]; // Once we hit a timer that isn't firing now, stop processing the priority list. if (timer.TimeFire > timeNow) { break; } // Set this timer into an available slot, or increase the size of the list. if (fired < availableSlots) { timersToFire[fired] = timer; } else { timersToFire.Add(timer); } fired++; if (timer.Repeat) { // Loop in case this repeating timer is faster than frame frequency. for (;;) { if (timer.IncTimeFireByDelay() > timeNow) { // This timer may wind up as first in the list again, but in that // case the surrounding loop will see it's past timeNow and break. timers.ReprioritizeFirst(); break; } // The same code as above, inlined for speed. if (fired < availableSlots) { timersToFire[fired] = timer; } else { timersToFire.Add(timer); } fired++; } } else { list.RemoveAt(0); if (--count == 0) { break; } } } if (fired > 0) { // Compute the next time a timer will fire. SetTimeNext(); // Fire the timers last in case their callbacks wind up registering others. for (int i = 0; i < fired; i++) { Timer timer = timersToFire[i]; timersToFire[i] = null; // Clear the slot to keep allocations minimal. timer.Callback(timer.Id, timer.Cookie); } } } } }
使用:
TestTimer.cs
using UnityEngine; using System.Collections; //using MyTimer; using System; using System.Timers; using MyScheduling; public class TestTimer : MonoBehaviour { uint timerId; uint timerId2; SimTimeEngine simTimeEngine; // Use this for initialization int testCount = 100; int testCount2 = 200; void Start () { simTimeEngine = new SimTimeEngine(33); new SimTimerManager(); } // Update is called once per frame void Update () { if (simTimeEngine!= null) { simTimeEngine.OnUpdate(); } } public void timercallback(uint id, object cookie) { print("----------------------timercallback " + testCount--); } public void timercallback2(uint id, object cookie) { print("----------------------timercallback2 " + testCount2--); } void OnGUI() { //启动计时器 if (GUI.Button(new Rect(150, 100, 220, 30), "start")) { Debug.Log("--------start"); timerId = Service.Get<SimTimerManager>().CreateSimTimer(1000, true, timercallback, null); timerId2 = Service.Get<SimTimerManager>().CreateSimTimer(2000, true, timercallback2, null); Debug.Log("--------timerId = " + timerId); Debug.Log("--------timerId2 = " + timerId2); } //停止第一个计时器 if (GUI.Button(new Rect(150, 200, 220, 30), "stop")) { Debug.Log("--------stop"); Service.Get<SimTimerManager>().KillSimTimer(timerId); } } }
运行结果
相关文章推荐
- 解读Unity中的CG编写Shader系列3——表面剔除与剪裁模式
- 解读Unity中的CG编写Shader系列2——shader的输入输出参数
- 解读Unity中的CG编写Shader系列1——初识CG
- Unity MVC:如何提升游戏开发质量
- unity EasyTouch虚拟摇杆的使用(边界情况处理)
- Unity3D学习资源—C#里氏转换
- Unity学习之切水果游戏
- Unity3D 游戏加密解密那些事
- ShaderLab Tutorials-Alpha Testing
- 【Unity】UnityEditor.UI.dll timestamps but is not known in guidmapper...
- ShaderLab Tutorials-Alpha Blending-1
- ShaderLab Tutorials-Additive Vert Color Blend
- unity 游戏性能优化
- RenderTexture实现实时阴影绘制
- 重置Ubuntu 14.04中的Unity和Compiz
- Using abstractions and interfaces with Unity3D
- Ubuntu下的UNITY和GNOME界面
- unityscript OnGUI无法生成GUI界面。
- iTween.MoveTo用法
- Unity3d 镜面反射 vertex and frag Shader源代码