A dependency-free, lightweight, fast, and easy-to-use Entity-Component System for Swift. Components are value types, identifiers are generational, and the default archetype storage iterates families linearly. Developed and maintained as part of the Fireblade Game Engine project.
Coming from 1.x? Read the migration guide, and see the Fireblade ECS Demo App or the online documentation.
Fireblade ECS requires Swift 6.2 or later and Swift Package Manager (SPM). It supports macOS, iOS, tvOS, watchOS, visionOS, Linux, Windows, and WebAssembly.
Add Fireblade ECS to your Package.swift:
// swift-tools-version:6.2
import PackageDescription
let package = Package(
name: "MyGame",
dependencies: [
.package(url: "https://github.com/fireblade-engine/ecs.git", from: "2.0.0")
],
targets: [
.target(
name: "MyGame",
dependencies: [
.product(name: "FirebladeECS", package: "ecs"),
.product(name: "FirebladeECSMacros", package: "ecs"), // Optional: @Component and @FamilyTraits
.product(name: "FirebladeFSM", package: "ecs") // Optional: Entity state machines
])
]
)Import FirebladeECS in your source files:
import FirebladeECS
import FirebladeECSMacros // Only where @Component or @FamilyTraits is used
import FirebladeFSM // Only where entity state machines are usedimport FirebladeECS
// 1. Define value-type components
struct Position: Component { var x = 0, y = 0 }
struct Velocity: Component { var dx = 0, dy = 0 }
// 2. Initialize a Nexus and create an entity
let nexus = Nexus()
let entity = nexus.createEntity {
Position(x: 0, y: 0)
Velocity(dx: 1, dy: 2)
}
// 3. Query matching entities and update in place
let family = nexus.family(requiresAll: Position.self, Velocity.self)
family.forEach(reading: Velocity.self, writing: Position.self) { velocity, position in
position.x += velocity.dx
position.y += velocity.dy
}The Nexus is the central manager that stores, organizes, and indexes entities and their components.
let nexus = Nexus() // Archetype storage (default)
let churningNexus = Nexus(storage: .sparseSet) // Sparse set storageFireblade ECS supports two storage models:
- Archetype Storage (Default): Entities with identical component combinations are packed contiguously in cache-local memory arrays. Highly optimized for linear, fast system iteration.
- Sparse Set Storage (
storage: .sparseSet): Stores components in separate sparse sets. Ideal for worlds with high structural churn (frequent component addition and removal).
Components are value types conforming to Component:
struct Position: Component {
var x: Int = 0
var y: Int = 0
}Create entities and manage their components:
let entity = nexus.createEntity()
// Assign or replace a component instance
entity.set(Position(x: 1, y: 2))
// Read a component (returns a copy)
let copy = entity.get(Position.self) // Position?, a copy
// Mutate a component in place (avoids copying and replacing the entire struct)
entity.modify(Position.self) { $0.x += 1 }
entity[Position.self]?.y += 1
entity.modify(Position.self, default: Position()) { $0.x += 1 }
// Create an entity with components inline:
nexus.createEntity {
Position(x: 1, y: 2)
Velocity(dx: 1, dy: 0)
}
// Bulk create entities:
nexus.createEntities(count: 100) { _ in
Position()
Velocity()
}A Family groups entities that share specific component traits for fast, cache-local iteration:
let family = nexus.family(requiresAll: Position.self, Velocity.self,
excludesAll: Frozen.self)Use forEach(reading:writing:) to update entities. reading: passes immutable copies, while writing: passes mutable inout references that write directly back to storage without heap allocations:
struct Frozen: Component {}
struct Velocity: Component { var dx = 0, dy = 0 }
final class MovementSystem {
let family: Family<Position, Velocity>
init(nexus: Nexus) {
family = nexus.family(requiresAll: Position.self, Velocity.self, excludesAll: Frozen.self)
}
func update() {
family.forEach(reading: Velocity.self, writing: Position.self) { velocity, position in
position.x += velocity.dx
position.y += velocity.dy
}
}
}Warning
The for var hazard: Value types returned by get or sequence iteration are copies. Mutating a local var in a loop does not change stored data:
for (var position, velocity) in family { // `position` is a copy
position.x += velocity.dx // lost!
}Always mutate components through forEach(reading:writing:), entity.modify, or entity.set.
For read-only operations, families conform to standard Sequence:
func inspect(_ family: Family<Position, Velocity>) {
for (position, velocity) in family {
_ = (position.x, velocity.dx) // Read-only copies
}
for (entity, position, _) in family.entityAndComponents {
_ = (entity.identifier, position)
}
for entity in family.entities {
entity.modify(Position.self) { $0.x = 0 }
}
}The optional FirebladeECSMacros product provides Swift macros to synthesize boilerplate code automatically.
@Component synthesizes protocol conformances based on stored properties:
| Generated Protocol | Enables |
|---|---|
RegistrableComponent |
Stable componentTypeName for type registry lookup |
CloneableComponent |
entity.clone() and world cloning with remapped entity references |
SerializableComponent |
nexus.encodeSnapshot(...) and nexus.decodeSnapshot(...) |
InspectableComponent |
Runtime property reflection (name, type, key paths) for editors |
DefaultInitializable |
init() if all stored properties have default values |
@Component
struct Transform {
var position: SIMD3<Float> = .zero
var parent: EntityIdentifier?
@ComponentIgnored var cachedMatrix: [Float] = [] // Cloned, but not serialized or inspected
}
@Component(excluding: .serializable) // Runtime-only component
struct RenderHandle: @unchecked Sendable {
var handle: Int = 0
}Declare family requirements as distinct types:
@FamilyTraits(reading: Velocity.self, writing: Position.self, excluding: Frozen.self)
struct Movers {}
Movers(in: nexus).forEach { velocity, position in
position.x += velocity.dx
}An EntityIdentifier combines a slot index with a generation counter. When an entity is destroyed, its identifier goes stale and safely rejects subsequent lookups:
let id = entity.identifier
nexus.destroy(entity: entity)
nexus.isAlive(id) // false
nexus.entity(id) // nilA Single represents an entity with exactly one component that persists throughout the lifetime of the Nexus:
struct GameState: SingleComponent {
var quitGame: Bool = false
}
final class GameLogicSystem {
let gameState: Single<GameState>
init(nexus: Nexus) {
gameState = nexus.single(GameState.self)
}
func update() {
gameState.modify { $0.quitGame = true }
_ = gameState.entity
}
}Clone entire worlds or encode and decode snapshots:
// Clone an entire world (entity references like parent/child are automatically remapped)
let playMode = Nexus()
try nexus.clone(into: playMode)
// Export and import full-world snapshots
var encoder = JSONEncoder()
let data = try nexus.encodeSnapshot(using: &encoder, handling: .skip)
var decoder = JSONDecoder()
try playMode.decodeSnapshot(from: data, using: &decoder)
// Family-level serialization
let family = nexus.family(requiresAll: MyComponent.self, YourComponent.self)
var jsonEncoder = JSONEncoder()
let encodedData = try family.encodeMembers(using: &jsonEncoder)
var jsonDecoder = JSONDecoder()
let newEntities = try family.decodeMembers(from: jsonData, using: &jsonDecoder)The optional FirebladeFSM product provides EntityStateMachine to dynamically swap components on an entity when it transitions between states:
import FirebladeECS
import FirebladeFSM
enum EnemyBehavior {
case patrol
case attack
}
struct Patrol: Component, DefaultInitializable {}
struct Attack: Component, DefaultInitializable {}
let entity = nexus.createEntity()
let fsm = EntityStateMachine<EnemyBehavior>(entity: entity)
// Define states and assign component types
fsm.createState(name: .patrol)
.addType(Patrol.self)
fsm.createState(name: .attack)
.addType(Attack.self)
// Transition state: removes previous state components and adds new state components
fsm.changeState(name: .attack)- Online Documentation: Read the API Documentation.
- Local Preview: Preview documentation locally with:
make docs-preview
- Site Build:
make docs-generatebuilds the site into.build/documentation. It first restores the published versions from thegh-pagesbranch oforigin, so it needs network access tooriginand fails if that restore fails (only a missinggh-pagesbranch is tolerated).
Explore the Fireblade ECS Demo App for practical examples, particle simulations, and asteroid games.
This project is maintained by Christian Treffs and contributors. Please review the Contribution Guide before submitting changes.
- Swift Package Manager (SPM)
- Homebrew (optional)
- Mint (optional)
- SwiftLint (optional)
- SwiftEnv (optional)
- Clone and set up:
git clone git@github.com:fireblade-engine/ecs.git fireblade-ecs cd fireblade-ecs make setup - Verify changes before committing:
make pre-commit
- Run benchmarks:
make benchmark
Licensed under the MIT License. See LICENSE for details.
Inspired by Ashley, Entitas, and EntitasKit.