推荐阅读

一、前言

今天,来分享一下如何实现定时器。

实现定时器需要搞明白二个问题:

一、定时器如何实现
二、为什么制作定时器系统

首选,我们来了解一下如何在Unity里面实现定时器。

二、Unity 定时器的三种实现方法

在Unity开发程序的时候,会学习到很多实现定时器效果的方法,比如说:

2-1、使用Time.deltaTime累加方式

在Update里面,使用Time.deltaTime实现:

using UnityEngine;

public class Test : MonoBehaviour
{
    public float timer = 0f; 
    
    void Update()
    {
        timer += Time.deltaTime;
        if (timer >= 2)
        {
            doSomething();
            timer = 0f; // 定时2秒
        }
    }

    void doSomething()
    {
        Debug.Log("每2秒执行一次");
    }
}

或者通过Time.time来实现:

using UnityEngine;

public class Test : MonoBehaviour
{
    public float timer = 0f;

    void Start()
    {
        timer = Time.time;
    }

    void Update()
    {
        timer += Time.deltaTime;
        if (Time.time - timer >= 2)// 定时2秒
        {
            doSomething();
            timer = Time.time; 
        }
    }

    void doSomething()
    {
        Debug.Log("每2秒执行一次");
    }
}

2-2、使用延迟调用函数

using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        //0秒后,每2秒执行一次doSomething
        InvokeRepeating("doSomething", 0, 2);
    }

    void doSomething()
    {
        Debug.Log("每2秒执行一次");
    }
}

2-3、使用协程

using System;
using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    void Start()
    {
        //每2秒执行一次doSomething
        StartCoroutine(UpdateTimer(2f, doSomething));
    }

    void doSomething()
    {
        Debug.Log("每2秒执行一次");
    }

    IEnumerator UpdateTimer(float timer,Action callBack)
    {
        var wait = new WaitForSeconds(timer);
        while (true)
        {
            yield return wait;
            callBack();
        }
    }
}

提示:场景内协程多了将会出现明显的卡顿。

可以看出来,这三种实现定时器的方法都很简单,在项目体量不是太大,并且代码量也还算简洁的时候,使用这三种实现方法是没有问题的。

但是,当代码量多的时候,再使用这些方法就会显得冗余杂乱。

这时候就需要一个定时器系统来管理这些定时的任务,这就是为何要制作定时器系统的原因。

三、实现定时器系统

实现原理:

每个定时器对象是一个Timer, 添加的时候交给定时器管理系统, 定时器系统负责管理所有的定时器对象,每次游戏Update的时候,遍历里面的每个定时器对象,

把它们过去的时间增加Time.deltaTime, 当过去的时间达到定时器触发时间的时候,触发定时器调用, 如果达到Timer触发次数,就把这个Timer移除。

否则就重置时间,继续直到下一个时间的触发。

代码:

一个TimerMgr,Timer管理类,用来管理定时器系统:

using System.Collections.Generic;
using UnityEngine;

class TimerNode
{
    public TimerMgr.TimerHandler callback;
    public float repeatRate; // 定时器触发的时间间隔;
    public float time; // 第一次触发要隔多少时间;
    public int repeat; // 你要触发的次数;
    public float passedTime; // 这个Timer过去的时间;
    public bool isRemoved; // 是否已经删除了
    public int timerId; // 标识这个timer的唯一Id号;
}

public class TimerMgr 
{
    public delegate void TimerHandler();
    private Dictionary<int, TimerNode> timers = null;//存放Timer对象

    private List<TimerNode> removeTimers = null;//新增Timer缓存队列
    private List<TimerNode> newAddTimers = null;//删除Timer缓存队列

    private int autoIncId = 1;//每个Timer的唯一标示

    //初始化Timer管理器
    public void Init()
    {
        timers = new Dictionary<int, TimerNode>();
        autoIncId = 1;
        removeTimers = new List<TimerNode>();
        newAddTimers = new List<TimerNode>();
    }

    /// <summary>
    /// 以秒为单位调用方法methodName,然后在每个repeatRate重复调用。
    /// </summary>
    /// <param name="methodName">回调函数</param>
    /// <param name="time">延迟调用</param>
    /// <param name="repeatRate">时间间隔</param>
    /// <param name="repeat">重复调用的次数 小于等于0表示无限触发</param>
    public int Schedule(TimerHandler methodName, float time, float repeatRate, int repeat=0)
    {
        TimerNode timer = new TimerNode();
        timer.callback = methodName;
        timer.repeat = repeat;
        timer.repeatRate = repeatRate;
        timer.time = time;
        timer.passedTime = timer.repeatRate; // 延迟调用
        timer.isRemoved = false;
        timer.timerId = autoIncId;
        autoIncId++;
        newAddTimers.Add(timer); // 加到缓存队列里面
        return timer.timerId;
    }

    //移除Timers
    public void Unschedule(int timerId)
    {
        if (!timers.ContainsKey(timerId))
        {
            return;
        }
        TimerNode timer = timers[timerId];
        timer.isRemoved = true; // 先标记,不直接删除
    }

    //在Update里面调用
    public void Update()
    {
        float dt = Time.deltaTime;
        // 添加新的Timers
        for (int i = 0; i < newAddTimers.Count; i++)
        {
            timers.Add(newAddTimers[i].timerId, newAddTimers[i]);
        }
        newAddTimers.Clear();
        foreach (TimerNode timer in timers.Values)
        {
            if (timer.isRemoved)
            {
                removeTimers.Add(timer);
                continue;
            }
            timer.passedTime += dt;
            if (timer.passedTime >= (timer.time + timer.repeatRate))
            {
                // 做一次触发
                timer.callback();
                timer.repeat--;
                timer.passedTime -= (timer.time + timer.repeatRate);
                timer.time = 0; 
                if (timer.repeat == 0)
                {
                    // 触发次数结束,将该删除的加入队列
                    timer.isRemoved = true;
                    removeTimers.Add(timer);
                }
            }
        }
        // 清理掉要删除的Timer;
        for (int i = 0; i < removeTimers.Count; i++)
        {
            timers.Remove(removeTimers[i].timerId);
        }
        removeTimers.Clear();
    }
}

调用:

using System;
using System.Collections;
using UnityEngine;

public class Test : MonoBehaviour
{
    TimerMgr timer;
    int TimerID;
    void Start()
    {
        //初始化
        timer = new TimerMgr();
        timer.Init();
        //启动定时器
        TimerID = timer.Schedule(doSomething, 0, 2);
    }
    void Update()
    {
        timer.Update();
        if (Input.GetKeyDown(KeyCode.W))
        {
            //关闭定时器
            timer.Unschedule(TimerID);
        }
    }

    private void doSomething()
    {
        Debug.Log("每2秒执行一次");
    }
}

四、后言

这个定时器系统,可以用来处理在开发中可能用到的定时器需求。

不用再去Time.deltatime或者用协程去制作定时器了。

制作完定时器后,在其他项目也可以使用。