mirror of
https://github.com/osukey/osukey.git
synced 2025-08-03 14:46:38 +09:00
Merge branch 'master' into fix-skipbutton-ordering
This commit is contained in:
19
osu.Game/Rulesets/UI/HitObjectContainer.cs
Normal file
19
osu.Game/Rulesets/UI/HitObjectContainer.cs
Normal file
@ -0,0 +1,19 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
public class HitObjectContainer : CompositeDrawable
|
||||
{
|
||||
public virtual IEnumerable<DrawableHitObject> Objects => InternalChildren.Cast<DrawableHitObject>();
|
||||
public virtual IEnumerable<DrawableHitObject> AliveObjects => AliveInternalChildren.Cast<DrawableHitObject>();
|
||||
|
||||
public virtual void Add(DrawableHitObject hitObject) => AddInternal(hitObject);
|
||||
public virtual bool Remove(DrawableHitObject hitObject) => RemoveInternal(hitObject);
|
||||
}
|
||||
}
|
@ -8,8 +8,6 @@ using osu.Game.Rulesets.Objects.Drawables;
|
||||
using OpenTK;
|
||||
using osu.Game.Rulesets.Judgements;
|
||||
using osu.Framework.Allocation;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
@ -18,7 +16,7 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <summary>
|
||||
/// The HitObjects contained in this Playfield.
|
||||
/// </summary>
|
||||
public HitObjectContainer HitObjects { get; protected set; }
|
||||
public HitObjectContainer HitObjects { get; private set; }
|
||||
|
||||
public Container<Drawable> ScaledContent;
|
||||
|
||||
@ -46,16 +44,14 @@ namespace osu.Game.Rulesets.UI
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
HitObjects = new HitObjectContainer
|
||||
{
|
||||
RelativeSizeAxes = Axes.Both,
|
||||
};
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
HitObjects = CreateHitObjectContainer();
|
||||
HitObjects.RelativeSizeAxes = Axes.Both;
|
||||
|
||||
Add(HitObjects);
|
||||
}
|
||||
|
||||
@ -89,12 +85,10 @@ namespace osu.Game.Rulesets.UI
|
||||
/// <param name="judgement">The <see cref="Judgement"/> that occurred.</param>
|
||||
public virtual void OnJudgement(DrawableHitObject judgedObject, Judgement judgement) { }
|
||||
|
||||
public class HitObjectContainer : CompositeDrawable
|
||||
{
|
||||
public virtual IEnumerable<DrawableHitObject> Objects => InternalChildren.OfType<DrawableHitObject>();
|
||||
public virtual void Add(DrawableHitObject hitObject) => AddInternal(hitObject);
|
||||
public virtual bool Remove(DrawableHitObject hitObject) => RemoveInternal(hitObject);
|
||||
}
|
||||
/// <summary>
|
||||
/// Creates the container that will be used to contain the <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer();
|
||||
|
||||
private class ScaledContainer : Container
|
||||
{
|
||||
|
25
osu.Game/Rulesets/UI/Scrolling/ScrollingDirection.cs
Normal file
25
osu.Game/Rulesets/UI/Scrolling/ScrollingDirection.cs
Normal file
@ -0,0 +1,25 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
public enum ScrollingDirection
|
||||
{
|
||||
/// <summary>
|
||||
/// Hitobjects will scroll vertically from the bottom of the hitobject container.
|
||||
/// </summary>
|
||||
Up,
|
||||
/// <summary>
|
||||
/// Hitobjects will scroll vertically from the top of the hitobject container.
|
||||
/// </summary>
|
||||
Down,
|
||||
/// <summary>
|
||||
/// Hitobjects will scroll horizontally from the right of the hitobject container.
|
||||
/// </summary>
|
||||
Left,
|
||||
/// <summary>
|
||||
/// Hitobjects will scroll horizontally from the left of the hitobject container.
|
||||
/// </summary>
|
||||
Right
|
||||
}
|
||||
}
|
116
osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
Normal file
116
osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs
Normal file
@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Caching;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Configuration;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using osu.Game.Rulesets.UI.Scrolling.Visualisers;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
public class ScrollingHitObjectContainer : HitObjectContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// The duration required to scroll through one length of the <see cref="ScrollingHitObjectContainer"/> before any control point adjustments.
|
||||
/// </summary>
|
||||
public readonly BindableDouble TimeRange = new BindableDouble
|
||||
{
|
||||
MinValue = 0,
|
||||
MaxValue = double.MaxValue
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// The control points that adjust the scrolling speed.
|
||||
/// </summary>
|
||||
protected readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>();
|
||||
|
||||
private readonly ScrollingDirection direction;
|
||||
|
||||
private Cached initialStateCache = new Cached();
|
||||
|
||||
public ScrollingHitObjectContainer(ScrollingDirection direction)
|
||||
{
|
||||
this.direction = direction;
|
||||
|
||||
RelativeSizeAxes = Axes.Both;
|
||||
|
||||
TimeRange.ValueChanged += v => initialStateCache.Invalidate();
|
||||
}
|
||||
|
||||
private ISpeedChangeVisualiser speedChangeVisualiser;
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load(OsuConfigManager config)
|
||||
{
|
||||
switch (config.Get<SpeedChangeVisualisationMethod>(OsuSetting.SpeedChangeVisualisation))
|
||||
{
|
||||
case SpeedChangeVisualisationMethod.Sequential:
|
||||
speedChangeVisualiser = new SequentialSpeedChangeVisualiser(ControlPoints);
|
||||
break;
|
||||
case SpeedChangeVisualisationMethod.Overlapping:
|
||||
speedChangeVisualiser = new OverlappingSpeedChangeVisualiser(ControlPoints);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public override void Add(DrawableHitObject hitObject)
|
||||
{
|
||||
initialStateCache.Invalidate();
|
||||
base.Add(hitObject);
|
||||
}
|
||||
|
||||
public override bool Remove(DrawableHitObject hitObject)
|
||||
{
|
||||
var result = base.Remove(hitObject);
|
||||
if (result)
|
||||
initialStateCache.Invalidate();
|
||||
return result;
|
||||
}
|
||||
|
||||
public void AddControlPoint(MultiplierControlPoint controlPoint)
|
||||
{
|
||||
ControlPoints.Add(controlPoint);
|
||||
initialStateCache.Invalidate();
|
||||
}
|
||||
|
||||
public bool RemoveControlPoint(MultiplierControlPoint controlPoint)
|
||||
{
|
||||
var result = ControlPoints.Remove(controlPoint);
|
||||
if (result)
|
||||
initialStateCache.Invalidate();
|
||||
return result;
|
||||
}
|
||||
|
||||
public override bool Invalidate(Invalidation invalidation = Invalidation.All, Drawable source = null, bool shallPropagate = true)
|
||||
{
|
||||
if ((invalidation & (Invalidation.RequiredParentSizeToFit | Invalidation.DrawInfo)) > 0)
|
||||
initialStateCache.Invalidate();
|
||||
|
||||
return base.Invalidate(invalidation, source, shallPropagate);
|
||||
}
|
||||
|
||||
protected override void Update()
|
||||
{
|
||||
base.Update();
|
||||
|
||||
if (!initialStateCache.IsValid)
|
||||
{
|
||||
speedChangeVisualiser.ComputeInitialStates(Objects, direction, TimeRange, DrawSize);
|
||||
initialStateCache.Validate();
|
||||
}
|
||||
}
|
||||
|
||||
protected override void UpdateAfterChildrenLife()
|
||||
{
|
||||
base.UpdateAfterChildrenLife();
|
||||
|
||||
// We need to calculate this as soon as possible after lifetimes so that hitobjects get the final say in their positions
|
||||
speedChangeVisualiser.ComputePositions(AliveObjects, direction, Time.Current, TimeRange, DrawSize);
|
||||
}
|
||||
}
|
||||
}
|
142
osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
Normal file
142
osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs
Normal file
@ -0,0 +1,142 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Allocation;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using OpenTK.Input;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of <see cref="Playfield"/> specialized towards scrolling <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public abstract class ScrollingPlayfield : Playfield
|
||||
{
|
||||
/// <summary>
|
||||
/// The default span of time visible by the length of the scrolling axes.
|
||||
/// This is clamped between <see cref="time_span_min"/> and <see cref="time_span_max"/>.
|
||||
/// </summary>
|
||||
private const double time_span_default = 1500;
|
||||
/// <summary>
|
||||
/// The minimum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_min = 50;
|
||||
/// <summary>
|
||||
/// The maximum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_max = 10000;
|
||||
/// <summary>
|
||||
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_step = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The span of time that is visible by the length of the scrolling axes.
|
||||
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="VisibleTimeRange"/> = 1000.
|
||||
/// </summary>
|
||||
public readonly BindableDouble VisibleTimeRange = new BindableDouble(time_span_default)
|
||||
{
|
||||
Default = time_span_default,
|
||||
MinValue = time_span_min,
|
||||
MaxValue = time_span_max
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether the player can change <see cref="VisibleTimeRange"/>.
|
||||
/// </summary>
|
||||
protected virtual bool UserScrollSpeedAdjustment => true;
|
||||
|
||||
/// <summary>
|
||||
/// The container that contains the <see cref="SpeedAdjustmentContainer"/>s and <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects;
|
||||
|
||||
private readonly ScrollingDirection direction;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ScrollingPlayfield"/>.
|
||||
/// </summary>
|
||||
/// <param name="scrollingAxes">The axes on which <see cref="DrawableHitObject"/>s in this container should scroll.</param>
|
||||
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width</param>
|
||||
protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null)
|
||||
: base(customWidth)
|
||||
{
|
||||
this.direction = direction;
|
||||
}
|
||||
|
||||
[BackgroundDependencyLoader]
|
||||
private void load()
|
||||
{
|
||||
HitObjects.TimeRange.BindTo(VisibleTimeRange);
|
||||
}
|
||||
|
||||
private List<ScrollingPlayfield> nestedPlayfields;
|
||||
/// <summary>
|
||||
/// All the <see cref="ScrollingPlayfield"/>s nested inside this playfield.
|
||||
/// </summary>
|
||||
public IEnumerable<ScrollingPlayfield> NestedPlayfields => nestedPlayfields;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="ScrollingPlayfield"/> to this playfield. The nested <see cref="ScrollingPlayfield"/>
|
||||
/// will be given all of the same speed adjustments as this playfield.
|
||||
/// </summary>
|
||||
/// <param name="otherPlayfield">The <see cref="ScrollingPlayfield"/> to add.</param>
|
||||
protected void AddNested(ScrollingPlayfield otherPlayfield)
|
||||
{
|
||||
if (nestedPlayfields == null)
|
||||
nestedPlayfields = new List<ScrollingPlayfield>();
|
||||
|
||||
nestedPlayfields.Add(otherPlayfield);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
|
||||
{
|
||||
if (!UserScrollSpeedAdjustment)
|
||||
return false;
|
||||
|
||||
if (state.Keyboard.ControlPressed)
|
||||
{
|
||||
switch (args.Key)
|
||||
{
|
||||
case Key.Minus:
|
||||
transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
|
||||
break;
|
||||
case Key.Plus:
|
||||
transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None)
|
||||
{
|
||||
this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing));
|
||||
}
|
||||
|
||||
protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(direction);
|
||||
|
||||
private class TransformVisibleTimeRange : Transform<double, ScrollingPlayfield>
|
||||
{
|
||||
private double valueAt(double time)
|
||||
{
|
||||
if (time < StartTime) return StartValue;
|
||||
if (time >= EndTime) return EndValue;
|
||||
|
||||
return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
|
||||
}
|
||||
|
||||
public override string TargetMember => "VisibleTimeRange.Value";
|
||||
|
||||
protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time);
|
||||
protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value;
|
||||
}
|
||||
}
|
||||
}
|
@ -13,7 +13,7 @@ using osu.Game.Rulesets.Objects;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
namespace osu.Game.Rulesets.UI.Scrolling
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of <see cref="RulesetContainer{TPlayfield,TObject}"/> that supports a <see cref="ScrollingPlayfield"/>.
|
||||
@ -70,12 +70,10 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
// Perform some post processing of the timing changes
|
||||
timingChanges = timingChanges
|
||||
// Collapse sections after the last hit object
|
||||
.Where(s => s.StartTime <= lastObjectTime)
|
||||
// Collapse sections with the same start time
|
||||
.GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime)
|
||||
// Collapse sections with the same beat length
|
||||
.GroupBy(s => s.TimingPoint.BeatLength * s.DifficultyPoint.SpeedMultiplier).Select(g => g.First());
|
||||
// Collapse sections after the last hit object
|
||||
.Where(s => s.StartTime <= lastObjectTime)
|
||||
// Collapse sections with the same start time
|
||||
.GroupBy(s => s.StartTime).Select(g => g.Last()).OrderBy(s => s.StartTime);
|
||||
|
||||
DefaultControlPoints.AddRange(timingChanges);
|
||||
|
||||
@ -88,7 +86,7 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
private void applySpeedAdjustment(MultiplierControlPoint controlPoint, ScrollingPlayfield playfield)
|
||||
{
|
||||
playfield.HitObjects.AddSpeedAdjustment(CreateSpeedAdjustmentContainer(controlPoint));
|
||||
playfield.HitObjects.AddControlPoint(controlPoint);
|
||||
playfield.NestedPlayfields.ForEach(p => applySpeedAdjustment(controlPoint, p));
|
||||
}
|
||||
|
||||
@ -108,12 +106,5 @@ namespace osu.Game.Rulesets.UI
|
||||
|
||||
return new MultiplierControlPoint(time, DefaultControlPoints[index].DeepClone());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a <see cref="SpeedAdjustmentContainer"/> that facilitates the movement of hit objects.
|
||||
/// </summary>
|
||||
/// <param name="controlPoint">The <see cref="MultiplierControlPoint"/> that provides the speed adjustments for the hitobjects.</param>
|
||||
/// <returns>The <see cref="SpeedAdjustmentContainer"/>.</returns>
|
||||
protected virtual SpeedAdjustmentContainer CreateSpeedAdjustmentContainer(MultiplierControlPoint controlPoint) => new SpeedAdjustmentContainer(controlPoint);
|
||||
}
|
||||
}
|
@ -0,0 +1,33 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
|
||||
{
|
||||
public interface ISpeedChangeVisualiser
|
||||
{
|
||||
/// <summary>
|
||||
/// Computes the states of <see cref="DrawableHitObject"/>s that are constant, such as lifetime and spatial length.
|
||||
/// This is invoked once whenever <paramref name="timeRange"/> or <paramref name="length"/> changes.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The <see cref="DrawableHitObject"/>s whose states should be computed.</param>
|
||||
/// <param name="direction">The scrolling direction.</param>
|
||||
/// <param name="timeRange">The duration required to scroll through one length of the screen before any control point adjustments.</param>
|
||||
/// <param name="length">The length of the screen that is scrolled through.</param>
|
||||
void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length);
|
||||
|
||||
/// <summary>
|
||||
/// Computes the states of <see cref="DrawableHitObject"/>s that change depending on <paramref name="currentTime"/>, such as position.
|
||||
/// This is invoked once per frame.
|
||||
/// </summary>
|
||||
/// <param name="hitObjects">The <see cref="DrawableHitObject"/>s whose states should be computed.</param>
|
||||
/// <param name="direction">The scrolling direction.</param>
|
||||
/// <param name="currentTime">The current time.</param>
|
||||
/// <param name="timeRange">The duration required to scroll through one length of the screen before any control point adjustments.</param>
|
||||
/// <param name="length">The length of the screen that is scrolled through.</param>
|
||||
void ComputePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length);
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System.Collections.Generic;
|
||||
using osu.Framework.Lists;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
|
||||
{
|
||||
public class OverlappingSpeedChangeVisualiser : ISpeedChangeVisualiser
|
||||
{
|
||||
private readonly SortedList<MultiplierControlPoint> controlPoints;
|
||||
|
||||
public OverlappingSpeedChangeVisualiser(SortedList<MultiplierControlPoint> controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
}
|
||||
|
||||
public void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
var controlPoint = controlPointAt(obj.HitObject.StartTime);
|
||||
obj.LifetimeStart = obj.HitObject.StartTime - timeRange / controlPoint.Multiplier;
|
||||
|
||||
if (obj.NestedHitObjects != null)
|
||||
{
|
||||
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
|
||||
ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ComputePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
var controlPoint = controlPointAt(obj.HitObject.StartTime);
|
||||
|
||||
var position = (obj.HitObject.StartTime - currentTime) * controlPoint.Multiplier / timeRange;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
obj.Y = (float)(position * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Down:
|
||||
obj.Y = (float)(-position * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
obj.X = (float)(position * length.X);
|
||||
break;
|
||||
case ScrollingDirection.Right:
|
||||
obj.X = (float)(-position * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly MultiplierControlPoint searchPoint = new MultiplierControlPoint();
|
||||
private MultiplierControlPoint controlPointAt(double time)
|
||||
{
|
||||
if (controlPoints.Count == 0)
|
||||
return new MultiplierControlPoint(double.NegativeInfinity);
|
||||
|
||||
if (time < controlPoints[0].StartTime)
|
||||
return controlPoints[0];
|
||||
|
||||
searchPoint.StartTime = time;
|
||||
int index = controlPoints.BinarySearch(searchPoint);
|
||||
|
||||
if (index < 0)
|
||||
index = ~index - 1;
|
||||
|
||||
return controlPoints[index];
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,103 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Objects.Types;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
using OpenTK;
|
||||
|
||||
namespace osu.Game.Rulesets.UI.Scrolling.Visualisers
|
||||
{
|
||||
public class SequentialSpeedChangeVisualiser : ISpeedChangeVisualiser
|
||||
{
|
||||
private readonly Dictionary<DrawableHitObject, double> hitObjectPositions = new Dictionary<DrawableHitObject, double>();
|
||||
|
||||
private readonly IReadOnlyList<MultiplierControlPoint> controlPoints;
|
||||
|
||||
public SequentialSpeedChangeVisualiser(IReadOnlyList<MultiplierControlPoint> controlPoints)
|
||||
{
|
||||
this.controlPoints = controlPoints;
|
||||
}
|
||||
|
||||
public void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length)
|
||||
{
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
var startPosition = hitObjectPositions[obj] = positionAt(obj.HitObject.StartTime, timeRange);
|
||||
|
||||
obj.LifetimeStart = obj.HitObject.StartTime - timeRange - 1000;
|
||||
|
||||
if (obj.HitObject is IHasEndTime endTime)
|
||||
{
|
||||
var diff = positionAt(endTime.EndTime, timeRange) - startPosition;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
case ScrollingDirection.Down:
|
||||
obj.Height = (float)(diff * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
case ScrollingDirection.Right:
|
||||
obj.Width = (float)(diff * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (obj.NestedHitObjects != null)
|
||||
{
|
||||
ComputeInitialStates(obj.NestedHitObjects, direction, timeRange, length);
|
||||
ComputePositions(obj.NestedHitObjects, direction, obj.HitObject.StartTime, timeRange, length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void ComputePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length)
|
||||
{
|
||||
var timelinePosition = positionAt(currentTime, timeRange);
|
||||
|
||||
foreach (var obj in hitObjects)
|
||||
{
|
||||
var finalPosition = hitObjectPositions[obj] - timelinePosition;
|
||||
|
||||
switch (direction)
|
||||
{
|
||||
case ScrollingDirection.Up:
|
||||
obj.Y = (float)(finalPosition * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Down:
|
||||
obj.Y = (float)(-finalPosition * length.Y);
|
||||
break;
|
||||
case ScrollingDirection.Left:
|
||||
obj.X = (float)(finalPosition * length.X);
|
||||
break;
|
||||
case ScrollingDirection.Right:
|
||||
obj.X = (float)(-finalPosition * length.X);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private double positionAt(double time, double timeRange)
|
||||
{
|
||||
double length = 0;
|
||||
for (int i = 0; i < controlPoints.Count; i++)
|
||||
{
|
||||
var current = controlPoints[i];
|
||||
var next = i < controlPoints.Count - 1 ? controlPoints[i + 1] : null;
|
||||
|
||||
if (i > 0 && current.StartTime > time)
|
||||
continue;
|
||||
|
||||
// Duration of the current control point
|
||||
var currentDuration = (next?.StartTime ?? double.PositiveInfinity) - current.StartTime;
|
||||
|
||||
length += Math.Min(currentDuration, time - current.StartTime) * current.Multiplier / timeRange;
|
||||
}
|
||||
|
||||
return length;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
|
||||
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using OpenTK.Input;
|
||||
using osu.Framework.Configuration;
|
||||
using osu.Framework.Graphics;
|
||||
using osu.Framework.Graphics.Containers;
|
||||
using osu.Framework.Graphics.Transforms;
|
||||
using osu.Framework.Input;
|
||||
using osu.Framework.MathUtils;
|
||||
using osu.Game.Rulesets.Objects.Drawables;
|
||||
using osu.Game.Rulesets.Timing;
|
||||
|
||||
namespace osu.Game.Rulesets.UI
|
||||
{
|
||||
/// <summary>
|
||||
/// A type of <see cref="Playfield"/> specialized towards scrolling <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public class ScrollingPlayfield : Playfield
|
||||
{
|
||||
/// <summary>
|
||||
/// The default span of time visible by the length of the scrolling axes.
|
||||
/// This is clamped between <see cref="time_span_min"/> and <see cref="time_span_max"/>.
|
||||
/// </summary>
|
||||
private const double time_span_default = 1500;
|
||||
/// <summary>
|
||||
/// The minimum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_min = 50;
|
||||
/// <summary>
|
||||
/// The maximum span of time that may be visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_max = 10000;
|
||||
/// <summary>
|
||||
/// The step increase/decrease of the span of time visible by the length of the scrolling axes.
|
||||
/// </summary>
|
||||
private const double time_span_step = 50;
|
||||
|
||||
/// <summary>
|
||||
/// The span of time that is visible by the length of the scrolling axes.
|
||||
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="VisibleTimeRange"/> = 1000.
|
||||
/// </summary>
|
||||
public readonly BindableDouble VisibleTimeRange = new BindableDouble(time_span_default)
|
||||
{
|
||||
Default = time_span_default,
|
||||
MinValue = time_span_min,
|
||||
MaxValue = time_span_max
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Whether to reverse the scrolling direction is reversed. Note that this does _not_ invert the hit objects.
|
||||
/// </summary>
|
||||
protected readonly BindableBool Reversed = new BindableBool();
|
||||
|
||||
/// <summary>
|
||||
/// The container that contains the <see cref="SpeedAdjustmentContainer"/>s and <see cref="DrawableHitObject"/>s.
|
||||
/// </summary>
|
||||
public new readonly ScrollingHitObjectContainer HitObjects;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ScrollingPlayfield"/>.
|
||||
/// </summary>
|
||||
/// <param name="scrollingAxes">The axes on which <see cref="DrawableHitObject"/>s in this container should scroll.</param>
|
||||
/// <param name="customWidth">Whether we want our internal coordinate system to be scaled to a specified width</param>
|
||||
protected ScrollingPlayfield(Axes scrollingAxes, float? customWidth = null)
|
||||
: base(customWidth)
|
||||
{
|
||||
base.HitObjects = HitObjects = new ScrollingHitObjectContainer(scrollingAxes) { RelativeSizeAxes = Axes.Both };
|
||||
HitObjects.VisibleTimeRange.BindTo(VisibleTimeRange);
|
||||
HitObjects.Reversed.BindTo(Reversed);
|
||||
}
|
||||
|
||||
private List<ScrollingPlayfield> nestedPlayfields;
|
||||
/// <summary>
|
||||
/// All the <see cref="ScrollingPlayfield"/>s nested inside this playfield.
|
||||
/// </summary>
|
||||
public IEnumerable<ScrollingPlayfield> NestedPlayfields => nestedPlayfields;
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="ScrollingPlayfield"/> to this playfield. The nested <see cref="ScrollingPlayfield"/>
|
||||
/// will be given all of the same speed adjustments as this playfield.
|
||||
/// </summary>
|
||||
/// <param name="otherPlayfield">The <see cref="ScrollingPlayfield"/> to add.</param>
|
||||
protected void AddNested(ScrollingPlayfield otherPlayfield)
|
||||
{
|
||||
if (nestedPlayfields == null)
|
||||
nestedPlayfields = new List<ScrollingPlayfield>();
|
||||
|
||||
nestedPlayfields.Add(otherPlayfield);
|
||||
}
|
||||
|
||||
protected override bool OnKeyDown(InputState state, KeyDownEventArgs args)
|
||||
{
|
||||
if (state.Keyboard.ControlPressed)
|
||||
{
|
||||
switch (args.Key)
|
||||
{
|
||||
case Key.Minus:
|
||||
transformVisibleTimeRangeTo(VisibleTimeRange + time_span_step, 200, Easing.OutQuint);
|
||||
break;
|
||||
case Key.Plus:
|
||||
transformVisibleTimeRangeTo(VisibleTimeRange - time_span_step, 200, Easing.OutQuint);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void transformVisibleTimeRangeTo(double newTimeRange, double duration = 0, Easing easing = Easing.None)
|
||||
{
|
||||
this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing));
|
||||
}
|
||||
|
||||
private class TransformVisibleTimeRange : Transform<double, ScrollingPlayfield>
|
||||
{
|
||||
private double valueAt(double time)
|
||||
{
|
||||
if (time < StartTime) return StartValue;
|
||||
if (time >= EndTime) return EndValue;
|
||||
|
||||
return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing);
|
||||
}
|
||||
|
||||
public override string TargetMember => "VisibleTimeRange.Value";
|
||||
|
||||
protected override void Apply(ScrollingPlayfield d, double time) => d.VisibleTimeRange.Value = valueAt(time);
|
||||
protected override void ReadIntoStartValue(ScrollingPlayfield d) => StartValue = d.VisibleTimeRange.Value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A container that provides the foundation for sorting <see cref="DrawableHitObject"/>s into <see cref="SpeedAdjustmentContainer"/>s.
|
||||
/// </summary>
|
||||
public class ScrollingHitObjectContainer : HitObjectContainer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the range of time that is visible by the length of the scrolling axes.
|
||||
/// For example, only hit objects with start time less than or equal to 1000 will be visible with <see cref="VisibleTimeRange"/> = 1000.
|
||||
/// </summary>
|
||||
public readonly BindableDouble VisibleTimeRange = new BindableDouble { Default = 1000 };
|
||||
|
||||
/// <summary>
|
||||
/// Whether to reverse the scrolling direction is reversed.
|
||||
/// </summary>
|
||||
public readonly BindableBool Reversed = new BindableBool();
|
||||
|
||||
private readonly SortedContainer speedAdjustments;
|
||||
public IReadOnlyList<SpeedAdjustmentContainer> SpeedAdjustments => speedAdjustments;
|
||||
|
||||
private readonly SpeedAdjustmentContainer defaultSpeedAdjustment;
|
||||
|
||||
private readonly Axes scrollingAxes;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new <see cref="ScrollingHitObjectContainer"/>.
|
||||
/// </summary>
|
||||
/// <param name="scrollingAxes">The axes upon which hit objects should appear to scroll inside this container.</param>
|
||||
public ScrollingHitObjectContainer(Axes scrollingAxes)
|
||||
{
|
||||
this.scrollingAxes = scrollingAxes;
|
||||
|
||||
AddInternal(speedAdjustments = new SortedContainer { RelativeSizeAxes = Axes.Both });
|
||||
|
||||
// Default speed adjustment
|
||||
AddSpeedAdjustment(defaultSpeedAdjustment = new SpeedAdjustmentContainer(new MultiplierControlPoint(0)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="SpeedAdjustmentContainer"/> to this container, re-sorting all hit objects
|
||||
/// in the last <see cref="SpeedAdjustmentContainer"/> that occurred (time-wise) before it.
|
||||
/// </summary>
|
||||
/// <param name="speedAdjustment">The <see cref="SpeedAdjustmentContainer"/>.</param>
|
||||
public void AddSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment)
|
||||
{
|
||||
speedAdjustment.ScrollingAxes = scrollingAxes;
|
||||
speedAdjustment.VisibleTimeRange.BindTo(VisibleTimeRange);
|
||||
speedAdjustment.Reversed.BindTo(Reversed);
|
||||
|
||||
if (speedAdjustments.Count > 0)
|
||||
{
|
||||
// We need to re-sort all hit objects in the speed adjustment container prior to figure out if they
|
||||
// should now lie within this one
|
||||
var existingAdjustment = adjustmentContainerAt(speedAdjustment.ControlPoint.StartTime);
|
||||
for (int i = 0; i < existingAdjustment.Count; i++)
|
||||
{
|
||||
DrawableHitObject hitObject = existingAdjustment[i];
|
||||
|
||||
if (!speedAdjustment.CanContain(hitObject.HitObject.StartTime))
|
||||
continue;
|
||||
|
||||
existingAdjustment.Remove(hitObject);
|
||||
speedAdjustment.Add(hitObject);
|
||||
|
||||
i--;
|
||||
}
|
||||
}
|
||||
|
||||
speedAdjustments.Add(speedAdjustment);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a <see cref="SpeedAdjustmentContainer"/> from this container, re-sorting all hit objects
|
||||
/// which it contained into new <see cref="SpeedAdjustmentContainer"/>s.
|
||||
/// </summary>
|
||||
/// <param name="speedAdjustment">The <see cref="SpeedAdjustmentContainer"/> to remove.</param>
|
||||
public void RemoveSpeedAdjustment(SpeedAdjustmentContainer speedAdjustment)
|
||||
{
|
||||
if (speedAdjustment == defaultSpeedAdjustment)
|
||||
throw new InvalidOperationException($"The default {nameof(SpeedAdjustmentContainer)} must not be removed.");
|
||||
|
||||
if (!speedAdjustments.Remove(speedAdjustment))
|
||||
return;
|
||||
|
||||
while (speedAdjustment.Count > 0)
|
||||
{
|
||||
DrawableHitObject hitObject = speedAdjustment[0];
|
||||
|
||||
speedAdjustment.Remove(hitObject);
|
||||
Add(hitObject);
|
||||
}
|
||||
}
|
||||
|
||||
public override IEnumerable<DrawableHitObject> Objects => speedAdjustments.SelectMany(s => s.Children);
|
||||
|
||||
/// <summary>
|
||||
/// Adds a hit object to this <see cref="ScrollingHitObjectContainer"/>. The hit objects will be queued to be processed
|
||||
/// new <see cref="SpeedAdjustmentContainer"/>s are added to this <see cref="ScrollingHitObjectContainer"/>.
|
||||
/// </summary>
|
||||
/// <param name="hitObject">The hit object to add.</param>
|
||||
public override void Add(DrawableHitObject hitObject)
|
||||
{
|
||||
if (!(hitObject is IScrollingHitObject))
|
||||
throw new InvalidOperationException($"Hit objects added to a {nameof(ScrollingHitObjectContainer)} must implement {nameof(IScrollingHitObject)}.");
|
||||
|
||||
adjustmentContainerAt(hitObject.HitObject.StartTime).Add(hitObject);
|
||||
}
|
||||
|
||||
public override bool Remove(DrawableHitObject hitObject) => speedAdjustments.Any(s => s.Remove(hitObject));
|
||||
|
||||
/// <summary>
|
||||
/// Finds the <see cref="SpeedAdjustmentContainer"/> which provides the speed adjustment active at a time.
|
||||
/// If there is no <see cref="SpeedAdjustmentContainer"/> active at the time, then the first (time-wise) speed adjustment is returned.
|
||||
/// </summary>
|
||||
/// <param name="time">The time to find the active <see cref="SpeedAdjustmentContainer"/> at.</param>
|
||||
/// <returns>The <see cref="SpeedAdjustmentContainer"/> active at <paramref name="time"/>. Null if there are no speed adjustments.</returns>
|
||||
private SpeedAdjustmentContainer adjustmentContainerAt(double time) => speedAdjustments.FirstOrDefault(c => c.CanContain(time)) ?? defaultSpeedAdjustment;
|
||||
|
||||
private class SortedContainer : Container<SpeedAdjustmentContainer>
|
||||
{
|
||||
protected override int Compare(Drawable x, Drawable y)
|
||||
{
|
||||
var sX = (SpeedAdjustmentContainer)x;
|
||||
var sY = (SpeedAdjustmentContainer)y;
|
||||
|
||||
int result = sY.ControlPoint.StartTime.CompareTo(sX.ControlPoint.StartTime);
|
||||
if (result != 0)
|
||||
return result;
|
||||
return base.Compare(y, x);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user