Update README.md to include project description, developer documentation links, and license information.
CI / build-test (push) Successful in 1m6s
CI / build-test (push) Successful in 1m6s
This commit is contained in:
@@ -0,0 +1,141 @@
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Diagnostics;
|
||||
using Microsoft.CodeAnalysis.Text;
|
||||
using MrGameEng.Assets.Generator;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Assets.Generator.Tests;
|
||||
|
||||
public class AssetHandlesGeneratorTests
|
||||
{
|
||||
private sealed class FakeAdditionalText(string path) : AdditionalText
|
||||
{
|
||||
public override string Path { get; } = path;
|
||||
|
||||
public override SourceText GetText(CancellationToken cancellationToken = default) =>
|
||||
SourceText.From(string.Empty);
|
||||
}
|
||||
|
||||
private sealed class FakeOptions(Dictionary<string, string> values) : AnalyzerConfigOptions
|
||||
{
|
||||
public override bool TryGetValue(string key, out string value) =>
|
||||
values.TryGetValue(key, out value!);
|
||||
}
|
||||
|
||||
private sealed class FakeOptionsProvider(Dictionary<string, string> values) : AnalyzerConfigOptionsProvider
|
||||
{
|
||||
public override AnalyzerConfigOptions GlobalOptions { get; } = new FakeOptions(values);
|
||||
|
||||
public override AnalyzerConfigOptions GetOptions(SyntaxTree tree) => GlobalOptions;
|
||||
|
||||
public override AnalyzerConfigOptions GetOptions(AdditionalText textFile) => GlobalOptions;
|
||||
}
|
||||
|
||||
private static string RunGenerator(string[] files, Dictionary<string, string>? options = null)
|
||||
{
|
||||
var driver = CSharpGeneratorDriver.Create(
|
||||
[new AssetHandlesGenerator().AsSourceGenerator()],
|
||||
additionalTexts: Array.ConvertAll(files, f => (AdditionalText)new FakeAdditionalText(f)),
|
||||
optionsProvider: new FakeOptionsProvider(options ?? new Dictionary<string, string>
|
||||
{
|
||||
["build_property.RootNamespace"] = "MyGame",
|
||||
}));
|
||||
|
||||
var compilation = CSharpCompilation.Create("test");
|
||||
var result = driver.RunGenerators(compilation).GetRunResult();
|
||||
return Assert.Single(Assert.Single(result.Results).GeneratedSources).SourceText.ToString();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratesTypedHandles_ForKnownExtensions()
|
||||
{
|
||||
var source = RunGenerator(
|
||||
[
|
||||
@"D:\game\Assets\Textures\player.png",
|
||||
@"D:\game\Assets\Sounds\jump.wav",
|
||||
@"D:\game\Assets\Fonts\main.ttf",
|
||||
@"D:\game\Assets\Music\theme.ogg",
|
||||
]);
|
||||
|
||||
Assert.Contains("namespace MyGame;", source);
|
||||
Assert.Contains("public static partial class GameAssets", source);
|
||||
Assert.Contains("public static class Textures", source);
|
||||
Assert.Contains(
|
||||
"AssetRef<global::Microsoft.Xna.Framework.Graphics.Texture2D> Player = new(\"Textures/player.png\")",
|
||||
source);
|
||||
Assert.Contains(
|
||||
"AssetRef<global::Microsoft.Xna.Framework.Audio.SoundEffect> Jump = new(\"Sounds/jump.wav\")",
|
||||
source);
|
||||
Assert.Contains("AssetRef<global::FontStashSharp.FontSystem> Main", source);
|
||||
Assert.Contains("AssetRef<global::MrGameEng.Core.MusicTrack> Theme", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IgnoresUnknownExtensions_AndFilesOutsideAssets()
|
||||
{
|
||||
var source = RunGenerator(
|
||||
[
|
||||
@"D:\game\Assets\readme.md",
|
||||
@"D:\game\Other\image.png",
|
||||
@"D:\game\Assets\valid.png",
|
||||
]);
|
||||
|
||||
Assert.Contains("Valid", source);
|
||||
Assert.DoesNotContain("Readme", source);
|
||||
Assert.DoesNotContain("Image", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NestedDirectories_BecomeNestedClasses()
|
||||
{
|
||||
var source = RunGenerator([@"C:\proj\Assets\UI\Icons\save-icon.png"]);
|
||||
|
||||
Assert.Contains("public static class UI", source);
|
||||
Assert.Contains("public static class Icons", source);
|
||||
Assert.Contains("SaveIcon = new(\"UI/Icons/save-icon.png\")", source);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GeneratedCode_Compiles()
|
||||
{
|
||||
var source = RunGenerator([@"D:\game\Assets\player.png"]);
|
||||
|
||||
// Подменяем внешние типы заглушками, чтобы скомпилировать сгенерированный код изолированно.
|
||||
const string stubs = """
|
||||
namespace MrGameEng.Assets { public readonly record struct AssetRef<T>(string Path) where T : class; }
|
||||
namespace Microsoft.Xna.Framework.Graphics { public sealed class Texture2D; }
|
||||
""";
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
"generated",
|
||||
[CSharpSyntaxTree.ParseText(source), CSharpSyntaxTree.ParseText(stubs)],
|
||||
[MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
|
||||
MetadataReference.CreateFromFile(System.Runtime.Loader.AssemblyLoadContext.Default
|
||||
.LoadFromAssemblyName(new System.Reflection.AssemblyName("System.Runtime")).Location)],
|
||||
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
var errors = compilation.GetDiagnostics().Where(d => d.Severity == DiagnosticSeverity.Error).ToList();
|
||||
Assert.Empty(errors);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("player", "Player")]
|
||||
[InlineData("save-icon", "SaveIcon")]
|
||||
[InlineData("8bit_font", "_8bitFont")]
|
||||
[InlineData("...", "_")]
|
||||
public void Identifier_SanitizesNames(string input, string expected)
|
||||
{
|
||||
Assert.Equal(expected, AssetHandlesGenerator.Identifier(input));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(@"D:\game\Assets\a.png", "a.png")]
|
||||
[InlineData("/home/user/game/Assets/sub/b.wav", "sub/b.wav")]
|
||||
[InlineData(@"D:\game\NotAssets\c.png", null)]
|
||||
[InlineData(@"D:\game\Assets\unknown.xyz", null)]
|
||||
public void ToAssetPath_ExtractsRelativePath(string fullPath, string? expected)
|
||||
{
|
||||
Assert.Equal(expected, AssetHandlesGenerator.ToAssetPath(fullPath));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Assets.Generator\MrGameEng.Assets.Generator.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,53 @@
|
||||
using MrGameEng.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Core.Tests;
|
||||
|
||||
public class GameClockTests
|
||||
{
|
||||
[Fact]
|
||||
public void Advance_SingleFrame_UpdatesDeltaTotalAndFrameCount()
|
||||
{
|
||||
var clock = new GameClock();
|
||||
|
||||
clock.Advance(0.016f);
|
||||
|
||||
Assert.Equal(0.016f, clock.DeltaTime);
|
||||
Assert.Equal(0.016f, clock.UnscaledDeltaTime);
|
||||
Assert.Equal(0.016, clock.TotalTime, 3);
|
||||
Assert.Equal(1, clock.FrameCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Advance_WithTimeScale_ScalesDeltaButNotUnscaled()
|
||||
{
|
||||
var clock = new GameClock { TimeScale = 0.5f };
|
||||
|
||||
clock.Advance(0.02f);
|
||||
|
||||
Assert.Equal(0.01f, clock.DeltaTime, 3);
|
||||
Assert.Equal(0.02f, clock.UnscaledDeltaTime, 3);
|
||||
Assert.Equal(0.01, clock.TotalTime, 3);
|
||||
Assert.Equal(0.02, clock.UnscaledTotalTime, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimeScale_Zero_PausesScaledTime()
|
||||
{
|
||||
var clock = new GameClock { TimeScale = 0f };
|
||||
|
||||
clock.Advance(0.016f);
|
||||
|
||||
Assert.Equal(0f, clock.DeltaTime);
|
||||
Assert.Equal(0.0, clock.TotalTime);
|
||||
Assert.Equal(0.016, clock.UnscaledTotalTime, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimeScale_Negative_ClampsToZero()
|
||||
{
|
||||
var clock = new GameClock { TimeScale = -1f };
|
||||
|
||||
Assert.Equal(0f, clock.TimeScale);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Core\MrGameEng.Core.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,87 @@
|
||||
using MrGameEng.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Core.Tests;
|
||||
|
||||
public class SceneManagerTests
|
||||
{
|
||||
private sealed class TrackingScene : Scene
|
||||
{
|
||||
public int LoadCount;
|
||||
public int UnloadCount;
|
||||
public int UpdateCount;
|
||||
|
||||
protected override void OnLoad() => LoadCount++;
|
||||
|
||||
protected override void OnUnload() => UnloadCount++;
|
||||
|
||||
public override void Update(GameClock clock)
|
||||
{
|
||||
UpdateCount++;
|
||||
base.Update(clock);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Switch_IsDeferred_UntilNextUpdate()
|
||||
{
|
||||
var context = new EngineContext();
|
||||
var scene = new TrackingScene();
|
||||
|
||||
context.Scenes.Switch(scene);
|
||||
|
||||
Assert.Null(context.Scenes.Current);
|
||||
Assert.Equal(0, scene.LoadCount);
|
||||
|
||||
context.Scenes.Update(context.Clock);
|
||||
|
||||
Assert.Same(scene, context.Scenes.Current);
|
||||
Assert.Equal(1, scene.LoadCount);
|
||||
Assert.Equal(1, scene.UpdateCount);
|
||||
Assert.True(scene.IsLoaded);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Switch_UnloadsPreviousScene_AndLoadsNext()
|
||||
{
|
||||
var context = new EngineContext();
|
||||
var first = new TrackingScene();
|
||||
var second = new TrackingScene();
|
||||
|
||||
context.Scenes.Switch(first);
|
||||
context.Scenes.Update(context.Clock);
|
||||
context.Scenes.Switch(second);
|
||||
context.Scenes.Update(context.Clock);
|
||||
|
||||
Assert.Equal(1, first.UnloadCount);
|
||||
Assert.False(first.IsLoaded);
|
||||
Assert.Same(second, context.Scenes.Current);
|
||||
Assert.Equal(1, second.LoadCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Switch_ToNull_UnloadsCurrentScene()
|
||||
{
|
||||
var context = new EngineContext();
|
||||
var scene = new TrackingScene();
|
||||
|
||||
context.Scenes.Switch(scene);
|
||||
context.Scenes.Update(context.Clock);
|
||||
context.Scenes.Switch(null);
|
||||
context.Scenes.Update(context.Clock);
|
||||
|
||||
Assert.Null(context.Scenes.Current);
|
||||
Assert.Equal(1, scene.UnloadCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_WithoutScene_DoesNothing()
|
||||
{
|
||||
var context = new EngineContext();
|
||||
|
||||
context.Scenes.Update(context.Clock);
|
||||
context.Scenes.Draw(context.Clock);
|
||||
|
||||
Assert.Null(context.Scenes.Current);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Friflo.Engine.ECS.Systems;
|
||||
using MrGameEng.Core;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Core.Tests;
|
||||
|
||||
public class SceneSystemsTests
|
||||
{
|
||||
private struct Velocity : IComponent
|
||||
{
|
||||
public float X;
|
||||
}
|
||||
|
||||
private struct Translation : IComponent
|
||||
{
|
||||
public float X;
|
||||
}
|
||||
|
||||
private sealed class MoveSystem : QuerySystem<Translation, Velocity>
|
||||
{
|
||||
protected override void OnUpdate()
|
||||
{
|
||||
foreach (var (translations, velocities, _) in Query.Chunks)
|
||||
{
|
||||
var t = translations.Span;
|
||||
var v = velocities.Span;
|
||||
for (var i = 0; i < t.Length; i++)
|
||||
{
|
||||
t[i].X += v[i].X * Tick.deltaTime;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class MovingScene : Scene
|
||||
{
|
||||
public Entity Mover;
|
||||
|
||||
protected override void OnLoad()
|
||||
{
|
||||
Mover = Store.CreateEntity(new Translation { X = 0f }, new Velocity { X = 10f });
|
||||
UpdateSystems.Add(new MoveSystem());
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Update_RunsRegisteredQuerySystem_WithClockDelta()
|
||||
{
|
||||
var context = new EngineContext();
|
||||
var scene = new MovingScene();
|
||||
context.Scenes.Switch(scene);
|
||||
|
||||
context.Clock.Advance(0.5f);
|
||||
context.Scenes.Update(context.Clock);
|
||||
|
||||
Assert.Equal(5f, scene.Mover.GetComponent<Translation>().X, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TimeScale_AffectsSystemDelta()
|
||||
{
|
||||
var context = new EngineContext();
|
||||
var scene = new MovingScene();
|
||||
context.Scenes.Switch(scene);
|
||||
context.Clock.TimeScale = 0f;
|
||||
|
||||
context.Clock.Advance(0.5f);
|
||||
context.Scenes.Update(context.Clock);
|
||||
|
||||
Assert.Equal(0f, scene.Mover.GetComponent<Translation>().X);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Graphics;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Graphics.Tests;
|
||||
|
||||
public class CameraMathTests
|
||||
{
|
||||
private static void AssertVector(Vector2 expected, Vector2 actual, float tolerance = 0.001f)
|
||||
{
|
||||
Assert.InRange(actual.X, expected.X - tolerance, expected.X + tolerance);
|
||||
Assert.InRange(actual.Y, expected.Y - tolerance, expected.Y + tolerance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CameraPosition_MapsToScreenCenter()
|
||||
{
|
||||
var camera = new Camera(new Vector2(500f, 300f), zoom: 2f, rotation: 0.7f);
|
||||
|
||||
var state = CameraMath.Compute(camera, 800, 600, ViewportMapping.Identity);
|
||||
|
||||
AssertVector(new Vector2(400f, 300f), state.WorldToScreen(camera.Position));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScreenToWorld_RoundTripsWithWorldToScreen()
|
||||
{
|
||||
var camera = new Camera(new Vector2(123f, -45f), zoom: 1.5f, rotation: 0.3f);
|
||||
var state = CameraMath.Compute(camera, 1280, 720, new ViewportMapping(new Vector2(0f, 60f), 1.5f));
|
||||
|
||||
var screen = new Vector2(200f, 500f);
|
||||
var world = state.ScreenToWorld(screen);
|
||||
|
||||
AssertVector(screen, state.WorldToScreen(world));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zoom_ShrinksCullRect()
|
||||
{
|
||||
var camera = new Camera(Vector2.Zero, zoom: 2f);
|
||||
|
||||
var state = CameraMath.Compute(camera, 800, 600, ViewportMapping.Identity);
|
||||
|
||||
Assert.Equal(400f, state.CullRect.Width, 1);
|
||||
Assert.Equal(300f, state.CullRect.Height, 1);
|
||||
AssertVector(Vector2.Zero, state.CullRect.Center);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Rotation_ExpandsCullRectToCoverRotatedView()
|
||||
{
|
||||
var straight = CameraMath.Compute(new Camera(Vector2.Zero), 800, 600, ViewportMapping.Identity);
|
||||
var rotated = CameraMath.Compute(new Camera(Vector2.Zero, rotation: MathF.PI / 4f), 800, 600, ViewportMapping.Identity);
|
||||
|
||||
Assert.True(rotated.CullRect.Width > straight.CullRect.Width);
|
||||
Assert.True(rotated.CullRect.Height > straight.CullRect.Height);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Bounds_ClampCameraToWorldEdges()
|
||||
{
|
||||
var bounds = new RectF(0f, 0f, 2000f, 1000f);
|
||||
var camera = new Camera(new Vector2(-500f, 500f), bounds: bounds);
|
||||
|
||||
var state = CameraMath.Compute(camera, 800, 600, ViewportMapping.Identity);
|
||||
|
||||
// Camera should be clamped so the view's left edge sits at the world's left edge.
|
||||
AssertVector(new Vector2(0f, 200f), state.ScreenToWorld(Vector2.Zero));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mapping_CentersVirtualResolutionInWiderWindow()
|
||||
{
|
||||
var mapping = CameraMath.ComputeMapping(1920, 1080, 640, 360);
|
||||
|
||||
Assert.Equal(3f, mapping.Scale);
|
||||
Assert.Equal(Vector2.Zero, mapping.Offset);
|
||||
|
||||
var letterboxed = CameraMath.ComputeMapping(1920, 1200, 640, 360);
|
||||
Assert.Equal(3f, letterboxed.Scale);
|
||||
Assert.Equal(new Vector2(0f, 60f), letterboxed.Offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Graphics;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Graphics.Tests;
|
||||
|
||||
public class CullingTests
|
||||
{
|
||||
[Fact]
|
||||
public void BoundingCircle_UnrotatedTopLeftOrigin_CenterIsRegionCenter()
|
||||
{
|
||||
var transform = Transform2D.At(new Vector2(100f, 200f));
|
||||
|
||||
var (center, radius) = CullingMath.SpriteBoundingCircle(transform, 32f, 32f, Vector2.Zero);
|
||||
|
||||
Assert.Equal(new Vector2(116f, 216f), center);
|
||||
Assert.Equal(0.5f * MathF.Sqrt(32f * 32f * 2f), radius, 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoundingCircle_CenterOrigin_CenterIsPosition()
|
||||
{
|
||||
var transform = Transform2D.At(new Vector2(50f, 50f));
|
||||
|
||||
var (center, _) = CullingMath.SpriteBoundingCircle(transform, 64f, 32f, new Vector2(32f, 16f));
|
||||
|
||||
Assert.Equal(new Vector2(50f, 50f), center);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BoundingCircle_Scale_GrowsRadius()
|
||||
{
|
||||
var transform = new Transform2D(Vector2.Zero, scale: new Vector2(2f, 2f));
|
||||
|
||||
var (_, radius) = CullingMath.SpriteBoundingCircle(transform, 10f, 10f, Vector2.Zero);
|
||||
|
||||
Assert.Equal(0.5f * MathF.Sqrt(800f), radius, 3);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(50f, 50f, true)] // inside
|
||||
[InlineData(-4f, 50f, true)] // touching from the left (radius 5)
|
||||
[InlineData(-20f, 50f, false)] // far left
|
||||
[InlineData(50f, 130f, false)] // far below
|
||||
public void CircleIntersectsRect_DetectsOverlap(float x, float y, bool expected)
|
||||
{
|
||||
var rect = new RectF(0f, 0f, 100f, 100f);
|
||||
|
||||
Assert.Equal(expected, CullingMath.CircleIntersectsRect(new Vector2(x, y), 5f, rect));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using MrGameEng.Graphics;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Graphics.Tests;
|
||||
|
||||
public class LayerRegistryTests
|
||||
{
|
||||
[Fact]
|
||||
public void Registry_StartsWithDefaultWorldLayer()
|
||||
{
|
||||
var registry = new LayerRegistry();
|
||||
|
||||
Assert.Equal(1, registry.Count);
|
||||
var layer = registry[LayerId.Default];
|
||||
Assert.Equal("Default", layer.Name);
|
||||
Assert.Equal(LayerSpace.World, layer.Space);
|
||||
Assert.Equal(LayerSortMode.Depth, layer.SortMode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Register_AssignsSequentialIds_InDrawOrder()
|
||||
{
|
||||
var registry = new LayerRegistry();
|
||||
|
||||
var world = registry.Register("World", LayerSpace.World, LayerSortMode.YSort);
|
||||
var ui = registry.Register("UI", LayerSpace.Screen);
|
||||
|
||||
Assert.Equal(1, world.Value);
|
||||
Assert.Equal(2, ui.Value);
|
||||
Assert.Equal(LayerSortMode.YSort, registry[world].SortMode);
|
||||
Assert.Equal(LayerSpace.Screen, registry[ui].Space);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Graphics\MrGameEng.Graphics.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,47 @@
|
||||
using MrGameEng.Graphics;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Graphics.Tests;
|
||||
|
||||
public class SortKeyTests
|
||||
{
|
||||
[Fact]
|
||||
public void LayerDominatesDepthAndTexture()
|
||||
{
|
||||
var lowLayer = SpriteSortKey.Make(0, 1000f, textureKey: 5);
|
||||
var highLayer = SpriteSortKey.Make(1, -1000f, textureKey: 1);
|
||||
|
||||
Assert.True(lowLayer < highLayer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DepthDominatesTexture_WithinLayer()
|
||||
{
|
||||
var behind = SpriteSortKey.Make(3, -5f, textureKey: 999);
|
||||
var inFront = SpriteSortKey.Make(3, 5f, textureKey: 1);
|
||||
|
||||
Assert.True(behind < inFront);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(-100f, -1f)]
|
||||
[InlineData(-1f, 0f)]
|
||||
[InlineData(0f, 1f)]
|
||||
[InlineData(1f, 100f)]
|
||||
[InlineData(-0.5f, 0.5f)]
|
||||
public void DepthBits_PreserveFloatOrder(float smaller, float larger)
|
||||
{
|
||||
Assert.True(SpriteSortKey.DepthToSortableBits(smaller) < SpriteSortKey.DepthToSortableBits(larger));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualLayerAndDepth_GroupByTexture()
|
||||
{
|
||||
var a1 = SpriteSortKey.Make(2, 1f, textureKey: 7);
|
||||
var b = SpriteSortKey.Make(2, 1f, textureKey: 9);
|
||||
var a2 = SpriteSortKey.Make(2, 1f, textureKey: 7);
|
||||
|
||||
Assert.Equal(a1, a2);
|
||||
Assert.NotEqual(a1, b);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
using Friflo.Engine.ECS;
|
||||
using Microsoft.Xna.Framework;
|
||||
using MrGameEng.Core;
|
||||
using MrGameEng.Graphics;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Graphics.Tests;
|
||||
|
||||
public class SpriteAnimationTests
|
||||
{
|
||||
private static Texture2DRegion Region(int x) => new(null!, new Rectangle(x, 0, 16, 16));
|
||||
|
||||
private static SpriteAnimationClip Clip(bool loop, params int[] xs) =>
|
||||
new(Array.ConvertAll(xs, Region), framesPerSecond: 10f, loop);
|
||||
|
||||
[Fact]
|
||||
public void FrameAt_LoopingClip_WrapsAround()
|
||||
{
|
||||
var clip = Clip(loop: true, 0, 1, 2);
|
||||
|
||||
Assert.Equal(0, clip.FrameAt(0.00f).Bounds.X);
|
||||
Assert.Equal(1, clip.FrameAt(0.10f).Bounds.X);
|
||||
Assert.Equal(2, clip.FrameAt(0.25f).Bounds.X);
|
||||
Assert.Equal(0, clip.FrameAt(0.30f).Bounds.X); // wrapped
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FrameAt_NonLoopingClip_ClampsToLastFrame()
|
||||
{
|
||||
var clip = Clip(loop: false, 0, 1);
|
||||
|
||||
Assert.Equal(1, clip.FrameAt(10f).Bounds.X);
|
||||
}
|
||||
|
||||
private sealed class AnimScene : Scene
|
||||
{
|
||||
public Entity Animated;
|
||||
public SpriteAnimationClip Clip = SpriteAnimationTests.Clip(loop: false, 0, 1, 2);
|
||||
|
||||
protected override void OnLoad()
|
||||
{
|
||||
this.UseSpriteAnimation();
|
||||
Animated = Store.CreateEntity(
|
||||
new Sprite { Color = Color.White },
|
||||
new SpriteAnimator(Clip));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AnimationSystem_AdvancesFrames_AndStopsAtEnd()
|
||||
{
|
||||
var context = new EngineContext();
|
||||
var scene = new AnimScene();
|
||||
context.Scenes.Switch(scene);
|
||||
|
||||
context.Clock.Advance(0.15f); // 10 fps → frame 1
|
||||
context.Scenes.Update(context.Clock);
|
||||
Assert.Equal(1, scene.Animated.GetComponent<Sprite>().Region!.Bounds.X);
|
||||
|
||||
context.Clock.Advance(1.0f); // far past the end of a non-looping clip
|
||||
context.Scenes.Update(context.Clock);
|
||||
Assert.Equal(2, scene.Animated.GetComponent<Sprite>().Region!.Bounds.X);
|
||||
Assert.False(scene.Animated.GetComponent<SpriteAnimator>().Playing);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using MrGameEng.Graphics;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Graphics.Tests;
|
||||
|
||||
public class SpriteBatcherTests
|
||||
{
|
||||
private static SpriteInstance Instance(byte layer) => new() { Layer = layer };
|
||||
|
||||
[Fact]
|
||||
public void Sort_OrdersByKey_ReturningOriginalIndices()
|
||||
{
|
||||
var batcher = new SpriteBatcher(initialCapacity: 2);
|
||||
|
||||
batcher.Submit(Instance(2), SpriteSortKey.Make(2, 0f, 0));
|
||||
batcher.Submit(Instance(0), SpriteSortKey.Make(0, 0f, 0));
|
||||
batcher.Submit(Instance(1), SpriteSortKey.Make(1, 0f, 0));
|
||||
|
||||
var order = batcher.Sort();
|
||||
|
||||
Assert.Equal(3, order.Length);
|
||||
Assert.Equal(0, batcher[order[0]].Layer);
|
||||
Assert.Equal(1, batcher[order[1]].Layer);
|
||||
Assert.Equal(2, batcher[order[2]].Layer);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Submit_GrowsBeyondInitialCapacity()
|
||||
{
|
||||
var batcher = new SpriteBatcher(initialCapacity: 1);
|
||||
|
||||
for (var i = 0; i < 100; i++)
|
||||
{
|
||||
batcher.Submit(Instance(0), (ulong)(100 - i));
|
||||
}
|
||||
|
||||
Assert.Equal(100, batcher.Count);
|
||||
var order = batcher.Sort();
|
||||
Assert.Equal(99, order[0]); // последний сабмит имеет наименьший ключ
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Clear_ResetsCount_KeepsWorking()
|
||||
{
|
||||
var batcher = new SpriteBatcher();
|
||||
batcher.Submit(Instance(0), 5);
|
||||
batcher.Clear();
|
||||
|
||||
Assert.Equal(0, batcher.Count);
|
||||
batcher.Submit(Instance(1), 1);
|
||||
Assert.Equal(1, batcher.Count);
|
||||
Assert.Equal(1, batcher[batcher.Sort()[0]].Layer);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using MrGameEng.Input;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Input.Tests;
|
||||
|
||||
public class ActionMapTests
|
||||
{
|
||||
private enum GameAction
|
||||
{
|
||||
Jump,
|
||||
MoveLeft,
|
||||
MoveRight,
|
||||
}
|
||||
|
||||
private static void Frame(InputManager input, params Keys[] keys) =>
|
||||
input.Apply(new KeyboardState(keys), default, GamePadState.Default);
|
||||
|
||||
[Fact]
|
||||
public void IsDown_TrueWhenAnyBindingIsHeld()
|
||||
{
|
||||
var input = new InputManager();
|
||||
var map = new ActionMap<GameAction>(input)
|
||||
.Bind(GameAction.Jump, Keys.Space)
|
||||
.Bind(GameAction.Jump, Keys.W);
|
||||
|
||||
Frame(input, Keys.W);
|
||||
|
||||
Assert.True(map.IsDown(GameAction.Jump));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void IsPressed_EdgeTriggered()
|
||||
{
|
||||
var input = new InputManager();
|
||||
var map = new ActionMap<GameAction>(input).Bind(GameAction.Jump, Keys.Space);
|
||||
|
||||
Frame(input, Keys.Space);
|
||||
Assert.True(map.IsPressed(GameAction.Jump));
|
||||
|
||||
Frame(input, Keys.Space);
|
||||
Assert.False(map.IsPressed(GameAction.Jump));
|
||||
Assert.True(map.IsDown(GameAction.Jump));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Unbind_RemovesAllBindings()
|
||||
{
|
||||
var input = new InputManager();
|
||||
var map = new ActionMap<GameAction>(input).Bind(GameAction.Jump, Keys.Space);
|
||||
|
||||
map.Unbind(GameAction.Jump);
|
||||
Frame(input, Keys.Space);
|
||||
|
||||
Assert.False(map.IsDown(GameAction.Jump));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetAxis_CombinesTwoActions()
|
||||
{
|
||||
var input = new InputManager();
|
||||
var map = new ActionMap<GameAction>(input)
|
||||
.Bind(GameAction.MoveLeft, Keys.A)
|
||||
.Bind(GameAction.MoveRight, Keys.D);
|
||||
|
||||
Frame(input, Keys.A);
|
||||
Assert.Equal(-1f, map.GetAxis(GameAction.MoveLeft, GameAction.MoveRight));
|
||||
|
||||
Frame(input, Keys.A, Keys.D);
|
||||
Assert.Equal(0f, map.GetAxis(GameAction.MoveLeft, GameAction.MoveRight));
|
||||
|
||||
Frame(input, Keys.D);
|
||||
Assert.Equal(1f, map.GetAxis(GameAction.MoveLeft, GameAction.MoveRight));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using Microsoft.Xna.Framework;
|
||||
using Microsoft.Xna.Framework.Input;
|
||||
using MrGameEng.Input;
|
||||
using Xunit;
|
||||
|
||||
namespace MrGameEng.Input.Tests;
|
||||
|
||||
public class InputManagerTests
|
||||
{
|
||||
private static MouseState Mouse(int x = 0, int y = 0, int wheel = 0, ButtonState left = ButtonState.Released) =>
|
||||
new(x, y, wheel, left, ButtonState.Released, ButtonState.Released, ButtonState.Released, ButtonState.Released);
|
||||
|
||||
private static void Frame(InputManager input, KeyboardState keyboard = default, MouseState mouse = default) =>
|
||||
input.Apply(keyboard, mouse, GamePadState.Default);
|
||||
|
||||
[Fact]
|
||||
public void KeyPressed_OnlyOnTheFrameItGoesDown()
|
||||
{
|
||||
var input = new InputManager();
|
||||
|
||||
Frame(input, new KeyboardState(Keys.Space));
|
||||
Assert.True(input.IsKeyPressed(Keys.Space));
|
||||
Assert.True(input.IsKeyDown(Keys.Space));
|
||||
|
||||
Frame(input, new KeyboardState(Keys.Space));
|
||||
Assert.False(input.IsKeyPressed(Keys.Space));
|
||||
Assert.True(input.IsKeyDown(Keys.Space));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeyReleased_OnlyOnTheFrameItGoesUp()
|
||||
{
|
||||
var input = new InputManager();
|
||||
|
||||
Frame(input, new KeyboardState(Keys.A));
|
||||
Frame(input);
|
||||
|
||||
Assert.True(input.IsKeyReleased(Keys.A));
|
||||
Frame(input);
|
||||
Assert.False(input.IsKeyReleased(Keys.A));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MouseDeltaAndWheelDelta_ComputedBetweenFrames()
|
||||
{
|
||||
var input = new InputManager();
|
||||
|
||||
Frame(input, mouse: Mouse(x: 10, y: 10, wheel: 0));
|
||||
Frame(input, mouse: Mouse(x: 25, y: 5, wheel: 120));
|
||||
|
||||
Assert.Equal(new Point(15, -5), input.MouseDelta);
|
||||
Assert.Equal(120, input.WheelDelta);
|
||||
Assert.Equal(new Point(25, 5), input.MousePosition);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MousePressed_DetectsLeftButtonEdge()
|
||||
{
|
||||
var input = new InputManager();
|
||||
|
||||
Frame(input, mouse: Mouse());
|
||||
Frame(input, mouse: Mouse(left: ButtonState.Pressed));
|
||||
|
||||
Assert.True(input.IsMousePressed(MouseButton.Left));
|
||||
Assert.False(input.IsMousePressed(MouseButton.Right));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="xunit" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\MrGameEng.Input\MrGameEng.Input.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
Reference in New Issue
Block a user