diff --git a/Directory.Build.props b/Directory.Build.props
index 87cb637..9e85214 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -8,45 +8,47 @@
true
$(NoWarn);1591
-
-
-
- Exe
- false
- false
-
-
-
-
-
-
-
- false
- true
- false
-
+
+
+
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ true
+ false
+
-
-
-
-
-
-
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
- all
- runtime; build; native; contentfiles; analyzers; buildtransitive
-
-
-
-
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
\ No newline at end of file
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..e23cfad
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,31 @@
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Hexecs.sln b/Hexecs.sln
index b6b32c4..b4a90ad 100644
--- a/Hexecs.sln
+++ b/Hexecs.sln
@@ -5,7 +5,11 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hexecs.Tests", "src\Hexecs.
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hexecs.Benchmarks", "src\Hexecs.Benchmarks\Hexecs.Benchmarks.csproj", "{6B3B5C57-80EF-4CC7-A0CE-5533B7628FDB}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hexecs.Benchmarks.MonoGame", "src\Hexecs.Benchmarks.MonoGame\Hexecs.Benchmarks.MonoGame.csproj", "{1F5BACA8-7AC3-48B4-9F28-532F7684C80B}"
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hexecs.Benchmarks.Noise", "src\Hexecs.Benchmarks.Noise\Hexecs.Benchmarks.Noise.csproj", "{0CA9A4D9-359D-4F10-8A45-DC3D1A3940AB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Hexecs.Benchmarks.City", "src\Hexecs.Benchmarks.City\Hexecs.Benchmarks.City.csproj", "{B95D5C8E-90D4-4719-A6B4-81120C3BAD39}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarks", "Benchmarks", "{9BB142FC-044B-48F4-A183-5B2BA50E4658}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
@@ -25,9 +29,18 @@ Global
{6B3B5C57-80EF-4CC7-A0CE-5533B7628FDB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6B3B5C57-80EF-4CC7-A0CE-5533B7628FDB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6B3B5C57-80EF-4CC7-A0CE-5533B7628FDB}.Release|Any CPU.Build.0 = Release|Any CPU
- {1F5BACA8-7AC3-48B4-9F28-532F7684C80B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {1F5BACA8-7AC3-48B4-9F28-532F7684C80B}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {1F5BACA8-7AC3-48B4-9F28-532F7684C80B}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {1F5BACA8-7AC3-48B4-9F28-532F7684C80B}.Release|Any CPU.Build.0 = Release|Any CPU
+ {0CA9A4D9-359D-4F10-8A45-DC3D1A3940AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {0CA9A4D9-359D-4F10-8A45-DC3D1A3940AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {0CA9A4D9-359D-4F10-8A45-DC3D1A3940AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {0CA9A4D9-359D-4F10-8A45-DC3D1A3940AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ {B95D5C8E-90D4-4719-A6B4-81120C3BAD39}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {B95D5C8E-90D4-4719-A6B4-81120C3BAD39}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {B95D5C8E-90D4-4719-A6B4-81120C3BAD39}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {B95D5C8E-90D4-4719-A6B4-81120C3BAD39}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {6B3B5C57-80EF-4CC7-A0CE-5533B7628FDB} = {9BB142FC-044B-48F4-A183-5B2BA50E4658}
+ {B95D5C8E-90D4-4719-A6B4-81120C3BAD39} = {9BB142FC-044B-48F4-A183-5B2BA50E4658}
+ {0CA9A4D9-359D-4F10-8A45-DC3D1A3940AB} = {9BB142FC-044B-48F4-A183-5B2BA50E4658}
EndGlobalSection
EndGlobal
diff --git a/src/Hexecs.Benchmarks.City/BenchmarkCounter.cs b/src/Hexecs.Benchmarks.City/BenchmarkCounter.cs
new file mode 100644
index 0000000..cd80598
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/BenchmarkCounter.cs
@@ -0,0 +1,89 @@
+using System.Globalization;
+using System.Text;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Hexecs.Benchmarks.Map;
+
+internal sealed class BenchmarkCounter
+{
+ private readonly Func _countResolver;
+ private readonly int[] _fpsHistory;
+
+ private double _frameTime;
+ private int _fps;
+ private int _frameCount;
+ private double _fpsTimer;
+
+ private int _historyIndex;
+ private bool _historyFull;
+ private double _avgFps;
+ private long _historySum;
+
+ private readonly SpriteFont _font;
+ private readonly SpriteBatch _spriteBatch;
+
+ // Используем StringBuilder как буфер
+ private readonly StringBuilder _stringBuilder = new(128);
+ private readonly Vector2 _textPos = new(10, 10);
+ private readonly Vector2 _shadowPos = new(11, 11);
+
+ public BenchmarkCounter(Func countResolver, ContentManager contentManager, GraphicsDevice graphicsDevice)
+ {
+ _countResolver = countResolver;
+ _fpsHistory = new int[60];
+ _font = contentManager.Load("DebugFont");
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public void Draw(GameTime gameTime)
+ {
+ _frameCount++;
+
+ _spriteBatch.Begin();
+
+ _spriteBatch.DrawString(_font, _stringBuilder, _shadowPos, Color.Black);
+ _spriteBatch.DrawString(_font, _stringBuilder, _textPos, Color.Yellow);
+
+ _spriteBatch.End();
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ var elapsedSeconds = gameTime.ElapsedGameTime.TotalSeconds;
+ _frameTime = gameTime.ElapsedGameTime.TotalMilliseconds;
+ _fpsTimer += elapsedSeconds;
+
+ if (_fpsTimer >= 1.0)
+ {
+ _fps = _frameCount;
+
+ _historySum -= _fpsHistory[_historyIndex];
+ _fpsHistory[_historyIndex] = _fps;
+ _historySum += _fps;
+
+ _historyIndex = (_historyIndex + 1) % 60;
+ if (_historyIndex == 0) _historyFull = true;
+
+ var historyCount = _historyFull ? 60 : _historyIndex;
+ _avgFps = (double)_historySum / historyCount;
+
+ var alloc = GC.GetTotalMemory(false) / 1024.0 / 1024.0;
+ var count = _countResolver();
+
+ // Очищаем буфер и записываем новые данные без создания строк
+ var culture = CultureInfo.InvariantCulture;
+
+ _stringBuilder.Clear();
+ _stringBuilder
+ .Append($"{_fps} FPS")
+ .Append(culture, $" | Avg:{_avgFps:F1} fps")
+ .Append(culture, $" | Entities:{count:N0}")
+ .Append(culture, $" | Frame time:{_frameTime:F1}ms")
+ .Append(culture, $" | Alloc:{alloc:F3}mb");
+
+ _frameCount = 0;
+ _fpsTimer = 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/CityGame.cs b/src/Hexecs.Benchmarks.City/CityGame.cs
new file mode 100644
index 0000000..928477d
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/CityGame.cs
@@ -0,0 +1,102 @@
+using Hexecs.Benchmarks.Map.Common;
+using Hexecs.Benchmarks.Map.Common.Visibles;
+using Hexecs.Benchmarks.Map.Terrains;
+using Hexecs.Benchmarks.Map.Terrains.Commands.Generate;
+using Hexecs.Benchmarks.Map.Utils;
+using Hexecs.Worlds;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+
+namespace Hexecs.Benchmarks.Map;
+
+internal sealed class CityGame : Game
+{
+ private BenchmarkCounter _benchmarkCounter = null!;
+ private Camera _camera = null!;
+ private readonly GraphicsDeviceManager _graphics;
+ private World _world = null!;
+
+ public CityGame()
+ {
+ _graphics = new GraphicsDeviceManager(this)
+ {
+ PreferredBackBufferWidth = 1280,
+ PreferredBackBufferHeight = 720,
+ GraphicsProfile = GraphicsProfile.HiDef,
+ PreferMultiSampling = true,
+ SynchronizeWithVerticalRetrace = true,
+ IsFullScreen = false,
+ HardwareModeSwitch = false
+ };
+
+ // Включаем поддержку сглаживания для устройства
+ _graphics.PreparingDeviceSettings += (_, e) =>
+ {
+ e.GraphicsDeviceInformation.PresentationParameters.MultiSampleCount = 8; // 8x MSAA
+ };
+
+ _graphics.ApplyChanges();
+
+ IsFixedTimeStep = false;
+ Content.RootDirectory = "Content";
+ }
+
+ protected override void Initialize()
+ {
+ GraphicsDevice.SamplerStates[0] = SamplerState.AnisotropicClamp;
+
+ _camera = new Camera(GraphicsDevice);
+ _world = new WorldBuilder()
+ .UseDefaultParallelWorker(Math.Min(6, Environment.ProcessorCount))
+ .UseSingleton(Content)
+ .UseSingleton(GraphicsDevice)
+ .UseSingleton(_camera)
+ .UseTerrain()
+ .UseDefaultActorContext(context => context
+ .Capacity(3_000_000)
+ .AddCommon()
+ .AddTerrain()
+ .AddVisible())
+ .Build();
+
+ _world.Actors.Execute(new GenerateTerrainCommand());
+
+ _benchmarkCounter = new BenchmarkCounter(() => _world.Actors.Length, Content, GraphicsDevice);
+
+ base.Initialize();
+ }
+
+ protected override void Dispose(bool disposing)
+ {
+ if (disposing)
+ {
+ _world.Dispose();
+ }
+
+ base.Dispose(disposing);
+ }
+
+ protected override void Draw(GameTime gameTime)
+ {
+ GraphicsDevice.Clear(Color.White);
+
+ _world.Draw(gameTime.ElapsedGameTime, gameTime.TotalGameTime);
+ _benchmarkCounter.Draw(gameTime);
+
+ base.Draw(gameTime);
+ }
+
+ protected override void Update(GameTime gameTime)
+ {
+ var keyboard = Keyboard.GetState();
+ if (keyboard.IsKeyDown(Keys.Space))
+ {
+ }
+
+ _camera.Update(gameTime);
+ _benchmarkCounter.Update(gameTime);
+ _world.Update(gameTime.ElapsedGameTime, gameTime.TotalGameTime);
+
+ base.Update(gameTime);
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/CommonInstaller.cs b/src/Hexecs.Benchmarks.City/Common/CommonInstaller.cs
new file mode 100644
index 0000000..58aa310
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/CommonInstaller.cs
@@ -0,0 +1,13 @@
+using Hexecs.Benchmarks.Map.Common.Positions;
+
+namespace Hexecs.Benchmarks.Map.Common;
+
+internal static class CommonInstaller
+{
+ public static ActorContextBuilder AddCommon(this ActorContextBuilder builder)
+ {
+ builder.AddPositions();
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/Positions/Position.cs b/src/Hexecs.Benchmarks.City/Common/Positions/Position.cs
new file mode 100644
index 0000000..98c7e99
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/Positions/Position.cs
@@ -0,0 +1,7 @@
+namespace Hexecs.Benchmarks.Map.Common.Positions;
+
+public struct Position : IActorComponent
+{
+ public Point Grid;
+ public Point World;
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/Positions/PositionAbility.cs b/src/Hexecs.Benchmarks.City/Common/Positions/PositionAbility.cs
new file mode 100644
index 0000000..ca9afd8
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/Positions/PositionAbility.cs
@@ -0,0 +1,3 @@
+namespace Hexecs.Benchmarks.Map.Common.Positions;
+
+public readonly struct PositionAbility: IAssetComponent;
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/Positions/PositionBuilder.cs b/src/Hexecs.Benchmarks.City/Common/Positions/PositionBuilder.cs
new file mode 100644
index 0000000..4fe897a
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/Positions/PositionBuilder.cs
@@ -0,0 +1,18 @@
+using Hexecs.Benchmarks.Map.Terrains;
+
+namespace Hexecs.Benchmarks.Map.Common.Positions;
+
+internal sealed class PositionBuilder(TerrainSettings terrainSettings) : IActorBuilder
+{
+ private readonly int _terrainTileSize = terrainSettings.TileSize;
+
+ public void Build(in Actor actor, in AssetRef asset, Args args)
+ {
+ var grid = args.Get(nameof(Point));
+ actor.Add(new Position
+ {
+ Grid = grid,
+ World = new Point(grid.X * _terrainTileSize, grid.Y * _terrainTileSize)
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/Positions/PositionExtensions.cs b/src/Hexecs.Benchmarks.City/Common/Positions/PositionExtensions.cs
new file mode 100644
index 0000000..3fdaca1
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/Positions/PositionExtensions.cs
@@ -0,0 +1,12 @@
+using Hexecs.Assets.Sources;
+
+namespace Hexecs.Benchmarks.Map.Common.Positions;
+
+internal static class PositionExtensions
+{
+ public static AssetConfigurator WithPosition(this AssetConfigurator configurator)
+ {
+ configurator.Set(new PositionAbility());
+ return configurator;
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/Positions/PositionsInstaller.cs b/src/Hexecs.Benchmarks.City/Common/Positions/PositionsInstaller.cs
new file mode 100644
index 0000000..a9b0dee
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/Positions/PositionsInstaller.cs
@@ -0,0 +1,20 @@
+using Hexecs.Benchmarks.Map.Terrains;
+using Hexecs.Dependencies;
+
+namespace Hexecs.Benchmarks.Map.Common.Positions;
+
+internal static class PositionsInstaller
+{
+ public static ActorContextBuilder AddPositions(this ActorContextBuilder builder)
+ {
+ var terrainSettings = builder.World.GetRequiredService();
+
+ builder.CreateBuilder();
+
+ builder
+ .ConfigureComponentPool(terrain => terrain
+ .Capacity(terrainSettings.Width * terrainSettings.Height));
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/Visibles/Visible.cs b/src/Hexecs.Benchmarks.City/Common/Visibles/Visible.cs
new file mode 100644
index 0000000..1c0e4e9
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/Visibles/Visible.cs
@@ -0,0 +1,3 @@
+namespace Hexecs.Benchmarks.Map.Common.Visibles;
+
+public struct Visible : IActorComponent;
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleInstaller.cs b/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleInstaller.cs
new file mode 100644
index 0000000..7c57260
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleInstaller.cs
@@ -0,0 +1,15 @@
+namespace Hexecs.Benchmarks.Map.Common.Visibles;
+
+internal static class VisibleInstaller
+{
+ public static ActorContextBuilder AddVisible(this ActorContextBuilder builder)
+ {
+ builder
+ .ConfigureComponentPool(terrain => terrain
+ .Capacity(4096));
+
+ builder.CreateUpdateSystem();
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleSystem.cs b/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleSystem.cs
new file mode 100644
index 0000000..32cc10e
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Common/Visibles/VisibleSystem.cs
@@ -0,0 +1,49 @@
+using Hexecs.Actors.Systems;
+using Hexecs.Benchmarks.Map.Common.Positions;
+using Hexecs.Benchmarks.Map.Terrains;
+using Hexecs.Benchmarks.Map.Utils;
+using Hexecs.Benchmarks.Map.Utils.Sprites;
+using Hexecs.Threading;
+using Hexecs.Worlds;
+
+namespace Hexecs.Benchmarks.Map.Common.Visibles;
+
+internal sealed class VisibleSystem : UpdateSystem
+{
+ private readonly Camera _camera;
+ private readonly int _tileSize;
+
+ private CameraViewport _currentViewport;
+
+ public VisibleSystem(ActorContext context, Camera camera, IParallelWorker parallelWorker, TerrainSettings settings)
+ : base(context, parallelWorker: parallelWorker)
+ {
+ _camera = camera;
+ _tileSize = settings.TileSize;
+ }
+
+ protected override bool BeforeUpdate(in WorldTime time)
+ {
+ var currentViewport = _camera.Viewport;
+
+ if (currentViewport.Equals(_currentViewport)) return false; // не обновляем, если камера не двигалась
+
+ _currentViewport = currentViewport;
+
+ return true;
+ }
+
+ protected override void Update(in ActorRef actor, in WorldTime time)
+ {
+ ref readonly var position = ref actor.Component1.World;
+
+ if (_currentViewport.Visible(position.X, position.Y, _tileSize, _tileSize))
+ {
+ actor.TryAdd(new Visible());
+ }
+ else
+ {
+ actor.Remove();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Content/Content.mgcb b/src/Hexecs.Benchmarks.City/Content/Content.mgcb
new file mode 100644
index 0000000..ad2226a
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Content/Content.mgcb
@@ -0,0 +1,13 @@
+#----------------------------- Global Properties ----------------------------#
+/outputDir:bin
+/intermediateDir:obj
+/platform:DesktopGL
+/config:
+/profile:Reach
+/compress:False
+
+#---------------------------------- References -------------------------------#
+
+#---------------------------------- Content ----------------------------------#
+/build:DebugFont.spritefont
+/build:terrain_atlas.png
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Content/DebugFont.spritefont b/src/Hexecs.Benchmarks.City/Content/DebugFont.spritefont
new file mode 100644
index 0000000..ba84b52
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Content/DebugFont.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ Consolas
+ 10
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Content/DebugFont.xnb b/src/Hexecs.Benchmarks.City/Content/DebugFont.xnb
new file mode 100644
index 0000000..47451f4
Binary files /dev/null and b/src/Hexecs.Benchmarks.City/Content/DebugFont.xnb differ
diff --git a/src/Hexecs.Benchmarks.City/Content/terrain_atlas.png b/src/Hexecs.Benchmarks.City/Content/terrain_atlas.png
new file mode 100644
index 0000000..79b1332
Binary files /dev/null and b/src/Hexecs.Benchmarks.City/Content/terrain_atlas.png differ
diff --git a/src/Hexecs.Benchmarks.City/Content/terrain_atlas.xnb b/src/Hexecs.Benchmarks.City/Content/terrain_atlas.xnb
new file mode 100644
index 0000000..0081eee
Binary files /dev/null and b/src/Hexecs.Benchmarks.City/Content/terrain_atlas.xnb differ
diff --git a/src/Hexecs.Benchmarks.City/Hexecs.Benchmarks.City.csproj b/src/Hexecs.Benchmarks.City/Hexecs.Benchmarks.City.csproj
new file mode 100644
index 0000000..e1d5f3a
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Hexecs.Benchmarks.City.csproj
@@ -0,0 +1,31 @@
+
+
+
+ Exe
+ net10.0
+ true
+ true
+ Hexecs.Benchmarks.Map
+
+
+
+
+
+
+
+
+
+
+
+
+ Never
+
+
+ PreserveNewest
+
+
+ PreserveNewest
+
+
+
+
diff --git a/src/Hexecs.Benchmarks.City/Program.cs b/src/Hexecs.Benchmarks.City/Program.cs
new file mode 100644
index 0000000..cc90eaf
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Program.cs
@@ -0,0 +1,4 @@
+using Hexecs.Benchmarks.Map;
+
+using var game = new CityGame();
+game.Run();
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/Assets/TerrainAsset.cs b/src/Hexecs.Benchmarks.City/Terrains/Assets/TerrainAsset.cs
new file mode 100644
index 0000000..12a7519
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/Assets/TerrainAsset.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+using Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+namespace Hexecs.Benchmarks.Map.Terrains.Assets;
+
+[method: MethodImpl(MethodImplOptions.AggressiveInlining)]
+public readonly struct TerrainAsset(string name, TerrainType type) : IAssetComponent
+{
+ public const string Ground = "Base";
+ public const string River = "River";
+ public const string UrbanConcrete = "UrbanConcrete";
+
+ public readonly string Name = name;
+ public readonly TerrainType Type = type;
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/Assets/TerrainAssetSource.cs b/src/Hexecs.Benchmarks.City/Terrains/Assets/TerrainAssetSource.cs
new file mode 100644
index 0000000..106b6b8
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/Assets/TerrainAssetSource.cs
@@ -0,0 +1,32 @@
+using Hexecs.Assets.Sources;
+using Hexecs.Benchmarks.Map.Common.Positions;
+using Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+namespace Hexecs.Benchmarks.Map.Terrains.Assets;
+
+internal sealed class TerrainAssetSource : IAssetSource
+{
+ private IAssetLoader _loader = null!;
+
+ public void Load(IAssetLoader loader)
+ {
+ _loader = loader;
+
+ Create(TerrainAsset.Ground, "Земля", TerrainType.Ground)
+ .WithPosition();
+
+ Create(TerrainAsset.River, "Река", TerrainType.WaterRiver)
+ .WithPosition();
+
+ Create(TerrainAsset.UrbanConcrete, "Бетон", TerrainType.UrbanConcrete)
+ .WithPosition();
+ }
+
+ private AssetConfigurator Create(
+ string alias,
+ string name,
+ TerrainType type)
+ {
+ return _loader.CreateAsset(alias, new TerrainAsset(name, type));
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainCommand.cs b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainCommand.cs
new file mode 100644
index 0000000..5b5a57a
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainCommand.cs
@@ -0,0 +1,5 @@
+using Hexecs.Pipelines;
+
+namespace Hexecs.Benchmarks.Map.Terrains.Commands.Generate;
+
+public readonly struct GenerateTerrainCommand : ICommand;
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs
new file mode 100644
index 0000000..39f3d6b
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/Commands/Generate/GenerateTerrainHandler.cs
@@ -0,0 +1,47 @@
+using Hexecs.Actors.Pipelines;
+using Hexecs.Benchmarks.Map.Terrains.Assets;
+using Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+using Hexecs.Pipelines;
+
+namespace Hexecs.Benchmarks.Map.Terrains.Commands.Generate;
+
+internal sealed class GenerateTerrainHandler : ActorCommandHandler
+{
+ private readonly TerrainSettings _settings;
+
+ public GenerateTerrainHandler(ActorContext context, TerrainSettings settings) : base(context)
+ {
+ _settings = settings;
+ }
+
+ public override Result Handle(in GenerateTerrainCommand terrainCommand)
+ {
+ var ground = Assets.GetAsset(TerrainAsset.Ground);
+ var river = Assets.GetAsset(TerrainAsset.River);
+ var urbanConcrete = Assets.GetAsset(TerrainAsset.UrbanConcrete);
+
+ var height = _settings.Height;
+ var width = _settings.Width;
+
+ for (var y = 0; y < height; y++)
+ {
+ for (var x = 0; x < width; x++)
+ {
+ var args = Args.Rent(nameof(Point), new Point(x, y));
+ var actor = x switch
+ {
+ // river
+ > 45 and < 55 => Context.BuildActor(river, args
+ .Set(nameof(Terrain.Elevation), Elevation.FromValue(-10))
+ .Set(nameof(Terrain.Moisture), Moisture.FromValue(35))),
+ // urban concrete
+ < 10 when y < 10 => Context.BuildActor(urbanConcrete, args),
+ // just ground
+ _ => Context.BuildActor(ground, args)
+ };
+ }
+ }
+
+ return Result.Ok();
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/Terrain.cs b/src/Hexecs.Benchmarks.City/Terrains/Terrain.cs
new file mode 100644
index 0000000..b8c0f30
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/Terrain.cs
@@ -0,0 +1,31 @@
+using Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+namespace Hexecs.Benchmarks.Map.Terrains;
+
+public struct Terrain : IActorComponent
+{
+ ///
+ /// Высота (100 - уровень моря, 150 - холм, 250 - гора)
+ ///
+ public Elevation Elevation;
+
+ ///
+ /// Влажность или загрязнение (100 - это 0)
+ ///
+ public Moisture Moisture;
+
+ ///
+ /// Покрытие
+ ///
+ public TerrainOverlay Overlay;
+
+ ///
+ /// Температура (100 - это 0)
+ ///
+ public Temperature Temperature;
+
+ ///
+ /// Основной тип
+ ///
+ public TerrainType Type;
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/TerrainBuilder.cs b/src/Hexecs.Benchmarks.City/Terrains/TerrainBuilder.cs
new file mode 100644
index 0000000..24ff904
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/TerrainBuilder.cs
@@ -0,0 +1,21 @@
+using Hexecs.Benchmarks.Map.Terrains.Assets;
+using Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+namespace Hexecs.Benchmarks.Map.Terrains;
+
+internal sealed class TerrainBuilder : IActorBuilder
+{
+ public void Build(in Actor actor, in AssetRef asset, Args args)
+ {
+ ref readonly var assetData = ref asset.Component1;
+
+ actor.Add(new Terrain
+ {
+ Elevation = args.GetOrDefault(nameof(Terrain.Elevation), Elevation.Default),
+ Moisture = args.GetOrDefault(nameof(Terrain.Moisture), Moisture.Default),
+ Overlay = TerrainOverlay.None,
+ Temperature = args.GetOrDefault(nameof(Terrain.Temperature), Temperature.Default),
+ Type = assetData.Type
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/TerrainDrawSystem.cs b/src/Hexecs.Benchmarks.City/Terrains/TerrainDrawSystem.cs
new file mode 100644
index 0000000..e763f7e
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/TerrainDrawSystem.cs
@@ -0,0 +1,58 @@
+using Hexecs.Actors.Systems;
+using Hexecs.Benchmarks.Map.Common.Positions;
+using Hexecs.Benchmarks.Map.Common.Visibles;
+using Hexecs.Benchmarks.Map.Utils;
+using Hexecs.Benchmarks.Map.Utils.Sprites;
+using Hexecs.Worlds;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Hexecs.Benchmarks.Map.Terrains;
+
+internal sealed class TerrainDrawSystem : DrawSystem
+{
+ private readonly Camera _camera;
+ private readonly TerrainSpriteAtlas _spriteAtlas;
+ private readonly SpriteBatch _spriteBatch;
+
+ public TerrainDrawSystem(
+ Camera camera,
+ ActorContext context,
+ GraphicsDevice graphicsDevice,
+ TerrainSpriteAtlas spriteAtlas)
+ : base(context, constraint => constraint.Include())
+ {
+ _camera = camera;
+ _spriteAtlas = spriteAtlas;
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ protected override bool BeforeDraw(in WorldTime time)
+ {
+ _spriteBatch.Begin(
+ transformMatrix: _camera.TransformationMatrix,
+ samplerState: SamplerState.PointClamp,
+ blendState: BlendState.AlphaBlend);
+
+ return true;
+ }
+
+ protected override void Draw(in ActorRef actor, in WorldTime time)
+ {
+ ref readonly var terrain = ref actor.Component2;
+ ref readonly var texture = ref _spriteAtlas.GetSprite(in terrain);
+
+ ref readonly var worldPosition = ref actor.Component1.World;
+ texture.Draw(_spriteBatch, new Vector2(worldPosition.X, worldPosition.Y));
+ }
+
+ protected override void AfterDraw(in WorldTime time)
+ {
+ _spriteBatch.End();
+ }
+
+ public override void Dispose()
+ {
+ _spriteBatch.Dispose();
+ base.Dispose();
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/TerrainInstaller.cs b/src/Hexecs.Benchmarks.City/Terrains/TerrainInstaller.cs
new file mode 100644
index 0000000..a692019
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/TerrainInstaller.cs
@@ -0,0 +1,48 @@
+using Hexecs.Benchmarks.Map.Terrains.Assets;
+using Hexecs.Benchmarks.Map.Terrains.Commands.Generate;
+using Hexecs.Configurations;
+using Hexecs.Dependencies;
+using Hexecs.Worlds;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Hexecs.Benchmarks.Map.Terrains;
+
+internal static class TerrainInstaller
+{
+ public static ActorContextBuilder AddTerrain(this ActorContextBuilder builder)
+ {
+ var terrainSettings = builder.World.GetRequiredService();
+
+ builder.CreateBuilder();
+
+ builder
+ .ConfigureComponentPool(terrain => terrain
+ .Capacity(terrainSettings.Width * terrainSettings.Height));
+
+ builder.CreateCommandHandler();
+
+ builder.CreateDrawSystem();
+
+ return builder;
+ }
+
+ public static WorldBuilder UseTerrain(this WorldBuilder builder)
+ {
+ builder
+ .UseAddAssetSource(new TerrainAssetSource());
+
+ builder
+ .UseSingleton(ctx => new TerrainSpriteAtlas(
+ contentManager: ctx.GetRequiredService(),
+ fileName: "terrain_atlas",
+ settings: ctx.GetRequiredService()));
+
+ builder
+ .UseSingleton(ctx => ctx
+ .GetService()?
+ .GetValue(TerrainSettings.Key) ?? TerrainSettings.Default);
+
+ return builder;
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/TerrainSettings.cs b/src/Hexecs.Benchmarks.City/Terrains/TerrainSettings.cs
new file mode 100644
index 0000000..9d4f8a3
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/TerrainSettings.cs
@@ -0,0 +1,22 @@
+namespace Hexecs.Benchmarks.Map.Terrains;
+
+public sealed class TerrainSettings
+{
+ public const string Key = "Map:Terrain";
+
+ public static readonly TerrainSettings Default = new()
+ {
+ TileSize = 16,
+ TileSpacing = 1,
+ Width = 768,
+ Height = 768
+ };
+
+ public int TileSize { get; init; }
+
+ public int TileSpacing { get; init; }
+
+ public int Width { get; init; }
+
+ public int Height { get; init; }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/TerrainSpriteAtlas.cs b/src/Hexecs.Benchmarks.City/Terrains/TerrainSpriteAtlas.cs
new file mode 100644
index 0000000..d5a6998
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/TerrainSpriteAtlas.cs
@@ -0,0 +1,37 @@
+using Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+using Hexecs.Benchmarks.Map.Utils.Sprites;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Hexecs.Benchmarks.Map.Terrains;
+
+internal sealed class TerrainSpriteAtlas : SpriteAtlas
+{
+ public TerrainSpriteAtlas(ContentManager contentManager, string fileName, TerrainSettings settings)
+ : base(contentManager, fileName, settings.TileSize, settings.TileSpacing)
+ {
+ }
+
+ protected override AtlasKey CreateKey(in Terrain key)
+ {
+ var type = key.Type;
+
+ var column = type switch
+ {
+ TerrainType.Ground => 6,
+ TerrainType.WaterRiver => 3,
+ TerrainType.UrbanConcrete => 7,
+ _ => 1
+ };
+
+ var row = type switch
+ {
+ TerrainType.Ground => 0,
+ TerrainType.WaterRiver => 1,
+ TerrainType.UrbanConcrete => 0,
+ _ => 1
+ };
+
+ return new AtlasKey(column, row);
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Elevation.cs b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Elevation.cs
new file mode 100644
index 0000000..90d99d1
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Elevation.cs
@@ -0,0 +1,57 @@
+using System.Runtime.CompilerServices;
+
+namespace Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+public readonly struct Elevation
+{
+ public static Elevation Default => FromValue(10);
+
+ public static Elevation FromValue(int value)
+ {
+ var raw = (byte)Math.Clamp(value + Offset, 0, 255);
+ return new Elevation(raw);
+ }
+
+ public static Elevation SeaLevel
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new(Offset);
+ }
+
+ private const byte Offset = 100; // sea level
+ private readonly byte _raw;
+
+ private Elevation(byte raw) => _raw = raw;
+
+ public int Value
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _raw - Offset;
+ }
+
+ ///
+ /// Находится ли уровень ниже базового (уровня моря).
+ ///
+ public bool IsBelowSeaLevel
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _raw < 100;
+ }
+
+ ///
+ /// Является ли это возвышенностью (холмом).
+ ///
+ public bool IsHighland
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _raw > 150;
+ }
+
+ public bool IsSeaLevel
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _raw == Offset;
+ }
+
+ public override string ToString() => $"{Value}m";
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Moisture.cs b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Moisture.cs
new file mode 100644
index 0000000..80b3752
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Moisture.cs
@@ -0,0 +1,43 @@
+using System.Diagnostics;
+using System.Runtime.CompilerServices;
+
+namespace Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+[DebuggerDisplay("{Value:F2}%")]
+public readonly struct Moisture
+{
+ public static Moisture Default
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => new(Offset);
+ }
+
+ public static Moisture FromValue(float value)
+ {
+ var raw = (byte)Math.Clamp(value + Offset, 0, 255);
+ return new Moisture(raw);
+ }
+
+ private const byte Offset = 100;
+ private readonly byte _raw;
+
+ private Moisture(byte raw) => _raw = raw;
+
+ public float Value
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _raw - Offset;
+ }
+
+ public bool IsDry
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Value < -50;
+ }
+
+ public bool IsSoaked
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Value > 50;
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Temperature.cs b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Temperature.cs
new file mode 100644
index 0000000..a2fde28
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/Temperature.cs
@@ -0,0 +1,39 @@
+using System.Runtime.CompilerServices;
+
+namespace Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+public readonly struct Temperature
+{
+ public static Temperature Default => FromCelsius(20);
+
+ public static Temperature FromCelsius(byte celsius)
+ {
+ var raw = (byte)Math.Clamp(celsius + Offset, 0, 255);
+ return new Temperature(raw);
+ }
+
+ private const byte Offset = 100;
+ private readonly byte _raw;
+
+ private Temperature(byte raw) => _raw = raw;
+
+ public float Celsius
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _raw - Offset;
+ }
+
+ public bool IsFreezing
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Celsius <= 0;
+ }
+
+ public bool IsBoiling
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => Celsius >= 100;
+ }
+
+ public override string ToString() => $"{Celsius}°C";
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/TerrainOverlay.cs b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/TerrainOverlay.cs
new file mode 100644
index 0000000..7ff7566
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/TerrainOverlay.cs
@@ -0,0 +1,62 @@
+namespace Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+public enum TerrainOverlay : byte
+{
+ None = 0,
+
+ // --- Природные состояния (1-19) ---
+ ///
+ /// Снежный покров.
+ ///
+ Snow = 1,
+ ///
+ /// Тонкий слой льда (на воде или на дороге).
+ ///
+ Ice = 2,
+ ///
+ /// Лужи или затопление после дождя.
+ ///
+ Puddles = 3,
+
+ // --- Растительность (20-39) ---
+ ///
+ /// Камыш или водные растения.
+ ///
+ Reeds = 20,
+ ///
+ /// Дикие кустарники или густая трава.
+ ///
+ Bushes = 21,
+ ///
+ /// Мох или лишайник (на камнях/бетоне).
+ ///
+ Moss = 22,
+ ///
+ /// Опавшие листья (городской декор).
+ ///
+ DeadLeaves = 23,
+
+ // --- Городские и техногенные эффекты (40-59) ---
+ ///
+ /// Мусор или строительные обломки (например, после сноса).
+ ///
+ Debris = 40,
+ ///
+ /// Пятна масла, топлива или химикатов.
+ ///
+ PollutionSpill = 41,
+ ///
+ /// Следы износа или трещины на асфальте.
+ ///
+ Cracks = 42,
+
+ // --- Следы событий (60-79) ---
+ ///
+ /// Следы гари после пожара.
+ ///
+ BurnMarks = 60,
+ ///
+ /// Кровь или следы происшествий.
+ ///
+ Blood = 61
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/TerrainType.cs b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/TerrainType.cs
new file mode 100644
index 0000000..e14165d
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Terrains/ValueTypes/TerrainType.cs
@@ -0,0 +1,67 @@
+namespace Hexecs.Benchmarks.Map.Terrains.ValueTypes;
+
+// ReSharper disable ConvertToExtensionBlock
+public enum TerrainType : byte
+{
+ None = 0,
+
+ // Природная земля (1-19)
+ Ground = 1, // пустая земля (например, сняли дёрн)
+ GroundGrass = 2,
+ GroundClay = 3,
+ GroundSand = 4,
+ GroundDirt = 5,
+
+ // Подготовленная городская почва (20-39)
+ UrbanGravel = 20, // Гравийная засыпка
+ UrbanPavement = 21, // Мощение
+ UrbanConcrete = 22, // Бетонное основание
+
+ // Камни и минералы (40-59)
+ Rock = 40,
+
+ // Болота (60-79)
+ Swamp = 60, // Глубокое болото (не проходимое)
+
+ // Горы (вертикальные препятствия) (80-99)
+ Mountains = 80,
+ Cliff = 81, // Утёс, резкий перепад высоты
+
+ // Вода (100-119)
+ WaterShallow = 100,
+ WaterRiver = 101, // (не проходимое)
+ WaterOcean = 102, // (не проходимое)
+}
+
+public static class TerrainTypeExtensions
+{
+ public static bool IsGround(this TerrainType type)
+ {
+ return type is >= TerrainType.GroundGrass and < TerrainType.UrbanGravel;
+ }
+
+ public static bool IsUrban(this TerrainType type) => type is >= TerrainType.UrbanGravel and < TerrainType.Rock;
+
+ public static bool IsRock(this TerrainType type) => type is >= TerrainType.Rock and < TerrainType.Swamp;
+
+ public static bool IsSwamp(this TerrainType type) => type is >= TerrainType.Swamp and < TerrainType.Mountains;
+
+ public static bool IsElevationObstacle(this TerrainType type)
+ {
+ return type is >= TerrainType.Mountains and < TerrainType.WaterShallow;
+ }
+
+ public static bool IsWater(this TerrainType type) => type >= TerrainType.WaterShallow;
+
+ ///
+ /// Проверка на проходимость для пеших юнитов.
+ ///
+ public static bool IsWalkable(this TerrainType type) => type switch
+ {
+ TerrainType.Swamp => false,
+ TerrainType.Mountains => false,
+ TerrainType.WaterRiver => false,
+ TerrainType.WaterOcean => false,
+ _ => true
+ };
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Usings.cs b/src/Hexecs.Benchmarks.City/Usings.cs
new file mode 100644
index 0000000..28f20c2
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Usings.cs
@@ -0,0 +1,6 @@
+// Global using directives
+
+global using Hexecs.Actors;
+global using Hexecs.Assets;
+global using Hexecs.Utils;
+global using Microsoft.Xna.Framework;
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Utils/Camera.cs b/src/Hexecs.Benchmarks.City/Utils/Camera.cs
new file mode 100644
index 0000000..949742f
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Utils/Camera.cs
@@ -0,0 +1,168 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework.Graphics;
+using Microsoft.Xna.Framework.Input;
+
+namespace Hexecs.Benchmarks.Map.Utils;
+
+internal sealed class Camera(GraphicsDevice graphicsDevice)
+{
+ ///
+ /// Позиция камеры в мировых координатах (центр экрана).
+ ///
+ public ref readonly Vector2 Position
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref _currentPosition;
+ }
+
+ ///
+ /// Матрица трансформации, учитывая позицию, зум и размер экрана.
+ ///
+ public ref readonly Matrix TransformationMatrix
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref _currentTransform;
+ }
+
+ ///
+ /// Viewport of world boundary
+ ///
+ public ref readonly CameraViewport Viewport
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref _currentViewport;
+ }
+
+ ///
+ /// Текущий масштаб камеры (1.0 = без изменений).
+ ///
+ public float Zoom
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _currentZoom;
+ }
+
+ private Vector2 _currentPosition;
+ private Matrix _currentTransform;
+ private CameraViewport _currentViewport;
+ private float _currentZoom = 1f;
+
+ private Vector2 _previousPosition;
+ private float _previousZoom;
+ private int _previousScrollValue;
+
+ ///
+ /// Изменяет текущий зум на множитель и ограничивает его допустимым диапазоном.
+ ///
+ /// Множитель масштаба (больше 1 для приближения, меньше 1 для отдаления).
+ public void AdjustZoom(float factor)
+ {
+ if (factor > 0) _currentZoom += 1f;
+ else _currentZoom -= 1f;
+
+ _currentZoom = MathHelper.Clamp(_currentZoom, 1f, 10f);
+ }
+
+ ///
+ /// Смещает камеру на указанный вектор.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Move(Vector2 direction) => _currentPosition += direction;
+
+ ///
+ /// Обрабатывает ввод игрока для перемещения и масштабирования камеры.
+ ///
+ public void Update(GameTime gameTime)
+ {
+ var keyboard = Keyboard.GetState();
+ var mouse = Mouse.GetState();
+
+ var dt = (float)gameTime.ElapsedGameTime.TotalSeconds;
+
+ // Базовое управление камерой
+ var speed = 500f / _currentZoom;
+ var moveDir = Vector2.Zero;
+
+ if (keyboard.IsKeyDown(Keys.W)) moveDir.Y -= 1;
+ if (keyboard.IsKeyDown(Keys.S)) moveDir.Y += 1;
+ if (keyboard.IsKeyDown(Keys.A)) moveDir.X -= 1;
+ if (keyboard.IsKeyDown(Keys.D)) moveDir.X += 1;
+
+ if (moveDir != Vector2.Zero)
+ {
+ moveDir.Normalize();
+ Move(moveDir * speed * dt);
+ }
+
+ var scrollDelta = mouse.ScrollWheelValue - _previousScrollValue;
+ if (scrollDelta != 0)
+ {
+ AdjustZoom(scrollDelta);
+ }
+
+ _previousScrollValue = mouse.ScrollWheelValue;
+
+ if (_currentPosition != _previousPosition || Math.Abs(_currentZoom - _previousZoom) > float.Epsilon)
+ {
+ UpdateTransformationMatrix();
+ UpdateViewportBoundary();
+
+ _previousPosition = _currentPosition;
+ _previousZoom = _currentZoom;
+ }
+
+ UpdateTransformationMatrix();
+ UpdateViewportBoundary();
+ }
+
+ ///
+ /// Переводит экранные координаты (например, позицию мыши) в мировые координаты игрового поля.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector2 ScreenToWorld(Vector2 screenPosition)
+ {
+ return Vector2.Transform(screenPosition, Matrix.Invert(_currentTransform));
+ }
+
+ ///
+ /// Переводит мировые координаты в координаты экрана (например, для отрисовки UI над объектами).
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Vector2 WorldToScreen(Vector2 worldPosition)
+ {
+ return Vector2.Transform(worldPosition, _currentTransform);
+ }
+
+ private void UpdateTransformationMatrix()
+ {
+ var viewport = graphicsDevice.Viewport;
+ var zoom = MathF.Round(_currentZoom);
+
+ var roundedPosition = new Vector2(
+ MathF.Round(_currentPosition.X),
+ MathF.Round(_currentPosition.Y)
+ );
+
+ _currentTransform = Matrix.CreateTranslation(new Vector3(-roundedPosition.X, -roundedPosition.Y, 0)) *
+ Matrix.CreateScale(new Vector3(zoom, zoom, 1)) *
+ Matrix.CreateTranslation(new Vector3(viewport.Width * 0.5f, viewport.Height * 0.5f, 0));
+ }
+
+ private void UpdateViewportBoundary()
+ {
+ var deviceViewport = graphicsDevice.Viewport;
+ var topLeft = ScreenToWorld(Vector2.Zero);
+ var bottomRight = ScreenToWorld(new Vector2(deviceViewport.Width, deviceViewport.Height));
+
+ var width = (int)MathF.Ceiling(bottomRight.X - topLeft.X);
+ var height = (int)MathF.Ceiling(bottomRight.Y - topLeft.Y);
+ var x = (int)topLeft.X;
+ var y = (int)topLeft.Y;
+
+ ref var viewport = ref _currentViewport;
+ viewport.Left = x;
+ viewport.Right = x + width;
+ viewport.Top = y;
+ viewport.Bottom = y + height;
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Utils/CameraViewport.cs b/src/Hexecs.Benchmarks.City/Utils/CameraViewport.cs
new file mode 100644
index 0000000..086648a
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Utils/CameraViewport.cs
@@ -0,0 +1,37 @@
+using System.Runtime.CompilerServices;
+
+namespace Hexecs.Benchmarks.Map.Utils;
+
+public struct CameraViewport : IEquatable
+{
+ public int Left;
+ public int Right;
+ public int Top;
+ public int Bottom;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Hidden(int x, int y, int width, int height) => !Visible(x, y, width, height);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Visible(int x, int y, int width, int height) =>
+ x < Right &&
+ Left < x + width
+ && y < Bottom &&
+ Top < y + height;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool Equals(CameraViewport other) => Left == other.Left &&
+ Right == other.Right &&
+ Top == other.Top &&
+ Bottom == other.Bottom;
+
+ public override bool Equals(object? obj) => obj is CameraViewport other && Equals(other);
+
+ public override int GetHashCode() => HashCode.Combine(Left, Right, Top, Bottom);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator ==(in CameraViewport left, in CameraViewport right) => left.Equals(right);
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool operator !=(in CameraViewport left, in CameraViewport right) => !left.Equals(right);
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Utils/PointExtensions.cs b/src/Hexecs.Benchmarks.City/Utils/PointExtensions.cs
new file mode 100644
index 0000000..77958bc
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Utils/PointExtensions.cs
@@ -0,0 +1,12 @@
+namespace Hexecs.Benchmarks.Map.Utils;
+
+public static class PointExtensions
+{
+ public static void GetNeighborPoints(int x, int y, ref Span neighbors)
+ {
+ neighbors[0] = new Point(x - 1, y);
+ neighbors[1] = new Point(x + 1, y);
+ neighbors[2] = new Point(x, y - 1);
+ neighbors[3] = new Point(x, y + 1);
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Utils/Sprites/Sprite.cs b/src/Hexecs.Benchmarks.City/Utils/Sprites/Sprite.cs
new file mode 100644
index 0000000..67958d5
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Utils/Sprites/Sprite.cs
@@ -0,0 +1,15 @@
+using System.Runtime.CompilerServices;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Hexecs.Benchmarks.Map.Utils.Sprites;
+
+[method: MethodImpl(MethodImplOptions.AggressiveInlining)]
+internal readonly struct Sprite(Texture2D texture, Rectangle region)
+{
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void Draw(SpriteBatch spriteBatch, Vector2 position) => spriteBatch.Draw(
+ texture,
+ position,
+ region,
+ Color.White);
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.City/Utils/Sprites/SpriteAtlas.cs b/src/Hexecs.Benchmarks.City/Utils/Sprites/SpriteAtlas.cs
new file mode 100644
index 0000000..30a8327
--- /dev/null
+++ b/src/Hexecs.Benchmarks.City/Utils/Sprites/SpriteAtlas.cs
@@ -0,0 +1,63 @@
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Hexecs.Benchmarks.Map.Utils.Sprites;
+
+internal abstract class SpriteAtlas : IDisposable
+ where TKey : struct
+{
+ private readonly Dictionary _sprites = new();
+ private readonly Texture2D _texture;
+
+ private readonly int _tileSize;
+ private readonly int _tileSpacing;
+
+ private bool _disposed;
+
+ protected SpriteAtlas(ContentManager contentManager, string fileName, int tileSize, int tileSpacing)
+ {
+ _texture = contentManager.Load(fileName);
+ _tileSize = tileSize;
+ _tileSpacing = tileSpacing;
+ }
+
+ public ref Sprite GetSprite(in TKey key)
+ {
+ ObjectDisposedException.ThrowIf(_disposed, this);
+
+ var atlasKey = CreateKey(in key);
+ ref var value = ref CollectionsMarshal.GetValueRefOrAddDefault(_sprites, key, out var exists);
+ if (exists)
+ {
+ return ref value;
+ }
+
+ value = CreateSprite(atlasKey.Column, atlasKey.Row);
+ return ref value;
+ }
+
+ protected abstract AtlasKey CreateKey(in TKey key);
+
+ private Sprite CreateSprite(int column, int row)
+ {
+ var x = column * (_tileSize + _tileSpacing);
+ var y = row * (_tileSize + _tileSpacing);
+ var sourceRect = new Rectangle(x, y, _tileSize, _tileSize);
+
+ return new Sprite(_texture, sourceRect);
+ }
+
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ _sprites.Clear();
+ _texture.Dispose();
+ }
+
+ [method: MethodImpl(MethodImplOptions.AggressiveInlining)]
+ protected readonly record struct AtlasKey(int Column, int Row);
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.MonoGame/Program.cs b/src/Hexecs.Benchmarks.MonoGame/Program.cs
deleted file mode 100644
index 51ac21d..0000000
--- a/src/Hexecs.Benchmarks.MonoGame/Program.cs
+++ /dev/null
@@ -1,4 +0,0 @@
-using Hexecs.Benchmarks.MonoGame;
-
-using var game = new BenchmarkGame();
-game.Run();
diff --git a/src/Hexecs.Benchmarks.MonoGame/Usings.cs b/src/Hexecs.Benchmarks.MonoGame/Usings.cs
deleted file mode 100644
index e12d09f..0000000
--- a/src/Hexecs.Benchmarks.MonoGame/Usings.cs
+++ /dev/null
@@ -1,3 +0,0 @@
-// Global using directives
-
-global using System.Runtime.CompilerServices;
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.Noise/BenchmarkCounter.cs b/src/Hexecs.Benchmarks.Noise/BenchmarkCounter.cs
new file mode 100644
index 0000000..9413e83
--- /dev/null
+++ b/src/Hexecs.Benchmarks.Noise/BenchmarkCounter.cs
@@ -0,0 +1,89 @@
+using System.Globalization;
+using System.Text;
+using Microsoft.Xna.Framework.Content;
+using Microsoft.Xna.Framework.Graphics;
+
+namespace Hexecs.Benchmarks.Noise;
+
+internal sealed class BenchmarkCounter
+{
+ private readonly Func _countResolver;
+ private readonly int[] _fpsHistory;
+
+ private double _frameTime;
+ private int _fps;
+ private int _frameCount;
+ private double _fpsTimer;
+
+ private int _historyIndex;
+ private bool _historyFull;
+ private double _avgFps;
+ private long _historySum;
+
+ private readonly SpriteFont _font;
+ private readonly SpriteBatch _spriteBatch;
+
+ // Используем StringBuilder как буфер
+ private readonly StringBuilder _stringBuilder = new(128);
+ private readonly Vector2 _textPos = new(10, 10);
+ private readonly Vector2 _shadowPos = new(11, 11);
+
+ public BenchmarkCounter(Func countResolver, ContentManager contentManager, GraphicsDevice graphicsDevice)
+ {
+ _countResolver = countResolver;
+ _fpsHistory = new int[60];
+ _font = contentManager.Load("DebugFont");
+ _spriteBatch = new SpriteBatch(graphicsDevice);
+ }
+
+ public void Draw(GameTime gameTime)
+ {
+ _frameCount++;
+
+ _spriteBatch.Begin();
+
+ _spriteBatch.DrawString(_font, _stringBuilder, _shadowPos, Color.Black);
+ _spriteBatch.DrawString(_font, _stringBuilder, _textPos, Color.Yellow);
+
+ _spriteBatch.End();
+ }
+
+ public void Update(GameTime gameTime)
+ {
+ var elapsedSeconds = gameTime.ElapsedGameTime.TotalSeconds;
+ _frameTime = gameTime.ElapsedGameTime.TotalMilliseconds;
+ _fpsTimer += elapsedSeconds;
+
+ if (_fpsTimer >= 1.0)
+ {
+ _fps = _frameCount;
+
+ _historySum -= _fpsHistory[_historyIndex];
+ _fpsHistory[_historyIndex] = _fps;
+ _historySum += _fps;
+
+ _historyIndex = (_historyIndex + 1) % 60;
+ if (_historyIndex == 0) _historyFull = true;
+
+ var historyCount = _historyFull ? 60 : _historyIndex;
+ _avgFps = (double)_historySum / historyCount;
+
+ var alloc = GC.GetTotalMemory(false) / 1024.0 / 1024.0;
+ var count = _countResolver();
+
+ // Очищаем буфер и записываем новые данные без создания строк
+ var culture = CultureInfo.InvariantCulture;
+
+ _stringBuilder.Clear();
+ _stringBuilder
+ .Append($"{_fps} FPS")
+ .Append(culture, $" | Avg:{_avgFps:F1} fps")
+ .Append(culture, $" | Entities:{count:N0}")
+ .Append(culture, $" | Frame time:{_frameTime:F1}ms")
+ .Append(culture, $" | Alloc:{alloc:F3}mb");
+
+ _frameCount = 0;
+ _fpsTimer = 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.MonoGame/Components/CircleColor.cs b/src/Hexecs.Benchmarks.Noise/Components/CircleColor.cs
similarity index 87%
rename from src/Hexecs.Benchmarks.MonoGame/Components/CircleColor.cs
rename to src/Hexecs.Benchmarks.Noise/Components/CircleColor.cs
index 8b132ec..69190a8 100644
--- a/src/Hexecs.Benchmarks.MonoGame/Components/CircleColor.cs
+++ b/src/Hexecs.Benchmarks.Noise/Components/CircleColor.cs
@@ -1,7 +1,6 @@
using Hexecs.Actors;
-using Microsoft.Xna.Framework;
-namespace Hexecs.Benchmarks.MonoGame.Components;
+namespace Hexecs.Benchmarks.Noise.Components;
public readonly struct CircleColor(Color value) : IActorComponent
{
diff --git a/src/Hexecs.Benchmarks.MonoGame/Components/Position.cs b/src/Hexecs.Benchmarks.Noise/Components/Position.cs
similarity index 80%
rename from src/Hexecs.Benchmarks.MonoGame/Components/Position.cs
rename to src/Hexecs.Benchmarks.Noise/Components/Position.cs
index 833c12e..e7bbd0a 100644
--- a/src/Hexecs.Benchmarks.MonoGame/Components/Position.cs
+++ b/src/Hexecs.Benchmarks.Noise/Components/Position.cs
@@ -1,7 +1,6 @@
using Hexecs.Actors;
-using Microsoft.Xna.Framework;
-namespace Hexecs.Benchmarks.MonoGame.Components;
+namespace Hexecs.Benchmarks.Noise.Components;
public struct Position(Vector2 value) : IActorComponent
{
diff --git a/src/Hexecs.Benchmarks.MonoGame/Components/Velocity.cs b/src/Hexecs.Benchmarks.Noise/Components/Velocity.cs
similarity index 77%
rename from src/Hexecs.Benchmarks.MonoGame/Components/Velocity.cs
rename to src/Hexecs.Benchmarks.Noise/Components/Velocity.cs
index 9f645a6..3e2d743 100644
--- a/src/Hexecs.Benchmarks.MonoGame/Components/Velocity.cs
+++ b/src/Hexecs.Benchmarks.Noise/Components/Velocity.cs
@@ -1,7 +1,6 @@
using Hexecs.Actors;
-using Microsoft.Xna.Framework;
-namespace Hexecs.Benchmarks.MonoGame.Components;
+namespace Hexecs.Benchmarks.Noise.Components;
public struct Velocity(Vector2 value) : IActorComponent
{
diff --git a/src/Hexecs.Benchmarks.Noise/Content/DebugFont.spritefont b/src/Hexecs.Benchmarks.Noise/Content/DebugFont.spritefont
new file mode 100644
index 0000000..ba84b52
--- /dev/null
+++ b/src/Hexecs.Benchmarks.Noise/Content/DebugFont.spritefont
@@ -0,0 +1,16 @@
+
+
+
+ Consolas
+ 10
+ 0
+ true
+
+
+
+
+ ~
+
+
+
+
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.Noise/Content/DebugFont.xnb b/src/Hexecs.Benchmarks.Noise/Content/DebugFont.xnb
new file mode 100644
index 0000000..47451f4
Binary files /dev/null and b/src/Hexecs.Benchmarks.Noise/Content/DebugFont.xnb differ
diff --git a/src/Hexecs.Benchmarks.MonoGame/Content/Instancing.fx b/src/Hexecs.Benchmarks.Noise/Content/Instancing.fx
similarity index 100%
rename from src/Hexecs.Benchmarks.MonoGame/Content/Instancing.fx
rename to src/Hexecs.Benchmarks.Noise/Content/Instancing.fx
diff --git a/src/Hexecs.Benchmarks.MonoGame/Content/Instancing.mgfx b/src/Hexecs.Benchmarks.Noise/Content/Instancing.mgfx
similarity index 100%
rename from src/Hexecs.Benchmarks.MonoGame/Content/Instancing.mgfx
rename to src/Hexecs.Benchmarks.Noise/Content/Instancing.mgfx
diff --git a/src/Hexecs.Benchmarks.MonoGame/Hexecs.Benchmarks.MonoGame.csproj b/src/Hexecs.Benchmarks.Noise/Hexecs.Benchmarks.Noise.csproj
similarity index 51%
rename from src/Hexecs.Benchmarks.MonoGame/Hexecs.Benchmarks.MonoGame.csproj
rename to src/Hexecs.Benchmarks.Noise/Hexecs.Benchmarks.Noise.csproj
index f5b53ac..80b5a75 100644
--- a/src/Hexecs.Benchmarks.MonoGame/Hexecs.Benchmarks.MonoGame.csproj
+++ b/src/Hexecs.Benchmarks.Noise/Hexecs.Benchmarks.Noise.csproj
@@ -7,8 +7,14 @@
true
+
+ Speed
+ true
+ link
+
+
-
+
@@ -16,13 +22,16 @@
-
- PreserveNewest
-
+
+ PreserveNewest
+
+
+ PreserveNewest
+
-
+
diff --git a/src/Hexecs.Benchmarks.MonoGame/BenchmarkGame.cs b/src/Hexecs.Benchmarks.Noise/NoiseGame.cs
similarity index 55%
rename from src/Hexecs.Benchmarks.MonoGame/BenchmarkGame.cs
rename to src/Hexecs.Benchmarks.Noise/NoiseGame.cs
index 3f5c73f..d7db849 100644
--- a/src/Hexecs.Benchmarks.MonoGame/BenchmarkGame.cs
+++ b/src/Hexecs.Benchmarks.Noise/NoiseGame.cs
@@ -1,50 +1,36 @@
using Hexecs.Actors;
-using Hexecs.Benchmarks.MonoGame.Components;
-using Hexecs.Benchmarks.MonoGame.Systems;
+using Hexecs.Benchmarks.Noise.Components;
+using Hexecs.Benchmarks.Noise.Systems;
using Hexecs.Dependencies;
using Hexecs.Threading;
using Hexecs.Worlds;
-using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
-namespace Hexecs.Benchmarks.MonoGame;
+namespace Hexecs.Benchmarks.Noise;
-public class BenchmarkGame : Game
+public class NoiseGame : Game
{
- private ActorFilter? _entitiesCountFilter;
- private readonly GraphicsDeviceManager _graphics;
- private World _world = null!;
+ private BenchmarkCounter _benchmarkCounter = null!;
private ActorContext _context = null!;
+ private readonly GraphicsDeviceManager _graphics;
private readonly Random _random = new();
-
- // Поля для статистики
- private double _frameTime;
- private int _fps;
- private int _frameCount;
- private double _fpsTimer;
- private int _secondsCounter;
-
- // Для среднего значения за минуту (Rolling Average)
- private readonly int[] _fpsHistory = new int[60];
- private int _historyIndex;
- private bool _historyFull;
- private double _avgFps;
+ private World _world = null!;
private const int InitialEntityCount = 2_000_000;
private const int MaxEntityCount = 3_000_000;
- public BenchmarkGame()
+ public NoiseGame()
{
_graphics = new GraphicsDeviceManager(this)
{
PreferredBackBufferWidth = 1280,
PreferredBackBufferHeight = 720,
- GraphicsProfile = GraphicsProfile.HiDef, // Используем профиль HiDef для поддержки расширенных возможностей
+ GraphicsProfile = GraphicsProfile.HiDef,
PreferMultiSampling = true,
- SynchronizeWithVerticalRetrace = false,
+ SynchronizeWithVerticalRetrace = true,
IsFullScreen = false,
- HardwareModeSwitch = false // Используем borderless fullscreen для удобства
+ HardwareModeSwitch = false
};
// Включаем поддержку сглаживания для устройства
@@ -56,6 +42,7 @@ public BenchmarkGame()
_graphics.ApplyChanges();
IsFixedTimeStep = false;
+ Content.RootDirectory = "Content";
}
protected override void Initialize()
@@ -66,8 +53,8 @@ protected override void Initialize()
var height = _graphics.PreferredBackBufferHeight;
_world = new WorldBuilder()
- .DefaultParallelWorker(Math.Min(6, Environment.ProcessorCount))
- .DefaultActorContext(builder => builder
+ .UseDefaultParallelWorker(Math.Min(6, Environment.ProcessorCount))
+ .UseDefaultActorContext(builder => builder
.Capacity(InitialEntityCount)
.ConfigureComponentPool(color => color.Capacity(InitialEntityCount))
.ConfigureComponentPool(position => position.Capacity(InitialEntityCount))
@@ -78,7 +65,7 @@ protected override void Initialize()
.Build();
_context = _world.Actors;
- _entitiesCountFilter = _context.Filter();
+ _benchmarkCounter = new BenchmarkCounter(() => _context.Length, Content, GraphicsDevice);
for (var i = 0; i < InitialEntityCount; i++)
{
@@ -93,23 +80,22 @@ private void SpawnEntity(CircleColor? color = null)
{
var actor = _context.CreateActor();
actor.Add(Position.Create(
- x: _graphics.PreferredBackBufferWidth / 2,
+ x: _graphics.PreferredBackBufferWidth / 2,
y: _graphics.PreferredBackBufferHeight / 2));
-
+
actor.Add(Velocity.Create(
x: (float)(_random.NextDouble() * 200 - 100),
y: (float)(_random.NextDouble() * 200 - 100)));
-
+
actor.Add(color ?? CircleColor.CreateRgba(_random));
}
protected override void Update(GameTime gameTime)
{
- var count = _entitiesCountFilter?.Length ?? 0;
-
var keyboard = Keyboard.GetState();
if (keyboard.IsKeyDown(Keys.Space))
{
+ var count = _context.Length;
var color = CircleColor.CreateRgba(_random);
for (var i = 0; i < 50; i++)
{
@@ -122,45 +108,9 @@ protected override void Update(GameTime gameTime)
}
}
+ _benchmarkCounter.Update(gameTime);
_world.Update(gameTime.ElapsedGameTime, gameTime.TotalGameTime);
- // Сбор статистики
- var elapsedSeconds = gameTime.ElapsedGameTime.TotalSeconds;
- _frameTime = gameTime.ElapsedGameTime.TotalMilliseconds;
- _fpsTimer += elapsedSeconds;
- _frameCount++;
-
- // Считаем FPS каждую секунду для точности истории
- if (_fpsTimer >= 1.0)
- {
- _fps = _frameCount;
-
- // Обновляем историю для Avg
- _fpsHistory[_historyIndex] = _fps;
- _historyIndex = (_historyIndex + 1) % 60;
- if (_historyIndex == 0) _historyFull = true;
-
- // Считаем среднее за минуту
- var historyCount = _historyFull ? 60 : _historyIndex;
- var sum = 0;
- for (var i = 0; i < historyCount; i++) sum += _fpsHistory[i];
- _avgFps = (double)sum / historyCount;
-
- _frameCount = 0;
- _fpsTimer -= 1.0;
- _secondsCounter++;
-
- if (_secondsCounter >= 1)
- {
- var alloc = GC.GetTotalMemory(false) / 1024.0 / 1024.0;
- count = _entitiesCountFilter?.Length ?? 0;
- Window.Title =
- $"FPS: {_fps} | Avg FPS: {_avgFps:F1} | Entities: {count:N0} | Frame Time: {_frameTime:F2}ms | Alloc: {alloc:F2}Mb";
-
- _secondsCounter = 0;
- }
- }
-
base.Update(gameTime);
}
@@ -169,6 +119,7 @@ protected override void Draw(GameTime gameTime)
GraphicsDevice.Clear(Color.White);
_world.Draw(gameTime.ElapsedGameTime, gameTime.TotalGameTime);
+ _benchmarkCounter.Draw(gameTime);
base.Draw(gameTime);
}
diff --git a/src/Hexecs.Benchmarks.Noise/Program.cs b/src/Hexecs.Benchmarks.Noise/Program.cs
new file mode 100644
index 0000000..9e6cfc0
--- /dev/null
+++ b/src/Hexecs.Benchmarks.Noise/Program.cs
@@ -0,0 +1,4 @@
+using Hexecs.Benchmarks.Noise;
+
+using var game = new NoiseGame();
+game.Run();
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.MonoGame/Systems/InstanceData.cs b/src/Hexecs.Benchmarks.Noise/Systems/InstanceData.cs
similarity index 90%
rename from src/Hexecs.Benchmarks.MonoGame/Systems/InstanceData.cs
rename to src/Hexecs.Benchmarks.Noise/Systems/InstanceData.cs
index 73c0395..b9dd6b9 100644
--- a/src/Hexecs.Benchmarks.MonoGame/Systems/InstanceData.cs
+++ b/src/Hexecs.Benchmarks.Noise/Systems/InstanceData.cs
@@ -1,8 +1,7 @@
using System.Runtime.InteropServices;
-using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
-namespace Hexecs.Benchmarks.MonoGame.Systems;
+namespace Hexecs.Benchmarks.Noise.Systems;
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct InstanceData : IVertexType
diff --git a/src/Hexecs.Benchmarks.MonoGame/Systems/MovementSystem.cs b/src/Hexecs.Benchmarks.Noise/Systems/MovementSystem.cs
similarity index 79%
rename from src/Hexecs.Benchmarks.MonoGame/Systems/MovementSystem.cs
rename to src/Hexecs.Benchmarks.Noise/Systems/MovementSystem.cs
index a81bebd..82d3117 100644
--- a/src/Hexecs.Benchmarks.MonoGame/Systems/MovementSystem.cs
+++ b/src/Hexecs.Benchmarks.Noise/Systems/MovementSystem.cs
@@ -1,11 +1,10 @@
using Hexecs.Actors;
using Hexecs.Actors.Systems;
-using Hexecs.Benchmarks.MonoGame.Components;
+using Hexecs.Benchmarks.Noise.Components;
using Hexecs.Threading;
using Hexecs.Worlds;
-using Microsoft.Xna.Framework;
-namespace Hexecs.Benchmarks.MonoGame.Systems;
+namespace Hexecs.Benchmarks.Noise.Systems;
public sealed class MovementSystem(
ActorContext context,
@@ -21,8 +20,8 @@ protected override void Update(
in ActorRef actor,
in WorldTime time)
{
- ref var pos = ref actor.Component1;
- ref var vel = ref actor.Component2;
+ var pos = actor.Component1;
+ var vel = actor.Component2;
pos.Value += vel.Value * time.DeltaTime;
@@ -36,5 +35,8 @@ protected override void Update(
{
vel.Value.Y *= -1;
}
+
+ actor.Update(pos);
+ actor.Update(vel);
}
}
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks.MonoGame/Systems/RenderSystem.cs b/src/Hexecs.Benchmarks.Noise/Systems/RenderSystem.cs
similarity index 96%
rename from src/Hexecs.Benchmarks.MonoGame/Systems/RenderSystem.cs
rename to src/Hexecs.Benchmarks.Noise/Systems/RenderSystem.cs
index 3b8bb42..206e3d3 100644
--- a/src/Hexecs.Benchmarks.MonoGame/Systems/RenderSystem.cs
+++ b/src/Hexecs.Benchmarks.Noise/Systems/RenderSystem.cs
@@ -1,11 +1,10 @@
using Hexecs.Actors;
using Hexecs.Actors.Systems;
-using Hexecs.Benchmarks.MonoGame.Components;
+using Hexecs.Benchmarks.Noise.Components;
using Hexecs.Worlds;
-using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
-namespace Hexecs.Benchmarks.MonoGame.Systems;
+namespace Hexecs.Benchmarks.Noise.Systems;
public sealed class RenderSystem : DrawSystem
{
diff --git a/src/Hexecs.Benchmarks.Noise/Usings.cs b/src/Hexecs.Benchmarks.Noise/Usings.cs
new file mode 100644
index 0000000..a1b37ef
--- /dev/null
+++ b/src/Hexecs.Benchmarks.Noise/Usings.cs
@@ -0,0 +1,4 @@
+// Global using directives
+
+global using System.Runtime.CompilerServices;
+global using Microsoft.Xna.Framework;
\ No newline at end of file
diff --git a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs
index 2954373..2e96d88 100644
--- a/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs
+++ b/src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs
@@ -70,8 +70,8 @@ public void Setup()
new DefaultEcs.Threading.DefaultParallelRunner(4));
_hexecsWorld = new WorldBuilder()
- .DefaultParallelWorker(4)
- .DefaultActorContext(ctx => ctx.CreateUpdateSystem())
+ .UseDefaultParallelWorker(4)
+ .UseDefaultActorContext(ctx => ctx.CreateUpdateSystem())
.Build();
_hexecsSystem = _hexecsWorld.Actors.GetUpdateSystem();
diff --git a/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj b/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj
index cf3e358..2cf767c 100644
--- a/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj
+++ b/src/Hexecs.Benchmarks/Hexecs.Benchmarks.csproj
@@ -1,6 +1,7 @@
+ Exe
net10.0
enable
enable
@@ -11,7 +12,7 @@
-
+
diff --git a/src/Hexecs.Benchmarks/Program.cs b/src/Hexecs.Benchmarks/Program.cs
index c1ae414..af5ba4f 100644
--- a/src/Hexecs.Benchmarks/Program.cs
+++ b/src/Hexecs.Benchmarks/Program.cs
@@ -1,5 +1,3 @@
using BenchmarkDotNet.Running;
-using Hexecs.Benchmarks.Actors;
-//BenchmarkRunner.Run();
BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args);
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Actors/ActorSystemShould.cs b/src/Hexecs.Tests/Actors/ActorSystemShould.cs
index def401f..b28c92d 100644
--- a/src/Hexecs.Tests/Actors/ActorSystemShould.cs
+++ b/src/Hexecs.Tests/Actors/ActorSystemShould.cs
@@ -14,8 +14,8 @@ public void ConfigureAndRunSystemsInParallel()
var systems = fixture.CreateArray(_ => new Mock());
using var world = new WorldBuilder()
- .DefaultParallelWorker(degreeOfParallelism: 4)
- .DefaultActorContext(cfg => cfg
+ .UseDefaultParallelWorker(degreeOfParallelism: 4)
+ .UseDefaultActorContext(cfg => cfg
.CreateParallelUpdateSystem(systems.Select(mock => mock.Object)))
.Build();
@@ -40,8 +40,8 @@ public void UpdateActorsInParallel(int degreeOfParallelism, int actorCount)
// arrange
using var world = new WorldBuilder()
- .DefaultParallelWorker(degreeOfParallelism)
- .DefaultActorContext(cfg => cfg
+ .UseDefaultParallelWorker(degreeOfParallelism)
+ .UseDefaultActorContext(cfg => cfg
.CreateUpdateSystem(ctx => new ParallelUpdateSystem(
ctx,
ctx.GetRequiredService())))
diff --git a/src/Hexecs.Tests/Actors/ActorTestFixture.cs b/src/Hexecs.Tests/Actors/ActorTestFixture.cs
index d880692..3f9d71a 100644
--- a/src/Hexecs.Tests/Actors/ActorTestFixture.cs
+++ b/src/Hexecs.Tests/Actors/ActorTestFixture.cs
@@ -15,9 +15,9 @@ public ActorTestFixture()
{
World = new WorldBuilder()
.CreateAssetData(CreateAssets)
- .DefaultActorContext(ctx => ctx
- .AddBuilder()
- .AddBuilder()
+ .UseDefaultActorContext(ctx => ctx
+ .CreateBuilder()
+ .CreateBuilder()
.ConfigureComponentPool(c => c.AddDisposeHandler()))
.Build();
}
diff --git a/src/Hexecs.Tests/Assets/AssetConstraintShould.cs b/src/Hexecs.Tests/Assets/AssetConstraintShould.cs
new file mode 100644
index 0000000..62c6e53
--- /dev/null
+++ b/src/Hexecs.Tests/Assets/AssetConstraintShould.cs
@@ -0,0 +1,94 @@
+using Hexecs.Assets;
+using Hexecs.Tests.Actors;
+using Hexecs.Tests.Mocks;
+
+namespace Hexecs.Tests.Assets;
+
+public sealed class AssetConstraintShould(AssetTestFixture fixture) : IClassFixture
+{
+ [Fact(DisplayName = "Должен успешно проходить проверку Applicable, если все условия соблюдены")]
+ public void Should_Be_Applicable_When_Conditions_Met()
+ {
+ // Arrange
+
+ var asset = fixture.CreateAsset();
+ var constraint = AssetConstraint
+ .Include(fixture.Assets)
+ .Build();
+
+ // Assert
+ constraint
+ .Applicable(asset.Id)
+ .Should()
+ .BeTrue();
+ }
+
+ [Fact(DisplayName = "Должен возвращать false в Applicable, если компонент исключен")]
+ public void Should_Not_Be_Applicable_When_Excluded_Component_Exists()
+ {
+ // Arrange
+ var asset = fixture.CreateAsset();
+ var constraint = AssetConstraint
+ .Exclude(fixture.Assets)
+ .Build();
+
+ // Assert
+ constraint
+ .Applicable(asset.Id)
+ .Should()
+ .BeFalse();
+ }
+
+ [Fact(DisplayName = "Builder должен выбрасывать исключение при добавлении дублирующегося компонента")]
+ public void Builder_Should_Throw_On_Duplicate_Component()
+ {
+ // Arrange
+ var builder = AssetConstraint.Include(fixture.Assets);
+
+ // Act
+ var action = () => builder.Include();
+
+ // Assert
+ action
+ .Should()
+ .Throw();
+ }
+
+ [Fact(DisplayName = "Два одинаковых ограничения должны иметь одинаковый HashCode и быть равны")]
+ public void Should_Implement_Equality_Correctly()
+ {
+ // Arrange
+ var context = fixture.Assets;
+ var constraint1 = AssetConstraint.Include(context)
+ .Exclude()
+ .Build();
+
+ var constraint2 = AssetConstraint.Include(context)
+ .Exclude()
+ .Build();
+
+ // Assert
+ constraint1
+ .Should()
+ .Be(constraint2);
+
+ constraint1.GetHashCode()
+ .Should()
+ .Be(constraint2.GetHashCode());
+ }
+
+ [Fact(DisplayName = "Должен корректно работать с несколькими Include компонентами")]
+ public void Should_Work_With_Multiple_Includes()
+ {
+ // Arrange
+ var actor = fixture.CreateAsset();
+ var constraint = AssetConstraint
+ .Include(fixture.Assets)
+ .Build();
+
+ constraint
+ .Applicable(actor.Id)
+ .Should()
+ .BeTrue();
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Assets/AssetContextShould.cs b/src/Hexecs.Tests/Assets/AssetContextShould.cs
index f9fd09f..5c211b2 100644
--- a/src/Hexecs.Tests/Assets/AssetContextShould.cs
+++ b/src/Hexecs.Tests/Assets/AssetContextShould.cs
@@ -9,7 +9,7 @@ public void GetAssetByAlias()
var alias = fixture.RandomString();
uint? assetId = null;
- var (assets, world) = fixture.CreateAssetContext(loader =>
+ fixture.CreateAssetContext(loader =>
{
var asset = loader.CreateAsset(alias);
assetId = asset.Id;
@@ -17,7 +17,7 @@ public void GetAssetByAlias()
// act
- var actual = assets.Invoking(ctx => ctx.GetAsset(alias))
+ var actual = fixture.Assets.Invoking(ctx => ctx.GetAsset(alias))
.Should()
.NotThrow()
.Which;
@@ -27,16 +27,15 @@ public void GetAssetByAlias()
actual.Id
.Should()
.Be(assetId);
-
- world.Dispose();
}
-
+
[Fact]
public void Throw_IfAssetByAlias_NotFound()
{
// act && assert
- fixture.Assets.Invoking(ctx => ctx.GetAsset(fixture.RandomString()))
+ var context = fixture.CreateAssetContext();
+ context.Invoking(ctx => ctx.GetAsset(fixture.RandomString()))
.Should()
.Throw();
}
diff --git a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs
index 87c08b6..4ddd7a8 100644
--- a/src/Hexecs.Tests/Assets/AssetFilter1Should.cs
+++ b/src/Hexecs.Tests/Assets/AssetFilter1Should.cs
@@ -10,7 +10,7 @@ public void ContainsAllAssets()
// arrange
var assetIds = new List();
- var (context, world) = fixture.CreateAssetContext(loader =>
+ var context = fixture.CreateAssetContext(loader =>
{
for (int i = 1; i < 100; i++)
{
@@ -31,8 +31,6 @@ public void ContainsAllAssets()
actualActors
.Should()
.Contain(expectedAssets);
-
- world.Dispose();
}
[Fact(DisplayName = "Фильтр ассетов можно перебирать как AssetRef")]
@@ -41,7 +39,7 @@ public void AssetFilterShouldEnumerable()
// arrange
var expectedIds = new Dictionary();
- var (context, world) = fixture.CreateAssetContext(loader =>
+ var context = fixture.CreateAssetContext(loader =>
{
for (var i = 0; i < 100; i++)
{
@@ -77,7 +75,107 @@ public void AssetFilterShouldEnumerable()
actualIds
.Should()
.Contain(expectedIds.Keys);
+ }
+
+ [Fact(DisplayName = "Фильтр должен быть пустым, если компоненты заданного типа отсутствуют")]
+ public void EmptyFilterWhenNoComponentsExist()
+ {
+ // arrange
+ var context = fixture.CreateAssetContext();
+
+ // act
+ var filter = context.Filter();
+
+ // assert
+ filter.Length
+ .Should()
+ .Be(0);
+ }
+
+ [Fact(DisplayName = "Фильтр должен учитывать constraint")]
+ public void FilterWithConstraint()
+ {
+ var notExpectedIds = new List();
+ uint expectedId = 0;
+
+ // arrange
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ notExpectedIds.Add(loader.CreateAsset(
+ new CarAsset(10, 10),
+ new UnitAsset()).Id);
+
+ notExpectedIds.Add(loader.CreateAsset(
+ new CarAsset(30, 30)).Id);
- world.Dispose();
+ expectedId = loader.CreateAsset(
+ new CarAsset(20, 20),
+ new BuildingAsset()).Id;
+ });
+
+ // act
+
+ var filter = context.Filter(constraint => constraint
+ .Exclude()
+ .Include());
+
+ // assert
+
+ filter.Length.Should().Be(1);
+
+ filter
+ .Contains(expectedId)
+ .Should()
+ .BeTrue();
+
+ foreach (var notExpectedId in notExpectedIds)
+ {
+ filter.Contains(notExpectedId)
+ .Should()
+ .BeFalse();
+ }
+ }
+
+ [Fact(DisplayName = "Метод Get должен выбрасывать исключение, если ассет не найден в фильтре")]
+ public void GetThrowsExceptionWhenNotFound()
+ {
+ // arrange
+ var context = fixture.CreateAssetContext(loader => { loader.CreateAsset(new CarAsset(1, 1)); });
+
+ var filter = context.Filter();
+
+ // act
+
+ Action act = () => filter.Get(999); // Несуществующий ID
+
+ // assert
+ act
+ .Should()
+ .Throw();
+ }
+
+ [Fact(DisplayName = "Contains возвращает корректный статус наличия ассета")]
+ public void ContainsReturnsCorrectStatus()
+ {
+ // arrange
+ uint existingId = 0;
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ var asset = loader.CreateAsset(new CarAsset(1, 1));
+ existingId = asset.Id;
+ });
+
+ var filter = context.Filter();
+
+ // act & assert
+ filter
+ .Contains(existingId)
+ .Should()
+ .BeTrue();
+
+ filter
+ .Contains(existingId + 100)
+ .Should()
+ .BeFalse();
}
}
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Assets/AssetFilter2Should.cs b/src/Hexecs.Tests/Assets/AssetFilter2Should.cs
new file mode 100644
index 0000000..02a3c9f
--- /dev/null
+++ b/src/Hexecs.Tests/Assets/AssetFilter2Should.cs
@@ -0,0 +1,193 @@
+using Hexecs.Tests.Mocks;
+
+namespace Hexecs.Tests.Assets;
+
+public sealed class AssetFilter2Should(AssetTestFixture fixture) : IClassFixture
+{
+ [Fact(DisplayName = "Фильтр ассетов должен содержать все созданные ассеты")]
+ public void ContainsAllAssets()
+ {
+ // arrange
+ var assetIds = new List();
+
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ for (int i = 1; i < 100; i++)
+ {
+ var asset = loader.CreateAsset(
+ new CarAsset(i, i),
+ new UnitAsset(i, i));
+ assetIds.Add(asset.Id);
+ }
+ });
+
+ var expectedAssets = assetIds.Select(id => context.GetAsset(id)).ToArray();
+
+ // act
+
+ var filter = context.Filter();
+ var actualActors = filter.ToArray();
+
+ // assert
+
+ actualActors
+ .Should()
+ .Contain(expectedAssets);
+ }
+
+ [Fact(DisplayName = "Фильтр ассетов можно перебирать как AssetRef")]
+ public void AssetFilterShouldEnumerable()
+ {
+ // arrange
+ var expectedIds = new Dictionary();
+
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ for (var i = 1; i < 100; i++)
+ {
+ var component1 = new CarAsset(i, i);
+ var component2 = new UnitAsset(i, i);
+ var asset = loader.CreateAsset(component1, component2);
+
+ expectedIds.Add(asset.Id, (component1, component2));
+ }
+ });
+
+ // act
+
+ var filter = context.Filter();
+
+ // assert
+
+ var actualIds = new List();
+ foreach (var asset in filter)
+ {
+ actualIds.Add(asset.Id);
+ asset
+ .Component1
+ .Should().Be(expectedIds[asset.Id].Item1);
+
+ asset
+ .Component2
+ .Should().Be(expectedIds[asset.Id].Item2);
+ }
+
+ filter.Length
+ .Should().Be(expectedIds.Count);
+
+ actualIds
+ .Should()
+ .HaveCount(expectedIds.Count);
+
+ actualIds
+ .Should()
+ .Contain(expectedIds.Keys);
+ }
+
+ [Fact(DisplayName = "Фильтр должен быть пустым, если компоненты заданного типа отсутствуют")]
+ public void EmptyFilterWhenNoComponentsExist()
+ {
+ // arrange
+ var context = fixture.CreateAssetContext();
+
+ // act
+ var filter = context.Filter();
+
+ // assert
+ filter.Length
+ .Should()
+ .Be(0);
+ }
+
+ [Fact(DisplayName = "Фильтр должен учитывать constraint")]
+ public void FilterWithConstraint()
+ {
+ var notExpectedIds = new List();
+ uint expectedId = 0;
+
+ // arrange
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ notExpectedIds.Add(loader.CreateAsset(
+ new CarAsset(10, 10),
+ new UnitAsset(),
+ new BuildingAsset()).Id);
+
+ notExpectedIds.Add(loader.CreateAsset(
+ new CarAsset(10, 10),
+ new UnitAsset(),
+ new NonExistentAsset()).Id);
+
+ expectedId = loader.CreateAsset(
+ new CarAsset(20, 20),
+ new UnitAsset(),
+ new SubjectAsset()).Id;
+ });
+
+ // act
+
+ var filter = context.Filter(constraint => constraint
+ .Exclude()
+ .Include());
+
+ // assert
+
+ filter.Length.Should().Be(1);
+
+ filter
+ .Contains(expectedId)
+ .Should()
+ .BeTrue();
+
+ foreach (var notExpectedId in notExpectedIds)
+ {
+ filter.Contains(notExpectedId)
+ .Should()
+ .BeFalse();
+ }
+ }
+
+ [Fact(DisplayName = "Метод Get должен выбрасывать исключение, если ассет не найден в фильтре")]
+ public void GetThrowsExceptionWhenNotFound()
+ {
+ // arrange
+ var context = fixture.CreateAssetContext(loader => loader
+ .CreateAsset(new CarAsset(1, 1), new UnitAsset()));
+
+ var filter = context.Filter();
+
+ // act
+
+ Action act = () => filter.Get(999); // Несуществующий ID
+
+ // assert
+ act
+ .Should()
+ .Throw();
+ }
+
+ [Fact(DisplayName = "Contains возвращает корректный статус наличия ассета")]
+ public void ContainsReturnsCorrectStatus()
+ {
+ // arrange
+ uint existingId = 0;
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ var asset = loader.CreateAsset(new CarAsset(1, 1), new UnitAsset());
+ existingId = asset.Id;
+ });
+
+ var filter = context.Filter();
+
+ // act & assert
+ filter
+ .Contains(existingId)
+ .Should()
+ .BeTrue();
+
+ filter
+ .Contains(existingId + 100)
+ .Should()
+ .BeFalse();
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Assets/AssetFilter3Should.cs b/src/Hexecs.Tests/Assets/AssetFilter3Should.cs
new file mode 100644
index 0000000..93d3769
--- /dev/null
+++ b/src/Hexecs.Tests/Assets/AssetFilter3Should.cs
@@ -0,0 +1,196 @@
+using Hexecs.Tests.Mocks;
+
+namespace Hexecs.Tests.Assets;
+
+public sealed class AssetFilter3Should(AssetTestFixture fixture) : IClassFixture
+{
+ [Fact(DisplayName = "Фильтр ассетов должен содержать все созданные ассеты")]
+ public void ContainsAllAssets()
+ {
+ // arrange
+ var assetIds = new List();
+
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ for (int i = 1; i < 100; i++)
+ {
+ var asset = loader.CreateAsset(
+ new CarAsset(i, i),
+ new DecisionAsset(i, i),
+ new UnitAsset(i, i));
+ assetIds.Add(asset.Id);
+ }
+ });
+
+ var expectedAssets = assetIds.Select(id => context.GetAsset(id)).ToArray();
+
+ // act
+
+ var filter = context.Filter();
+ var actualActors = filter.ToArray();
+
+ // assert
+
+ actualActors
+ .Should()
+ .Contain(expectedAssets);
+ }
+
+ [Fact(DisplayName = "Фильтр ассетов можно перебирать как AssetRef")]
+ public void AssetFilterShouldEnumerable()
+ {
+ // arrange
+ var expectedIds = new Dictionary();
+
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ for (var i = 1; i < 100; i++)
+ {
+ var component1 = new CarAsset(i, i);
+ var component2 = new DecisionAsset(i, i);
+ var component3 = new UnitAsset(i, i);
+ var asset = loader.CreateAsset(component1, component2, component3);
+
+ expectedIds.Add(asset.Id, (component1, component2, component3));
+ }
+ });
+
+ // act
+
+ var filter = context.Filter();
+
+ // assert
+
+ var actualIds = new List();
+ foreach (var asset in filter)
+ {
+ actualIds.Add(asset.Id);
+ asset
+ .Component1
+ .Should().Be(expectedIds[asset.Id].Item1);
+
+ asset
+ .Component2
+ .Should().Be(expectedIds[asset.Id].Item2);
+
+ asset
+ .Component3
+ .Should().Be(expectedIds[asset.Id].Item3);
+ }
+
+ filter.Length
+ .Should().Be(expectedIds.Count);
+
+ actualIds
+ .Should()
+ .HaveCount(expectedIds.Count);
+
+ actualIds
+ .Should()
+ .Contain(expectedIds.Keys);
+ }
+
+ [Fact(DisplayName = "Фильтр должен быть пустым, если компоненты заданного типа отсутствуют")]
+ public void EmptyFilterWhenNoComponentsExist()
+ {
+ // arrange
+ var context = fixture.CreateAssetContext();
+
+ // act
+ var filter = context.Filter();
+
+ // assert
+ filter.Length
+ .Should()
+ .Be(0);
+ }
+
+ [Fact(DisplayName = "Фильтр должен учитывать constraint")]
+ public void FilterWithConstraint()
+ {
+ var notExpectedIds = new List();
+ uint expectedId = 0;
+
+ // arrange
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ var asset = loader.CreateAsset(new CarAsset(10, 10), new DecisionAsset(), new UnitAsset());
+ asset.Set(new BuildingAsset());
+ notExpectedIds.Add(asset.Id);
+
+ asset = loader.CreateAsset(new CarAsset(10, 10), new DecisionAsset(), new UnitAsset());
+ asset.Set(new NonExistentAsset());
+ notExpectedIds.Add(asset.Id);
+
+ asset = loader.CreateAsset(new CarAsset(10, 10), new DecisionAsset(), new UnitAsset());
+ asset.Set(new SubjectAsset());
+ expectedId = asset.Id;
+ });
+
+ // act
+
+ var filter = context.Filter(constraint => constraint
+ .Exclude()
+ .Include());
+
+ // assert
+
+ filter.Length.Should().Be(1);
+
+ filter
+ .Contains(expectedId)
+ .Should()
+ .BeTrue();
+
+ foreach (var notExpectedId in notExpectedIds)
+ {
+ filter.Contains(notExpectedId)
+ .Should()
+ .BeFalse();
+ }
+ }
+
+ [Fact(DisplayName = "Метод Get должен выбрасывать исключение, если ассет не найден в фильтре")]
+ public void GetThrowsExceptionWhenNotFound()
+ {
+ // arrange
+ var context = fixture.CreateAssetContext(loader => loader
+ .CreateAsset(new CarAsset(1, 1), new DecisionAsset(), new UnitAsset()));
+
+ var filter = context.Filter();
+
+ // act
+
+ Action act = () => filter.Get(999); // Несуществующий ID
+
+ // assert
+ act
+ .Should()
+ .Throw();
+ }
+
+ [Fact(DisplayName = "Contains возвращает корректный статус наличия ассета")]
+ public void ContainsReturnsCorrectStatus()
+ {
+ // arrange
+ uint existingId = 0;
+ var context = fixture.CreateAssetContext(loader =>
+ {
+ var asset = loader.CreateAsset(new CarAsset(1, 1), new DecisionAsset(), new UnitAsset());
+ existingId = asset.Id;
+ });
+
+ var filter = context.Filter();
+
+ // act & assert
+ filter
+ .Contains(existingId)
+ .Should()
+ .BeTrue();
+
+ filter
+ .Contains(existingId + 100)
+ .Should()
+ .BeFalse();
+ }
+}
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Assets/AssetTestFixture.cs b/src/Hexecs.Tests/Assets/AssetTestFixture.cs
index 1ff2130..4f7e646 100644
--- a/src/Hexecs.Tests/Assets/AssetTestFixture.cs
+++ b/src/Hexecs.Tests/Assets/AssetTestFixture.cs
@@ -7,31 +7,75 @@ namespace Hexecs.Tests.Assets;
public sealed class AssetTestFixture : BaseFixture, IDisposable
{
- public ActorContext Actors => World.Actors;
- public AssetContext Assets => World.Assets;
- public readonly World World;
+ public AssetContext Assets => _assets ?? throw new Exception("Assets isn't configured");
- public AssetTestFixture()
+ public World World
{
- World = new WorldBuilder()
+ get => _world ?? throw new Exception("World isn't configured");
+ set
+ {
+ if (_world != null)
+ {
+ _assets = null;
+ _world.Dispose();
+ }
+
+ _world = value;
+ }
+ }
+
+ private AssetContext? _assets;
+ private World? _world;
+
+ public Asset CreateAsset() where T : struct, IAssetComponent
+ {
+ var assetId = Asset.EmptyId;
+ _world = new WorldBuilder()
.CreateAssetData(CreateAssets)
+ .CreateAssetData(loader => { assetId = loader.CreateAsset(CreateComponent()).Id; })
.Build();
+
+ _assets = _world.Assets;
+ return Assets.GetAsset(assetId);
}
- public (AssetContext, World) CreateAssetContext(Action assets)
+ public Asset CreateAsset()
+ where T1 : struct, IAssetComponent
+ where T2 : struct, IAssetComponent
{
- var world = new WorldBuilder()
+ var assetId = Asset.EmptyId;
+ _world = new WorldBuilder()
.CreateAssetData(CreateAssets)
- .CreateAssetData(assets)
+ .CreateAssetData(loader =>
+ {
+ var asset = loader.CreateAsset(CreateComponent());
+ asset.Set(CreateComponent());
+ assetId = asset.Id;
+ })
.Build();
- return (world.Assets, world);
+ _assets = _world.Assets;
+ return Assets.GetAsset(assetId);
+ }
+
+ public AssetContext CreateAssetContext(Action? assets = null)
+ {
+ var worldBuilder = new WorldBuilder();
+ worldBuilder.CreateAssetData(CreateAssets);
+
+ if (assets != null) worldBuilder.CreateAssetData(assets);
+
+ _world = worldBuilder.Build();
+ _assets = _world.Assets;
+
+ return _assets;
}
public T CreateComponent() where T : struct, IAssetComponent
{
object? result = null;
+ if (typeof(T) == typeof(CarAsset)) result = new CarAsset(RandomInt(1, 10), RandomInt(11, 20));
if (typeof(T) == typeof(UnitAsset)) result = new UnitAsset(RandomInt(1, 10), RandomInt(11, 20));
return result == null
@@ -50,6 +94,7 @@ private void CreateAssets(IAssetLoader loader)
public void Dispose()
{
- World.Dispose();
+ _assets?.Dispose();
+ _world?.Dispose();
}
}
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Mocks/BuildingAsset.cs b/src/Hexecs.Tests/Mocks/BuildingAsset.cs
new file mode 100644
index 0000000..a124a1b
--- /dev/null
+++ b/src/Hexecs.Tests/Mocks/BuildingAsset.cs
@@ -0,0 +1,5 @@
+using Hexecs.Assets;
+
+namespace Hexecs.Tests.Mocks;
+
+public readonly struct BuildingAsset : IAssetComponent;
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Mocks/DecisionAsset.cs b/src/Hexecs.Tests/Mocks/DecisionAsset.cs
new file mode 100644
index 0000000..040d776
--- /dev/null
+++ b/src/Hexecs.Tests/Mocks/DecisionAsset.cs
@@ -0,0 +1,9 @@
+using Hexecs.Assets;
+
+namespace Hexecs.Tests.Mocks;
+
+public readonly struct DecisionAsset(int min, int max) : IAssetComponent
+{
+ public readonly int Min = min;
+ public readonly int Max = max;
+}
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Mocks/NonExistentAsset.cs b/src/Hexecs.Tests/Mocks/NonExistentAsset.cs
new file mode 100644
index 0000000..39b823a
--- /dev/null
+++ b/src/Hexecs.Tests/Mocks/NonExistentAsset.cs
@@ -0,0 +1,5 @@
+using Hexecs.Assets;
+
+namespace Hexecs.Tests.Mocks;
+
+public readonly struct NonExistentAsset : IAssetComponent;
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Mocks/SubjectAsset.cs b/src/Hexecs.Tests/Mocks/SubjectAsset.cs
new file mode 100644
index 0000000..38fef95
--- /dev/null
+++ b/src/Hexecs.Tests/Mocks/SubjectAsset.cs
@@ -0,0 +1,5 @@
+using Hexecs.Assets;
+
+namespace Hexecs.Tests.Mocks;
+
+public readonly struct SubjectAsset : IAssetComponent;
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Utils/MoneyShould.cs b/src/Hexecs.Tests/Utils/MoneyShould.cs
deleted file mode 100644
index fd48c19..0000000
--- a/src/Hexecs.Tests/Utils/MoneyShould.cs
+++ /dev/null
@@ -1,330 +0,0 @@
-using Hexecs.Utils;
-
-namespace Hexecs.Tests.Utils;
-
-public class MoneyTests
-{
- [Fact(DisplayName = "Конструктор создает экземпляр с правильным значением")]
- public void Constructor_ShouldCreateInstanceWithCorrectValue()
- {
- // Arrange & Act
- var money = new Money(10050);
-
- // Assert
- money.Value.Should().Be(10050);
- money.Whole.Should().Be(100);
- money.Fraction.Should().Be(50);
- }
-
- [Fact(DisplayName = "Zero должен возвращать экземпляр с нулевым значением")]
- public void Zero_ShouldReturnInstanceWithZeroValue()
- {
- // Act
- var zero = Money.Zero;
-
- // Assert
- zero.Value.Should().Be(0);
- zero.Whole.Should().Be(0);
- zero.Fraction.Should().Be(0);
- }
-
- [Theory(DisplayName = "Create должен корректно создавать экземпляр с заданными значениями")]
- [InlineData(100, 50, 10050)]
- [InlineData(0, 0, 0)]
- [InlineData(-100, 50, -10050)]
- public void Create_ShouldCreateCorrectInstance(long whole, int fraction, long expectedValue)
- {
- // Act
- var money = Money.Create(whole, fraction);
-
- // Assert
- money.Value.Should().Be(expectedValue);
- money.Whole.Should().Be(whole);
- money.Fraction.Should().Be(fraction);
- }
-
- [Fact(DisplayName = "Create должен вызывать исключение, если дробная часть вне диапазона")]
- public void Create_ShouldThrowOverflowException_WhenFractionOutOfRange()
- {
- // Act & Assert
- Action act1 = () => Money.Create(100, -1);
- act1.Should().Throw()
- .WithMessage("Fraction should be between 0 and 100");
-
- Action act2 = () => Money.Create(100, 100);
- act2.Should().Throw()
- .WithMessage("Fraction should be between 0 and 100");
- }
-
- [Theory(DisplayName = "TryParse должен корректно преобразовывать строку в Money")]
- [InlineData("100.50", 10050, true)]
- [InlineData("0", 0, true)]
- [InlineData("-100.50", -10050, true)]
- [InlineData("invalid", 0, false)]
- public void TryParse_ShouldCorrectlyParseString(string input, long expectedValue, bool expectedResult)
- {
- // Act
- var success = Money.TryParse(input, out var result);
-
- // Assert
- success.Should().Be(expectedResult);
- if (expectedResult)
- {
- result.Value.Should().Be(expectedValue);
- }
- }
-
- [Fact(DisplayName = "Abs должен возвращать абсолютное значение")]
- public void Abs_ShouldReturnAbsoluteValue()
- {
- // Arrange
- var negative = new Money(-10050);
- var positive = new Money(10050);
-
- // Act
- var absNegative = negative.Abs();
- var absPositive = positive.Abs();
-
- // Assert
- absNegative.Value.Should().Be(10050);
- absPositive.Value.Should().Be(10050);
- }
-
- [Fact(DisplayName = "Min должен возвращать минимальное значение")]
- public void Min_ShouldReturnMinimumValue()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(20050);
-
- // Act
- var min1 = money1.Min(money2);
- var min2 = money2.Min(money1);
-
- // Assert
- min1.Value.Should().Be(10050);
- min2.Value.Should().Be(10050);
- }
-
- [Fact(DisplayName = "Max должен возвращать максимальное значение")]
- public void Max_ShouldReturnMaximumValue()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(20050);
-
- // Act
- var max1 = money1.Max(money2);
- var max2 = money2.Max(money1);
-
- // Assert
- max1.Value.Should().Be(20050);
- max2.Value.Should().Be(20050);
- }
-
- [Fact(DisplayName = "ToString должен возвращать корректное строковое представление")]
- public void ToString_ShouldReturnCorrectStringRepresentation()
- {
- // Arrange
- var money = new Money(10050);
- var negMoney = new Money(-10050);
-
- // Act
- var str = money.ToString();
- var negStr = negMoney.ToString();
-
- // Assert
- str.Should().Be("100.50");
- negStr.Should().Be("-100.50");
- }
-
- [Theory(DisplayName = "ToString с форматом должен корректно форматировать значение")]
- [InlineData(null, null, "100.50")]
- [InlineData("F4", null, "100.5000")]
- [InlineData("N1", null, "100.5")]
- public void ToString_WithFormat_ShouldFormatValueCorrectly(string? format, IFormatProvider? provider, string expected)
- {
- // Arrange
- var money = new Money(10050);
-
- // Act
- var result = money.ToString(format, provider);
-
- // Assert
- result.Should().Be(expected);
- }
-
- [Fact(DisplayName = "TryFormat должен корректно форматировать значение в буфер символов")]
- public void TryFormat_ShouldFormatValueToCharSpan()
- {
- // Arrange
- var money = new Money(10050);
- var destination = new char[10];
-
- // Act
- var success = money.TryFormat(destination, out var charsWritten, "N2", null);
-
- // Assert
- success.Should().BeTrue();
- charsWritten.Should().BeGreaterThan(0);
- new string(destination, 0, charsWritten).Should().Be("100.50");
- }
-
- [Fact(DisplayName = "Операторы сложения должны работать корректно")]
- public void AdditionOperators_ShouldWorkCorrectly()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(20050);
- var longValue = 100L;
-
- // Act & Assert
- (money1 + money2).Value.Should().Be(30100);
- (money1 + longValue).Value.Should().Be(10150);
- (longValue + money1).Value.Should().Be(10150);
- (+money1).Value.Should().Be(10050);
- }
-
- [Fact(DisplayName = "Операторы вычитания должны работать корректно")]
- public void SubtractionOperators_ShouldWorkCorrectly()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(5050);
- var longValue = 100L;
-
- // Act & Assert
- (money1 - money2).Value.Should().Be(5000);
- (money1 - longValue).Value.Should().Be(9950);
- (longValue - money1).Value.Should().Be(-9950);
- (-money1).Value.Should().Be(-10050);
- }
-
- [Fact(DisplayName = "Операторы умножения должны работать корректно")]
- public void MultiplicationOperators_ShouldWorkCorrectly()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(2);
- var longValue = 2L;
-
- // Act & Assert
- (money1 * money2).Value.Should().Be(20100);
- (money1 * longValue).Value.Should().Be(20100);
- (longValue * money1).Value.Should().Be(20100);
- }
-
- [Fact(DisplayName = "Операторы деления должны работать корректно")]
- public void DivisionOperators_ShouldWorkCorrectly()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(2);
- var longValue = 2L;
-
- // Act & Assert
- (money1 / money2).Value.Should().Be(5025);
- (money1 / longValue).Value.Should().Be(5025);
- (longValue / money1).Value.Should().Be(0);
- }
-
- [Fact(DisplayName = "Операторы сравнения должны работать корректно")]
- public void ComparisonOperators_ShouldWorkCorrectly()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(20050);
- var money3 = new Money(10050);
-
- // Act & Assert
- (money1 == money3).Should().BeTrue();
- (money1 != money2).Should().BeTrue();
- (money1 < money2).Should().BeTrue();
- (money2 > money1).Should().BeTrue();
- (money1 <= money3).Should().BeTrue();
- (money1 >= money3).Should().BeTrue();
- (money1 <= money2).Should().BeTrue();
- (money2 >= money1).Should().BeTrue();
- }
-
- [Fact(DisplayName = "Equals должен корректно определять равенство")]
- public void Equals_ShouldWorkCorrectly()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(10050);
- var money3 = new Money(20050);
- var obj = new object();
-
- // Act & Assert
- money1.Equals(money2).Should().BeTrue();
- money1.Equals(money3).Should().BeFalse();
- money1.Equals(obj).Should().BeFalse();
- }
-
- [Fact(DisplayName = "CompareTo должен корректно сравнивать значения")]
- public void CompareTo_ShouldCompareValuesCorrectly()
- {
- // Arrange
- var money1 = new Money(10050);
- var money2 = new Money(20050);
- var money3 = new Money(10050);
-
- // Act & Assert
- money1.CompareTo(money2).Should().BeLessThan(0);
- money2.CompareTo(money1).Should().BeGreaterThan(0);
- money1.CompareTo(money3).Should().Be(0);
- }
-
- [Fact(DisplayName = "GetHashCode должен возвращать корректный хэш-код")]
- public void GetHashCode_ShouldReturnCorrectHashCode()
- {
- // Arrange
- var money = new Money(10050);
-
- // Act
- var hashCode = money.GetHashCode();
-
- // Assert
- hashCode.Should().Be(10050.GetHashCode());
- }
-
- [Fact(DisplayName = "Неявное преобразование в числовые типы должно работать корректно")]
- public void ImplicitConversionToNumericTypes_ShouldWorkCorrectly()
- {
- // Arrange
- var money = new Money(10050);
-
- // Act
- float floatValue = money;
- double doubleValue = money;
- decimal decimalValue = money;
-
- // Assert
- floatValue.Should().BeApproximately(100.5f, 0.001f);
- doubleValue.Should().BeApproximately(100.5, 0.001);
- decimalValue.Should().Be(100.5m);
- }
-
- [Fact(DisplayName = "Неявное преобразование из числовых типов должно работать корректно")]
- public void ImplicitConversionFromNumericTypes_ShouldWorkCorrectly()
- {
- // Act
- Money moneyFromFloat = 100.5f;
- Money moneyFromDouble = 100.5;
- Money moneyFromDecimal = 100.5m;
-
- // Assert
- moneyFromFloat.Value.Should().Be(10050);
- moneyFromDouble.Value.Should().Be(10050);
- moneyFromDecimal.Value.Should().Be(10050);
- }
-
- [Fact(DisplayName = "MaxValue и MinValue должны содержать правильные значения")]
- public void MaxValueAndMinValue_ShouldHaveCorrectValues()
- {
- // Act & Assert
- Money.MaxValue.Should().Be(Money.Create(long.MaxValue / 100L - 1, 99));
- Money.MinValue.Should().Be(Money.Create(long.MinValue / 100L + 1, 99));
- }
-}
\ No newline at end of file
diff --git a/src/Hexecs.Tests/Worlds/WordDependencyShould.cs b/src/Hexecs.Tests/Worlds/WordDependencyShould.cs
index 503d4e7..ad5f43d 100644
--- a/src/Hexecs.Tests/Worlds/WordDependencyShould.cs
+++ b/src/Hexecs.Tests/Worlds/WordDependencyShould.cs
@@ -7,10 +7,10 @@ namespace Hexecs.Tests.Worlds;
public sealed class WordDependencyShould
{
private readonly World _world = new WorldBuilder()
- .Singleton(_ => new Singleton())
- .Singleton(_ => new Singleton())
- .Scoped(_ => new Scoped())
- .Transient(_ => new Transient())
+ .UseSingleton(_ => new Singleton())
+ .UseSingleton(_ => new Singleton())
+ .UseScoped(_ => new Scoped())
+ .UseTransient(_ => new Transient())
.Build();
[Fact]
diff --git a/src/Hexecs/Actors/ActorContext.Entry.cs b/src/Hexecs/Actors/ActorContext.Entry.cs
index be87629..166021b 100644
--- a/src/Hexecs/Actors/ActorContext.Entry.cs
+++ b/src/Hexecs/Actors/ActorContext.Entry.cs
@@ -16,7 +16,15 @@ public readonly void Serialize(Utf8JsonWriter writer)
writer.WriteStartObject();
writer.WriteProperty("Key", Key);
- //writer.WriteProperty("Components", in Components);
+ writer.WritePropertyName("Components");
+
+ writer.WriteStartArray();
+ foreach (var component in Components)
+ {
+ writer.WriteNumberValue(component);
+ }
+
+ writer.WriteStartArray();
writer.WriteEndObject();
}
@@ -26,7 +34,7 @@ public readonly void Serialize(Utf8JsonWriter writer)
[method: MethodImpl(MethodImplOptions.AggressiveInlining)]
internal struct ComponentBucket()
{
- public const int InlineArraySize = 6;
+ private const int InlineArraySize = 6;
public int Length
{
diff --git a/src/Hexecs/Actors/ActorContext.cs b/src/Hexecs/Actors/ActorContext.cs
index f1bc97f..027a955 100644
--- a/src/Hexecs/Actors/ActorContext.cs
+++ b/src/Hexecs/Actors/ActorContext.cs
@@ -1,5 +1,4 @@
-using System.Collections.Concurrent;
-using System.Collections.Frozen;
+using System.Collections.Frozen;
using Hexecs.Actors.Components;
using Hexecs.Actors.Delegates;
using Hexecs.Actors.Relations;
diff --git a/src/Hexecs/Actors/ActorContextBuilder.Extensions.cs b/src/Hexecs/Actors/ActorContextBuilder.Extensions.cs
index e067fa1..41afff0 100644
--- a/src/Hexecs/Actors/ActorContextBuilder.Extensions.cs
+++ b/src/Hexecs/Actors/ActorContextBuilder.Extensions.cs
@@ -6,59 +6,76 @@ namespace Hexecs.Actors;
public static class ActorContextBuilderExtensions
{
- ///
- /// Регистрирует метод создания обработчика команды указанного типа.
- ///
- ///
- /// Использует рефлексию.
- ///
- public static ActorContextBuilder CreateCommandHandler<
+ extension(ActorContextBuilder builder)
+ {
+ ///
+ /// Создаёт строитель актёров указанного типа.
+ ///
+ /// Тип строителя актёров.
+ /// Этот же экземпляр ActorContextBuilder для цепочки вызовов.
+ public ActorContextBuilder CreateBuilder<
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces |
DynamicallyAccessedMemberTypes.PublicConstructors)]
- THandler>
- (this ActorContextBuilder builder)
- where THandler : class, ICommandHandler
- {
- var commandType = PipelineUtils.GetCommandType(typeof(THandler));
- var commandId = CommandType.GetId(commandType);
+ T>() where T : class, IActorBuilder
+ {
+ builder.CreateBuilder(static ctx => (IActorBuilder)ctx.Activate(typeof(T)));
+ return builder;
+ }
- builder.InsertCommandHandlerEntry(
- commandId,
- commandType,
- new ActorContextBuilder.Entry(static ctx =>
- (ICommandHandler)ctx.Activate(typeof(THandler))));
+ ///
+ /// Регистрирует метод создания обработчика команды указанного типа.
+ ///
+ ///
+ /// Использует рефлексию.
+ ///
+ public ActorContextBuilder CreateCommandHandler<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces |
+ DynamicallyAccessedMemberTypes.PublicConstructors)]
+ THandler>
+ ()
+ where THandler : class, ICommandHandler
+ {
+ var commandType = PipelineUtils.GetCommandType(typeof(THandler));
+ var commandId = CommandType.GetId(commandType);
- return builder;
- }
+ builder.InsertCommandHandlerEntry(
+ commandId,
+ commandType,
+ new ActorContextBuilder.Entry(static ctx =>
+ (ICommandHandler)ctx.Activate(typeof(THandler))));
- ///
- /// Регистрирует метод создания системы отрисовки актёров.
- ///
- ///
- /// Использует рефлексию.
- ///
- public static ActorContextBuilder CreateDrawSystem<
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
- TSystem>
- (this ActorContextBuilder builder) where TSystem : class, IDrawSystem
- {
- builder.CreateDrawSystem(static ctx => (IDrawSystem)ctx.Activate(typeof(TSystem)));
- return builder;
- }
+ return builder;
+ }
- ///
- /// Регистрирует метод создания системы обновления актёров.
- ///
- ///
- /// Использует рефлексию.
- ///
- public static ActorContextBuilder CreateUpdateSystem<
- [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
- TSystem>
- (this ActorContextBuilder builder) where TSystem : class, IUpdateSystem
- {
- builder.CreateUpdateSystem(static ctx => (IUpdateSystem)ctx.Activate(typeof(TSystem)));
- return builder;
+ ///
+ /// Регистрирует метод создания системы отрисовки актёров.
+ ///
+ ///
+ /// Использует рефлексию.
+ ///
+ public ActorContextBuilder CreateDrawSystem<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+ TSystem>
+ () where TSystem : class, IDrawSystem
+ {
+ builder.CreateDrawSystem(static ctx => (IDrawSystem)ctx.Activate(typeof(TSystem)));
+ return builder;
+ }
+
+ ///
+ /// Регистрирует метод создания системы обновления актёров.
+ ///
+ ///
+ /// Использует рефлексию.
+ ///
+ public ActorContextBuilder CreateUpdateSystem<
+ [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)]
+ TSystem>
+ () where TSystem : class, IUpdateSystem
+ {
+ builder.CreateUpdateSystem(static ctx => (IUpdateSystem)ctx.Activate(typeof(TSystem)));
+ return builder;
+ }
}
///
diff --git a/src/Hexecs/Actors/ActorContextBuilder.cs b/src/Hexecs/Actors/ActorContextBuilder.cs
index 2d1673d..f30deeb 100644
--- a/src/Hexecs/Actors/ActorContextBuilder.cs
+++ b/src/Hexecs/Actors/ActorContextBuilder.cs
@@ -93,17 +93,6 @@ public ActorContextBuilder AddBuilder(IActorBuilder builder)
return this;
}
- ///
- /// Регистрирует строитель актёров указанного типа.
- ///
- /// Тип строителя актёров.
- /// Этот же экземпляр ActorContextBuilder для цепочки вызовов.
- public ActorContextBuilder AddBuilder() where T : class, IActorBuilder, new()
- {
- _builders.Add(new Entry(static _ => new T()));
- return this;
- }
-
///
/// Регистрирует функцию для создания строителя актёров с доступом к контексту актёров.
///
diff --git a/src/Hexecs/Actors/Systems/DrawSystem.cs b/src/Hexecs/Actors/Systems/DrawSystem.cs
index 551bff1..ee47e11 100644
--- a/src/Hexecs/Actors/Systems/DrawSystem.cs
+++ b/src/Hexecs/Actors/Systems/DrawSystem.cs
@@ -6,7 +6,7 @@
namespace Hexecs.Actors.Systems;
-public abstract class DrawSystem(ActorContext context) : IDrawSystem
+public abstract class DrawSystem(ActorContext context) : IDrawSystem, IDisposable
{
public bool Enabled { get; set; } = true;
@@ -52,118 +52,8 @@ private ContextLogger CreateLogger() => Context
.CreateContext(GetType());
ActorContext IDrawSystem.Context => Context;
-}
-public abstract class DrawSystem : DrawSystem
- where T1 : struct, IActorComponent
-{
- private readonly ActorFilter _filter;
-
- protected DrawSystem(ActorContext context, Action? constraint = null) : base(context)
- {
- _filter = constraint == null
- ? context.Filter()
- : context.Filter(constraint);
- }
-
- protected virtual void AfterDraw(in WorldTime time)
- {
- }
-
- protected virtual void BeforeDraw(in WorldTime time)
- {
- }
-
- public sealed override void Draw(in WorldTime time)
- {
- if (!Enabled) return;
-
- BeforeDraw(in time);
-
- foreach (var actor in _filter)
- {
- Draw(in actor, time);
- }
-
- AfterDraw(in time);
- }
-
- protected abstract void Draw(in ActorRef actor, in WorldTime time);
-}
-
-public abstract class DrawSystem : DrawSystem
- where T1 : struct, IActorComponent
- where T2 : struct, IActorComponent
-{
- private readonly ActorFilter _filter;
-
- protected DrawSystem(ActorContext context, Action? constraint = null) : base(context)
- {
- _filter = constraint == null
- ? context.Filter()
- : context.Filter(constraint);
- }
-
- protected virtual void AfterDraw(in WorldTime time)
- {
- }
-
- protected virtual void BeforeDraw(in WorldTime time)
+ public virtual void Dispose()
{
}
-
- public sealed override void Draw(in WorldTime time)
- {
- if (!Enabled) return;
-
- BeforeDraw(in time);
-
- foreach (var actor in _filter)
- {
- Draw(in actor, time);
- }
-
- AfterDraw(in time);
- }
-
- protected abstract void Draw(in ActorRef actor, in WorldTime time);
-}
-
-public abstract class DrawSystem : DrawSystem
- where T1 : struct, IActorComponent
- where T2 : struct, IActorComponent
- where T3 : struct, IActorComponent
-{
- private readonly ActorFilter _filter;
-
- protected DrawSystem(ActorContext context, Action? constraint = null) : base(context)
- {
- _filter = constraint == null
- ? context.Filter()
- : context.Filter(constraint);
- }
-
- protected virtual void AfterDraw(in WorldTime time)
- {
- }
-
- protected virtual void BeforeDraw(in WorldTime time)
- {
- }
-
- public sealed override void Draw(in WorldTime time)
- {
- if (!Enabled) return;
-
- BeforeDraw(in time);
-
- foreach (var actor in _filter)
- {
- Draw(in actor, time);
- }
-
- AfterDraw(in time);
- }
-
- protected abstract void Draw(in ActorRef actor, in WorldTime time);
}
\ No newline at end of file
diff --git a/src/Hexecs/Actors/Systems/DrawSystem1.cs b/src/Hexecs/Actors/Systems/DrawSystem1.cs
new file mode 100644
index 0000000..00aed6e
--- /dev/null
+++ b/src/Hexecs/Actors/Systems/DrawSystem1.cs
@@ -0,0 +1,44 @@
+using Hexecs.Worlds;
+
+namespace Hexecs.Actors.Systems;
+
+public abstract class DrawSystem : DrawSystem
+ where T1 : struct, IActorComponent
+{
+ private readonly ActorFilter _filter;
+
+ protected DrawSystem(ActorContext context, Action? constraint = null) : base(context)
+ {
+ _filter = constraint == null
+ ? context.Filter()
+ : context.Filter(constraint);
+ }
+
+ protected virtual void AfterDraw(in WorldTime time)
+ {
+ }
+
+ ///
+ /// Метод запускается до полного обновления
+ ///
+ /// Время мира
+ /// Если возвращает false, то обновление не происходит
+ protected virtual bool BeforeDraw(in WorldTime time) => true;
+
+ public sealed override void Draw(in WorldTime time)
+ {
+ if (!Enabled) return;
+
+ if (BeforeDraw(in time))
+ {
+ foreach (var actor in _filter)
+ {
+ Draw(in actor, time);
+ }
+
+ AfterDraw(in time);
+ }
+ }
+
+ protected abstract void Draw(in ActorRef actor, in WorldTime time);
+}
\ No newline at end of file
diff --git a/src/Hexecs/Actors/Systems/DrawSystem2.cs b/src/Hexecs/Actors/Systems/DrawSystem2.cs
new file mode 100644
index 0000000..e0c3a43
--- /dev/null
+++ b/src/Hexecs/Actors/Systems/DrawSystem2.cs
@@ -0,0 +1,45 @@
+using Hexecs.Worlds;
+
+namespace Hexecs.Actors.Systems;
+
+public abstract class DrawSystem : DrawSystem
+ where T1 : struct, IActorComponent
+ where T2 : struct, IActorComponent
+{
+ private readonly ActorFilter _filter;
+
+ protected DrawSystem(ActorContext context, Action? constraint = null) : base(context)
+ {
+ _filter = constraint == null
+ ? context.Filter()
+ : context.Filter(constraint);
+ }
+
+ protected virtual void AfterDraw(in WorldTime time)
+ {
+ }
+
+ ///
+ /// Метод запускается до полного обновления
+ ///
+ /// Время мира
+ /// Если возвращает false, то обновление не происходит
+ protected virtual bool BeforeDraw(in WorldTime time) => true;
+
+ public sealed override void Draw(in WorldTime time)
+ {
+ if (!Enabled) return;
+
+ if (BeforeDraw(in time))
+ {
+ foreach (var actor in _filter)
+ {
+ Draw(in actor, time);
+ }
+
+ AfterDraw(in time);
+ }
+ }
+
+ protected abstract void Draw(in ActorRef actor, in WorldTime time);
+}
\ No newline at end of file
diff --git a/src/Hexecs/Actors/Systems/DrawSystem3.cs b/src/Hexecs/Actors/Systems/DrawSystem3.cs
new file mode 100644
index 0000000..5655877
--- /dev/null
+++ b/src/Hexecs/Actors/Systems/DrawSystem3.cs
@@ -0,0 +1,46 @@
+using Hexecs.Worlds;
+
+namespace Hexecs.Actors.Systems;
+
+public abstract class DrawSystem : DrawSystem
+ where T1 : struct, IActorComponent
+ where T2 : struct, IActorComponent
+ where T3 : struct, IActorComponent
+{
+ private readonly ActorFilter _filter;
+
+ protected DrawSystem(ActorContext context, Action? constraint = null) : base(context)
+ {
+ _filter = constraint == null
+ ? context.Filter()
+ : context.Filter(constraint);
+ }
+
+ protected virtual void AfterDraw(in WorldTime time)
+ {
+ }
+
+ ///
+ /// Метод запускается до полного обновления
+ ///
+ /// Время мира
+ /// Если возвращает false, то обновление не происходит
+ protected virtual bool BeforeDraw(in WorldTime time) => true;
+
+ public sealed override void Draw(in WorldTime time)
+ {
+ if (!Enabled) return;
+
+ if (BeforeDraw(in time))
+ {
+ foreach (var actor in _filter)
+ {
+ Draw(in actor, time);
+ }
+
+ AfterDraw(in time);
+ }
+ }
+
+ protected abstract void Draw(in ActorRef actor, in WorldTime time);
+}
\ No newline at end of file
diff --git a/src/Hexecs/Actors/Systems/UpdateSystem.cs b/src/Hexecs/Actors/Systems/UpdateSystem.cs
index d7243b0..e986b3f 100644
--- a/src/Hexecs/Actors/Systems/UpdateSystem.cs
+++ b/src/Hexecs/Actors/Systems/UpdateSystem.cs
@@ -1,7 +1,6 @@
using Hexecs.Assets;
using Hexecs.Dependencies;
using Hexecs.Loggers;
-using Hexecs.Threading;
using Hexecs.Values;
using Hexecs.Worlds;
diff --git a/src/Hexecs/Actors/Systems/UpdateSystem1.cs b/src/Hexecs/Actors/Systems/UpdateSystem1.cs
index e6431a5..519f970 100644
--- a/src/Hexecs/Actors/Systems/UpdateSystem1.cs
+++ b/src/Hexecs/Actors/Systems/UpdateSystem1.cs
@@ -35,9 +35,12 @@ protected virtual void AfterUpdate(in WorldTime time)
{
}
- protected virtual void BeforeUpdate(in WorldTime time)
- {
- }
+ ///
+ /// Метод запускается до полного обновления
+ ///
+ /// Время мира
+ /// Если возвращает false, то обновление не происходит
+ protected virtual bool BeforeUpdate(in WorldTime time) => true;
public sealed override void Update(in WorldTime time)
{
@@ -46,7 +49,7 @@ public sealed override void Update(in WorldTime time)
var length = Filter.Length;
if (length > 0)
{
- BeforeUpdate(in time);
+ if (!BeforeUpdate(in time)) return;
if (_parallelWorker == null)
{
diff --git a/src/Hexecs/Actors/Systems/UpdateSystem2.cs b/src/Hexecs/Actors/Systems/UpdateSystem2.cs
index 4ace729..b97e396 100644
--- a/src/Hexecs/Actors/Systems/UpdateSystem2.cs
+++ b/src/Hexecs/Actors/Systems/UpdateSystem2.cs
@@ -36,9 +36,12 @@ protected virtual void AfterUpdate(in WorldTime time)
{
}
- protected virtual void BeforeUpdate(in WorldTime time)
- {
- }
+ ///
+ /// Метод запускается до полного обновления
+ ///
+ /// Время мира
+ /// Если возвращает false, то обновление не происходит
+ protected virtual bool BeforeUpdate(in WorldTime time) => true;
public sealed override void Update(in WorldTime time)
{
diff --git a/src/Hexecs/Actors/Systems/UpdateSystem3.cs b/src/Hexecs/Actors/Systems/UpdateSystem3.cs
index 6376adf..7e34e0b 100644
--- a/src/Hexecs/Actors/Systems/UpdateSystem3.cs
+++ b/src/Hexecs/Actors/Systems/UpdateSystem3.cs
@@ -38,9 +38,12 @@ protected virtual void AfterUpdate(in WorldTime time)
{
}
- protected virtual void BeforeUpdate(in WorldTime time)
- {
- }
+ ///
+ /// Метод запускается до полного обновления
+ ///
+ /// Время мира
+ /// Если возвращает false, то обновление не происходит
+ protected virtual bool BeforeUpdate(in WorldTime time) => true;
public sealed override void Update(in WorldTime time)
{
diff --git a/src/Hexecs/Assets/AssetConstraint.Builder.cs b/src/Hexecs/Assets/AssetConstraint.Builder.cs
index 469d380..12f8eed 100644
--- a/src/Hexecs/Assets/AssetConstraint.Builder.cs
+++ b/src/Hexecs/Assets/AssetConstraint.Builder.cs
@@ -26,7 +26,7 @@ public AssetConstraint Build()
var subscriptions = new Subscription[_length];
Array.Copy(_subscriptions, subscriptions, _length);
- var instance = new AssetConstraint(_hash, _subscriptions);
+ var instance = new AssetConstraint(_hash, subscriptions);
ArrayPool.Shared.Return(_subscriptions, true);
@@ -78,7 +78,7 @@ private void AddSubscription(bool include, IAssetComponentPool pool, Func.Id;
// ReSharper disable once ForeachCanBePartlyConvertedToQueryUsingAnotherGetEnumerator
- foreach (var exists in _subscriptions)
+ foreach (var exists in _subscriptions.AsSpan(0, _length))
{
if (exists.ComponentId == id) AssetError.ConstraintExists();
}
@@ -91,9 +91,9 @@ private void AddSubscription(bool include, IAssetComponentPool pool, Func
{
+ ///
+ /// Создает построитель ограничений с исключением указанного компонента.
+ ///
+ /// Тип компонента, который должен отсутствовать у ассета
+ /// Контекст ассетов
+ /// Построитель ограничений
+ public static Builder Exclude(AssetContext context) where T1 : struct, IAssetComponent
+ {
+ return new Builder(context).Exclude();
+ }
+
+ ///
+ /// Создает построитель ограничений с включением указанного компонента.
+ ///
+ /// Тип компонента, который должен присутствовать у ассета
+ /// Контекст ассетов
+ /// Построитель ограничений
+ public static Builder Include(AssetContext context) where T1 : struct, IAssetComponent
+ {
+ return new Builder(context).Include();
+ }
+
+ ///
+ /// Создает построитель ограничений с включением двух указанных компонентов.
+ ///
+ /// Первый тип компонента, который должен присутствовать у ассета
+ /// Второй тип компонента, который должен присутствовать у ассета
+ /// Контекст актёров
+ /// Построитель ограничений
+ public static Builder Include(AssetContext context)
+ where T1 : struct, IAssetComponent
+ where T2 : struct, IAssetComponent
+ {
+ return new Builder(context)
+ .Include()
+ .Include();
+ }
+
+ ///
+ /// Создает построитель ограничений с включением трех указанных компонентов.
+ ///
+ /// Первый тип компонента, который должен присутствовать у ассета
+ /// Второй тип компонента, который должен присутствовать у ассета
+ /// Третий тип компонента, который должен присутствовать у ассета
+ /// Контекст актёров
+ /// Построитель ограничений
+ public static Builder Include(AssetContext context)
+ where T1 : struct, IAssetComponent
+ where T2 : struct, IAssetComponent
+ where T3 : struct, IAssetComponent
+ {
+ return new Builder(context)
+ .Include()
+ .Include()
+ .Include();
+ }
+
private readonly int _hash;
private readonly Subscription[] _subscriptions;
diff --git a/src/Hexecs/Assets/AssetContext.Components.cs b/src/Hexecs/Assets/AssetContext.Components.cs
index 3d53bd3..1344a18 100644
--- a/src/Hexecs/Assets/AssetContext.Components.cs
+++ b/src/Hexecs/Assets/AssetContext.Components.cs
@@ -21,7 +21,7 @@ public ComponentEnumerator Components(uint assetId)
ref var entry = ref GetEntry(assetId);
return Unsafe.IsNullRef(ref entry)
? ComponentEnumerator.Empty
- : new ComponentEnumerator(assetId, _componentPools, entry.AsReadOnlySpan());
+ : new ComponentEnumerator(assetId, _componentPools, entry.ToArray());
}
///
diff --git a/src/Hexecs/Assets/AssetContext.Dictionary.cs b/src/Hexecs/Assets/AssetContext.Dictionary.cs
index 46a8fda..fb6823b 100644
--- a/src/Hexecs/Assets/AssetContext.Dictionary.cs
+++ b/src/Hexecs/Assets/AssetContext.Dictionary.cs
@@ -18,6 +18,16 @@ private ref Entry AddEntry(uint id)
return ref entry;
}
+ private void ClearEntries()
+ {
+ foreach (var value in _entries.Values)
+ {
+ value.Dispose();
+ }
+
+ _entries.Clear();
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private ref Entry GetEntry(uint id) => ref CollectionsMarshal.GetValueRefOrNullRef(_entries, id);
diff --git a/src/Hexecs/Assets/AssetContext.Entry.cs b/src/Hexecs/Assets/AssetContext.Entry.cs
index 48b8688..ab8378d 100644
--- a/src/Hexecs/Assets/AssetContext.Entry.cs
+++ b/src/Hexecs/Assets/AssetContext.Entry.cs
@@ -1,21 +1,128 @@
namespace Hexecs.Assets;
+[DebuggerDisplay("Length = {Length}")]
public sealed partial class AssetContext
{
- private struct Entry
+ [method: MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private struct Entry()
{
- private ushort[]? _array;
- private int _length;
+ private const int InlineArraySize = 6;
- public void Add(ushort componentId)
+ public int Length
{
- ArrayUtils.InsertOrCreate(ref _array, _length, componentId);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => _length;
+ }
+
+ private InlineItemArray _inlineArray;
+ private int _length = 0;
+ private ushort[] _array = [];
+
+ public void Add(ushort item)
+ {
+ if (_length < InlineArraySize) _inlineArray[_length] = item;
+ else ArrayUtils.Insert(ref _array, ArrayPool.Shared, _length - InlineArraySize, item);
+
_length++;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
- public readonly ReadOnlySpan AsReadOnlySpan() => _length == 0
- ? ReadOnlySpan.Empty
- : new ReadOnlySpan(_array, 0, _length);
+ public readonly bool Contains(ushort item) => IndexOf(item) > -1;
+
+ public void Dispose()
+ {
+ if (_array is { Length: > 0 }) ArrayPool.Shared.Return(_array);
+ _array = [];
+ _length = 0;
+ }
+
+ public ComponentBucketEnumerator GetEnumerator()
+ {
+ ref var reference = ref Unsafe.As(ref _inlineArray);
+ var span = MemoryMarshal.CreateSpan(ref reference, InlineArraySize);
+ return new ComponentBucketEnumerator(span, _array, _length);
+ }
+
+ public readonly int IndexOf(ushort item)
+ {
+ if (_length == 0) return -1;
+
+ var inlineLength = Math.Min(_length, InlineArraySize);
+ for (var i = 0; i < inlineLength; i++)
+ {
+ if (_inlineArray[i] == item)
+ {
+ return i;
+ }
+ }
+
+ if (_array == null || _array.Length == 0) return -1;
+
+ var span = _array.AsSpan(0, _length - InlineArraySize);
+ for (var i = 0; i < span.Length; i++)
+ {
+ if (span[i] == item) return InlineArraySize + i;
+ }
+
+ return -1;
+ }
+
+ public ushort this[int index]
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ readonly get => index < InlineArraySize ? _inlineArray[index] : _array[index - InlineArraySize];
+ set
+ {
+ if (index < InlineArraySize) _inlineArray[index] = value;
+ else _array[index - InlineArraySize] = value;
+ }
+ }
+
+ public readonly ushort[] ToArray()
+ {
+ if (_length == 0) return [];
+
+ var result = ArrayUtils.Create(_length);
+ for (var i = 0; i < _length; i++)
+ {
+ result[i] = this[i];
+ }
+
+ return result;
+ }
+
+ public ref struct ComponentBucketEnumerator
+ {
+ public readonly ref ushort Current
+ {
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ get => ref _index < InlineArraySize
+ ? ref _inlineArray[_index]
+ : ref _array[_index - InlineArraySize];
+ }
+
+ private readonly Span _inlineArray;
+ private readonly ushort[] _array;
+ private readonly int _length;
+ private int _index;
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ internal ComponentBucketEnumerator(Span inlineArray, ushort[] array, int length)
+ {
+ _inlineArray = inlineArray;
+ _array = array;
+ _length = length;
+ _index = -1;
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public bool MoveNext() => ++_index < _length;
+ }
+
+ [InlineArray(InlineArraySize)]
+ private struct InlineItemArray
+ {
+ private ushort _item;
+ }
}
}
\ No newline at end of file
diff --git a/src/Hexecs/Assets/AssetContext.cs b/src/Hexecs/Assets/AssetContext.cs
index ca9fb36..11f473e 100644
--- a/src/Hexecs/Assets/AssetContext.cs
+++ b/src/Hexecs/Assets/AssetContext.cs
@@ -7,11 +7,13 @@ namespace Hexecs.Assets;
/// Контекст ассетов, управляющий их жизненным циклом и содержащий коллекции их компонентов.
///
[DebuggerDisplay("Length = {Length}")]
-public sealed partial class AssetContext : IEnumerable
+public sealed partial class AssetContext : IEnumerable, IDisposable
{
public readonly World World;
private readonly Dictionary _aliases;
+
+ private bool _disposed;
internal AssetContext(World world, int capacity = 256)
{
@@ -26,6 +28,16 @@ internal AssetContext(World world, int capacity = 256)
_filtersWithConstraint = new List(8);
}
+ public void Dispose()
+ {
+ if (_disposed) return;
+ _disposed = true;
+
+ ClearEntries();
+
+ _aliases.Clear();
+ }
+
///
/// Проверяет существование ассета с указанным идентификатором.
///
@@ -134,8 +146,6 @@ public Asset GetAsset(string alias)
public AssetRef GetAssetRef(uint assetId)
where T1 : struct, IAssetComponent
{
- Debug.Assert(ExistsAsset(assetId), $"Asset {assetId} isn't found");
-
var pool = GetComponentPool();
if (pool == null) AssetError.ComponentNotFound(assetId);
@@ -172,7 +182,7 @@ public void GetDescription(uint assetId, ref ValueStringBuilder builder, int max
builder.Append("Id = ");
builder.Append(assetId);
- var components = entry.AsReadOnlySpan();
+ ref var components = ref entry;
var componentsLength = components.Length;
if (componentsLength == 0) return;
diff --git a/src/Hexecs/Dependencies/IDependencyCollection.cs b/src/Hexecs/Dependencies/IDependencyCollection.cs
index 6eb05ea..e06a1c2 100644
--- a/src/Hexecs/Dependencies/IDependencyCollection.cs
+++ b/src/Hexecs/Dependencies/IDependencyCollection.cs
@@ -6,15 +6,15 @@ public interface IDependencyCollection
IDependencyCollection AddRegistrar(IDependencyRegistrar registrar);
- IDependencyCollection Singleton(Type contract, Func resolver);
+ IDependencyCollection UseSingleton(Type contract, Func resolver);
- IDependencyCollection Singleton(Func resolver) where T : class;
+ IDependencyCollection UseSingleton(Func resolver) where T : class;
- IDependencyCollection Scoped(Type contract, Func resolver);
+ IDependencyCollection UseScoped(Type contract, Func resolver);
- IDependencyCollection Scoped(Func resolver) where T : class;
+ IDependencyCollection UseScoped(Func resolver) where T : class;
- IDependencyCollection Transient(Type contract, Func resolver);
+ IDependencyCollection UseTransient(Type contract, Func resolver);
- IDependencyCollection Transient(Func resolver) where T : class;
+ IDependencyCollection UseTransient(Func resolver) where T : class;
}
\ No newline at end of file
diff --git a/src/Hexecs/Hexecs.csproj b/src/Hexecs/Hexecs.csproj
index 1799564..2d4a2d0 100644
--- a/src/Hexecs/Hexecs.csproj
+++ b/src/Hexecs/Hexecs.csproj
@@ -31,7 +31,7 @@
-
+
@@ -240,6 +240,9 @@
ActorFilter1.cs
+
+ WorldBuilder.cs
+
diff --git a/src/Hexecs/Loggers/LogBuilder.cs b/src/Hexecs/Loggers/LogBuilder.cs
index 01d026a..3560cf3 100644
--- a/src/Hexecs/Loggers/LogBuilder.cs
+++ b/src/Hexecs/Loggers/LogBuilder.cs
@@ -32,8 +32,7 @@ internal LogBuilder()
new KeyValuePair(typeof(Actor), ActorLogWriter.Instance),
new KeyValuePair(typeof(Asset), AssetLogWriter.Instance),
new KeyValuePair(typeof(ActorId), ActorIdLogWriter.Instance),
- new KeyValuePair(typeof(AssetId), AssetIdLogWriter.Instance),
- new KeyValuePair(typeof(Money), new DefaultMoneyWriter()),
+ new KeyValuePair(typeof(AssetId), AssetIdLogWriter.Instance)
], ReferenceComparer.Instance);
_valueFactories = new Queue(4);
diff --git a/src/Hexecs/Loggers/Writers/DefaultMoneyWriter.cs b/src/Hexecs/Loggers/Writers/DefaultMoneyWriter.cs
deleted file mode 100644
index fa32f64..0000000
--- a/src/Hexecs/Loggers/Writers/DefaultMoneyWriter.cs
+++ /dev/null
@@ -1,11 +0,0 @@
-using System.Globalization;
-
-namespace Hexecs.Loggers.Writers;
-
-internal sealed class DefaultMoneyWriter : ILogValueWriter
-{
- public void Write(ref ValueStringBuilder stringBuilder, Money arg)
- {
- stringBuilder.Append(arg.Value, "N2", CultureInfo.InvariantCulture);
- }
-}
\ No newline at end of file
diff --git a/src/Hexecs/Serializations/JsonWriterExtensions.cs b/src/Hexecs/Serializations/JsonWriterExtensions.cs
index 749ca1f..3833b3b 100644
--- a/src/Hexecs/Serializations/JsonWriterExtensions.cs
+++ b/src/Hexecs/Serializations/JsonWriterExtensions.cs
@@ -7,140 +7,138 @@ namespace Hexecs.Serializations;
public static class JsonWriterExtensions
{
- public static Utf8JsonWriter WriteProperty(
- this Utf8JsonWriter writer,
- string propertyName,
- Action value)
+ extension(Utf8JsonWriter writer)
{
- writer.WritePropertyName(propertyName);
- value(writer);
- return writer;
- }
-
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer,
- string propertyName,
- in TArray array,
- Action value)
- where TArray: struct, IArray
- {
- writer.WritePropertyName(propertyName);
- writer.WriteStartArray();
-
- for (var i = 0; i < array.Length; i++)
+ public Utf8JsonWriter WriteProperty(string propertyName,
+ Action value)
{
- value(writer, array[i]);
+ writer.WritePropertyName(propertyName);
+ value(writer);
+ return writer;
}
- writer.WriteEndArray();
+ public Utf8JsonWriter WriteProperty(string propertyName,
+ in TArray array,
+ Action value)
+ where TArray: struct, IArray
+ {
+ writer.WritePropertyName(propertyName);
+ writer.WriteStartArray();
- return writer;
- }
+ for (var i = 0; i < array.Length; i++)
+ {
+ value(writer, array[i]);
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, ActorId value)
- {
- writer.WriteNumber(propertyName, value.Value);
- return writer;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, ActorId value)
- where T : struct, IActorComponent
- {
- writer.WriteNumber(propertyName, value.Value);
- return writer;
- }
+ writer.WriteEndArray();
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, AssetId value)
- {
- writer.WriteNumber(propertyName, value.Value);
- return writer;
- }
+ return writer;
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, AssetId value)
- where T : struct, IAssetComponent
- {
- writer.WriteNumber(propertyName, value.Value);
- return writer;
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, ActorId value)
+ {
+ writer.WriteNumber(propertyName, value.Value);
+ return writer;
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, bool value)
- {
- writer.WriteBoolean(propertyName, value);
- return writer;
- }
-
- [SkipLocalsInit]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, DateOnly value)
- {
- writer.WritePropertyName(propertyName);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, ActorId value)
+ where T : struct, IActorComponent
+ {
+ writer.WriteNumber(propertyName, value.Value);
+ return writer;
+ }
- Span buffer = stackalloc char[68];
- if (value.TryFormat(buffer, out var charsWritten))
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, AssetId value)
{
- writer.WriteStringValue(buffer[..charsWritten]);
+ writer.WriteNumber(propertyName, value.Value);
+ return writer;
}
- else
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, AssetId value)
+ where T : struct, IAssetComponent
{
- writer.WriteStringValue(string.Empty);
+ writer.WriteNumber(propertyName, value.Value);
+ return writer;
}
- return writer;
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, bool value)
+ {
+ writer.WriteBoolean(propertyName, value);
+ return writer;
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, int value)
- {
- writer.WriteNumber(propertyName, value);
- return writer;
- }
+ [SkipLocalsInit]
+ public Utf8JsonWriter WriteProperty(string propertyName, DateOnly value)
+ {
+ writer.WritePropertyName(propertyName);
+
+ Span buffer = stackalloc char[68];
+ if (value.TryFormat(buffer, out var charsWritten))
+ {
+ writer.WriteStringValue(buffer[..charsWritten]);
+ }
+ else
+ {
+ writer.WriteStringValue(string.Empty);
+ }
+
+ return writer;
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, long value)
- {
- writer.WriteNumber(propertyName, value);
- return writer;
- }
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, string value)
- {
- writer.WriteString(propertyName, value);
- return writer;
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, int value)
+ {
+ writer.WriteNumber(propertyName, value);
+ return writer;
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, Type value)
- {
- writer.WriteString(propertyName, value.FullName);
- return writer;
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, long value)
+ {
+ writer.WriteNumber(propertyName, value);
+ return writer;
+ }
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Utf8JsonWriter WriteProperty(this Utf8JsonWriter writer, string propertyName, uint value)
- {
- writer.WriteNumber(propertyName, value);
- return writer;
- }
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, string value)
+ {
+ writer.WriteString(propertyName, value);
+ return writer;
+ }
- internal static Utf8JsonWriter WriteProperty(
- this Utf8JsonWriter writer,
- string propertyName,
- ref readonly InlineBucket value)
- {
- writer.WritePropertyName(propertyName);
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, Type value)
+ {
+ writer.WriteString(propertyName, value.FullName);
+ return writer;
+ }
- writer.WriteStartArray();
- foreach (var element in value)
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public Utf8JsonWriter WriteProperty(string propertyName, uint value)
{
- writer.WriteNumberValue(element);
+ writer.WriteNumber(propertyName, value);
+ return writer;
}
- writer.WriteEndArray();
+ internal Utf8JsonWriter WriteProperty(string propertyName,
+ ref readonly InlineBucket value)
+ {
+ writer.WritePropertyName(propertyName);
+
+ writer.WriteStartArray();
+ foreach (var element in value)
+ {
+ writer.WriteNumberValue(element);
+ }
+
+ writer.WriteEndArray();
- return writer;
+ return writer;
+ }
}
}
\ No newline at end of file
diff --git a/src/Hexecs/Utils/Args.cs b/src/Hexecs/Utils/Args.cs
index b533803..d0c0d15 100644
--- a/src/Hexecs/Utils/Args.cs
+++ b/src/Hexecs/Utils/Args.cs
@@ -45,6 +45,29 @@ public TValue Get(string name)
return value;
}
+ ///
+ /// Получает значение аргумента по имени.
+ /// Выбрасывает исключение, если значение не найдено.
+ ///
+ /// Тип значения.
+ /// Имя аргумента.
+ public TValue GetOrDefault(string name)
+ {
+ return TryGet(name, out var value) ? value : default!;
+ }
+
+ ///
+ /// Получает значение аргумента по имени.
+ /// Выбрасывает исключение, если значение не найдено.
+ ///
+ /// Тип значения.
+ /// Имя аргумента.
+ /// Значение по умолчанию
+ public TValue GetOrDefault(string name, TValue defaultValue)
+ {
+ return TryGet(name, out var value) ? value : defaultValue;
+ }
+
///
/// Возвращает экземпляр Args в пул после использования.
/// Очищает все хранилища значений и возвращает их в соответствующие пулы.
diff --git a/src/Hexecs/Utils/Money.cs b/src/Hexecs/Utils/Money.cs
deleted file mode 100644
index c8a4d04..0000000
--- a/src/Hexecs/Utils/Money.cs
+++ /dev/null
@@ -1,314 +0,0 @@
-using System.Globalization;
-using System.Numerics;
-
-namespace Hexecs.Utils;
-
-///
-/// Представляет денежную сумму с точностью до двух знаков после запятой.
-/// Внутренне хранит значение в наименьших единицах валюты (копейках).
-/// Обеспечивает арифметические операции, сравнение и преобразование между различными числовыми форматами.
-///
-///
-/// Денежные значения хранятся как целое число (), представляющее сотые доли (копейки),
-/// что позволяет избежать проблем с точностью при использовании чисел с плавающей точкой.
-///
-[DebuggerDisplay("{ToString()}")]
-[method: MethodImpl(MethodImplOptions.AggressiveInlining)]
-public readonly struct Money(long value) :
- IComparable, IEquatable,
- IMinMaxValue,
- ISpanFormattable, IUtf8SpanFormattable
-{
- ///
- /// Максимальное значение
- ///
- public static readonly Money MaxValue = Create(long.MaxValue / 100L - 1, 99);
-
- ///
- /// Минимальное значение
- ///
- public static readonly Money MinValue = Create(long.MinValue / 100L + 1, 99);
-
- ///
- /// Создает экземпляр структуры Money с указанными значениями целой и дробной части.
- ///
- /// Целая часть суммы денег.
- /// Дробная часть суммы денег (от 0 до 99).
- /// Новый экземпляр структуры Money.
- /// Если дробная часть не находится в диапазоне от 0 до 99.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public static Money Create(long whole, int? fraction = null)
- {
- var result = whole * 100;
-
- if (fraction is < 0 or > 99) ThrowOverflow();
-
- // ReSharper disable once InvertIf
- if (fraction.HasValue)
- {
- if (whole >= 0) result += fraction.Value;
- else result -= fraction.Value;
- }
-
- return new Money(result);
- }
-
- ///
- /// Пытается преобразовать строковое представление суммы денег в эквивалентный экземпляр структуры Money.
- ///
- /// Строка, содержащая сумму денег для преобразования.
- /// При успешном выполнении содержит значение типа Money, эквивалентное строке s.
- /// True, если s успешно преобразована; иначе false.
- public static bool TryParse(ReadOnlySpan s, out Money result)
- {
- if (double.TryParse(s, CultureInfo.InvariantCulture, out var doubleValue))
- {
- result = new Money((long)(doubleValue * 100));
- return true;
- }
-
- result = Zero;
- return false;
- }
-
- ///
- /// Получает экземпляр структуры Money со значением ноль.
- ///
- public static Money Zero
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => new(0);
- }
-
- ///
- /// Дробная часть
- ///
- ///
- /// Это остаток от деления на 100
- ///
- public int Fraction
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get
- {
- var fraction = Value % 100;
- return Value >= 0 ? (int)fraction : (int)-fraction;
- }
- }
-
- ///
- /// Целая часть
- ///
- ///
- /// Это результат целочисленного деления на 100
- ///
- public long Whole
- {
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- get => Value / 100;
- }
-
- ///
- /// Внутреннее значение, представляющее деньги в наименьших единицах (копейках).
- ///
- public readonly long Value = value;
-
- ///
- /// Возвращает абсолютное значение суммы.
- ///
- /// Абсолютное значение текущей суммы.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public Money Abs() => new(Math.Abs(Value));
-
- ///
- /// Возвращает минимальное значение из текущей суммы и указанного параметра.
- ///
- /// Сумма для сравнения.
- /// Минимальное из двух значений.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public Money Min(in Money b) => Value < b.Value ? this : b;
-
- ///
- /// Возвращает максимальное значение из текущей суммы и указанного параметра.
- ///
- /// Сумма для сравнения.
- /// Максимальное из двух значений.
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- public Money Max(in Money b) => Value > b.Value ? this : b;
-
- ///
- /// Возвращает строковое представление суммы в формате с двумя десятичными знаками.
- ///
- /// Строковое представление суммы.
- public override string ToString() => (Value / 100.0).ToString("N2", CultureInfo.InvariantCulture);
-
- ///
- /// Форматирует значение в строковом представлении с использованием указанного формата и провайдера форматирования.
- ///
- /// Строка формата (поддерживаются "G", "F", "N" или null для формата по умолчанию).
- /// Объект, который предоставляет информацию о форматировании.
- /// Строка, представляющая форматированное значение Money.
- public string ToString(string? format, IFormatProvider? formatProvider)
- {
- formatProvider ??= CultureInfo.InvariantCulture;
- format ??= "N2";
-
- return (Value / 100.0).ToString(format, formatProvider);
- }
-
- ///
- /// Пытается форматировать значение в буфере символов, используя указанный формат и провайдер форматирования.
- ///
- /// Буфер символов для форматирования результата.
- /// Количество записанных символов в буфер.
- /// Строка формата
- /// Объект, который предоставляет информацию о форматировании.
- /// True, если форматирование выполнено успешно; в противном случае — false.
- public bool TryFormat(
- Span destination,
- out int charsWritten,
- ReadOnlySpan format,
- IFormatProvider? formatProvider)
- {
- formatProvider ??= CultureInfo.InvariantCulture;
- var formatString = format.IsEmpty ? "N2" : format;
- var decimalValue = Value / 100.0M;
-
- // Используем стандартный метод форматирования double для вывода в буфер
- return decimalValue.TryFormat(destination, out charsWritten, formatString, formatProvider);
- }
-
- ///