1
0
mirror of https://github.com/ppy/osu.git synced 2024-09-30 20:17:34 +08:00
osu-lazer/osu.Game/Rulesets/UI/GameplaySampleTriggerSource.cs
Dean Herbert 555ce7684b Adjust GameplaySampleTriggerSource to only switch samples when close enough to the next hit object
Closes #23963.

To simplify things, I've removed the optimisation of using
`AliveObject`s because it would break the way this whole lookup works.
2023-06-20 20:06:07 +09:00

121 lines
4.6 KiB
C#

// 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.
#nullable disable
using System.Linq;
using osu.Framework.Graphics.Containers;
using osu.Game.Audio;
using osu.Game.Rulesets.Objects;
using osu.Game.Rulesets.Scoring;
using osu.Game.Skinning;
namespace osu.Game.Rulesets.UI
{
/// <summary>
/// A component which can trigger the most appropriate hit sound for a given point in time, based on the state of a <see cref="HitObjectContainer"/>
/// </summary>
public partial class GameplaySampleTriggerSource : CompositeDrawable
{
/// <summary>
/// The number of concurrent samples allowed to be played concurrently so that it feels better when spam-pressing a key.
/// </summary>
private const int max_concurrent_hitsounds = OsuGameBase.SAMPLE_CONCURRENCY;
private readonly HitObjectContainer hitObjectContainer;
private int nextHitSoundIndex;
private readonly Container<SkinnableSound> hitSounds;
public GameplaySampleTriggerSource(HitObjectContainer hitObjectContainer)
{
this.hitObjectContainer = hitObjectContainer;
InternalChild = hitSounds = new Container<SkinnableSound>
{
Name = "concurrent sample pool",
ChildrenEnumerable = Enumerable.Range(0, max_concurrent_hitsounds).Select(_ => new PausableSkinnableSound())
};
}
private HitObjectLifetimeEntry mostValidObject;
/// <summary>
/// Play the most appropriate hit sound for the current point in time.
/// </summary>
public virtual void Play()
{
var nextObject = GetMostValidObject();
if (nextObject == null)
return;
var samples = nextObject.Samples
.Cast<ISampleInfo>()
.ToArray();
PlaySamples(samples);
}
protected virtual void PlaySamples(ISampleInfo[] samples) => Schedule(() =>
{
var hitSound = getNextSample();
hitSound.Samples = samples;
hitSound.Play();
});
protected HitObject GetMostValidObject()
{
if (mostValidObject == null || isAlreadyHit(mostValidObject))
{
// We need to use lifetime entries to find the next object (we can't just use `hitObjectContainer.Objects` due to pooling - it may even be empty).
// If required, we can make this lookup more efficient by adding support to get next-future-entry in LifetimeEntryManager.
var candidate = hitObjectContainer.Entries.Where(e => !isAlreadyHit(e)).MinBy(e => e.HitObject.StartTime);
// In the case there are no non-judged objects, the last hit object should be used instead.
if (candidate == null)
mostValidObject = hitObjectContainer.Entries.LastOrDefault();
else
{
if (isCloseEnoughToCurrentTime(candidate))
mostValidObject = candidate;
else
mostValidObject ??= hitObjectContainer.Entries.FirstOrDefault();
}
}
if (mostValidObject == null)
return null;
// If the fallback has been judged then we want the sample from the object itself.
if (isAlreadyHit(mostValidObject))
return mostValidObject.HitObject;
// Else we want the earliest (including nested).
// In cases of nested objects, they will always have earlier sample data than their parent object.
return getEarliestNestedObject(mostValidObject.HitObject);
}
private bool isAlreadyHit(HitObjectLifetimeEntry h) => h.Result?.HasResult == true;
private bool isCloseEnoughToCurrentTime(HitObjectLifetimeEntry h) => Time.Current > h.HitObject.StartTime - h.HitObject.HitWindows.WindowFor(HitResult.Miss) * 1.5;
private HitObject getEarliestNestedObject(HitObject hitObject)
{
var nested = hitObject.NestedHitObjects.FirstOrDefault();
return nested != null ? getEarliestNestedObject(nested) : hitObject;
}
private SkinnableSound getNextSample()
{
SkinnableSound hitSound = hitSounds[nextHitSoundIndex];
// round robin over available samples to allow for concurrent playback.
nextHitSoundIndex = (nextHitSoundIndex + 1) % max_concurrent_hitsounds;
return hitSound;
}
}
}