2019-01-24 16:43:03 +08:00
// 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.
2018-04-13 17:19:50 +08:00
2019-09-02 14:02:16 +08:00
using System.Collections.Generic ;
2021-05-31 22:07:32 +08:00
using System.Diagnostics ;
2018-11-06 11:01:54 +08:00
using osu.Framework.Allocation ;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables ;
2018-04-13 17:19:50 +08:00
using osu.Framework.Graphics ;
2020-02-24 19:52:15 +08:00
using osu.Framework.Layout ;
2021-05-31 22:07:32 +08:00
using osu.Game.Rulesets.Objects ;
2018-04-13 17:19:50 +08:00
using osu.Game.Rulesets.Objects.Drawables ;
2018-10-30 17:00:55 +08:00
using osu.Game.Rulesets.Objects.Types ;
2020-05-25 17:26:28 +08:00
using osuTK ;
2018-04-13 17:19:50 +08:00
namespace osu.Game.Rulesets.UI.Scrolling
{
public class ScrollingHitObjectContainer : HitObjectContainer
{
2019-08-26 15:47:23 +08:00
private readonly IBindable < double > timeRange = new BindableDouble ( ) ;
2018-11-06 14:46:36 +08:00
private readonly IBindable < ScrollingDirection > direction = new Bindable < ScrollingDirection > ( ) ;
2020-11-24 15:06:01 +08:00
2021-06-11 17:28:48 +08:00
/// <summary>
2021-06-11 22:50:41 +08:00
/// Whether the scrolling direction is horizontal or vertical.
2021-06-11 17:28:48 +08:00
/// </summary>
2021-06-11 22:50:41 +08:00
private Direction scrollingAxis = > direction . Value = = ScrollingDirection . Left | | direction . Value = = ScrollingDirection . Right ? Direction . Horizontal : Direction . Vertical ;
2021-06-11 17:28:48 +08:00
2021-06-14 12:10:07 +08:00
/// <summary>
2021-06-15 03:51:32 +08:00
/// The scrolling axis is inverted if objects temporally farther in the future have a smaller position value across the scrolling axis.
2021-06-14 12:10:07 +08:00
/// </summary>
2021-06-15 03:51:32 +08:00
/// <example>
/// <see cref="ScrollingDirection.Down"/> is inverted, because given two objects, one of which is at the current time and one of which is 1000ms in the future,
/// in the current time instant the future object is spatially above the current object, and therefore has a smaller value of the Y coordinate of its position.
/// </example>
2021-06-14 12:10:07 +08:00
private bool axisInverted = > direction . Value = = ScrollingDirection . Down | | direction . Value = = ScrollingDirection . Right ;
2020-11-30 14:54:20 +08:00
/// <summary>
2021-05-31 15:02:33 +08:00
/// A set of top-level <see cref="DrawableHitObject"/>s which have an up-to-date layout.
2020-11-30 14:54:20 +08:00
/// </summary>
2021-05-18 18:55:31 +08:00
private readonly HashSet < DrawableHitObject > layoutComputed = new HashSet < DrawableHitObject > ( ) ;
2018-10-30 17:33:24 +08:00
2018-11-06 14:46:36 +08:00
[Resolved]
private IScrollingInfo scrollingInfo { get ; set ; }
2020-05-10 12:49:08 +08:00
// Responds to changes in the layout. When the layout changes, all hit object states must be recomputed.
2020-05-08 17:49:58 +08:00
private readonly LayoutValue layoutCache = new LayoutValue ( Invalidation . RequiredParentSizeToFit | Invalidation . DrawInfo ) ;
2018-11-06 11:01:54 +08:00
public ScrollingHitObjectContainer ( )
2018-04-13 17:19:50 +08:00
{
RelativeSizeAxes = Axes . Both ;
2020-02-24 19:52:15 +08:00
2020-05-08 17:49:58 +08:00
AddLayout ( layoutCache ) ;
2018-11-06 14:46:36 +08:00
}
[BackgroundDependencyLoader]
private void load ( )
{
direction . BindTo ( scrollingInfo . Direction ) ;
2018-11-07 16:24:05 +08:00
timeRange . BindTo ( scrollingInfo . TimeRange ) ;
2020-05-08 17:49:58 +08:00
direction . ValueChanged + = _ = > layoutCache . Invalidate ( ) ;
timeRange . ValueChanged + = _ = > layoutCache . Invalidate ( ) ;
2018-04-13 17:19:50 +08:00
}
2020-05-25 21:09:09 +08:00
/// <summary>
2021-06-15 03:51:32 +08:00
/// Given a position at <paramref name="currentTime"/>, return the time of the object corresponding to the position.
2020-05-25 21:09:09 +08:00
/// </summary>
2021-06-14 11:41:44 +08:00
/// <remarks>
/// If there are multiple valid time values, one arbitrary time is returned.
/// </remarks>
2021-06-14 12:10:07 +08:00
public double TimeAtPosition ( float localPosition , double currentTime )
2020-05-25 17:26:28 +08:00
{
2021-06-14 12:10:07 +08:00
float scrollPosition = axisInverted ? scrollLength - localPosition : localPosition ;
return scrollingInfo . Algorithm . TimeAt ( scrollPosition , currentTime , timeRange . Value , scrollLength ) ;
2020-05-25 17:26:28 +08:00
}
2020-05-25 21:09:09 +08:00
/// <summary>
2021-06-14 11:41:44 +08:00
/// Given a position at the current time in screen space, return the time of the object corresponding the position.
2020-05-25 21:09:09 +08:00
/// </summary>
2021-06-14 11:41:44 +08:00
/// <remarks>
/// If there are multiple valid time values, one arbitrary time is returned.
/// </remarks>
2021-06-11 17:28:48 +08:00
public double TimeAtScreenSpacePosition ( Vector2 screenSpacePosition )
2020-05-25 17:26:28 +08:00
{
2021-06-14 12:10:07 +08:00
Vector2 localPosition = ToLocalSpace ( screenSpacePosition ) ;
return TimeAtPosition ( scrollingAxis = = Direction . Horizontal ? localPosition . X : localPosition . Y , Time . Current ) ;
2021-06-11 17:28:48 +08:00
}
2020-05-25 17:26:28 +08:00
2021-06-11 17:28:48 +08:00
/// <summary>
/// Given a time, return the position along the scrolling axis within this <see cref="HitObjectContainer"/> at time <paramref name="currentTime"/>.
/// </summary>
public float PositionAtTime ( double time , double currentTime )
{
2021-06-14 12:10:07 +08:00
float scrollPosition = scrollingInfo . Algorithm . PositionAt ( time , currentTime , timeRange . Value , scrollLength ) ;
return axisInverted ? scrollLength - scrollPosition : scrollPosition ;
2020-05-25 17:26:28 +08:00
}
2021-06-11 17:28:48 +08:00
/// <summary>
/// Given a time, return the position along the scrolling axis within this <see cref="HitObjectContainer"/> at the current time.
/// </summary>
public float PositionAtTime ( double time ) = > PositionAtTime ( time , Time . Current ) ;
/// <summary>
/// Given a time, return the screen space position within this <see cref="HitObjectContainer"/>.
/// In the non-scrolling axis, the center of this <see cref="HitObjectContainer"/> is returned.
/// </summary>
public Vector2 ScreenSpacePositionAtTime ( double time )
2020-05-25 17:26:28 +08:00
{
2021-06-14 12:10:07 +08:00
float localPosition = PositionAtTime ( time , Time . Current ) ;
2021-06-11 22:50:41 +08:00
return scrollingAxis = = Direction . Horizontal
2021-06-14 12:10:07 +08:00
? ToScreenSpace ( new Vector2 ( localPosition , DrawHeight / 2 ) )
: ToScreenSpace ( new Vector2 ( DrawWidth / 2 , localPosition ) ) ;
2020-05-25 17:26:28 +08:00
}
2021-06-11 17:28:48 +08:00
/// <summary>
/// Given a start time and end time of a scrolling object, return the length of the object along the scrolling axis.
/// </summary>
public float LengthAtTime ( double startTime , double endTime )
2020-05-25 17:26:28 +08:00
{
2021-06-11 17:28:48 +08:00
return scrollingInfo . Algorithm . GetLength ( startTime , endTime , timeRange . Value , scrollLength ) ;
2020-05-25 17:26:28 +08:00
}
2021-06-11 22:50:41 +08:00
private float scrollLength = > scrollingAxis = = Direction . Horizontal ? DrawWidth : DrawHeight ;
2021-06-11 17:28:48 +08:00
2021-05-31 22:07:32 +08:00
protected override void AddDrawable ( HitObjectLifetimeEntry entry , DrawableHitObject drawable )
2020-11-27 12:36:40 +08:00
{
2021-05-31 22:07:32 +08:00
base . AddDrawable ( entry , drawable ) ;
invalidateHitObject ( drawable ) ;
drawable . DefaultsApplied + = invalidateHitObject ;
2020-11-27 12:36:40 +08:00
}
2021-05-31 22:07:32 +08:00
protected override void RemoveDrawable ( HitObjectLifetimeEntry entry , DrawableHitObject drawable )
2020-11-27 12:36:40 +08:00
{
2021-05-31 22:07:32 +08:00
base . RemoveDrawable ( entry , drawable ) ;
2020-11-27 12:36:40 +08:00
2021-05-31 22:07:32 +08:00
drawable . DefaultsApplied - = invalidateHitObject ;
layoutComputed . Remove ( drawable ) ;
2020-11-27 12:36:40 +08:00
}
2020-11-30 16:44:58 +08:00
private void invalidateHitObject ( DrawableHitObject hitObject )
{
2021-05-31 15:24:13 +08:00
hitObject . LifetimeStart = computeOriginAdjustedLifetimeStart ( hitObject ) ;
2020-11-30 16:44:58 +08:00
layoutComputed . Remove ( hitObject ) ;
}
2020-11-27 12:36:40 +08:00
2018-04-13 17:19:50 +08:00
protected override void Update ( )
{
base . Update ( ) ;
2021-05-31 15:24:13 +08:00
if ( layoutCache . IsValid ) return ;
2021-05-31 15:10:31 +08:00
2021-05-31 15:50:47 +08:00
layoutComputed . Clear ( ) ;
2021-05-18 18:55:25 +08:00
2021-05-31 15:50:47 +08:00
// Reset lifetime to the conservative estimation.
// If a drawable becomes alive by this lifetime, its lifetime will be updated to a more precise lifetime in the next update.
foreach ( var entry in Entries )
entry . SetInitialLifetime ( ) ;
2021-05-18 18:55:31 +08:00
2021-05-31 15:24:13 +08:00
scrollingInfo . Algorithm . Reset ( ) ;
2021-05-18 18:55:31 +08:00
2021-05-31 15:24:13 +08:00
layoutCache . Validate ( ) ;
2020-12-07 16:26:12 +08:00
}
protected override void UpdateAfterChildrenLife ( )
{
base . UpdateAfterChildrenLife ( ) ;
2020-11-24 17:52:15 +08:00
2021-05-18 18:55:25 +08:00
// We need to calculate hit object positions (including nested hit objects) as soon as possible after lifetimes
// to prevent hit objects displayed in a wrong position for one frame.
2020-12-07 16:26:12 +08:00
// Only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes).
2020-11-24 15:06:01 +08:00
foreach ( var obj in AliveObjects )
{
2020-12-07 16:26:12 +08:00
updatePosition ( obj , Time . Current ) ;
2020-11-24 17:52:15 +08:00
if ( layoutComputed . Contains ( obj ) )
2020-11-24 15:06:01 +08:00
continue ;
2020-11-24 12:57:20 +08:00
2020-11-24 15:06:01 +08:00
updateLayoutRecursive ( obj ) ;
2020-11-24 12:57:20 +08:00
2020-11-24 17:52:15 +08:00
layoutComputed . Add ( obj ) ;
2020-11-24 12:57:20 +08:00
}
2018-10-30 17:00:55 +08:00
}
2021-05-18 18:55:31 +08:00
private double computeOriginAdjustedLifetimeStart ( DrawableHitObject hitObject )
2019-12-27 03:23:16 +08:00
{
2021-05-31 22:07:32 +08:00
// Origin position may be relative to the parent size
Debug . Assert ( hitObject . Parent ! = null ) ;
2021-05-18 18:55:31 +08:00
float originAdjustment = 0.0f ;
2019-12-27 03:23:16 +08:00
2021-05-18 18:55:31 +08:00
// calculate the dimension of the part of the hitobject that should already be visible
// when the hitobject origin first appears inside the scrolling container
2019-12-27 03:23:16 +08:00
switch ( direction . Value )
{
2021-05-18 18:55:31 +08:00
case ScrollingDirection . Up :
originAdjustment = hitObject . OriginPosition . Y ;
2019-12-27 03:23:16 +08:00
break ;
case ScrollingDirection . Down :
2021-05-18 18:55:31 +08:00
originAdjustment = hitObject . DrawHeight - hitObject . OriginPosition . Y ;
2019-12-27 03:23:16 +08:00
break ;
case ScrollingDirection . Left :
2021-05-18 18:55:31 +08:00
originAdjustment = hitObject . OriginPosition . X ;
2019-12-27 03:23:16 +08:00
break ;
2021-05-18 18:55:31 +08:00
case ScrollingDirection . Right :
originAdjustment = hitObject . DrawWidth - hitObject . OriginPosition . X ;
2019-12-27 03:23:16 +08:00
break ;
}
2021-05-18 18:55:31 +08:00
return scrollingInfo . Algorithm . GetDisplayStartTime ( hitObject . HitObject . StartTime , originAdjustment , timeRange . Value , scrollLength ) ;
2019-12-27 03:23:16 +08:00
}
2020-11-24 15:06:01 +08:00
private void updateLayoutRecursive ( DrawableHitObject hitObject )
2019-09-02 14:02:16 +08:00
{
2020-05-27 11:38:39 +08:00
if ( hitObject . HitObject is IHasDuration e )
2018-10-30 17:00:55 +08:00
{
2021-06-11 17:28:48 +08:00
float length = LengthAtTime ( hitObject . HitObject . StartTime , e . EndTime ) ;
2021-06-11 22:50:41 +08:00
if ( scrollingAxis = = Direction . Horizontal )
2021-06-11 17:28:48 +08:00
hitObject . Width = length ;
else
hitObject . Height = length ;
2018-10-30 17:00:55 +08:00
}
2019-08-26 18:06:23 +08:00
2018-10-30 17:00:55 +08:00
foreach ( var obj in hitObject . NestedHitObjects )
{
2020-11-24 15:06:01 +08:00
updateLayoutRecursive ( obj ) ;
2018-10-30 17:00:55 +08:00
// Nested hitobjects don't need to scroll, but they do need accurate positions
updatePosition ( obj , hitObject . HitObject . StartTime ) ;
2018-04-13 17:19:50 +08:00
}
2020-11-24 15:06:01 +08:00
}
2018-04-13 17:19:50 +08:00
2018-10-30 17:00:55 +08:00
private void updatePosition ( DrawableHitObject hitObject , double currentTime )
{
2021-06-11 17:28:48 +08:00
float position = PositionAtTime ( hitObject . HitObject . StartTime , currentTime ) ;
2019-04-01 11:44:46 +08:00
2021-06-14 12:10:07 +08:00
// The position returned from `PositionAtTime` is assuming the `TopLeft` anchor.
// A correction is needed because the hit objects are using a different anchor for each direction (e.g. `BottomCentre` for `Bottom` direction).
float anchorCorrection = axisInverted ? scrollLength : 0 ;
2021-06-11 22:50:41 +08:00
if ( scrollingAxis = = Direction . Horizontal )
2021-06-14 12:10:07 +08:00
hitObject . X = position - anchorCorrection ;
2021-06-11 17:28:48 +08:00
else
2021-06-14 12:10:07 +08:00
hitObject . Y = position - anchorCorrection ;
2018-04-13 17:19:50 +08:00
}
}
}