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,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user