SkillAgentSearch skills...

Garnet

F# game composition library

Install / Use

/learn @bcarruthers/Garnet
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Garnet

Build status

NuGet package

Garnet is a lightweight game composition library for F# with entity-component-system (ECS) and actor-like messaging features.

open Garnet.Composition

// events
[<Struct>] type Update = { dt : float32 }

// components
[<Struct>] type Position = { x : float32; y : float32 }
[<Struct>] type Velocity = { vx : float32; vy : float32 }

// create a world
let world = Container()

// register a system that updates position
let system =
    world.On<Update> <| fun e ->
        for r in world.Query<Position, Velocity>() do
            let p = &r.Value1
            let v = r.Value2
            p <- {
                x = p.x + v.vx * e.dt
                y = p.y + v.vy * e.dt
                }

// add an entity to world
let entity = 
    world.Create()
        .With({ x = 10.0f; y = 5.0f })
        .With({ vx = 1.0f; vy = 2.0f })

// run updates and print world state
for i = 1 to 10 do
    world.Run <| { dt = 0.1f }
    printfn "%O\n\n%O\n\n" world entity

Table of contents

Getting started

  1. Create either a .NET Framework, Core, or 6.0+ application.
  2. Reference the Garnet NuGet package.
  3. For sample code, see unit tests or sample projects.

Background

ECS is a common architecture for games, often contrasted with OOP inheritance. It focuses on separation of data and behavior and is typically implemented in a data-oriented way to achieve high performance. It's similar to a database, where component tables are related using a common entity ID, allowing systems to query and iterate over entities with specific combinations of components present. EC (entity-component) is a related approach that attaches behavior to components and avoids systems.

While ECS focuses on managing shared state, the actor model isolates state into separate actors which communicate only through messages. Actors can send and receive messages, change their behavior as a result of messages, and create new actors. This approach offers scaleability and an abstraction layer over message delivery, and games can use it at a high level to model independent processes, worlds, or agents.

Goals

  • Lightweight: Garnet is essentially a simplified in-memory database and messaging system suitable for games. No inheritance, attributes, or interface implementations are required in your code. It's more of a library than a framework or engine, and most of your code shouldn't depend on it.

  • Fast: Garbage collection spikes can cause dropped frames and inconsistent performance, so Garnet minimizes allocations and helps library users do so too. Component storage is data-oriented for fast iteration.

  • Minimal: The core library focuses on events, scheduling, and storage, and anything game-specific like physics, rendering, or update loops should be implemented separately.

Containers

ECS containers provide a useful bundle of functionality for working with shared game state, including event handling, component storage, entity ID generation, coroutine scheduling, and resource resolution.

// create a container/world
let c = Container()

Registry

Containers store single instances of types such as component lists, ID pools, settings, and any other arbitrary type. You can access instances by type, with optional lazy resolution. This is the service locator (anti-)pattern.

// option 1: add specific instance
c.SetValue(defaultWorldSettings)
// option 2: register a factory
c.SetFactory(fun () -> defaultWorldSettings)
// resolve type
let settings = c.GetValue<WorldSettings>()

This works for value types as well:

c.SetValue { zoomLevel = 0.5f }
let zoom = c.GetValue<Zoom>>()

Object pooling

Avoiding GC generally amounts to use of structs, pooling, and avoiding closures. Almost all objects are either pooled within a container or on the stack, so there's little or no GC impact or allocation once maximum load is reached. If needed, warming up or provisioning buffers ahead of time is possible for avoiding GC entirely during gameplay.

Commits

Certain operations on containers, such as sending events or adding/removing components, are staged until a commit occurs, allowing any running event handlers to observe the original state. Commits occur automatically after all subscribers have completed handling a list of events, so you typically shouldn't need to explicitly commit.

// create an entity
let e = c.Create().With("test")
// not yet visible
c.Commit()
// now visible

Entities

An entity is any identifiable thing in your game which you can attach components to. At minimum, an entity consists only of an entity ID.

Entity ID

Entity IDs are 32 bits and stored in a component list. This means they can be accessed and iterated over like any other component type without special handling. IDs use a special Eid type rather than a raw int32, which offers better type safety but means you need a direct dependency on Garnet if you want to define types with an Eid (or you can manage converting to your own ID type if this is an issue).

let entity = c.Create()
printfn "%A" entity.id

Generations

A portion of an ID is dedicated to its generation number. The purpose of a generation is to avoid reusing IDs while still allowing buffer slots to be reused, keeping components stored as densely as possible.

Partitioning

Component storage could become inefficient if it grows too sparse (i.e. the average number of occupied elements per segment becomes low). If this is a concern (or you just want to organize your entities), you can optionally use partitions to specify a high bit mask in ID generation. For example, if ship and bullet entities shared the same ID space, they may become mixed over time and the ship components would become sparse. Instead, with separate partitions, both entities would remain dense. Note: this will likely be replaced with groups in the future.

Generic storage

Storage should work well for both sequential and sparse data and support generic key types. Entity IDs are typically used as keys, but other types like grid location should be possible as well.

Inspecting

You can print the components of an entity at any time, which is useful in REPL scenarios as an alternative to using a debugger.

printfn "%s" <| c.Get(Eid 64).ToString()
Entity 0x40: 20 bytes
Eid 0x40
Loc {x = 10;
 y = 2;}
UnitType Archer
UnitSize {unitSize = 5;}

Components

Components are any arbitrary data type associated with an entity. Combined with systems that operate on them, components provide a way to specify behavior or capabilities of entities.

Data types

Components should ideally be pure data rather than classes with behavior and dependencies. They should typically be structs to avoid jumping around in memory or incurring allocations and garbage collection. Structs should almost always be immutable, but mutable structs (with their gotchas) are possible too.

[<Struct>] type Position = { x : float32; y : float32 }
[<Struct>] type Velocity = { vx : float32; vy : float32 }

// create an entity and add two components to it
let entity = 
    c.Create()
        .With({ x = 10.0f; y = 5.0f })
        .With({ vx = 1.0f; vy = 2.0f })

Storage

Components are stored in 64-element segments with a mask, ordered by ID. This provides CPU-friendly iteration over densely stored data while retaining some benefits of sparse storage. Some ECS implementations provide a variety of specialized data structures, but Garnet attempts a middle ground that works moderately well for both sequential entity IDs and sparse keys such as grid locations.

Only a single component of a type is allowed per entity, but there is no hard limit on the total number of different component types used (i.e. there is no fixed-size mask defining which components an entity has).

Iteration

You can iterate over entities with specific combinations of components using queries. In this way you could define a system that updates all entities with a position and velocity, and iteration would skip over any entities with only a position and not velocity.

let healthSub =
    c.On<DestroyZeroHealth> <| fun e ->
        for r in c.Query<Eid, Position, Health>() do
            let h = r.Value3
            if h.hp <= 0 then
                let eid = r.Value1
                c.Destroy(eid)

For batch operations or to improve performance further, you can iterate over segments:

let healthSub =
    c.On<DestroyZeroHealth> <| fun e ->
        for seg, eids, _, hs in c.QuerySegments<Eid, Position, Health>() do
            for i in seg do
                let h = hs.[i]
                if h.hp <= 0 then
                    let eid = eids.[i]
                    c.Destroy(eid)

Note that writes to existing components during iteration occur immediately, unlike adding or removing components.

Adding

Additions are deferred until a commit occurs, so any code dependent on those operations completing needs to be implemented as a coroutine.

let e = c.Get(Eid 100)
e.Add<Position> { x = 1.0f; y = 2.0f }
// change not yet visible

Removing

Like additions, removals are also deferred until commit. Note that you can repeatedly add and remove components for the same e

Related Skills

View on GitHub
GitHub Stars168
CategoryDevelopment
Updated1mo ago
Forks18

Languages

F#

Security Score

95/100

Audited on Mar 7, 2026

No findings