apecs 0.4.0.1 → 0.4.1.0
raw patch · 6 files changed
+162/−51 lines, 6 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
+ Apecs.Core: EitherStore :: sa -> sb -> EitherStore sa sb
+ Apecs.Core: data EitherStore sa sb
+ Apecs.Core: instance (Apecs.Core.Component ca, Apecs.Core.Component cb) => Apecs.Core.Component (Data.Either.Either ca cb)
+ Apecs.Core: instance (Apecs.Core.ExplGet sa, Apecs.Core.ExplGet sb) => Apecs.Core.ExplGet (Apecs.Core.EitherStore sa sb)
+ Apecs.Core: instance (Apecs.Core.ExplSet sa, Apecs.Core.ExplSet sb) => Apecs.Core.ExplSet (Apecs.Core.EitherStore sa sb)
+ Apecs.Core: instance (Apecs.Core.Has w ca, Apecs.Core.Has w cb) => Apecs.Core.Has w (Data.Either.Either ca cb)
+ Apecs.Core: instance Apecs.Core.Component ()
+ Apecs.Core: instance Apecs.Core.ExplDestroy ()
+ Apecs.Core: instance Apecs.Core.ExplGet ()
+ Apecs.Core: instance Apecs.Core.ExplMembers s => Apecs.Core.ExplMembers (Apecs.Core.FilterStore s)
+ Apecs.Core: instance Apecs.Core.ExplSet ()
+ Apecs.System: cfold :: forall w c a. (Members w c, Get w c) => (a -> c -> a) -> a -> System w a
+ Apecs.System: cfoldM :: forall w c a. (Members w c, Get w c) => (a -> c -> System w a) -> a -> System w a
+ Apecs.System: cfoldM_ :: forall w c a. (Members w c, Get w c) => (a -> c -> System w a) -> a -> System w ()
Files
- README.md +6/−8
- apecs.cabal +7/−4
- src/Apecs/Core.hs +80/−17
- src/Apecs/Stores.hs +32/−15
- src/Apecs/System.hs +34/−5
- src/Apecs/Util.hs +3/−2
README.md view
@@ -1,15 +1,15 @@ # apecs [](https://travis-ci.org/jonascarpay/apecs) [](https://hackage.haskell.org/package/apecs)-[](http://stackage.org/lts-10/package/apecs)+[](https://www.stackage.org/package/apecs) -apecs is an _Entity Component System_ framework inspired by [specs](https://github.com/slide-rs/specs) and [Entitas](https://github.com/sschmid/Entitas-CSharp).-The front-end DSL allows you to concisely express your game logic, with an efficient back-end storage framework that can be extended to meet any performance needs.+apecs is an _Entity Component System_ (ECS) framework inspired by [specs](https://github.com/slide-rs/specs) and [Entitas](https://github.com/sschmid/Entitas-CSharp).+The front-end DSL uses a small set of combinators to concisely express game logic, which then translate to fast primitive operations on back-end stores.+Both the DSL and storage framework can easily be extended to meet any performance/expressivity needs. #### Links - [documentation](https://hackage.haskell.org/package/apecs/docs/Apecs.html)-- [introductory tutorial](https://github.com/jonascarpay/apecs/blob/master/tutorials/RTS.md)-- [performance guide](https://github.com/jonascarpay/apecs/blob/master/tutorials/GoingFast.md)+- [manual](https://github.com/jonascarpay/apecs/blob/master/prepub.pdf) (see [#19](https://github.com/jonascarpay/apecs/issues/19)) - [apecs-physics](https://github.com/jonascarpay/apecs-physics) #### Performance@@ -22,9 +22,7 @@ {-# LANGUAGE DataKinds, ScopedTypeVariables, TypeFamilies, MultiParamTypeClasses, TemplateHaskell #-} import Apecs-import Apecs.Stores-import Apecs.Core-import Linear+import Linear (V2 (..)) newtype Position = Position (V2 Double) deriving Show -- To declare a component, we need to specify how to store it
apecs.cabal view
@@ -1,5 +1,5 @@ name: apecs-version: 0.4.0.1+version: 0.4.1.0 homepage: https://github.com/jonascarpay/apecs#readme license: BSD3 license-file: LICENSE@@ -9,8 +9,9 @@ build-type: Simple cabal-version: >=1.10 extra-source-files: README.md-synopsis: A fast ECS for game engine programming-description: A fast ECS for game engine programming+synopsis: Fast ECS framework for game programming+description:+ Apecs is an Entity Component System framework, suitable for high-performance game programming. source-repository head type: git@@ -78,7 +79,9 @@ ghc-options: -Wall -Odph- -fllvm+ -- LLVM is disabled by default for travis/compatibility reasons+ -- For serious benchmarks, please run with -fllvm+ -- -fllvm -optlo-O3 -threaded -funfolding-use-threshold1000
src/Apecs/Core.hs view
@@ -17,45 +17,56 @@ import qualified Apecs.THTuples as T -- | An Entity is just an integer, used to index into a component store.+-- In general, use @newEntity@, @cmap@, and component tags instead of manipulating these directly.+-- +-- For performance reasons, negative values like (-1) are reserved for stores to represent special values, so avoid using these. newtype Entity = Entity {unEntity :: Int} deriving (Num, Eq, Ord, Show) -- | A System is a newtype around `ReaderT w IO a`, where `w` is the game world variable.+-- Systems mainly serve to+-- +-- * Lift side effects into the IO Monad.+--+-- * Allow type-based lookup of a component's store through @getStore@. newtype System w a = System {unSystem :: ReaderT w IO a} deriving (Functor, Monad, Applicative, MonadIO) --- | A component is defined by the type of its storage--- The storage in turn supplies runtime types for the component.--- For the component to be valid, its Storage must be an instance of Store.+-- | A component is defined by specifying how it is stored.+-- The constraint ensures that stores and components are mapped one-to-one. class (Elem (Storage c) ~ c) => Component c where type Storage c --- | A world `Has` a component if it can produce its Storage+-- | @Has w c@ means that world @w@ can produce a @Storage c@. class Component c => Has w c where getStore :: System w (Storage c) --- | The type of components stored by a Store+-- | The type of components stored by a store, e.g. @Elem (Map c) = c@. type family Elem s --- | Holds components indexed by entities+-- | Indicates that the store @s@ can be initialized.+-- Generally, \"base\" stores like @Map c@ can be initialized, but composite stores like @MaybeStore s@ cannot. class ExplInit s where- -- | Initialize the store with its initialization arguments.+ -- | Initialize a new empty store. explInit :: IO s --- | Stores that support @get@ and @exists@ in the IO monad--- If @existsIO@+-- | Stores that we can read using @explGet@ and @explExists@.+-- For some entity @e@, @eplGet s e@ is only guaranteed to be safe if @explExists s e@ returns @True@. class ExplGet s where- -- | Reads a component from the store. What happens if the component does not exist is left undefined.+ -- | Reads a component from the store. What happens if the component does not exist is left undefined, and might not necessarily crash. explGet :: s -> Int -> IO (Elem s)- -- | Returns whether there is a component for the given index+ -- | Returns whether there is a component for the given index. explExists :: s -> Int -> IO Bool +-- | Stores that can be written. class ExplSet s where- -- | Writes a component+ -- | Writes a component to the store. explSet :: s -> Int -> Elem s -> IO () +-- | Stores that components can be removed from. class ExplDestroy s where -- | Destroys the component for a given index. explDestroy :: s -> Int -> IO () +-- | Stores that we can request a list of member entities for. class ExplMembers s where -- | Returns an unboxed vector of member indices explMembers :: s -> IO (U.Vector Int)@@ -65,6 +76,7 @@ type Members w c = (Has w c, ExplMembers (Storage c)) type Destroy w c = (Has w c, ExplDestroy (Storage c)) +-- | Identity component/store. @Identity c@ is equivalent to @c@, so using it is mostly useless. instance Component c => Component (Identity c) where type Storage (Identity c) = Identity (Storage c) @@ -84,14 +96,14 @@ instance ExplDestroy s => ExplDestroy (Identity s) where explDestroy (Identity s) = explDestroy s --- Tuple Instances TODO T.makeInstances [2..8] - -- | Psuedocomponent indicating the absence of @a@.+-- Mainly used as e.g. @cmap $ \(a, Not b) -> c@ to iterate over entities with an @a@ but no @b@.+-- Can also be used to delete components, like @cmap $ \a -> (Not :: Not a)@ to delete every @a@ component. data Not a = Not --- | Pseudostore used to produce values of type @Not a@+-- | Pseudostore used to produce values of type @Not a@, inverts @explExists@, and destroys instead of @explSet@. newtype NotStore s = NotStore s instance Component c => Component (Not c) where@@ -109,7 +121,9 @@ instance ExplDestroy s => ExplSet (NotStore s) where explSet (NotStore sa) ety _ = explDestroy sa ety --- | Pseudostore used to produce values of type @Maybe a@+-- | Pseudostore used to produce values of type @Maybe a@.+-- Will always return @True@ for @explExists@.+-- Writing can both set and delete a component using @Just@ and @Nothing@ respectively. newtype MaybeStore s = MaybeStore s instance Component c => Component (Maybe c) where type Storage (Maybe c) = MaybeStore (Storage c)@@ -130,7 +144,51 @@ explSet (MaybeStore sa) ety Nothing = explDestroy sa ety explSet (MaybeStore sa) ety (Just x) = explSet sa ety x +-- | Used for 'Either', a logical disjunction between two components.+-- As expected, Either is used to model error values.+-- Getting an @Either a b@ will first attempt to get a @b@ and return it as @Right b@, or if it does not exist, get an @a@ as @Left a@.+-- Can also be used to set one of two things.+data EitherStore sa sb = EitherStore sa sb+instance (Component ca, Component cb) => Component (Either ca cb) where+ type Storage (Either ca cb) = EitherStore (Storage ca) (Storage cb)++instance (Has w ca, Has w cb) => Has w (Either ca cb) where+ getStore = EitherStore <$> getStore <*> getStore++type instance Elem (EitherStore sa sb) = Either (Elem sa) (Elem sb)++instance (ExplGet sa, ExplGet sb) => ExplGet (EitherStore sa sb) where+ explGet (EitherStore sa sb) ety = do+ e <- explExists sb ety+ if e then Right <$> explGet sb ety+ else Left <$> explGet sa ety+ explExists (EitherStore sa sb) ety = do+ e <- explExists sb ety+ if e then return True+ else explExists sa ety++instance (ExplSet sa, ExplSet sb) => ExplSet (EitherStore sa sb) where+ explSet (EitherStore _ sb) ety (Right b) = explSet sb ety b+ explSet (EitherStore sa _) ety (Left a) = explSet sa ety a++instance Component () where+ type Storage () = ()+type instance Elem () = ()+instance ExplGet () where+ explExists _ _ = return True+ explGet _ _ = return ()+instance ExplSet () where+ explSet _ _ _ = return ()+instance ExplDestroy () where+ explDestroy _ _ = return ()++-- | Pseudocomponent that functions normally for @explExists@ and @explMembers@, but always return @Filter@ for @explGet@.+-- Can be used in cmap as @cmap $ \(Filter :: Filter a) -> b@.+-- Since the above can be written more consicely as @cmap $ \(_ :: a) -> b@, it is rarely directly.+-- More interestingly, we can define reusable filters like @movables = Filter :: Filter (Position, Velocity)@. data Filter c = Filter deriving (Eq, Show)++-- Pseudostore for 'Filter'. newtype FilterStore s = FilterStore s instance Component c => Component (Filter c) where@@ -145,7 +203,12 @@ explGet _ _ = return Filter explExists (FilterStore s) ety = explExists s ety --- | Pseudostore used to produce components of type @Entity@+instance ExplMembers s => ExplMembers (FilterStore s) where+ explMembers (FilterStore s) = explMembers s++-- | Pseudostore used to produce components of type 'Entity'.+-- Always returns @True@ for @explExists@, and echoes back the entity argument for @explGet@.+-- Used in e.g. @cmap $ \(a, ety :: Entity) -> b@ to access the current entity. data EntityStore = EntityStore instance Component Entity where type Storage Entity = EntityStore
src/Apecs/Stores.hs view
@@ -1,4 +1,5 @@ {-# LANGUAGE DataKinds #-}+{-# LANGUAGE LambdaCase #-} {-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE KindSignatures #-}@@ -54,35 +55,37 @@ -- | A Unique contains zero or one component. -- Writing to it overwrites both the previous component and its owner. -- Its main purpose is to be a @Map@ optimized for when only ever one component inhabits it.-data Unique c = Unique (IORef Int) (IORef c)+data Unique c = Unique (IORef (Maybe (Int, c))) type instance Elem (Unique c) = c instance ExplInit (Unique c) where- explInit = Unique <$> newIORef (-1) <*> newIORef (error "Uninitialized Unique value")+ explInit = Unique <$> newIORef Nothing instance ExplGet (Unique c) where {-# INLINE explGet #-}- explGet (Unique _ cref) _ = readIORef cref+ explGet (Unique ref) _ = readIORef ref >>= return . \case+ Nothing -> error "Reading empty Unique"+ Just (_, c) -> c {-# INLINE explExists #-}- explExists (Unique eref _) ety = (==ety) <$> readIORef eref+ explExists (Unique ref) ety = maybe False ((==ety) . fst) <$> readIORef ref instance ExplSet (Unique c) where {-# INLINE explSet #-}- explSet (Unique eref cref) ety x = writeIORef eref ety >> writeIORef cref x+ explSet (Unique ref) ety c = writeIORef ref (Just (ety, c)) instance ExplDestroy (Unique c) where {-# INLINE explDestroy #-}- explDestroy (Unique eref _) ety = do e <- readIORef eref; when (e==ety) (writeIORef eref (-1))+ explDestroy (Unique ref) ety = writeIORef ref Nothing instance ExplMembers (Unique c) where {-# INLINE explMembers #-}- explMembers (Unique eref _) = f <$> readIORef eref- where f (-1) = mempty- f x = U.singleton x+ explMembers (Unique ref) = readIORef ref >>= return . \case+ Nothing -> mempty+ Just (ety, _) -> U.singleton ety -- | A Global contains exactly one component.--- Initialized with 'mempty'--- The store will return true for every existence check, but only ever gives (-1) as its inhabitant.--- The entity argument is ignored when setting/getting a global.+-- The initial value is 'mempty' from the component's 'Monoid' instance.+-- When operating on a global, any entity arguments are ignored.+-- For example, we can get a global component with @get 0@ or @get 1@ or even @get undefined@. newtype Global c = Global (IORef c) type instance Elem (Global c) = c instance Monoid c => ExplInit (Global c) where@@ -96,21 +99,32 @@ explSet (Global ref) _ c = writeIORef ref c -- | An empty type class indicating that the store behaves like a regular map, and can therefore safely be cached.+-- An example of a store that cannot be cached is 'Unique'. class Cachable s instance Cachable (Map s) instance (KnownNat n, Cachable s) => Cachable (Cache n s) -- | A cache around another store.+-- Caches store their members in a fixed-size vector, so operations run in O(1).+-- Caches can provide huge performance boosts, especially for large numbers of components.+-- The cache size is given as a type-level argument.+-- -- Note that iterating over a cache is linear in cache size, so sparsely populated caches might actually decrease performance.+-- In general, the exact size of the cache does not matter as long as it reasonably approximates the number of components present.+--+-- The cache uses entity (-1) to internally represent missing entities, so be wary when manually manipulating entities. data Cache (n :: Nat) s = Cache Int (UM.IOVector Int) (VM.IOVector (Elem s)) s++cacheMiss = error "Cache miss!"+ type instance Elem (Cache n s) = Elem s instance (ExplInit s, KnownNat n, Cachable s) => ExplInit (Cache n s) where explInit = do let n = fromIntegral$ natVal (Proxy @n) tags <- UM.replicate n (-1)- cache <- VM.new n+ cache <- VM.replicate n cacheMiss child <- explInit return (Cache n tags cache child) @@ -137,10 +151,13 @@ VM.unsafeWrite cache index x instance ExplDestroy s => ExplDestroy (Cache n s) where- explDestroy (Cache n tags _ s) ety = do+ explDestroy (Cache n tags cache s) ety = do+ let index = ety `rem` n tag <- UM.unsafeRead tags (ety `rem` n) if tag == ety- then UM.unsafeWrite tags (ety `rem` n) (-1)+ then do+ UM.unsafeWrite tags index (-1)+ VM.unsafeWrite cache index cacheMiss else explDestroy s ety instance ExplMembers s => ExplMembers (Cache n s) where
src/Apecs/System.hs view
@@ -45,7 +45,7 @@ s :: Storage c <- getStore liftIO$ explExists s ety --- | Maps a function over all entities with a @cx@, and writes their @cy@+-- | Maps a function over all entities with a @cx@, and writes their @cy@. {-# INLINE cmap #-} cmap :: forall w cx cy. (Get w cx, Members w cx, Set w cy) => (cx -> cy) -> System w ()@@ -58,7 +58,7 @@ r <- explGet sx e explSet sy e (f r) --- | Monadically iterates over all entites with a cx, and writes their cy+-- | Monadically iterates over all entites with a @cx@, and writes their @cy@. {-# INLINE cmapM #-} cmapM :: forall w cx cy. (Get w cx, Set w cy, Members w cx) => (cx -> System w cy) -> System w ()@@ -71,7 +71,7 @@ y <- sys x liftIO$ explSet sy e y --- | Monadically iterates over all entites with a cx+-- | Monadically iterates over all entites with a @cx@ {-# INLINE cmapM_ #-} cmapM_ :: forall w c a. (Get w c, Members w c) => (c -> System w a) -> System w ()@@ -82,8 +82,38 @@ x <- liftIO$ explGet s ety sys x +-- | Fold over the game world; for example, @cfold max (minBound :: Foo)@ will find the maximum value of @Foo@.+-- Strict in the accumulator.+{-# INLINE cfold #-}+cfold :: forall w c a. (Members w c, Get w c)+ => (a -> c -> a) -> a -> System w a+cfold f a0 = do+ s :: Storage c <- getStore+ sl <- liftIO$ explMembers s+ liftIO$ U.foldM' (\a e -> f a <$> explGet s e) a0 sl++-- | Monadically fold over the game world.+-- Strict in the accumulator.+{-# INLINE cfoldM #-}+cfoldM :: forall w c a. (Members w c, Get w c)+ => (a -> c -> System w a) -> a -> System w a+cfoldM sys a0 = do+ s :: Storage c <- getStore+ sl <- liftIO$ explMembers s+ U.foldM' (\a e -> liftIO (explGet s e) >>= sys a) a0 sl++-- | Monadically fold over the game world.+-- Strict in the accumulator.+{-# INLINE cfoldM_ #-}+cfoldM_ :: forall w c a. (Members w c, Get w c)+ => (a -> c -> System w a) -> a -> System w ()+cfoldM_ sys a0 = do+ s :: Storage c <- getStore+ sl <- liftIO$ explMembers s+ U.foldM'_ (\a e -> liftIO (explGet s e) >>= sys a) a0 sl+ -- | Get all components @c@.--- Call as @[(c,Entity)]@ to read the entity/index.+-- Call as @[(c,Entity)]@ to also read the entity index. {-# INLINE getAll #-} getAll :: forall w c. (Get w c, Members w c) => System w [c]@@ -91,7 +121,6 @@ s :: Storage c <- getStore sl <- liftIO$ explMembers s forM (U.toList sl) $ liftIO . explGet s- -- | Destroys component @c@ for the given entity. -- Note that @c@ is a phantom argument, used only to convey the type of the entity to be destroyed.
src/Apecs/Util.hs view
@@ -34,9 +34,10 @@ import Apecs.System import Apecs.Core --- | Convenience entity (-1), used in places where the exact entity value does not matter, i.e. a global store.+-- | Convenience entity, for use in places where the entity value does not matter, i.e. a global store.+-- Its value is -2, to avoid potential conflicts with caches, which reserve -1. global :: Entity-global = Entity (-1)+global = Entity (-2) -- | Component used by newEntity to track the number of issued entities. -- Automatically added to any world created with @makeWorld@