1
0
mirror of https://github.com/ppy/osu.git synced 2025-03-23 08:27:23 +08:00

Split out scrolling algorithm

This commit is contained in:
smoogipoo 2018-01-07 12:47:09 +09:00
parent 2d345b2f80
commit 117ab8a26d
9 changed files with 205 additions and 107 deletions

View File

@ -99,19 +99,17 @@ namespace osu.Game.Tests.Visual
private class TestPlayfield : Playfield
private class TestPlayfield : ScrollingPlayfield
{
public readonly BindableDouble TimeRange = new BindableDouble(5000);
public readonly ScrollingDirection Direction;
public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects;
public TestPlayfield(ScrollingDirection direction)
: base(direction)
{
Direction = direction;
base.HitObjects = new ScrollingHitObjectContainer(direction);
HitObjects.TimeRange.BindTo(TimeRange);
}
}

View 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-framework/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);
}
}

View File

@ -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 readonly HitObjectContainer HitObjects;
public Container<Drawable> ScaledContent;
@ -52,10 +50,8 @@ namespace osu.Game.Rulesets.UI
}
});
HitObjects = new HitObjectContainer
{
RelativeSizeAxes = Axes.Both,
};
HitObjects = CreateHitObjectContainer();
HitObjects.RelativeSizeAxes = Axes.Both;
}
[BackgroundDependencyLoader]
@ -94,14 +90,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.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);
}
/// <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
{

View File

@ -0,0 +1,98 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/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.Algorithms
{
public class GlobalScrollingAlgorithm : IScrollingAlgorithm
{
private readonly Dictionary<DrawableHitObject, double> hitObjectPositions = new Dictionary<DrawableHitObject, double>();
private readonly IReadOnlyList<MultiplierControlPoint> controlPoints;
public GlobalScrollingAlgorithm(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;
obj.LifetimeEnd = ((obj.HitObject as IHasEndTime)?.EndTime ?? obj.HitObject.StartTime) + timeRange + 1000;
if (!(obj.HitObject is IHasEndTime endTime))
continue;
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;
}
}
}
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;
}
}
}

View File

@ -0,0 +1,15 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE
using System.Collections.Generic;
using osu.Game.Rulesets.Objects.Drawables;
using OpenTK;
namespace osu.Game.Rulesets.UI.Scrolling.Algorithms
{
public interface IScrollingAlgorithm
{
void ComputeInitialStates(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double timeRange, Vector2 length);
void ComputePositions(IEnumerable<DrawableHitObject> hitObjects, ScrollingDirection direction, double currentTime, double timeRange, Vector2 length);
}
}

View File

@ -0,0 +1,17 @@
// Copyright (c) 2007-2018 ppy Pty Ltd <contact@ppy.sh>.
// Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu-framework/master/LICENCE
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
namespace osu.Game.Rulesets.UI.Scrolling
{
public class GlobalScrollingHitObjectContainer : ScrollingHitObjectContainer
{
public GlobalScrollingHitObjectContainer(ScrollingDirection direction)
: base(direction)
{
}
protected override IScrollingAlgorithm CreateScrollingAlgorithm() => new GlobalScrollingAlgorithm(ControlPoints);
}
}

View File

@ -1,19 +1,17 @@
// Copyright (c) 2007-2017 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.Framework.Caching;
using osu.Framework.Configuration;
using osu.Framework.Graphics;
using osu.Framework.Lists;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Objects.Types;
using osu.Game.Rulesets.Timing;
using osu.Game.Rulesets.UI.Scrolling.Algorithms;
namespace osu.Game.Rulesets.UI.Scrolling
{
public class ScrollingHitObjectContainer : Playfield.HitObjectContainer
public abstract class ScrollingHitObjectContainer : HitObjectContainer
{
public readonly BindableDouble TimeRange = new BindableDouble
{
@ -21,22 +19,32 @@ namespace osu.Game.Rulesets.UI.Scrolling
MaxValue = double.MaxValue
};
protected readonly SortedList<MultiplierControlPoint> ControlPoints = new SortedList<MultiplierControlPoint>();
private readonly ScrollingDirection direction;
private Cached positionCache = new Cached();
private Cached initialStateCache = new Cached();
public ScrollingHitObjectContainer(ScrollingDirection direction)
protected ScrollingHitObjectContainer(ScrollingDirection direction)
{
this.direction = direction;
RelativeSizeAxes = Axes.Both;
TimeRange.ValueChanged += v => positionCache.Invalidate();
TimeRange.ValueChanged += v => initialStateCache.Invalidate();
}
private IScrollingAlgorithm scrollingAlgorithm;
protected override void LoadComplete()
{
base.LoadComplete();
scrollingAlgorithm = CreateScrollingAlgorithm();
}
public override void Add(DrawableHitObject hitObject)
{
positionCache.Invalidate();
initialStateCache.Invalidate();
base.Add(hitObject);
}
@ -44,69 +52,42 @@ namespace osu.Game.Rulesets.UI.Scrolling
{
var result = base.Remove(hitObject);
if (result)
positionCache.Invalidate();
initialStateCache.Invalidate();
return result;
}
private readonly SortedList<MultiplierControlPoint> controlPoints = new SortedList<MultiplierControlPoint>();
public void AddControlPoint(MultiplierControlPoint controlPoint)
{
controlPoints.Add(controlPoint);
positionCache.Invalidate();
ControlPoints.Add(controlPoint);
initialStateCache.Invalidate();
}
public bool RemoveControlPoint(MultiplierControlPoint controlPoint)
{
var result = controlPoints.Remove(controlPoint);
var result = ControlPoints.Remove(controlPoint);
if (result)
positionCache.Invalidate();
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)
positionCache.Invalidate();
initialStateCache.Invalidate();
return base.Invalidate(invalidation, source, shallPropagate);
}
private readonly Dictionary<DrawableHitObject, double> hitObjectPositions = new Dictionary<DrawableHitObject, double>();
protected override void Update()
{
base.Update();
if (positionCache.IsValid)
if (initialStateCache.IsValid)
return;
foreach (var obj in Objects)
{
var startPosition = hitObjectPositions[obj] = positionAt(obj.HitObject.StartTime);
scrollingAlgorithm.ComputeInitialStates(Objects, direction, TimeRange, DrawSize);
obj.LifetimeStart = obj.HitObject.StartTime - TimeRange - 1000;
obj.LifetimeEnd = ((obj.HitObject as IHasEndTime)?.EndTime ?? obj.HitObject.StartTime) + TimeRange + 1000;
if (!(obj.HitObject is IHasEndTime endTime))
continue;
var length = positionAt(endTime.EndTime) - startPosition;
switch (direction)
{
case ScrollingDirection.Up:
case ScrollingDirection.Down:
obj.Height = (float)(length * DrawHeight);
break;
case ScrollingDirection.Left:
case ScrollingDirection.Right:
obj.Width = (float)(length * DrawWidth);
break;
}
}
positionCache.Validate();
initialStateCache.Validate();
}
protected override void UpdateAfterChildrenLife()
@ -116,48 +97,12 @@ namespace osu.Game.Rulesets.UI.Scrolling
// We need to calculate this as soon as possible after lifetimes so that hitobjects
// get the final say in their positions
var timelinePosition = positionAt(Time.Current);
foreach (var obj in AliveObjects)
{
var finalPosition = hitObjectPositions[obj] - timelinePosition;
switch (direction)
{
case ScrollingDirection.Up:
obj.Y = (float)(finalPosition * DrawHeight);
break;
case ScrollingDirection.Down:
obj.Y = (float)(-finalPosition * DrawHeight);
break;
case ScrollingDirection.Left:
obj.X = (float)(finalPosition * DrawWidth);
break;
case ScrollingDirection.Right:
obj.X = (float)(-finalPosition * DrawWidth);
break;
}
}
scrollingAlgorithm.ComputePositions(AliveObjects, direction, Time.Current, TimeRange, DrawSize);
}
private double positionAt(double time)
{
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;
}
/// <summary>
/// Creates the algorithm that will process the positions of the <see cref="DrawableHitObject"/>s.
/// </summary>
protected abstract IScrollingAlgorithm CreateScrollingAlgorithm();
}
}

View File

@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// <summary>
/// A type of <see cref="Playfield"/> specialized towards scrolling <see cref="DrawableHitObject"/>s.
/// </summary>
public class ScrollingPlayfield : Playfield
public abstract class ScrollingPlayfield : Playfield
{
/// <summary>
/// The default span of time visible by the length of the scrolling axes.
@ -49,7 +49,9 @@ namespace osu.Game.Rulesets.UI.Scrolling
/// <summary>
/// The container that contains the <see cref="SpeedAdjustmentContainer"/>s and <see cref="DrawableHitObject"/>s.
/// </summary>
public new readonly ScrollingHitObjectContainer HitObjects;
public new ScrollingHitObjectContainer HitObjects => (ScrollingHitObjectContainer)base.HitObjects;
private readonly ScrollingDirection direction;
/// <summary>
/// Creates a new <see cref="ScrollingPlayfield"/>.
@ -59,7 +61,7 @@ namespace osu.Game.Rulesets.UI.Scrolling
protected ScrollingPlayfield(ScrollingDirection direction, float? customWidth = null)
: base(customWidth)
{
base.HitObjects = HitObjects = new ScrollingHitObjectContainer(direction) { RelativeSizeAxes = Axes.Both };
this.direction = direction;
HitObjects.TimeRange.BindTo(VisibleTimeRange);
}
@ -105,6 +107,14 @@ namespace osu.Game.Rulesets.UI.Scrolling
this.TransformTo(this.PopulateTransform(new TransformVisibleTimeRange(), newTimeRange, duration, easing));
}
protected sealed override HitObjectContainer CreateHitObjectContainer() => CreateScrollingHitObjectContainer();
/// <summary>
/// Creates the <see cref="ScrollingHitObjectContainer"/> that will handle the scrolling of the <see cref="DrawableHitObject"/>s.
/// </summary>
/// <returns></returns>
protected virtual ScrollingHitObjectContainer CreateScrollingHitObjectContainer() => new GlobalScrollingHitObjectContainer(direction);
private class TransformVisibleTimeRange : Transform<double, ScrollingPlayfield>
{
private double valueAt(double time)

View File

@ -315,6 +315,10 @@
<Compile Include="Rulesets\Mods\IApplicableMod.cs" />
<Compile Include="Rulesets\Mods\IApplicableToBeatmapConverter.cs" />
<Compile Include="Rulesets\Mods\IApplicableToDrawableHitObject.cs" />
<Compile Include="Rulesets\UI\HitObjectContainer.cs" />
<Compile Include="Rulesets\UI\Scrolling\Algorithms\GlobalScrollingAlgorithm.cs" />
<Compile Include="Rulesets\UI\Scrolling\Algorithms\IScrollingAlgorithm.cs" />
<Compile Include="Rulesets\UI\Scrolling\GlobalScrollingHitObjectContainer.cs" />
<Compile Include="Rulesets\UI\Scrolling\ScrollingDirection.cs" />
<Compile Include="Rulesets\UI\Scrolling\ScrollingHitObjectContainer.cs" />
<Compile Include="Rulesets\UI\Scrolling\ScrollingPlayfield.cs" />