1
0
mirror of https://github.com/ppy/osu.git synced 2024-12-21 11:12:55 +08:00
osu-lazer/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

354 lines
13 KiB
C#
Raw Normal View History

// 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
using System;
2020-07-22 15:37:38 +08:00
using System.Linq;
using JetBrains.Annotations;
using osu.Framework.Allocation;
2019-02-21 18:04:31 +08:00
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
2020-11-19 19:40:30 +08:00
using osu.Game.Audio;
using osu.Game.Graphics.Containers;
2018-11-14 13:29:22 +08:00
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Osu.Skinning;
2020-12-04 19:21:53 +08:00
using osu.Game.Rulesets.Osu.Skinning.Default;
using osu.Game.Rulesets.Scoring;
2018-12-08 05:24:24 +08:00
using osu.Game.Skinning;
using osuTK;
using osuTK.Graphics;
2018-04-13 17:19:50 +08:00
2017-04-18 15:05:58 +08:00
namespace osu.Game.Rulesets.Osu.Objects.Drawables
{
public class DrawableSlider : DrawableOsuHitObject
{
2020-11-05 12:51:46 +08:00
public new Slider HitObject => (Slider)base.HitObject;
public DrawableSliderHead HeadCircle => headContainer.Child;
public DrawableSliderTail TailCircle => tailContainer.Child;
2018-04-13 17:19:50 +08:00
[Cached]
public DrawableSliderBall Ball { get; private set; }
2020-11-05 12:51:46 +08:00
public SkinnableDrawable Body { get; private set; }
2018-04-13 17:19:50 +08:00
private ShakeContainer shakeContainer;
/// <summary>
/// A target container which can be used to add top level elements to the slider's display.
/// Intended to be used for proxy purposes only.
/// </summary>
public Container OverlayElementContainer { get; private set; }
public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects;
[CanBeNull]
public PlaySliderBody SliderBody => Body.Drawable as PlaySliderBody;
2019-12-17 17:16:25 +08:00
public IBindable<int> PathVersion => pathVersion;
private readonly Bindable<int> pathVersion = new Bindable<int>();
2020-11-05 12:51:46 +08:00
private Container<DrawableSliderHead> headContainer;
private Container<DrawableSliderTail> tailContainer;
private Container<DrawableSliderTick> tickContainer;
private Container<DrawableSliderRepeat> repeatContainer;
2020-11-19 19:40:30 +08:00
private PausableSkinnableSound slidingSample;
2020-11-10 23:22:06 +08:00
public DrawableSlider()
: this(null)
{
}
public DrawableSlider([CanBeNull] Slider s = null)
: base(s)
{
Ball = new DrawableSliderBall
{
GetInitialHitAction = () => HeadCircle.HitAction,
BypassAutoSizeAxes = Axes.Both,
AlwaysPresent = true,
Alpha = 0
};
2020-11-05 12:51:46 +08:00
}
2018-04-13 17:19:50 +08:00
2020-11-05 12:51:46 +08:00
[BackgroundDependencyLoader]
private void load()
{
AddRangeInternal(new Drawable[]
{
shakeContainer = new ShakeContainer
{
ShakeDuration = 30,
RelativeSizeAxes = Axes.Both,
Children = new Drawable[]
{
Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling),
tailContainer = new Container<DrawableSliderTail> { RelativeSizeAxes = Axes.Both },
tickContainer = new Container<DrawableSliderTick> { RelativeSizeAxes = Axes.Both },
repeatContainer = new Container<DrawableSliderRepeat> { RelativeSizeAxes = Axes.Both },
}
},
// slider head is not included in shake as it handles hit detection, and handles its own shaking.
headContainer = new Container<DrawableSliderHead> { RelativeSizeAxes = Axes.Both },
OverlayElementContainer = new Container { RelativeSizeAxes = Axes.Both, },
Ball,
2020-11-19 19:40:30 +08:00
slidingSample = new PausableSkinnableSound { Looping = true }
});
2018-04-13 17:19:50 +08:00
PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition);
ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue));
2018-04-13 17:19:50 +08:00
2019-07-22 13:45:25 +08:00
AccentColour.BindValueChanged(colour =>
{
2018-07-02 15:10:56 +08:00
foreach (var drawableHitObject in NestedHitObjects)
2019-07-22 13:45:25 +08:00
drawableHitObject.AccentColour.Value = colour.NewValue;
2020-11-16 21:40:25 +08:00
updateBallTint();
2019-07-22 13:45:25 +08:00
}, true);
2020-07-22 15:37:38 +08:00
Tracking.BindValueChanged(updateSlidingSample);
}
protected override void OnApply()
{
base.OnApply();
// Ensure that the version will change after the upcoming BindTo().
pathVersion.Value = int.MaxValue;
PathVersion.BindTo(HitObject.Path.Version);
}
public override void Shake() => shakeContainer.Shake();
protected override void OnFree()
{
base.OnFree();
PathVersion.UnbindFrom(HitObject.Path.Version);
2020-11-19 19:40:30 +08:00
slidingSample.Samples = null;
}
2020-07-22 15:37:38 +08:00
protected override void LoadSamples()
{
// Note: base.LoadSamples() isn't called since the slider plays the tail's hitsounds for the time being.
if (HitObject.SampleControlPoint == null)
{
throw new InvalidOperationException($"{nameof(HitObject)}s must always have an attached {nameof(HitObject.SampleControlPoint)}."
+ $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}.");
}
Samples.Samples = HitObject.TailSamples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
slidingSample.Samples = HitObject.CreateSlidingSamples().Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast<ISampleInfo>().ToArray();
2020-07-22 15:37:38 +08:00
}
public override void StopAllSamples()
{
base.StopAllSamples();
slidingSample?.Stop();
}
2020-07-22 15:37:38 +08:00
private void updateSlidingSample(ValueChangedEvent<bool> tracking)
{
2020-09-29 11:45:20 +08:00
if (tracking.NewValue)
2020-07-22 15:37:38 +08:00
slidingSample?.Play();
else
slidingSample?.Stop();
}
2018-04-13 17:19:50 +08:00
protected override void AddNestedHitObject(DrawableHitObject hitObject)
{
base.AddNestedHitObject(hitObject);
2019-10-17 11:53:54 +08:00
switch (hitObject)
{
case DrawableSliderHead head:
headContainer.Child = head;
break;
case DrawableSliderTail tail:
tailContainer.Child = tail;
break;
case DrawableSliderTick tick:
tickContainer.Add(tick);
break;
case DrawableSliderRepeat repeat:
repeatContainer.Add(repeat);
break;
}
}
protected override void ClearNestedHitObjects()
{
base.ClearNestedHitObjects();
2020-11-12 14:59:48 +08:00
headContainer.Clear(false);
tailContainer.Clear(false);
repeatContainer.Clear(false);
tickContainer.Clear(false);
OverlayElementContainer.Clear(false);
}
protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject)
{
switch (hitObject)
{
case SliderTailCircle tail:
2020-11-05 12:51:46 +08:00
return new DrawableSliderTail(tail);
case SliderHeadCircle head:
2020-11-12 14:59:48 +08:00
return new DrawableSliderHead(head);
case SliderTick tick:
2020-11-12 14:59:48 +08:00
return new DrawableSliderTick(tick);
2020-03-19 13:42:02 +08:00
case SliderRepeat repeat:
2020-11-12 14:59:48 +08:00
return new DrawableSliderRepeat(repeat);
}
return base.CreateNestedHitObject(hitObject);
}
public readonly Bindable<bool> Tracking = new Bindable<bool>();
2018-04-13 17:19:50 +08:00
protected override void Update()
2016-11-17 20:29:35 +08:00
{
base.Update();
2018-04-13 17:19:50 +08:00
Tracking.Value = Ball.Tracking;
2018-04-13 17:19:50 +08:00
2020-07-22 15:37:38 +08:00
if (Tracking.Value && slidingSample != null)
// keep the sliding sample playing at the current tracking position
slidingSample.Balance.Value = CalculateSamplePlaybackBalance(CalculateDrawableRelativePosition(Ball));
2020-07-22 15:37:38 +08:00
2020-11-05 12:51:46 +08:00
double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1);
2018-04-13 17:19:50 +08:00
Ball.UpdateProgress(completionProgress);
SliderBody?.UpdateProgress(completionProgress);
foreach (DrawableHitObject hitObject in NestedHitObjects)
{
if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(SliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(SliderBody?.SnakedEnd ?? 0));
if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking;
}
2018-04-13 17:19:50 +08:00
Size = SliderBody?.Size ?? Vector2.Zero;
OriginPosition = SliderBody?.PathOffset ?? Vector2.Zero;
2018-04-13 17:19:50 +08:00
2018-02-26 15:11:26 +08:00
if (DrawSize != Vector2.Zero)
{
var childAnchorPosition = Vector2.Divide(OriginPosition, DrawSize);
foreach (var obj in NestedHitObjects)
obj.RelativeAnchorPosition = childAnchorPosition;
Ball.RelativeAnchorPosition = childAnchorPosition;
}
}
2018-04-13 17:19:50 +08:00
public override void OnKilled()
{
base.OnKilled();
SliderBody?.RecyclePath();
}
protected override void ApplySkin(ISkinSource skin, bool allowFallback)
2018-12-08 05:24:24 +08:00
{
base.ApplySkin(skin, allowFallback);
2019-01-07 19:12:39 +08:00
2020-11-16 21:40:25 +08:00
updateBallTint();
}
private void updateBallTint()
{
if (CurrentSkin == null)
return;
bool allowBallTint = CurrentSkin.GetConfig<OsuSkinConfiguration, bool>(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false;
Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White;
2018-12-08 05:24:24 +08:00
}
protected override void CheckForResult(bool userTriggered, double timeOffset)
2016-11-29 20:40:24 +08:00
{
2020-11-05 12:51:46 +08:00
if (userTriggered || Time.Current < HitObject.EndTime)
return;
2021-02-10 17:52:39 +08:00
// If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes.
// But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc).
if (HitObject.OnlyJudgeNestedObjects)
{
ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult);
return;
}
2021-02-10 20:27:12 +08:00
// Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring.
ApplyResult(r =>
{
int totalTicks = NestedHitObjects.Count;
int hitTicks = NestedHitObjects.Count(h => h.IsHit);
if (hitTicks == totalTicks)
r.Type = HitResult.Great;
2021-02-10 20:24:41 +08:00
else if (hitTicks == 0)
r.Type = HitResult.Miss;
2021-02-10 20:24:41 +08:00
else
{
2021-02-10 21:09:24 +08:00
double hitFraction = (double)hitTicks / totalTicks;
2021-02-10 20:25:31 +08:00
r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh;
2021-02-10 20:24:41 +08:00
}
});
2020-03-26 18:51:02 +08:00
}
public override void PlaySamples()
{
// rather than doing it this way, we should probably attach the sample to the tail circle.
// this can only be done after we stop using LegacyLastTick.
if (!TailCircle.SamplePlaysOnlyOnHit || TailCircle.IsHit)
2020-03-26 18:51:02 +08:00
base.PlaySamples();
2016-11-29 20:40:24 +08:00
}
2018-04-13 17:19:50 +08:00
protected override void UpdateInitialTransforms()
{
base.UpdateInitialTransforms();
Body.FadeInFromZero(HitObject.TimeFadeIn);
}
2020-11-04 15:19:07 +08:00
protected override void UpdateStartTimeStateTransforms()
{
2020-11-04 15:19:07 +08:00
base.UpdateStartTimeStateTransforms();
2019-09-13 17:49:21 +08:00
Ball.FadeIn();
Ball.ScaleTo(HitObject.Scale);
2020-11-04 15:19:07 +08:00
}
2018-04-13 17:19:50 +08:00
2020-11-04 15:19:07 +08:00
protected override void UpdateHitStateTransforms(ArmedState state)
{
base.UpdateHitStateTransforms(state);
2018-04-13 17:19:50 +08:00
2020-11-04 15:19:07 +08:00
const float fade_out_time = 450;
2018-04-13 17:19:50 +08:00
2020-11-04 15:19:07 +08:00
switch (state)
{
case ArmedState.Hit:
if (SliderBody?.SnakingOut.Value == true)
Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear.
2020-11-04 15:19:07 +08:00
break;
}
2020-11-04 15:19:07 +08:00
this.FadeOut(fade_out_time, Easing.OutQuint).Expire();
}
2018-04-13 17:19:50 +08:00
public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => SliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos);
private class DefaultSliderBody : PlaySliderBody
{
}
}
}