jonascarpay / apecs

a fast, extensible, type driven Haskell ECS framework for games
392 stars 43 forks source link

Symbols as components? #42

Closed cpennington closed 5 years ago

cpennington commented 5 years ago

I'm working on an application where I want to be able to tag entities with a number of boolean properties. Thus far, I've been adding code like:

data Active = Active
    deriving (Show, Generic, Flat, Eq)
instance Component Active where
    type Storage Active = Map Active

which then lets me do set e Active and to pattern match on Active in cmap calls (for instance). However, it would be nice to not need to implement an instance for each new boolean flag that I add.

One possibility that I had thought of was to use DataKinds and have a type-level string for each of the boolean properties that I want to tag entities with. However, I can't figure out if there's any reasonable way to satisfy Has w m c without writing a new instance for Proxy "mysymbol" each time.

Are there any other obvious ways to accomplish what I'm trying to accomplish?

jonascarpay commented 5 years ago

Maybe something like this?

import           Apecs
import qualified Data.Set as S

data Flags
  = Active
  | SomeOtherFlag
  | SomeThirdFlag
  deriving (Eq, Show, Ord)

newtype FlagSet = FlagSet (S.Set Flags)

instance Component FlagSet where type Storage FlagSet = Map FlagSet

makeWorld "World" [ ''FlagSet ]

cmapFlags :: (Get World IO cx, Set World IO cy) => [Flags] -> (cx -> cy) -> System World ()
cmapFlags flags f = cmap $ \(FlagSet set,x) ->
  if all (`S.member` set) flags
     then Right (f x)
     else Left ()

setFlag :: Entity -> Flags -> System World ()
setFlag ety flag = do
  mset <- get ety
  set ety . FlagSet $ case mset of
    Just (FlagSet set) -> S.insert flag set
    Nothing -> S.singleton flag

unsetFlag :: Entity -> Flags -> System World ()
unsetFlag ety flag = do
  mset <- get ety
  set ety $ case mset of
    Nothing -> Nothing
    Just (FlagSet set) ->
      let set' = S.delete flag set
       in if null set' then Nothing else Just (FlagSet set')

cmapFlags is then cmap that only operates on entities with the required flags. You can also consider representing the flag set as a bit vector.

cpennington commented 5 years ago

Yeah, that makes sense...

Maybe I've had naive assumptions about the specifics of apecs performance. Would that behave significantly differently from having each flag as a separate component, when it comes to iterating over the entities that have that component?

For instance, if there are 1000 entities, 1 of which has FlagA and FlagB, and 100 have FlagB, does cmap \(FlagA) -. ... iterate over roughly 1 item? Or does it iterate over all 1000, and only call the function on the 1?

jonascarpay commented 5 years ago

cmap $ \FlagA -> ... will iterate over everything that has a FlagA cmap $ \(FlagA,FlagB) -> ... will iterate over everything that has a FlagA, and then filter out everything that does not have a FlagB, thereby operating on everything that has both. So if FlagA is unique, it will iterate over just the one.

But to answer your question, yes, this cmapFlags would definitely be slower, since it now iterates over ever entity that has a FlagSet, which would be most/all of them. On the other hand, I know that many modern game engines do essentially this using a bitmask/bit vector, and they're still plenty fast. Maybe it's worth making a specialized store for that at some point.

A third option might actually be to make the Template Haskell functions a bit more flexible. makeWorldAndComponents already exists, we could expose the part that generates the Component instances:

data FlagA = FlagA deriving ...
data FlagB  = FlagB deriving ...
flags = [''FlagA, ''FlagB]
makeDefaultInstances flags
makeWorld "World" (flags ++ [ ''OtherComponent1, ... ]

Maybe it even makes sense to generate the data declarations themselves?

jonascarpay commented 5 years ago

This is now possible in 4dc1bec5ea939c82419a5e7b1018add5ce9839b8