Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update "Argon" skin gameplay counters to new design #25226

Merged
merged 58 commits into from
Nov 12, 2023
Merged
Show file tree
Hide file tree
Changes from 52 commits
Commits
Show all changes
58 commits
Select commit Hold shift + click to select a range
12bb4b6
Support retrieving textures in argon skin from game resources
frenzibyte Oct 22, 2023
8ed660d
Remove incorrect logic
frenzibyte Oct 22, 2023
9ca672d
Add "Argon" total score counter
frenzibyte Oct 22, 2023
1f36acc
Add "Argon" accuracy counter
frenzibyte Oct 26, 2023
0aa0562
Add "Argon" combo counter
frenzibyte Oct 26, 2023
daf4a03
Abstractify PP counter logic from the "Triangles" implementation
frenzibyte Oct 26, 2023
56eeb11
Add "Argon" performance points counter
frenzibyte Oct 26, 2023
50af157
Add "Argon" background wedges
frenzibyte Oct 26, 2023
df80b4d
Update "Argon" health display specifications to match design
frenzibyte Oct 26, 2023
e119e04
Update "Argon" skin components layout and use the new components
frenzibyte Oct 26, 2023
07e7d53
Bake external scale factors into glyph scale
frenzibyte Oct 26, 2023
841c5bb
Add modified skin for serialisation test coverage
frenzibyte Oct 26, 2023
caddbac
Merge branch 'master' into gameplay-hud-redesign/counters
peppy Oct 30, 2023
4ae9b40
Update resources
peppy Oct 30, 2023
91cf237
Revert health display settings changes
frenzibyte Nov 3, 2023
1c844a1
Remove health line detail
frenzibyte Nov 4, 2023
58a830f
Revert "Add "Argon" performance points counter"
frenzibyte Nov 4, 2023
a23dfbe
Revert "Abstractify PP counter logic from the "Triangles" implementat…
frenzibyte Nov 4, 2023
6c3169a
Remove PP wedge and logic for gameplay layout
frenzibyte Nov 4, 2023
77f5a4c
Update skin deserialisation archive
frenzibyte Nov 4, 2023
1d4f4cf
Merge branch 'master' into gameplay-hud-redesign/counters
frenzibyte Nov 4, 2023
634795e
Adjust failing test scenes
frenzibyte Nov 4, 2023
01e59d1
Adjust health bar settings on default components initialiser to match…
frenzibyte Nov 6, 2023
754e052
Update argon score wedge design
frenzibyte Nov 6, 2023
4c7db4c
Make score counter right-aligned
frenzibyte Nov 6, 2023
ce36884
Make score wireframes display up to required digits count
frenzibyte Nov 6, 2023
7c1c62b
Remove argon combo wedge and update combo counter position
frenzibyte Nov 6, 2023
0dbba13
Split argon score sprite text and update combo counter design
frenzibyte Nov 6, 2023
cbea2db
Support absolute-sized health bar and use it for default layout
frenzibyte Nov 6, 2023
e6d3085
Update accuracy counter design
frenzibyte Nov 7, 2023
d30bac3
Move "required display digits" feature to reside in argon score counter
frenzibyte Nov 7, 2023
fdc714a
Support percentages and ignore dot characters in wireframes part
frenzibyte Nov 7, 2023
4de5454
Bring back left-side line next to health display
frenzibyte Nov 7, 2023
07b7e13
Place health display in front of the score wedge
frenzibyte Nov 7, 2023
18c71c9
Merge branch 'master' into gameplay-hud-redesign/counters
frenzibyte Nov 7, 2023
d0fea38
Update skin deserialisation archives
frenzibyte Nov 7, 2023
2ef17c5
Remove unused local
frenzibyte Nov 7, 2023
6f5d905
Update argon accuracy counter design
frenzibyte Nov 9, 2023
d3af3c6
Merge branch 'master' into gameplay-hud-redesign/counters
frenzibyte Nov 9, 2023
f31c1c9
Rename and move skinnable line component to a more commomn place
peppy Nov 10, 2023
99d9db5
Use a better default size for line
peppy Nov 10, 2023
6c1d48d
Remove unused function
peppy Nov 10, 2023
7c3a626
Add basic animation for combo counter
peppy Nov 10, 2023
e861681
Adjust argon rolling easings
peppy Nov 10, 2023
7e0b412
Change animation to only affect number portion, add miss animation
peppy Nov 10, 2023
4f90ac1
Reduce the default wireframe opacity a bit
peppy Nov 10, 2023
60df272
Rename `RoundedLine` to `BoxElement` and make more generically useful
peppy Nov 10, 2023
7db14ba
Update resources
peppy Nov 10, 2023
fa59218
Update deserialising test
peppy Nov 10, 2023
b4ec624
Remove width/height bindables from `ArgonWedgePiece`
peppy Nov 10, 2023
67312a2
Remove `ArgonScoreWedge` and use `ArgonWedgePiece` directly
peppy Nov 10, 2023
a02aeed
Adjust combo animation slightly
peppy Nov 10, 2023
b7972e3
Merge branch 'skin-size-editing' into gameplay-hud-redesign/counters
bdach Nov 11, 2023
870e4ce
Fix argon health display not handling invalidation correctly
bdach Nov 11, 2023
ea556ab
Merge branch 'skin-size-editing' into gameplay-hud-redesign/counters
bdach Nov 11, 2023
04a16b3
Merge branch 'master' into gameplay-hud-redesign/counters
peppy Nov 11, 2023
c3201d5
Merge branch 'master' into gameplay-hud-redesign/counters
bdach Nov 11, 2023
3e8c89e
Fix one more reference to removed setting
bdach Nov 11, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
2 changes: 2 additions & 0 deletions osu.Game.Tests/Skins/SkinDeserialisationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ public class SkinDeserialisationTest
"Archives/modified-argon-pro-20231001.osk",
// Covers player name text component.
"Archives/modified-argon-20231106.osk",
// Covers "Argon" accuracy/score/combo counters, and wedges
"Archives/modified-argon-20231108.osk",
};

/// <summary>
Expand Down
5 changes: 2 additions & 3 deletions osu.Game.Tests/Visual/Gameplay/TestSceneSkinEditor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public void TestDragSelection()

AddStep("Begin drag top left", () =>
{
InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4));
InputManager.MoveMouseTo(box1.ScreenSpaceDrawQuad.TopLeft - new Vector2(box1.ScreenSpaceDrawQuad.Width / 4, box1.ScreenSpaceDrawQuad.Height / 8));
InputManager.PressButton(MouseButton.Left);
});

Expand Down Expand Up @@ -146,8 +146,7 @@ public void TestCyclicSelection()
{
AddStep("Add big black box", () =>
{
InputManager.MoveMouseTo(skinEditor.ChildrenOfType<BigBlackBox>().First());
InputManager.Click(MouseButton.Left);
skinEditor.ChildrenOfType<SkinComponentToolbox.ToolboxComponentButton>().First(b => b.ChildrenOfType<BigBlackBox>().FirstOrDefault() != null).TriggerClick();
});

AddStep("store box", () =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public partial class TestSceneSkinnableAccuracyCounter : SkinnableHUDComponentTe
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());

protected override Drawable CreateArgonImplementation() => new ArgonAccuracyCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultAccuracyCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyAccuracyCounter();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public partial class TestSceneSkinnableComboCounter : SkinnableHUDComponentTestS
[Cached]
private ScoreProcessor scoreProcessor = new ScoreProcessor(new OsuRuleset());

protected override Drawable CreateArgonImplementation() => new ArgonComboCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultComboCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyComboCounter();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public partial class TestSceneSkinnableHealthDisplay : SkinnableHUDComponentTest
[Cached(typeof(HealthProcessor))]
private HealthProcessor healthProcessor = new DrainingHealthProcessor(0);

protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f) };
protected override Drawable CreateArgonImplementation() => new ArgonHealthDisplay { Scale = new Vector2(0.6f), BarLength = { Value = 1f } };
protected override Drawable CreateDefaultImplementation() => new DefaultHealthDisplay { Scale = new Vector2(0.6f) };
protected override Drawable CreateLegacyImplementation() => new LegacyHealthDisplay { Scale = new Vector2(0.6f) };

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public partial class TestSceneSkinnableScoreCounter : SkinnableHUDComponentTestS
[Cached(typeof(ScoreProcessor))]
private ScoreProcessor scoreProcessor = TestGameplayState.Create(new OsuRuleset()).ScoreProcessor;

protected override Drawable CreateArgonImplementation() => new ArgonScoreCounter();
protected override Drawable CreateDefaultImplementation() => new DefaultScoreCounter();
protected override Drawable CreateLegacyImplementation() => new LegacyScoreCounter();

Expand Down
90 changes: 90 additions & 0 deletions osu.Game/Screens/Play/HUD/ArgonAccuracyCounter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// 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.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Skinning;
using osuTK;

namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonAccuracyCounter : GameplayAccuracyCounter, ISerialisableDrawable
{
protected override double RollingDuration => 500;
protected override Easing RollingEasing => Easing.OutQuint;

[SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")]
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f)
{
Precision = 0.01f,
MinValue = 0,
MaxValue = 1,
};

public bool UsesFixedAnchor { get; set; }

protected override IHasText CreateText() => new ArgonAccuracyTextComponent
{
WireframeOpacity = { BindTarget = WireframeOpacity },
};

private partial class ArgonAccuracyTextComponent : CompositeDrawable, IHasText
{
private readonly ArgonCounterTextComponent wholePart;
private readonly ArgonCounterTextComponent fractionPart;

public IBindable<float> WireframeOpacity { get; } = new BindableFloat();

public LocalisableString Text
{
get => wholePart.Text;
set
{
string[] split = value.ToString().Replace("%", string.Empty).Split(".");

wholePart.Text = split[0];
fractionPart.Text = "." + split[1];
Comment on lines +47 to +50
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record this only kinda works due to PercentageCounter.FormatCount() / FormatUtils.FormatAccuracy(). You generally cannot and should not be doing such shenanigans with stringed numbers because you might not necessarily know which locale they're formatted in and assuming things about where the dot or percent sign are will end badly.

}
}

public ArgonAccuracyTextComponent()
{
AutoSizeAxes = Axes.Both;

InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Horizontal,
Children = new Drawable[]
{
new Container
{
AutoSizeAxes = Axes.Both,
Child = wholePart = new ArgonCounterTextComponent(Anchor.TopRight, "ACCURACY")
{
RequiredDisplayDigits = { Value = 3 },
WireframeOpacity = { BindTarget = WireframeOpacity }
}
},
fractionPart = new ArgonCounterTextComponent(Anchor.TopLeft)
{
Margin = new MarginPadding { Top = 12f * 2f + 4f }, // +4 to account for the extra spaces above the digits.
WireframeOpacity = { BindTarget = WireframeOpacity },
Scale = new Vector2(0.5f),
},
new ArgonCounterTextComponent(Anchor.TopLeft)
{
Text = @"%",
Margin = new MarginPadding { Top = 12f },
WireframeOpacity = { BindTarget = WireframeOpacity }
},
}
};
}
}
}
}
61 changes: 61 additions & 0 deletions osu.Game/Screens/Play/HUD/ArgonComboCounter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// 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 osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Game.Configuration;
using osu.Game.Rulesets.Scoring;
using osuTK;
using osuTK.Graphics;

namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonComboCounter : ComboCounter
{
private ArgonCounterTextComponent text = null!;

protected override double RollingDuration => 500;
protected override Easing RollingEasing => Easing.OutQuint;

[SettingSource("Wireframe opacity", "Controls the opacity of the wire frames behind the digits.")]
public BindableFloat WireframeOpacity { get; } = new BindableFloat(0.25f)
{
Precision = 0.01f,
MinValue = 0,
MaxValue = 1,
};

[BackgroundDependencyLoader]
private void load(ScoreProcessor scoreProcessor)
{
Current.BindTo(scoreProcessor.Combo);
Current.BindValueChanged(combo =>
{
bool wasIncrease = combo.NewValue > combo.OldValue;
bool wasMiss = combo.OldValue > 1 && combo.NewValue == 0;

float newScale = Math.Clamp(text.NumberContainer.Scale.X * (wasIncrease ? 1.1f : 0.8f), 0.6f, 1.4f);

float duration = wasMiss ? 2000 : 500;

text.NumberContainer
.ScaleTo(new Vector2(newScale))
.ScaleTo(Vector2.One, duration, Easing.OutQuint);

if (wasMiss)
text.FlashColour(Color4.Red, duration, Easing.OutQuint);
});
}

protected override LocalisableString FormatCount(int count) => $@"{count}x";

protected override IHasText CreateText() => text = new ArgonCounterTextComponent(Anchor.TopLeft, "COMBO")
{
WireframeOpacity = { BindTarget = WireframeOpacity },
};
}
}
171 changes: 171 additions & 0 deletions osu.Game/Screens/Play/HUD/ArgonCounterTextComponent.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
// 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.Linq;
using System.Threading.Tasks;
using osu.Framework.Allocation;
using osu.Framework.Bindables;
using osu.Framework.Graphics;
using osu.Framework.Graphics.Containers;
using osu.Framework.Graphics.Sprites;
using osu.Framework.Localisation;
using osu.Framework.Text;
using osu.Game.Graphics;
using osu.Game.Graphics.Sprites;
using osu.Game.Skinning;
using osuTK;

namespace osu.Game.Screens.Play.HUD
{
public partial class ArgonCounterTextComponent : CompositeDrawable, IHasText
{
private readonly ArgonCounterSpriteText wireframesPart;
private readonly ArgonCounterSpriteText textPart;
private readonly OsuSpriteText labelText;

public IBindable<float> WireframeOpacity { get; } = new BindableFloat();
public Bindable<int> RequiredDisplayDigits { get; } = new BindableInt();

public Container NumberContainer { get; private set; }

public LocalisableString Text
{
get => textPart.Text;
set
{
int remainingCount = RequiredDisplayDigits.Value - value.ToString().Count(char.IsDigit);
string remainingText = remainingCount > 0 ? new string('#', remainingCount) : string.Empty;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to reconsider these implementation from a performance perspective in the next profiling round. These string functions can be quite expensive when this text is being updated every update frame.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've spent some time benchmarking this with various methods of allocating a string, and ended up with the following results:

Method Count Mean Error StdDev Gen0 Gen1 Allocated
AllocateWithStringConstructor 1 198.7 ns 2.44 ns 2.28 ns 0.2487 - 520 B
AllocateWithStringBuilder 1 200.3 ns 3.64 ns 3.04 ns 0.2487 - 520 B
AllocateWithStringCreateMethod 1 290.3 ns 2.06 ns 1.72 ns 0.1988 - 416 B
AllocateAndCache 1 378.5 ns 7.45 ns 8.28 ns 0.6733 1.38 KB
AllocateWithStringConstructor 10 1,667.8 ns 33.02 ns 46.30 ns 1.9341 - 4048 B
AllocateWithStringBuilder 10 1,629.6 ns 16.52 ns 14.64 ns 1.9341 - 4048 B
AllocateWithStringCreateMethod 10 2,790.3 ns 25.84 ns 24.17 ns 1.8845 - 3944 B
AllocateAndCache 10 920.1 ns 17.87 ns 16.71 ns 1.0166 2.08 KB
AllocateWithStringConstructor 100 16,720.6 ns 320.20 ns 393.23 ns 18.7988 - 39328 B
AllocateWithStringBuilder 100 16,475.0 ns 249.08 ns 286.85 ns 18.7988 - 39328 B
AllocateWithStringCreateMethod 100 29,170.3 ns 362.28 ns 302.52 ns 18.7378 - 39224 B
AllocateAndCache 100 6,215.7 ns 124.16 ns 174.06 ns 4.4479 9.11 KB
AllocateWithStringConstructor 1000 175,346.1 ns 2,218.49 ns 2,075.18 ns 133.5449 35.1563 392128 B
AllocateWithStringBuilder 1000 172,832.6 ns 1,270.34 ns 991.80 ns 133.0566 34.9121 392128 B
AllocateWithStringCreateMethod 1000 291,833.8 ns 5,300.95 ns 4,958.51 ns 82.0313 33.6914 392024 B
AllocateAndCache 1000 60,663.7 ns 875.24 ns 730.86 ns 38.4521 79.42 KB
code
// 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.Collections.Generic;
using System.Text;
using BenchmarkDotNet.Attributes;

namespace osu.Game.Benchmarks
{
    public class BenchmarkStrings : BenchmarkTest
    {
        [Params(1, 10, 100, 1000)]
        public int Count { get; set; }

        [Benchmark]
        public string[] AllocateWithStringConstructor()
        {
            StringBuilder builder = new StringBuilder();
            string[] array = new string[Count * 10];

            for (int i = 0; i < Count; i++)
            {
                for (int j = 0; j < 10; j++)
                {
                    builder.Clear();
                    builder.Append('#', j);
                    array[i * 10 + j] = builder.ToString();
                }
            }

            return array;
        }

        [Benchmark]
        public string[] AllocateWithStringBuilder()
        {
            StringBuilder builder = new StringBuilder();
            string[] array = new string[Count * 10];

            for (int i = 0; i < Count; i++)
            {
                for (int j = 0; j < 10; j++)
                {
                    builder.Clear();
                    builder.Append('#', j);
                    array[i * 10 + j] = builder.ToString();
                }
            }

            return array;
        }

        [Benchmark]
        public string[] AllocateWithStringCreateMethod()
        {
            string[] array = new string[Count * 10];

            for (int i = 0; i < Count; i++)
            {
                for (int j = 0; j < 10; j++)
                    array[i * 10 + j] = string.Create(j, '#', (span, state) => span.Fill(state));
            }

            return array;
        }

        [Benchmark]
        public string[] AllocateAndCache()
        {
            Dictionary<int, string> dictionary = new Dictionary<int, string>();
            string[] array = new string[Count * 10];

            for (int i = 0; i < Count; i++)
            {
                for (int j = 0; j < 10; j++)
                {
                    if (!dictionary.TryGetValue(j, out string? value))
                        dictionary[j] = value = new string('#', j);

                    array[i * 10 + j] = value;
                }
            }

            return array;
        }
    }
}

With allocating strings of length 0-10 constant characters over thousand iterations, time sits around 0.175ms, so as far as I understand, it takes around ~0.000175ms on average to allocate a single string per frame, which should be safe enough to not have to add some sort of caching for, but you're more knowledgable in this area than I do.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's not cpu but memory concerns (which leads to GC etc.) so look at the "allocated" column if anything.


wireframesPart.Text = remainingText + value;
textPart.Text = value;
}
}

public ArgonCounterTextComponent(Anchor anchor, LocalisableString? label = null)
{
Anchor = anchor;
Origin = anchor;
AutoSizeAxes = Axes.Both;

InternalChild = new FillFlowContainer
{
AutoSizeAxes = Axes.Both,
Direction = FillDirection.Vertical,
Children = new Drawable[]
{
labelText = new OsuSpriteText
{
Alpha = label != null ? 1 : 0,
Text = label.GetValueOrDefault(),
Font = OsuFont.Torus.With(size: 12, weight: FontWeight.Bold),
Margin = new MarginPadding { Left = 2.5f },
},
NumberContainer = new Container
{
AutoSizeAxes = Axes.Both,
Children = new[]
{
wireframesPart = new ArgonCounterSpriteText(wireframesLookup)
{
Anchor = anchor,
Origin = anchor,
},
textPart = new ArgonCounterSpriteText(textLookup)
{
Anchor = anchor,
Origin = anchor,
},
}
}
}
};
}

private string textLookup(char c)
{
switch (c)
{
case '.':
return @"dot";

case '%':
return @"percentage";

default:
return c.ToString();
}
}

private string wireframesLookup(char c)
{
if (c == '.') return @"dot";

return @"wireframes";
}

[BackgroundDependencyLoader]
private void load(OsuColour colours)
{
labelText.Colour = colours.Blue0;
}

protected override void LoadComplete()
{
base.LoadComplete();
WireframeOpacity.BindValueChanged(v => wireframesPart.Alpha = v.NewValue, true);
}

private partial class ArgonCounterSpriteText : OsuSpriteText
{
private readonly Func<char, string> getLookup;

private GlyphStore glyphStore = null!;

protected override char FixedWidthReferenceCharacter => '5';

public ArgonCounterSpriteText(Func<char, string> getLookup)
{
this.getLookup = getLookup;

Shadow = false;
UseFullGlyphHeight = false;
}

[BackgroundDependencyLoader]
private void load(ISkinSource skin)
{
Spacing = new Vector2(-2f, 0f);
Font = new FontUsage(@"argon-counter", 1);
glyphStore = new GlyphStore(skin, getLookup);
}

protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore);

private class GlyphStore : ITexturedGlyphLookupStore
{
private readonly ISkin skin;
private readonly Func<char, string> getLookup;

public GlyphStore(ISkin skin, Func<char, string> getLookup)
{
this.skin = skin;
this.getLookup = getLookup;
}

public ITexturedCharacterGlyph? Get(string fontName, char character)
{
string lookup = getLookup(character);
var texture = skin.GetTexture($"{fontName}-{lookup}");

if (texture == null)
return null;

return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, texture.Height, null), texture, 0.125f);
}

public Task<ITexturedCharacterGlyph?> GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character));
}
}
}
}
Loading