Path: blob/master/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs
4345 views
// Copyright (c) ppy Pty Ltd <[email protected]>. Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Lists; using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Timing; using osu.Game.Rulesets.UI.Scrolling.Algorithms; using osu.Game.Screens.Play; namespace osu.Game.Rulesets.UI.Scrolling { /// <summary> /// A type of <see cref="DrawableRuleset{TObject}"/> that supports a <see cref="ScrollingPlayfield"/>. /// <see cref="HitObject"/>s inside this <see cref="DrawableRuleset{TObject}"/> will scroll within the playfield. /// </summary> public abstract partial class DrawableScrollingRuleset<TObject> : DrawableRuleset<TObject>, IDrawableScrollingRuleset, IKeyBindingHandler<GlobalAction> where TObject : HitObject { /// <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 = 20000; /// <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 = 200; protected readonly Bindable<ScrollingDirection> Direction = new Bindable<ScrollingDirection>(); /// <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="TimeRange"/> = 1000. /// </summary> protected readonly BindableDouble TimeRange = new BindableDouble(time_span_default) { MinValue = time_span_min, MaxValue = time_span_max }; /// <summary> /// Whether the player can change <see cref="TimeRange"/>. /// </summary> protected virtual bool UserScrollSpeedAdjustment => true; /// <summary> /// Whether at the current point in time, whether scroll speed adjustments should be applied to gameplay. /// This can potentially become false at some point during gameplay for game balance reasons. /// </summary> protected bool AllowScrollSpeedAdjustment => UserScrollSpeedAdjustment && player?.AllowCriticalSettingsAdjustment != false; /// <summary> /// Whether <see cref="TimingControlPoint"/> beat lengths should scale relative to the most common beat length in the <see cref="Beatmap"/>. /// </summary> protected virtual bool RelativeScaleBeatLengths => false; /// <summary> /// The <see cref="MultiplierControlPoint"/>s that adjust the scrolling rate of <see cref="HitObject"/>s inside this <see cref="DrawableRuleset{TObject}"/>. /// </summary> protected readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>(Comparer<MultiplierControlPoint>.Default); public IScrollingInfo ScrollingInfo => scrollingInfo; [Cached(Type = typeof(IScrollingInfo))] private readonly LocalScrollingInfo scrollingInfo; [Resolved] private Player? player { get; set; } protected DrawableScrollingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList<Mod>? mods = null) : base(ruleset, beatmap, mods) { scrollingInfo = new LocalScrollingInfo(); scrollingInfo.Direction.BindTo(Direction); scrollingInfo.TimeRange.BindTo(TimeRange); } [BackgroundDependencyLoader] private void load() { updateScrollAlgorithm(); double lastObjectTime = Beatmap.HitObjects.Any() ? Beatmap.GetLastObjectTime() : double.MaxValue; double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; if (RelativeScaleBeatLengths) { baseBeatLength = Beatmap.GetMostCommonBeatLength(); // The slider multiplier is post-multiplied to determine the final velocity, but for relative scale beat lengths // the multiplier should not affect the effective timing point (the longest in the beatmap), so it is factored out here baseBeatLength /= Beatmap.Difficulty.SliderMultiplier; } // Merge sequences of timing and difficulty control points to create the aggregate "multiplier" control point var lastTimingPoint = new TimingControlPoint(); var lastEffectPoint = new EffectControlPoint(); var allPoints = new SortedList<ControlPoint>(Comparer<ControlPoint>.Default); allPoints.AddRange(Beatmap.ControlPointInfo.TimingPoints); allPoints.AddRange(Beatmap.ControlPointInfo.EffectPoints); // Generate the timing points, making non-timing changes use the previous timing change and vice-versa var timingChanges = allPoints.Select(c => { switch (c) { case TimingControlPoint timingPoint: lastTimingPoint = timingPoint; break; case EffectControlPoint difficultyPoint: lastEffectPoint = difficultyPoint; break; } return new MultiplierControlPoint(c.Time) { Velocity = Beatmap.Difficulty.SliderMultiplier, BaseBeatLength = baseBeatLength, TimingPoint = lastTimingPoint, EffectPoint = lastEffectPoint }; }); // Trim unwanted sequences of timing changes timingChanges = timingChanges // Collapse sections after the last hit object .Where(s => s.Time <= lastObjectTime) // Collapse sections with the same start time .GroupBy(s => s.Time).Select(g => g.Last()).OrderBy(s => s.Time); ControlPoints.AddRange(timingChanges); if (ControlPoints.Count == 0) ControlPoints.Add(new MultiplierControlPoint { Velocity = Beatmap.Difficulty.SliderMultiplier }); } protected override void LoadComplete() { base.LoadComplete(); if (!(Playfield is ScrollingPlayfield)) throw new ArgumentException($"{nameof(Playfield)} must be a {nameof(ScrollingPlayfield)} when using {nameof(DrawableScrollingRuleset<TObject>)}."); } private ScrollVisualisationMethod visualisationMethod = ScrollVisualisationMethod.Sequential; public ScrollVisualisationMethod VisualisationMethod { get => visualisationMethod; set { visualisationMethod = value; updateScrollAlgorithm(); } } private void updateScrollAlgorithm() { switch (VisualisationMethod) { case ScrollVisualisationMethod.Sequential: scrollingInfo.Algorithm.Value = new SequentialScrollAlgorithm(ControlPoints); break; case ScrollVisualisationMethod.Overlapping: scrollingInfo.Algorithm.Value = new OverlappingScrollAlgorithm(ControlPoints); break; case ScrollVisualisationMethod.Constant: scrollingInfo.Algorithm.Value = new ConstantScrollAlgorithm(); break; } } /// <summary> /// Adjusts the scroll speed of <see cref="HitObject"/>s. /// </summary> /// <param name="amount">The amount to adjust by. Greater than 0 if the scroll speed should be increased, less than 0 if it should be decreased.</param> protected virtual void AdjustScrollSpeed(int amount) { this.TransformBindableTo(TimeRange, TimeRange.Value - amount * time_span_step, 200, Easing.OutQuint); } public bool OnPressed(KeyBindingPressEvent<GlobalAction> e) { switch (e.Action) { case GlobalAction.IncreaseScrollSpeed: if (AllowScrollSpeedAdjustment) AdjustScrollSpeed(1); return true; case GlobalAction.DecreaseScrollSpeed: if (AllowScrollSpeedAdjustment) AdjustScrollSpeed(-1); return true; } return false; } private ScheduledDelegate? scheduledScrollSpeedAdjustment; public void OnReleased(KeyBindingReleaseEvent<GlobalAction> e) { scheduledScrollSpeedAdjustment?.Cancel(); scheduledScrollSpeedAdjustment = null; } private class LocalScrollingInfo : IScrollingInfo { public IBindable<ScrollingDirection> Direction { get; } = new Bindable<ScrollingDirection>(); public IBindable<double> TimeRange { get; } = new BindableDouble(); public readonly Bindable<IScrollAlgorithm> Algorithm = new Bindable<IScrollAlgorithm>(new ConstantScrollAlgorithm()); IBindable<IScrollAlgorithm> IScrollingInfo.Algorithm => Algorithm; } } }