Hero
August 31, 2022 · View on GitHub
Hero is an entity component system in Haskell and is inspired by apecs. Hero aims to be more performant than apecs by using sparse sets as the main data structure for storing component data. In the future, it also intends to parallelize systems automatically as well.
Entities
Entities are objects which have components. You can create entities with the createEntity system and add components to them.
Components
Components are Haskell datastructure and mostly contain raw data.
data Position = Position Float Float
In order to use Position as a component, you need to make it an instance of Component.
instance Component Position where
type Store Position = BoxedSparseSet
-- StorableSparseSet is faster but Position would need to implement Storable
Stores
Each component has a store which holds the component data. Depending on the use of the component, different stores might be best. The most important stores are:
BoxedSparseSet: Can be used with any datatype. Each entity has its own component value.StorableSparseSet: Can be used withStorabledatatypes. Each entity has its own component value. Faster thanBoxedSparseSet.Global: Can be used with any datatype. Each entity has the same component value. Can also be accessed without an entity.
Systems
Systems are functions which operate on the components of a world. For example, you can map the Position component of every entity:
cmap_ $ \(Position x y) -> Position (x + 1) y
Systems have the type system :: System input output. Systems can be chained together through their Applicative, Category and Arrow instance. However, Systems dot have an instance for Monad! If one is familiar with arrowized FRP, then they will feel that Systems have a similar interface as signal functions.
Systems do not have a Monad instance since they are separated into a compilation and a run phase. Before you can run a System, you need to compile it with compileSystem :: System m input output -> World -> IO (input -> IO output). In the compilation phase, Systems look up the used components so that run time is faster.
Example
{-# LANGUAGE TypeFamilies #-}
import Control.Monad ( forM_ )
import Hero
import Data.Foldable (for_)
data Position = Position Int Int
data Velocity = Velocity Int Int
instance Component Position where
type Store Position = BoxedSparseSet
instance Component Velocity where
type Store Velocity = BoxedSparseSet
main :: IO ()
main = do
world <- createWorld 10000
runSystem <- compileSystem system world
runSystem ()
system :: System () ()
system =
-- Create two entities
(pure (Position 0 0, Velocity 1 0) >>> createEntity) *>
(pure (Position 10 0, Velocity 0 1) >>> createEntity) *>
-- Map position 10 times
for_ [1..10] (\_ -> cmap_ (\(Position x y, Velocity vx vy) -> Position (x + vx) (y + vy))) *>
-- Print the current position
cmapM_ (\(Position x y) -> print (x,y))
Installation
To download the project and execute it, you need at least GHC 9. I have tested it with GHC 9.2.1.
Then, you can run the following commands to build the project.
git clone https://github.com/Simre1/hero
cd hero
cabal build all
To run the example, do:
cabal run example
To run the tests, do:
cabal test
To run the benchmarks, do:
cabal run hero-bench
cabal run apecs-bench
To generate documentation, do:
cabal haddock
cabal haddock hero-sdl2
Hero SDL2
The folder hero-sdl2 contains a small libary to use SDL2 with Hero. It needs the C libraries SDL2, SDL2_gdx and SDL2_image.
The Hero SDL2 examples can be run with:
cabal run rotating-shapes
cabal run image-rendering
cabal run move-shape
More information can be found in hero-sdl2.
Cabal dependency
To use Hero as a dependency, add hero to the build-depends section. Additional, create a cabal.project with:
packages: *.cabal
source-repository-package
type: git
location: https://github.com/Simre1/hero
If you also want to use hero-sdl2, add hero-sdl2 to the build-depends section as well. The cabal.project is then:
packages: *.cabal
source-repository-package
type: git
location: https://github.com/Simre1/hero
source-repository-package
type: git
location: https://github.com/Simre1/hero
subdir: hero-sdl2
Benchmark
A basic benchmark seems to suggest that Hero is much faster. However, it relies heavily on
GHC inlining. It is best to use concrete types when working with systems or add an INLINE pragma to
functions which deal with polymorphic systems.
The following queries are used to test the iteration speed of both libraries:
cmap_ (\(Velocity vx vy, Acceleration ax ay) -> Velocity (vx + ax) (vy + ay)) *>
cmap_ (\(Position x y, Velocity vx vy) -> Position (x + vx) (y + vy))
Hero
simple physics (3 components): OK (0.20s)
394 μs ± 25 μs
Apecs
simple physics (3 components): OK (0.13s)
17.5 ms ± 1.5 ms