Components

Components contain the data associated to an Entity, i.e. their properties or state variables.

Component types

Components are distinguished by their type, and each entity can only have one component of a certain type.

In Ark, any type can be used as a component. However, it is highly recommended to use immutable types, because mutable objects are usually allocated on the heap in Julia, which defeats Ark's claim of high performance. Mutable types are disallowed by default, but can be enabled when constructing a World by the optional argument allow_mutable of the world constructor.

Accessing components

Although the majority of the logic in an application that uses Ark will be performed in Queries, it may be necessary to access components for a particular entity. One or more components of an entity can be accessed via get_components:

(pos, vel) = get_components(world, entity, (Position, Velocity))

Similarly, the components of an entity can be overwritten by new values via set_components!, which is particularly useful for immutable components (which are the default):

set_components!(world, entity, (Position(0, 0), Velocity(1,1)))

Adding and removing components

A feature that makes ECS particularly flexible and powerful is the ability to add components to and remove them from entities at runtime. This works similar to component access and can be done via add_components! and remove_components!:

entity = new_entity!(world, ())

add_components!(world, entity, (Position(0, 0), Velocity(1,1)))
remove_components!(world, entity, (Velocity,))

Note that adding an already existing component or removing a missing one results in an error.

Also note that it is more efficient to add/remove multiple components at once instead of one by one. To allow for efficient exchange of components (i.e. add some and remove others in the same operation), exchange_components! can be used:

entity = new_entity!(world, (Position(0, 0), Velocity(1,1)))

exchange_components!(world, entity; 
    add    = (Health(100),),
    remove = (Position, Velocity),
)

For manipulating entities in batches, add_components!, remove_components! and exchange_components! come with versions that take a filter instead of a single entity as argument. See chapter Batch operations for details.

Default component storages

Components are stored in archetypes, with the values for each component type stored in a separate array-like column. For these columns, Ark offers storage types for both CPU anf GPU computing by default.

CPU Storages

  • Vector storage stores components in a simple vector per column. This is the default.

  • StructArray storage stores components in an SoA data structure similar to StructArrays. This allows access to field vectors in queries, enabling SIMD-accelerated, vectorized operations and increased cache-friendliness if not all of the component's fields are used. StructArray storage has some limitations:

    • Not allowed for mutable components.
    • Not allowed for components without fields, like labels and primitives.
    • ≈10-20% runtime overhead for component operations and entity creation.
    • Slower component access with get_components and set_components!.

GPU Storages

  • GPUVector storage stores components using unified memory for mixed CPU/GPU operations. GPUVector is compatible with CUDA.jl, Metal.jl, oneAPI.jl or OpenCL.jl. Mutable components are not allowed.

  • GPUStructArray storage stores components in an SoA data structure similar to StructArrays using unified memory for mixed CPU/GPU operations. GPUVector is compatible with CUDA.jl, Metal.jl, oneAPI.jl or OpenCL.jl. The same limitations of StructArray storage apply.

Storage Selection

The storage mode can be selected per component type by using the Storage wrapper during world construction.

world = World(
    Position => Storage{Vector},
    Velocity => Storage{StructArray},
)

The default is Storage{Vector} if no storage mode is specified:

world = World(
    Position,
    Velocity => Storage{StructArray},
)

To use the GPUVector or the GPUStructArray storage, also the GPU backend must be specified (which can be either :CUDA, :Metal, :oneAPI or :OpenCL) depending on the GPU, as shown below:

using CUDA

world = World(
    Position => Storage{GPUVector{:CUDA}},
    Velocity => Storage{GPUStructArray{:CUDA}},
)

User-defined component storages

New storage modes can be created by the user. The new storage must be a one-indexed subtype of AbstractVector and must implement its required interface along with some optional methods. A complete example of a custom type is this one:

struct WrappedVector{C} <: AbstractVector{C}
    v::Vector{C}
end
WrappedVector{C}() where C = WrappedVector{C}(Vector{C}())

Base.size(w::WrappedVector) = size(w.v)
Base.getindex(w::WrappedVector, i::Integer) = getindex(w.v, i)
Base.setindex!(w::WrappedVector, v, i::Integer) = setindex!(w.v, v, i)
Base.empty!(w::WrappedVector) = empty!(w.v)
Base.resize!(w::WrappedVector, i::Integer) = resize!(w.v, i)
Base.sizehint!(w::WrappedVector, i::Integer) = sizehint!(w.v, i)
Base.pop!(w::WrappedVector) = pop!(w.v)

world = World(
    Position => Storage{WrappedVector},
    Velocity => Storage{StructArray},
)

All the methods in the example need to be defined, along with the empty constructor.