Documentation ¶
Overview ¶
Package gamestate allows for buffering of state changes to the ECS dbStorage layer, and either committing those changes in an atomic Redis transaction, or discarding the changes. In either case, the underlying Redis DB is never in an intermediate state.
Atomic options ¶
There are two ways a batch of state changes can be grouped and applied/discarded.
EntityCommandBuffer.AtomicFn takes in a function that returns an error. The passed in function will be executed, and any state made during that function call will be stored as pending state changes. During this time, reads using the EntityCommandBuffer will report the pending values. Conversely, reading data directly from Redis the original value (before AtomicFn was called).
If the passed in function returns an error, all pending state changes will be discarded.
If the passed in function returns no error, all pending state changes will be committed to Redis in an atomic transaction.
Alternatively, EntityCommandBuffer can be used outside an AtomicFn context. State changes are stored as pending operations. Read operations will report the pending state. Note, no changes to Redis are applied while pending operations are accumulated.
Pending changes can be discarded with EntityCommandBuffer.DiscardPending. A subsequent read will return identical data to the data stored in Redis.
Pending changes can be committed to redis with EntityCommandBuffer.FinalizeTick. All pending changes will be packaged into a single redis [multi/exec pipeline](https://redis.io/docs/interact/transactions/) and applied atomically. Reads to redis during this time will never return any pending state. For example, if a series of 100 commands increments some value from 0 to 100, and then FinalizeTick is called, reading this value from the DB will only ever return 0 or 100 (depending on the exact timing of the call).
Redis PrimitiveStorage Model ¶
The Redis keys that store data in redis are defined in keys.go. All keys are prefixed with "ECB".
key: "ECB:NEXT-ENTITY-ID" value: An integer that represents the next available entity ID that can be assigned to some entity. It can be assumed that entity IDs smaller than this value have already been assigned.
key: fmt.Sprintf("ECB:COMPONENT-VALUE:TYPE-ID-%d:ENTITY-ID-%d", componentTypeID, entityID) value: JSON serialized bytes that can be deserialized to the component with the matching componentTypeID. This component data has been assigned to the entity matching the entityID.
key: fmt.Sprintf("ECB:ARCHETYPE-ID:ENTITY-ID-%d", entityID) value: An integer that represents the archetype ID that the matching entityID has been assigned to.
key: fmt.Sprintf("ECB:ACTIVE-ENTITY-IDS:ARCHETYPE-ID-%d", archetypeID) value: JSON serialized bytes that can be deserialized to a slice of integers. The integers represent the entity IDs that currently belong to the matching archetypeID. Note, this is a reverse mapping of the previous key.
key: "ECB:ARCHETYPE-ID-TO-COMPONENT-TYPES" value: JSON serialized bytes that can be deserialized to a map of archetype.ID to []component.ID. This field represents what archetype IDs have already been assigned and what groups of components each archetype ID corresponds to. This field must be loaded into memory before any entity creation or component addition/removals take place.
key: "ECB:START-TICK" value: An integer that represents the last tick that was started.
key: "ECB:END-TICK" value: An integer that represents the last tick that was successfully completed.
In-memory storage model ¶
The in-memory data model roughly matches the model that is stored in redis, but there are some differences:
Components are stored as generic interfaces and not as serialized JSON.
Potential Improvements ¶
In redis, the ECB:ACTIVE-ENTITY-IDS and ECB:ARCHETYPE-ID:ENTITY-ID keys contains the same data, but are just reversed mapping of one another. The amount of data in redis, and the data written can likely be reduced if we abandon one of these keys and rebuild the other mapping in memory.
In memory, compValues are written to redis during a FinalizeTick cycle. Components that were not actually changed (e.g. only read operations were performed) are still written to the DB.
Index ¶
- Variables
- type ArchetypeIterator
- type EntityCommandBuffer
- func (m *EntityCommandBuffer) AddComponentToEntity(cType types.ComponentMetadata, id types.EntityID) error
- func (m *EntityCommandBuffer) ArchetypeCount() int
- func (m *EntityCommandBuffer) Close() error
- func (m *EntityCommandBuffer) CreateEntity(comps ...types.ComponentMetadata) (types.EntityID, error)
- func (m *EntityCommandBuffer) CreateManyEntities(num int, comps ...types.ComponentMetadata) ([]types.EntityID, error)
- func (m *EntityCommandBuffer) DiscardPending() error
- func (m *EntityCommandBuffer) FinalizeTick(ctx context.Context) error
- func (m *EntityCommandBuffer) GetArchIDForComponents(components []types.ComponentMetadata) (types.ArchetypeID, error)
- func (m *EntityCommandBuffer) GetComponentForEntity(cType types.ComponentMetadata, id types.EntityID) (any, error)
- func (m *EntityCommandBuffer) GetComponentForEntityInRawJSON(cType types.ComponentMetadata, id types.EntityID) (json.RawMessage, error)
- func (m *EntityCommandBuffer) GetComponentTypesForArchID(archID types.ArchetypeID) ([]types.ComponentMetadata, error)
- func (m *EntityCommandBuffer) GetComponentTypesForEntity(id types.EntityID) ([]types.ComponentMetadata, error)
- func (m *EntityCommandBuffer) GetEntitiesForArchID(archID types.ArchetypeID) ([]types.EntityID, error)
- func (m *EntityCommandBuffer) GetLastFinalizedTick() (uint64, error)
- func (m *EntityCommandBuffer) RegisterComponents(comps []types.ComponentMetadata) error
- func (m *EntityCommandBuffer) RemoveComponentFromEntity(cType types.ComponentMetadata, id types.EntityID) error
- func (m *EntityCommandBuffer) RemoveEntity(idToRemove types.EntityID) error
- func (m *EntityCommandBuffer) SearchFrom(filter filter.ComponentFilter, start int) *ArchetypeIterator
- func (m *EntityCommandBuffer) SetComponentForEntity(cType types.ComponentMetadata, id types.EntityID, value any) error
- func (m *EntityCommandBuffer) ToReadOnly() Reader
- type Manager
- type MapStorage
- type PrimitiveStorage
- type Reader
- type RedisStorage
- func (r *RedisStorage) Clear(ctx context.Context) error
- func (r *RedisStorage) Close(ctx context.Context) error
- func (r *RedisStorage) Decr(ctx context.Context, key string) error
- func (r *RedisStorage) Delete(ctx context.Context, key string) error
- func (r *RedisStorage) EndTransaction(ctx context.Context) error
- func (r *RedisStorage) Get(ctx context.Context, key string) (any, error)
- func (r *RedisStorage) GetBool(ctx context.Context, key string) (bool, error)
- func (r *RedisStorage) GetBytes(ctx context.Context, key string) ([]byte, error)
- func (r *RedisStorage) GetFloat32(ctx context.Context, key string) (float32, error)
- func (r *RedisStorage) GetFloat64(ctx context.Context, key string) (float64, error)
- func (r *RedisStorage) GetInt(ctx context.Context, key string) (int, error)
- func (r *RedisStorage) GetInt64(ctx context.Context, key string) (int64, error)
- func (r *RedisStorage) GetUInt64(ctx context.Context, key string) (uint64, error)
- func (r *RedisStorage) Incr(ctx context.Context, key string) error
- func (r *RedisStorage) Keys(ctx context.Context) ([]string, error)
- func (r *RedisStorage) Set(ctx context.Context, key string, value any) error
- func (r *RedisStorage) StartTransaction(_ context.Context) (Transaction[string], error)
- type TickStorage
- type Transaction
- type VolatileStorage
- type Writer
Constants ¶
This section is empty.
Variables ¶
var ( ErrEntityDoesNotExist = errors.New("entity does not exist") ErrComponentAlreadyOnEntity = errors.New("component already on entity") ErrComponentNotOnEntity = errors.New("component not on entity") ErrEntityMustHaveAtLeastOneComponent = errors.New("entities must have at least 1 component") ErrMustRegisterComponent = errors.New("must register component") // ErrComponentMismatchWithSavedState is an error that is returned when a ComponentID from // the saved state is not found in the passed in list of components. ErrComponentMismatchWithSavedState = errors.New("registered components do not match with the saved state") )
var (
ErrArchetypeNotFound = errors.New("archetype for components not found")
)
var (
ErrNoArchIDMappingFound = errors.New("no mapping of archID to components found")
)
var ErrNotFound = errors.New("key not found in map")
Functions ¶
This section is empty.
Types ¶
type ArchetypeIterator ¶ added in v1.6.0
type ArchetypeIterator struct { Current int Values []types.ArchetypeID }
func (*ArchetypeIterator) HasNext ¶ added in v1.6.0
func (it *ArchetypeIterator) HasNext() bool
func (*ArchetypeIterator) Next ¶ added in v1.6.0
func (it *ArchetypeIterator) Next() types.ArchetypeID
type EntityCommandBuffer ¶
type EntityCommandBuffer struct {
// contains filtered or unexported fields
}
func NewEntityCommandBuffer ¶
func NewEntityCommandBuffer(storage PrimitiveStorage[string]) (*EntityCommandBuffer, error)
NewEntityCommandBuffer creates a new command buffer manager that is able to queue up a series of states changes and atomically commit them to the underlying redis dbStorage layer.
func (*EntityCommandBuffer) AddComponentToEntity ¶
func (m *EntityCommandBuffer) AddComponentToEntity(cType types.ComponentMetadata, id types.EntityID) error
AddComponentToEntity adds the given component to the given entity. An error is returned if the entity already has this component.
func (*EntityCommandBuffer) ArchetypeCount ¶
func (m *EntityCommandBuffer) ArchetypeCount() int
ArchetypeCount returns the number of archetypes that have been generated.
func (*EntityCommandBuffer) Close ¶
func (m *EntityCommandBuffer) Close() error
Close closes the manager.
func (*EntityCommandBuffer) CreateEntity ¶
func (m *EntityCommandBuffer) CreateEntity(comps ...types.ComponentMetadata) (types.EntityID, error)
CreateEntity creates a single entity with the given set of components.
func (*EntityCommandBuffer) CreateManyEntities ¶
func (m *EntityCommandBuffer) CreateManyEntities(num int, comps ...types.ComponentMetadata) ([]types.EntityID, error)
CreateManyEntities creates many entities with the given set of components.
func (*EntityCommandBuffer) DiscardPending ¶
func (m *EntityCommandBuffer) DiscardPending() error
DiscardPending discards any pending state changes.
func (*EntityCommandBuffer) FinalizeTick ¶
func (m *EntityCommandBuffer) FinalizeTick(ctx context.Context) error
FinalizeTick combines all pending state changes into a single multi/exec redis transactions and commits them to the DB.
func (*EntityCommandBuffer) GetArchIDForComponents ¶
func (m *EntityCommandBuffer) GetArchIDForComponents(components []types.ComponentMetadata) (types.ArchetypeID, error)
GetArchIDForComponents returns the archetype EntityID that has been assigned to this set of components. If this set of components does not have an archetype EntityID assigned to it, an error is returned.
func (*EntityCommandBuffer) GetComponentForEntity ¶
func (m *EntityCommandBuffer) GetComponentForEntity(cType types.ComponentMetadata, id types.EntityID) (any, error)
GetComponentForEntity returns the saved component data for the given entity.
func (*EntityCommandBuffer) GetComponentForEntityInRawJSON ¶
func (m *EntityCommandBuffer) GetComponentForEntityInRawJSON(cType types.ComponentMetadata, id types.EntityID) ( json.RawMessage, error, )
GetComponentForEntityInRawJSON returns the saved component data as JSON encoded bytes for the given entity.
func (*EntityCommandBuffer) GetComponentTypesForArchID ¶
func (m *EntityCommandBuffer) GetComponentTypesForArchID(archID types.ArchetypeID) ([]types.ComponentMetadata, error)
GetComponentTypesForArchID returns the set of components that are associated with the given archetype id.
func (*EntityCommandBuffer) GetComponentTypesForEntity ¶
func (m *EntityCommandBuffer) GetComponentTypesForEntity(id types.EntityID) ([]types.ComponentMetadata, error)
GetComponentTypesForEntity returns all the component types that are currently on the given entity. Only types are returned. To get the actual component data, use GetComponentForEntity.
func (*EntityCommandBuffer) GetEntitiesForArchID ¶
func (m *EntityCommandBuffer) GetEntitiesForArchID(archID types.ArchetypeID) ([]types.EntityID, error)
GetEntitiesForArchID returns all the entities that currently belong to the given archetype EntityID.
func (*EntityCommandBuffer) GetLastFinalizedTick ¶ added in v1.6.0
func (m *EntityCommandBuffer) GetLastFinalizedTick() (uint64, error)
GetLastFinalizedTick returns the last tick that was successfully finalized. If the latest finalized tick is 0, it means that no tick has been finalized yet.
func (*EntityCommandBuffer) RegisterComponents ¶
func (m *EntityCommandBuffer) RegisterComponents(comps []types.ComponentMetadata) error
func (*EntityCommandBuffer) RemoveComponentFromEntity ¶
func (m *EntityCommandBuffer) RemoveComponentFromEntity(cType types.ComponentMetadata, id types.EntityID) error
RemoveComponentFromEntity removes the given component from the given entity. An error is returned if the entity does not have the component.
func (*EntityCommandBuffer) RemoveEntity ¶
func (m *EntityCommandBuffer) RemoveEntity(idToRemove types.EntityID) error
RemoveEntity removes the given entity from the ECS data model.
func (*EntityCommandBuffer) SearchFrom ¶
func (m *EntityCommandBuffer) SearchFrom(filter filter.ComponentFilter, start int) *ArchetypeIterator
SearchFrom returns an ArchetypeIterator based on a component filter. The iterator will iterate over all archetypes that match the given filter.
func (*EntityCommandBuffer) SetComponentForEntity ¶
func (m *EntityCommandBuffer) SetComponentForEntity( cType types.ComponentMetadata, id types.EntityID, value any, ) error
SetComponentForEntity sets the given entity's component data to the given value.
func (*EntityCommandBuffer) ToReadOnly ¶
func (m *EntityCommandBuffer) ToReadOnly() Reader
type Manager ¶
type Manager interface { TickStorage Reader Writer ToReadOnly() Reader }
Manager represents all the methods required to track Component, Entity, and Archetype information which powers the ECS dbStorage layer.
type MapStorage ¶
type MapStorage[K comparable, V any] struct { // contains filtered or unexported fields }
func NewMapStorage ¶
func NewMapStorage[K comparable, V any]() *MapStorage[K, V]
func (*MapStorage[K, V]) Clear ¶
func (m *MapStorage[K, V]) Clear() error
func (*MapStorage[K, V]) Delete ¶
func (m *MapStorage[K, V]) Delete(key K) error
func (*MapStorage[K, V]) Get ¶
func (m *MapStorage[K, V]) Get(key K) (V, error)
func (*MapStorage[K, V]) Keys ¶
func (m *MapStorage[K, V]) Keys() ([]K, error)
func (*MapStorage[K, V]) Len ¶
func (m *MapStorage[K, V]) Len() int
func (*MapStorage[K, V]) Set ¶
func (m *MapStorage[K, V]) Set(key K, value V) error
type PrimitiveStorage ¶
type PrimitiveStorage[K comparable] interface { GetFloat64(ctx context.Context, key K) (float64, error) GetFloat32(ctx context.Context, key K) (float32, error) GetUInt64(ctx context.Context, key K) (uint64, error) GetInt64(ctx context.Context, key K) (int64, error) GetInt(ctx context.Context, key K) (int, error) GetBool(ctx context.Context, key K) (bool, error) GetBytes(ctx context.Context, key K) ([]byte, error) Get(ctx context.Context, key K) (any, error) Set(ctx context.Context, key K, value any) error Incr(ctx context.Context, key K) error Decr(ctx context.Context, key K) error Delete(ctx context.Context, key K) error StartTransaction(ctx context.Context) (Transaction[K], error) EndTransaction(ctx context.Context) error Close(ctx context.Context) error Clear(ctx context.Context) error Keys(ctx context.Context) ([]K, error) }
PrimitiveStorage is the interface for all available stores related to the game loop there is another store like interface for other logistical values located in `ecs.storage`
type Reader ¶
type Reader interface { // One Component One Entity GetComponentForEntity(cType types.ComponentMetadata, id types.EntityID) (any, error) GetComponentForEntityInRawJSON(cType types.ComponentMetadata, id types.EntityID) (json.RawMessage, error) // Many Components One Entity GetComponentTypesForEntity(id types.EntityID) ([]types.ComponentMetadata, error) // One Archetype Many Components GetComponentTypesForArchID(archID types.ArchetypeID) ([]types.ComponentMetadata, error) GetArchIDForComponents(components []types.ComponentMetadata) (types.ArchetypeID, error) // One Archetype Many Entities GetEntitiesForArchID(archID types.ArchetypeID) ([]types.EntityID, error) // Misc SearchFrom(filter filter.ComponentFilter, start int) *ArchetypeIterator ArchetypeCount() int }
type RedisStorage ¶
type RedisStorage struct {
// contains filtered or unexported fields
}
func NewRedisPrimitiveStorage ¶
func NewRedisPrimitiveStorage(client redis.Cmdable) RedisStorage
func (*RedisStorage) EndTransaction ¶
func (r *RedisStorage) EndTransaction(ctx context.Context) error
func (*RedisStorage) Get ¶
Underlying type is a string. Unfortunately this is the way redis works and this is the most generic return value.
func (*RedisStorage) GetFloat32 ¶
func (*RedisStorage) GetFloat64 ¶
func (*RedisStorage) StartTransaction ¶
func (r *RedisStorage) StartTransaction(_ context.Context) (Transaction[string], error)
type TickStorage ¶
type Transaction ¶
type Transaction[K comparable] interface { PrimitiveStorage[K] }
type VolatileStorage ¶
type VolatileStorage[K comparable, V any] interface { Get(key K) (V, error) Set(key K, value V) error Delete(key K) error Keys() ([]K, error) Clear() error Len() int }
this interface is meant for in memory storage
type Writer ¶
type Writer interface { // One Entity RemoveEntity(id types.EntityID) error // Many Components CreateEntity(comps ...types.ComponentMetadata) (types.EntityID, error) CreateManyEntities(num int, comps ...types.ComponentMetadata) ([]types.EntityID, error) // One Component One Entity SetComponentForEntity(cType types.ComponentMetadata, id types.EntityID, value any) error AddComponentToEntity(cType types.ComponentMetadata, id types.EntityID) error RemoveComponentFromEntity(cType types.ComponentMetadata, id types.EntityID) error // Misc Close() error RegisterComponents([]types.ComponentMetadata) error }