Garnet
F# game composition library
Install / Use
/learn @bcarruthers/GarnetREADME
Garnet
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
- Introduction
- Guide
- FAQ
- License
- Maintainers
Getting started
- Create either a .NET Framework, Core, or 6.0+ application.
- Reference the Garnet NuGet package.
- 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
node-connect
349.9kDiagnose OpenClaw node connection and pairing failures for Android, iOS, and macOS companion apps
frontend-design
109.8kCreate distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.
openai-whisper-api
349.9kTranscribe audio via OpenAI Audio Transcriptions API (Whisper).
qqbot-media
349.9kQQBot 富媒体收发能力。使用 <qqmedia> 标签,系统根据文件扩展名自动识别类型(图片/语音/视频/文件)。
