// Copyright (c) 2007-2017 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Rulesets.UI; using OpenTK; using OpenTK.Graphics; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Extensions.Color4Extensions; using System.Linq; using osu.Game.Rulesets.Taiko.Objects.Drawables; using System; namespace osu.Game.Rulesets.Taiko.UI { public class TaikoPlayfield : Playfield { /// /// The default play field height. /// public const float DEFAULT_PLAYFIELD_HEIGHT = 178f; /// /// The offset from which the center of the hit target lies at. /// public const float HIT_TARGET_OFFSET = TaikoHitObject.DEFAULT_STRONG_CIRCLE_DIAMETER / 2f + 40; /// /// The size of the left area of the playfield. This area contains the input drum. /// private const float left_area_size = 240; protected override Container Content => hitObjectContainer; private readonly Container hitExplosionContainer; private readonly Container kiaiExplosionContainer; private readonly Container barLineContainer; private readonly Container judgementContainer; private readonly Container hitObjectContainer; private readonly Container topLevelHitContainer; private readonly Container overlayBackgroundContainer; private readonly Container backgroundContainer; private readonly Box overlayBackground; private readonly Box background; public TaikoPlayfield() { AddInternal(new Drawable[] { new ScaleFixContainer { RelativeSizeAxes = Axes.X, Height = DEFAULT_PLAYFIELD_HEIGHT, Children = new[] { backgroundContainer = new Container { Name = "Transparent playfield background", RelativeSizeAxes = Axes.Both, BorderThickness = 2, Masking = true, EdgeEffect = new EdgeEffect { Type = EdgeEffectType.Shadow, Colour = Color4.Black.Opacity(0.2f), Radius = 5, }, Children = new Drawable[] { background = new Box { RelativeSizeAxes = Axes.Both, Alpha = 0.6f }, } }, new Container { Name = "Right area", RelativeSizeAxes = Axes.Both, Margin = new MarginPadding { Left = left_area_size }, Children = new Drawable[] { hitExplosionContainer = new Container { RelativeSizeAxes = Axes.Y, BlendingMode = BlendingMode.Additive }, new Container { Name = "Masked elements", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Left = HIT_TARGET_OFFSET }, Masking = true, Children = new Drawable[] { barLineContainer = new Container { RelativeSizeAxes = Axes.Both, }, new HitTarget { Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, }, hitObjectContainer = new Container { RelativeSizeAxes = Axes.Both, }, } }, kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Y, Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, BlendingMode = BlendingMode.Additive }, judgementContainer = new Container { Name = "Judgements", RelativeSizeAxes = Axes.Y, Margin = new MarginPadding { Left = HIT_TARGET_OFFSET }, BlendingMode = BlendingMode.Additive }, } }, overlayBackgroundContainer = new Container { Name = "Left overlay", Size = new Vector2(left_area_size, DEFAULT_PLAYFIELD_HEIGHT), BorderThickness = 1, Children = new Drawable[] { overlayBackground = new Box { RelativeSizeAxes = Axes.Both, }, new InputDrum { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativePositionAxes = Axes.X, Position = new Vector2(0.10f, 0), Scale = new Vector2(0.9f) }, new Box { Anchor = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = 10, ColourInfo = Framework.Graphics.Colour.ColourInfo.GradientHorizontal(Color4.Black.Opacity(0.6f), Color4.Black.Opacity(0)), }, } }, } }, topLevelHitContainer = new Container { Name = "Top level hit objects", RelativeSizeAxes = Axes.Both, } }); } [BackgroundDependencyLoader] private void load(OsuColour colours) { overlayBackgroundContainer.BorderColour = colours.Gray0; overlayBackground.Colour = colours.Gray1; backgroundContainer.BorderColour = colours.Gray1; background.Colour = colours.Gray0; } public override void Add(DrawableHitObject h) { h.Depth = (float)h.HitObject.StartTime; base.Add(h); // Swells should be moved at the very top of the playfield when they reach the hit target var swell = h as DrawableSwell; if (swell != null) swell.OnStart += () => topLevelHitContainer.Add(swell.CreateProxy()); } public void AddBarLine(DrawableBarLine barLine) { barLineContainer.Add(barLine); } public override void OnJudgement(DrawableHitObject judgedObject) { bool wasHit = judgedObject.Judgement.Result == HitResult.Hit; bool secondHit = judgedObject.Judgement.SecondHit; judgementContainer.Add(new DrawableTaikoJudgement(judgedObject.Judgement) { Anchor = wasHit ? Anchor.TopLeft : Anchor.CentreLeft, Origin = wasHit ? Anchor.BottomCentre : Anchor.Centre, RelativePositionAxes = Axes.X, X = wasHit ? judgedObject.Position.X : 0, }); if (!wasHit) return; bool isRim = judgedObject.HitObject is RimHit; if (!secondHit) { if (judgedObject.X >= -0.05f && !(judgedObject is DrawableSwell)) { // If we're far enough away from the left stage, we should bring outselves in front of it topLevelHitContainer.Add(judgedObject.CreateProxy()); } hitExplosionContainer.Add(new HitExplosion(judgedObject.Judgement, isRim)); if (judgedObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(judgedObject.Judgement, isRim)); } else hitExplosionContainer.Children.FirstOrDefault(e => e.Judgement == judgedObject.Judgement)?.VisualiseSecondHit(); } /// /// This is a very special type of container. It serves a similar purpose to , however unlike , /// this will only adjust the scale relative to the height of its parent and will maintain the original width relative to its parent. /// /// /// By adjusting the scale relative to the height of its parent, the aspect ratio of this container's children is maintained, however this is undesirable /// in the case where the hit object container should not have its width adjusted by scale. To counteract this, another container is nested inside this /// container which takes care of reversing the width adjustment while appearing transparent to the user. /// /// private class ScaleFixContainer : Container { protected override Container Content => widthAdjustmentContainer; private readonly WidthAdjustmentContainer widthAdjustmentContainer; /// /// We only want to apply DrawScale in the Y-axis to preserve aspect ratio and doesn't care about having its width adjusted. /// protected override Vector2 DrawScale => Scale * RelativeToAbsoluteFactor.Y / DrawHeight; public ScaleFixContainer() { AddInternal(widthAdjustmentContainer = new WidthAdjustmentContainer { ParentDrawScaleReference = () => DrawScale.X }); } /// /// The container type that reverses the width adjustment. /// private class WidthAdjustmentContainer : Container { /// /// This container needs to know its parent's so it can reverse the width adjustment caused by . /// public Func ParentDrawScaleReference; public WidthAdjustmentContainer() { // This container doesn't care about height, it should always fill its parent RelativeSizeAxes = Axes.Y; } protected override void Update() { base.Update(); // Reverse the DrawScale adjustment Width = Parent.DrawSize.X / ParentDrawScaleReference(); } } } } }