apecs

A fast ECS for game engine programming

https://github.com/jonascarpay/apecs#readme

Version on this page:0.2.2.0
LTS Haskell 22.18:0.9.6
Stackage Nightly 2024-04-24:0.9.6
Latest on Hackage:0.9.6

See all snapshots apecs appears in

BSD-3-Clause licensed by Jonas Carpay
Maintained by [email protected]
This version can be pinned in stack with:apecs-0.2.2.0@sha256:35faf3702738c5b86169e7664d8cc4ff87e86de33cf00a00ff9ad81f1c6ff733,1468

Module documentation for 0.2.2.0

apecs

hackage | documentation | tutorials

apecs is an Entity Component System inspired by specs and Entitas. It exposes a DSL that translates to fast storage operations, resulting in expressivity without sacrificing performance or safety.

There is an example below, and a tutorial can be found here. For a general introduction to ECS, see this talk or here.

Performance

Performance is good. Running the ecs-bench pos_vel benchmark shows that we can keep up with specs, which was written in Rust:

specs apecs
build 699 us 285 us
update 34 us 46 us

Example

import Apecs
import Apecs.Stores (Cache)
import Linear

-- Component data definitions
newtype Velocity = Velocity (V2 Double) deriving (Eq, Show)
newtype Position = Position (V2 Double) deriving (Eq, Show)
data Enemy = Enemy -- A single constructor for tagging entites as enemies

-- Define Velocity as a component by giving it a storage type
instance Component Velocity where
  -- Store velocities in a cached map
  type Storage Velocity = Cache 100 (Map Velocity)

instance Component Position where
  type Storage Position = Cache 100 (Map Position)

instance Flag Enemy where flag = Enemy
instance Component Enemy where
  -- Because enemy is just a flag, we can use a set
  type Storage Enemy = Set Enemy

-- Define your world as containing the storages of your components
data World = World
  { positions     :: Storage Position
  , velocities    :: Storage Velocity
  , enemies       :: Storage Enemy
  , entityCounter :: Storage EntityCounter }

-- Define Has instances for components to allow type-driven access to their storages
instance World `Has` Position      where getStore = System $ asks positions
instance World `Has` Velocity      where getStore = System $ asks velocities
instance World `Has` Enemy         where getStore = System $ asks enemies
instance World `Has` EntityCounter where getStore = System $ asks entityCounter

type System' a = System World a

game :: System' ()
game = do
  -- Create new entities
  ety <- newEntity (Position 0)
  -- Components can be composed using tuples
  newEntity (Position 0, Velocity 1)
  -- Tagging one as an enemy is a matter of adding the constructor
  newEntity (Position 1, Velocity 1, Enemy)

  -- Side effects
  liftIO$ putStrLn "Stepping velocities"
  -- rmap maps a pure function over all entities in its domain
  rmap $ \(Position p, Velocity v) -> Position (v+p)

  -- Set can be used to (over)write components
  set ety (Position 2, Enemy)

  -- Print the positions of all enemies
  cmapM_ $ \(Enemy, Position p) -> liftIO (print p)

main :: IO ()
main = do w <- World <$> initStore <*> initStore <*> initStore <*> initCounter
          runSystem game w