Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add warnings on storing pointers, extend docs #162

Merged
merged 5 commits into from
Mar 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion ecs/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ var entityIndexSize = sizeOf(typeOf[entityIndex]())

//var wildcard = Entity{1, 0}

// Entity identifier.
// Entity is an identifier for entities.
//
// Can be stored safely in components, resources or elsewhere.
// For stored entities, it may be necessary to check their alive status with [World.Alive].
//
// ⚠️ Always store entities by value, never by pointer!
type Entity struct {
id entityID // Entity ID
gen uint32 // Entity generation
Expand Down
96 changes: 88 additions & 8 deletions ecs/exchange_gen.go

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions ecs/filter_gen.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion ecs/generate/exchange.go.template
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import "unsafe"
// Exchange{{.}} allows to exchange components of entities.
// It adds the given components. Use [Exchange{{.}}.Removes]
// to set components to be removed.
//
// Instances should be created during initialization and stored, e.g. in systems.
type Exchange{{.}}{{$generics}} struct {
world *World
ids []ID
Expand Down Expand Up @@ -59,6 +61,8 @@ func (ex *Exchange{{.}}{{$genericsShort}}) Add(entity Entity, {{$args}}, rel ...

// AddFn adds the mapped components to the given entity and runs a callback instead of using components for initialization.
// The callback can be nil.
//
// ⚠️ Do not store the obtained pointers outside of the current context!
func (ex *Exchange{{.}}{{$genericsShort}}) AddFn(entity Entity, fn func({{$args}}), rel ...Relation) {
ex.relations = relations(rel).toRelations(ex.world, ex.ids, nil, ex.relations)
ex.world.exchange(entity, ex.ids, nil, nil, ex.relations)
Expand All @@ -83,10 +87,12 @@ func (ex *Exchange{{.}}{{$genericsShort}}) Exchange(entity Entity, {{$args}}, re
}, ex.relations)
}

// Exchange performs the exchange on the given entity, adding the provided components
// ExchangeFn performs the exchange on the given entity, adding the provided components
// and removing those previously specified with [Exchange{{.}}.Removes].
// It runs a callback instead of using components for initialization.
// The callback can be nil.
//
// ⚠️ Do not store the obtained pointers outside of the current context!
func (ex *Exchange{{.}}{{$genericsShort}}) ExchangeFn(entity Entity, fn func({{$args}}), rel ...Relation) {
ex.relations = relations(rel).toRelations(ex.world, ex.ids, nil, ex.relations)
ex.world.exchange(entity, ex.ids, ex.remove, nil, ex.relations)
Expand All @@ -107,6 +113,8 @@ func (ex *Exchange{{.}}{{$genericsShort}}) AddBatch(batch *Batch, {{$args}}, rel

// AddBatchFn adds the mapped components to all entities matching the given batch filter,
// running the given function on each. The function can be nil.
//
// ⚠️ Do not store the obtained pointers outside of the current context!
func (ex *Exchange{{.}}{{$genericsShort}}) AddBatchFn(batch *Batch, fn func(entity Entity, {{$args}}), rel ...Relation) {
ex.exchangeBatchFn(batch, fn, false, rel...)
}
Expand Down Expand Up @@ -143,6 +151,8 @@ func (ex *Exchange{{.}}{{$genericsShort}}) ExchangeBatch(batch *Batch, {{$args}}

// ExchangeBatchFn performs the exchange on all entities matching the given batch filter,
// running the given function on each. The function can be nil.
//
// ⚠️ Do not store the obtained pointers outside of the current context!
func (ex *Exchange{{.}}{{$genericsShort}}) ExchangeBatchFn(batch *Batch, fn func(entity Entity, {{$args}}), rel ...Relation) {
ex.exchangeBatchFn(batch, fn, true, rel...)
}
Expand Down
2 changes: 2 additions & 0 deletions ecs/generate/filter.go.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ package ecs
{{- end}}

// Filter{{.}} is a filter for {{.}} components.
//
// Instances should be created during initialization and stored, e.g. in systems.
type Filter{{.}}{{$generics}} struct {
world *World
ids []ID
Expand Down
14 changes: 12 additions & 2 deletions ecs/generate/maps.go.template
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import "unsafe"
{{- $mask := join "id" ", id" "" $upper -}}

// Map{{.}} is a mapper to access {{.}} components of an entity.
//
// Instances should be created during initialization and stored, e.g. in systems.
type Map{{.}}{{$generics}} struct {
world *World
ids []ID
Expand Down Expand Up @@ -53,6 +55,8 @@ func (m *Map{{.}}{{$genericsShort}}) NewEntity({{$args}}, rel ...Relation) Entit

// NewEntityFn creates a new entity with the mapped component and runs a callback instead of using a component for initialization.
// The callback can be nil.
//
// ⚠️ Do not store the obtained pointers outside of the current context!
func (m *Map{{.}}{{$genericsShort}}) NewEntityFn(fn func({{$args}}), rel ...Relation) Entity {
m.relations = relations(rel).toRelations(m.world, m.ids, nil, m.relations)
entity := m.world.newEntityWith(m.ids, nil, m.relations)
Expand Down Expand Up @@ -80,6 +84,8 @@ func (m *Map{{.}}{{$genericsShort}}) NewBatch(count int, {{$args}}, rel ...Relat

// NewBatchFn creates a batch of new entities with the mapped components, running the given initializer function on each.
// The initializer function can be nil.
//
// ⚠️ Do not store the obtained pointers outside of the current context!
func (m *Map{{.}}{{$genericsShort}}) NewBatchFn(count int, fn func(entity Entity, {{$args}}), rel ...Relation) {
m.relations = relations(rel).toRelations(m.world, m.ids, nil, m.relations)
tableID, start := m.world.newEntities(count, m.ids, m.relations)
Expand Down Expand Up @@ -107,7 +113,7 @@ func (m *Map{{.}}{{$genericsShort}}) NewBatchFn(count int, fn func(entity Entity

// Get returns the mapped components for the given entity.
//
// ⚠️ Do not store the obtained pointer outside of the current context!
// ⚠️ Do not store the obtained pointers outside of the current context!
func (m *Map{{.}}{{$genericsShort}}) Get(entity Entity) {{$return}} {
if !m.world.Alive(entity) {
panic("can't get components of a dead entity")
Expand All @@ -119,7 +125,7 @@ func (m *Map{{.}}{{$genericsShort}}) Get(entity Entity) {{$return}} {
// In contrast to [Map{{.}}.Get], it does not check whether the entity is alive.
// Can be used as an optimization when it is certain that the entity is alive.
//
// ⚠️ Do not store the obtained pointer outside of the current context!
// ⚠️ Do not store the obtained pointers outside of the current context!
func (m *Map{{.}}{{$genericsShort}}) GetUnchecked(entity Entity) {{$return}} {
{{- range $n}}
m.world.storage.checkHasComponent(entity, m.ids[{{.}}])
Expand Down Expand Up @@ -154,6 +160,8 @@ func (m *Map{{.}}{{$genericsShort}}) Add(entity Entity, {{$args}}, rel ...Relati

// AddFn adds the mapped components to the given entity and runs a callback instead of using components for initialization.
// The callback can be nil.
//
// ⚠️ Do not store the obtained pointers outside of the current context!
func (m *Map{{.}}{{$genericsShort}}) AddFn(entity Entity, fn func({{$args}}), rel ...Relation) {
m.relations = relations(rel).toRelations(m.world, m.ids, nil, m.relations)
m.world.exchange(entity, m.ids, nil, nil, m.relations)
Expand All @@ -180,6 +188,8 @@ func (m *Map{{.}}{{$genericsShort}}) AddBatch(batch *Batch, {{$args}}, rel ...Re

// AddBatchFn adds the mapped components to all entities matching the given batch filter,
// running the given function on each. The function can be nil.
//
// ⚠️ Do not store the obtained pointers outside of the current context!
func (m *Map{{.}}{{$genericsShort}}) AddBatchFn(batch *Batch, fn func(entity Entity, {{$args}}), rel ...Relation) {
m.relations = relations(rel).toRelations(m.world, m.ids, nil, m.relations)

Expand Down
6 changes: 4 additions & 2 deletions ecs/generate/query.go.template
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type cursor struct {

// Query{{.}} is a query for {{.}} components.
// Use a [NewFilter{{.}}] to create one.
//
// Queries are one-time use iterators and must be re-created each time before iterating.
type Query{{.}}{{$generics}} struct {
world *World
filter *filter
Expand Down Expand Up @@ -83,7 +85,7 @@ func (q *Query{{.}}{{$genericsShort}}) Entity() Entity {
{{if . -}}
// Get returns the queried components of the current entity.
//
// ⚠️ Do not store the obtained pointer outside of the current context (i.e. the query loop)!
// ⚠️ Do not store the obtained pointers outside of the current context (i.e. the query loop)!
func (q *Query{{.}}{{$genericsShort}}) Get() {{$return}} {
q.world.checkQueryGet(&q.cursor)
return {{range $i, $v := $upper}}{{if $i}},
Expand All @@ -98,7 +100,7 @@ func (q *Query{{.}}{{$genericsShort}}) GetRelation(index int) Entity {

// Close closes the Query and unlocks the world.
//
// Automatically called when iteration finishes.
// Automatically called when iteration completes.
// Needs to be called only if breaking out of the query iteration or not iterating at all.
func (q *Query{{.}}{{$genericsShort}}) Close() {
q.cursor.archetype = -2
Expand Down
6 changes: 6 additions & 0 deletions ecs/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package ecs
import "unsafe"

// Map is a mapper to access and manipulate components of an entity.
//
// Instances should be created during initialization and stored, e.g. in systems.
type Map[T any] struct {
world *World
id ID
Expand All @@ -28,6 +30,8 @@ func (m *Map[T]) NewEntity(comp *T, rel ...Entity) Entity {

// NewEntityFn creates a new entity with the mapped component and runs a callback instead of using a component for initialization.
// The callback can be nil.
//
// ⚠️ Do not store the obtained pointer outside of the current context!
func (m *Map[T]) NewEntityFn(fn func(a *T), rel ...Entity) Entity {
m.relations = relationEntities(rel).toRelation(m.world, m.id, m.relations)
entity := m.world.newEntityWith([]ID{m.id}, nil, m.relations)
Expand Down Expand Up @@ -85,6 +89,8 @@ func (m *Map[T]) Add(entity Entity, comp *T, rel ...Entity) {

// AddFn adds the mapped component to the given entity and runs a callback instead of using a component for initialization.
// The callback can be nil.
//
// ⚠️ Do not store the obtained pointer outside of the current context!
func (m *Map[T]) AddFn(entity Entity, fn func(a *T), rel ...Entity) {
if !m.world.Alive(entity) {
panic("can't add a component to a dead entity")
Expand Down
Loading
Loading