// 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; using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osuTK; namespace osu.Game.Graphics { /// <summary> /// An explosion of textured particles based on how osu-stable randomises the explosion pattern. /// </summary> public class ParticleExplosion : Sprite { private readonly int particleCount; private readonly double duration; private double startTime; private readonly List<ParticlePart> parts = new List<ParticlePart>(); public ParticleExplosion(Texture texture, int particleCount, double duration) { Texture = texture; this.particleCount = particleCount; this.duration = duration; Blending = BlendingParameters.Additive; } protected override void LoadComplete() { base.LoadComplete(); Restart(); } /// <summary> /// Restart the animation from the current point in time. /// Supports transform time offset chaining. /// </summary> public void Restart() { startTime = TransformStartTime; this.FadeOutFromOne(duration); parts.Clear(); for (int i = 0; i < particleCount; i++) parts.Add(new ParticlePart(duration)); } protected override void Update() { base.Update(); Invalidate(Invalidation.DrawNode); } protected override DrawNode CreateDrawNode() => new ParticleExplosionDrawNode(this); private class ParticleExplosionDrawNode : SpriteDrawNode { private readonly List<ParticlePart> parts = new List<ParticlePart>(); private ParticleExplosion source => (ParticleExplosion)Source; private double startTime; private double currentTime; private Vector2 sourceSize; public ParticleExplosionDrawNode(Sprite source) : base(source) { } public override void ApplyState() { base.ApplyState(); parts.Clear(); parts.AddRange(source.parts); sourceSize = source.Size; startTime = source.startTime; currentTime = source.Time.Current; } protected override void Blit(Action<TexturedVertex2D> vertexAction) { double time = currentTime - startTime; foreach (var p in parts) { Vector2 pos = p.PositionAtTime(time); float alpha = p.AlphaAtTime(time); var rect = new RectangleF( pos.X * sourceSize.X - Texture.DisplayWidth / 2, pos.Y * sourceSize.Y - Texture.DisplayHeight / 2, Texture.DisplayWidth, Texture.DisplayHeight); // convert to screen space. var quad = new Quad( Vector2Extensions.Transform(rect.TopLeft, DrawInfo.Matrix), Vector2Extensions.Transform(rect.TopRight, DrawInfo.Matrix), Vector2Extensions.Transform(rect.BottomLeft, DrawInfo.Matrix), Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix) ); DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction, new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), null, TextureCoords); } } protected override bool CanDrawOpaqueInterior => false; } private readonly struct ParticlePart { private readonly double duration; private readonly float direction; private readonly float distance; public ParticlePart(double availableDuration) { distance = RNG.NextSingle(0.5f); duration = RNG.NextDouble(availableDuration / 3, availableDuration); direction = RNG.NextSingle(0, MathF.PI * 2); } public float AlphaAtTime(double time) => 1 - progressAtTime(time); public Vector2 PositionAtTime(double time) { float travelledDistance = distance * progressAtTime(time); return new Vector2(0.5f) + travelledDistance * new Vector2(MathF.Sin(direction), MathF.Cos(direction)); } private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1); } } }