yuna-engine 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- yuna/README.md +1017 -0
- yuna/__init__.py +30 -0
- yuna/ai/__init__.py +0 -0
- yuna/ai/behavior_tree/__init__.py +0 -0
- yuna/ai/behavior_tree/component.py +57 -0
- yuna/ai/behavior_tree/composite.py +161 -0
- yuna/ai/behavior_tree/decorator.py +202 -0
- yuna/ai/behavior_tree/node.py +44 -0
- yuna/ai/behavior_tree/system.py +106 -0
- yuna/ai/behavior_tree/types.py +68 -0
- yuna/ai/blackboard.py +162 -0
- yuna/ai/events.py +175 -0
- yuna/ai/optimization.py +75 -0
- yuna/ai/pathfinding.py +412 -0
- yuna/ai/perception.py +461 -0
- yuna/ai/profiling.py +36 -0
- yuna/ai/query.py +184 -0
- yuna/ai/steering.py +480 -0
- yuna/api-docs/ERROR_HANDLING.md +429 -0
- yuna/commands/__init__.py +0 -0
- yuna/commands/applicator.py +153 -0
- yuna/commands/command.py +118 -0
- yuna/commands/effect_modifiers.py +201 -0
- yuna/commands/invoker.py +137 -0
- yuna/commands/macro.py +165 -0
- yuna/commands/permissions.py +159 -0
- yuna/commands/result.py +29 -0
- yuna/commands/validator.py +335 -0
- yuna/config/__init__.py +0 -0
- yuna/config/component.py +69 -0
- yuna/config/modifiers.py +144 -0
- yuna/config/patterns.py +139 -0
- yuna/config/schema.py +310 -0
- yuna/config/store.py +203 -0
- yuna/config/types.py +179 -0
- yuna/config/validators.py +33 -0
- yuna/deprecation.py +153 -0
- yuna/ecs/__init__.py +0 -0
- yuna/ecs/archetype.py +241 -0
- yuna/ecs/archetype_store.py +374 -0
- yuna/ecs/buffer.py +131 -0
- yuna/ecs/component.py +29 -0
- yuna/ecs/dirty.py +84 -0
- yuna/ecs/entity.py +152 -0
- yuna/ecs/hierarchy.py +208 -0
- yuna/ecs/lifecycle.py +232 -0
- yuna/ecs/markers.py +27 -0
- yuna/ecs/priority.py +69 -0
- yuna/ecs/prototype.py +179 -0
- yuna/ecs/query.py +265 -0
- yuna/ecs/relationships.py +170 -0
- yuna/ecs/store.py +235 -0
- yuna/ecs/system.py +130 -0
- yuna/ecs/world.py +771 -0
- yuna/events/__init__.py +0 -0
- yuna/events/bus.py +149 -0
- yuna/events/consumption.py +38 -0
- yuna/events/dispatcher.py +201 -0
- yuna/events/event.py +42 -0
- yuna/events/filtering.py +168 -0
- yuna/events/queue.py +123 -0
- yuna/exceptions.py +450 -0
- yuna/fsm/__init__.py +0 -0
- yuna/fsm/component.py +54 -0
- yuna/fsm/machine.py +168 -0
- yuna/fsm/state.py +81 -0
- yuna/fsm/system.py +81 -0
- yuna/fsm/transition.py +51 -0
- yuna/loop/__init__.py +0 -0
- yuna/loop/dependencies.py +229 -0
- yuna/loop/game_loop.py +247 -0
- yuna/loop/job.py +126 -0
- yuna/loop/parallel.py +144 -0
- yuna/loop/scheduler.py +207 -0
- yuna/loop/time.py +79 -0
- yuna/modifiers/__init__.py +0 -0
- yuna/modifiers/applicator.py +42 -0
- yuna/modifiers/batch.py +94 -0
- yuna/modifiers/cache.py +192 -0
- yuna/modifiers/conditions.py +327 -0
- yuna/modifiers/config.py +324 -0
- yuna/modifiers/context.py +41 -0
- yuna/modifiers/conventions.py +171 -0
- yuna/modifiers/conversions.py +198 -0
- yuna/modifiers/expiration.py +58 -0
- yuna/modifiers/interceptor.py +42 -0
- yuna/modifiers/modifier.py +125 -0
- yuna/modifiers/operations.py +154 -0
- yuna/modifiers/pipeline.py +234 -0
- yuna/modifiers/query.py +194 -0
- yuna/modifiers/reader.py +40 -0
- yuna/modifiers/relationships.py +402 -0
- yuna/modifiers/scaling.py +253 -0
- yuna/modifiers/stages.py +1046 -0
- yuna/modifiers/tracker.py +180 -0
- yuna/modifiers/types.py +80 -0
- yuna/network/__init__.py +0 -0
- yuna/network/authority.py +27 -0
- yuna/network/budget.py +180 -0
- yuna/network/component_tags.py +29 -0
- yuna/network/compression.py +153 -0
- yuna/network/delta.py +171 -0
- yuna/network/interest.py +293 -0
- yuna/network/priority.py +265 -0
- yuna/network/quantization.py +179 -0
- yuna/network/relevancy.py +319 -0
- yuna/network/replication.py +288 -0
- yuna/particles/__init__.py +0 -0
- yuna/particles/field.py +79 -0
- yuna/particles/simulation.py +271 -0
- yuna/physics/__init__.py +0 -0
- yuna/physics/body.py +93 -0
- yuna/physics/engine.py +356 -0
- yuna/physics/shapes.py +266 -0
- yuna/physics/system.py +150 -0
- yuna/pipeline/__init__.py +0 -0
- yuna/pipeline/context.py +17 -0
- yuna/pipeline/pipeline.py +187 -0
- yuna/pipeline/registry.py +95 -0
- yuna/pipeline/stage.py +47 -0
- yuna/prefabs/__init__.py +0 -0
- yuna/prefabs/manager.py +277 -0
- yuna/prefabs/prefab.py +50 -0
- yuna/profiling/__init__.py +0 -0
- yuna/profiling/decorators.py +85 -0
- yuna/profiling/monitor.py +370 -0
- yuna/profiling/stats.py +68 -0
- yuna/py.typed +0 -0
- yuna/replay/__init__.py +0 -0
- yuna/replay/config.py +70 -0
- yuna/replay/controls.py +59 -0
- yuna/replay/incremental.py +571 -0
- yuna/replay/player.py +293 -0
- yuna/replay/recorder.py +320 -0
- yuna/replay/storage.py +223 -0
- yuna/resources/__init__.py +0 -0
- yuna/resources/flyweight.py +74 -0
- yuna/resources/pool.py +93 -0
- yuna/resources/pools.py +288 -0
- yuna/scene/__init__.py +0 -0
- yuna/scene/chunk.py +221 -0
- yuna/scene/manager.py +156 -0
- yuna/scene/scene.py +91 -0
- yuna/scene/streaming.py +301 -0
- yuna/scene/transition.py +59 -0
- yuna/script/__init__.py +0 -0
- yuna/script/compiler.py +198 -0
- yuna/script/instructions.py +77 -0
- yuna/script/vm.py +232 -0
- yuna/services/__init__.py +0 -0
- yuna/services/lifetime.py +16 -0
- yuna/services/locator.py +117 -0
- yuna/services/provider.py +32 -0
- yuna/spatial/__init__.py +0 -0
- yuna/spatial/bvh.py +380 -0
- yuna/spatial/collision.py +15 -0
- yuna/spatial/factory.py +134 -0
- yuna/spatial/grid.py +819 -0
- yuna/spatial/integration.py +202 -0
- yuna/spatial/quadtree.py +433 -0
- yuna/spatial/queries.py +84 -0
- yuna/state/__init__.py +0 -0
- yuna/state/component_registry.py +218 -0
- yuna/state/manager.py +169 -0
- yuna/state/migrations.py +302 -0
- yuna/state/raw_snapshot.py +40 -0
- yuna/state/serializer.py +730 -0
- yuna/state/serializers.py +122 -0
- yuna/state/snapshot.py +33 -0
- yuna/state/versioning.py +121 -0
- yuna/types/__init__.py +0 -0
- yuna/types/bounds.py +99 -0
- yuna/types/identifiers.py +7 -0
- yuna/types/registry.py +233 -0
- yuna/types/type_object.py +63 -0
- yuna/types/vector.py +99 -0
- yuna_engine-0.1.0.dist-info/METADATA +124 -0
- yuna_engine-0.1.0.dist-info/RECORD +180 -0
- yuna_engine-0.1.0.dist-info/WHEEL +4 -0
- yuna_engine-0.1.0.dist-info/licenses/LICENSE +21 -0
yuna/README.md
ADDED
|
@@ -0,0 +1,1017 @@
|
|
|
1
|
+
# Engine Core v3
|
|
2
|
+
|
|
3
|
+
A high-performance Entity Component System (ECS) game engine with advanced patterns for game development.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Quick Start](#quick-start)
|
|
8
|
+
- [Architecture Overview](#architecture-overview)
|
|
9
|
+
- [Core Patterns](#core-patterns)
|
|
10
|
+
- [API Reference](#api-reference)
|
|
11
|
+
- [Integration Examples](#integration-examples)
|
|
12
|
+
- [Performance Characteristics](#performance-characteristics)
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
### Creating Your First Game Loop
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from yuna.ecs.world import ECSWorld
|
|
20
|
+
from yuna.loop.game_loop import GameLoop
|
|
21
|
+
from yuna.loop.scheduler import SystemScheduler
|
|
22
|
+
from yuna.loop.time import TimeManager
|
|
23
|
+
|
|
24
|
+
time_manager = TimeManager(fixed_delta=1 / 60)
|
|
25
|
+
scheduler = SystemScheduler()
|
|
26
|
+
world = ECSWorld()
|
|
27
|
+
|
|
28
|
+
game_loop = GameLoop(
|
|
29
|
+
time_manager=time_manager,
|
|
30
|
+
scheduler=scheduler,
|
|
31
|
+
world=world,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
while running:
|
|
35
|
+
elapsed = get_frame_time()
|
|
36
|
+
game_loop.update(elapsed=elapsed)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Creating Entities and Components
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from dataclasses import dataclass
|
|
43
|
+
from yuna.ecs.component import Component
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class Position(Component):
|
|
48
|
+
x: float
|
|
49
|
+
y: float
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class Velocity(Component):
|
|
54
|
+
dx: float
|
|
55
|
+
dy: float
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
entity = world.create_entity()
|
|
59
|
+
world.add_component(entity_id=entity, component=Position(x=0.0, y=0.0))
|
|
60
|
+
world.add_component(entity_id=entity, component=Velocity(dx=1.0, dy=0.0))
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Creating Systems
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
from yuna.ecs.system import System
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class MovementSystem(System):
|
|
70
|
+
@property
|
|
71
|
+
def priority(self) -> int:
|
|
72
|
+
return 100
|
|
73
|
+
|
|
74
|
+
def update(self, world: ECSWorld, delta_time: float) -> None:
|
|
75
|
+
for entity_id, (pos, vel) in (
|
|
76
|
+
world.query().with_components(Position, Velocity).iterator()
|
|
77
|
+
):
|
|
78
|
+
pos.x += vel.dx * delta_time
|
|
79
|
+
pos.y += vel.dy * delta_time
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
scheduler.register(system=MovementSystem())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Architecture Overview
|
|
86
|
+
|
|
87
|
+
The engine core v3 is built on a modular architecture with 11 core patterns:
|
|
88
|
+
|
|
89
|
+
1. **Foundation**: Core types (Vector2, EntityID) and utilities
|
|
90
|
+
2. **Service Locator**: Dependency injection with singleton/transient lifetimes
|
|
91
|
+
3. **Entity Component System**: Data-oriented design for game entities
|
|
92
|
+
4. **Event System**: Double-buffered event queue with priority ordering
|
|
93
|
+
5. **Command Pattern**: Undo/redo support with validation
|
|
94
|
+
6. **Pipeline Pattern**: Composable processing stages
|
|
95
|
+
7. **Modifier Pipeline**: Stat modification with stacking rules
|
|
96
|
+
8. **Spatial Indexing**: O(1) spatial queries with grid-based hashing
|
|
97
|
+
9. **Resource Management**: Object pooling and flyweight pattern
|
|
98
|
+
10. **Game Loop**: Fixed timestep with accumulator pattern
|
|
99
|
+
11. **Integration**: All patterns working together
|
|
100
|
+
|
|
101
|
+
## Core Patterns
|
|
102
|
+
|
|
103
|
+
### Entity Component System (ECS)
|
|
104
|
+
|
|
105
|
+
The ECS pattern separates data (Components) from logic (Systems) for better performance and maintainability.
|
|
106
|
+
|
|
107
|
+
**Key Classes:**
|
|
108
|
+
- `ECSWorld`: Central coordinator for entities and components
|
|
109
|
+
- `Component`: Base class for data containers
|
|
110
|
+
- `System`: Base class for game logic
|
|
111
|
+
- `Query`: Efficient entity filtering by component composition
|
|
112
|
+
|
|
113
|
+
**Benefits:**
|
|
114
|
+
- Data locality for cache-friendly iteration
|
|
115
|
+
- Easy to add/remove functionality
|
|
116
|
+
- Natural parallelization opportunities
|
|
117
|
+
|
|
118
|
+
### Event System
|
|
119
|
+
|
|
120
|
+
Double-buffered event queue with priority-based ordering ensures deterministic event processing.
|
|
121
|
+
|
|
122
|
+
**Key Classes:**
|
|
123
|
+
- `Event`: Immutable event data with timestamp and tick
|
|
124
|
+
- `EventBus`: Unified event queue and dispatcher
|
|
125
|
+
- `EventQueue`: Double-buffered priority queue
|
|
126
|
+
- `EventDispatcher`: Type-based event routing
|
|
127
|
+
|
|
128
|
+
**Features:**
|
|
129
|
+
- Frame-based event delays
|
|
130
|
+
- Priority ordering within frames
|
|
131
|
+
- Type-safe event subscriptions
|
|
132
|
+
|
|
133
|
+
### Command Pattern
|
|
134
|
+
|
|
135
|
+
Encapsulates actions with validation, execution, and optional undo support.
|
|
136
|
+
|
|
137
|
+
**Key Classes:**
|
|
138
|
+
- `Command`: Abstract base for all commands
|
|
139
|
+
- `CommandInvoker`: Execution with history tracking
|
|
140
|
+
- `CommandMacro`: Batch multiple commands
|
|
141
|
+
|
|
142
|
+
**Use Cases:**
|
|
143
|
+
- User input handling
|
|
144
|
+
- AI decision execution
|
|
145
|
+
- Network command replication
|
|
146
|
+
|
|
147
|
+
### Modifier Pipeline
|
|
148
|
+
|
|
149
|
+
Processes stat modifications through configurable pipeline stages.
|
|
150
|
+
|
|
151
|
+
**Key Classes:**
|
|
152
|
+
- `ModifierPipeline`: Orchestrates all pipeline stages
|
|
153
|
+
- `Modifier`: Immutable modification data
|
|
154
|
+
- `ModifierConfig`: Stat definitions and constraints
|
|
155
|
+
|
|
156
|
+
**Pipeline Stages:**
|
|
157
|
+
1. Collect: Gather all queued modifiers
|
|
158
|
+
2. Filter: Remove invalid modifiers
|
|
159
|
+
3. Sort: Order by priority
|
|
160
|
+
4. Group: Group by target stat
|
|
161
|
+
5. Stack: Apply stacking rules (add/multiply/max/min)
|
|
162
|
+
6. Clamp: Enforce min/max constraints
|
|
163
|
+
7. Apply: Write final values
|
|
164
|
+
|
|
165
|
+
### Spatial Indexing
|
|
166
|
+
|
|
167
|
+
Grid-based spatial indexing provides O(1) point queries and efficient radius searches.
|
|
168
|
+
|
|
169
|
+
**Key Classes:**
|
|
170
|
+
- `SpatialGrid`: Cell-based entity positioning
|
|
171
|
+
- `SpatialQuery`: Protocol for spatial queries
|
|
172
|
+
|
|
173
|
+
**Performance:**
|
|
174
|
+
- O(1) point queries
|
|
175
|
+
- O(k) radius queries where k = entities in nearby cells
|
|
176
|
+
- Efficient updates when entities move
|
|
177
|
+
|
|
178
|
+
### Resource Management
|
|
179
|
+
|
|
180
|
+
Object pooling and flyweight patterns reduce allocations.
|
|
181
|
+
|
|
182
|
+
**Key Classes:**
|
|
183
|
+
- `ObjectPool`: Reusable object instances
|
|
184
|
+
- `FlyweightFactory`: Shared instances by key
|
|
185
|
+
|
|
186
|
+
**Benefits:**
|
|
187
|
+
- Reduced garbage collection pressure
|
|
188
|
+
- Consistent allocation patterns
|
|
189
|
+
- Memory efficiency through sharing
|
|
190
|
+
|
|
191
|
+
### Game Loop
|
|
192
|
+
|
|
193
|
+
Fixed timestep game loop with accumulator pattern ensures deterministic simulation.
|
|
194
|
+
|
|
195
|
+
**Key Classes:**
|
|
196
|
+
- `GameLoop`: Main loop orchestrator
|
|
197
|
+
- `TimeManager`: Fixed timestep with accumulator
|
|
198
|
+
- `SystemScheduler`: Priority-based system ordering
|
|
199
|
+
|
|
200
|
+
**Execution Order:**
|
|
201
|
+
1. Modifier pipeline processing
|
|
202
|
+
2. Event processing
|
|
203
|
+
3. System updates (by priority)
|
|
204
|
+
4. Event bus tick finalization
|
|
205
|
+
|
|
206
|
+
## API Reference
|
|
207
|
+
|
|
208
|
+
### ECS World
|
|
209
|
+
|
|
210
|
+
#### ECSWorld
|
|
211
|
+
|
|
212
|
+
Central coordinator for the Entity Component System.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
world = ECSWorld()
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
**Methods:**
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
def create_entity() -> EntityID:
|
|
222
|
+
"""Create a new entity."""
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def destroy_entity(entity_id: EntityID) -> None:
|
|
226
|
+
"""Mark entity for destruction at end of update cycle."""
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def add_component(entity_id: EntityID, component: Component) -> None:
|
|
230
|
+
"""Add component to entity."""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def get_component(
|
|
234
|
+
entity_id: EntityID, component_type: type[Component]
|
|
235
|
+
) -> Component | None:
|
|
236
|
+
"""Get component from entity."""
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def has_component(entity_id: EntityID, component_type: type[Component]) -> bool:
|
|
240
|
+
"""Check if entity has component."""
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def remove_component(entity_id: EntityID, component_type: type[Component]) -> None:
|
|
244
|
+
"""Remove component from entity."""
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def query() -> Query:
|
|
248
|
+
"""Create a new component query."""
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def register_system(system: System) -> None:
|
|
252
|
+
"""Register system for execution."""
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def update(delta_time: float) -> None:
|
|
256
|
+
"""Update all systems and flush destroyed entities."""
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
#### Query
|
|
260
|
+
|
|
261
|
+
Filters entities based on component composition.
|
|
262
|
+
|
|
263
|
+
```python
|
|
264
|
+
query = world.query()
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Methods:**
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
def with_components(*component_types: type[Component]) -> Query:
|
|
271
|
+
"""Filter for entities that have all specified components."""
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def without_components(*component_types: type[Component]) -> Query:
|
|
275
|
+
"""Filter for entities that don't have any specified components."""
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def iterator() -> Iterator[tuple[EntityID, tuple[Component, ...]]]:
|
|
279
|
+
"""Iterate over entities matching the query with their components."""
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def get_entities() -> set[EntityID]:
|
|
283
|
+
"""Get all entity IDs matching the query."""
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def count() -> int:
|
|
287
|
+
"""Count number of entities matching the query."""
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
**Examples:**
|
|
291
|
+
|
|
292
|
+
```python
|
|
293
|
+
entities = world.query().with_components(Position).get_entities()
|
|
294
|
+
|
|
295
|
+
entities = (
|
|
296
|
+
world
|
|
297
|
+
.query()
|
|
298
|
+
.with_components(Position, Velocity)
|
|
299
|
+
.without_components(Dead)
|
|
300
|
+
.get_entities()
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
for entity_id, (pos, vel) in (
|
|
304
|
+
world.query().with_components(Position, Velocity).iterator()
|
|
305
|
+
):
|
|
306
|
+
pos.x += vel.dx
|
|
307
|
+
pos.y += vel.dy
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Event System
|
|
311
|
+
|
|
312
|
+
#### EventBus
|
|
313
|
+
|
|
314
|
+
Unified event bus combining queue and dispatcher.
|
|
315
|
+
|
|
316
|
+
```python
|
|
317
|
+
bus = EventBus()
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
**Methods:**
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
def emit(event: Event, delay_frames: int = 0) -> None:
|
|
324
|
+
"""Emit event with optional frame delay."""
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def emit_priority(event: Event, priority: int, delay_frames: int = 0) -> None:
|
|
328
|
+
"""Emit event with specific priority."""
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def subscribe(event_type: str, handler: Callable[[Event], None]) -> None:
|
|
332
|
+
"""Subscribe handler to specific event type."""
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def subscribe_all(handler: Callable[[Event], None]) -> None:
|
|
336
|
+
"""Subscribe handler to all event types."""
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def unsubscribe(event_type: str, handler: Callable[[Event], None]) -> None:
|
|
340
|
+
"""Unsubscribe handler from specific event type."""
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def process_events() -> None:
|
|
344
|
+
"""Process all events in current frame."""
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def end_tick() -> None:
|
|
348
|
+
"""End current tick and move delayed events to current frame."""
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Example:**
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
from dataclasses import dataclass
|
|
355
|
+
from yuna.events.event import Event
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
@dataclass(frozen=True)
|
|
359
|
+
class PlayerDiedEvent(Event):
|
|
360
|
+
player_id: EntityID
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def on_player_died(event: Event) -> None:
|
|
364
|
+
print(f"Player {event.player_id} died!")
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
bus.subscribe(event_type="PlayerDiedEvent", handler=on_player_died)
|
|
368
|
+
bus.emit(event=PlayerDiedEvent(timestamp=0.0, tick=100, player_id=player_id))
|
|
369
|
+
bus.process_events()
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
### Modifier Pipeline
|
|
373
|
+
|
|
374
|
+
#### ModifierPipeline
|
|
375
|
+
|
|
376
|
+
Pipeline for processing stat modifications.
|
|
377
|
+
|
|
378
|
+
```python
|
|
379
|
+
from yuna.modifiers.config import ModifierConfig, StackingRule
|
|
380
|
+
from yuna.modifiers.pipeline import ModifierPipeline
|
|
381
|
+
|
|
382
|
+
config = ModifierConfig()
|
|
383
|
+
config.register_stat(
|
|
384
|
+
name="health",
|
|
385
|
+
min_value=0.0,
|
|
386
|
+
max_value=100.0,
|
|
387
|
+
stacking_rule=StackingRule.ADD,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
pipeline = ModifierPipeline(config=config)
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Methods:**
|
|
394
|
+
|
|
395
|
+
```python
|
|
396
|
+
def queue_modifier(modifier: Modifier) -> None:
|
|
397
|
+
"""Add modifier to processing queue."""
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def process(world: Any | None = None) -> dict[tuple, float]:
|
|
401
|
+
"""Process all queued modifiers through pipeline."""
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def clear_queue() -> None:
|
|
405
|
+
"""Clear all queued modifiers without processing."""
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def get_queue_size() -> int:
|
|
409
|
+
"""Get number of queued modifiers."""
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
**Example:**
|
|
413
|
+
|
|
414
|
+
```python
|
|
415
|
+
from yuna.modifiers.modifier import Modifier
|
|
416
|
+
from yuna.modifiers.types import ModificationType, ModifierPriority
|
|
417
|
+
|
|
418
|
+
modifier = Modifier(
|
|
419
|
+
entity_id=player_id,
|
|
420
|
+
stat="health",
|
|
421
|
+
modification_type=ModificationType.FLAT,
|
|
422
|
+
value=10.0,
|
|
423
|
+
priority=ModifierPriority.NORMAL,
|
|
424
|
+
source="healing_potion",
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
pipeline.queue_modifier(modifier=modifier)
|
|
428
|
+
result = pipeline.process()
|
|
429
|
+
final_health = result[(player_id, "health")]
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
### Spatial Indexing
|
|
433
|
+
|
|
434
|
+
#### SpatialGrid
|
|
435
|
+
|
|
436
|
+
Grid-based spatial index for fast entity position queries.
|
|
437
|
+
|
|
438
|
+
```python
|
|
439
|
+
from yuna.spatial.grid import SpatialGrid
|
|
440
|
+
|
|
441
|
+
grid = SpatialGrid(cell_size=10)
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
**Methods:**
|
|
445
|
+
|
|
446
|
+
```python
|
|
447
|
+
def add(entity_id: EntityID, position: Vector2) -> None:
|
|
448
|
+
"""Add entity to spatial grid at position."""
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def remove(entity_id: EntityID) -> None:
|
|
452
|
+
"""Remove entity from spatial grid."""
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def move(entity_id: EntityID, new_position: Vector2) -> None:
|
|
456
|
+
"""Update entity position in spatial grid."""
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
def get_at(position: Vector2) -> set[EntityID]:
|
|
460
|
+
"""Get all entities at a specific position (O(1) lookup)."""
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
def get_in_radius(position: Vector2, radius: float) -> set[EntityID]:
|
|
464
|
+
"""Get all entities within radius of position."""
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
**Example:**
|
|
468
|
+
|
|
469
|
+
```python
|
|
470
|
+
grid.add(entity_id=enemy_id, position=Vector2(x=50.0, y=50.0))
|
|
471
|
+
|
|
472
|
+
nearby = grid.get_in_radius(position=player_pos, radius=10.0)
|
|
473
|
+
for enemy_id in nearby:
|
|
474
|
+
attack(enemy_id)
|
|
475
|
+
|
|
476
|
+
grid.move(entity_id=enemy_id, new_position=Vector2(x=55.0, y=55.0))
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### Resource Management
|
|
480
|
+
|
|
481
|
+
#### ObjectPool
|
|
482
|
+
|
|
483
|
+
Reusable object pool to reduce allocations.
|
|
484
|
+
|
|
485
|
+
```python
|
|
486
|
+
from yuna.resources.pool import ObjectPool
|
|
487
|
+
|
|
488
|
+
|
|
489
|
+
def factory() -> dict:
|
|
490
|
+
return {"x": 0.0, "y": 0.0}
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
def reset(obj: dict) -> None:
|
|
494
|
+
obj["x"] = 0.0
|
|
495
|
+
obj["y"] = 0.0
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
pool = ObjectPool[dict](factory=factory, reset=reset, max_size=100)
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
**Methods:**
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
def acquire() -> T:
|
|
505
|
+
"""Get object from pool or create new one."""
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
def release(obj: T) -> None:
|
|
509
|
+
"""Return object to pool after reset."""
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def clear() -> None:
|
|
513
|
+
"""Remove all objects from pool."""
|
|
514
|
+
|
|
515
|
+
|
|
516
|
+
def count() -> int:
|
|
517
|
+
"""Get number of objects in pool."""
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
**Example:**
|
|
521
|
+
|
|
522
|
+
```python
|
|
523
|
+
obj = pool.acquire()
|
|
524
|
+
obj["x"] = 10.0
|
|
525
|
+
obj["y"] = 20.0
|
|
526
|
+
pool.release(obj=obj)
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
#### FlyweightFactory
|
|
530
|
+
|
|
531
|
+
Shared instance caching by key.
|
|
532
|
+
|
|
533
|
+
```python
|
|
534
|
+
from yuna.resources.flyweight import FlyweightFactory
|
|
535
|
+
|
|
536
|
+
factory = FlyweightFactory[str, dict]()
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
**Methods:**
|
|
540
|
+
|
|
541
|
+
```python
|
|
542
|
+
def get(key: K, factory: Callable[[], T]) -> T:
|
|
543
|
+
"""Get shared instance for key, creating if needed."""
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def clear() -> None:
|
|
547
|
+
"""Remove all cached instances."""
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def count() -> int:
|
|
551
|
+
"""Get number of cached instances."""
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
**Example:**
|
|
555
|
+
|
|
556
|
+
```python
|
|
557
|
+
config_a = factory.get(key="level_1", factory=lambda: load_level_config("level_1"))
|
|
558
|
+
config_b = factory.get(key="level_1", factory=lambda: load_level_config("level_1"))
|
|
559
|
+
assert config_a is config_b
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### Game Loop
|
|
563
|
+
|
|
564
|
+
#### GameLoop
|
|
565
|
+
|
|
566
|
+
Orchestrates game loop with fixed timestep execution.
|
|
567
|
+
|
|
568
|
+
```python
|
|
569
|
+
from yuna.loop.game_loop import GameLoop
|
|
570
|
+
|
|
571
|
+
game_loop = GameLoop(
|
|
572
|
+
time_manager=time_manager,
|
|
573
|
+
scheduler=scheduler,
|
|
574
|
+
world=world,
|
|
575
|
+
event_bus=event_bus,
|
|
576
|
+
modifier_pipeline=modifier_pipeline,
|
|
577
|
+
)
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
**Methods:**
|
|
581
|
+
|
|
582
|
+
```python
|
|
583
|
+
def tick() -> None:
|
|
584
|
+
"""Execute one game tick."""
|
|
585
|
+
|
|
586
|
+
|
|
587
|
+
def update(elapsed: float) -> int:
|
|
588
|
+
"""Update game loop with elapsed time, returns number of ticks executed."""
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def reset_time() -> None:
|
|
592
|
+
"""Reset time manager accumulator to zero."""
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
**Properties:**
|
|
596
|
+
|
|
597
|
+
```python
|
|
598
|
+
@property
|
|
599
|
+
def fixed_delta() -> float:
|
|
600
|
+
"""Get fixed timestep value."""
|
|
601
|
+
|
|
602
|
+
|
|
603
|
+
@property
|
|
604
|
+
def accumulator() -> float:
|
|
605
|
+
"""Get current time accumulator."""
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
#### TimeManager
|
|
609
|
+
|
|
610
|
+
Manages fixed timestep game loop timing.
|
|
611
|
+
|
|
612
|
+
```python
|
|
613
|
+
from yuna.loop.time import TimeManager
|
|
614
|
+
|
|
615
|
+
time_manager = TimeManager(fixed_delta=1 / 60)
|
|
616
|
+
```
|
|
617
|
+
|
|
618
|
+
**Methods:**
|
|
619
|
+
|
|
620
|
+
```python
|
|
621
|
+
def update(elapsed: float) -> int:
|
|
622
|
+
"""Update accumulator and calculate ticks to execute."""
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def reset() -> None:
|
|
626
|
+
"""Reset accumulator to zero."""
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
**Properties:**
|
|
630
|
+
|
|
631
|
+
```python
|
|
632
|
+
@property
|
|
633
|
+
def fixed_delta() -> float:
|
|
634
|
+
"""Get fixed timestep value."""
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
@property
|
|
638
|
+
def accumulator() -> float:
|
|
639
|
+
"""Get current accumulator value."""
|
|
640
|
+
```
|
|
641
|
+
|
|
642
|
+
#### SystemScheduler
|
|
643
|
+
|
|
644
|
+
Manages system registration and execution ordering.
|
|
645
|
+
|
|
646
|
+
```python
|
|
647
|
+
from yuna.loop.scheduler import SystemScheduler
|
|
648
|
+
|
|
649
|
+
scheduler = SystemScheduler()
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
**Methods:**
|
|
653
|
+
|
|
654
|
+
```python
|
|
655
|
+
def register(system: System) -> None:
|
|
656
|
+
"""Register system for execution."""
|
|
657
|
+
|
|
658
|
+
|
|
659
|
+
def get_ordered_systems() -> list[System]:
|
|
660
|
+
"""Get systems in priority order."""
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def clear() -> None:
|
|
664
|
+
"""Remove all systems from scheduler."""
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
**Properties:**
|
|
668
|
+
|
|
669
|
+
```python
|
|
670
|
+
@property
|
|
671
|
+
def count() -> int:
|
|
672
|
+
"""Get number of registered systems."""
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
### Service Locator
|
|
676
|
+
|
|
677
|
+
#### ServiceLocator
|
|
678
|
+
|
|
679
|
+
Central registry for dependency injection.
|
|
680
|
+
|
|
681
|
+
```python
|
|
682
|
+
from yuna.services.locator import ServiceLocator
|
|
683
|
+
|
|
684
|
+
locator = ServiceLocator()
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
**Methods:**
|
|
688
|
+
|
|
689
|
+
```python
|
|
690
|
+
def register(
|
|
691
|
+
interface: type[T],
|
|
692
|
+
factory: Callable[[], T],
|
|
693
|
+
lifetime: ServiceLifetime = ServiceLifetime.TRANSIENT,
|
|
694
|
+
) -> None:
|
|
695
|
+
"""Register a service with its factory and lifetime."""
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
def resolve(interface: type[T]) -> T:
|
|
699
|
+
"""Resolve service instance by interface type."""
|
|
700
|
+
|
|
701
|
+
|
|
702
|
+
def clear() -> None:
|
|
703
|
+
"""Clear all registrations and singletons."""
|
|
704
|
+
```
|
|
705
|
+
|
|
706
|
+
**Example:**
|
|
707
|
+
|
|
708
|
+
```python
|
|
709
|
+
from yuna.services.lifetime import ServiceLifetime
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
class Logger:
|
|
713
|
+
def log(self, message: str) -> None:
|
|
714
|
+
print(message)
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
locator.register(
|
|
718
|
+
interface=Logger,
|
|
719
|
+
factory=lambda: Logger(),
|
|
720
|
+
lifetime=ServiceLifetime.SINGLETON,
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
logger = locator.resolve(interface=Logger)
|
|
724
|
+
logger.log("Hello, world!")
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
## Integration Examples
|
|
728
|
+
|
|
729
|
+
### Complete Game Loop with All Systems
|
|
730
|
+
|
|
731
|
+
```python
|
|
732
|
+
from yuna.ecs.world import ECSWorld
|
|
733
|
+
from yuna.events.bus import EventBus
|
|
734
|
+
from yuna.loop.game_loop import GameLoop
|
|
735
|
+
from yuna.loop.scheduler import SystemScheduler
|
|
736
|
+
from yuna.loop.time import TimeManager
|
|
737
|
+
from yuna.modifiers.config import ModifierConfig, StackingRule
|
|
738
|
+
from yuna.modifiers.pipeline import ModifierPipeline
|
|
739
|
+
from yuna.spatial.grid import SpatialGrid
|
|
740
|
+
from yuna.services.locator import ServiceLocator
|
|
741
|
+
|
|
742
|
+
world = ECSWorld()
|
|
743
|
+
event_bus = EventBus()
|
|
744
|
+
time_manager = TimeManager(fixed_delta=1 / 60)
|
|
745
|
+
scheduler = SystemScheduler()
|
|
746
|
+
|
|
747
|
+
config = ModifierConfig()
|
|
748
|
+
config.register_stat(
|
|
749
|
+
name="health",
|
|
750
|
+
min_value=0.0,
|
|
751
|
+
max_value=100.0,
|
|
752
|
+
stacking_rule=StackingRule.ADD,
|
|
753
|
+
)
|
|
754
|
+
modifier_pipeline = ModifierPipeline(config=config)
|
|
755
|
+
|
|
756
|
+
grid = SpatialGrid(cell_size=10)
|
|
757
|
+
|
|
758
|
+
locator = ServiceLocator()
|
|
759
|
+
locator.register(interface=SpatialGrid, factory=lambda: grid)
|
|
760
|
+
|
|
761
|
+
game_loop = GameLoop(
|
|
762
|
+
time_manager=time_manager,
|
|
763
|
+
scheduler=scheduler,
|
|
764
|
+
world=world,
|
|
765
|
+
event_bus=event_bus,
|
|
766
|
+
modifier_pipeline=modifier_pipeline,
|
|
767
|
+
)
|
|
768
|
+
|
|
769
|
+
scheduler.register(system=MovementSystem())
|
|
770
|
+
scheduler.register(system=CombatSystem())
|
|
771
|
+
scheduler.register(system=RenderSystem())
|
|
772
|
+
|
|
773
|
+
while running:
|
|
774
|
+
elapsed = get_frame_time()
|
|
775
|
+
game_loop.update(elapsed=elapsed)
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### Movement System with Spatial Grid
|
|
779
|
+
|
|
780
|
+
```python
|
|
781
|
+
class MovementSystem(System):
|
|
782
|
+
@property
|
|
783
|
+
def priority(self) -> int:
|
|
784
|
+
return 100
|
|
785
|
+
|
|
786
|
+
def update(self, world: ECSWorld, delta_time: float) -> None:
|
|
787
|
+
spatial_grid = locator.resolve(interface=SpatialGrid)
|
|
788
|
+
|
|
789
|
+
for entity_id, (pos, vel) in (
|
|
790
|
+
world.query().with_components(Position, Velocity).iterator()
|
|
791
|
+
):
|
|
792
|
+
old_pos = Vector2(x=pos.x, y=pos.y)
|
|
793
|
+
pos.x += vel.dx * delta_time
|
|
794
|
+
pos.y += vel.dy * delta_time
|
|
795
|
+
|
|
796
|
+
spatial_grid.move(
|
|
797
|
+
entity_id=entity_id,
|
|
798
|
+
new_position=Vector2(x=pos.x, y=pos.y),
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
event_bus.emit(
|
|
802
|
+
event=EntityMovedEvent(
|
|
803
|
+
timestamp=time.time(),
|
|
804
|
+
tick=current_tick,
|
|
805
|
+
entity_id=entity_id,
|
|
806
|
+
old_position=old_pos,
|
|
807
|
+
new_position=Vector2(x=pos.x, y=pos.y),
|
|
808
|
+
)
|
|
809
|
+
)
|
|
810
|
+
```
|
|
811
|
+
|
|
812
|
+
### Combat System with Modifiers
|
|
813
|
+
|
|
814
|
+
```python
|
|
815
|
+
class CombatSystem(System):
|
|
816
|
+
@property
|
|
817
|
+
def priority(self) -> int:
|
|
818
|
+
return 200
|
|
819
|
+
|
|
820
|
+
def update(self, world: ECSWorld, delta_time: float) -> None:
|
|
821
|
+
spatial_grid = locator.resolve(interface=SpatialGrid)
|
|
822
|
+
|
|
823
|
+
for entity_id, (pos, attack) in (
|
|
824
|
+
world.query().with_components(Position, Attack).iterator()
|
|
825
|
+
):
|
|
826
|
+
nearby = spatial_grid.get_in_radius(
|
|
827
|
+
position=Vector2(x=pos.x, y=pos.y),
|
|
828
|
+
radius=attack.range,
|
|
829
|
+
)
|
|
830
|
+
|
|
831
|
+
for target_id in nearby:
|
|
832
|
+
if target_id == entity_id:
|
|
833
|
+
continue
|
|
834
|
+
|
|
835
|
+
if world.has_component(entity_id=target_id, component_type=Health):
|
|
836
|
+
modifier = Modifier(
|
|
837
|
+
entity_id=target_id,
|
|
838
|
+
stat="health",
|
|
839
|
+
modification_type=ModificationType.FLAT,
|
|
840
|
+
value=-attack.damage,
|
|
841
|
+
priority=ModifierPriority.NORMAL,
|
|
842
|
+
source=f"attack_{entity_id}",
|
|
843
|
+
)
|
|
844
|
+
modifier_pipeline.queue_modifier(modifier=modifier)
|
|
845
|
+
```
|
|
846
|
+
|
|
847
|
+
### Event-Driven System
|
|
848
|
+
|
|
849
|
+
```python
|
|
850
|
+
class DeathSystem(System):
|
|
851
|
+
@property
|
|
852
|
+
def priority(self) -> int:
|
|
853
|
+
return 300
|
|
854
|
+
|
|
855
|
+
def update(self, world: ECSWorld, delta_time: float) -> None:
|
|
856
|
+
for entity_id, health in world.query().with_components(Health).iterator():
|
|
857
|
+
if health.value <= 0:
|
|
858
|
+
event_bus.emit(
|
|
859
|
+
event=EntityDiedEvent(
|
|
860
|
+
timestamp=time.time(),
|
|
861
|
+
tick=current_tick,
|
|
862
|
+
entity_id=entity_id,
|
|
863
|
+
)
|
|
864
|
+
)
|
|
865
|
+
world.destroy_entity(entity_id=entity_id)
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
def on_entity_died(event: Event) -> None:
|
|
869
|
+
print(f"Entity {event.entity_id} died!")
|
|
870
|
+
spawn_death_particles(event.entity_id)
|
|
871
|
+
play_death_sound()
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
event_bus.subscribe(event_type="EntityDiedEvent", handler=on_entity_died)
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
## Performance Characteristics
|
|
878
|
+
|
|
879
|
+
### ECS World
|
|
880
|
+
|
|
881
|
+
- **Entity Creation**: O(1)
|
|
882
|
+
- **Component Add/Remove**: O(1)
|
|
883
|
+
- **Component Get**: O(1)
|
|
884
|
+
- **Query (single component)**: O(n) where n = entities with that component
|
|
885
|
+
- **Query (multiple components)**: O(k) where k = entities with least common component
|
|
886
|
+
|
|
887
|
+
### Spatial Grid
|
|
888
|
+
|
|
889
|
+
- **Point Query**: O(1) - constant time cell lookup
|
|
890
|
+
- **Radius Query**: O(k) where k = entities in nearby cells
|
|
891
|
+
- **Insert**: O(1)
|
|
892
|
+
- **Move**: O(1) if same cell, O(2) if different cell
|
|
893
|
+
- **Remove**: O(1)
|
|
894
|
+
|
|
895
|
+
### Event System
|
|
896
|
+
|
|
897
|
+
- **Emit**: O(log n) for priority queue insertion
|
|
898
|
+
- **Process**: O(n) where n = events in current frame
|
|
899
|
+
- **Subscribe**: O(1)
|
|
900
|
+
|
|
901
|
+
### Modifier Pipeline
|
|
902
|
+
|
|
903
|
+
- **Queue**: O(1)
|
|
904
|
+
- **Process**: O(n log n) where n = queued modifiers (dominated by sorting)
|
|
905
|
+
|
|
906
|
+
### Resource Management
|
|
907
|
+
|
|
908
|
+
- **ObjectPool Acquire**: O(1) from pool, O(k) if creating new (k = factory time)
|
|
909
|
+
- **ObjectPool Release**: O(1)
|
|
910
|
+
- **Flyweight Get**: O(1) hash lookup
|
|
911
|
+
|
|
912
|
+
### Benchmark Results
|
|
913
|
+
|
|
914
|
+
Tested on 1000 entities with multiple components:
|
|
915
|
+
|
|
916
|
+
- Entity creation with 3 components: < 1.0s
|
|
917
|
+
- Query iteration: < 0.1s
|
|
918
|
+
- Spatial grid point queries (10,000 iterations): < 1.0s
|
|
919
|
+
- Spatial grid radius queries vs linear search: 2-5x faster
|
|
920
|
+
- Component add/remove (10,000 iterations): < 1.0s
|
|
921
|
+
- Entity destruction (1000 entities): < 1.0s
|
|
922
|
+
|
|
923
|
+
## Best Practices
|
|
924
|
+
|
|
925
|
+
### Component Design
|
|
926
|
+
|
|
927
|
+
- Keep components as pure data containers
|
|
928
|
+
- Use frozen dataclasses for immutability where possible
|
|
929
|
+
- Avoid circular references between components
|
|
930
|
+
|
|
931
|
+
```python
|
|
932
|
+
@dataclass
|
|
933
|
+
class Position(Component):
|
|
934
|
+
x: float
|
|
935
|
+
y: float
|
|
936
|
+
```
|
|
937
|
+
|
|
938
|
+
### System Design
|
|
939
|
+
|
|
940
|
+
- Systems should be stateless where possible
|
|
941
|
+
- Use service locator for shared resources
|
|
942
|
+
- Keep systems focused on single responsibility
|
|
943
|
+
- Use priority to control execution order
|
|
944
|
+
|
|
945
|
+
```python
|
|
946
|
+
class MovementSystem(System):
|
|
947
|
+
@property
|
|
948
|
+
def priority(self) -> int:
|
|
949
|
+
return 100
|
|
950
|
+
|
|
951
|
+
def update(self, world: ECSWorld, delta_time: float) -> None:
|
|
952
|
+
pass
|
|
953
|
+
```
|
|
954
|
+
|
|
955
|
+
### Event Handling
|
|
956
|
+
|
|
957
|
+
- Events should be immutable (frozen dataclasses)
|
|
958
|
+
- Include timestamp and tick for debugging
|
|
959
|
+
- Use specific event types rather than generic events
|
|
960
|
+
|
|
961
|
+
```python
|
|
962
|
+
@dataclass(frozen=True)
|
|
963
|
+
class CollisionEvent(Event):
|
|
964
|
+
entity_a: EntityID
|
|
965
|
+
entity_b: EntityID
|
|
966
|
+
position: Vector2
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
### Modifier Usage
|
|
970
|
+
|
|
971
|
+
- Register all stats in config before using pipeline
|
|
972
|
+
- Use appropriate stacking rules for each stat
|
|
973
|
+
- Set min/max constraints to prevent invalid values
|
|
974
|
+
|
|
975
|
+
```python
|
|
976
|
+
config.register_stat(
|
|
977
|
+
name="speed",
|
|
978
|
+
min_value=0.0,
|
|
979
|
+
max_value=100.0,
|
|
980
|
+
stacking_rule=StackingRule.MULTIPLY,
|
|
981
|
+
)
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
### Spatial Grid Configuration
|
|
985
|
+
|
|
986
|
+
- Choose cell size based on average query radius
|
|
987
|
+
- Generally: cell_size = 2 * average_query_radius
|
|
988
|
+
- Larger cells reduce overhead but less precision
|
|
989
|
+
- Smaller cells improve precision but more overhead
|
|
990
|
+
|
|
991
|
+
```python
|
|
992
|
+
grid = SpatialGrid(cell_size=10)
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
## Testing
|
|
996
|
+
|
|
997
|
+
The engine core v3 includes comprehensive test coverage:
|
|
998
|
+
|
|
999
|
+
- **Unit Tests**: Individual module functionality
|
|
1000
|
+
- **Integration Tests**: All patterns working together
|
|
1001
|
+
- **Performance Tests**: Benchmark critical operations
|
|
1002
|
+
|
|
1003
|
+
Run tests:
|
|
1004
|
+
|
|
1005
|
+
```bash
|
|
1006
|
+
pytest tests/services/engine/core/
|
|
1007
|
+
```
|
|
1008
|
+
|
|
1009
|
+
Run with coverage:
|
|
1010
|
+
|
|
1011
|
+
```bash
|
|
1012
|
+
pytest tests/services/engine/core/ --cov=yuna --cov-report=term-missing
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
## License
|
|
1016
|
+
|
|
1017
|
+
Hello Yuna LLC
|