IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 游戏开发 -> 【案例设计】基于时间的事件响应管理器 -> 正文阅读

[游戏开发]【案例设计】基于时间的事件响应管理器

开发平台: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 考虑计时器的基础属性

??计时器作为统筹时间变化的重要,必要的参数包括如下:

参数数据类型说明
StartTimedecimal计时器 起始时间
EndTimedecimal计时器 终止时间
LerpTimedecimal计时器 插值时间(限制时间精度)
IsPositiveTimebool判断 计时器为 正序 or 逆序,
OnTimeEventDictionary<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 上偶然看见同开发者自己开发作品时萌发的设想。虽然他的视频内容我并没有太多关于他讲解时的记忆。但基于时间驱动事件这一需求设计,深深的给我留下了印象。以思考 —— 如果是我,我会怎么去实现这个需求?这篇文章也并非完整的内容。仅提供实现方式的思路参考。后续仍不定期更新完善内容。

  游戏开发 最新文章
6、英飞凌-AURIX-TC3XX: PWM实验之使用 GT
泛型自动装箱
CubeMax添加Rtthread操作系统 组件STM32F10
python多线程编程:如何优雅地关闭线程
数据类型隐式转换导致的阻塞
WebAPi实现多文件上传,并附带参数
from origin ‘null‘ has been blocked by
UE4 蓝图调用C++函数(附带项目工程)
Unity学习笔记(一)结构体的简单理解与应用
【Memory As a Programming Concept in C a
上一篇文章      下一篇文章      查看所有文章
加:2022-10-08 21:12:26  更:2022-10-08 21:13:19 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2025年1日历 -2025/1/17 5:57:47-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码