1
0
mirror of https://github.com/ppy/osu.git synced 2025-02-15 12:42:56 +08:00

Merge pull request #203 from peppy/slider-loading

Add interactivity to sliders, follow circle, (non-buffered) paths etc.
This commit is contained in:
Dean Herbert 2016-11-29 13:06:00 +09:00 committed by GitHub
commit bb37e5d955
11 changed files with 407 additions and 74 deletions

@ -1 +1 @@
Subproject commit fdea70aee37b040d56fac5e9b27a18ed77f2bfb9
Subproject commit e125c03d8c39fd86e02e872a8d46654d2ea2759f

View File

@ -40,7 +40,8 @@ namespace osu.Desktop.VisualTests.Tests
protected override void Dispose(bool isDisposing)
{
Dependencies.Cache(oldDb, true);
if (oldDb != null)
Dependencies.Cache(oldDb, true);
base.Dispose(isDisposing);
}

View File

@ -2,6 +2,7 @@
//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Framework.Allocation;
using osu.Framework.GameModes.Testing;
using osu.Framework.MathUtils;
using osu.Framework.Timing;
@ -9,6 +10,8 @@ using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Formats;
using OpenTK;
using osu.Framework.Graphics.Sprites;
using osu.Game.Database;
using osu.Game.Modes;
using osu.Game.Modes.Objects;
using osu.Game.Modes.Osu.Objects;
using osu.Game.Screens.Play;
@ -18,10 +21,17 @@ namespace osu.Desktop.VisualTests.Tests
{
class TestCasePlayer : TestCase
{
private WorkingBeatmap beatmap;
public override string Name => @"Player";
public override string Description => @"Showing everything to play the game.";
[BackgroundDependencyLoader]
private void load(BeatmapDatabase db)
{
beatmap = db.GetWorkingBeatmap(db.Query<BeatmapInfo>().Where(b => b.Mode == PlayMode.Osu).FirstOrDefault());
}
public override void Reset()
{
base.Reset();
@ -29,31 +39,37 @@ namespace osu.Desktop.VisualTests.Tests
//ensure we are at offset 0
Clock = new FramedClock();
var objects = new List<HitObject>();
int time = 1500;
for (int i = 0; i < 50; i++)
if (beatmap == null)
{
objects.Add(new HitCircle()
var objects = new List<HitObject>();
int time = 1500;
for (int i = 0; i < 50; i++)
{
StartTime = time,
Position = new Vector2(i % 4 == 0 || i % 4 == 2 ? 0 : 512,
i % 4 < 2 ? 0 : 384),
NewCombo = i % 4 == 0
});
objects.Add(new HitCircle()
{
StartTime = time,
Position = new Vector2(i % 4 == 0 || i % 4 == 2 ? 0 : 512,
i % 4 < 2 ? 0 : 384),
NewCombo = i % 4 == 0
});
time += 500;
time += 500;
}
var decoder = new ConstructableBeatmapDecoder();
Beatmap b = new Beatmap
{
HitObjects = objects
};
decoder.Process(b);
beatmap = new WorkingBeatmap(b);
}
var decoder = new ConstructableBeatmapDecoder();
Beatmap b = new Beatmap
{
HitObjects = objects
};
decoder.Process(b);
Add(new Box
{
RelativeSizeAxes = Framework.Graphics.Axes.Both,
@ -62,7 +78,8 @@ namespace osu.Desktop.VisualTests.Tests
Add(new Player
{
Beatmap = new WorkingBeatmap(b)
PreferredPlayMode = PlayMode.Osu,
Beatmap = beatmap
});
}

View File

@ -59,15 +59,15 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
Colour = osuObject.Colour,
}
};
//may not be so correct
Size = circle.DrawSize;
}
protected override void LoadComplete()
{
base.LoadComplete();
//may not be so correct
Size = circle.DrawSize;
//force application of the state that was set before we loaded.
UpdateState(State);
}
@ -104,16 +104,9 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
Judgement.Result = HitResult.Miss;
}
protected override void UpdateState(ArmedState state)
protected override void UpdateInitialState()
{
if (!IsLoaded) return;
Flush(true); //move to DrawableHitObject
ApproachCircle.Flush(true);
double t = osuObject.EndTime + Judgement.TimeOffset;
Alpha = 0;
base.UpdateInitialState();
//sane defaults
ring.Alpha = circle.Alpha = number.Alpha = glow.Alpha = 1;
@ -121,29 +114,30 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
ApproachCircle.Scale = new Vector2(2);
explode.Alpha = 0;
Scale = new Vector2(0.5f); //this will probably need to be moved to DrawableHitObject at some point.
}
const float preempt = 600;
protected override void UpdatePreemptState()
{
base.UpdatePreemptState();
const float fadein = 400;
ApproachCircle.FadeIn(Math.Min(TIME_FADEIN * 2, TIME_PREEMPT));
ApproachCircle.ScaleTo(0.6f, TIME_PREEMPT);
}
Delay(t - Time.Current - preempt, true);
protected override void UpdateState(ArmedState state)
{
if (!IsLoaded) return;
FadeIn(fadein);
ApproachCircle.FadeIn(Math.Min(fadein * 2, preempt));
ApproachCircle.ScaleTo(0.6f, preempt);
Delay(preempt, true);
base.UpdateState(state);
ApproachCircle.FadeOut();
glow.FadeOut(400);
switch (state)
{
case ArmedState.Idle:
Delay(osuObject.Duration + 500);
FadeOut(500);
Delay(osuObject.Duration + TIME_PREEMPT);
FadeOut(TIME_FADEOUT);
explosion?.Expire();
explosion = null;

View File

@ -11,6 +11,10 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
{
public class DrawableOsuHitObject : DrawableHitObject
{
protected const float TIME_PREEMPT = 600;
protected const float TIME_FADEIN = 400;
protected const float TIME_FADEOUT = 500;
public DrawableOsuHitObject(OsuHitObject hitObject)
: base(hitObject)
{
@ -20,7 +24,27 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
protected override void UpdateState(ArmedState state)
{
throw new NotImplementedException();
if (!IsLoaded) return;
Flush(true);
UpdateInitialState();
Delay(HitObject.StartTime - Time.Current - TIME_PREEMPT + Judgement.TimeOffset, true);
UpdatePreemptState();
Delay(TIME_PREEMPT, true);
}
protected virtual void UpdatePreemptState()
{
FadeIn(TIME_FADEIN);
}
protected virtual void UpdateInitialState()
{
Alpha = 0;
}
}

View File

@ -1,26 +1,52 @@
using osu.Framework.Graphics;
using System.Collections.Generic;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Game.Modes.Objects.Drawables;
using osu.Game.Modes.Osu.Objects.Drawables.Pieces;
using OpenTK;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Graphics.Transformations;
using OpenTK.Graphics;
using osu.Framework.Input;
using OpenTK.Graphics.ES30;
using osu.Framework.Allocation;
using osu.Framework.Graphics.Textures;
namespace osu.Game.Modes.Osu.Objects.Drawables
{
class DrawableSlider : DrawableOsuHitObject
{
public DrawableSlider(Slider h) : base(h)
private Slider slider;
private DrawableHitCircle startCircle;
private Container ball;
private Body body;
public DrawableSlider(Slider s) : base(s)
{
Origin = Anchor.Centre;
Position = new Vector2(h.Position.X, h.Position.Y);
slider = s;
Path sliderPath;
Add(sliderPath = new Path());
Origin = Anchor.TopLeft;
Position = Vector2.Zero;
RelativeSizeAxes = Axes.Both;
for (int i = 0; i < h.Curve.Path.Count; ++i)
sliderPath.Positions.Add(h.Curve.Path[i] - h.Position);
h.Position = Vector2.Zero;
Add(new DrawableHitCircle(h));
Children = new Drawable[]
{
body = new Body(s)
{
Position = s.Position,
},
ball = new Ball(),
startCircle = new DrawableHitCircle(new HitCircle
{
StartTime = s.StartTime,
Position = s.Position,
Colour = s.Colour,
})
{
Depth = 1 //override time-based depth.
},
};
}
protected override void LoadComplete()
@ -31,19 +57,222 @@ namespace osu.Game.Modes.Osu.Objects.Drawables
UpdateState(State);
}
protected override void Update()
{
base.Update();
ball.Alpha = Time.Current >= slider.StartTime && Time.Current <= slider.EndTime ? 1 : 0;
double t = (Time.Current - slider.StartTime) / slider.Duration;
if (slider.RepeatCount > 1)
{
int currentRepeat = (int)(t * slider.RepeatCount);
t = (t * slider.RepeatCount) % 1;
if (currentRepeat % 2 == 1)
t = 1 - t;
}
ball.Position = slider.Curve.PositionAt(t);
}
protected override void UpdateState(ArmedState state)
{
if (!IsLoaded) return;
base.UpdateState(state);
Flush(true); //move to DrawableHitObject
Delay(HitObject.Duration);
FadeOut(100);
}
Alpha = 0;
private class Ball : Container
{
private Box follow;
Delay(HitObject.StartTime - 450 - Time.Current, true);
public Ball()
{
Masking = true;
AutoSizeAxes = Axes.Both;
BlendingMode = BlendingMode.Additive;
Origin = Anchor.Centre;
FadeIn(200);
Delay(450 + HitObject.Duration);
FadeOut(200);
Children = new Drawable[]
{
follow = new Box
{
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Colour = Color4.Orange,
Width = 64,
Height = 64,
},
new Container
{
Masking = true,
AutoSizeAxes = Axes.Both,
Origin = Anchor.Centre,
Anchor = Anchor.Centre,
Colour = Color4.Cyan,
CornerRadius = 32,
Children = new[]
{
new Box
{
Width = 64,
Height = 64,
},
}
}
};
}
private InputState lastState;
protected override bool OnMouseDown(InputState state, MouseDownEventArgs args)
{
lastState = state;
return base.OnMouseDown(state, args);
}
protected override bool OnMouseUp(InputState state, MouseUpEventArgs args)
{
lastState = state;
return base.OnMouseUp(state, args);
}
protected override bool OnMouseMove(InputState state)
{
lastState = state;
return base.OnMouseMove(state);
}
bool tracking;
protected bool Tracking
{
get { return tracking; }
set
{
if (value == tracking) return;
tracking = value;
follow.ScaleTo(tracking ? 2.4f : 1, 140, EasingTypes.Out);
follow.FadeTo(tracking ? 0.8f : 0, 140, EasingTypes.Out);
}
}
protected override void Update()
{
base.Update();
CornerRadius = DrawWidth / 2;
Tracking = lastState != null && Contains(lastState.Mouse.NativeState.Position) && lastState.Mouse.HasMainButtonPressed;
}
}
private class Body : Container
{
private Path path;
private BufferedContainer container;
private double? drawnProgress;
private Slider slider;
public Body(Slider s)
{
slider = s;
Children = new Drawable[]
{
container = new BufferedContainer
{
CacheDrawnFrameBuffer = true,
Children = new Drawable[]
{
path = new Path
{
Colour = s.Colour,
BlendingMode = BlendingMode.None,
},
}
}
};
container.Attach(RenderbufferInternalFormat.DepthComponent16);
}
[BackgroundDependencyLoader]
private void load(TextureStore textures)
{
// Surprisingly, this looks somewhat okay and works well as a test for self-overlaps.
// TODO: Don't do this.
path.Texture = textures.Get(@"Menu/logo");
}
protected override void LoadComplete()
{
base.LoadComplete();
path.PathWidth = 32;
}
protected override void Update()
{
base.Update();
if (updateSnaking())
{
// Autosizing does not give us the desired behaviour here.
// We want the container to have the same size as the slider,
// and to be positioned such that the slider head is at (0,0).
container.Size = path.Size;
container.Position = -path.HeadPosition;
container.ForceRedraw();
}
}
private bool updateSnaking()
{
double progress = MathHelper.Clamp((Time.Current - slider.StartTime + TIME_PREEMPT) / TIME_FADEIN, 0, 1);
if (progress == drawnProgress) return false;
bool madeChanges = false;
if (progress == 0)
{
//if we have gone backwards, just clear the path for now.
drawnProgress = 0;
path.ClearVertices();
madeChanges = true;
}
Vector2 startPosition = slider.Curve.PositionAt(0);
if (drawnProgress == null)
{
drawnProgress = 0;
path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition);
madeChanges = true;
}
double segmentSize = 1 / (slider.Curve.Length / 5);
while (drawnProgress + segmentSize < progress)
{
drawnProgress += segmentSize;
path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition);
madeChanges = true;
}
if (progress == 1 && drawnProgress != progress)
{
drawnProgress = progress;
path.AddVertex(slider.Curve.PositionAt(drawnProgress.Value) - startPosition);
madeChanges = true;
}
return madeChanges;
}
}
}
}

View File

@ -2,18 +2,27 @@
//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using System.Collections.Generic;
using osu.Game.Database;
using OpenTK;
using osu.Game.Beatmaps;
using System;
namespace osu.Game.Modes.Osu.Objects
{
public class Slider : OsuHitObject
{
public override double EndTime => StartTime + (RepeatCount + 1) * Curve.Length;
public override double EndTime => StartTime + RepeatCount * Curve.Length / Velocity;
public double Velocity;
public override void SetDefaultsFromBeatmap(Beatmap beatmap)
{
Velocity = 100 / beatmap.BeatLengthAt(StartTime, true) * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier;
}
public int RepeatCount;
public SliderCurve Curve;
}
public class SliderCurve
@ -42,11 +51,14 @@ namespace osu.Game.Modes.Osu.Objects
public Vector2 PositionAt(double progress)
{
int index = (int)(progress * (calculatedPath.Count - 1));
progress = MathHelper.Clamp(progress, 0, 1);
Vector2 pos = calculatedPath[index];
if (index != progress)
pos += (calculatedPath[index + 1] - pos) * (float)(progress - index);
double index = progress * (calculatedPath.Count - 1);
int flooredIndex = (int)index;
Vector2 pos = calculatedPath[flooredIndex];
if (index != flooredIndex)
pos += (calculatedPath[flooredIndex + 1] - pos) * (float)(index - flooredIndex);
return pos;
}
@ -201,5 +213,5 @@ namespace osu.Game.Modes.Osu.Objects
Bezier,
Linear,
PerfectCurve
};
}
}

View File

@ -16,5 +16,27 @@ namespace osu.Game.Beatmaps
public List<HitObject> HitObjects { get; set; }
public List<ControlPoint> ControlPoints { get; set; }
public List<Color4> ComboColors { get; set; }
public double BeatLengthAt(double time, bool applyMultipliers = false)
{
int point = 0;
int samplePoint = 0;
for (int i = 0; i < ControlPoints.Count; i++)
if (ControlPoints[i].Time <= time)
{
if (ControlPoints[i].TimingChange)
point = i;
else
samplePoint = i;
}
double mult = 1;
if (applyMultipliers && samplePoint > point && ControlPoints[samplePoint].BeatLength < 0)
mult = ControlPoints[samplePoint].VelocityAdjustment;
return ControlPoints[point].BeatLength * mult;
}
}
}

View File

@ -187,7 +187,25 @@ namespace osu.Game.Beatmaps.Formats
private void handleTimingPoints(Beatmap beatmap, string val)
{
// TODO
ControlPoint cp = null;
string[] split = val.Split(',');
if (split.Length > 2)
{
int kiai_flags = split.Length > 7 ? Convert.ToInt32(split[7], NumberFormatInfo.InvariantInfo) : 0;
double beatLength = double.Parse(split[1].Trim(), NumberFormatInfo.InvariantInfo);
cp = new ControlPoint
{
Time = double.Parse(split[0].Trim(), NumberFormatInfo.InvariantInfo),
BeatLength = beatLength > 0 ? beatLength : 0,
VelocityAdjustment = beatLength < 0 ? -beatLength / 100.0 : 1,
TimingChange = split.Length <= 6 || split[6][0] == '1',
};
}
if (cp != null)
beatmap.ControlPoints.Add(cp);
}
private void handleColours(Beatmap beatmap, string key, string val)
@ -275,8 +293,12 @@ namespace osu.Game.Beatmaps.Formats
break;
case Section.HitObjects:
var obj = parser?.Parse(val);
if (obj != null)
{
obj.SetDefaultsFromBeatmap(beatmap);
beatmap.HitObjects.Add(obj);
}
break;
}
}

View File

@ -12,5 +12,14 @@ namespace osu.Game.Beatmaps.Timing
public class ControlPoint
{
public double Time;
public double BeatLength;
public double VelocityAdjustment;
public bool TimingChange;
}
internal enum TimeSignatures
{
SimpleQuadruple = 4,
SimpleTriple = 3
}
}

View File

@ -1,6 +1,7 @@
//Copyright (c) 2007-2016 ppy Pty Ltd <contact@ppy.sh>.
//Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE
using osu.Game.Beatmaps;
using osu.Game.Beatmaps.Samples;
using OpenTK.Graphics;
@ -21,5 +22,7 @@ namespace osu.Game.Modes.Objects
public double Duration => EndTime - StartTime;
public HitSampleInfo Sample;
public virtual void SetDefaultsFromBeatmap(Beatmap beatmap) { }
}
}