feat/generational ids - #34
Merged
Merged
Conversation
…rcises id recycling)
…tityId, null handle EntityId now packs the slot (low 32 bits) and its generation (high 32 bits, starting at 1). RemoveEntity bumps the slot's generation, so a handle kept after its entity was removed is dead even once the slot is recycled: Exists reports false, GetComponent nil, Add*/Remove* an error — never the slot's new occupant. The zero value is the null handle. Index(), Generation() and IsNull() expose the parts of a handle. The record keeps a tombstone (key -1) next to the generation, so a forged (index, generation+1) handle cannot resolve to a freed slot. The same unplaced state fixes a latent bug: an entity created bare then given a component was appended to its new archetype without being swap-removed from the empty archetype (stale entry, counted twice by optional-only queries, and whose later swap-remove rewrites the key of a live entity). A failed CreateEntityWithComponentsN now gives its slot back and returns the null handle. Benchmarks (5800X, n=6, vs v1.9.0): create 15.6 ms vs 16.4, create+remove 6.5 ms vs 6.2 (median +1.6 %), add/remove/iterate within ±2 % — inside the ±5 % budget. Tests: default and -race, all green.
… entity on swap-remove
The pinned golangci-lint v1.64.5 is built with Go 1.24 and cannot type-check the Go 1.25 toolchain that `go-version: stable` now resolves to, which fails the lint job. golangci-lint-action v9 runs on Node 24 (Node 20 is deprecated on GitHub runners) and requires golangci-lint v2. No configuration file: the default linters report 0 issues locally with v2.
|
@ragoune I highly recommend to handle dead entities more strictly, i.e. always panic when the user tries to do anything about them (incl. getting component or removing entity). Experience has shown that keeping a dead entity somewhere is one of the most common sources of bugs. If you let things slip through silently, these bugs will be hard to notice and to track down. |
Contributor
Author
|
Hello, indeed I hesitated to panic in this case, so it confirms it is the best way to handle this case. I will do it ASAP, before the next release. Thanks for the feedback ! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Generational entity handles
EntityIdis now a generational handle: the slot lives in the low 32 bits, its generation in the high 32 bits. Generations start at 1, soEntityId(0)is a guaranteed null handle (IsNull()).Index()andGeneration()expose the parts.RemoveEntitybumps the slot's generation. A handle kept after its entity was removed is dead, even once the slot is recycled by a new entity: it never aliases the new occupant.Existsreports false,GetComponentreturns nil,Add*/Remove*return an error,RemoveEntityis a no-op. The slot keeps a tombstone next to the generation, so a forged handle cannot resolve to a freed slot either.Bug fix
An entity created bare (
CreateEntity) and then given a component was appended to its new archetype without being swap-removed from the empty archetype. The stale entry was counted twice by optional-only queries, and a later swap-remove in the empty archetype rewrote the key of a live entity, aliasing its row with a neighbour's. Two tests reproduce it (both fail on v1.9.0).Also
BenchmarkCreateRemoveVolt: full remove+recreate cycle, exercises slot recycling.Breaking change
Any code indexing a slice or array by a raw
EntityIdmust useentityId.Index()instead, and== 0sentinels becomeIsNull().