1
0
mirror of https://github.com/ppy/osu.git synced 2026-05-26 06:49:54 +08:00
This commit is contained in:
vunyunt
2025-02-20 15:41:23 +01:00
committed by GitHub
Unverified
5 changed files with 206 additions and 29 deletions
+134 -2
View File
@@ -1,6 +1,11 @@
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Mods;
using osu.Game.Rulesets.Objects.Drawables;
using osu.Game.Rulesets.Taiko.Objects;
@@ -12,16 +17,143 @@ namespace osu.Game.Rulesets.Taiko.Mods
{
public class TaikoModClassic : ModClassic, IApplicableToDrawableRuleset<TaikoHitObject>, IApplicableToDrawableHitObject
{
private DrawableTaikoRuleset drawableTaikoRuleset = null!;
private TaikoPlayfieldAdjustmentContainer adjustmentContainer = null!;
private IReadOnlyList<Mod> mods = Array.Empty<Mod>();
private const float hd_base_fade_out_duration = 0.375f;
private const float hd_base_initial_alpha = 0.75f;
private const float hdhr_base_fade_out_duration = 0.2f;
private const float hdhr_base_initial_alpha = 0.2f;
private const float hidden_base_aspect = 4f / 3f;
/// <summary>
/// Time range at which notes start to become visible in milliseconds.
/// </summary>
private const double nm_fade_in_time_range = 10000d / 1.4d;
/// <summary>
/// Duration notes take to fade in in milliseconds.
/// </summary>
private const float nm_fade_in_duration = 400f;
/// <summary>
/// Whether note fading in is enabled.
/// </summary>
private bool fadeInEnabled = true;
private readonly BindableFloat hiddenFadeOutDuration = new BindableFloat(hd_base_fade_out_duration);
private readonly BindableFloat hiddenInitialAlpha = new BindableFloat(hd_base_initial_alpha);
public void ApplyToDrawableRuleset(DrawableRuleset<TaikoHitObject> drawableRuleset)
{
var drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
drawableTaikoRuleset.LockPlayfieldAspectRange.Value = false;
drawableTaikoRuleset = (DrawableTaikoRuleset)drawableRuleset;
adjustmentContainer = (TaikoPlayfieldAdjustmentContainer)drawableRuleset.PlayfieldAdjustmentContainer;
// drawableRuleset.Mods should always be non-null here, but just in case.
mods = drawableRuleset.Mods ?? mods;
// Disable uppper bound for playfield aspect ratio
// In stable, nomod time range is limited by fading notes in instead
adjustmentContainer.MaximumAspect = float.PositiveInfinity;
adjustmentContainer.MinimumAspect = 5f / 4f;
TaikoModHidden? hidden = mods.OfType<TaikoModHidden>().FirstOrDefault();
if (mods.OfType<TaikoModHardRock>().Any())
{
// Hardrock disables note fading in
fadeInEnabled = false;
// For hardrock, the playfield time range is clamped to within classicMaxTimeRange and the equivalent
// time range for a 16:10 aspect ratio.
adjustmentContainer.TrimOnOverflow = false;
// Apply stable aspect ratio limits for hardrock (visually taken)
adjustmentContainer.MaximumAspect = 1.963f;
// This is accurate to 4:3, but slightly off for 5:4
adjustmentContainer.MinimumAspect = 1.666f;
adjustmentContainer.MinimumRelativeHeight = 0.26f;
adjustmentContainer.MaximumRelativeHeight = 0.26f;
if (hidden != null)
{
hiddenInitialAlpha.BindTo(hidden.InitialAlpha);
hiddenFadeOutDuration.BindTo(hidden.FadeOutDuration);
drawableRuleset.OnUpdate += d => adjustHidden(
hdhr_base_fade_out_duration, hdhr_base_initial_alpha, 16f / 9f, 0.8f);
}
}
else if (hidden != null)
{
// Hidden disables note fading in
fadeInEnabled = false;
// Stable limits the aspect ratio to 4:3
adjustmentContainer.MaximumAspect = hidden_base_aspect;
// Enable playfield trimming for hidden
adjustmentContainer.TrimOnOverflow = true;
// Enable aspect ratio adjustment for hidden (see adjustHidden)
hiddenInitialAlpha.BindTo(hidden.InitialAlpha);
hiddenFadeOutDuration.BindTo(hidden.FadeOutDuration);
drawableRuleset.OnUpdate += d => adjustHidden(
hd_base_fade_out_duration, hd_base_initial_alpha, hidden_base_aspect);
}
}
public void ApplyToDrawableHitObject(DrawableHitObject drawable)
{
if (drawable is DrawableTaikoHitObject hit)
{
hit.SnapJudgementLocation = true;
if (fadeInEnabled)
{
drawable.ApplyCustomUpdateState += fadeIn;
}
}
}
// Fade in notes with fixed duration.
private void fadeIn(DrawableHitObject o, ArmedState state)
{
double preempt = nm_fade_in_time_range / drawableTaikoRuleset.ControlPointAt(o.HitObject.StartTime).Multiplier;
double start = o.HitObject.StartTime - preempt;
o.Alpha = 0;
using (o.BeginAbsoluteSequence(start))
{
o.FadeInFromZero(nm_fade_in_duration);
}
}
// Adjust hidden initial alpha and fade out duration for different aspect ratios
private void adjustHidden(
float baseFadeOutDuration,
float baseInitialAlpha,
float baseAspect,
float adjustmentRatio = 1f)
{
float clampedAspect = adjustmentContainer.ClampedCurrentAspect;
float fadeOutDurationAdjustment = clampedAspect / baseAspect - 1;
fadeOutDurationAdjustment *= adjustmentRatio;
hiddenFadeOutDuration.Value = baseFadeOutDuration + fadeOutDurationAdjustment;
float initialAlphaAdjustment = clampedAspect / baseAspect - 1;
initialAlphaAdjustment *= adjustmentRatio;
hiddenInitialAlpha.Value = baseInitialAlpha + initialAlphaAdjustment;
}
}
}
+17 -2
View File
@@ -1,6 +1,7 @@
// 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.
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Localisation;
using osu.Game.Rulesets.Mods;
@@ -29,7 +30,20 @@ namespace osu.Game.Rulesets.Taiko.Mods
/// How long hitobjects take to fade out, in terms of the scrolling length.
/// Range: [0, 1]
/// </summary>
private const float fade_out_duration = 0.375f;
public readonly BindableFloat FadeOutDuration = new BindableFloat(0.375f)
{
MinValue = 0f,
MaxValue = 1f
};
/// <summary>
/// The initial alpha of hitobjects when they appear.
/// </summary>
public readonly BindableFloat InitialAlpha = new BindableFloat(1f)
{
MinValue = 0f,
MaxValue = 1f
};
private DrawableTaikoRuleset drawableRuleset = null!;
@@ -51,7 +65,8 @@ namespace osu.Game.Rulesets.Taiko.Mods
case DrawableHit:
double preempt = drawableRuleset.TimeRange.Value / drawableRuleset.ControlPointAt(hitObject.HitObject.StartTime).Multiplier;
double start = hitObject.HitObject.StartTime - preempt * fade_out_start_time;
double duration = preempt * fade_out_duration;
double duration = preempt * FadeOutDuration.Value;
hitObject.Alpha = InitialAlpha.Value;
using (hitObject.BeginAbsoluteSequence(start))
{
@@ -35,8 +35,6 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public new BindableDouble TimeRange => base.TimeRange;
public readonly BindableBool LockPlayfieldAspectRange = new BindableBool(true);
public new TaikoInputManager KeyBindingInputManager => (TaikoInputManager)base.KeyBindingInputManager;
protected new TaikoPlayfieldAdjustmentContainer PlayfieldAdjustmentContainer => (TaikoPlayfieldAdjustmentContainer)base.PlayfieldAdjustmentContainer;
@@ -110,10 +108,7 @@ namespace osu.Game.Rulesets.Taiko.UI
return ControlPoints[result];
}
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer
{
LockPlayfieldAspectRange = { BindTarget = LockPlayfieldAspectRange }
};
public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new TaikoPlayfieldAdjustmentContainer();
protected override PassThroughInputManager CreateInputManager() => new TaikoInputManager(Ruleset.RulesetInfo);
@@ -120,6 +120,7 @@ namespace osu.Game.Rulesets.Taiko.UI
new Container
{
Name = "Bar line content",
Masking = true,
RelativeSizeAxes = Axes.Both,
Padding = new MarginPadding { Left = hit_target_width / 2 + hit_target_offset },
Children = new Drawable[]
@@ -2,7 +2,6 @@
// See the LICENCE file in the repository root for full licence text.
using System;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Game.Rulesets.Taiko.Beatmaps;
using osu.Game.Rulesets.UI;
@@ -12,12 +11,38 @@ namespace osu.Game.Rulesets.Taiko.UI
{
public partial class TaikoPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer
{
public const float MAXIMUM_ASPECT = 16f / 9f;
public const float MINIMUM_ASPECT = 5f / 4f;
private const float stable_gamefield_height = 480f;
public readonly IBindable<bool> LockPlayfieldAspectRange = new BindableBool(true);
/// <summary>
/// The maximum aspect ratio the playfield can be adjusted to.
/// </summary>
protected internal float MaximumAspect = 16f / 9f;
/// <summary>
/// The minimum aspect ratio the playfield can be adjusted to.
/// </summary>
protected internal float MinimumAspect = 5f / 4f;
/// <summary>
/// Aspect ratio of the playfield's resolution clamped between <see cref="MinimumAspect"/> and
/// <see cref="MaximumAspect"/>.
/// </summary>
protected internal float ClampedCurrentAspect { get; private set; } = 16f / 9f;
/// <summary>
/// The maximum relative height of the playfield. This is a fraction of the available area.
/// </summary>
protected internal float MaximumRelativeHeight = 1f / 3f;
/// <summary>
/// The minimum relative height of the playfield. This is a fraction of the available area.
/// </summary>
protected internal float MinimumRelativeHeight = 0f;
/// <summary>
/// Whether the playfield should be trimmed when the aspect ratio exceeds the maximum.
/// </summary>
protected internal bool TrimOnOverflow = false;
public TaikoPlayfieldAdjustmentContainer()
{
@@ -37,41 +62,50 @@ namespace osu.Game.Rulesets.Taiko.UI
float relativeHeight = base_relative_height;
float widthScale = 1.0f;
// Players coming from stable expect to be able to change the aspect ratio regardless of the window size.
// We originally wanted to limit this more, but there was considerable pushback from the community.
//
// As a middle-ground, the aspect ratio can still be adjusted in the downwards direction but has a maximum limit.
// This is still a bit weird, because readability changes with window size, but it is what it is.
if (LockPlayfieldAspectRange.Value)
{
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;
//
// This is separate from CurrentAspect as this needs to be the unbounded aspect ratio.
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;
if (currentAspect > MAXIMUM_ASPECT)
relativeHeight *= currentAspect / MAXIMUM_ASPECT;
else if (currentAspect < MINIMUM_ASPECT)
relativeHeight *= currentAspect / MINIMUM_ASPECT;
if (currentAspect > MaximumAspect)
{
if (TrimOnOverflow)
{
widthScale = MaximumAspect / currentAspect;
}
else
{
relativeHeight *= currentAspect / MaximumAspect;
}
}
else if (currentAspect < MinimumAspect)
{
relativeHeight *= currentAspect / MinimumAspect;
}
// Limit the maximum relative height of the playfield to one-third of available area to avoid it masking out on extreme resolutions.
relativeHeight = Math.Min(relativeHeight, 1f / 3f);
relativeHeight = Math.Clamp(relativeHeight, MinimumRelativeHeight, MaximumRelativeHeight);
Scale = new Vector2(Math.Max((Parent!.ChildSize.Y / 768f) * (relativeHeight / base_relative_height), 1f));
Width = 1 / Scale.X;
Width = 1 / Scale.X * widthScale;
}
public double ComputeTimeRange()
{
float currentAspect = Parent!.ChildSize.X / Parent!.ChildSize.Y;
if (LockPlayfieldAspectRange.Value)
currentAspect = Math.Clamp(currentAspect, MINIMUM_ASPECT, MAXIMUM_ASPECT);
ClampedCurrentAspect = Math.Clamp(Parent!.ChildSize.X / Parent!.ChildSize.Y, MinimumAspect, MaximumAspect);
// in a game resolution of 1024x768, stable's scrolling system consists of objects being placed 600px (widthScaled - 40) away from their hit location.
// however, the point at which the object renders at the end of the screen is exactly x=640, but stable makes the object start moving from beyond the screen instead of the boundary point.
// therefore, in lazer we have to adjust the "in length" so that it's in a 640px->160px fashion before passing it down as a "time range".
// see stable's "in length": https://github.com/peppy/osu-stable-reference/blob/013c3010a9d495e3471a9c59518de17006f9ad89/osu!/GameplayElements/HitObjectManagerTaiko.cs#L168
const float stable_hit_location = 160f;
float widthScaled = currentAspect * stable_gamefield_height;
float widthScaled = ClampedCurrentAspect * stable_gamefield_height;
float inLength = widthScaled - stable_hit_location;
// also in a game resolution of 1024x768, stable makes hit objects scroll from 760px->160px at a duration of 6000ms, divided by slider velocity (i.e. at a rate of 0.1px/ms)