开发平台:Unity 2021 编程语言:CSharp 编程平台:Visual Studio 2022 ?
一、前言
??程序运行的特性是 逐行关联即执行。在执行单行代码语句的效率上基于物理设备基础与底层架构逻辑。对一般程序员来看,只需要学会应用即可。但有时候,并不期望于其立即执行,甚至是不断的自我检测、等待若干时间、等待若干帧后执行。于是有了计时器的说法。本文重点记录计时器的构想与设计。 ?
二、思考:计时器类型表现
类型 | 描述 |
---|
顺序 | 从单位时间 0 开始计数或计次。每次值累加固定单位值。默认情况下,无上限值。即启用即不停止,直至应用程序结束。 | 逆序 | 从固定时间 任意值 开始计数或计次。每次值累减固定值。默认情况下,存在下限值 0。当应用程序强制关闭或到达下限值时,停止当前逻辑。 |
?
三、思考:目前能设想的计时器实现方式(从 Unity 角度构想)
3.1 实现一:Unity 生命周期 Update/FixedUpdate
public void Updaete() { while(timer <= 0) { timer -= Time.deltaTime; } }
public void FixedUpdate() { while(timer <= 0) { timer -= Time.FixedTime; } }
??使用 Update 更新,在一定程度上解决计时所带来的问题。但在其他方面上,Update 中计算计时选项不是明举的实现方法,于是较于 Update 有了 FixedUpdate 的更新方案。因为计时器,记录的是事件发生的时间节点。一般的,对于个人开发者而言,时间的计算精确并不需要精确至 毫秒。所以 FixedUpdate 在整体刷新上并不会较 Update 次数多且耗能大。也是不错的优化方案。但最终不是最佳。 ?
3.2 实现二:IEnumerator 协程(常用)
? ??协程是目前 Unity 开发者中最为常用的计时器实现方式。其特点用时则用,无用时无需调用(即 与 Update 相比,不需要持续更新,或在 Update 中添加 bool 判断环节是否执行等特点),每次使用 yield return new WaitForSecond()\WaitForEndOfFrame() 等来延迟执行。
public IEnumerator DoTimeCount()
{
yield return new WaitForSeconds(1f);
timer += 1f;
TimerEvent?.Invoke(timer);
StartCoroutine(DoTimeCount());
}
- 协程特点:基于主线程上,单开一分支独立运行。不影响原主分支执行。
- 依赖于 MonoBahaviour 实现 IEnumerator 操作(对 非
MonoBehaviour 的类对象无法调用) ?
四、思考:基于 IEnumerator 的计时器类封装
4.1 解决依赖问题
??IEnumerator 的启用基于 MonoBehaviour 基础上进行。非 MonoBehaviour 继承的子项无法使用 IEnumerator 进行协程操作。故其实现上应基于该对象上进行。但从以下两个角度进行思考,选择 继承 MonoBahviour 的 Mono单例 将更具备优势。
- 单例模式下,调用方法更加直观、快速。无需实例化对象。
- 计时器作为辅助功能,无需准备额外的 Prefab 对象用于加载时程序添加对象,或 持续存在于场景内上。从调用与项目维护上,这是不可取的行为。但在
MonoBehaviour 环境下的单例模式,建立调用时,若场景无该对象,则程序创建该对象并完成引用的行为。减少了资产中需存储特定对象的 Prefab 并管理的麻烦性。
?
4.2 考虑计时器的基础属性
??计时器作为统筹时间变化的重要,必要的参数包括如下:
参数 | 数据类型 | 说明 |
---|
StartTime | decimal | 计时器 起始时间 | EndTime | decimal | 计时器 终止时间 | LerpTime | decimal | 计时器 插值时间(限制时间精度) | IsPositiveTime | bool | 判断 计时器为 正序 or 逆序, | OnTimeEvent | Dictionary<decimal, Action> | 记录 时间戳事件 |
- 从精度角度考虑,
float 的精确度存在较小的误差,但为了保证计时器在确切的时间。例如 0.1m ,而非 0.1000002231f 。使用十进制 decimal 数据类型作为标准,后续设计提供基础奠定。 - 从程序设计角度上考虑,参数变量过多的情况,并不建议使用复数的局部变量作为配置。相比较可参考 事件 的设计思想,将变量集中于 类 中以供调用实现。
?
4.3 考虑计时器的拓展设计
??实际上,计时器并非仅用于计时,在应用层面上,涉及程序逻辑响应 + 时间显示。例如 0 -10s 的计时器,以单位1s作为过渡。在每个时间戳结点下,调用对应的方法与内容,以强化时间事件效果。具体如下:
- 加入 第2s时,执行方法体
Debug.Log("Hello 2s, taking fire! Now!") - 加入 第2s时,执行方法体
Debug.Log("Hello 2s, Where are u from ?") - 加入 第4s时,执行方法体
Debug.Log("4s, R U Hungry?") - 加入 第9s时,执行方法体
Debug.Log("The Time will over soon")
??那么 Action 将毫无疑问的成为程序开发的首选选项。为什么?每一秒时间响应的方法内容并不局限于一种方法的内容执行。例如 在第 2s 时,添加新的方法体内容以执行。从解耦度角度考虑,新添方法体 + 原方法体 之间互不影响干涉,即两者之一的有无均不会影响 第2s 时刻的时间事件执行。则 委托、事件 是最直接可用的方法。在时间事件的注册上也更便捷快捷。 ?
五、程序设计
5.1 关于 GameTime 的说明
public class GameTime : MonoSington<GameTime>
{
public static Coroutine DoCoroutine(TimeData data)
{
return Instance.StartCoroutine(DoPositiveTime(data));
}
private static IEnumerator DoPositiveTime(TimeData data)
{
yield return new WaitForSeconds(data.LerpTime);
data.CurrentTime += data.LerpTime;
data.TimeAction[data.CurrentTime]?.Invoke();
if(data.CurrentTime <= data.EndTIime)
{
Instance.StartCoroutine(DoPositiveTime(data));
yield break;
}
Debug.Log($"[Time Coroutinue] The Time Event Is Finish");
}
}
- 从实现角度上:
- 使用
MonoSingleton 的 Mono 单例模式实现 Mono 环境下调用 Coroutinue 的前置条件(重要)。 - 选择静态方法。直接同通过
GameTime.DoCoroutine(new Time()); 快速启用协程,降低程序上调用的复杂性。 - 从方法命名角度上,
- 使用 Positive / Nagetive 以区分计时器的正反顺序。
- 更多可优化与补充内容:
- TimeData 中注册的
TimeAction 未提供便捷的初始化方法。 可构建 public TimeAction(decimal startTime, decimal endTime, decimal lerpTime) 用于初始化 TimeData 类。 - 在使用习惯角度上,集成 事件注册、注销、启用、禁用 方法于 GameTime 类是最佳的选择。
无需在 new 之后,引用新建对象中的事件注册进行。(记住多的API倒不如集合在一起,自己找)。 - 在判断时间事件属于 顺序计时/逆序计时 上可进行数据判断,执行对应的数据内容。
扩展 DoCoroutine ,增加判断计时器应用类型方法。 - 考虑多类型的计时器可能同时运行,未区别其计时器具体属于何种应用的计时器,可添加
Name 属性用于 Debug 识别。
?
5.2 关于 TimeData 的说明
public class TimeData
{
private decimal currentTime;
public decimal CurrentTime { get { return currentTime; }}
public decimal StartTime { get; set; }
public decimal EndTime { get; set; }
public decimal LerpTime { get; set; }
public Dictionary<decimal, Action> TimeAction { get; set; }
public TimeData(decimal startTime, decimal endTime, decimal lerpTime)
{
this.currenTime = startTime;
this.StartTime = startTime;
this.EndTime = endTime;
this.LerpTime = lerpTime;
this.OnTimeEvent = new Dictionary<decimal, Action>();
}
}
- TimeData:配置事件计时器相关属性与事件的配置文件。
- 一般情况下,一个时间触发计时器需要 开始时间、结束时间、时间插值。随着需求变化,增加展示当前数据、区别计时器类型等内容。(视实际情况而定)或许
Action 也会因为实际需求改用 Actoin<int> 或其他也说不定。
六、后记
??关于时间管理的事件管理器是作者在 BILIBILI 上偶然看见同开发者自己开发作品时萌发的设想。虽然他的视频内容我并没有太多关于他讲解时的记忆。但基于时间驱动事件这一需求设计,深深的给我留下了印象。以思考 —— 如果是我,我会怎么去实现这个需求?这篇文章也并非完整的内容。仅提供实现方式的思路参考。后续仍不定期更新完善内容。
|