packages feed

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 view
@@ -1,15 +1,15 @@ # apecs [![Build Status](https://travis-ci.org/jonascarpay/apecs.svg?branch=master)](https://travis-ci.org/jonascarpay/apecs) [![Hackage](https://img.shields.io/hackage/v/apecs.svg)](https://hackage.haskell.org/package/apecs)-[![apecs on Stackage LTS 11](http://stackage.org/package/apecs/badge/lts-11)](http://stackage.org/lts-10/package/apecs)+[![Stackage](https://www.stackage.org/package/apecs/badge/lts?label=Stackage)](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@