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); - } - - /// - /// Пытается форматировать значение в виде последовательности байтов UTF-8 в предоставленном буфере. - /// - /// Буфер байтов UTF-8 для вывода форматированного значения. - /// Количество байтов, записанных в буфер. - /// Строка формата. - /// Объект, предоставляющий информацию о форматировании. - /// True, если форматирование выполнено успешно; в противном случае — false. - public bool TryFormat( - Span utf8Destination, - out int bytesWritten, - ReadOnlySpan format, - IFormatProvider? formatProvider) - { - formatProvider ??= CultureInfo.InvariantCulture; - var formatString = format.IsEmpty ? "N2" : format.ToString(); - var decimalValue = Value / 100.0M; - - return decimalValue.TryFormat(utf8Destination, out bytesWritten, formatString, formatProvider); - } - - [DoesNotReturn] - private static void ThrowOverflow() => throw new OverflowException("Fraction should be between 0 and 100"); - - #region + and - - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator +(in Money left, in Money right) => new(left.Value + right.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator -(in Money left, in Money right) => new(left.Value - right.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator -(in Money money) => new(-money.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator +(in Money money) => money; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator +(in Money left, in long right) => new(left.Value + right); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator -(in Money left, in long right) => new(left.Value - right); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator +(in long left, in Money right) => new(left + right.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator -(in long left, in Money right) => new(left - right.Value); - - #endregion - - #region * and / - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator *(in Money left, in Money right) => new(left.Value * right.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator /(in Money left, in Money right) => new(left.Value / right.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator *(in long left, in Money right) => new(left * right.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator /(in long left, in Money right) => new(left / right.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator *(in Money left, in long right) => new(left.Value * right); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static Money operator /(in Money left, in long right) => new(left.Value / right); - - #endregion - - #region Equality - - public int CompareTo(Money other) => Value.CompareTo(other.Value); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public bool Equals(Money other) => Value == other.Value; - - public override bool Equals(object? obj) => obj is Money other && Equals(other); - - public override int GetHashCode() => Value.GetHashCode(); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator ==(in Money left, in Money right) => left.Equals(right); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator !=(in Money left, in Money right) => !left.Equals(right); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator >(in Money left, in Money right) => left.Value > right.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator >=(in Money left, in Money right) => left.Value >= right.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator <(in Money left, in Money right) => left.Value < right.Value; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static bool operator <=(in Money left, in Money right) => left.Value <= right.Value; - - #endregion - - #region implicit - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator float(in Money money) => money.Value / 100f; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator double(in Money money) => money.Value / 100.0; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator decimal(in Money money) => money.Value / 100m; - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator Money(float value) => new((long)(value * 100)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator Money(double value) => new((long)(value * 100)); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static implicit operator Money(decimal value) => new((long)(value * 100)); - - #endregion - - #region interfaces - - static Money IMinMaxValue.MaxValue => MaxValue; - static Money IMinMaxValue.MinValue => MinValue; - - #endregion -} \ No newline at end of file diff --git a/src/Hexecs/Worlds/Dice.cs b/src/Hexecs/Worlds/Dice.cs index 88eb8ab..f475ad0 100644 --- a/src/Hexecs/Worlds/Dice.cs +++ b/src/Hexecs/Worlds/Dice.cs @@ -41,6 +41,9 @@ public int GetNext(int start, int end) return (int)value + start; } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public double GetNextDouble() => GetNext() / 32768.0; + #region Roll /// diff --git a/src/Hexecs/Worlds/WorldBuilder.Extensions.cs b/src/Hexecs/Worlds/WorldBuilder.Extensions.cs new file mode 100644 index 0000000..c551887 --- /dev/null +++ b/src/Hexecs/Worlds/WorldBuilder.Extensions.cs @@ -0,0 +1,14 @@ +using Hexecs.Dependencies; + +namespace Hexecs.Worlds; + +public static class WorldBuilderExtensions +{ + public static WorldBuilder UseSingleton< + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces | + DynamicallyAccessedMemberTypes.PublicConstructors)] + TService>(this WorldBuilder builder) where TService : class + { + return builder.UseSingleton(ctx => (TService)ctx.Activate(typeof(TService))); + } +} \ No newline at end of file diff --git a/src/Hexecs/Worlds/WorldBuilder.cs b/src/Hexecs/Worlds/WorldBuilder.cs index dff5b2b..d44e497 100644 --- a/src/Hexecs/Worlds/WorldBuilder.cs +++ b/src/Hexecs/Worlds/WorldBuilder.cs @@ -25,9 +25,9 @@ public WorldBuilder Add(DependencyLifetime lifetime, Type contract, Func Singleton(contract, resolver), - DependencyLifetime.Scoped => Scoped(contract, resolver), - DependencyLifetime.Transient => Transient(contract, resolver), + DependencyLifetime.Singleton => UseSingleton(contract, resolver), + DependencyLifetime.Scoped => UseScoped(contract, resolver), + DependencyLifetime.Transient => UseTransient(contract, resolver), _ => DependencyError.NotSupportedLifetime(lifetime) }; } @@ -38,16 +38,45 @@ public WorldBuilder AddRegistrar(IDependencyRegistrar registrar) return this; } + public World Build() + { + foreach (var registrar in _registrars) + { + registrar.TryRegister(this); + } + + _registrars.Clear(); + + var dependencyProvider = new DependencyProvider(_dependencies + .GroupBy(static dep => dep.Contract) + .Select(static group => new KeyValuePair(group.Key, group.ToArray())) + .ToDictionary(ReferenceComparer.Instance), null); + + var instance = new World( + _configurationService ?? ConfigurationService.Empty, + _logService ?? LogService.Empty, + dependencyProvider, + _defaultActorContextBuilder ?? DelegateUtils.EmptyAction, + _valueStorage ?? ValueService.Empty); + + if (_debugWorld) WorldDebug.World = instance; + + LoadAssets(instance); + + _dependencies.Clear(); + + return instance; + } #region AssetSource - public WorldBuilder AddAssetSource(IAssetSource source) + public WorldBuilder UseAddAssetSource(IAssetSource source) { _assetSourceBuilders.Add(_ => source); return this; } - public WorldBuilder AddAssetSource() where T : class, IAssetSource, new() + public WorldBuilder UseAddAssetSource() where T : class, IAssetSource, new() { _assetSourceBuilders.Add(static _ => new T()); return this; @@ -69,47 +98,57 @@ public WorldBuilder CreateAssetData(int order, Action source) #endregion - public World Build() + public WorldBuilder UseAsDebugWorld(bool value = true) { - foreach (var registrar in _registrars) - { - registrar.TryRegister(this); - } - - _registrars.Clear(); + _debugWorld = value; + return this; + } - var dependencyProvider = new DependencyProvider(_dependencies - .GroupBy(static dep => dep.Contract) - .Select(static group => new KeyValuePair(group.Key, group.ToArray())) - .ToDictionary(ReferenceComparer.Instance), null); + public WorldBuilder UseConfiguration(Action configuration) + { + var builder = new ConfigurationBuilder(); + configuration(builder); - var instance = new World( - _configurationService ?? ConfigurationService.Empty, - _logService ?? LogService.Empty, - dependencyProvider, - _defaultActorContextBuilder ?? DelegateUtils.EmptyAction, - _valueStorage ?? ValueService.Empty); + _configurationService = builder.Build(); - if (_debugWorld) WorldDebug.World = instance; + return this; + } - LoadAssets(instance); + public WorldBuilder UseDefaultActorContext(Action defaultActorContext) + { + _defaultActorContextBuilder = defaultActorContext; + return this; + } - _dependencies.Clear(); + #region ParallelWorker - return instance; + public WorldBuilder UseDefaultParallelWorker(int? degreeOfParallelism = null) + { + return UseDefaultParallelWorker(ctx => + { + if (degreeOfParallelism == null) + { + var configuration = ctx.GetService(); + degreeOfParallelism = configuration?.GetValue("ParallelWorker:DegreeOfParallelism"); + } + + return new DefaultParallelWorker(degreeOfParallelism ?? Environment.ProcessorCount); + }); } - public WorldBuilder CreateConfiguration(Action configuration) + public WorldBuilder UseDefaultParallelWorker(IParallelWorker worker) { - var builder = new ConfigurationBuilder(); - configuration(builder); - - _configurationService = builder.Build(); + return UseDefaultParallelWorker(_ => worker); + } - return this; + public WorldBuilder UseDefaultParallelWorker(Func worker) + { + return UseSingleton(worker); } - public WorldBuilder CreateLogger(Action logger) + #endregion + + public WorldBuilder UseLogger(Action logger) { var builder = new LogBuilder(); logger(builder); @@ -119,7 +158,7 @@ public WorldBuilder CreateLogger(Action logger) return this; } - public WorldBuilder CreateValues(Action values) + public WorldBuilder UseValues(Action values) { var builder = new ValueServiceBuilder(); values(builder); @@ -129,62 +168,59 @@ public WorldBuilder CreateValues(Action values) return this; } - public WorldBuilder DebugWorld(bool value = true) - { - _debugWorld = value; - return this; - } + #region Singleton - public WorldBuilder DefaultActorContext(Action defaultActorContext) + public WorldBuilder UseSingleton(T value) + where T : class { - _defaultActorContextBuilder = defaultActorContext; + _dependencies.Add(new Dependency(DependencyLifetime.Singleton, typeof(T), _ => value)); return this; } - public WorldBuilder DefaultParallelWorker(int? degreeOfParallelism = null) - { - var value = degreeOfParallelism ?? Environment.ProcessorCount; - return DefaultParallelWorker(new DefaultParallelWorker(value)); - } - public WorldBuilder DefaultParallelWorker(IParallelWorker worker) - { - return Singleton(typeof(IParallelWorker), _ => worker); - } - - public WorldBuilder Singleton(Type contract, Func resolver) + public WorldBuilder UseSingleton(Type contract, Func resolver) { _dependencies.Add(new Dependency(DependencyLifetime.Singleton, contract, resolver)); return this; } - public WorldBuilder Singleton(Func resolver) where T : class + public WorldBuilder UseSingleton(Func resolver) where T : class { - return Singleton(typeof(T), resolver); + return UseSingleton(typeof(T), resolver); } - public WorldBuilder Scoped(Type contract, Func resolver) + #endregion + + #region Scoped + + public WorldBuilder UseScoped(Type contract, Func resolver) { _dependencies.Add(new Dependency(DependencyLifetime.Scoped, contract, resolver)); return this; } - public WorldBuilder Scoped(Func resolver) where T : class + public WorldBuilder UseScoped(Func resolver) where T : class { - return Scoped(typeof(T), resolver); + return UseScoped(typeof(T), resolver); } - public WorldBuilder Transient(Type contract, Func resolver) + #endregion + + #region Transient + + public WorldBuilder UseTransient(Type contract, Func resolver) { _dependencies.Add(new Dependency(DependencyLifetime.Transient, contract, resolver)); return this; } - public WorldBuilder Transient(Func resolver) where T : class + public WorldBuilder UseTransient(Func resolver) where T : class { - return Transient(typeof(T), resolver); + return UseTransient(typeof(T), resolver); } + #endregion + private void LoadAssets(World world) { var sortedAssetSources = _assetSourceBuilders @@ -211,34 +247,34 @@ IDependencyCollection IDependencyCollection.Add( return Add(lifetime, contract, resolver); } - IDependencyCollection IDependencyCollection.Singleton(Type contract, Func resolver) + IDependencyCollection IDependencyCollection.UseSingleton(Type contract, Func resolver) { - return Singleton(contract, resolver); + return UseSingleton(contract, resolver); } - IDependencyCollection IDependencyCollection.Singleton(Func resolver) where T : class + IDependencyCollection IDependencyCollection.UseSingleton(Func resolver) where T : class { - return Singleton(resolver); + return UseSingleton(resolver); } - IDependencyCollection IDependencyCollection.Scoped(Type contract, Func resolver) + IDependencyCollection IDependencyCollection.UseScoped(Type contract, Func resolver) { - return Scoped(contract, resolver); + return UseScoped(contract, resolver); } - IDependencyCollection IDependencyCollection.Scoped(Func resolver) where T : class + IDependencyCollection IDependencyCollection.UseScoped(Func resolver) where T : class { - return Scoped(resolver); + return UseScoped(resolver); } - IDependencyCollection IDependencyCollection.Transient(Type contract, Func resolver) + IDependencyCollection IDependencyCollection.UseTransient(Type contract, Func resolver) { - return Transient(contract, resolver); + return UseTransient(contract, resolver); } - IDependencyCollection IDependencyCollection.Transient(Func resolver) where T : class + IDependencyCollection IDependencyCollection.UseTransient(Func resolver) where T : class { - return Transient(resolver); + return UseTransient(resolver); } #endregion