apecs 0.2.4.3 → 0.2.4.4
raw patch · 11 files changed
+79/−77 lines, 11 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Apecs: readGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => System w c
- Apecs: writeGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => c -> System w ()
- Apecs.System: readGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => System w c
- Apecs.System: writeGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => c -> System w ()
- Apecs.Types: instance Apecs.Types.Cast (Apecs.Types.Entity a) (Apecs.Types.Entity b)
- Apecs.Types: instance Apecs.Types.Cast (Apecs.Types.Slice a) (Apecs.Types.Slice b)
+ Apecs: getGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => System w c
+ Apecs: makeWorld :: String -> [Name] -> Q [Dec]
+ Apecs: setGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => c -> System w ()
+ Apecs.System: getGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => System w c
+ Apecs.System: setGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => c -> System w ()
+ Apecs.Types: instance Apecs.Types.Cast Apecs.Types.Entity
+ Apecs.Types: instance Apecs.Types.Cast Apecs.Types.Slice
+ Apecs.Util: proxy :: Entity c
- Apecs: cast :: Cast a b => a -> b
+ Apecs: cast :: forall a. Cast m => m a -> forall b. m b
- Apecs.Types: cast :: Cast a b => a -> b
+ Apecs.Types: cast :: forall a. Cast m => m a -> forall b. m b
- Apecs.Types: class Cast a b
+ Apecs.Types: class Cast m
Files
- README.md +0/−1
- apecs.cabal +1/−1
- bench/Main.hs +0/−1
- src/Apecs.hs +5/−4
- src/Apecs/Stores.hs +1/−1
- src/Apecs/System.hs +9/−9
- src/Apecs/TH.hs +1/−1
- src/Apecs/Types.hs +5/−4
- src/Apecs/Util.hs +7/−2
- test/Main.hs +1/−2
- tutorials/RTS.md +49/−51
README.md view
@@ -26,7 +26,6 @@ ### Example ```haskell import Apecs-import Apecs.TH (makeWorld) import Apecs.Stores (Cache) import Apecs.Concurrent (prmap) import Linear
apecs.cabal view
@@ -1,5 +1,5 @@ name: apecs-version: 0.2.4.3+version: 0.2.4.4 homepage: https://github.com/jonascarpay/apecs#readme license: BSD3 license-file: LICENSE
bench/Main.hs view
@@ -9,7 +9,6 @@ import Linear import Apecs-import Apecs.TH import Apecs.Stores import Apecs.Concurrent
src/Apecs.hs view
@@ -18,14 +18,14 @@ -- ** GlobalRW wrapper functions- readGlobal, writeGlobal, modifyGlobal,+ getGlobal, setGlobal, modifyGlobal, -- * Other runSystem, runWith, runGC, EntityCounter, newEntity,-- -- Reader- asks, ask, liftIO, lift,+ makeWorld,+ -- Reader+ asks, ask, liftIO, lift, ) where import Control.Monad.Reader (asks, ask, liftIO, lift)@@ -34,4 +34,5 @@ import Apecs.System import Apecs.Stores import Apecs.Util+import Apecs.TH
src/Apecs/Stores.hs view
@@ -209,7 +209,7 @@ -- | A cache around another store. -- The wrapped store must produce safe representations using Maybe.--- Note that iterating over a cache is linear in its size, so large, sparsely populated caches will actually decrease performance.+-- Note that iterating over a cache is linear in its size, so large, sparsely populated caches might actually decrease performance. data Cache (n :: Nat) s = Cache Int (UM.IOVector Int) (VM.IOVector (Stores s)) s
src/Apecs/System.hs view
@@ -174,19 +174,19 @@ explSetMaybe sw e (getSafe . f . Safe $ r) -- | Reads a global value-{-# INLINE readGlobal #-}-readGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => System w c-readGlobal = do s :: Storage c <- getStore- liftIO$ explGet s 0+{-# INLINE getGlobal #-}+getGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => System w c+getGlobal = do s :: Storage c <- getStore+ liftIO$ explGet s 0 -- | Writes a global value-{-# INLINE writeGlobal #-}-writeGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => c -> System w ()-writeGlobal c = do s :: Storage c <- getStore- liftIO$ explSet s 0 c+{-# INLINE setGlobal #-}+setGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => c -> System w ()+setGlobal c = do s :: Storage c <- getStore+ liftIO$ explSet s 0 c -- | Modifies a global value {-# INLINE modifyGlobal #-} modifyGlobal :: forall w c. (Has w c, GlobalStore (Storage c)) => (c -> c) -> System w ()-modifyGlobal f = readGlobal >>= writeGlobal . f+modifyGlobal f = getGlobal >>= setGlobal . f
src/Apecs/TH.hs view
@@ -54,7 +54,7 @@ > instance WorldName `Has` EntityCounter where ... > > initWorldName :: IO WorldName-> initWorldName = WorldName <$> initStore <*> initStore <*> ... <*> initCounter+> initWorldName = WorldName <$> initStore <*> initStore <*> ... <*> initStore |-} makeWorld :: String -> [Name] -> Q [Dec]
src/Apecs/Types.hs view
@@ -1,5 +1,6 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE MultiParamTypeClasses #-} {-# LANGUAGE FlexibleContexts, FlexibleInstances #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}@@ -130,12 +131,12 @@ class (SafeRW s ~ Stores s, Store s) => GlobalStore s where -- | Casts for entities and slices-class Cast a b where- cast :: a -> b-instance Cast (Entity a) (Entity b) where+class Cast m where cast :: forall a. m a -> forall b. m b++instance Cast Entity where {-# INLINE cast #-} cast (Entity ety) = Entity ety-instance Cast (Slice a) (Slice b) where+instance Cast Slice where {-# INLINE cast #-} cast (Slice vec) = Slice vec
src/Apecs/Util.hs view
@@ -5,6 +5,7 @@ module Apecs.Util ( -- * Utility initStore, runGC,+ proxy, -- * EntityCounter EntityCounter, nextEntity, newEntity,@@ -31,6 +32,10 @@ import Apecs.Stores import Apecs.System +-- | A proxy entity for TODO+proxy :: Entity c+proxy = Entity (-1)+ -- | Secretly just an int in a newtype newtype EntityCounter = EntityCounter {getCounter :: Sum Int} deriving (Monoid, Num, Eq, Show) @@ -40,8 +45,8 @@ -- | Bumps the EntityCounter and yields its value {-# INLINE nextEntity #-} nextEntity :: Has w EntityCounter => System w (Entity ())-nextEntity = do n <- readGlobal- writeGlobal (n+1)+nextEntity = do n <- getGlobal+ setGlobal (n+1) return (Entity . getSum . getCounter $ n) -- | Writes the given components to a new entity, and yields that entity
test/Main.hs view
@@ -18,7 +18,6 @@ import Apecs import Apecs.Types-import Apecs.TH import Apecs.Stores import Apecs.Logs import Apecs.Util@@ -150,7 +149,7 @@ prop_global = assertSys initGProp $ do modifyGlobal $ \(G x) -> G (not x)- G x <- readGlobal+ G x <- getGlobal return $ x == True return []
tutorials/RTS.md view
@@ -70,14 +70,18 @@ type Storage MouseState = Global MouseState ``` -We'll probably look into the `Storage` type in more detail in a future tutorial.-Using the right storage type is important when optimizing performance, but for now these will do just fine.+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.-The only extra thing to look out for is the `EntityCounter`.-Adding an `EntityCounter` means we can use `newEntity` to add entities to our game world, which is nice.+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@@ -87,19 +91,7 @@ , entityCounter :: Storage EntityCounter } ```-`World` simply holds the storages of each component.-Or, to be more precise, it holds immutable references to mutable storage containers for each of your components.-When actually executing the game, we produce a world in the IO monad:-```haskell-initWorld = do- positions <- initStore -- initStore = initStoreWith (), used to initialize most stores- targets <- initStore- selected <- initStore- mouseState <- initStoreWith Rest -- A global needs to be initialized with a value- counter <- initCounter- return $ World positions targets selected counter-```-One last thing is to make sure we can access each of these at the type level by defining instances for `Has`:+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@@ -107,24 +99,33 @@ instance World `Has` MouseState where getStore = System $ asks mouseState instance World `Has` EntityCounter where getStore = System $ asks entityCounter ```-The boilerplate ends here, you will never need to touch your `World` or the `Has` class again.-In the future, this might be automated using Template Haskell, but it's still good to at least know what's being generated.+When actually executing the game, we produce a world in the IO monad like this:+```haskell+initWorld = do+ positions <- initStore -- initStore = initStoreWith (), used to initialize most stores+ targets <- initStore+ selected <- initStore+ mouseState <- initStore -- A global needs to be initialized with a value+ 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` is a `ReaderT w IO`, but it doesn't really matter.-All that matters is the System allows for access to the World's underlying component stores.-Just add this alias for convenience' sake:+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 ```-and remember that IO looks like:++Here's a system to get you started: ```haskell helloWorld :: System' () helloWorld = liftIO $ putStrLn "Hello World!" ```--Here's a system to get you started:+`liftIO` is also used to make render calls. Here's another system: ```haskell newGuy :: System' () newGuy = newEntity (Position (V2 0 0))@@ -135,7 +136,7 @@ newGuy2 :: System' () newGuy2 = newEntity (Player, Position (V2 0 0), Velocity (V2 0 0)) ```-That's right; components can be tupled up and used as if they were a single component.+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@@ -152,24 +153,30 @@ ```haskell cmap $ \(Position p) -> Position (p+1) ```-`cmap` takes a pure function and maps it over all components in the domain of the function.+`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 `c -> Safe c`.+`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. -There's also `rmap`, of type `(r -> w) -> System world ()`.+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`. -Finally, there's these mapping functions, whose effect you can see from the type signature:+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.+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. @@ -187,11 +194,12 @@ ``` 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 <- readGlobal+ m :: MouseState <- getGlobal case m of Rest -> return () Dragging (V2 ax ay) (V2 bx by) -> do@@ -201,7 +209,7 @@ rmap' f ``` We start by reading the `MouseState` global.-The result of `readGlobal` is determined by the type it is instantiated with.+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`.@@ -210,25 +218,25 @@ `f` looks at every `Position`, and returns `Safe True` if the position was inside the selection box. ### Events-Handling events is unpacking SDL Event types and matching them to a piece of game logic:+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 writeGlobal (Dragging p' p')+ let p' = fromIntegral <$> p in setGlobal (Dragging p' p') handleEvent (SDL.MouseButtonEvent (SDL.MouseButtonEventData _ SDL.Released _ SDL.ButtonLeft _ _)) =- writeGlobal Rest+ setGlobal Rest ``` This is how we update the selection box when the mouse moves: ```haskell handleEvent (SDL.MouseMotionEvent (SDL.MouseMotionEventData _ _ _ (P p) _)) = do- md <- readGlobal+ md <- getGlobal case md of Rest -> return ()- Dragging a _ -> writeGlobal (Dragging a (fromIntegral <$> p))+ Dragging a _ -> setGlobal (Dragging a (fromIntegral <$> p)) ``` And finally, what to do when the right mouse button is pressed.@@ -240,7 +248,7 @@ ```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+ let r = (*3) . subtract 1 . sqrt . fromIntegral . S.size $ sl S.forM_ sl $ \e -> do dx <- liftIO$ randomRIO (-r,r)@@ -252,7 +260,6 @@ `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.-There's a few more interesting functions here. `S.forM_` monadically iteraters over a `Slice`. `set entity component` then explicitly writes a component for an entity, overwriting whatever might have been there. @@ -261,7 +268,7 @@ It looks like this: ```haskell cimapM_ $ \(e, Position p) -> do- e <- exists (cast e :: Entity Selected)+ 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)) ```@@ -278,13 +285,4 @@ 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).--The reason for writing this tutorial at this point is that apecs is now sufficiently developed where it has most of the functionality of other ECS, and is now a viable way of developing games in Haskell.-The library is still under development, but for now, that is mostly on parts outside the scope of this tutorial.-I hope to have a version on hackage soon!--There will be at least one more tutorial, on how to make things fast.-We'll be taking a look at- - How to cache your components for O(1) reads and writes- - How to use add Logs to your component storages- - How to use those Logs to get a free spatial hash of our positions+If you have any questions or suggestions, feel free to open an issue or PR.