@
CI / build-test (push) Failing after 1m8s

Add MrGameEng.AI utility-AI module; format codebase with CSharpier

New MrGameEng.AI module (ResponseCurve, Consideration, UtilityAction,
UtilityAi selector, Blackboard) plus CSharpier formatting applied across
the whole engine. Documents the CSharpier convention in CLAUDE.md.
@
This commit is contained in:
Leonid Pershin
2026-06-12 07:19:10 +03:00
parent 4ae730cafa
commit fd6343bd09
96 changed files with 2066 additions and 589 deletions
+39 -11
View File
@@ -52,23 +52,35 @@ public readonly struct CameraState
public static class CameraMath
{
/// <summary>Computes the full camera state for a frame.</summary>
public static CameraState Compute(in Camera camera, int virtualWidth, int virtualHeight, ViewportMapping mapping)
public static CameraState Compute(
in Camera camera,
int virtualWidth,
int virtualHeight,
ViewportMapping mapping
)
{
var zoom = camera.Zoom <= 0f ? 1f : camera.Zoom;
var position = ClampToBounds(camera, virtualWidth, virtualHeight, zoom);
var view =
Matrix.CreateTranslation(-position.X, -position.Y, 0f) *
Matrix.CreateRotationZ(-camera.Rotation) *
Matrix.CreateScale(zoom, zoom, 1f) *
Matrix.CreateTranslation(virtualWidth / 2f, virtualHeight / 2f, 0f);
Matrix.CreateTranslation(-position.X, -position.Y, 0f)
* Matrix.CreateRotationZ(-camera.Rotation)
* Matrix.CreateScale(zoom, zoom, 1f)
* Matrix.CreateTranslation(virtualWidth / 2f, virtualHeight / 2f, 0f);
var inverseView = Matrix.Invert(view);
return new CameraState
{
View = view,
Projection = Matrix.CreateOrthographicOffCenter(0f, virtualWidth, virtualHeight, 0f, 0f, 1f),
Projection = Matrix.CreateOrthographicOffCenter(
0f,
virtualWidth,
virtualHeight,
0f,
0f,
1f
),
InverseView = inverseView,
CullRect = ComputeCullRect(inverseView, virtualWidth, virtualHeight),
VirtualWidth = virtualWidth,
@@ -81,14 +93,29 @@ public static class CameraMath
/// Computes the letterbox mapping that fits the virtual resolution into a physical
/// viewport, preserving aspect ratio and centering.
/// </summary>
public static ViewportMapping ComputeMapping(int screenWidth, int screenHeight, int virtualWidth, int virtualHeight)
public static ViewportMapping ComputeMapping(
int screenWidth,
int screenHeight,
int virtualWidth,
int virtualHeight
)
{
var scale = MathF.Min((float)screenWidth / virtualWidth, (float)screenHeight / virtualHeight);
var offset = new Vector2(screenWidth - virtualWidth * scale, screenHeight - virtualHeight * scale) / 2f;
var scale = MathF.Min(
(float)screenWidth / virtualWidth,
(float)screenHeight / virtualHeight
);
var offset =
new Vector2(screenWidth - virtualWidth * scale, screenHeight - virtualHeight * scale)
/ 2f;
return new ViewportMapping(offset, scale);
}
private static Vector2 ClampToBounds(in Camera camera, int virtualWidth, int virtualHeight, float zoom)
private static Vector2 ClampToBounds(
in Camera camera,
int virtualWidth,
int virtualHeight,
float zoom
)
{
if (camera.Bounds is not { } bounds)
{
@@ -100,7 +127,8 @@ public static class CameraMath
var halfH = virtualHeight / (2f * zoom);
return new Vector2(
ClampAxis(camera.Position.X, bounds.Left + halfW, bounds.Right - halfW),
ClampAxis(camera.Position.Y, bounds.Top + halfH, bounds.Bottom - halfH));
ClampAxis(camera.Position.Y, bounds.Top + halfH, bounds.Bottom - halfH)
);
}
private static float ClampAxis(float value, float min, float max) =>
+24 -8
View File
@@ -10,7 +10,11 @@ public static class CullingMath
/// (valid for any rotation), given its transform, region size in pixels and origin.
/// </summary>
public static (Vector2 Center, float Radius) SpriteBoundingCircle(
in Transform2D transform, float regionWidth, float regionHeight, Vector2 origin)
in Transform2D transform,
float regionWidth,
float regionHeight,
Vector2 origin
)
{
var scaledW = regionWidth * transform.Scale.X;
var scaledH = regionHeight * transform.Scale.Y;
@@ -24,20 +28,33 @@ public static class CullingMath
/// diagonal (no square root per sprite; conservative for non-uniform scale, exact for uniform).
/// </summary>
public static (Vector2 Center, float Radius) SpriteBoundingCircle(
in Transform2D transform, Texture2DRegion region, Vector2 origin)
in Transform2D transform,
Texture2DRegion region,
Vector2 origin
)
{
var center = SpriteCenter(
in transform, region.Width * transform.Scale.X, region.Height * transform.Scale.Y, origin);
in transform,
region.Width * transform.Scale.X,
region.Height * transform.Scale.Y,
origin
);
var maxScale = MathF.Max(MathF.Abs(transform.Scale.X), MathF.Abs(transform.Scale.Y));
return (center, 0.5f * region.Diagonal * maxScale);
}
private static Vector2 SpriteCenter(in Transform2D transform, float scaledW, float scaledH, Vector2 origin)
private static Vector2 SpriteCenter(
in Transform2D transform,
float scaledW,
float scaledH,
Vector2 origin
)
{
// Offset from the pivot (= transform.Position) to the sprite's geometric center.
var toCenter = new Vector2(
scaledW / 2f - origin.X * transform.Scale.X,
scaledH / 2f - origin.Y * transform.Scale.Y);
scaledH / 2f - origin.Y * transform.Scale.Y
);
if (transform.Rotation == 0f)
{
@@ -45,9 +62,8 @@ public static class CullingMath
}
var (sin, cos) = MathF.SinCos(transform.Rotation);
return transform.Position + new Vector2(
toCenter.X * cos - toCenter.Y * sin,
toCenter.X * sin + toCenter.Y * cos);
return transform.Position
+ new Vector2(toCenter.X * cos - toCenter.Y * sin, toCenter.X * sin + toCenter.Y * cos);
}
/// <summary>True when the circle overlaps the rectangle.</summary>
+11 -3
View File
@@ -52,14 +52,20 @@ public sealed class LayerRegistry
public int Count => _layers.Length;
/// <summary>Registers a layer drawn after all previously registered ones.</summary>
public LayerId Register(string name, LayerSpace space = LayerSpace.World, LayerSortMode sortMode = LayerSortMode.Depth)
public LayerId Register(
string name,
LayerSpace space = LayerSpace.World,
LayerSortMode sortMode = LayerSortMode.Depth
)
{
lock (_sync)
{
var layers = _layers;
if (layers.Length == 256)
{
throw new InvalidOperationException("Maximum number of render layers (256) reached.");
throw new InvalidOperationException(
"Maximum number of render layers (256) reached."
);
}
var id = new LayerId((byte)layers.Length);
@@ -80,7 +86,9 @@ public sealed class LayerRegistry
return id.Value < layers.Length
? layers[id.Value]
: throw new ArgumentOutOfRangeException(
nameof(id), $"Render layer {id.Value} is not registered (registered: {layers.Length}).");
nameof(id),
$"Render layer {id.Value} is not registered (registered: {layers.Length})."
);
}
}
}
@@ -1,5 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
@@ -7,5 +6,4 @@
<ItemGroup>
<ProjectReference Include="..\MrGameEng.Core\MrGameEng.Core.csproj" />
</ItemGroup>
</Project>
+16 -12
View File
@@ -96,20 +96,24 @@ public sealed class SpriteRenderSystem : QuerySystem<Sprite, Transform2D>
}
_renderer.BeginChunkedSubmit(_segmentLengths.AsSpan(0, _segments.Count));
Parallel.For(0, _segments.Count, segmentIndex =>
{
var (chunk, start, length) = _segments[segmentIndex];
var (sprites, transforms) = _chunks[chunk];
var writer = _renderer.GetChunkWriter(segmentIndex);
var s = sprites.Span.Slice(start, length);
var t = transforms.Span.Slice(start, length);
for (var i = 0; i < s.Length; i++)
Parallel.For(
0,
_segments.Count,
segmentIndex =>
{
_renderer.SubmitInto(ref writer, in t[i], in s[i]);
}
var (chunk, start, length) = _segments[segmentIndex];
var (sprites, transforms) = _chunks[chunk];
var writer = _renderer.GetChunkWriter(segmentIndex);
var s = sprites.Span.Slice(start, length);
var t = transforms.Span.Slice(start, length);
for (var i = 0; i < s.Length; i++)
{
_renderer.SubmitInto(ref writer, in t[i], in s[i]);
}
_renderer.EndChunk(segmentIndex, in writer);
});
_renderer.EndChunk(segmentIndex, in writer);
}
);
_renderer.CommitChunkedSubmit();
}
}
+92 -29
View File
@@ -17,7 +17,9 @@ public sealed class Renderer2D : IDisposable
private const int MaxQuadsPerDraw = 8192;
private const int ParallelBlock = 4096;
private static readonly int VertexStride = VertexPositionColorTexture.VertexDeclaration.VertexStride;
private static readonly int VertexStride = VertexPositionColorTexture
.VertexDeclaration
.VertexStride;
/// <summary>Render layer registry. Register layers before the first frame.</summary>
public LayerRegistry Layers { get; } = new();
@@ -71,7 +73,11 @@ public sealed class Renderer2D : IDisposable
_batcher = new SpriteBatcher(_options.InitialCapacity);
_vertices = new VertexPositionColorTexture[_options.InitialCapacity * 4];
_vertexBuffer = new DynamicVertexBuffer(
device, VertexPositionColorTexture.VertexDeclaration, _vertices.Length * 2, BufferUsage.WriteOnly);
device,
VertexPositionColorTexture.VertexDeclaration,
_vertices.Length * 2,
BufferUsage.WriteOnly
);
_effect = new BasicEffect(device)
{
@@ -91,7 +97,11 @@ public sealed class Renderer2D : IDisposable
var (virtualW, virtualH, mapping) = ResolveVirtualResolution();
Camera = CameraMath.Compute(camera, virtualW, virtualH, mapping);
_screenCamera = CameraMath.Compute(
new Camera(new Vector2(virtualW / 2f, virtualH / 2f)), virtualW, virtualH, mapping);
new Camera(new Vector2(virtualW / 2f, virtualH / 2f)),
virtualW,
virtualH,
mapping
);
_batcher.Clear();
SubmittedSprites = 0;
@@ -155,14 +165,18 @@ public sealed class Renderer2D : IDisposable
if (count >= _options.ParallelThreshold)
{
var blocks = (count + ParallelBlock - 1) / ParallelBlock;
Parallel.For(0, blocks, block =>
{
var end = Math.Min((block + 1) * ParallelBlock, count);
for (var i = block * ParallelBlock; i < end; i++)
Parallel.For(
0,
blocks,
block =>
{
BuildVertex(order, i);
var end = Math.Min((block + 1) * ParallelBlock, count);
for (var i = block * ParallelBlock; i < end; i++)
{
BuildVertex(order, i);
}
}
});
);
}
else
{
@@ -190,7 +204,14 @@ public sealed class Renderer2D : IDisposable
hint = SetDataOptions.Discard;
}
_vertexBuffer.SetData(_ringBaseVertex * VertexStride, _vertices, 0, vertexCount, VertexStride, hint);
_vertexBuffer.SetData(
_ringBaseVertex * VertexStride,
_vertices,
0,
vertexCount,
VertexStride,
hint
);
_ringCursor = _ringBaseVertex + vertexCount;
var uploadEnd = Stopwatch.GetTimestamp();
UploadMs = ToMs(uploadEnd - buildEnd);
@@ -213,7 +234,8 @@ public sealed class Renderer2D : IDisposable
(int)MathF.Round(mapping.Offset.X),
(int)MathF.Round(mapping.Offset.Y),
(int)MathF.Round(Camera.VirtualWidth * mapping.Scale),
(int)MathF.Round(Camera.VirtualHeight * mapping.Scale));
(int)MathF.Round(Camera.VirtualHeight * mapping.Scale)
);
}
DrawBatches(order, count);
@@ -247,9 +269,14 @@ public sealed class Renderer2D : IDisposable
_batcher.BeginChunks(chunkLengths);
}
internal SpriteChunkWriter GetChunkWriter(int chunkIndex) => _batcher.GetChunkWriter(chunkIndex);
internal SpriteChunkWriter GetChunkWriter(int chunkIndex) =>
_batcher.GetChunkWriter(chunkIndex);
internal void SubmitInto(ref SpriteChunkWriter writer, in Transform2D transform, in Sprite sprite)
internal void SubmitInto(
ref SpriteChunkWriter writer,
in Transform2D transform,
in Sprite sprite
)
{
switch (TryBuildInstance(in transform, in sprite, out var instance, out var key))
{
@@ -262,7 +289,8 @@ public sealed class Renderer2D : IDisposable
}
}
internal void EndChunk(int chunkIndex, in SpriteChunkWriter writer) => _batcher.EndChunk(chunkIndex, in writer);
internal void EndChunk(int chunkIndex, in SpriteChunkWriter writer) =>
_batcher.EndChunk(chunkIndex, in writer);
internal void CommitChunkedSubmit()
{
@@ -278,7 +306,11 @@ public sealed class Renderer2D : IDisposable
}
private SubmitResult TryBuildInstance(
in Transform2D transform, in Sprite sprite, out SpriteInstance instance, out ulong key)
in Transform2D transform,
in Sprite sprite,
out SpriteInstance instance,
out ulong key
)
{
instance = default;
key = 0;
@@ -288,10 +320,16 @@ public sealed class Renderer2D : IDisposable
}
var layer = Layers[sprite.Layer];
var (center, radius) = CullingMath.SpriteBoundingCircle(in transform, region, sprite.Origin);
var (center, radius) = CullingMath.SpriteBoundingCircle(
in transform,
region,
sprite.Origin
);
if (layer.Space == LayerSpace.World &&
!CullingMath.CircleIntersectsRect(center, radius, Camera.CullRect))
if (
layer.Space == LayerSpace.World
&& !CullingMath.CircleIntersectsRect(center, radius, Camera.CullRect)
)
{
return SubmitResult.Culled;
}
@@ -304,7 +342,9 @@ public sealed class Renderer2D : IDisposable
{
Region = region,
Center = center,
HalfSize = new Vector2(region.Width * transform.Scale.X, region.Height * transform.Scale.Y) / 2f,
HalfSize =
new Vector2(region.Width * transform.Scale.X, region.Height * transform.Scale.Y)
/ 2f,
Rotation = transform.Rotation,
Color = sprite.Color,
Flip = sprite.Flip,
@@ -319,7 +359,8 @@ public sealed class Renderer2D : IDisposable
if (!_begun)
{
throw new InvalidOperationException(
"Renderer used outside BeginFrame/EndFrame (is CameraSystem registered first?).");
"Renderer used outside BeginFrame/EndFrame (is CameraSystem registered first?)."
);
}
}
@@ -331,8 +372,11 @@ public sealed class Renderer2D : IDisposable
return (viewport.Width, viewport.Height, ViewportMapping.Identity);
}
return (virtualSize.X, virtualSize.Y,
CameraMath.ComputeMapping(viewport.Width, viewport.Height, virtualSize.X, virtualSize.Y));
return (
virtualSize.X,
virtualSize.Y,
CameraMath.ComputeMapping(viewport.Width, viewport.Height, virtualSize.X, virtualSize.Y)
);
}
private void BuildVertex(int[] order, int i)
@@ -355,7 +399,8 @@ public sealed class Renderer2D : IDisposable
(v0, v1) = (v1, v0);
}
Vector2 rx, ry;
Vector2 rx,
ry;
if (instance.Rotation == 0f)
{
rx = new Vector2(instance.HalfSize.X, 0f);
@@ -432,7 +477,11 @@ public sealed class Renderer2D : IDisposable
{
pass.Apply();
_device.DrawIndexedPrimitives(
PrimitiveType.TriangleList, _ringBaseVertex + firstQuad * 4, 0, quads * 2);
PrimitiveType.TriangleList,
_ringBaseVertex + firstQuad * 4,
0,
quads * 2
);
DrawCalls++;
}
@@ -461,15 +510,24 @@ public sealed class Renderer2D : IDisposable
{
_vertexBuffer.Dispose();
_vertexBuffer = new DynamicVertexBuffer(
_device, VertexPositionColorTexture.VertexDeclaration, wantedBuffer, BufferUsage.WriteOnly);
_device,
VertexPositionColorTexture.VertexDeclaration,
wantedBuffer,
BufferUsage.WriteOnly
);
_ringCursor = 0;
}
}
private static float ToMs(long timestampDelta) => (float)timestampDelta * 1000f / Stopwatch.Frequency;
private static float ToMs(long timestampDelta) =>
(float)timestampDelta * 1000f / Stopwatch.Frequency;
private static VertexPositionColorTexture Vertex(Vector2 position, Color color, float u, float v) =>
new(new Vector3(position, 0f), color, new Vector2(u, v));
private static VertexPositionColorTexture Vertex(
Vector2 position,
Color color,
float u,
float v
) => new(new Vector3(position, 0f), color, new Vector2(u, v));
private static IndexBuffer CreateQuadIndexBuffer(GraphicsDevice device)
{
@@ -486,7 +544,12 @@ public sealed class Renderer2D : IDisposable
indices[index + 5] = (ushort)(vertex + 3);
}
var buffer = new IndexBuffer(device, IndexElementSize.SixteenBits, indices.Length, BufferUsage.WriteOnly);
var buffer = new IndexBuffer(
device,
IndexElementSize.SixteenBits,
indices.Length,
BufferUsage.WriteOnly
);
buffer.SetData(indices);
return buffer;
}
@@ -13,7 +13,10 @@ public static class SceneGraphicsExtensions
/// is created on first use and shared between scenes. Call from <c>OnLoad</c>.
/// </summary>
public static Renderer2D UseRenderer2D(
this Scene scene, Renderer2DOptions? options = null, params BaseSystem[] extraDrawSystems)
this Scene scene,
Renderer2DOptions? options = null,
params BaseSystem[] extraDrawSystems
)
{
var services = scene.Context.Services;
var renderer = services.GetOrDefault<Renderer2D>();
@@ -25,8 +28,9 @@ public static class SceneGraphicsExtensions
else if (options is not null)
{
Log.Warning(
"UseRenderer2D: the renderer already exists, the passed options are ignored " +
"(Renderer2D is a shared service configured by its first user).");
"UseRenderer2D: the renderer already exists, the passed options are ignored "
+ "(Renderer2D is a shared service configured by its first user)."
);
}
scene.DrawSystems.Add(new CameraSystem(renderer));
+9 -2
View File
@@ -19,11 +19,18 @@ public sealed class SpriteAnimationClip
public float Duration => Frames.Count / FramesPerSecond;
/// <summary>Creates a clip.</summary>
public SpriteAnimationClip(IReadOnlyList<Texture2DRegion> frames, float framesPerSecond = 12f, bool loop = true)
public SpriteAnimationClip(
IReadOnlyList<Texture2DRegion> frames,
float framesPerSecond = 12f,
bool loop = true
)
{
if (frames.Count == 0)
{
throw new ArgumentException("An animation clip needs at least one frame.", nameof(frames));
throw new ArgumentException(
"An animation clip needs at least one frame.",
nameof(frames)
);
}
Frames = frames;
+2 -1
View File
@@ -130,7 +130,8 @@ public sealed class SpriteBatcher
// Биты, различающиеся хотя бы у одной пары ключей: проходы по одинаковым
// разрядам (один слой, одна глубина) пропускаются целиком.
ulong orBits = 0, andBits = ~0UL;
ulong orBits = 0,
andBits = ~0UL;
for (var i = 0; i < n; i++)
{
orBits |= _keys[i];
+3 -1
View File
@@ -9,7 +9,9 @@ public static class SpriteSortKey
{
/// <summary>Composes a sort key from layer, depth and texture grouping key.</summary>
public static ulong Make(byte layer, float depth, int textureKey) =>
((ulong)layer << 56) | ((ulong)DepthToSortableBits(depth) << 24) | ((uint)textureKey & 0xFF_FFFF);
((ulong)layer << 56)
| ((ulong)DepthToSortableBits(depth) << 24)
| ((uint)textureKey & 0xFF_FFFF);
/// <summary>
/// Maps a float to bits whose unsigned order matches the float order
+4 -4
View File
@@ -35,7 +35,9 @@ public sealed class Texture2DRegion
Texture = texture;
Bounds = bounds;
TextureSortKey = System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(texture);
Diagonal = MathF.Sqrt((float)bounds.Width * bounds.Width + (float)bounds.Height * bounds.Height);
Diagonal = MathF.Sqrt(
(float)bounds.Width * bounds.Width + (float)bounds.Height * bounds.Height
);
// UV предрассчитаны один раз — в кадре на каждый спрайт экономятся 4 деления.
// texture может быть null только в headless-тестах.
@@ -50,7 +52,5 @@ public sealed class Texture2DRegion
/// <summary>Creates a region covering the whole <paramref name="texture"/>.</summary>
public Texture2DRegion(Texture2D texture)
: this(texture, new Rectangle(0, 0, texture.Width, texture.Height))
{
}
: this(texture, new Rectangle(0, 0, texture.Width, texture.Height)) { }
}