Update scripts

This commit is contained in:
Soneoy_Mac
2024-04-16 03:40:27 -07:00
parent d26e1bba1f
commit b776940c48
40 changed files with 1113 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
# Macintosh index files
.DS_Store

View File

@@ -0,0 +1,20 @@
using SimaiSharp.Structures;
namespace AstroDX.Models.Scoring.Metrics.Extended
{
public class HealthStats : IReactiveStatistic
{
public HealthStats(int maxHealth)
{
MaxHealth = maxHealth;
}
public void Push(in NoteType type, in JudgeData data)
{
Loss += GameSettings.Settings.Profile.Mods.LifeDrain.GetHealthLoss(type, data);
}
public int MaxHealth { get; }
public int Loss { get; private set; }
}
}

View File

@@ -0,0 +1,9 @@
using SimaiSharp.Structures;
namespace AstroDX.Models.Scoring.Metrics
{
public interface IReactiveStatistic
{
void Push(in NoteType type, in JudgeData data);
}
}

View File

@@ -0,0 +1,110 @@
using System;
namespace AstroDX.Models.Scoring.Metrics.Internal
{
public sealed class AchievementStats
{
private readonly Statistics _statistics;
private readonly double _baseScore;
public AchievementStats(Statistics statistics)
{
_statistics = statistics;
var totalPointCount = statistics.JudgementStats.MaxTapCount +
statistics.JudgementStats.MaxTouchCount +
statistics.JudgementStats.MaxHoldCount * 2 +
statistics.JudgementStats.MaxSlideCount * 3 +
statistics.JudgementStats.MaxBreakCount * 5;
_baseScore = totalPointCount > 0 ? 100.00 / totalPointCount : 0;
}
/// <param name="precise">Used when displaying achievement as text to avoid double rounding</param>
public double GetAchievement(bool precise = false)
{
var baseAchievement = GetBaseAchievement();
var extraAchievement = _statistics.JudgementStats.BreakRecord.MaxCount > 0
? _statistics.JudgementStats.BreakRecord.Extras /
_statistics.JudgementStats.BreakRecord.MaxCount
: 0;
var totalAchievement = baseAchievement +
extraAchievement;
return precise ? totalAchievement : Math.Round(totalAchievement, 4);
}
private double GetBaseAchievement()
{
if (_baseScore == 0)
return 0;
var tapAchievement = _statistics.JudgementStats.TapRecord.Points
* _baseScore;
var touchAchievement = _statistics.JudgementStats.TouchRecord.Points
* _baseScore;
var holdAchievement = _statistics.JudgementStats.HoldRecord.Points
* _baseScore * 2;
var slideAchievement = _statistics.JudgementStats.SlideRecord.Points
* _baseScore * 3;
var breakAchievement = _statistics.JudgementStats.BreakRecord.Points
* _baseScore * 5;
return tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement;
}
public double CalculateMaxAchievable(bool includeExtras)
{
var tapAchievement = _statistics.JudgementStats.TapRecord.PassedCount * _baseScore;
var touchAchievement = _statistics.JudgementStats.TouchRecord.PassedCount * _baseScore;
var holdAchievement = _statistics.JudgementStats.HoldRecord.PassedCount * _baseScore * 2;
var slideAchievement = _statistics.JudgementStats.SlideRecord.PassedCount * _baseScore * 3;
var breakAchievement = _statistics.JudgementStats.BreakRecord.PassedCount * _baseScore * 5;
return includeExtras && _statistics.JudgementStats.BreakRecord.MaxCount > 0
? tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement +
(double)_statistics.JudgementStats.BreakRecord.PassedCount /
_statistics.JudgementStats.BreakRecord.MaxCount
: tapAchievement + touchAchievement + holdAchievement + slideAchievement + breakAchievement;
}
public ClearType EvaluateClearType(bool whilePlaying = false)
{
var achievement = GetAchievement();
var cleared = achievement >= 80 && _statistics.JudgementStats.AllNotesPassed;
var missed = _statistics.JudgementStats.JudgedMissCount >= 1;
var anyGood = _statistics.JudgementStats.JudgedGoodCount >= 1;
var anyGreat = _statistics.JudgementStats.JudgedGreatCount >= 1;
var chartHasBreaks = _statistics.JudgementStats.MaxBreakCount > 0;
if (!whilePlaying && !cleared)
return ClearType.Failed;
if (missed)
return ClearType.Clear;
if (anyGood)
return ClearType.FullCombo;
if (anyGreat)
return ClearType.FullComboPlus;
if (!chartHasBreaks)
return ClearType.AllPerfect;
var maxBreak = _statistics.JudgementStats.BreakRecord.CriticalCount ==
_statistics.JudgementStats.BreakRecord.PassedCount;
return maxBreak
? ClearType.AllPerfectPlus
: ClearType.AllPerfect;
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using SimaiSharp.Structures;
namespace AstroDX.Models.Scoring.Metrics.Internal
{
public sealed class ComboStats : IReactiveStatistic
{
public long lastCombo { get; private set; }
public long maxCombo { get; private set; }
public void Push(in NoteType type, in JudgeData data = default)
{
if (data.grade == JudgeGrade.Miss)
{
lastCombo = 0;
return;
}
lastCombo++;
maxCombo = Math.Max(lastCombo, maxCombo);
}
}
}

View File

@@ -0,0 +1,49 @@
using System.Linq;
namespace AstroDX.Models.Scoring.Metrics.Internal
{
public sealed class DxScoreStats
{
private readonly Statistics _statistics;
private const int CriticalWeight = 3;
private static readonly int[] Weights =
{
CriticalWeight, 2, 1, 0, 0
};
public DxScoreStats(Statistics statistics) => _statistics = statistics;
public uint TotalDxScore => _statistics.JudgementStats.MaxNoteCount * CriticalWeight;
/// <summary>
/// This is used to generate max health or max dx score.
/// </summary>
public uint MaxAchievableDxScore => _statistics.JudgementStats.JudgedNoteCount * CriticalWeight;
/// <summary>
/// This is used to generate the current health or dx score.
/// </summary>
public uint DxScore =>
(uint)(_statistics.JudgementStats.TapRecord.Judged.Sum(c => Weights[(int)c.grade]) +
_statistics.JudgementStats.TouchRecord.Judged.Sum(c => Weights[(int)c.grade]) +
_statistics.JudgementStats.HoldRecord.Judged.Sum(c => Weights[(int)c.grade]) +
_statistics.JudgementStats.SlideRecord.Judged.Sum(c => Weights[(int)c.grade]) +
_statistics.JudgementStats.BreakRecord.Judged.Sum(c => Weights[(int)c.grade]));
public int GetGrade()
{
var dxScorePercentage = (float)DxScore / TotalDxScore;
// // add 1 for every passed threshold
for (var i = 0; i < DxGradeThresholds.Thresholds.Length; i++)
{
if (dxScorePercentage < DxGradeThresholds.Thresholds[i])
return i;
}
return DxGradeThresholds.Thresholds.Length;
}
}
}

View File

@@ -0,0 +1,38 @@
using AstroDX.Utilities.Extensions;
using SimaiSharp.Structures;
namespace AstroDX.Models.Scoring.Metrics.Internal
{
public sealed class FluctuationStats : IReactiveStatistic
{
public long EarlyCount { get; private set; }
public long LateCount { get; private set; }
public double TotalDeviation { get; private set; }
public long NotesCounted { get; private set; }
public double Deviation => TotalDeviation / NotesCounted;
public void Push(in NoteType type, in JudgeData data)
{
if ((GameSettings.Settings.Profile.Metrics.fluctuationVisibility & data.grade.AsFlag()) == 0)
return;
if (type == NoteType.Slide)
return;
NotesCounted++;
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (data.state)
{
case JudgeState.Early:
EarlyCount++;
break;
case JudgeState.Late:
LateCount++;
break;
}
TotalDeviation += data.offset;
}
}
}

View File

@@ -0,0 +1,139 @@
using System;
using SimaiSharp.Structures;
using UnityEngine;
namespace AstroDX.Models.Scoring.Metrics.Internal
{
public sealed class JudgementStats : IReactiveStatistic
{
public NoteRecord HoldRecord { get; }
public NoteRecord SlideRecord { get; }
public NoteRecord TapRecord { get; }
public NoteRecord TouchRecord { get; }
public NoteRecord BreakRecord { get; }
public uint MaxTapCount { get; }
public uint MaxTouchCount { get; }
public uint MaxHoldCount { get; }
public uint MaxSlideCount { get; }
public uint MaxBreakCount { get; }
public uint JudgedCriticalCount { get; private set; }
public uint JudgedPerfectCount { get; private set; }
public uint JudgedGreatCount { get; private set; }
public uint JudgedGoodCount { get; private set; }
public uint JudgedMissCount { get; private set; }
public uint JudgedNoteCount { get; private set; }
public uint MaxNoteCount =>
TapRecord.MaxCount + TouchRecord.MaxCount +
HoldRecord.MaxCount + SlideRecord.MaxCount +
BreakRecord.MaxCount;
public bool AllNotesPassed =>
TapRecord.MaxCount == TapRecord.PassedCount &&
TouchRecord.MaxCount == TouchRecord.PassedCount &&
HoldRecord.MaxCount == HoldRecord.PassedCount &&
SlideRecord.MaxCount == SlideRecord.PassedCount &&
BreakRecord.MaxCount == BreakRecord.PassedCount;
public JudgementStats(ReadOnlyMemory<NoteCollection> noteCollections)
{
foreach (var noteCollection in noteCollections.Span)
{
foreach (var note in noteCollection)
{
// ReSharper disable once SwitchStatementMissingSomeEnumCasesNoDefault
switch (note.type)
{
case NoteType.Tap:
MaxTapCount++;
break;
case NoteType.Touch:
MaxTouchCount++;
break;
case NoteType.Hold:
MaxHoldCount++;
break;
case NoteType.Break:
MaxBreakCount++;
break;
}
foreach (var slide in note.slidePaths)
{
if (slide.type == NoteType.Break)
MaxBreakCount++;
else
MaxSlideCount++;
}
}
}
TapRecord = new NoteRecord(MaxTapCount);
TouchRecord = new NoteRecord(MaxTouchCount);
HoldRecord = new NoteRecord(MaxHoldCount);
SlideRecord = new NoteRecord(MaxSlideCount);
BreakRecord = new NoteRecord(MaxBreakCount);
}
public void Push(in NoteType type, in JudgeData data)
{
switch (type)
{
case NoteType.Tap:
TapRecord.Push(in data);
break;
case NoteType.Touch:
TouchRecord.Push(in data);
break;
case NoteType.Hold:
HoldRecord.Push(in data);
break;
case NoteType.Slide:
SlideRecord.Push(in data);
break;
case NoteType.Break:
BreakRecord.Push(in data);
BreakRecord.Extras += data.grade switch
{
JudgeGrade.CriticalPerfect => 1,
JudgeGrade.Perfect =>
Mathf.Abs((float)data.offset) <= 0.033335f ? 0.75 : 0.5,
JudgeGrade.Great => 0.4,
JudgeGrade.Good => 0.3,
JudgeGrade.Miss => 0,
_ => 0
};
break;
case NoteType.ForceInvalidate:
default:
break;
}
switch (data.grade)
{
case JudgeGrade.CriticalPerfect:
JudgedCriticalCount++;
break;
case JudgeGrade.Perfect:
JudgedPerfectCount++;
break;
case JudgeGrade.Great:
JudgedGreatCount++;
break;
case JudgeGrade.Good:
JudgedGoodCount++;
break;
case JudgeGrade.Miss:
JudgedMissCount++;
break;
default:
throw new ArgumentOutOfRangeException();
}
JudgedNoteCount++;
}
}
}

View File

@@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using System.Linq;
using AstroDX.Models.Scoring.Metrics.Internal;
using Sigtrap.Relays;
using SimaiSharp.Structures;
namespace AstroDX.Models.Scoring.Metrics
{
[Serializable]
public sealed class Statistics
{
public Relay<NoteType, JudgeData> OnJudgementReceived { get; } = new();
public FluctuationStats FluctuationStats { get; }
public ComboStats ComboStats { get; }
public JudgementStats JudgementStats { get; }
public AchievementStats AchievementStats { get; }
public DxScoreStats DxScoreStats { get; }
private HashSet<IReactiveStatistic> Extensions { get; }
public Statistics(ReadOnlyMemory<NoteCollection> chart)
{
JudgementStats = new JudgementStats(chart);
FluctuationStats = new FluctuationStats();
ComboStats = new ComboStats();
AchievementStats = new AchievementStats(this);
DxScoreStats = new DxScoreStats(this);
Extensions = new HashSet<IReactiveStatistic>();
}
public void Push(in NoteType type, in JudgeData data)
{
JudgementStats.Push(in type, in data);
ComboStats.Push(in type, in data);
FluctuationStats.Push(in type, in data);
foreach (var statistic in Extensions)
statistic.Push(in type, in data);
OnJudgementReceived.Dispatch(type, data);
}
public void RegisterExtension(IReactiveStatistic extension)
{
Extensions.Add(extension);
}
public IReactiveStatistic GetExtensionOrDefault<T>()
where T : IReactiveStatistic
{
return Extensions.FirstOrDefault(e => e.GetType() == typeof(T));
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
namespace AstroDX.Models.Scoring
{
public class NoteRecord
{
public IReadOnlyList<JudgeData> Judged { get; }
public uint MaxCount { get; }
public uint PassedCount { get; private set; }
public uint CriticalCount { get; private set; }
public uint PerfectCount { get; private set; }
public uint GreatCount { get; private set; }
public uint GoodCount { get; private set; }
public uint MissCount { get; private set; }
public double Extras { get; set; }
public NoteRecord(uint maxCount)
{
Judged = new List<JudgeData>();
MaxCount = maxCount;
PassedCount = 0;
Extras = 0;
}
public void Push(in JudgeData data)
{
((List<JudgeData>)Judged).Add(data);
PassedCount++;
switch (data.grade)
{
case JudgeGrade.CriticalPerfect:
CriticalCount++;
break;
case JudgeGrade.Perfect:
PerfectCount++;
break;
case JudgeGrade.Great:
GreatCount++;
break;
case JudgeGrade.Good:
GoodCount++;
break;
case JudgeGrade.Miss:
MissCount++;
break;
default:
throw new ArgumentOutOfRangeException();
}
Points += GetScore(data.grade);
}
public double Points { get; private set; }
private static double GetScore(JudgeGrade grade)
{
return grade switch
{
JudgeGrade.CriticalPerfect => 1,
JudgeGrade.Perfect => 1,
JudgeGrade.Great => 0.8,
JudgeGrade.Good => 0.5,
JudgeGrade.Miss => 0,
_ => throw new
ArgumentOutOfRangeException()
};
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6a0d1a0a74b14c94af6f7d4d943075d8
timeCreated: 1710933820

View File

@@ -0,0 +1,101 @@
using System.Collections.Generic;
using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.Pool;
using UnityEngine.UI;
namespace AstroDX.UI.RecyclingScrollRect.Interfaces
{
[RequireComponent(typeof(ScrollRect))]
public abstract class ScrollRectRecyclingBase<TData> : MonoBehaviour,
IDragHandler,
IBeginDragHandler,
IInitializePotentialDragHandler,
IEndDragHandler
{
protected IList<TData> DataSource { get; private set; } = new List<TData>();
protected ObjectPool<ScrollViewItem<TData>> itemsPool;
[field: Disable, SerializeField]
protected List<ScrollViewItem<TData>> ShownItemsUnordered { get; private set; } = new();
[BeginGroup("List View")]
[NotNull, SerializeField, Tooltip("Where the items will be created.")]
protected ScrollRect scrollRect;
[NotNull, SerializeField, Tooltip("The visual representation of each item.")]
[EndGroup]
protected ScrollViewItem<TData> itemPrefab;
private bool _initialized;
protected virtual void OnEnable()
{
if (_initialized)
return;
itemsPool = new ObjectPool<ScrollViewItem<TData>>(() =>
{
var item = Instantiate(itemPrefab, scrollRect.content);
item.DisableItem();
return item;
},
item => item.EnableItem(),
item => item.DisableItem());
}
public void SetDataSource(IList<TData> dataSource)
{
DataSource = dataSource;
ReloadItems();
}
/// <summary>
/// Reloads the scroll rect to match up with the data source.
/// </summary>
protected abstract void ReloadItems();
/// <summary>
/// Sets the current visible range, load and discards items depending on the previous range.
/// <param name="resetActiveItems">Set the item data again to items that can be recycled. Triggered when data source changes.</param>
/// </summary>
protected abstract void UpdateVisibleRange(bool resetActiveItems);
/// <summary>
/// Jumps to the item in the scroll rect.
/// </summary>
public void JumpToItem(TData item)
{
// We don't do any error handling here so that the error is thrown and developers using this can debug what's happening.
JumpToIndex(DataSource.IndexOf(item));
}
/// <summary>
/// Scrolls to the item in the scroll rect.
/// </summary>
public void ScrollToItem(TData item, float duration, Ease easing = default)
{
// We don't do any error handling here so that the error is thrown and developers using this can debug what's happening.
ScrollToIndex(DataSource.IndexOf(item), duration, easing);
}
/// <summary>
/// Jumps to the index provided by the data source in the scroll rect.
/// </summary>
public abstract void JumpToIndex(int index);
public abstract void ScrollToIndex(int index, float duration, Ease easing = default);
protected virtual void OnDestroy() => itemsPool.Dispose();
public abstract void OnDrag(PointerEventData eventData);
public abstract void OnBeginDrag(PointerEventData eventData);
public abstract void OnInitializePotentialDrag(PointerEventData eventData);
public abstract void OnEndDrag(PointerEventData eventData);
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 5843eb5b52bc4cd085d670e17ff8be63
timeCreated: 1710933385

View File

@@ -0,0 +1,34 @@
using UnityEngine;
namespace AstroDX.UI.RecyclingScrollRect.Interfaces
{
public abstract class ScrollViewItem<T> : MonoBehaviour
{
public T Data { get; private set; }
public int Index { get; private set; }
public void SetData(T data, int index)
{
Data = data;
Index = index;
OnDataUpdated();
}
/// <summary>
/// This is called whenever this instance is requested to render a new item with a different index.
/// </summary>
protected abstract void OnDataUpdated();
/// <summary>
/// When this object instance scrolls out of view, this is called to disable relevant components;
/// Should be as lightweight as possible since disabled items might get reused instantly.
/// </summary>
internal abstract void DisableItem();
/// <summary>
/// Called when an item is retrieved from the pool.
/// </summary>
internal abstract void EnableItem();
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2f1b83f97d7847728b5f776276799111
timeCreated: 1710933857

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d313c27e4a974b3baea75f0d80917046
timeCreated: 1710933392

View File

@@ -0,0 +1,236 @@
using System;
using System.Collections.Generic;
using AstroDX.GameSettings;
using AstroDX.UI.RecyclingScrollRect.Interfaces;
using AstroDX.Utilities.Extensions;
using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
namespace AstroDX.UI.RecyclingScrollRect.ScrollRects
{
[Serializable]
public class RecyclingGridView<TData> : ScrollRectRecyclingBase<TData>
{
[BeginGroup("Scrolling")]
[SerializeField]
private float gap;
[SerializeField]
private float marginTop;
[SerializeField]
private float marginBottom;
[Disable, SerializeField]
private Vector2 itemSize;
[Disable, SerializeField]
private int visibleStart;
[Disable, SerializeField]
private int columnCount;
[Disable, SerializeField]
private int rowCount;
[SerializeField, Disable]
private float usableWidth;
[Disable, SerializeField]
private int maxVisibleCount;
[Disable, SerializeField]
private float contentHeight;
private Tweener _positionTween;
private void OnValidate()
{
itemSize = itemPrefab?.transform.AsRectTransform().sizeDelta ?? Vector2.zero;
scrollRect.content.anchorMin = new Vector2(0, 1);
scrollRect.content.anchorMax = new Vector2(1, 1);
scrollRect.content.pivot = new Vector2(0.5f, 1);
scrollRect.content.localScale = Vector3.one;
var dt = new DrivenRectTransformTracker();
dt.Clear();
//Object to drive the transform
dt.Add(this, scrollRect.content, DrivenTransformProperties.All);
}
protected override void OnEnable()
{
base.OnEnable();
Settings.Profile.Graphics.scaleFactorChanged.AddListener(ReloadItems);
}
private void Update()
{
if (scrollRect.velocity.y == 0)
return;
UpdateVisibleRange(false);
}
protected override void ReloadItems()
{
columnCount = Mathf.FloorToInt((scrollRect.viewport.rect.width + gap) / (itemSize.x + gap));
usableWidth = columnCount * itemSize.x + (columnCount - 1) * gap;
rowCount = Mathf.CeilToInt((float)DataSource.Count / columnCount);
maxVisibleCount = Mathf.CeilToInt((scrollRect.viewport.rect.size.y + gap) / (itemSize.y + gap)) * columnCount;
contentHeight = marginTop + rowCount * (itemSize.y + gap) + marginBottom;
scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, contentHeight);
UpdateVisibleRange(true);
}
private readonly HashSet<int> _itemsToAdd = new();
private int _previousStartIndexInclusive;
private int _previousEndIndexExclusive;
protected override void UpdateVisibleRange(bool resetActiveItems)
{
visibleStart = scrollRect.content.anchoredPosition.y <= marginTop
? 0
: columnCount *
Mathf.FloorToInt((scrollRect.content.anchoredPosition.y - marginTop) / (itemSize.y + gap));
var startIndexInclusive = visibleStart;
var endIndexExclusive = Mathf.Min(startIndexInclusive + maxVisibleCount, DataSource.Count);
// If the visible range hasn't changed, just update the positions of all items in range
if (startIndexInclusive == _previousStartIndexInclusive &&
endIndexExclusive == _previousEndIndexExclusive)
{
foreach (var item in ShownItemsUnordered)
{
item.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(item.Index);
if (resetActiveItems)
item.SetData(DataSource[item.Index], item.Index);
}
return;
}
_previousStartIndexInclusive = startIndexInclusive;
_previousEndIndexExclusive = endIndexExclusive;
// Populate the list of items that should be shown by the end of this procedure
for (var i = startIndexInclusive; i < endIndexExclusive; i++)
_itemsToAdd.Add(i);
// Find and skip items that are already shown
// Since the shown items are unordered (for example, we may request to show 1 after 2 and 3 are shown, resulting in [2, 3, 1]),
// We can't make any assumptions about items not appearing later in the list (e.g. [1, 2, 4, 5, 6, 3])
for (var i = 0; i < ShownItemsUnordered.Count; i++)
{
var item = ShownItemsUnordered[i];
if (_itemsToAdd.Contains(item.Index))
{
item.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(item.Index);
if (resetActiveItems)
item.SetData(DataSource[item.Index], item.Index);
_itemsToAdd.Remove(item.Index);
}
else
{
// We don't need to render this item in the new visible range, so we can safely release it
itemsPool.Release(item);
// Modifying a list while enumerating it isn't recommended
// But we're doing it here for performance
ShownItemsUnordered.RemoveAt(i);
i--;
}
}
// Lastly, generate the actual missing items
foreach (var index in _itemsToAdd)
{
var itemInstance = itemsPool.Get();
ShownItemsUnordered.Add(itemInstance);
itemInstance.SetData(DataSource[index], index);
itemInstance.GetRectTransform().anchoredPosition = AbsolutePositionAtIndex(index);
}
_itemsToAdd.Clear();
}
public override void JumpToIndex(int index)
{
scrollRect.velocity = new Vector3(0, 0);
scrollRect.verticalNormalizedPosition = GetScrollPosition(index);
UpdateVisibleRange(false);
}
public override void ScrollToIndex(int index, float duration, Ease easing = default)
{
scrollRect.velocity = new Vector3(0, 0);
_positionTween?.Kill();
_positionTween = DOTween.To(() => scrollRect.verticalNormalizedPosition,
p =>
{
scrollRect.verticalNormalizedPosition = p;
UpdateVisibleRange(false);
},
GetScrollPosition(index),
duration).SetEase(easing);
}
private float GetScrollPosition(int index)
{
// absolute position at index starts 0 at the top, and decreases when descending
var normalizedPosition = 1 + AbsolutePositionAtIndex(index).y / contentHeight;
normalizedPosition -= ((itemSize.y + marginBottom) * (1 - normalizedPosition) - marginTop * normalizedPosition) /
contentHeight;
return normalizedPosition;
}
public override void OnInitializePotentialDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
_positionTween?.Kill();
}
public override void OnBeginDrag(PointerEventData eventData)
{
}
public override void OnEndDrag(PointerEventData eventData)
{
}
public override void OnDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
UpdateVisibleRange(false);
}
private Vector2 AbsolutePositionAtIndex(int index)
{
var column = index % columnCount;
var fullContainerWidth = scrollRect.content.rect.size.x;
var row = Mathf.FloorToInt((float)index / columnCount);
return new Vector2(column * (itemSize.x + gap) + (fullContainerWidth - usableWidth) / 2,
-marginTop - row * (itemSize.y + gap));
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 163b88140e634e719f1a2e8ae214523f
timeCreated: 1710933419

View File

@@ -0,0 +1,207 @@
using System.Collections.Generic;
using AstroDX.UI.RecyclingScrollRect.Interfaces;
using AstroDX.Utilities.Extensions;
using DG.Tweening;
using UnityEngine;
using UnityEngine.EventSystems;
namespace AstroDX.UI.RecyclingScrollRect.ScrollRects
{
public class RecyclingVerticalListView<TData> : ScrollRectRecyclingBase<TData>
{
[BeginGroup("Scrolling")]
[SerializeField]
private float gap;
[SerializeField]
private float marginTop;
[SerializeField]
[EndGroup]
private float marginBottom;
[Disable, SerializeField]
private float itemHeight;
[Disable, SerializeField]
private int maxVisibleCount;
[Disable, SerializeField]
private float contentHeight;
private void OnValidate()
{
itemHeight = itemPrefab?.GetRectTransform().sizeDelta.y ?? 0;
scrollRect.content.anchorMin = new Vector2(0, 1);
scrollRect.content.anchorMax = new Vector2(1, 1);
scrollRect.content.pivot = new Vector2(0.5f, 1);
scrollRect.content.localScale = Vector3.one;
var dt = new DrivenRectTransformTracker();
dt.Clear();
//Object to drive the transform
dt.Add(this, scrollRect.content, DrivenTransformProperties.All);
}
private void Update()
{
if (scrollRect.velocity.y == 0)
return;
UpdateVisibleRange(false);
}
protected override void ReloadItems()
{
maxVisibleCount = Mathf.CeilToInt((scrollRect.viewport.rect.size.y + gap) / (itemHeight + gap));
contentHeight = marginTop + (itemHeight + gap) + marginBottom;
scrollRect.content.sizeDelta = new Vector2(scrollRect.content.sizeDelta.x, contentHeight);
UpdateVisibleRange(true);
}
private readonly HashSet<int> _itemsToAdd = new();
private int _previousStartIndexInclusive;
private int _previousEndIndexExclusive;
protected override void UpdateVisibleRange(bool resetActiveItems)
{
var startIndexInclusive = scrollRect.content.anchoredPosition.y <= marginTop
? 0
: Mathf.FloorToInt((scrollRect.content.anchoredPosition.y - marginTop) /
(itemHeight + gap));
var endIndexExclusive = Mathf.Min(startIndexInclusive + maxVisibleCount, DataSource.Count);
// If the visible range hasn't changed, just update the positions of all items in range
if (startIndexInclusive == _previousStartIndexInclusive &&
endIndexExclusive == _previousEndIndexExclusive)
{
foreach (var item in ShownItemsUnordered)
{
var position = item.GetRectTransform().anchoredPosition;
position.y = AbsolutePositionAtIndex(item.Index);
item.GetRectTransform().anchoredPosition = position;
if (resetActiveItems)
item.SetData(DataSource[item.Index], item.Index);
}
return;
}
_previousStartIndexInclusive = startIndexInclusive;
_previousEndIndexExclusive = endIndexExclusive;
// Populate the list of items that should be shown by the end of this procedure
for (var i = startIndexInclusive; i < endIndexExclusive; i++)
_itemsToAdd.Add(i);
// Find and skip items that are already shown
// Since the shown items are unordered (for example, we may request to show 1 after 2 and 3 are shown, resulting in [2, 3, 1]),
// We can't make any assumptions about items not appearing later in the list (e.g. [1, 2, 4, 5, 6, 3])
for (var i = 0; i < ShownItemsUnordered.Count; i++)
{
var item = ShownItemsUnordered[i];
if (_itemsToAdd.Contains(item.Index))
{
var position = item.GetRectTransform().anchoredPosition;
position.y = AbsolutePositionAtIndex(item.Index);
item.GetRectTransform().anchoredPosition = position;
if (resetActiveItems)
item.SetData(DataSource[item.Index], item.Index);
_itemsToAdd.Remove(item.Index);
}
else
{
// We don't need to render this item in the new visible range, so we can safely release it
itemsPool.Release(item);
// Modifying a list while enumerating it isn't recommended
// But we're doing it here for performance
ShownItemsUnordered.RemoveAt(i);
i--;
}
}
// Lastly, generate the actual missing items
foreach (var index in _itemsToAdd)
{
var itemInstance = itemsPool.Get();
ShownItemsUnordered.Add(itemInstance);
itemInstance.SetData(DataSource[index], index);
var position = itemInstance.GetRectTransform().anchoredPosition;
position.y = AbsolutePositionAtIndex(index);
itemInstance.GetRectTransform().anchoredPosition = position;
}
_itemsToAdd.Clear();
}
public override void JumpToIndex(int index)
{
scrollRect.velocity = new Vector3(0, 0);
scrollRect.verticalNormalizedPosition = GetScrollPosition(index);
UpdateVisibleRange(false);
}
private Tweener _positionTween;
public override void ScrollToIndex(int index, float duration, Ease easing = default)
{
scrollRect.velocity = Vector2.zero;
_positionTween?.Kill();
_positionTween = DOTween.To(() => scrollRect.verticalNormalizedPosition, p =>
{
scrollRect.verticalNormalizedPosition = p;
UpdateVisibleRange(false);
}, GetScrollPosition(index), duration).SetEase(easing);
}
private float GetScrollPosition(int index)
{
// absolute position at index starts 0 at the top, and decreases when descending
var normalizedPosition = 1 + AbsolutePositionAtIndex(index) / contentHeight;
normalizedPosition -= ((itemHeight + marginBottom) * (1 - normalizedPosition) - marginTop * normalizedPosition) /
contentHeight;
return normalizedPosition;
}
public override void OnInitializePotentialDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
_positionTween?.Kill();
}
public override void OnBeginDrag(PointerEventData eventData)
{
}
public override void OnEndDrag(PointerEventData eventData)
{
}
public override void OnDrag(PointerEventData eventData)
{
if (eventData.button != PointerEventData.InputButton.Left)
return;
UpdateVisibleRange(false);
}
private float AbsolutePositionAtIndex(int index)
{
return -index * (itemHeight + gap) - marginTop;
}
}
}

View File

@@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6421e225fef64262bce238235e6f6e0a
timeCreated: 1711013891