Skip to content

docs: pin the ECS layering as the decided design; stop allocating bundle lists per tick - #2359

Merged
erwan-joly merged 1 commit into
arch/map-tick-loopfrom
arch/ecs-conventions
Aug 30, 2026
Merged

docs: pin the ECS layering as the decided design; stop allocating bundle lists per tick#2359
erwan-joly merged 1 commit into
arch/map-tick-loopfrom
arch/ecs-conventions

Conversation

@erwan-joly

Copy link
Copy Markdown
Collaborator

Architecture-review PR 6: "decide what the ECS is".

The review flagged the component/bundle hybrid as a half-migration risk. Reading the generator changed the verdict: bundles are generated projections over real Arch components — the design is coherent, it just wasn't written down, so nothing stopped the next feature from parking state in a bundle body, a keyed service dictionary, or a static.

  • CLAUDE.md gains an "ECS layering" section pinning the roles: components own all state; bundles are the generated facade (hand-written members = computed views only); extension methods are per-entity behaviour; systems for iteration-heavy queries; hot paths never materialise bundle lists. This is the "declare the hybrid deliberate" option from the review — a full systems migration would rewrite working combat code for no player-visible gain.
  • The one hot-path violation is fixed: the map life loop read Monsters/Npcs, which allocate a fresh List per access — 4 list snapshots per map per 400 ms tick. The tick now enumerates the backing dictionaries.

Stacked on #2354 (base: arch/map-tick-loop) since it edits the same tick body — merge #2354 first, then retarget/rebase this onto master (one click).

Build clean; GameObject.Tests 535/535.

If you'd rather push further toward real systems (e.g. moving MonsterAi target scans into a query-based system), that's a follow-up we can scope separately — this PR deliberately locks in the current shape instead.

🤖 Generated with Claude Code

…dle lists per tick

Components own state, generated bundles are the facade, extensions are
per-entity behaviour, systems are for iteration-heavy queries - written
down so new state stops landing in whichever layer was convenient. The
map life loop also enumerated the Monsters/Npcs properties, which
materialise a fresh List per access; the tick now walks the backing
dictionaries directly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 30, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 736140e4-bfa2-4de1-8c59-9cc27aa06f94

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@erwan-joly
erwan-joly merged commit a481849 into arch/map-tick-loop Aug 30, 2026
1 check passed
erwan-joly added a commit that referenced this pull request Aug 30, 2026
…ty (#2354)

* perf: one awaited life loop per map instead of a 400ms timer per entity

Every monster and NPC ran its own Observable.Interval with a
fire-and-forget async tick: a slow tick overlapped the next one on the
same entity, and idle maps kept thousands of timers alive just to
early-out on IsSleeping. Entity AI now steps sequentially inside the
map's PeriodicTimer loop, which awaits each tick before scheduling the
next, so load shows up as tick latency instead of unbounded overlap.
Movement cadence is unchanged - MoveAsync keeps its own randomized
400-3200ms gate. The per-entity Life handle and the stop/start plumbing
around sleep transitions go away with the timers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* docs: pin the ECS layering as the decided design; stop allocating bundle lists per tick (#2359)

Components own state, generated bundles are the facade, extensions are
per-entity behaviour, systems are for iteration-heavy queries - written
down so new state stops landing in whichever layer was convenient. The
map life loop also enumerated the Monsters/Npcs properties, which
materialise a fresh List per access; the tick now walks the backing
dictionaries directly.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: start the life loop once, and let a tick finish before the world goes

Two from review.

StartLifeAsync checked _lifeLoop and assigned it without holding anything,
so two callers could both see null and start a second loop - every entity
on the map ticked twice per interval. The check and the assignment are one
critical section now.

Dispose cancelled the token and disposed EcsWorld in the same breath. The
token only ends the wait between ticks: a tick already inside TickLifeAsync
walks the monsters, npcs and sessions of that world and never sees it. It
now waits for the loop to come back before the world goes.

Tested: builds with 0 warnings, suite green - 1125 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant