1
0
mirror of https://github.com/ppy/osu.git synced 2024-11-14 15:57:24 +08:00
osu-lazer/osu.Game/Storyboards/StoryboardSprite.cs

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

287 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
2017-09-09 00:00:17 +08:00
using System;
2017-09-08 05:55:05 +08:00
using System.Collections.Generic;
using System.Linq;
using osu.Framework.Graphics;
using osu.Game.Storyboards.Drawables;
using osuTK;
2018-04-13 17:19:50 +08:00
2017-09-08 05:55:05 +08:00
namespace osu.Game.Storyboards
{
public class StoryboardSprite : IStoryboardElementWithDuration
2017-09-08 05:55:05 +08:00
{
private readonly List<CommandLoop> loops = new List<CommandLoop>();
private readonly List<CommandTrigger> triggers = new List<CommandTrigger>();
2018-04-13 17:19:50 +08:00
2020-01-20 22:59:21 +08:00
public string Path { get; }
public bool IsDrawable => HasCommands;
2018-04-13 17:19:50 +08:00
2017-09-08 05:55:05 +08:00
public Anchor Origin;
public Vector2 InitialPosition;
2018-04-13 17:19:50 +08:00
public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup();
2018-04-13 17:19:50 +08:00
2022-09-12 13:20:48 +08:00
public double StartTime
{
get
{
2022-09-06 15:58:51 +08:00
// To get the initial start time, we need to check whether the first alpha command to exist (across all loops) has a StartValue of zero.
// A StartValue of zero governs, above all else, the first valid display time of a sprite.
//
// You can imagine that the first command of each type decides that type's start value, so if the initial alpha is zero,
// anything before that point can be ignored (the sprite is not visible after all).
var alphaCommands = new List<(double startTime, bool isZeroStartValue)>();
2022-09-06 15:58:51 +08:00
var command = TimelineGroup.Alpha.Commands.FirstOrDefault();
if (command != null) alphaCommands.Add((command.StartTime, command.StartValue == 0));
2022-09-06 15:58:51 +08:00
foreach (var loop in loops)
{
2022-09-06 15:58:51 +08:00
command = loop.Alpha.Commands.FirstOrDefault();
2022-09-06 16:46:03 +08:00
if (command != null) alphaCommands.Add((command.StartTime + loop.LoopStartTime, command.StartValue == 0));
}
2022-09-06 15:58:51 +08:00
if (alphaCommands.Count > 0)
{
var firstAlpha = alphaCommands.MinBy(t => t.startTime);
2022-09-06 15:58:51 +08:00
if (firstAlpha.isZeroStartValue)
return firstAlpha.startTime;
}
return EarliestTransformTime;
}
}
public double EarliestTransformTime
{
get
{
2022-09-06 15:58:51 +08:00
// If we got to this point, either no alpha commands were present, or the earliest had a non-zero start value.
// The sprite's StartTime will be determined by the earliest command, regardless of type.
double earliestStartTime = TimelineGroup.StartTime;
foreach (var l in loops)
earliestStartTime = Math.Min(earliestStartTime, l.StartTime);
return earliestStartTime;
}
}
public double EndTime
{
get
{
double latestEndTime = TimelineGroup.EndTime;
foreach (var l in loops)
latestEndTime = Math.Max(latestEndTime, l.EndTime);
return latestEndTime;
}
}
2018-04-13 17:19:50 +08:00
public double EndTimeForDisplay
{
get
{
2024-03-01 04:23:57 +08:00
double latestEndTime = double.MaxValue;
// Ignore the whole setup if there are loops. In theory they can be handled here too, however the logic will be overly complex.
if (loops.Count == 0)
{
// Take the minimum time of all the potential "death" reasons.
2024-03-06 19:23:29 +08:00
latestEndTime = calculateOptimisedEndTime(TimelineGroup);
2024-03-01 04:23:57 +08:00
}
// If the logic above fails to find anything or discarded by the fact that there are loops present, latestEndTime will be double.MaxValue
// and thus conservativeEndTime will be used.
double conservativeEndTime = TimelineGroup.EndTime;
foreach (var l in loops)
2024-03-01 04:23:57 +08:00
conservativeEndTime = Math.Max(conservativeEndTime, l.StartTime + l.CommandsDuration * l.TotalIterations);
2024-03-01 04:23:57 +08:00
return Math.Min(latestEndTime, conservativeEndTime);
}
}
public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands);
2018-04-13 17:19:50 +08:00
2017-09-09 03:23:24 +08:00
private delegate void DrawablePropertyInitializer<in T>(Drawable drawable, T value);
2019-02-28 12:31:40 +08:00
2017-09-09 03:23:24 +08:00
private delegate void DrawableTransformer<in T>(Drawable drawable, T value, double duration, Easing easing);
2018-04-13 17:19:50 +08:00
2017-09-13 17:22:24 +08:00
public StoryboardSprite(string path, Anchor origin, Vector2 initialPosition)
2017-09-08 05:55:05 +08:00
{
Path = path;
Origin = origin;
InitialPosition = initialPosition;
}
2018-04-13 17:19:50 +08:00
public CommandLoop AddLoop(double startTime, int repeatCount)
2017-09-08 05:55:05 +08:00
{
var loop = new CommandLoop(startTime, repeatCount);
2017-09-08 05:55:05 +08:00
loops.Add(loop);
return loop;
}
2018-04-13 17:19:50 +08:00
2017-09-08 05:55:05 +08:00
public CommandTrigger AddTrigger(string triggerName, double startTime, double endTime, int groupNumber)
{
var trigger = new CommandTrigger(triggerName, startTime, endTime, groupNumber);
triggers.Add(trigger);
return trigger;
}
2018-04-13 17:19:50 +08:00
2017-09-08 05:55:05 +08:00
public virtual Drawable CreateDrawable()
2017-09-13 17:22:24 +08:00
=> new DrawableStoryboardSprite(this);
2018-04-13 17:19:50 +08:00
2023-04-25 13:28:56 +08:00
public void ApplyTransforms(Drawable drawable, IEnumerable<Tuple<CommandTimelineGroup, double>>? triggeredGroups = null)
2017-09-09 00:00:17 +08:00
{
// For performance reasons, we need to apply the commands in order by start time. Not doing so will cause many functions to be interleaved, resulting in O(n^2) complexity.
// To achieve this, commands are "generated" as pairs of (command, initFunc, transformFunc) and batched into a contiguous list
// The list is then stably-sorted (to preserve command order), and applied to the drawable sequentially.
List<IGeneratedCommand> generated = new List<IGeneratedCommand>();
generateCommands(generated, getCommands(g => g.X, triggeredGroups), (d, value) => d.X = value, (d, value, duration, easing) => d.MoveToX(value, duration, easing));
generateCommands(generated, getCommands(g => g.Y, triggeredGroups), (d, value) => d.Y = value, (d, value, duration, easing) => d.MoveToY(value, duration, easing));
generateCommands(generated, getCommands(g => g.Scale, triggeredGroups), (d, value) => d.Scale = new Vector2(value), (d, value, duration, easing) => d.ScaleTo(value, duration, easing));
generateCommands(generated, getCommands(g => g.Rotation, triggeredGroups), (d, value) => d.Rotation = value, (d, value, duration, easing) => d.RotateTo(value, duration, easing));
generateCommands(generated, getCommands(g => g.Colour, triggeredGroups), (d, value) => d.Colour = value, (d, value, duration, easing) => d.FadeColour(value, duration, easing));
generateCommands(generated, getCommands(g => g.Alpha, triggeredGroups), (d, value) => d.Alpha = value, (d, value, duration, easing) => d.FadeTo(value, duration, easing));
2022-06-24 20:25:23 +08:00
generateCommands(generated, getCommands(g => g.BlendingParameters, triggeredGroups), (d, value) => d.Blending = value, (d, value, duration, _) => d.TransformBlendingMode(value, duration),
false);
if (drawable is IVectorScalable vectorScalable)
{
2022-06-24 20:25:23 +08:00
generateCommands(generated, getCommands(g => g.VectorScale, triggeredGroups), (_, value) => vectorScalable.VectorScale = value,
(_, value, duration, easing) => vectorScalable.VectorScaleTo(value, duration, easing));
}
2018-04-13 17:19:50 +08:00
2019-02-28 13:35:00 +08:00
if (drawable is IFlippable flippable)
2017-09-09 00:00:17 +08:00
{
2022-06-24 20:25:23 +08:00
generateCommands(generated, getCommands(g => g.FlipH, triggeredGroups), (_, value) => flippable.FlipH = value, (_, value, duration, _) => flippable.TransformFlipH(value, duration),
false);
2022-06-24 20:25:23 +08:00
generateCommands(generated, getCommands(g => g.FlipV, triggeredGroups), (_, value) => flippable.FlipV = value, (_, value, duration, _) => flippable.TransformFlipV(value, duration),
false);
2017-09-09 00:00:17 +08:00
}
foreach (var command in generated.OrderBy(g => g.StartTime))
command.ApplyTo(drawable);
2017-09-09 00:00:17 +08:00
}
2018-04-13 17:19:50 +08:00
private void generateCommands<T>(List<IGeneratedCommand> resultList, IEnumerable<CommandTimeline<T>.TypedCommand> commands,
DrawablePropertyInitializer<T> initializeProperty, DrawableTransformer<T> transform, bool alwaysInitialize = true)
2017-09-09 00:00:17 +08:00
{
bool initialized = false;
2019-04-01 11:16:05 +08:00
foreach (var command in commands)
2017-09-09 00:00:17 +08:00
{
2023-04-25 13:28:56 +08:00
DrawablePropertyInitializer<T>? initFunc = null;
2017-09-09 00:00:17 +08:00
if (!initialized)
{
2017-09-09 21:34:26 +08:00
if (alwaysInitialize || command.StartTime == command.EndTime)
initFunc = initializeProperty;
2017-09-09 00:00:17 +08:00
initialized = true;
}
2019-02-28 12:31:40 +08:00
resultList.Add(new GeneratedCommand<T>(command, initFunc, transform));
2017-09-09 00:00:17 +08:00
}
}
2018-04-13 17:19:50 +08:00
2023-04-25 13:28:56 +08:00
private IEnumerable<CommandTimeline<T>.TypedCommand> getCommands<T>(CommandTimelineSelector<T> timelineSelector, IEnumerable<Tuple<CommandTimelineGroup, double>>? triggeredGroups)
2017-09-08 05:55:05 +08:00
{
var commands = TimelineGroup.GetCommands(timelineSelector);
foreach (var loop in loops)
commands = commands.Concat(loop.GetCommands(timelineSelector));
2019-11-11 20:05:36 +08:00
2017-09-09 00:00:17 +08:00
if (triggeredGroups != null)
2019-11-11 19:53:22 +08:00
{
2017-09-09 00:00:17 +08:00
foreach (var pair in triggeredGroups)
commands = commands.Concat(pair.Item1.GetCommands(timelineSelector, pair.Item2));
2019-11-11 19:53:22 +08:00
}
2017-09-09 17:00:58 +08:00
return commands;
2017-09-08 05:55:05 +08:00
}
2018-04-13 17:19:50 +08:00
2024-03-06 19:23:29 +08:00
private static double calculateOptimisedEndTime(CommandTimelineGroup timelineGroup)
{
// Here we are starting from maximum value and trying to minimise the end time on each step.
// There are few solid guesses we can make using which sprite's end time can be minimised: alpha = 0, scale = 0, colour.a = 0.
double[] deathTimes =
{
double.MaxValue, // alpha
double.MaxValue, // colour alpha
double.MaxValue, // scale
double.MaxValue, // scale x
double.MaxValue, // scale y
};
// The loops below are following the same pattern.
// We could be using TimelineGroup.EndValue here, however it's possible to have multiple commands with 0 value in a row
// so we are saving the earliest of them.
foreach (var alphaCommand in timelineGroup.Alpha.Commands)
{
if (alphaCommand.EndValue == 0)
// commands are ordered by the start time, however end time may vary. Save the earliest.
deathTimes[0] = Math.Min(alphaCommand.EndTime, deathTimes[0]);
else
// If value isn't 0 (sprite becomes visible again), revert the saved state.
deathTimes[0] = double.MaxValue;
}
foreach (var colourCommand in timelineGroup.Colour.Commands)
deathTimes[1] = colourCommand.EndValue.A == 0 ? Math.Min(colourCommand.EndTime, deathTimes[1]) : double.MaxValue;
foreach (var scaleCommand in timelineGroup.Scale.Commands)
deathTimes[2] = scaleCommand.EndValue == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[2]) : double.MaxValue;
foreach (var scaleCommand in timelineGroup.VectorScale.Commands)
{
deathTimes[3] = scaleCommand.EndValue.X == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[3]) : double.MaxValue;
deathTimes[4] = scaleCommand.EndValue.Y == 0 ? Math.Min(scaleCommand.EndTime, deathTimes[4]) : double.MaxValue;
}
return deathTimes.Min();
}
2017-09-08 05:55:05 +08:00
public override string ToString()
=> $"{Path}, {Origin}, {InitialPosition}";
private interface IGeneratedCommand
{
double StartTime { get; }
void ApplyTo(Drawable drawable);
}
private readonly struct GeneratedCommand<T> : IGeneratedCommand
{
public double StartTime => command.StartTime;
2023-04-25 13:28:56 +08:00
private readonly DrawablePropertyInitializer<T>? initializeProperty;
private readonly DrawableTransformer<T> transform;
private readonly CommandTimeline<T>.TypedCommand command;
2023-04-25 13:28:56 +08:00
public GeneratedCommand(CommandTimeline<T>.TypedCommand command, DrawablePropertyInitializer<T>? initializeProperty, DrawableTransformer<T> transform)
{
this.command = command;
this.initializeProperty = initializeProperty;
this.transform = transform;
}
public void ApplyTo(Drawable drawable)
{
initializeProperty?.Invoke(drawable, command.StartValue);
using (drawable.BeginAbsoluteSequence(command.StartTime))
{
transform(drawable, command.StartValue, 0, Easing.None);
transform(drawable, command.EndValue, command.Duration, command.Easing);
}
}
}
2017-09-08 05:55:05 +08:00
}
}