Skip to content

Simre1/hero

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

72 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Hero

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 with Storable datatypes. Each entity has its own component value. Faster than BoxedSparseSet.
  • 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