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.pswp.cn/bicheng/78110.shtml
繁體地址,請注明出處:http://hk.pswp.cn/bicheng/78110.shtml
英文地址,請注明出處:http://en.pswp.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;這一技術舉措引發了行業廣泛關…