您的位置:首页 > 移动开发 > Unity3D

Unity 计时器

2015-10-12 14:39 597 查看

Unity 计时器

ISimTimeObserver.cs

using 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);
}

}
}


运行结果

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: