Unity-无限滚动列表实现Timer时间管理实现

今天我们来做一个UI里经常做的东西:无限滚动列表。

首先我们得写清楚实现的基本思路:

所谓的无限滚动当然不是真的无限滚动,我们只要把离开列表的框再丢到列表的后面就行,核心理念和对象池是类似的。

我们来一点一点实现:

首先是:

public enum UICyclicScrollDirection {Vertical,Horizontal
}

滚动列表的方向枚举,竖直和水平。

public class ViewCellBundle<TCell> : IPoolObject where TCell : MonoBehaviour {public int index;          // 当前Bundle在数据源中的起始索引public Vector2 position;   // 在Content中的锚点位置public TCell[] Cells { get; } // 单元格对象数组public int CellCapacity => Cells.Length; // 当前Bundle的容量public ViewCellBundle(int capacity) {Cells = new TCell[capacity]; // 预初始化对象池}public void Clear() {index = -1;foreach(var cell in Cells) {cell.gameObject.SetActive(false); // 对象池回收逻辑}}
}

这个是我们的视图单元格类,支持泛型的同时带有约束。

[SerializeField] protected C _cellObject;      // 单元格预制体
[SerializeField] protected RectTransform content; // 内容容器
[SerializeField] private RectTransform _viewRange; // 可视区域矩形
[SerializeField] private Vector2 _cellSpace;    // 单元格间距
private LinkedList<ViewCellBundle<C>> viewCellBundles; // 当前显示的Bundle链表

我们用一个LinkedList来存储单元格。

public Vector2 ItemSize => CellSize + _cellSpace; // 单元格+间距的总尺寸
private Vector2 CellSize => _cellRectTransform.sizeDelta; // 原始单元格尺寸

这里只是定义了一个总尺寸和一个单元格尺寸,这里可以说一下=>,C++中这个一般用于lambda表达式,但是在这里:

public virtual void Initlize(ICollection<D> datas, bool resetPos = false) {_cellRectTransform = _cellObject.GetComponent<RectTransform>();Datas = datas;RecalculateContentSize(resetPos); // 根据数据量计算Content尺寸UpdateDisplay(); // 初始渲染
}public void Refrash(bool resetContentPos = false) {RecalculateContentSize(resetContentPos);UpdateDisplay(); // 数据变化时重新渲染
}

 初始化函数中,我们输入数据以及一个bool变量来表示是否有更新Content,我们获取单元格的transform与数据,然后根据传入的bool变量来决定是否重新计算Content尺寸之后进行初始渲染;

Refrash函数中就是负责重新计算尺寸和渲染的,用于需要更新Content时。

private void UpdateDisplay() {RemoveHead(); RemoveTail();if(viewCellBundles.Count == 0) {RefreshAllCellInViewRange(); // 初始填充可视区域} else {AddHead(); // 滚动时动态扩展AddTail();}RemoveItemOutOfListRange(); // 清理越界元素
}

 渲染函数中,我们先移除头部尾部元素,之后如果链表的长度为0则一口气填充所有可视区域,否则我们就填充滑动离开列表的空缺,对于已经离开列表的元素我们进行清除。

private void AddHead() {// 计算需要新增的Bundle位置while(OnViewRange(newHeadPos)) {var bundle = GetViewBundle(index, pos);viewCellBundles.AddFirst(bundle);}
}private bool OnViewRange(Vector2 pos) {// 判断坐标是否在可视区域内[1](@ref)return viewDirection == UICyclicScrollDirection.Horizontal ? !InViewRangeLeft(pos) && !InViewRangeRight(pos): !AboveViewRange(pos) && !UnderViewRange(pos);
}

 添加头部元素的函数中,我们首先从链表中找到用于填充的视图元素和填充的位置,然后用LinkedList中的AddFirst方法填充到到链表头部。

判断坐标是否在可视范围内,我们根据滑动列表的方向来判断是否超过范围。

public int GetIndex(Vector2 position) {return viewDirection == UICyclicScrollDirection.Vertical ? Mathf.RoundToInt(-position.y / ItemSize.y) : Mathf.RoundToInt(position.x / ItemSize.x);
}public Vector2 CaculateRelativePostion(Vector2 curPosition) {// 将绝对坐标转换为相对Content的坐标return viewDirection == UICyclicScrollDirection.Horizontal ? new Vector2(curPosition.x + content.anchoredPosition.x, curPosition.y): new Vector2(curPosition.x, curPosition.y + content.anchoredPosition.y);
}

GetIndex函数的作用是根据具体的坐标得到具体的序号,RoundToInt的用法就是一个基于四舍五入的将浮点数转换成整数的方法。

坐标转换方法则是一个基于锚点位置来计算相对位置的过程。

效果如图。

关于定时器:

Unity是有自己的定时器的:

这些方法各有优劣,但是总的来说:

所以我们需要一个更独立(不依赖MonoBehavior等)、更精准(误差更小)、更灵活(不用频繁地通过如StopCoroutine方法来控制)的定时器方法。

public float Duration { get; }          // 定时器总时长(秒)
public bool IsLooped { get; }           // 是否循环执行
public bool IsCompleted { get; private set; } // 是否完成(非循环任务完成时设置)
public bool UsesRealTime { get; }       // 使用游戏时间(Time.time)或真实时间(Time.realtimeSinceStartup)
public bool IsPaused => _timeElapsedBeforePause.HasValue; // 暂停状态
public bool IsCancelled => _timeElapsedBeforeCancel.HasValue; // 取消状态
public bool IsDone => IsCompleted || IsCancelled || IsOwnerDestroyed; // 终止条件

定义了一系列变量,都有注释。

public static Timer Register(float duration, Action onComplete, Action<float> onUpdate = null,bool isLooped = false, bool useRealTime = false, MonoBehaviour autoDestroyOwner = null){if (_manager == null){var managerInScene = Object.FindObjectOfType<TimerManager>();if (managerInScene != null){_manager = managerInScene;}else{var managerObject = new GameObject { name = "TimerManager" };_manager = managerObject.AddComponent<TimerManager>();}}var timer = new Timer(duration, onComplete, onUpdate, isLooped, useRealTime, autoDestroyOwner);_manager.RegisterTimer(timer);return timer;}public static Timer Register(float duration, bool isLooped, bool useRealTime, Action onComplete) {return Register(duration, onComplete, null, isLooped, useRealTime);}

这里是两个重载的静态方法,我们首先判断场景中是否有manager,没有的话就新建一个manager,这个manager是我们实现时间管理的基础和载体。

生成一个Timer,也就是定时器,包含一系列参数如持续时长,计时完成的回调,每帧更新的回调,是否循环等。我们生成定时器之后把定时器加入manager的列表中并返回定时器。

下面还有一个简化版的注册方法,没有每帧调用的回调函数参数,显然更符合不需要实时更新的定时器。

    /// <summary>/// Cancels a timer. The main benefit of this over the method on the instance is that you will not get/// a <see cref="NullReferenceException"/> if the timer is null./// </summary>/// <param name="timer">The timer to cancel.</param>public static void Cancel(Timer timer){timer?.Cancel();}/// <summary>/// Pause a timer. The main benefit of this over the method on the instance is that you will not get/// a <see cref="NullReferenceException"/> if the timer is null./// </summary>/// <param name="timer">The timer to pause.</param>public static void Pause(Timer timer){timer?.Pause();}/// <summary>/// Resume a timer. The main benefit of this over the method on the instance is that you will not get/// a <see cref="NullReferenceException"/> if the timer is null./// </summary>/// <param name="timer">The timer to resume.</param>public static void Resume(Timer timer){if (timer != null){timer.Resume();}}public static void CancelAllRegisteredTimers(){if (Timer._manager != null){Timer._manager.CancelAllTimers();}// if the manager doesn't exist, we don't have any registered timers yet, so don't// need to do anything in this case}public static void PauseAllRegisteredTimers(){if (Timer._manager != null){Timer._manager.PauseAllTimers();}// if the manager doesn't exist, we don't have any registered timers yet, so don't// need to do anything in this case}public static void ResumeAllRegisteredTimers(){if (Timer._manager != null){Timer._manager.ResumeAllTimers();}// if the manager doesn't exist, we don't have any registered timers yet, so don't// need to do anything in this case}

写了一系列方法:Cancel,Pause,Resume,CancelAllRegisteredTimers,PauseAllRegisteredTimers,ResumeAllRegisteredTimers。总的来说就是针对单个计时器的删除,暂停和复原以及针对所有已注册的计时器的删除,暂停和复原。这些都是静态方法,显然是专门针对我们的静态变量,也就是我们的manager的。

/// <summary>/// Stop a timer that is in-progress or paused. The timer's on completion callback will not be called./// </summary>public void Cancel(){if (IsDone){return;}_timeElapsedBeforeCancel = GetTimeElapsed();_timeElapsedBeforePause = null;}/// <summary>/// Pause a running timer. A paused timer can be resumed from the same point it was paused./// </summary>public void Pause(){if (IsPaused || IsDone){return;}_timeElapsedBeforePause = GetTimeElapsed();}/// <summary>/// Continue a paused timer. Does nothing if the timer has not been paused./// </summary>public void Resume(){if (!IsPaused || IsDone){return;}_timeElapsedBeforePause = null;}/// <summary>/// Get how many seconds have elapsed since the start of this timer's current cycle./// </summary>/// <returns>The number of seconds that have elapsed since the start of this timer's current cycle, i.e./// the current loop if the timer is looped, or the start if it isn't.////// If the timer has finished running, this is equal to the duration.////// If the timer was cancelled/paused, this is equal to the number of seconds that passed between the timer/// starting and when it was cancelled/paused.</returns>public float GetTimeElapsed(){if (IsCompleted || GetWorldTime() >= GetFireTime()){return Duration;}return _timeElapsedBeforeCancel ??_timeElapsedBeforePause ??GetWorldTime() - _startTime;}/// <summary>/// Get how many seconds remain before the timer completes./// </summary>/// <returns>The number of seconds that remain to be elapsed until the timer is completed. A timer/// is only elapsing time if it is not paused, cancelled, or completed. This will be equal to zero/// if the timer completed.</returns>public float GetTimeRemaining(){return Duration - GetTimeElapsed();}/// <summary>/// Get how much progress the timer has made from start to finish as a ratio./// </summary>/// <returns>A value from 0 to 1 indicating how much of the timer's duration has been elapsed.</returns>public float GetRatioComplete(){return GetTimeElapsed() / Duration;}/// <summary>/// Get how much progress the timer has left to make as a ratio./// </summary>/// <returns>A value from 0 to 1 indicating how much of the timer's duration remains to be elapsed.</returns>public float GetRatioRemaining(){return GetTimeRemaining() / Duration;}

这里就是上述manager方法中具体调用的函数

首先依然是我们的删除,暂停和重启:

剩下的函数用于查询计时器状态:

    #region Private Static Properties/Fields// responsible for updating all registered timersprivate static TimerManager _manager;#endregion#region Private Properties/Fieldsprivate bool IsOwnerDestroyed => _hasAutoDestroyOwner && _autoDestroyOwner == null;private readonly Action _onComplete;private readonly Action<float> _onUpdate;private float _startTime;private float _lastUpdateTime;// for pausing, we push the start time forward by the amount of time that has passed.// this will mess with the amount of time that elapsed when we're cancelled or paused if we just// check the start time versus the current world time, so we need to cache the time that was elapsed// before we paused/cancelledprivate float? _timeElapsedBeforeCancel;private float? _timeElapsedBeforePause;// after the auto destroy owner is destroyed, the timer will expire// this way you don't run into any annoying bugs with timers running and accessing objects// after they have been destroyedprivate readonly MonoBehaviour _autoDestroyOwner;private readonly bool _hasAutoDestroyOwner;

生成唯一的静态变量实例_manager。

定义两个只读委托:计时完成和每帧更新,两个浮点数:开始时间和最后更新时间,取消前时长和暂停前时长,至于最后的两个DestoryOwner:

一个是显式的自动销毁标志,一个则是判断MonoBehavior是否存在的自动销毁。_autoDestroyOwner实现的是定时器与宿主对象的生命周期绑定,而TimerManager作为全局单例独立存在。只有当显式启用_hasAutoDestroyOwner且宿主对象销毁时,定时器自身才会终止,但不会影响_manager的存活状态

private Timer(float duration, Action onComplete, Action<float> onUpdate,bool isLooped, bool usesRealTime, MonoBehaviour autoDestroyOwner){Duration = duration;_onComplete = onComplete;_onUpdate = onUpdate;IsLooped = isLooped;UsesRealTime = usesRealTime;_autoDestroyOwner = autoDestroyOwner;_hasAutoDestroyOwner = autoDestroyOwner != null;_startTime = GetWorldTime();_lastUpdateTime = _startTime;}

构造函数,主要就是获取输入参数。

private float GetWorldTime(){return UsesRealTime ? Time.realtimeSinceStartup : Time.time;}private float GetFireTime(){return _startTime + Duration;}private float GetTimeDelta(){return GetWorldTime() - _lastUpdateTime;}

三个获取时间的函数,第一个函数提供两个时间基准:不受游戏影响的真实时间和受游戏影响的游戏逻辑时间;第二个函数计算预期的计时结束时间;第三个函数计算两次更新之间的时间间隔。

private void Update(){if (IsDone){return;}if (IsPaused){_startTime += GetTimeDelta();_lastUpdateTime = GetWorldTime();return;}_lastUpdateTime = GetWorldTime();if (_onUpdate != null){_onUpdate(GetTimeElapsed());}if (GetWorldTime() >= GetFireTime()){if (_onComplete != null){_onComplete();}if (IsLooped){_startTime = GetWorldTime();}else{IsCompleted = true;}}}

Update生命周期函数,如果计时完成则返回,如果暂停则:把更新间隔的时间加到开始时间上,然后更新上次更新时间;暂停结束后再更新一次最后更新时间;如果有每帧更新的回调,我们执行回调(把计时器启动以来的时长作为参数传入);如果时间已经到了计时结束的时间点:如果有计时完成的回调则执行,如果开启了循环计时则更新开始时间,否则返回IsCompleted = true。

private class TimerManager : MonoBehaviour{private List<Timer> _timers = new List<Timer>();// buffer adding timers so we don't edit a collection during iterationprivate List<Timer> _timersToAdd = new List<Timer>();public void RegisterTimer(Timer timer){_timersToAdd.Add(timer);}public void CancelAllTimers(){foreach (var timer in _timers){timer.Cancel();}_timers = new List<Timer>();_timersToAdd = new List<Timer>();}public void PauseAllTimers(){foreach (var timer in _timers){timer.Pause();}}public void ResumeAllTimers(){foreach (var timer in _timers){timer.Resume();}}// update all the registered timers on every frame[UsedImplicitly]private void Update(){UpdateAllTimers();}private void UpdateAllTimers(){if (_timersToAdd.Count > 0){_timers.AddRange(_timersToAdd);_timersToAdd.Clear();}foreach (var timer in _timers){timer.Update();}_timers.RemoveAll(t => t.IsDone);}}

这是我们的管理器类的内部:

我们有两个数组:一个存储timer计时器而另一个存储准备加入数组的计时器。我们首先在这里实现了之前使用过的针对所有计时器的删除、暂停和重启,当然还有注册;然后在我们的update里,我们会把准备加入数组的计时器加入数组并清空另一个数组,然后对数组中每一个计时器执行Update函数(前文已定义),最后我们批量删除满足IsDone的计时器。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/78110.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Docker的基本概念和一些运用场景

Docker 是一种开源的容器化平台&#xff0c;可以帮助开发人员更加高效地打包、发布和运行应用程序。以下是 Docker 的基本概念和优势&#xff1a; 基本概念&#xff1a; 容器&#xff1a;Docker 使用容器来打包应用程序及其依赖项&#xff0c;容器是一个独立且可移植的运行环境…

Unity中基于第三方插件扩展的对于文件流处理的工具脚本

在Unity的项目中对应文件处理,在很多地方用到,常见的功能,就是保存文件,加载文件,判断文件或者文件夹是否存在,删除文件等。 在之前已经写过通过C#的IO实现的这些功能,可查看《Unity C# 使用IO流对文件的常用操作》,但是不能保证所有平台都可以使用 现在基于第三方跨…

Flink介绍——实时计算核心论文之MillWheel论文详解

引入 通过前面的文章&#xff0c;我们从S4到Storm&#xff0c;再到Storm结合Kafka成为当时的实时处理最佳实践&#xff1a; S4论文详解S4论文总结Storm论文详解Storm论文总结Kafka论文详解Kafka论文总结 然而KafkaStorm的第一代流式数据处理组合&#xff0c;还面临的三个核心…

python异步协程async调用过程图解

1.背景&#xff1a; 项目中有用到协程&#xff0c;但是对于协程&#xff0c;线程&#xff0c;进程的区别还不是特别了解&#xff0c;所以用图示的方式画了出来&#xff0c;用于理清三者的概念。 2.概念理解&#xff1a; 2.1协程&#xff0c;线程&#xff0c;进程包含关系 一…

【React】获取元素距离页面顶部的距离

文章目录 代码实现 代码实现 import { useEffect, useRef, useState } from react;const DynamicPositionTracker () > {const [distance, setDistance] useState(0);const divRef useRef(null);useEffect(() > {const targetDiv divRef.current;if (!targetDiv) re…

26.OpenCV形态学操作

OpenCV形态学操作 形态学操作&#xff08;Morphological Operations&#xff09;源自二值图像处理&#xff0c;主要用于分析和处理图像中的结构元素&#xff0c;对图像进行去噪、提取边缘、分割等预处理步骤。OpenCV库中提供了丰富的形态学函数&#xff0c;常见的包括&#xf…

逻辑回归:损失和正则化技术的深入研究

逻辑回归&#xff1a;损失和正则化技术的深入研究 引言 逻辑回归是一种广泛应用于分类问题的统计模型&#xff0c;尤其在机器学习领域中占据着重要的地位。尽管其名称中包含"回归"&#xff0c;但逻辑回归本质上是一种分类算法。它的核心思想是在线性回归的基础上添…

大模型面经 | 介绍一下CLIP和BLIP

大家好,我是皮先生!! 今天给大家分享一些关于大模型面试常见的面试题,希望对大家的面试有所帮助。 往期回顾: 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题一) 大模型面经 | 春招、秋招算法面试常考八股文附答案(RAG专题二) 大模型面经 | 春招、秋招算法…

【MCP】第二篇:IDE革命——用MCP构建下一代智能工具链

【MCP】第二篇&#xff1a;IDE革命——用MCP构建下一代智能工具链 一、引言二、IDE集成MCP2.1 VSCode2.1.1 安装VSCode2.1.2 安装Cline2.1.3 配置Cline2.1.4 环境准备2.1.5 安装MCP服务器2.1.5.1 自动安装2.1.5.2 手动安装 2.2 Trae CN2.2.1 安装Trae CN2.2.2 Cline使用2.2.3 内…

【新能源科学与技术】MATALB/Simulink小白教程(一)实验文档【新能源电力转换与控制仿真】

DP读书&#xff1a;新能源科学与工程——专业课「新能源发电系统」 2025a 版本 MATLAB下面进入正题 仿真一&#xff1a;Buck 电路一、仿真目的二、仿真内容&#xff08;一&#xff09;Buck电路基本构成及工作原理&#xff08;二&#xff09;Buck电路仿真模型及元件连接&#xf…

BootStrap:首页排版(其一)

今天我要介绍的是在BootStrap中有关于首页排版的内容知识点&#xff0c;即&#xff08;模态框&#xff0c;选项卡&#xff09;。 模态框&#xff1a; 模态框经过了优化&#xff0c;更加灵活&#xff0c;以弹出对话框的形式出现&#xff0c;具有最小和最实用的功能集。 在运行…

Spring Data

目录 一、Spring Data 简介与生态概览 什么是 Spring Data&#xff1f; Spring Data 与 Spring Data JPA 的关系 Spring Data 家族&#xff1a;JPA、MongoDB、Redis、Elasticsearch、JDBC、R2DBC…… 与 MyBatis 的本质差异&#xff08;ORM vs SQL 显式控制&#xff09; 二…

建筑末端配电回路用电安全解决方案

一、电气火灾的严峻现状 根据国家应急管理部消防救援局的数据&#xff0c;电气火灾长期占据各类火灾原因之首&#xff0c;2021年占比高达50.4%。其中&#xff0c;末端配电回路因保护不足、监测手段落后&#xff0c;成为火灾高发隐患点。私拉电线、线路老化、接触不良、过载等问…

华为开发岗暑期实习笔试(2025年4月16日)

刷题小记&#xff1a; 第一题怀疑测试样例不完整&#xff0c;贪心法不应该能够解决该题。第二题使用0-1BFS解决单源最短路径的问题&#xff0c;往往搭配双端队列实现。第三题是运用动态规划解决最大不重叠子区间个数的问题&#xff0c;难点在于满足3重判断规则&#xff0c;所需…

Rust: 从内存地址信息看内存布局

内存布局其实有几个&#xff1a;address&#xff08;地址&#xff09;、size&#xff08;大小&#xff09;、alignment&#xff08;对齐位数&#xff0c;2 的自然数次幂&#xff0c;2&#xff0c;4&#xff0c;8…&#xff09;。 今天主要从address来看内存的布局。 说明&…

每日一题算法——两个数组的交集

两个数组的交集 力扣题目链接 我的解法&#xff1a;利用数组下标。 缺点&#xff1a;当取值范围很大时&#xff0c;浪费空间。 class Solution { public:vector<int> intersection(vector<int>& nums1, vector<int>& nums2) {int count1[1001]{0…

c++ 互斥锁

为练习c 线程同步&#xff0c;做了LeeCode 1114题. 按序打印&#xff1a; 给你一个类&#xff1a; public class Foo {public void first() { print("first"); }public void second() { print("second"); }public void third() { print("third"…

山东大学软件学院创新项目实训开发日志(20)之中医知识问答自动生成对话标题bug修改

在原代码中存在一个bug&#xff1a;当前对话的标题不是现有对话的用户的第一段的前几个字&#xff0c;而是历史对话的第一段的前几个字。 这是生成标题的逻辑出了错误&#xff1a; 当改成size()-1即可

WSL2-Ubuntu22.04下拉取Docker MongoDB镜像并启动

若未安装docker可参考此教程&#xff1a;可以直接在wsl上安装docker吗&#xff0c;而不是安装docker desktop&#xff1f;-CSDN博客 1. 拉取镜像 docker pull mongo:latest 2.打开网络加速&#xff0c;再次拉取镜像 3.创建docker-compose.yml 进入vim编辑器后输入i进行编辑&a…

中通 Redis 集群从 VM 迁移至 PVE:技术差异、PVE 优劣势及应用场景深度解析

在数字化转型浪潮下&#xff0c;企业对服务器资源的高效利用与成本控制愈发重视。近期&#xff0c;中通快递将服务器上的 Redis 集群服务从 VM&#xff08;VMware 虚拟化技术&#xff09;迁移至 PVE&#xff08;Proxmox VE&#xff09;&#xff0c;这一技术举措引发了行业广泛关…