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
2022-06-17 15:37:17 +08:00
#nullable disable
2022-09-27 22:54:24 +08:00
using System ;
2019-09-02 14:02:16 +08:00
using System.Collections.Generic ;
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 ;
2022-10-04 13:01:36 +08:00
using osu.Framework.Graphics.Primitives ;
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
{
2022-11-24 13:32:20 +08:00
public partial class ScrollingHitObjectContainer : HitObjectContainer
2018-04-13 17:19:50 +08:00
{
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-15 12:11:07 +08:00
float scrollPosition = axisInverted ? - localPosition : localPosition ;
2021-06-14 12:10:07 +08:00
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-15 12:11:07 +08:00
Vector2 pos = ToLocalSpace ( screenSpacePosition ) ;
float localPosition = scrollingAxis = = Direction . Horizontal ? pos . X : pos . Y ;
localPosition - = axisInverted ? scrollLength : 0 ;
return TimeAtPosition ( localPosition , 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>
2022-10-18 15:15:21 +08:00
public float PositionAtTime ( double time , double currentTime , double? originTime = null )
2021-06-11 17:28:48 +08:00
{
2022-10-18 15:15:21 +08:00
float scrollPosition = scrollingInfo . Algorithm . PositionAt ( time , currentTime , timeRange . Value , scrollLength , originTime ) ;
2021-06-15 12:11:07 +08:00
return axisInverted ? - 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-15 12:11:07 +08:00
localPosition + = axisInverted ? scrollLength : 0 ;
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
2022-10-04 13:01:36 +08:00
public override void Add ( HitObjectLifetimeEntry entry )
{
// Scroll info is not available until loaded.
// The lifetime of all entries will be updated in the first Update.
if ( IsLoaded )
setComputedLifetimeStart ( entry ) ;
base . Add ( entry ) ;
}
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 )
{
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
foreach ( var entry in Entries )
2022-10-04 13:01:36 +08:00
setComputedLifetimeStart ( entry ) ;
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
}
2022-10-04 13:01:36 +08:00
/// <summary>
/// Get a conservative maximum bounding box of a <see cref="DrawableHitObject"/> corresponding to <paramref name="entry"/>.
/// It is used to calculate when the hit object appears.
/// </summary>
protected virtual RectangleF GetConservativeBoundingBox ( HitObjectLifetimeEntry entry ) = > new RectangleF ( ) . Inflate ( 100 ) ;
2021-05-31 22:07:32 +08:00
2022-10-04 13:01:36 +08:00
private double computeDisplayStartTime ( HitObjectLifetimeEntry entry )
{
RectangleF boundingBox = GetConservativeBoundingBox ( entry ) ;
float startOffset = 0 ;
2019-12-27 03:23:16 +08:00
switch ( direction . Value )
{
2022-10-04 13:01:36 +08:00
case ScrollingDirection . Right :
startOffset = boundingBox . Right ;
2019-12-27 03:23:16 +08:00
break ;
case ScrollingDirection . Down :
2022-10-04 13:01:36 +08:00
startOffset = boundingBox . Bottom ;
2019-12-27 03:23:16 +08:00
break ;
case ScrollingDirection . Left :
2022-10-04 13:01:36 +08:00
startOffset = - boundingBox . Left ;
2019-12-27 03:23:16 +08:00
break ;
2022-10-04 13:01:36 +08:00
case ScrollingDirection . Up :
startOffset = - boundingBox . Top ;
2019-12-27 03:23:16 +08:00
break ;
}
2022-10-04 13:01:36 +08:00
return scrollingInfo . Algorithm . GetDisplayStartTime ( entry . HitObject . StartTime , startOffset , timeRange . Value , scrollLength ) ;
}
private void setComputedLifetimeStart ( HitObjectLifetimeEntry entry )
{
double computedStartTime = computeDisplayStartTime ( entry ) ;
2022-09-27 22:54:24 +08:00
// always load the hitobject before its first judgement offset
2022-10-04 13:01:36 +08:00
double judgementOffset = entry . HitObject . HitWindows ? . WindowFor ( Scoring . HitResult . Miss ) ? ? 0 ;
entry . LifetimeStart = Math . Min ( entry . HitObject . StartTime - judgementOffset , computedStartTime ) ;
2019-12-27 03:23:16 +08:00
}
2022-10-20 21:30:30 +08:00
private void updateLayoutRecursive ( DrawableHitObject hitObject , double? parentHitObjectStartTime = null )
2019-09-02 14:02:16 +08:00
{
2022-10-20 21:30:30 +08:00
parentHitObjectStartTime ? ? = hitObject . HitObject . StartTime ;
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 )
{
2022-10-20 21:30:30 +08:00
updateLayoutRecursive ( obj , parentHitObjectStartTime ) ;
2018-10-30 17:00:55 +08:00
2022-10-04 14:03:04 +08:00
// Nested hitobjects don't need to scroll, but they do need accurate positions and start lifetime
2022-10-20 21:30:30 +08:00
updatePosition ( obj , hitObject . HitObject . StartTime , parentHitObjectStartTime ) ;
2022-10-04 14:03:04 +08:00
setComputedLifetimeStart ( obj . Entry ) ;
2018-04-13 17:19:50 +08:00
}
2020-11-24 15:06:01 +08:00
}
2018-04-13 17:19:50 +08:00
2022-10-18 15:15:21 +08:00
private void updatePosition ( DrawableHitObject hitObject , double currentTime , double? parentHitObjectStartTime = null )
2018-10-30 17:00:55 +08:00
{
2022-10-18 15:15:21 +08:00
float position = PositionAtTime ( hitObject . HitObject . StartTime , currentTime , parentHitObjectStartTime ) ;
2019-04-01 11:44:46 +08:00
2021-06-11 22:50:41 +08:00
if ( scrollingAxis = = Direction . Horizontal )
2021-06-15 12:11:07 +08:00
hitObject . X = position ;
2021-06-11 17:28:48 +08:00
else
2021-06-15 12:11:07 +08:00
hitObject . Y = position ;
2018-04-13 17:19:50 +08:00
}
}
}