osu/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs

160 lines
5.0 KiB
C#

// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
// See the LICENCE file in the repository root for full licence text.
#nullable disable
using System;
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Osu.Objects;
using osu.Game.Rulesets.Osu.Objects.Drawables;
using osuTK;
namespace osu.Game.Rulesets.Osu.Skinning.Default
{
/// <summary>
/// A <see cref="SliderBody"/> which changes its curve depending on the snaking progress.
/// </summary>
public abstract class SnakingSliderBody : SliderBody, ISliderProgress
{
public readonly List<Vector2> CurrentCurve = new List<Vector2>();
public readonly Bindable<bool> SnakingIn = new Bindable<bool>();
public readonly Bindable<bool> SnakingOut = new Bindable<bool>();
public double? SnakedStart { get; private set; }
public double? SnakedEnd { get; private set; }
public override float PathRadius
{
get => base.PathRadius;
set
{
if (base.PathRadius == value)
return;
base.PathRadius = value;
Refresh();
}
}
public override Vector2 PathOffset => snakedPathOffset;
/// <summary>
/// The top-left position of the path when fully snaked.
/// </summary>
private Vector2 snakedPosition;
/// <summary>
/// The offset of the path from <see cref="snakedPosition"/> when fully snaked.
/// </summary>
private Vector2 snakedPathOffset;
private DrawableSlider drawableSlider;
[BackgroundDependencyLoader]
private void load(DrawableHitObject drawableObject)
{
drawableSlider = (DrawableSlider)drawableObject;
Refresh();
}
public void UpdateProgress(double completionProgress)
{
if (drawableSlider?.HitObject == null)
return;
Slider slider = drawableSlider.HitObject;
int span = slider.SpanAt(completionProgress);
double spanProgress = slider.ProgressAt(completionProgress);
double start = 0;
double end = SnakingIn.Value ? Math.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1;
if (span >= slider.SpanCount() - 1)
{
if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1)
{
start = 0;
end = SnakingOut.Value ? spanProgress : 1;
}
else
{
start = SnakingOut.Value ? spanProgress : 0;
}
}
setRange(start, end);
}
public void Refresh()
{
if (drawableSlider?.HitObject == null)
return;
// Generate the entire curve
drawableSlider.HitObject.Path.GetPathToProgress(CurrentCurve, 0, 1);
SetVertices(CurrentCurve);
// Force the body to be the final path size to avoid excessive autosize computations
Path.AutoSizeAxes = Axes.Both;
Size = Path.Size;
updatePathSize();
snakedPosition = Path.PositionInBoundingBox(Vector2.Zero);
snakedPathOffset = Path.PositionInBoundingBox(Path.Vertices[0]);
double lastSnakedStart = SnakedStart ?? 0;
double lastSnakedEnd = SnakedEnd ?? 0;
SnakedStart = null;
SnakedEnd = null;
setRange(lastSnakedStart, lastSnakedEnd);
}
public override void RecyclePath()
{
base.RecyclePath();
updatePathSize();
}
private void updatePathSize()
{
// Force the path to its final size to avoid excessive framebuffer resizes
Path.AutoSizeAxes = Axes.None;
Path.Size = Size;
}
private void setRange(double p0, double p1)
{
if (p0 > p1)
(p0, p1) = (p1, p0);
if (SnakedStart == p0 && SnakedEnd == p1) return;
SnakedStart = p0;
SnakedEnd = p1;
drawableSlider.HitObject.Path.GetPathToProgress(CurrentCurve, p0, p1);
SetVertices(CurrentCurve);
// The bounding box of the path expands as it snakes, which in turn shifts the position of the path.
// Depending on the direction of expansion, it may appear as if the path is expanding towards the position of the slider
// rather than expanding out from the position of the slider.
// To remove this effect, the path's position is shifted towards its final snaked position
Path.Position = snakedPosition - Path.PositionInBoundingBox(Vector2.Zero);
}
}
}