Skip to content

Commit 1481176

Browse files
authored
Feature/0.4.3 (#2)
* fix actorcontext.entry
1 parent b3ef6d1 commit 1481176

20 files changed

Lines changed: 502 additions & 30 deletions

README.md

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,10 @@ DI необходим для того, чтобы всё это работало
1616
Так как всё в этом мире завязано на "мир" (`World`), то мы начнём с его создания.
1717
```csharp
1818
var world = new WorldBuilder()
19+
.DefaultParallelWorker(4) // чтобы обрабатывать сущности в 4 потока
1920
.DebugWorld() // чтобы получать больше информации в Debug-режиме
20-
.CreateLogger(logger => logger.UseConsoleSink()); // чтобы логи выводились в консоль
21-
.Build();
21+
.CreateLogger(logger => logger.UseConsoleSink()) // чтобы логи выводились в консоль
22+
.Build();
2223
```
2324
Этот код создаёт сконфигурированный объект World. Так как библиотека AOT-friendly, тут нет сканеров assembly для регистрации отдельных компонентов. Всё нужно регистрировать вручную.
2425

@@ -29,8 +30,8 @@ var world = new WorldBuilder()
2930

3031
```csharp
3132
public struct FlyComponent(int current, int max) : IActorComponent { // необходимо указать маркерный интерфейс
32-
public int CurrentSpeed;
33-
public int MaxSpeed;
33+
public int CurrentSpeed = current;
34+
public int MaxSpeed = max;
3435
}
3536
```
3637
Компоненты должны быть `struct`, чтобы всё в вашем мире могло работать эффективно и быстро.
@@ -125,4 +126,10 @@ Actor passenger = ...;
125126
plane.AddChild(passenger);
126127
```
127128

128-
Если повторить это много раз, то в самолёте будут сидеть 2-400 пассажиров (в зависимости от типа самолёта, конечно).
129+
Если повторить это много раз, то в самолёте будут сидеть множество пассажиров (в зависимости от типа самолёта, конечно).
130+
131+
## Системы
132+
133+
У нас же ecS, а значит у нас есть системы.
134+
Системы - это классы, которые обрабатывают компоненты актёров.
135+
Они могут быть зарегистрированы в мире и запущены для обработки компонентов.

src/Hexecs.Benchmarks.MonoGame/BenchmarkGame.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,10 @@ protected override void Initialize()
6666
_world = new WorldBuilder()
6767
.DefaultParallelWorker(Math.Min(6, Environment.ProcessorCount))
6868
.DefaultActorContext(builder => builder
69-
// Передаем зависимости явно через фабрику или DI
69+
.Capacity(InitialEntityCount)
70+
.ConfigureComponentPool<CircleColor>(color => color.Capacity(InitialEntityCount))
71+
.ConfigureComponentPool<Position>(position => position.Capacity(InitialEntityCount))
72+
.ConfigureComponentPool<Velocity>(velocity => velocity.Capacity(InitialEntityCount))
7073
.CreateUpdateSystem(ctx =>
7174
new MovementSystem(ctx, ctx.GetRequiredService<IParallelWorker>(), width, height))
7275
.CreateDrawSystem(ctx => new RenderSystem(ctx, GraphicsDevice, MaxEntityCount * 2)))

src/Hexecs.Benchmarks/Actors/UpdateSystemWithParallelWorkerBenchmark.cs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,21 @@ namespace Hexecs.Benchmarks.Actors;
1111
// .NET SDK 10.0.101
1212
// [Host] : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a
1313
// .NET 10.0 : .NET 10.0.1 (10.0.1, 10.0.125.57005), Arm64 RyuJIT armv8.0-a
14-
14+
//
1515
// Job=.NET 10.0 Runtime=.NET 10.0
16-
17-
// | Method | Mean | Ratio | Allocated | Alloc Ratio |
18-
// |-------------------- |----------:|------:|----------:|------------:|
19-
// | Hexecs_Parallel | 48.63 us | 1.00 | - | NA |
20-
// | DefaultEcs_Parallel | 117.87 us | 2.42 | - | NA |
16+
//
17+
// | Method | Mean | Ratio | Allocated | Alloc Ratio |
18+
// |-------------------- |-----------:|------:|----------:|------------:|
19+
// | Hexecs_Parallel | 769.8 us | 1.00 | - | NA |
20+
// | DefaultEcs_Parallel | 1,576.7 us | 2.05 | - | NA |
2121

2222
[SimpleJob(RuntimeMoniker.Net10_0)]
2323
[Orderer(SummaryOrderPolicy.FastestToSlowest)]
2424
[MeanColumn, MemoryDiagnoser]
2525
[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
2626
public class UpdateSystemWithParallelWorkerBenchmark
2727
{
28-
private const int Count = 100_000;
28+
private const int Count = 2_000_000;
2929

3030
private DefaultEcs.World _defaultWorld = null!;
3131
private DefaultEcsParallelSystem _defaultSystem = null!;

src/Hexecs.Benchmarks/Program.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
using BenchmarkDotNet.Running;
2-
using Hexecs.Benchmarks.Collections;
2+
using Hexecs.Benchmarks.Actors;
33

4-
BenchmarkRunner.Run<SparsePageDictionaryBenchmark>();
4+
BenchmarkRunner.Run<UpdateSystemWithParallelWorkerBenchmark>();
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
using Hexecs.Tests.Mocks;
2+
3+
namespace Hexecs.Tests.Actors;
4+
5+
public sealed class ActorConstraintShould(ActorTestFixture fixture) : IClassFixture<ActorTestFixture>
6+
{
7+
[Fact(DisplayName = "Должен успешно проходить проверку Applicable, если все условия соблюдены")]
8+
public void Should_Be_Applicable_When_Conditions_Met()
9+
{
10+
// Arrange
11+
var actor = fixture.CreateActor<Description>(); // Создаем актора с компонентом
12+
var constraint = ActorConstraint.Include<Description>(fixture.Actors).Build();
13+
14+
// Act
15+
var result = constraint.Applicable(actor.Id);
16+
17+
// Assert
18+
result.Should().BeTrue();
19+
}
20+
21+
[Fact(DisplayName = "Должен возвращать false в Applicable, если компонент исключен")]
22+
public void Should_Not_Be_Applicable_When_Excluded_Component_Exists()
23+
{
24+
// Arrange
25+
var actor = fixture.CreateActor<Description>();
26+
var constraint = ActorConstraint.Exclude<Description>(fixture.Actors).Build();
27+
28+
// Act
29+
var result = constraint.Applicable(actor.Id);
30+
31+
// Assert
32+
result.Should().BeFalse();
33+
}
34+
35+
[Fact(DisplayName = "Должен вызывать событие Added, когда актор начинает соответствовать фильтру")]
36+
public void Should_Raise_Added_When_Actor_Becomes_Valid()
37+
{
38+
// Arrange
39+
var actor = fixture.CreateActor(); // Пустой актор
40+
var constraint = ActorConstraint.Include<Description>(fixture.Actors).Build();
41+
42+
using var monitoredSubject = constraint.Monitor();
43+
44+
// Act
45+
actor.Add(new Description()); // Теперь актор соответствует Include<Description>
46+
47+
// Assert
48+
monitoredSubject.Should().Raise(nameof(constraint.Added))
49+
.WithArgs<uint>(id => id == actor.Id);
50+
}
51+
52+
[Fact(DisplayName = "Должен вызывать событие Removing, когда актор перестает соответствовать фильтру")]
53+
public void Should_Raise_Removing_When_Actor_Becomes_Invalid()
54+
{
55+
// Arrange
56+
var actor = fixture.CreateActor<Description>();
57+
var constraint = ActorConstraint.Include<Description>(fixture.Actors).Build();
58+
59+
using var monitoredSubject = constraint.Monitor();
60+
61+
// Act
62+
actor.Remove<Description>();
63+
64+
// Assert
65+
monitoredSubject.Should().Raise(nameof(constraint.Removing))
66+
.WithArgs<uint>(id => id == actor.Id);
67+
}
68+
69+
[Fact(DisplayName = "Builder должен выбрасывать исключение при добавлении дублирующегося компонента")]
70+
public void Builder_Should_Throw_On_Duplicate_Component()
71+
{
72+
// Arrange
73+
var builder = ActorConstraint.Include<Description>(fixture.Actors);
74+
75+
// Act
76+
var action = () => builder.Include<Description>();
77+
78+
// Assert
79+
action.Should().Throw<Exception>();
80+
}
81+
82+
[Fact(DisplayName = "Два одинаковых ограничения должны иметь одинаковый HashCode и быть равны")]
83+
public void Should_Implement_Equality_Correctly()
84+
{
85+
// Arrange
86+
var constraint1 = ActorConstraint.Include<Description>(fixture.Actors)
87+
.Exclude<Defence>()
88+
.Build();
89+
90+
var constraint2 = ActorConstraint.Include<Description>(fixture.Actors)
91+
.Exclude<Defence>()
92+
.Build();
93+
94+
// Assert
95+
constraint1
96+
.Should()
97+
.Be(constraint2);
98+
99+
constraint1.GetHashCode()
100+
.Should()
101+
.Be(constraint2.GetHashCode());
102+
}
103+
104+
[Fact(DisplayName = "Должен корректно работать с несколькими Include компонентами")]
105+
public void Should_Work_With_Multiple_Includes()
106+
{
107+
// Arrange
108+
var actor = fixture.CreateActor<Description>();
109+
var constraint = ActorConstraint.Include<Description, Defence>(fixture.Actors).Build();
110+
111+
// Act & Assert
112+
constraint.Applicable(actor.Id).Should().BeFalse("Нужны оба компонента");
113+
114+
actor.Add(new Defence());
115+
constraint.Applicable(actor.Id).Should().BeTrue("Теперь оба компонента на месте");
116+
}
117+
118+
[Fact(DisplayName = "Должен отписаться от всех событий пула при Dispose")]
119+
public void Should_Unsubscribe_On_Dispose()
120+
{
121+
// Arrange
122+
var actor = fixture.CreateActor();
123+
var constraint = ActorConstraint.Include<Description>(fixture.Actors).Build();
124+
125+
using var monitoredSubject = constraint.Monitor();
126+
127+
// Act
128+
((IDisposable)constraint).Dispose();
129+
actor.Add(new Description());
130+
131+
// Assert
132+
monitoredSubject.Should().NotRaise(nameof(constraint.Added));
133+
}
134+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
using Hexecs.Tests.Mocks;
2+
3+
namespace Hexecs.Tests.Actors;
4+
5+
public sealed class ActorDictionaryShould(ActorTestFixture fixture) : IClassFixture<ActorTestFixture>
6+
{
7+
[Fact(DisplayName = "Должен автоматически добавлять актора в словарь при добавлении компонента")]
8+
public void Should_Add_Actor_When_Component_Added()
9+
{
10+
// Arrange
11+
// Используем Name как ключ
12+
using var dict = new ActorDictionary<string, Description>(
13+
fixture.Actors,
14+
c => c.Name);
15+
16+
// Act
17+
var actor = fixture.CreateActor<Description>();
18+
var expectedName = actor.Get<Description>().Name;
19+
20+
// Assert
21+
dict
22+
.ContainsKey(expectedName)
23+
.Should()
24+
.BeTrue();
25+
26+
dict[expectedName]
27+
.Id.Should()
28+
.Be(actor.Id);
29+
}
30+
31+
[Fact(DisplayName = "Должен обновлять ключ в словаре при изменении компонента")]
32+
public void Should_Update_Key_When_Component_Updated()
33+
{
34+
// Arrange
35+
var actor = fixture.CreateActor();
36+
actor.Add(new Description { Name = "OldName" });
37+
38+
using var dict = new ActorDictionary<string, Description>(
39+
fixture.Actors,
40+
c => c.Name);
41+
42+
// Act
43+
// Обновляем компонент, что должно вызвать OnComponentUpdating
44+
actor.Update(new Description { Name = "NewName" });
45+
46+
// Assert
47+
dict
48+
.ContainsKey("OldName")
49+
.Should()
50+
.BeFalse();
51+
52+
dict
53+
.ContainsKey("NewName")
54+
.Should()
55+
.BeTrue();
56+
57+
dict["NewName"]
58+
.Id
59+
.Should()
60+
.Be(actor.Id);
61+
}
62+
63+
[Fact(DisplayName = "Должен удалять актора из словаря при удалении компонента")]
64+
public void Should_Remove_Actor_When_Component_Removed()
65+
{
66+
// Arrange
67+
var actor = fixture.CreateActor();
68+
actor.Add(new Description { Name = "ToRemove" });
69+
70+
using var dict = new ActorDictionary<string, Description>(
71+
fixture.Actors,
72+
c => c.Name);
73+
74+
// Act
75+
actor.Remove<Description>();
76+
77+
// Assert
78+
dict
79+
.ContainsKey("ToRemove")
80+
.Should()
81+
.BeFalse();
82+
83+
dict
84+
.Length
85+
.Should()
86+
.Be(0);
87+
}
88+
89+
[Fact(DisplayName = "Должен генерировать события при добавлении и удалении")]
90+
public void Should_Raise_Events_On_Changes()
91+
{
92+
// Arrange
93+
using var dict = new ActorDictionary<string, Description>(
94+
fixture.Actors,
95+
c => c.Name);
96+
97+
var addedRaised = false;
98+
var removedRaised = false;
99+
dict.Added += _ => addedRaised = true;
100+
dict.Removed += _ => removedRaised = true;
101+
102+
// Act
103+
var actor = fixture.CreateActor();
104+
actor.Add(new Description { Name = "EventTest" });
105+
actor.Remove<Description>();
106+
107+
// Assert
108+
addedRaised
109+
.Should()
110+
.BeTrue();
111+
112+
removedRaised
113+
.Should()
114+
.BeTrue();
115+
}
116+
117+
[Fact(DisplayName = "Должен возвращать ActorRef через TryGetActorRef")]
118+
public void Should_TryGet_ActorRef()
119+
{
120+
// Arrange
121+
var actor = fixture.CreateActor();
122+
actor.Add(new Description { Name = "Target" });
123+
124+
using var dict = new ActorDictionary<string, Description>(
125+
fixture.Actors,
126+
c => c.Name);
127+
128+
// Act
129+
var found = dict.TryGetActorRef("Target", out var actorRef);
130+
var notFound = dict.TryGetActorRef("Unknown", out _);
131+
132+
// Assert
133+
found.Should().BeTrue();
134+
actorRef.Id.Should().Be(actor.Id);
135+
notFound.Should().BeFalse();
136+
}
137+
138+
[Fact(DisplayName = "Должен выбрасывать исключение при обращении по несуществующему ключу через индексер")]
139+
public void Should_Throw_When_Key_Not_Found_In_Indexer()
140+
{
141+
// Arrange
142+
using var dict = new ActorDictionary<string, Description>(
143+
fixture.Actors,
144+
c => c.Name);
145+
146+
// Act
147+
var action = () => { _ = dict["Missing"]; };
148+
149+
// Assert
150+
// Исходя из кода ActorError.KeyNotFound()
151+
action.Should().Throw<Exception>();
152+
}
153+
154+
[Fact(DisplayName = "Должен очищаться при очистке контекста")]
155+
public void Should_Clear_When_Context_Cleared()
156+
{
157+
// Arrange
158+
var actor = fixture.CreateActor();
159+
actor.Add(new Description { Name = "ClearMe" });
160+
161+
using var dict = new ActorDictionary<string, Description>(
162+
fixture.Actors,
163+
c => c.Name);
164+
165+
// Act
166+
fixture.Actors.Clear();
167+
168+
// Assert
169+
dict.Length.Should().Be(0);
170+
dict.ContainsKey("ClearMe").Should().BeFalse();
171+
}
172+
}

0 commit comments

Comments
 (0)