apecs 0.3.0.0 → 0.3.0.1
raw patch · 7 files changed
+24/−311 lines, 7 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Apecs.Util: initStore :: Store s => IO s
+ Apecs: data Cache (n :: Nat) s
Files
- README.md +0/−1
- apecs.cabal +2/−2
- src/Apecs.hs +6/−7
- src/Apecs/Core.hs +2/−2
- src/Apecs/Stores.hs +6/−3
- src/Apecs/Util.hs +8/−5
- tutorials/RTS.md +0/−291
README.md view
@@ -1,7 +1,6 @@ # apecs [](https://travis-ci.org/jonascarpay/apecs) [](https://hackage.haskell.org/package/apecs)-[](http://stackage.org/lts-9/package/apecs) [](http://stackage.org/lts-10/package/apecs) apecs is an _Entity Component System_ inspired by [specs](https://github.com/slide-rs/specs) and [Entitas](https://github.com/sschmid/Entitas-CSharp).
apecs.cabal view
@@ -1,5 +1,5 @@ name: apecs-version: 0.3.0.0+version: 0.3.0.1 homepage: https://github.com/jonascarpay/apecs#readme license: BSD3 license-file: LICENSE@@ -8,7 +8,7 @@ category: Game, Control, Data build-type: Simple cabal-version: >=1.10-extra-source-files: README.md, tutorials/RTS.md+extra-source-files: README.md synopsis: A fast ECS for game engine programming description: A fast ECS for game engine programming
src/Apecs.hs view
@@ -3,15 +3,14 @@ It selectively re-exports the user-facing functions from the submodules. -} module Apecs (- -- * Types- System(..),- Component(..), Entity(..), Has(..),- Not(..),-- Map, Unique, Global,+ -- * Core types+ System(..), Component(..), Entity(..), Has(..), Not(..), - -- * Store wrapper functions+ -- * Stores+ Map, Unique, Global, Cache, initStore,++ -- * Systems get, set, cmap, cmapM, cmapM_, modify, destroy, exists,
src/Apecs/Core.hs view
@@ -15,7 +15,7 @@ import qualified Apecs.THTuples as T --- | An Entity is really just an Int in a newtype.+-- | An Entity is really just an Int in a newtype, used to index into a component store. newtype Entity = Entity Int deriving (Eq, Ord, Show) -- | A system is a newtype around `ReaderT w IO a`, where `w` is the game world variable.@@ -42,7 +42,7 @@ -- | The type of components stored by this Store type Elem s - -- Initialize the store with its initialization arguments.+ -- | Initialize the store with its initialization arguments. initStore :: IO s -- | Writes a component
src/Apecs/Stores.hs view
@@ -26,8 +26,7 @@ import Apecs.Core --- | A map from Data.Intmap.Strict. O(log(n)) for most operations.--- Yields safe runtime representations of type @Maybe c@.+-- | A map based on @Data.Intmap.Strict@. O(log(n)) for most operations. newtype Map c = Map (IORef (M.IntMap c)) instance Store (Map c) where type Elem (Map c) = c@@ -43,8 +42,9 @@ {-# INLINE explMembers #-} {-# INLINE explExists #-} --- | A Unique contains at most one component.+-- | 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) instance Store (Unique c) where type Elem (Unique c) = c@@ -64,6 +64,8 @@ -- | 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. newtype Global c = Global (IORef c) instance Monoid c => Store (Global c) where type Elem (Global c) = c@@ -84,6 +86,7 @@ data Cache (n :: Nat) s = Cache Int (UM.IOVector Int) (VM.IOVector (Elem s)) s +-- | An empty type class indicating that the store behaves like a regular map, and can therefore safely be cached. class Store s => Cachable s instance Cachable (Map s) instance (KnownNat n, Cachable s) => Cachable (Cache n s)
src/Apecs/Util.hs view
@@ -8,8 +8,7 @@ module Apecs.Util ( -- * Utility- initStore, runGC,- global, proxy,+ runGC, global, proxy, -- * EntityCounter EntityCounter, nextEntity, newEntity,@@ -33,13 +32,16 @@ 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. global :: Entity global = Entity (-1) +-- | Convenience proxy value proxy :: forall t. t-proxy = error "proxy entity"+proxy = error "Proxy value" --- | Secretly just an int in a newtype+-- | Component used by newEntity to track the number of issued entities.+-- Automatically added to any world created with @makeWorld@ newtype EntityCounter = EntityCounter {getCounter :: Sum Int} deriving (Monoid, Eq, Show) instance Component EntityCounter where@@ -52,7 +54,8 @@ set global (EntityCounter $ n+1) return (Entity . getSum $ n) --- | Writes the given components to a new entity, and yields that entity+-- | Writes the given components to a new entity, and yields that entity.+-- The return value is often ignored. {-# INLINE newEntity #-} newEntity :: (Store (Storage c), Has w c, Has w EntityCounter) => c -> System w Entity
− tutorials/RTS.md
@@ -1,291 +0,0 @@-## apecs tutorial--##### Warning!-With the release of apecs 0.3, this tutorial does not (fully) apply anymore.-The main difference is that mapping operations have been consolidated in `cmap`.-The rts executable has been removed and there is a new example game, `shmup`, in the examples project.-I will either update or delete this tutorial soon.--### An RTS-like game--In this tutorial we'll take a look at how to write a simple RTS-like game using apecs.-We'll be using [SDL2](https://github.com/haskell-game/sdl2) for graphics.-Don't worry if you don't know SDL2, neither do I.-We'll only be drawing single pixels to the screen, so it should be pretty easy to follow what's going on.-The final result can be found [here](https://github.com/jonascarpay/apecs/blob/master/examples/RTS.hs).-You can run it with `stack build && stack exec rts`.-I will be skipping some details, so make sure to keep the source code handy if you want to follow along.--#### Entity Component Systems-Entity Component Systems are frameworks for game engines.-The concept is as follows:--Your game world consists of entities.-An entity is essentially an ID and a collection of components.-Components are pieces of data like position, velocity, health, or 3D model.--The game logic is defined in systems that operate on the game world.-The typical example of a system is one that looks at all entities with both a position and a velocity, and adds their velocity to their position.--As in most ECS, components are stored together in memory, indexed by entity.-This makes entities mostly implicit;-an entity can be said to exist as long as there is at least one component associating itself with that entity's ID.--#### Components-In our game, we want to be able to select units and order them around.-We start by defining our components.--First up is position.-A `Position` is just a two-dimensional vector of `Double`s.-When defining a data type as a component, you have to specify how the component is stored in memory.-In this case, we can simply store the position in a `Map`.-```haskell-newtype Position = Position {getPos :: V2 Double} deriving (Show, Num)--instance Component Position where- type Storage Position = Map Position-```--A `Target` is whatever position the entity is moving towards.-Again, the storage is a simple `Map`-```haskell-newtype Target = Target (V2 Double)--instance Component Target where- type Storage Target = Map Target-```--We use `Selected` to tag an entity as being currently selected by the mouse.-We can designate `Selected` as being a flag by defining a Flag instance, which in turn gives us access to the `Set` storage.-```haskell-data Selected = Selected--instance Flag Selected where flag = Selected-instance Component Selected where- type Storage Selected = Set Selected-```--Finally, we need to store some global information about the mouse.-`Dragging` indicates that we're currently performing a box-selection.-```haskell-data MouseState = Rest | Dragging (V2 Double) (V2 Double)-instance Component MouseState where- type Storage MouseState = Global MouseState-```--Different `Storage` types have different performance characteristics, but in general, these will do just fine.-In fact, in this example SDL will become a bottleneck before game logic will.-For more information, check out [this performance guide](https://github.com/jonascarpay/apecs/blob/master/tutorials/GoingFast.md) and the [Stores module documentation](https://hackage.haskell.org/package/apecs-0.2.4.3/docs/Apecs-Stores.html).--#### The game world-Defining your game world is straightforward.-This is generally automated with `makeWorld`, but it's useful to know what's being generated.--`World` holds the stores for each component.-Or, to be more precise, it holds immutable references to mutable storage containers for each of your components.--Adding an `EntityCounter` component allows us to use `newEntity` to add entities to our game world, which is nice.-```haskell-data World = World- { positions :: Storage Position- , targets :: Storage Target- , selected :: Storage Selected- , mouseState :: Storage MouseState- , entityCounter :: Storage EntityCounter- }-```-We then make sure we can access each of these at the type level by defining instances for `Has`, using `asks` from `ReaderT`:-```haskell-instance World `Has` Position where getStore = System $ asks positions-instance World `Has` Target where getStore = System $ asks targets-instance World `Has` Selected where getStore = System $ asks selected-instance World `Has` MouseState where getStore = System $ asks mouseState-instance World `Has` EntityCounter where getStore = System $ asks entityCounter-```-When actually executing the game, we produce a world in the IO monad like this:-```haskell-initWorld = do- positions <- initStore- targets <- initStore- selected <- initStore- mouseState <- initStore- counter <- initStore- return $ World positions targets selected counter-```---#### Systems-Most of your code takes place in the `System` monad.-If you want to know, a `System w a` is a newtype for `ReaderT w IO a`, but it doesn't really matter if you don't know what that means.-All that matters is that a `System world` allows for access to the `world`'s underlying component stores.-After defining the world, I like to add this alias for convenience' sake:-```haskell-type System' a = System World a-```--Here's a system to get you started:-```haskell-helloWorld :: System' ()-helloWorld = liftIO $ putStrLn "Hello World!"-```-`liftIO` is also used to make render calls. Here's another system:-```haskell-newGuy :: System' ()-newGuy = newEntity (Position (V2 0 0))-```-It makes a new guy with a position of (0,0).-Here's another:-```haskell-newGuy2 :: System' ()-newGuy2 = newEntity (Player, Position (V2 0 0), Velocity (V2 0 0))-```-As you can see, components can be tupled up and used as if they were a single component.--And now for something more practical:-```haskell-addUnits :: System' ()-addUnits = replicateM_ 100 $ do- x <- liftIO$ randomRIO (0,hres)- y <- liftIO$ randomRIO (0,vres)- newEntity (Position (V2 x y))-```-It adds a hundred units scattered over the field.--Say you wanted to add 1 to all positions.-That would look like this:-```haskell-cmap $ \(Position p) -> Position (p+1)-```-`cmap :: (c -> c) -> System world ()` takes a pure function and maps it over all components in the domain of the function.--`cmap'` is analogous, but takes a function of type `c -> Safe c`.-A `Safe` value comes up when performing a read that might fail, or a write that might delete.-At runtime, it looks like e.g. `Safe (Just (Position p), Nothing) :: Safe (Position, Target)` when reading an entity that has a position but no target.-In the case of `cmap'`, it means that the function might delete the component it's mapped over.--Note that while the lefthand side of `::` has `Just` and `Nothing`, there is no `Maybe` on the righthand side.-This is because the `Safe` representation is determined by the `Store`'s `SafeRW` type.-For a `Map c`, that's `Maybe c`, but a `Set c`, for instance, has `Bool`.-Don't worry, if you mess up, GHC will happily and verbosely let you know where and how.--Continuing with the mapping functions, we also have `rmap`, of type `(r -> w) -> System world ()`.-It still iterates over the components in the domain, but instead of mapping to those same components, it writes the result to a different component (creating one if none exists).-This can be used to write something like `rmap $ \(Position p, Velocity v) -> Position (p+v)` to step positions, or `rmap $ \ Player -> Selected` to add the `Selected` tag to the player.-Note that `rmap` is a more general version of `cmap`, and you are free to use it wherever you could have used `cmap`.--These are the rest of the mapping functions, whose effect you can infer from their type signature:-```haskell-rmap' :: (r -> Safe w) -> System world ()-wmap :: (Safe r -> w) -> System world ()-wmap' :: (Safe r -> Safe w) -> System world ()-```-Note that `wmap` has a `Safe` _argument_ in its function.-`wmap` iterates over the entities/components in the codomain of its function.-Those entities are not guaranteed to have an `r` component, so we need `Safe` here.--Let's write the first part of our game loop.-We will use `cmap'` to delete a target once we are sufficiently close:-```haskell-step = do- let speed = 5- stepPosition :: (Target, Position) -> Safe (Target, Position)- stepPosition (Target t, Position p)- | V.vlength (p-t) < speed = Safe (Nothing, Just (Position t))- | otherwise = Safe (Just (Target t), Just (Position (p + speed * normalize (t-p))))-- cmap' stepPosition-```-There's a lot there.-First try to understand what `stepPosition`'s type signature means, then what the body means, and then what it means to `cmap'` that function.-It performs a step of size `speed` in the direction of `Target`, until it reaches its target at which point the `Target` component is deleted. -Once an entity loses its `Target` component, it will no longer be affected by the function above, because it's no longer in the domain of `stepPosition`.--This is the second part of the game loop:-```haskell- m :: MouseState <- getGlobal- case m of- Rest -> return ()- Dragging (V2 ax ay) (V2 bx by) -> do- resetStore (Proxy :: Proxy Selected)- let f :: Position -> Safe Selected- f (Position (V2 x y)) = Safe (x >= min ax bx && x <= max ax bx && y >= min ay by && y <= max ay by)- rmap' f-```-We start by reading the `MouseState` global.-The result of `getGlobal` is determined by the type it is instantiated with.-`resetStore` is semantically equivalent to `cmap' $ \(_ :: Selected) -> Safe False`, i.e. it just deletes every component of some type, but more general and usually faster.-Because `Selected` is a `Set`, its `Safe` representation is a `Bool` rather than `Maybe c`.-For components in a `Map`, the equivalent of `resetStore` is `cmap' $ \(_ :: c) -> Nothing`.-After resetting the store, we determine what units are selected.-We can do this using `rmap'`.-`f` looks at every `Position`, and returns `Safe True` if the position was inside the selection box.--### Events-Handling events is unpacking SDL `Event`s and matching them to a piece of game logic:--Here we start tracking the mouse when the left button is pressed, and stop when it is released.-```haskell-handleEvent :: SDL.EventPayload -> System' ()-handleEvent (SDL.MouseButtonEvent (SDL.MouseButtonEventData _ SDL.Pressed _ SDL.ButtonLeft _ (P p))) =- let p' = fromIntegral <$> p in setGlobal (Dragging p' p')--handleEvent (SDL.MouseButtonEvent (SDL.MouseButtonEventData _ SDL.Released _ SDL.ButtonLeft _ _)) =- setGlobal Rest-```--This is how we update the selection box when the mouse moves:-```haskell-handleEvent (SDL.MouseMotionEvent (SDL.MouseMotionEventData _ _ _ (P p) _)) = do- md <- getGlobal- case md of- Rest -> return ()- Dragging a _ -> setGlobal (Dragging a (fromIntegral <$> p))-```--And finally, what to do when the right mouse button is pressed.-As per genre convention, the selected units are to start moving to wherever we clicked with the right mouse button.-Now, this is an interesting piece of game logic.-How do you direct a group of units?-You can't just send them all to the same location, or they'd end up overlapping.-For simplicity's sake, I chose to arrange them randomly in a square, with area proportional to the number of selected units.-```haskell-handleEvent (SDL.MouseButtonEvent (SDL.MouseButtonEventData _ SDL.Pressed _ SDL.ButtonRight _ (P (V2 px py)))) = do- sl :: Slice Selected <- owners- let r = (*3) . subtract 1 . sqrt . fromIntegral . S.size $ sl-- S.forM_ sl $ \e -> do- dx <- liftIO$ randomRIO (-r,r)- dy <- liftIO$ randomRIO (-r,r)- set e (Target (V2 (fromIntegral px+dx) (fromIntegral py+dy)))--handleEvent _ = return ()-```-`owners` returns a `Slice` of all members that have that particular component.-A `Slice` is a list of entities.-The reason we need a slice instead of a map is that we need to know the amount of selected units.-`S.forM_` monadically iteraters over a `Slice`.-`set entity component` then explicitly writes a component for an entity, overwriting whatever might have been there.--#### Rendering-Rendering turns out to be really easy.-It looks like this:-```haskell-cimapM_ $ \(e, Position p) -> do- e <- exists (cast e @Selected)- liftIO$ SDL.rendererDrawColor renderer $= if e then V4 255 255 255 255 else V4 255 0 0 255- SDL.drawPoint renderer (P (round <$> p))-```-`cmapM_` is to `cmap` as `mapM_` is to `map`.-Here we see `cimapM_`, note the extra `i`, which gives both the read component, and the current entity.-We then check whether or not it has a `Selected` component.-`exists :: Entity c -> System w ()` checks to see if the entity has a certain component.-We could emulate this with `get`, but this is, like `resetStore`, more general and usually faster.-Because the entities we iterate over are only guaranteed to have a `Position`, their type is `Entity Position`.-To check whether or not they are `Selected`, we need to explicitly cast them.-If you were to call `exists` with an `Entity (Position, Velocity)`, it'd tell you whether or not that entity has both a `Position` and `Velocity`.--#### Conclusion-These are the tools you need to build a game in apecs.-I did not discuss every line in the final program, as they were mostly SDL-related.-Again, the final version in its full glory can be found [here](https://github.com/jonascarpay/apecs/blob/master/examples/RTS.hs).-If you have any questions or suggestions, feel free to open an issue or PR.