Component System

← ECS & Storage | Entity Lifecycle → | Home


FieldProxy — SoA with OOP Syntax

FieldProxy<T, FieldWidth> is the core component abstraction. It wraps raw SoA array pointers behind operator overloads so gameplay code reads as OOP while compiling as direct array access.

// Gameplay author writes:
transform.PosX += velocity.VelX * dt;   // OOP syntax

// Compiles to:
writeArrayPosX[entityIndex] += writeArrayVelX[entityIndex] * dt;   // direct SoA access
// No virtual dispatch, no map lookup.

Each proxy holds:

  • WriteArray — pointer to the field's SoA array for the current write frame
  • index — current entity offset within the array
  • mask — AVX2 mask for WideMask partial-lane writes (zero-size in Scalar mode to save 32 bytes/field)

Three Field Widths

WidthEntities/iterationUse case
Scalar1Construct tick access — safe, default
Wide8 (AVX2)Full SIMD batch — count must be multiple of 8
WideMask8 with tail maskHandles non-multiple-of-8 tail chunks

The same component type participates in both scalar Construct ticks and 8-wide entity sweeps without any code change at the component level. The width is a template parameter, not a runtime choice.

FieldProxy as a Row in the Spreadsheet

Using the global spreadsheet model:

  • The field (Transform.PosX) is the row
  • EntityCacheIndex is the column
  • FieldProxy::Bind(writePtr, idx) positions the cursor at cell (row=this field, column=entityIndex)
  • Advance(step) moves +1 (Scalar) or +8 (Wide) columns

FieldProxyMask

FieldProxyMask<WIDTH> is a zero-size base type for Scalar mode — saves 32 bytes per field vs always storing __m256i mask. Wide and WideMask access the mask via this->mask.


Component Registration Macros

Three macros declare a component's storage tier and field list:

Fields live in the Temporal SoA tier (N-frame rollback ring). Used for physics-authoritative and networked data.

template <FieldWidth WIDTH = FieldWidth::Scalar>
struct CTransform
{
    FieldProxy<SimFloat, WIDTH> PosX, PosY, PosZ;
    FieldProxy<SimFloat, WIDTH> RotQx, RotQy, RotQz, RotQw;

    TNX_TEMPORAL_FIELDS(CTransform, SystemGroup::None,
        PosX, PosY, PosZ, RotQx, RotQy, RotQz, RotQw)
};
TNX_REGISTER_COMPONENT(CTransform)

SystemGroup::None means this component alone doesn't determine partition placement (other components on the same entity do).

Fields live in the Volatile SoA tier (3-frame triple-buffer). Used for cosmetic data that doesn't need rollback.

template <FieldWidth WIDTH = FieldWidth::Scalar>
struct CColor
{
    FieldProxy<float, WIDTH> R, G, B, A;

    TNX_VOLATILE_FIELDS(CColor, SystemGroup::Render, R, G, B, A)
};
TNX_REGISTER_COMPONENT(CColor)

SystemGroup::Render means entities with this component go into the RENDER or DUAL partition.

Fields live in archetype chunk memory (Cold tier). No slab storage, no rollback, no tier cost. Used for config data read during logic but never iterated in hot paths.

template <FieldWidth WIDTH = FieldWidth::Scalar>
struct CJoltBody
{
    FieldProxy<float, WIDTH> ShapeHalfExtentX, ShapeHalfExtentY, ShapeHalfExtentZ;
    FieldProxy<float, WIDTH> Mass;

    TNX_REGISTER_FIELDS(CJoltBody, ShapeHalfExtentX, ShapeHalfExtentY, ShapeHalfExtentZ, Mass)
};
TNX_REGISTER_COMPONENT(CJoltBody)

Cold components contribute no slab storage and do not affect the entity's tier classification.


Entity Definition

Entities are CRTP structs templated on FieldWidth. The partition group is auto-derived from component SystemGroup tags — no manual annotation on the entity.

template <FieldWidth WIDTH = FieldWidth::Scalar>
struct EInstanced : EntityView<EInstanced, WIDTH>
{
    CTransform<WIDTH> Transform;  // SystemGroup::None
    CJoltBody<WIDTH>  Jolt;       // SystemGroup::Phys (Cold, but tagged)
    CScale<WIDTH>     Scale;      // SystemGroup::Render (Volatile)
    CColor<WIDTH>     Color;      // SystemGroup::Render (Volatile)
    CMeshRef<WIDTH>   Mesh;       // SystemGroup::Render (Volatile)

    // Derived partition: Phys + Render → DUAL

    void PrePhysics(SimFloat dt)
    {
        // User logic here
    }

    TNX_REGISTER_SCHEMA(EInstanced, EntityView, Transform, Jolt, Scale, Color, Mesh)
};

Dynamic Chunk Sizing

The target entity count per chunk can be specified at the class level:

template <FieldWidth WIDTH = FieldWidth::Scalar>
class EProjectile : public EntityView<EProjectile, WIDTH>
{
    static constexpr uint32_t EntitiesPerChunk = 4096;  // High count for SIMD throughput
    // ...
};

Default is 256 entities per chunk. Data-heavy entities (projectiles, particles) benefit from larger chunks. Low-count entities (players) can use smaller chunks to reduce padding waste.

Inheritance Pattern

// Base entity (defines shared fields and PrePhysics logic)
template <typename Derived, FieldWidth WIDTH = FieldWidth::Scalar>
struct EBaseCube : EntityView<Derived, WIDTH>
{
    CTransform<WIDTH> Transform;
    CColor<WIDTH>     Color;

    void PrePhysics(SimFloat dt) { Transform.RotQy += SimFloat(0.7f) * dt; }

    TNX_REGISTER_SUPER_SCHEMA(EBaseCube, EntityView, Transform, Color)
};

// Derived entity (adds physics body, overrides PrePhysics)
template <FieldWidth WIDTH = FieldWidth::Scalar>
struct ESuperCube : EBaseCube<ESuperCube, WIDTH>
{
    CRigidBody<WIDTH> Body;  // SystemGroup::Phys → promotes to Temporal

    void PrePhysics(SimFloat dt)
    {
        EBaseCube<ESuperCube, WIDTH>::PrePhysics(dt);
        Transform.PosX += Body.VelX * dt;
        Transform.PosY += Body.VelY * dt;
        Transform.PosZ += Body.VelZ * dt;
    }

    TNX_REGISTER_SCHEMA(ESuperCube, EBaseCube, Body)
};

Schema Validation

SchemaValidation.h enforces two hard constraints at compile time:

  1. No virtual functions. A vtable pointer in a component struct pollutes every SoA array slot and breaks the schema. VALIDATE_COMPONENT_IS_POD catches this.
  2. All DefineFields()-registered fields must be FieldProxy types. A raw float added to a component struct but not registered in DefineFields() is inert — invisible to the SoA system, can't corrupt layout. The schema is the enforcement boundary; the Bind/Advance requirement catches mismatches.

Schema validation errors produce clear, actionable compile-time messages. See Schema Error Reference.


Built-In Components

The engine provides a set of ready-to-use components. Gameplay authors can build entirely without defining custom components.

ComponentTierSystemGroupPurpose
CTransformTemporalNonePosition (Fixed32) + rotation (quaternion)
CTranslationTemporalNonePosition-only (no rotation)
CRotationTemporalNoneRotation-only
CVelocityTemporalPhysLinear velocity
CRigidBodyTemporalPhysFull rigid body (vel + angular vel)
CJoltBodyVolatilePhysJolt shape/motion/mass data (Cold in chunks)
CScaleVolatileRenderNon-uniform scale
CColorVolatileRenderRGBA color
CMeshRefVolatileRenderMesh slot reference
CVisualTransformVolatileRenderInterpolated visual position (EInterpEntity)
CCameraLayerColdNoneCamera layer config

SimFloat — Determinism Alias

SimFloat is the canonical numeric type for all physics-authoritative simulation data:

#if TNX_DETERMINISTIC
    using SimFloat = Fixed32;  // Integer determinism for rollback
#else
    using SimFloat = float;    // Float for non-deterministic builds
#endif

template <FieldWidth WIDTH = FieldWidth::Scalar>
using FloatProxy = FieldProxy<SimFloat, WIDTH>;

Entity and component authors always write FloatProxy<WIDTH> — the backing type is decided at compile time by TNX_DETERMINISM. Code written against SimFloat works in both modes unchanged.

See Fixed-Point Math for the full fixed-point system.